diff --git a/.github/workflows/test-master.yml b/.github/workflows/test-master.yml index 7dc97488..e5bef351 100644 --- a/.github/workflows/test-master.yml +++ b/.github/workflows/test-master.yml @@ -5,37 +5,44 @@ on: branches: - "master" +permissions: + contents: read + jobs: build-tests: runs-on: ubuntu-latest strategy: matrix: - java: [ "21" ] + java: [ "25" ] type: [ "Examples", "Guides" ] lang: [ "Java", "Kotlin" ] feature: [ "Database", "Client", "Server", "Graalvm", "Kafka", "Other" ] - name: Pull Request Java ${{ matrix.java }} for ${{ matrix.type }}-${{ matrix.lang }}-${{ matrix.feature }} + exclude: + - type: "Examples" + lang: "Kotlin" + feature: "Graalvm" + - type: "Guides" + feature: "Graalvm" + name: Master Java ${{ matrix.java }} for ${{ matrix.type }}-${{ matrix.lang }}-${{ matrix.feature }} env: GRADLE_TASK_SUFFIX: ${{ matrix.type }}${{ matrix.lang }}${{ matrix.feature }} + MAX_PARALLEL_WORKERS: 1 + GRADLE_ACTIONS_SKIP_BUILD_RESULT_CAPTURE: true steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: JDK setup uses: actions/setup-java@v4 with: java-version: ${{ matrix.java }} - distribution: "adopt" + distribution: "temurin" - name: Gradle setup uses: gradle/actions/setup-gradle@v4 with: - cache-read-only: ${{ github.ref != 'refs/heads/master' }} - gradle-home-cache-cleanup: ${{ github.ref == 'refs/heads/master' }} - - - name: Gradle build classes - run: "./gradlew classes${{ env.GRADLE_TASK_SUFFIX }} --no-daemon --no-parallel --scan --stacktrace" - - - name: Gradle build test classes - run: "./gradlew testClasses${{ env.GRADLE_TASK_SUFFIX }} --no-daemon --no-parallel --scan --stacktrace" + cache-read-only: false + gradle-home-cache-cleanup: true - name: Gradle run tests - run: "./gradlew test${{ env.GRADLE_TASK_SUFFIX }} --no-daemon --no-parallel --scan --stacktrace" + run: "./gradlew test${{ env.GRADLE_TASK_SUFFIX }} --no-daemon --no-parallel --console=plain --warning-mode=none --no-problems-report" diff --git a/.github/workflows/test-pr.yml b/.github/workflows/test-pr.yml index 8190c43c..a907a8f3 100644 --- a/.github/workflows/test-pr.yml +++ b/.github/workflows/test-pr.yml @@ -2,14 +2,13 @@ name: Test PR on: pull_request: - branches: - - "master" +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true permissions: contents: read - checks: write - pull-requests: write jobs: @@ -17,32 +16,35 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ "21" ] + java: [ "25" ] type: [ "Examples", "Guides" ] lang: [ "Java", "Kotlin" ] feature: [ "Database", "Client", "Server", "Graalvm", "Kafka", "Other" ] + exclude: + - type: "Examples" + lang: "Kotlin" + feature: "Graalvm" + - type: "Guides" + feature: "Graalvm" name: Pull Request Java ${{ matrix.java }} for ${{ matrix.type }}-${{ matrix.lang }}-${{ matrix.feature }} env: GRADLE_TASK_SUFFIX: ${{ matrix.type }}${{ matrix.lang }}${{ matrix.feature }} + MAX_PARALLEL_WORKERS: 1 + GRADLE_ACTIONS_SKIP_BUILD_RESULT_CAPTURE: true steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: JDK setup uses: actions/setup-java@v4 with: java-version: ${{ matrix.java }} - distribution: "adopt" + distribution: "temurin" - name: Gradle setup uses: gradle/actions/setup-gradle@v4 with: - cache-read-only: ${{ github.ref != 'refs/heads/master' }} - gradle-home-cache-cleanup: ${{ github.ref == 'refs/heads/master' }} - - - name: Gradle build classes - run: "./gradlew classes${{ env.GRADLE_TASK_SUFFIX }} --no-daemon --no-parallel --scan --stacktrace" - - - name: Gradle build test classes - run: "./gradlew testClasses${{ env.GRADLE_TASK_SUFFIX }} --no-daemon --no-parallel --scan --stacktrace" + cache-read-only: true - name: Gradle run tests - run: "./gradlew test${{ env.GRADLE_TASK_SUFFIX }} --no-daemon --no-parallel --scan --stacktrace" + run: "./gradlew test${{ env.GRADLE_TASK_SUFFIX }} --no-daemon --no-parallel --console=plain --warning-mode=none --no-problems-report" diff --git a/.gitignore b/.gitignore index 206d5a24..88b6e47e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,10 @@ target/ .idea .settings/ *.iml -out/ \ No newline at end of file +out/ + +### Local reference checkouts used during the Kora 2.0 migration +agents-md/ + +### Local working artifacts (never commit) +docs/superpowers/ diff --git a/build.gradle b/build.gradle index 1889f273..721d68f7 100644 --- a/build.gradle +++ b/build.gradle @@ -3,13 +3,20 @@ import org.gradle.api.services.BuildServiceParameters plugins { id "com.diffplug.spotless" version "6.19.0" - id "com.asarkar.gradle.build-time-tracker" version "4.3.0" - id "org.graalvm.buildtools.native" version "0.11.5" apply false // or dependsOn fails in graalvm examples + id "com.asarkar.gradle.build-time-tracker" version "4.3.0" apply false + id "org.graalvm.buildtools.native" version "1.1.7" apply false // or dependsOn fails in graalvm examples id "org.jetbrains.kotlin.jvm" version "2.4.10" apply false - id "org.jetbrains.kotlin.kapt" version "2.4.10" apply false id "com.google.devtools.ksp" version "2.3.11" apply false } +def isCiBuild = providers.environmentVariable("CI").map { it.toBoolean() }.orElse(false).get() +if (isCiBuild) { + URLConnection.setDefaultUseCaches("jar", false) +} +if (!isCiBuild) { + apply plugin: "com.asarkar.gradle.build-time-tracker" +} + group = groupId version = koraVersion @@ -53,7 +60,9 @@ subprojects { } apply plugin: "java" - apply plugin: "com.asarkar.gradle.build-time-tracker" + if (!isCiBuild) { + apply plugin: "com.asarkar.gradle.build-time-tracker" + } if (!project.name.contains("kotlin")) { apply plugin: "com.diffplug.spotless" @@ -93,4 +102,21 @@ subprojects { tasks.register("allDeps", DependencyReportTask) } +if (isCiBuild) { + subprojects { + afterEvaluate { + tasks.withType(Test).configureEach { + testLogging { + showStandardStreams = false + events = [org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED] + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + showExceptions = true + showCauses = true + showStackTraces = true + } + } + } + } +} + apply from: "${project.rootDir}/grouping.gradle" diff --git a/examples/graalvm/kora-java-graalvm-crud-cassandra/Dockerfile b/examples/graalvm/kora-java-graalvm-crud-cassandra/Dockerfile index 26bb3f57..501a0108 100644 --- a/examples/graalvm/kora-java-graalvm-crud-cassandra/Dockerfile +++ b/examples/graalvm/kora-java-graalvm-crud-cassandra/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/graalvm/native-image-community:21 as builder +FROM ghcr.io/graalvm/native-image-community:25 as builder ARG TARGET_DIR=/opt/app ARG SOURCE_DIR=build/libs diff --git a/examples/graalvm/kora-java-graalvm-crud-cassandra/README.md b/examples/graalvm/kora-java-graalvm-crud-cassandra/README.md index 457b2de9..fe6f33bb 100644 --- a/examples/graalvm/kora-java-graalvm-crud-cassandra/README.md +++ b/examples/graalvm/kora-java-graalvm-crud-cassandra/README.md @@ -15,7 +15,7 @@ - [Validation](https://kora-projects.github.io/kora-docs/ru/documentation/validation/) - [Cache Redis](https://kora-projects.github.io/kora-docs/ru/documentation/cache/#redis) -Скомпилирован с помощью [GraalVM](https://www.graalvm.org/release-notes/JDK_21/) +Скомпилирован с помощью [GraalVM](https://www.graalvm.org/release-notes/JDK_25/) ## Build diff --git a/examples/graalvm/kora-java-graalvm-crud-cassandra/build.gradle b/examples/graalvm/kora-java-graalvm-crud-cassandra/build.gradle index c07daaff..ececce38 100644 --- a/examples/graalvm/kora-java-graalvm-crud-cassandra/build.gradle +++ b/examples/graalvm/kora-java-graalvm-crud-cassandra/build.gradle @@ -34,7 +34,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "org.mapstruct:mapstruct-processor:1.5.5.Final" annotationProcessor "io.koraframework:annotation-processors" annotationProcessor "io.goodforgod:graalvm-hint-processor:1.2.0" @@ -51,7 +51,6 @@ dependencies { implementation "io.koraframework:openapi-management" implementation "io.koraframework:logging-logback" - implementation "io.projectreactor:reactor-core:3.6.18" // For reactive examples (optional) implementation "org.mapstruct:mapstruct:1.5.5.Final" testImplementation "org.json:json:20231013" @@ -60,8 +59,8 @@ dependencies { testImplementation "org.mockito:mockito-core:5.18.0" testImplementation "io.koraframework:test-junit5" - testImplementation "io.goodforgod:testcontainers-extensions-scylla:0.13.1" - testImplementation "io.goodforgod:testcontainers-extensions-redis:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-scylla:0.15.0" + testImplementation "io.goodforgod:testcontainers-extensions-redis:0.15.0" testImplementation "org.testcontainers:junit-jupiter:1.21.4" } @@ -89,8 +88,8 @@ compileJava.dependsOn openApiGenerateHttpServer graalvmNative { binaries { main { - imageName = "$project.name" - mainClass = "$application.mainClass" + imageName = project.name + mainClass = application.mainClass javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) vendor = JvmVendorSpec.matching("GraalVM Community") @@ -119,7 +118,7 @@ run { ]) } -jar.enabled = false +// nativeCompile takes the project's own artifact as its classpath entry, so the plain jar must stay enabled shadowJar { mergeServiceFiles() manifest { @@ -138,10 +137,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true @@ -181,4 +176,4 @@ javadoc { if (JavaVersion.current().isJava9Compatible()) { options.addBooleanOption("html5", true) } -} \ No newline at end of file +} diff --git a/examples/graalvm/kora-java-graalvm-crud-cassandra/docker-compose.yml b/examples/graalvm/kora-java-graalvm-crud-cassandra/docker-compose.yml index 836ac87c..2919a697 100644 --- a/examples/graalvm/kora-java-graalvm-crud-cassandra/docker-compose.yml +++ b/examples/graalvm/kora-java-graalvm-crud-cassandra/docker-compose.yml @@ -2,8 +2,40 @@ services: scylla: image: scylladb/scylla:2025.3 restart: unless-stopped + command: --smp 1 --developer-mode 1 --authenticator PasswordAuthenticator ports: - - '5432:5432' + - '9042:9042' + healthcheck: + test: [ "CMD-SHELL", "cqlsh -u cassandra -p cassandra -e 'DESCRIBE KEYSPACES' || exit 1" ] + interval: 10s + timeout: 10s + retries: 30 + start_period: 20s + + + redis: + image: redis:8.0-alpine + restart: unless-stopped + command: [ "redis-server", "--requirepass", "redis" ] + ports: + - '6379:6379' + + + scylla-init: + image: scylladb/scylla:2025.3 + restart: "no" + depends_on: + scylla: + condition: service_healthy + volumes: + - ./src/main/resources/migrations:/migrations:ro + entrypoint: [ "sh", "-c" ] + command: + - | + cqlsh scylla -u cassandra -p cassandra -e "CREATE KEYSPACE IF NOT EXISTS petshop WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}" && + cqlsh scylla -u cassandra -p cassandra -k petshop -f /migrations/setup.cql + + # Можно закомментировать секцию application и использовать docker-compose # как окружения для запуска сервиса через gradle run application: @@ -14,14 +46,18 @@ services: - '8080:8080' - '8085:8085' environment: - CASSANDRA_CONTACT_POINTS: jdbc:postgresql://postgres:5432/postgres + CASSANDRA_CONTACT_POINTS: scylla:9042 CASSANDRA_USER: cassandra CASSANDRA_PASS: cassandra - CASSANDRA_DC: cassandra - CASSANDRA_KEYSPACE: cassandra - REDIS_URL: redis - REDIS_USER: redis + CASSANDRA_DC: datacenter1 + CASSANDRA_KEYSPACE: petshop + REDIS_URL: redis://redis:6379/0 + REDIS_USER: default REDIS_PASS: redis depends_on: - - scylla - - redis + scylla: + condition: service_healthy + scylla-init: + condition: service_completed_successfully + redis: + condition: service_started diff --git a/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/controller/HttpExceptionHandler.java b/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/controller/HttpExceptionHandler.java index bb4fcbb4..05b416f1 100644 --- a/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/controller/HttpExceptionHandler.java +++ b/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/controller/HttpExceptionHandler.java @@ -1,19 +1,22 @@ package io.koraframework.example.graalvm.crud.cassandra.controller; -import io.micrometer.core.instrument.config.validate.ValidationException; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.TimeoutException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import io.koraframework.common.annotation.Component; -import io.koraframework.common.Context; import io.koraframework.common.annotation.Tag; import io.koraframework.example.graalvm.crud.openapi.server.model.MessageTO; import io.koraframework.http.common.body.HttpBody; -import io.koraframework.http.server.common.*; +import io.koraframework.http.server.common.HttpServer; +import io.koraframework.http.server.common.interceptor.HttpServerInterceptor; +import io.koraframework.http.server.common.request.HttpServerRequest; +import io.koraframework.http.server.common.response.HttpServerResponse; +import io.koraframework.http.server.common.response.HttpServerResponseException; import io.koraframework.json.common.JsonWriter; +import io.micrometer.core.instrument.config.validate.ValidationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeoutException; -@Tag(HttpServerModule.class) +@Tag(HttpServer.class) @Component public final class HttpExceptionHandler implements HttpServerInterceptor { @@ -26,14 +29,15 @@ public HttpExceptionHandler(JsonWriter errorJsonWriter) { } @Override - public CompletionStage intercept(Context context, HttpServerRequest request, InterceptChain chain) - throws Exception { - return chain.process(context, request).exceptionally(e -> { + public HttpServerResponse intercept(HttpServerRequest request, InterceptChain chain) { + try { + return chain.process(request); + } catch (Exception e) { if (e instanceof HttpServerResponseException ex) { return ex; } - var body = HttpBody.json(errorJsonWriter.toByteArrayUnchecked(new MessageTO(e.getMessage()))); + var body = HttpBody.json(errorJsonWriter.toByteArray(new MessageTO(e.getMessage()))); if (e instanceof IllegalArgumentException || e instanceof ValidationException) { return HttpServerResponse.of(400, body); } else if (e instanceof TimeoutException) { @@ -42,6 +46,6 @@ public CompletionStage intercept(Context context, HttpServer logger.error("Request '{} {}' failed", request.method(), request.path(), e); return HttpServerResponse.of(500, body); } - }); + } } } diff --git a/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/controller/PetDelegate.java b/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/controller/PetDelegate.java index e0ed57c8..755ca7de 100644 --- a/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/controller/PetDelegate.java +++ b/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/controller/PetDelegate.java @@ -2,7 +2,6 @@ import static io.koraframework.example.graalvm.crud.openapi.server.api.PetApiResponses.*; -import reactor.core.publisher.Mono; import io.koraframework.common.annotation.Component; import io.koraframework.example.graalvm.crud.cassandra.model.mapper.PetMapper; import io.koraframework.example.graalvm.crud.cassandra.service.PetService; @@ -23,57 +22,54 @@ public PetDelegate(PetMapper petMapper, PetService petService) { } @Override - public Mono getPetById(long petId) { + public GetPetByIdApiResponse getPetById(long petId) { if (petId < 0) { - return Mono.just(new GetPetByIdApiResponse.GetPetById400ApiResponse(malformedId(petId))); + return new GetPetByIdApiResponse.GetPetById400ApiResponse(malformedId(petId)); } - return petService.findByID(petId) - .map(pet -> { - var body = petMapper.asDTO(pet); - return ((GetPetByIdApiResponse) new GetPetByIdApiResponse.GetPetById200ApiResponse(body)); - }) - .switchIfEmpty(Mono.fromSupplier(() -> new GetPetByIdApiResponse.GetPetById404ApiResponse(notFound(petId)))); + var pet = petService.findByID(petId); + if (pet == null) { + return new GetPetByIdApiResponse.GetPetById404ApiResponse(notFound(petId)); + } + + var body = petMapper.asDTO(pet); + return new GetPetByIdApiResponse.GetPetById200ApiResponse(body); } @Override - public Mono addPet(PetCreateTO petCreateTO) { - return petService.add(petCreateTO) - .map(pet -> { - var body = petMapper.asDTO(pet); - return new AddPetApiResponse.AddPet200ApiResponse(body); - }); + public AddPetApiResponse addPet(PetCreateTO petCreateTO) { + var pet = petService.add(petCreateTO); + var body = petMapper.asDTO(pet); + return new AddPetApiResponse.AddPet200ApiResponse(body); } @Override - public Mono updatePet(long petId, PetUpdateTO petUpdateTO) { + public UpdatePetApiResponse updatePet(long petId, PetUpdateTO petUpdateTO) { if (petId < 0) { - return Mono.just(new UpdatePetApiResponse.UpdatePet400ApiResponse(malformedId(petId))); + return new UpdatePetApiResponse.UpdatePet400ApiResponse(malformedId(petId)); } - return petService.update(petId, petUpdateTO) - .map(updated -> { - var body = petMapper.asDTO(updated); - return ((UpdatePetApiResponse) new UpdatePetApiResponse.UpdatePet200ApiResponse(body)); - }) - .switchIfEmpty(Mono.fromSupplier(() -> new UpdatePetApiResponse.UpdatePet404ApiResponse(notFound(petId)))); + var updated = petService.update(petId, petUpdateTO); + if (updated == null) { + return new UpdatePetApiResponse.UpdatePet404ApiResponse(notFound(petId)); + } + + var body = petMapper.asDTO(updated); + return new UpdatePetApiResponse.UpdatePet200ApiResponse(body); } @Override - public Mono deletePet(long petId) { + public DeletePetApiResponse deletePet(long petId) { if (petId < 0) { - return Mono.just(new DeletePetApiResponse.DeletePet400ApiResponse(malformedId(petId))); + return new DeletePetApiResponse.DeletePet400ApiResponse(malformedId(petId)); } - return petService.delete(petId) - .map(isDeleted -> { - if (isDeleted) { - return new DeletePetApiResponse.DeletePet200ApiResponse( - new MessageTO("Successfully deleted pet with ID: " + petId)); - } else { - return new DeletePetApiResponse.DeletePet404ApiResponse(notFound(petId)); - } - }); + if (petService.delete(petId)) { + return new DeletePetApiResponse.DeletePet200ApiResponse( + new MessageTO("Successfully deleted pet with ID: " + petId)); + } else { + return new DeletePetApiResponse.DeletePet404ApiResponse(notFound(petId)); + } } private static MessageTO notFound(long petId) { diff --git a/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/repository/PetRepository.java b/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/repository/PetRepository.java index 9b8f077f..a8ac0576 100644 --- a/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/repository/PetRepository.java +++ b/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/repository/PetRepository.java @@ -1,6 +1,6 @@ package io.koraframework.example.graalvm.crud.cassandra.repository; -import reactor.core.publisher.Mono; +import org.jspecify.annotations.Nullable; import io.koraframework.database.cassandra.CassandraRepository; import io.koraframework.database.common.annotation.Query; import io.koraframework.database.common.annotation.Repository; @@ -14,14 +14,15 @@ public interface PetRepository extends CassandraRepository { FROM %{return#table} WHERE id = :id """) - Mono findById(long id); + @Nullable + Pet findById(long id); @Query("INSERT INTO %{entity#inserts}") - Mono insert(Pet entity); + void insert(Pet entity); @Query("UPDATE %{entity#table} SET %{entity#updates} WHERE %{entity#where = @id}") - Mono update(Pet entity); + void update(Pet entity); @Query("DELETE FROM pets WHERE id = :id") - Mono deleteById(long id); + void deleteById(long id); } diff --git a/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/repository/mapper/PetStatusParameterMapper.java b/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/repository/mapper/PetStatusParameterMapper.java index 34f15930..f229cee9 100644 --- a/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/repository/mapper/PetStatusParameterMapper.java +++ b/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/repository/mapper/PetStatusParameterMapper.java @@ -10,7 +10,7 @@ public final class PetStatusParameterMapper implements CassandraParameterColumnMapper { @Override - public void apply(SettableByName stmt, int index, @Nullable Pet.Status value) { + public void apply(SettableByName stmt, int index, Pet.@Nullable Status value) { if (value == null) { stmt.setToNull(index); } else { diff --git a/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/service/PetCircuitBreaker.java b/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/service/PetCircuitBreaker.java new file mode 100644 index 00000000..4046affc --- /dev/null +++ b/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/service/PetCircuitBreaker.java @@ -0,0 +1,9 @@ +package io.koraframework.example.graalvm.crud.cassandra.service; + +import io.koraframework.resilient.circuitbreaker.CircuitBreaker; +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakerSpec; + +@CircuitBreakerSpec("resilient.circuitbreaker.pet") +public interface PetCircuitBreaker extends CircuitBreaker { + +} diff --git a/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/service/PetRetry.java b/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/service/PetRetry.java new file mode 100644 index 00000000..36e38927 --- /dev/null +++ b/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/service/PetRetry.java @@ -0,0 +1,9 @@ +package io.koraframework.example.graalvm.crud.cassandra.service; + +import io.koraframework.resilient.retry.Retry; +import io.koraframework.resilient.retry.annotation.RetrySpec; + +@RetrySpec("resilient.retry.pet") +public interface PetRetry extends Retry { + +} diff --git a/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/service/PetService.java b/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/service/PetService.java index 9ede1b14..961de789 100644 --- a/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/service/PetService.java +++ b/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/service/PetService.java @@ -1,7 +1,7 @@ package io.koraframework.example.graalvm.crud.cassandra.service; import java.util.concurrent.ThreadLocalRandom; -import reactor.core.publisher.Mono; +import org.jspecify.annotations.Nullable; import io.koraframework.cache.annotation.CacheInvalidate; import io.koraframework.cache.annotation.CachePut; import io.koraframework.cache.annotation.Cacheable; @@ -10,8 +10,8 @@ import io.koraframework.example.graalvm.crud.cassandra.repository.PetRepository; import io.koraframework.example.graalvm.crud.openapi.server.model.PetCreateTO; import io.koraframework.example.graalvm.crud.openapi.server.model.PetUpdateTO; -import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreaker; -import io.koraframework.resilient.retry.annotation.Retry; +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakable; +import io.koraframework.resilient.retry.annotation.Retryable; import io.koraframework.resilient.timeout.annotation.Timeout; @Component @@ -24,45 +24,53 @@ public PetService(PetRepository petRepository) { } @Cacheable(PetCache.class) - @CircuitBreaker("pet") - @Retry("pet") - @Timeout("pet") - public Mono findByID(long petId) { + @CircuitBreakable(PetCircuitBreaker.class) + @Retryable(PetRetry.class) + @Timeout(PetTimeouter.class) + @Nullable + public Pet findByID(long petId) { return petRepository.findById(petId); } - @CircuitBreaker("pet") - @Timeout("pet") - public Mono add(PetCreateTO createTO) { + @CircuitBreakable(PetCircuitBreaker.class) + @Timeout(PetTimeouter.class) + public Pet add(PetCreateTO createTO) { final long petId = ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE); final Pet pet = new Pet(petId, createTO.name(), Pet.Status.AVAILABLE, createTO.category().name()); - return petRepository.insert(pet).then(Mono.just(pet)); + petRepository.insert(pet); + return pet; } - @CircuitBreaker("pet") - @Timeout("pet") + @CircuitBreakable(PetCircuitBreaker.class) + @Timeout(PetTimeouter.class) @CachePut(value = PetCache.class, args = "id") - public Mono update(long id, PetUpdateTO updateTO) { - return petRepository.findById(id) - .flatMap(pet -> { - var status = (updateTO.status() == null) - ? pet.status() - : toStatus(updateTO.status()); + @Nullable + public Pet update(long id, PetUpdateTO updateTO) { + final Pet pet = petRepository.findById(id); + if (pet == null) { + return null; + } - var category = (updateTO.category() == null) - ? pet.category() - : updateTO.category().name(); + var status = (updateTO.status() == null) + ? pet.status() + : toStatus(updateTO.status()); - var petUpdate = new Pet(pet.id(), updateTO.name(), status, category); - return petRepository.update(petUpdate).then(Mono.just(petUpdate)); - }); + var category = (updateTO.category() == null) + ? pet.category() + : updateTO.category().name(); + + var petUpdate = new Pet(pet.id(), updateTO.name(), status, category); + petRepository.update(petUpdate); + return petUpdate; } - @CircuitBreaker("pet") - @Timeout("pet") + @CircuitBreakable(PetCircuitBreaker.class) + @Timeout(PetTimeouter.class) @CacheInvalidate(PetCache.class) - public Mono delete(long petId) { - return petRepository.deleteById(petId).thenReturn(true); + public boolean delete(long petId) { + // Cassandra DELETE reports no affected row count, so a completed statement is the only success signal + petRepository.deleteById(petId); + return true; } private static Pet.Status toStatus(PetUpdateTO.StatusEnum statusEnum) { diff --git a/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/service/PetTimeouter.java b/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/service/PetTimeouter.java new file mode 100644 index 00000000..a5d8a655 --- /dev/null +++ b/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/java/io/koraframework/example/graalvm/crud/cassandra/service/PetTimeouter.java @@ -0,0 +1,9 @@ +package io.koraframework.example.graalvm.crud.cassandra.service; + +import io.koraframework.resilient.timeout.Timeouter; +import io.koraframework.resilient.timeout.annotation.TimeoutSpec; + +@TimeoutSpec("resilient.timeout.pet") +public interface PetTimeouter extends Timeouter { + +} diff --git a/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/resources/application.conf b/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/resources/application.conf index 51bb6bf0..85645305 100644 --- a/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/resources/application.conf +++ b/examples/graalvm/kora-java-graalvm-crud-cassandra/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } @@ -39,11 +39,11 @@ lettuce { openapi { management { enabled = true - file = "openapi/http-server.yaml" + files = "openapi/http-server.yaml" swaggerui { enabled = true } - rapidoc { + scalar { enabled = true } } @@ -52,7 +52,8 @@ openapi { resilient { circuitbreaker.pet { - slidingWindowSize = 50 + type = FIXED_WINDOW + countBased.windowSize = 50 minimumRequiredCalls = 25 failureRateThreshold = 50 waitDurationInOpenState = "25s" diff --git a/examples/graalvm/kora-java-graalvm-crud-cassandra/src/test/java/io/koraframework/example/graalvm/crud/cassandra/ComponentTests.java b/examples/graalvm/kora-java-graalvm-crud-cassandra/src/test/java/io/koraframework/example/graalvm/crud/cassandra/ComponentTests.java index 1e07438c..3da249b9 100644 --- a/examples/graalvm/kora-java-graalvm-crud-cassandra/src/test/java/io/koraframework/example/graalvm/crud/cassandra/ComponentTests.java +++ b/examples/graalvm/kora-java-graalvm-crud-cassandra/src/test/java/io/koraframework/example/graalvm/crud/cassandra/ComponentTests.java @@ -4,12 +4,10 @@ import static org.mockito.ArgumentMatchers.*; import java.util.Collections; -import java.util.concurrent.CompletableFuture; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.Mockito; -import reactor.core.publisher.Mono; import io.koraframework.example.graalvm.crud.cassandra.repository.PetRepository; import io.koraframework.example.graalvm.crud.cassandra.service.PetCache; import io.koraframework.example.graalvm.crud.cassandra.service.PetService; @@ -40,7 +38,8 @@ public KoraConfigModification config() { return KoraConfigModification.ofString(""" resilient { circuitbreaker.pet { - slidingWindowSize = 2 + type = FIXED_WINDOW + countBased.windowSize = 2 minimumRequiredCalls = 2 failureRateThreshold = 100 permittedCallsInHalfOpenState = 1 @@ -63,19 +62,21 @@ void updatePetWithNewCategoryCreated() { mockCache(); mockRepository(); - var added = petService.add(new PetCreateTO("dog", new CategoryCreateTO("dog"))).block(); + var added = petService.add(new PetCreateTO("dog", new CategoryCreateTO("dog"))); assertNotEquals(0L, added.id()); + assertEquals("dog", added.category()); // when - Mockito.when(petRepository.findById(anyLong())).thenReturn(Mono.just(added)); - Mockito.when(petRepository.update(any())).thenReturn(Mono.empty()); + Mockito.when(petRepository.findById(anyLong())).thenReturn(added); var updated = petService.update(added.id(), - new PetUpdateTO(PetUpdateTO.StatusEnum.PENDING, "cat", new CategoryCreateTO("cat"))).blockOptional(); - assertTrue(updated.isPresent()); - assertNotEquals(0L, updated.get().id()); + new PetUpdateTO(PetUpdateTO.StatusEnum.PENDING, "cat", new CategoryCreateTO("cat"))); // then + assertNotNull(updated); + assertEquals(added.id(), updated.id()); + assertEquals("cat", updated.category()); Mockito.verify(petRepository).insert(any()); + Mockito.verify(petRepository).update(any()); } @Test @@ -84,29 +85,29 @@ void updatePetWithSameCategory() { mockCache(); mockRepository(); - var added = petService.add(new PetCreateTO("dog", new CategoryCreateTO("dog"))).block(); + var added = petService.add(new PetCreateTO("dog", new CategoryCreateTO("dog"))); assertNotEquals(0L, added.id()); // when - Mockito.when(petRepository.findById(anyLong())).thenReturn(Mono.just(added)); - Mockito.when(petRepository.update(any())).thenReturn(Mono.empty()); + Mockito.when(petRepository.findById(anyLong())).thenReturn(added); var updated = petService.update(added.id(), - new PetUpdateTO(PetUpdateTO.StatusEnum.PENDING, "cat", new CategoryCreateTO("dog"))).blockOptional(); - assertTrue(updated.isPresent()); - assertNotEquals(0L, updated.get().id()); + new PetUpdateTO(PetUpdateTO.StatusEnum.PENDING, "cat", new CategoryCreateTO("dog"))); // then + assertNotNull(updated); + assertEquals(added.id(), updated.id()); + assertEquals("dog", updated.category()); Mockito.verify(petRepository).insert(any()); + Mockito.verify(petRepository).update(any()); } private void mockCache() { - Mockito.when(petCache.getAsync(anyLong())).thenReturn(CompletableFuture.completedFuture(null)); + Mockito.when(petCache.get(anyLong())).thenReturn(null); Mockito.when(petCache.put(anyLong(), any())).then(invocation -> invocation.getArguments()[1]); - Mockito.when(petCache.getAsync(anyCollection())).thenReturn(CompletableFuture.completedFuture(Collections.emptyMap())); + Mockito.when(petCache.get(anyCollection())).thenReturn(Collections.emptyMap()); } private void mockRepository() { - Mockito.when(petRepository.insert(any())).thenReturn(Mono.empty()); - Mockito.when(petRepository.findById(anyLong())).thenReturn(Mono.empty()); + Mockito.when(petRepository.findById(anyLong())).thenReturn(null); } } diff --git a/examples/graalvm/kora-java-graalvm-crud-jdbc/Dockerfile b/examples/graalvm/kora-java-graalvm-crud-jdbc/Dockerfile index 26bb3f57..501a0108 100644 --- a/examples/graalvm/kora-java-graalvm-crud-jdbc/Dockerfile +++ b/examples/graalvm/kora-java-graalvm-crud-jdbc/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/graalvm/native-image-community:21 as builder +FROM ghcr.io/graalvm/native-image-community:25 as builder ARG TARGET_DIR=/opt/app ARG SOURCE_DIR=build/libs diff --git a/examples/graalvm/kora-java-graalvm-crud-jdbc/README.md b/examples/graalvm/kora-java-graalvm-crud-jdbc/README.md index 69e7a720..0a32cb7e 100644 --- a/examples/graalvm/kora-java-graalvm-crud-jdbc/README.md +++ b/examples/graalvm/kora-java-graalvm-crud-jdbc/README.md @@ -15,7 +15,7 @@ - [Validation](https://kora-projects.github.io/kora-docs/ru/documentation/validation/) - [Cache Caffeine](https://kora-projects.github.io/kora-docs/ru/documentation/cache/#caffeine) -Скомпилирован с помощью [GraalVM](https://www.graalvm.org/release-notes/JDK_21/) +Скомпилирован с помощью [GraalVM](https://www.graalvm.org/release-notes/JDK_25/) ## Build diff --git a/examples/graalvm/kora-java-graalvm-crud-jdbc/build.gradle b/examples/graalvm/kora-java-graalvm-crud-jdbc/build.gradle index 9a827a3f..ce98308b 100644 --- a/examples/graalvm/kora-java-graalvm-crud-jdbc/build.gradle +++ b/examples/graalvm/kora-java-graalvm-crud-jdbc/build.gradle @@ -35,7 +35,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "org.mapstruct:mapstruct-processor:1.5.5.Final" annotationProcessor "io.koraframework:annotation-processors" annotationProcessor "io.goodforgod:graalvm-hint-processor:1.2.0" @@ -60,7 +60,7 @@ dependencies { testImplementation "org.mockito:mockito-core:5.18.0" testImplementation "io.koraframework:test-junit5" - testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.15.0" testImplementation "org.testcontainers:junit-jupiter:1.21.4" } @@ -88,8 +88,8 @@ compileJava.dependsOn openApiGenerateHttpServer graalvmNative { binaries { main { - imageName = "$project.name" - mainClass = "$application.mainClass" + imageName = project.name + mainClass = application.mainClass javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) vendor = JvmVendorSpec.matching("GraalVM Community") @@ -113,7 +113,7 @@ run { ]) } -jar.enabled = false +// nativeCompile takes the project's own artifact as its classpath entry, so the plain jar must stay enabled shadowJar { mergeServiceFiles() manifest { @@ -132,10 +132,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true @@ -182,4 +178,4 @@ javadoc { if (JavaVersion.current().isJava9Compatible()) { options.addBooleanOption("html5", true) } -} \ No newline at end of file +} diff --git a/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/controller/HttpExceptionHandler.java b/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/controller/HttpExceptionHandler.java index 066d282d..77f266b6 100644 --- a/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/controller/HttpExceptionHandler.java +++ b/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/controller/HttpExceptionHandler.java @@ -1,19 +1,22 @@ package io.koraframework.example.graalvm.crud.jdbc.controller; -import io.micrometer.core.instrument.config.validate.ValidationException; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.TimeoutException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import io.koraframework.common.annotation.Component; -import io.koraframework.common.Context; import io.koraframework.common.annotation.Tag; import io.koraframework.example.graalvm.crud.openapi.server.model.MessageTO; import io.koraframework.http.common.body.HttpBody; -import io.koraframework.http.server.common.*; +import io.koraframework.http.server.common.HttpServer; +import io.koraframework.http.server.common.interceptor.HttpServerInterceptor; +import io.koraframework.http.server.common.request.HttpServerRequest; +import io.koraframework.http.server.common.response.HttpServerResponse; +import io.koraframework.http.server.common.response.HttpServerResponseException; import io.koraframework.json.common.JsonWriter; +import io.micrometer.core.instrument.config.validate.ValidationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeoutException; -@Tag(HttpServerModule.class) +@Tag(HttpServer.class) @Component public final class HttpExceptionHandler implements HttpServerInterceptor { @@ -26,14 +29,15 @@ public HttpExceptionHandler(JsonWriter errorJsonWriter) { } @Override - public CompletionStage intercept(Context context, HttpServerRequest request, InterceptChain chain) - throws Exception { - return chain.process(context, request).exceptionally(e -> { + public HttpServerResponse intercept(HttpServerRequest request, InterceptChain chain) { + try { + return chain.process(request); + } catch (Exception e) { if (e instanceof HttpServerResponseException ex) { return ex; } - var body = HttpBody.json(errorJsonWriter.toByteArrayUnchecked(new MessageTO(e.getMessage()))); + var body = HttpBody.json(errorJsonWriter.toByteArray(new MessageTO(e.getMessage()))); if (e instanceof IllegalArgumentException || e instanceof ValidationException) { return HttpServerResponse.of(400, body); } else if (e instanceof TimeoutException) { @@ -42,6 +46,6 @@ public CompletionStage intercept(Context context, HttpServer logger.error("Request '{} {}' failed", request.method(), request.path(), e); return HttpServerResponse.of(500, body); } - }); + } } } diff --git a/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/model/dao/PetWithCategory.java b/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/model/dao/PetWithCategory.java index 96a6d592..68de1589 100644 --- a/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/model/dao/PetWithCategory.java +++ b/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/model/dao/PetWithCategory.java @@ -7,7 +7,7 @@ @EntityJdbc public record PetWithCategory(@Column("id") long id, @Column("name") String name, - @Column("status") Pet.Status status, + Pet.@Column("status") Status status, @Embedded("category_") PetCategory category) { public Pet getPet() { diff --git a/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/service/PetCircuitBreaker.java b/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/service/PetCircuitBreaker.java new file mode 100644 index 00000000..17f27d48 --- /dev/null +++ b/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/service/PetCircuitBreaker.java @@ -0,0 +1,9 @@ +package io.koraframework.example.graalvm.crud.jdbc.service; + +import io.koraframework.resilient.circuitbreaker.CircuitBreaker; +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakerSpec; + +@CircuitBreakerSpec("resilient.circuitbreaker.pet") +public interface PetCircuitBreaker extends CircuitBreaker { + +} diff --git a/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/service/PetRetry.java b/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/service/PetRetry.java new file mode 100644 index 00000000..d6869751 --- /dev/null +++ b/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/service/PetRetry.java @@ -0,0 +1,9 @@ +package io.koraframework.example.graalvm.crud.jdbc.service; + +import io.koraframework.resilient.retry.Retry; +import io.koraframework.resilient.retry.annotation.RetrySpec; + +@RetrySpec("resilient.retry.pet") +public interface PetRetry extends Retry { + +} diff --git a/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/service/PetService.java b/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/service/PetService.java index 999af481..56a37591 100644 --- a/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/service/PetService.java +++ b/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/service/PetService.java @@ -13,8 +13,8 @@ import io.koraframework.example.graalvm.crud.jdbc.repository.PetRepository; import io.koraframework.example.graalvm.crud.openapi.server.model.PetCreateTO; import io.koraframework.example.graalvm.crud.openapi.server.model.PetUpdateTO; -import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreaker; -import io.koraframework.resilient.retry.annotation.Retry; +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakable; +import io.koraframework.resilient.retry.annotation.Retryable; import io.koraframework.resilient.timeout.annotation.Timeout; @Component @@ -29,15 +29,15 @@ public PetService(PetRepository petRepository, CategoryRepository categoryReposi } @Cacheable(PetCache.class) - @CircuitBreaker("pet") - @Retry("pet") - @Timeout("pet") + @CircuitBreakable(PetCircuitBreaker.class) + @Retryable(PetRetry.class) + @Timeout(PetTimeouter.class) public Optional findByID(long petId) { return petRepository.findById(petId); } - @CircuitBreaker("pet") - @Timeout("pet") + @CircuitBreakable(PetCircuitBreaker.class) + @Timeout(PetTimeouter.class) public PetWithCategory add(PetCreateTO createTO) { final long petCategoryId = categoryRepository.findByName(createTO.category().name()) .map(PetCategory::id) @@ -51,8 +51,8 @@ public PetWithCategory add(PetCreateTO createTO) { new PetCategory(petCategoryId, createTO.category().name())); } - @CircuitBreaker("pet") - @Timeout("pet") + @CircuitBreakable(PetCircuitBreaker.class) + @Timeout(PetTimeouter.class) @CachePut(value = PetCache.class, args = "id") public Optional update(long id, PetUpdateTO updateTO) { final Optional existing = petRepository.findById(id); @@ -77,8 +77,8 @@ public Optional update(long id, PetUpdateTO updateTO) { return Optional.of(result); } - @CircuitBreaker("pet") - @Timeout("pet") + @CircuitBreakable(PetCircuitBreaker.class) + @Timeout(PetTimeouter.class) @CacheInvalidate(PetCache.class) public boolean delete(long petId) { return petRepository.deleteById(petId).value() == 1; diff --git a/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/service/PetTimeouter.java b/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/service/PetTimeouter.java new file mode 100644 index 00000000..c351232d --- /dev/null +++ b/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/java/io/koraframework/example/graalvm/crud/jdbc/service/PetTimeouter.java @@ -0,0 +1,9 @@ +package io.koraframework.example.graalvm.crud.jdbc.service; + +import io.koraframework.resilient.timeout.Timeouter; +import io.koraframework.resilient.timeout.annotation.TimeoutSpec; + +@TimeoutSpec("resilient.timeout.pet") +public interface PetTimeouter extends Timeouter { + +} diff --git a/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/resources/META-INF/native-image/ru.tinkoff.kora.examples/logback/reflect-config.json b/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/resources/META-INF/native-image/io.koraframework.examples/logback/reflect-config.json similarity index 100% rename from examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/resources/META-INF/native-image/ru.tinkoff.kora.examples/logback/reflect-config.json rename to examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/resources/META-INF/native-image/io.koraframework.examples/logback/reflect-config.json diff --git a/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/resources/META-INF/native-image/ru.tinkoff.kora.examples/logback/resource-config.json b/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/resources/META-INF/native-image/io.koraframework.examples/logback/resource-config.json similarity index 100% rename from examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/resources/META-INF/native-image/ru.tinkoff.kora.examples/logback/resource-config.json rename to examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/resources/META-INF/native-image/io.koraframework.examples/logback/resource-config.json diff --git a/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/resources/application.conf b/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/resources/application.conf index 05b57809..c4eeadf5 100644 --- a/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/resources/application.conf +++ b/examples/graalvm/kora-java-graalvm-crud-jdbc/src/main/resources/application.conf @@ -1,11 +1,11 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } -db { +jdbc { jdbcUrl = ${POSTGRES_JDBC_URL} username = ${POSTGRES_USER} password = ${POSTGRES_PASS} @@ -26,11 +26,11 @@ pet-cache { openapi { management { enabled = true - file = "openapi/http-server.yaml" + files = "openapi/http-server.yaml" swaggerui { enabled = true } - rapidoc { + scalar { enabled = true } } @@ -39,7 +39,8 @@ openapi { resilient { circuitbreaker.pet { - slidingWindowSize = 50 + type = FIXED_WINDOW + countBased.windowSize = 50 minimumRequiredCalls = 25 failureRateThreshold = 50 waitDurationInOpenState = "25s" diff --git a/examples/graalvm/kora-java-graalvm-crud-jdbc/src/test/java/io/koraframework/example/graalvm/crud/jdbc/ComponentTests.java b/examples/graalvm/kora-java-graalvm-crud-jdbc/src/test/java/io/koraframework/example/graalvm/crud/jdbc/ComponentTests.java index d3ccc889..b0d240a9 100644 --- a/examples/graalvm/kora-java-graalvm-crud-jdbc/src/test/java/io/koraframework/example/graalvm/crud/jdbc/ComponentTests.java +++ b/examples/graalvm/kora-java-graalvm-crud-jdbc/src/test/java/io/koraframework/example/graalvm/crud/jdbc/ComponentTests.java @@ -44,7 +44,8 @@ public KoraConfigModification config() { return KoraConfigModification.ofString(""" resilient { circuitbreaker.pet { - slidingWindowSize = 2 + type = FIXED_WINDOW + countBased.windowSize = 2 minimumRequiredCalls = 2 failureRateThreshold = 100 permittedCallsInHalfOpenState = 1 diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/Dockerfile b/examples/graalvm/kora-java-graalvm-crud-r2dbc/Dockerfile deleted file mode 100644 index 26bb3f57..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -FROM ghcr.io/graalvm/native-image-community:21 as builder - -ARG TARGET_DIR=/opt/app -ARG SOURCE_DIR=build/libs -WORKDIR $TARGET_DIR - -COPY $SOURCE_DIR/*-all.jar $TARGET_DIR/application.jar - -RUN native-image --no-fallback -classpath $TARGET_DIR/application.jar - -FROM ubuntu:noble-20240212 as runner - -ARG TARGET_DIR=/opt/app -WORKDIR $TARGET_DIR - -COPY --from=builder $TARGET_DIR/application $TARGET_DIR/application - -ARG DOCKER_USER=app -RUN groupadd -r $DOCKER_USER && useradd -rg $DOCKER_USER $DOCKER_USER -RUN chmod +x application -USER $DOCKER_USER - -EXPOSE 8080/tcp -EXPOSE 8085/tcp -CMD "/opt/app/application" \ No newline at end of file diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/README.md b/examples/graalvm/kora-java-graalvm-crud-r2dbc/README.md deleted file mode 100644 index 43480785..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/README.md +++ /dev/null @@ -1,88 +0,0 @@ -# Kora Java GraalVM CRUD R2DBC Service - -Пример сервиса реализованного на Kora с упрощенным HTTP [CRUD](https://github.com/swagger-api/swagger-petstore) API -и использованием [GraalVM Native Image](https://www.graalvm.org/latest/reference-manual/native-image/), -в качестве базы данных выступает Postgres, используется кэш Caffeine, а также другие модули которые использовались бы в реальном приложении в бою. - -В примере использовались модули: -- [HTTP Server](https://kora-projects.github.io/kora-docs/ru/documentation/http-server/) -- [HTTP Server OpenAPI Generation](https://kora-projects.github.io/kora-docs/ru/documentation/openapi-codegen/) -- [Probes](https://kora-projects.github.io/kora-docs/ru/documentation/probes/) -- [Metrics](https://kora-projects.github.io/kora-docs/ru/documentation/metrics/) -- [Database R2DBC](https://kora-projects.github.io/kora-docs/ru/documentation/database-r2dbc/) -- [JSON](https://kora-projects.github.io/kora-docs/ru/documentation/json/) -- [Resilient](https://kora-projects.github.io/kora-docs/ru/documentation/resilient/) -- [Validation](https://kora-projects.github.io/kora-docs/ru/documentation/validation/) -- [Cache Caffeine](https://kora-projects.github.io/kora-docs/ru/documentation/cache/#caffeine) - -Скомпилирован с помощью [GraalVM](https://www.graalvm.org/release-notes/JDK_21/) - -## Build - -Собрать классы: - -```shell -./gradlew classes -``` - -Собрать Java артефакт: - -```shell -./gradlew shadowJar -``` - -### Generate - -Сгенерировать API для HTTP Server: -```shell -./gradlew openApiGenerateHttpServer -``` - -### Image - -Собрать образ приложения: -```shell -docker build -t kora-java-graalvm-crud-r2dbc . -``` - -Получить GraalVM Native Image артефакт: - -```shell -docker run --rm --entrypoint cat kora-java-graalvm-crud-r2dbc /opt/app/application > application -``` - -## Migration - -Миграции вызываются с помощью Flyway Gradle Plugin: -```shell -./gradlew flywayMigrate -``` - -## Run - -Запустить Java локально: -```shell -./gradlew run -``` - -## Run Docker-Compose - -Требуется сначала собрать артефакт: - -```shell -./gradlew shadowJar -``` - -Запустить как docker-compose: -```shell -docker-compose up -``` - -## Test - -Тесты используют [Testcontainers](https://java.testcontainers.org/), требуется [Docker](https://docs.docker.com/engine/install/) окружение для запуска тестов или аналогичные контейнерные окружения ([colima](https://github.com/abiosoft/colima) / итп) - -Протестировать локально: -```shell -./gradlew test -``` diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/build.gradle b/examples/graalvm/kora-java-graalvm-crud-r2dbc/build.gradle deleted file mode 100644 index 05ac2977..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/build.gradle +++ /dev/null @@ -1,187 +0,0 @@ -import org.openapitools.generator.gradle.plugin.tasks.GenerateTask - -buildscript { - repositories { - mavenLocal() - mavenCentral() - } - dependencies { - classpath("io.koraframework:openapi-generator:$koraVersion") - } -} - -plugins { - id "java" - id "jacoco" - id "application" - - id "org.openapi.generator" version "7.23.0" - id "org.flywaydb.flyway" version "8.4.2" - id "com.gradleup.shadow" version "9.4.1" - id "org.graalvm.buildtools.native" -} - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(25) - vendor = JvmVendorSpec.ADOPTIUM - } -} - -configurations { - koraBom - annotationProcessor.extendsFrom(koraBom); compileOnly.extendsFrom(koraBom); implementation.extendsFrom(koraBom) - api.extendsFrom(koraBom); testImplementation.extendsFrom(koraBom); testAnnotationProcessor.extendsFrom(koraBom) -} - -dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") - annotationProcessor "org.mapstruct:mapstruct-processor:1.5.5.Final" - annotationProcessor "io.koraframework:annotation-processors" - annotationProcessor "io.goodforgod:graalvm-hint-processor:1.2.0" - compileOnly "io.goodforgod:graalvm-hint-annotations:1.2.0" - compileOnly "org.postgresql:postgresql:42.7.7" - - implementation "io.koraframework:http-server-undertow" - implementation "io.koraframework:database-r2dbc" - implementation "io.koraframework:micrometer-module" - implementation "io.koraframework:json-common" - implementation "io.koraframework:validation-module" - implementation "io.koraframework:cache-caffeine" - implementation "io.koraframework:resilient-kora" - implementation "io.koraframework:config-hocon" - implementation "io.koraframework:openapi-management" - implementation "io.koraframework:logging-logback" - - implementation "org.mapstruct:mapstruct:1.5.5.Final" - implementation "org.postgresql:r2dbc-postgresql:1.0.4.RELEASE" - - testImplementation "org.json:json:20231013" - testImplementation "org.skyscreamer:jsonassert:1.5.1" - testImplementation "org.postgresql:postgresql:42.7.7" - - testImplementation "org.mockito:mockito-core:5.18.0" - testImplementation "io.koraframework:test-junit5" - testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.13.1" - testImplementation "org.testcontainers:junit-jupiter:1.21.4" -} - -application { - mainClass = "io.koraframework.example.graalvm.crud.r2dbc.Application" -} - -def openApiGenerateHttpServer = tasks.register("openApiGenerateHttpServer", GenerateTask) { - generatorName = "kora" - group = "openapi tools" - inputSpec = layout.projectDirectory.file("src/main/resources/openapi/http-server.yaml") - outputDir = layout.buildDirectory.dir("generated/openapi") - def corePackage = "io.koraframework.example.graalvm.crud.openapi.server" - apiPackage = "${corePackage}.api" - modelPackage = "${corePackage}.model" - invokerPackage = "${corePackage}.invoker" - configOptions = [ - mode : "java-server", // так же есть java-server вариация HTTP Server"а - enableServerValidation: "true" - ] -} -sourceSets.main { java.srcDirs += openApiGenerateHttpServer.get().outputDir } -compileJava.dependsOn openApiGenerateHttpServer - -graalvmNative { - binaries { - main { - imageName = "$project.name" - mainClass = "$application.mainClass" - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(25) - vendor = JvmVendorSpec.matching("GraalVM Community") - } - } - } - metadataRepository { - enabled = true - } -} - -processResources.dependsOn tasks.collectReachabilityMetadata -sourceSets.main { resources.srcDirs += "$buildDir/native-reachability-metadata" } - -//noinspection GroovyAssignabilityCheck -run { - environment([ - "POSTGRES_R2DBC_URL": "r2dbc:postgresql://$postgresHost:$postgresPort/$postgresDatabase", - "POSTGRES_USER" : "$postgresUser", - "POSTGRES_PASS" : "$postgresPassword", - ]) -} - -jar.enabled = false -shadowJar { - mergeServiceFiles() - manifest { - attributes "Main-Class": application.mainClass - attributes "Implementation-Version": koraVersion - } -} - -assemble.dependsOn shadowJar - -test { - dependsOn tasks.shadowJar - - jvmArgs += [ - "-XX:+TieredCompilation", - "-XX:TieredStopAtLevel=1", - ] - - environment([ - "": "" - ]) - - useJUnitPlatform() - testLogging { - showStandardStreams = true - events("passed", "skipped", "failed") - exceptionFormat = "full" - } - - exclude("**/\$*") - - jacoco { - excludes += ["**/generated/**", "**/Application*", "**/\$*"] - } - - reports { - html.required = false - junitXml.required = false - } -} - -flyway { - url = "jdbc:postgresql://$postgresHost:$postgresPort/$postgresDatabase" - user = "$postgresUser" - password = "$postgresPassword" - locations = ["classpath:db/migration"] -} - -compileJava { - options.encoding = "UTF-8" - options.incremental = true - options.fork = false -} - -check.dependsOn jacocoTestReport -jacocoTestReport { - reports { - xml.required = true - html.outputLocation = layout.buildDirectory.dir("jacocoHtml") - } - classDirectories = files(classDirectories.files.collect { fileTree(dir: it, excludes: test.jacoco.excludes) }) -} - -javadoc { - options.encoding = "UTF-8" - if (JavaVersion.current().isJava9Compatible()) { - options.addBooleanOption("html5", true) - } -} \ No newline at end of file diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/docker-compose.yml b/examples/graalvm/kora-java-graalvm-crud-r2dbc/docker-compose.yml deleted file mode 100644 index 078bceb7..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/docker-compose.yml +++ /dev/null @@ -1,38 +0,0 @@ -services: - postgres: - image: postgres:16.4-alpine - restart: unless-stopped - ports: - - '5432:5432' - environment: - POSTGRES_DATABASE: postgres - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - - - flyway: - image: flyway/flyway:10.2-alpine - restart: no - command: -url=jdbc:postgresql://postgres:5432/postgres -schemas=public -user=postgres -password=postgres -connectRetries=60 migrate - volumes: - - ./src/main/resources/db/migration:/flyway/sql - depends_on: - - postgres - - -# Можно закомментировать секцию application и использовать docker-compose -# как окружения для запуска сервиса через gradle run - application: - image: kora-java-graalvm-crud-r2dbc - build: . - restart: unless-stopped - ports: - - '8080:8080' - - '8085:8085' - environment: - POSTGRES_R2DBC_URL: jdbc:postgresql://postgres:5432/postgres - POSTGRES_USER: postgres - POSTGRES_PASS: postgres - depends_on: - - postgres - - flyway diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/gradle.properties b/examples/graalvm/kora-java-graalvm-crud-r2dbc/gradle.properties deleted file mode 100644 index 8e7a5091..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/gradle.properties +++ /dev/null @@ -1,6 +0,0 @@ -##### POSTGRES ##### -postgresHost=localhost -postgresPort=5432 -postgresUser=postgres -postgresPassword=postgres -postgresDatabase=postgres diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/Application.java b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/Application.java deleted file mode 100644 index faa8f9fd..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/Application.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.koraframework.example.graalvm.crud.r2dbc; - -import io.goodforgod.graalvm.hint.annotation.NativeImageHint; -import io.goodforgod.graalvm.hint.annotation.ResourceHint; -import io.koraframework.application.graph.KoraApplication; -import io.koraframework.cache.caffeine.CaffeineCacheModule; -import io.koraframework.common.annotation.KoraApp; -import io.koraframework.config.hocon.HoconConfigModule; -import io.koraframework.database.r2dbc.R2dbcDatabaseModule; -import io.koraframework.http.server.undertow.UndertowPublicHttpServerModule; -import io.koraframework.json.common.JsonModule; -import io.koraframework.logging.logback.LogbackModule; -import io.koraframework.micrometer.module.MetricsModule; -import io.koraframework.openapi.management.OpenApiManagementModule; -import io.koraframework.resilient.ResilientModule; -import io.koraframework.validation.module.ValidationModule; - -@ResourceHint(include = { "openapi/http-server.yaml" }) -@NativeImageHint(name = "application", entrypoint = Application.class) -@KoraApp -public interface Application extends - HoconConfigModule, - LogbackModule, - R2dbcDatabaseModule, - ValidationModule, - JsonModule, - CaffeineCacheModule, - ResilientModule, - MetricsModule, - OpenApiManagementModule, - UndertowPublicHttpServerModule { - - static void main(String[] args) { - KoraApplication.run(ApplicationGraph::graph); - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/controller/HttpExceptionHandler.java b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/controller/HttpExceptionHandler.java deleted file mode 100644 index bb04c750..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/controller/HttpExceptionHandler.java +++ /dev/null @@ -1,47 +0,0 @@ -package io.koraframework.example.graalvm.crud.r2dbc.controller; - -import io.micrometer.core.instrument.config.validate.ValidationException; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.TimeoutException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import io.koraframework.common.annotation.Component; -import io.koraframework.common.Context; -import io.koraframework.common.annotation.Tag; -import io.koraframework.example.graalvm.crud.openapi.server.model.MessageTO; -import io.koraframework.http.common.body.HttpBody; -import io.koraframework.http.server.common.*; -import io.koraframework.json.common.JsonWriter; - -@Tag(HttpServerModule.class) -@Component -public final class HttpExceptionHandler implements HttpServerInterceptor { - - private static final Logger logger = LoggerFactory.getLogger(HttpExceptionHandler.class); - - private final JsonWriter errorJsonWriter; - - public HttpExceptionHandler(JsonWriter errorJsonWriter) { - this.errorJsonWriter = errorJsonWriter; - } - - @Override - public CompletionStage intercept(Context context, HttpServerRequest request, InterceptChain chain) - throws Exception { - return chain.process(context, request).exceptionally(e -> { - if (e instanceof HttpServerResponseException ex) { - return ex; - } - - var body = HttpBody.json(errorJsonWriter.toByteArrayUnchecked(new MessageTO(e.getMessage()))); - if (e instanceof IllegalArgumentException || e instanceof ValidationException) { - return HttpServerResponse.of(400, body); - } else if (e instanceof TimeoutException) { - return HttpServerResponse.of(408, body); - } else { - logger.error("Request '{} {}' failed", request.method(), request.path(), e); - return HttpServerResponse.of(500, body); - } - }); - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/controller/PetDelegate.java b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/controller/PetDelegate.java deleted file mode 100644 index f40c36c7..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/controller/PetDelegate.java +++ /dev/null @@ -1,82 +0,0 @@ -package io.koraframework.example.graalvm.crud.r2dbc.controller; - -import static io.koraframework.example.graalvm.crud.openapi.server.api.PetApiResponses.*; - -import reactor.core.publisher.Mono; -import io.koraframework.common.annotation.Component; -import io.koraframework.example.graalvm.crud.openapi.server.api.PetApiDelegate; -import io.koraframework.example.graalvm.crud.openapi.server.model.MessageTO; -import io.koraframework.example.graalvm.crud.openapi.server.model.PetCreateTO; -import io.koraframework.example.graalvm.crud.openapi.server.model.PetUpdateTO; -import io.koraframework.example.graalvm.crud.r2dbc.model.mapper.PetMapper; -import io.koraframework.example.graalvm.crud.r2dbc.service.PetService; - -@Component -public final class PetDelegate implements PetApiDelegate { - - private final PetMapper petMapper; - private final PetService petService; - - public PetDelegate(PetMapper petMapper, PetService petService) { - this.petMapper = petMapper; - this.petService = petService; - } - - @Override - public Mono getPetById(long petId) { - if (petId < 0) { - return Mono.just(new GetPetByIdApiResponse.GetPetById400ApiResponse(malformedId(petId))); - } - - return petService.findByID(petId) - .map(pet -> { - var body = petMapper.asDTO(pet); - return ((GetPetByIdApiResponse) new GetPetByIdApiResponse.GetPetById200ApiResponse(body)); - }) - .switchIfEmpty(Mono.fromSupplier(() -> new GetPetByIdApiResponse.GetPetById404ApiResponse(notFound(petId)))); - } - - @Override - public Mono addPet(PetCreateTO petCreateTO) { - return petService.add(petCreateTO) - .map(pet -> { - var body = petMapper.asDTO(pet); - return new AddPetApiResponse.AddPet200ApiResponse(body); - }); - } - - @Override - public Mono updatePet(long petId, PetUpdateTO petUpdateTO) { - if (petId < 0) { - return Mono.just(new UpdatePetApiResponse.UpdatePet400ApiResponse(malformedId(petId))); - } - - return petService.update(petId, petUpdateTO) - .map(pet -> { - var body = petMapper.asDTO(pet); - return ((UpdatePetApiResponse) new UpdatePetApiResponse.UpdatePet200ApiResponse(body)); - }) - .switchIfEmpty(Mono.fromSupplier(() -> new UpdatePetApiResponse.UpdatePet404ApiResponse(notFound(petId)))); - } - - @Override - public Mono deletePet(long petId) { - if (petId < 0) { - return Mono.just(new DeletePetApiResponse.DeletePet400ApiResponse(malformedId(petId))); - } - - return petService.delete(petId) - .map(isDeleted -> (isDeleted) - ? new DeletePetApiResponse.DeletePet200ApiResponse( - new MessageTO("Successfully deleted pet with ID: " + petId)) - : new DeletePetApiResponse.DeletePet404ApiResponse(notFound(petId))); - } - - private static MessageTO notFound(long petId) { - return new MessageTO("Pet not found for ID: " + petId); - } - - private static MessageTO malformedId(long petId) { - return new MessageTO("Pet malformed ID: " + petId); - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/model/dao/Pet.java b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/model/dao/Pet.java deleted file mode 100644 index 2442b092..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/model/dao/Pet.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.koraframework.example.graalvm.crud.r2dbc.model.dao; - -import io.koraframework.database.common.annotation.Column; -import io.koraframework.database.common.annotation.Id; -import io.koraframework.database.common.annotation.Table; - -@Table("pets") -public record Pet(@Id @Column("id") long id, - @Column("name") String name, - @Column("status") Status status, - @Column("category_id") long categoryId) { - - public enum Status { - - AVAILABLE(0), - PENDING(10), - SOLD(20); - - public final int code; - - Status(int code) { - this.code = code; - } - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/model/dao/PetCategory.java b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/model/dao/PetCategory.java deleted file mode 100644 index 82c2bda7..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/model/dao/PetCategory.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.koraframework.example.graalvm.crud.r2dbc.model.dao; - -import io.koraframework.database.common.annotation.Id; -import io.koraframework.database.common.annotation.Table; - -@Table("categories") -public record PetCategory(@Id long id, - String name) {} diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/model/dao/PetWithCategory.java b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/model/dao/PetWithCategory.java deleted file mode 100644 index c105f10a..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/model/dao/PetWithCategory.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.koraframework.example.graalvm.crud.r2dbc.model.dao; - -import io.koraframework.database.common.annotation.Column; -import io.koraframework.database.common.annotation.Embedded; - -public record PetWithCategory(@Column("id") long id, - @Column("name") String name, - @Column("status") Pet.Status status, - @Embedded("category_") PetCategory category) { - - public PetWithCategory(Pet pet, PetCategory category) { - this(pet.id(), pet.name(), pet.status(), category); - } - - public Pet getPet() { - return new Pet(id, name, status, category.id()); - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/model/mapper/PetMapper.java b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/model/mapper/PetMapper.java deleted file mode 100644 index abbc6882..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/model/mapper/PetMapper.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.koraframework.example.graalvm.crud.r2dbc.model.mapper; - -import org.mapstruct.Mapper; -import org.mapstruct.ReportingPolicy; -import io.koraframework.example.graalvm.crud.openapi.server.model.CategoryTO; -import io.koraframework.example.graalvm.crud.openapi.server.model.PetTO; -import io.koraframework.example.graalvm.crud.r2dbc.model.dao.PetCategory; -import io.koraframework.example.graalvm.crud.r2dbc.model.dao.PetWithCategory; - -@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE) -public interface PetMapper { - - PetTO asDTO(PetWithCategory pet); - - CategoryTO asDTO(PetCategory category); -} diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/repository/CategoryRepository.java b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/repository/CategoryRepository.java deleted file mode 100644 index 77f751ea..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/repository/CategoryRepository.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.koraframework.example.graalvm.crud.r2dbc.repository; - -import reactor.core.publisher.Mono; -import io.koraframework.database.common.UpdateCount; -import io.koraframework.database.common.annotation.Id; -import io.koraframework.database.common.annotation.Query; -import io.koraframework.database.common.annotation.Repository; -import io.koraframework.database.r2dbc.R2dbcRepository; -import io.koraframework.example.graalvm.crud.r2dbc.model.dao.PetCategory; - -@Repository -public interface CategoryRepository extends R2dbcRepository { - - @Query("SELECT %{return#selects} FROM %{return#table} WHERE name = :name") - Mono findByName(String name); - - @Id - @Query("INSERT INTO categories(name) VALUES (:categoryName)") - Mono insert(String categoryName); - - @Query("DELETE FROM categories WHERE id = :id") - Mono deleteById(long id); -} diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/repository/PetRepository.java b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/repository/PetRepository.java deleted file mode 100644 index a1bf55bb..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/repository/PetRepository.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.koraframework.example.graalvm.crud.r2dbc.repository; - -import reactor.core.publisher.Mono; -import io.koraframework.database.common.UpdateCount; -import io.koraframework.database.common.annotation.Id; -import io.koraframework.database.common.annotation.Query; -import io.koraframework.database.common.annotation.Repository; -import io.koraframework.database.r2dbc.R2dbcRepository; -import io.koraframework.example.graalvm.crud.r2dbc.model.dao.Pet; -import io.koraframework.example.graalvm.crud.r2dbc.model.dao.PetWithCategory; - -@Repository -public interface PetRepository extends R2dbcRepository { - - @Query(""" - SELECT p.id, p.name, p.status, p.category_id, c.name as category_name - FROM pets p - JOIN categories c on c.id = p.category_id - WHERE p.id = :id - """) - Mono findById(long id); - - @Id - @Query("INSERT INTO %{entity#inserts -= id}") - Mono insert(Pet entity); - - @Query("UPDATE %{entity#table} SET %{entity#updates} WHERE %{entity#where = @id}") - Mono update(Pet entity); - - @Query("DELETE FROM pets WHERE id = :id") - Mono deleteById(long id); -} diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/repository/mapper/PetStatusParameterMapper.java b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/repository/mapper/PetStatusParameterMapper.java deleted file mode 100644 index f6e0429f..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/repository/mapper/PetStatusParameterMapper.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.koraframework.example.graalvm.crud.r2dbc.repository.mapper; - -import io.r2dbc.spi.Statement; -import org.jspecify.annotations.Nullable; -import io.koraframework.common.annotation.Component; -import io.koraframework.database.r2dbc.mapper.parameter.R2dbcParameterColumnMapper; -import io.koraframework.example.graalvm.crud.r2dbc.model.dao.Pet; - -@Component -public final class PetStatusParameterMapper implements R2dbcParameterColumnMapper { - - @Override - public void apply(Statement stmt, int index, @Nullable Pet.Status value) { - if (value == null) { - stmt.bindNull(index, Integer.class); - } else { - stmt.bind(index, value.code); - } - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/repository/mapper/PetStatusResultMapper.java b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/repository/mapper/PetStatusResultMapper.java deleted file mode 100644 index 91186fb5..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/repository/mapper/PetStatusResultMapper.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.koraframework.example.graalvm.crud.r2dbc.repository.mapper; - -import io.r2dbc.spi.Row; -import io.koraframework.common.annotation.Component; -import io.koraframework.database.r2dbc.mapper.result.R2dbcResultColumnMapper; -import io.koraframework.example.graalvm.crud.r2dbc.model.dao.Pet; - -@Component -public final class PetStatusResultMapper implements R2dbcResultColumnMapper { - - private final Pet.Status[] statuses = Pet.Status.values(); - - @Override - public Pet.Status apply(Row row, String label) { - final int code = row.get(label, Integer.class); - for (Pet.Status status : statuses) { - if (code == status.code) { - return status; - } - } - - throw new IllegalStateException("Unknown code: " + code); - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/service/PetCache.java b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/service/PetCache.java deleted file mode 100644 index 4748d52b..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/service/PetCache.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.koraframework.example.graalvm.crud.r2dbc.service; - -import io.koraframework.cache.annotation.Cache; -import io.koraframework.cache.caffeine.CaffeineCache; -import io.koraframework.example.graalvm.crud.r2dbc.model.dao.PetWithCategory; - -@Cache("pet-cache") -public interface PetCache extends CaffeineCache { - -} diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/service/PetService.java b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/service/PetService.java deleted file mode 100644 index e0c5e80a..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/java/io/koraframework/example/graalvm/crud/r2dbc/service/PetService.java +++ /dev/null @@ -1,91 +0,0 @@ -package io.koraframework.example.graalvm.crud.r2dbc.service; - -import reactor.core.publisher.Mono; -import io.koraframework.cache.annotation.CacheInvalidate; -import io.koraframework.cache.annotation.CachePut; -import io.koraframework.cache.annotation.Cacheable; -import io.koraframework.common.annotation.Component; -import io.koraframework.example.graalvm.crud.openapi.server.model.PetCreateTO; -import io.koraframework.example.graalvm.crud.openapi.server.model.PetUpdateTO; -import io.koraframework.example.graalvm.crud.r2dbc.model.dao.Pet; -import io.koraframework.example.graalvm.crud.r2dbc.model.dao.PetCategory; -import io.koraframework.example.graalvm.crud.r2dbc.model.dao.PetWithCategory; -import io.koraframework.example.graalvm.crud.r2dbc.repository.CategoryRepository; -import io.koraframework.example.graalvm.crud.r2dbc.repository.PetRepository; -import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreaker; -import io.koraframework.resilient.retry.annotation.Retry; -import io.koraframework.resilient.timeout.annotation.Timeout; - -@Component -public class PetService { - - private final PetRepository petRepository; - private final CategoryRepository categoryRepository; - - public PetService(PetRepository petRepository, CategoryRepository categoryRepository) { - this.petRepository = petRepository; - this.categoryRepository = categoryRepository; - } - - @Cacheable(PetCache.class) - @CircuitBreaker("pet") - @Retry("pet") - @Timeout("pet") - public Mono findByID(long petId) { - return petRepository.findById(petId); - } - - @CircuitBreaker("pet") - @Timeout("pet") - public Mono add(PetCreateTO createTO) { - return categoryRepository.findByName(createTO.category().name()) - .map(PetCategory::id) - .switchIfEmpty(categoryRepository.insert(createTO.category().name())) - .flatMap(categoryId -> { - var pet = new Pet(0, createTO.name(), Pet.Status.AVAILABLE, categoryId); - return petRepository.insert(pet) - .map(petId -> new PetWithCategory(petId, pet.name(), pet.status(), - new PetCategory(categoryId, createTO.category().name()))); - }); - } - - @CircuitBreaker("pet") - @Timeout("pet") - @CachePut(value = PetCache.class, args = "id") - public Mono update(long id, PetUpdateTO updateTO) { - return petRepository.findById(id) - .flatMap(existingPet -> { - var categoryMono = Mono.just(existingPet.category()); - if (updateTO.category() != null) { - categoryMono = categoryRepository.findByName(updateTO.category().name()) - .switchIfEmpty(Mono.defer(() -> categoryRepository.insert(updateTO.category().name()) - .map(newCategoryId -> new PetCategory(newCategoryId, updateTO.category().name())))); - } - - var status = (updateTO.status() == null) - ? existingPet.status() - : toStatus(updateTO.status()); - - return categoryMono - .flatMap(category -> { - var result = new PetWithCategory(existingPet.id(), updateTO.name(), status, category); - return petRepository.update(result.getPet()).then(Mono.just(result)); - }); - }); - } - - @CircuitBreaker("pet") - @Timeout("pet") - @CacheInvalidate(PetCache.class) - public Mono delete(long petId) { - return petRepository.deleteById(petId).map(counter -> counter.value() == 1); - } - - private static Pet.Status toStatus(PetUpdateTO.StatusEnum statusEnum) { - return switch (statusEnum) { - case AVAILABLE -> Pet.Status.AVAILABLE; - case PENDING -> Pet.Status.PENDING; - case SOLD -> Pet.Status.SOLD; - }; - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/resources/META-INF/native-image/ru.tinkoff.kora.examples/logback/resource-config.json b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/resources/META-INF/native-image/ru.tinkoff.kora.examples/logback/resource-config.json deleted file mode 100644 index bd2d3ecc..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/resources/META-INF/native-image/ru.tinkoff.kora.examples/logback/resource-config.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "resources": { - "includes": [ - { - "pattern": "\\Qlogback.xml\\E" - }, - { - "pattern": "\\Qapplication.conf\\E" - } - ] - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/resources/application.conf b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/resources/application.conf deleted file mode 100644 index 0d9eda74..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/resources/application.conf +++ /dev/null @@ -1,80 +0,0 @@ -httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 - telemetry.logging.enabled = true -} - - -db { - r2dbcUrl = ${POSTGRES_R2DBC_URL} - username = ${POSTGRES_USER} - password = ${POSTGRES_PASS} - maxPoolSize = 10 - poolName = "kora" - initializationFailTimeout = "10s" - telemetry.logging.enabled = true -} - - -pet-cache { - maximumSize = 1000 - maximumSize = ${?CACHE_MAX_SIZE} - expireAfterWrite = ${?CACHE_EXPIRE_WRITE} -} - - -openapi { - management { - enabled = true - file = "openapi/http-server.yaml" - swaggerui { - enabled = true - } - rapidoc { - enabled = true - } - } -} - - -resilient { - circuitbreaker.pet { - slidingWindowSize = 50 - minimumRequiredCalls = 25 - failureRateThreshold = 50 - waitDurationInOpenState = "25s" - permittedCallsInHalfOpenState = 10 - } - timeout.pet { - duration = "5000ms" - } - retry.pet { - delay = "500ms" - delayStep = "5s" - attempts = 3 - attempts = ${?RETRY_ATTEMPTS} - } -} - - -logging.level { - "root": "WARN" - "root": ${?LOGGING_LEVEL_ALL} - "root": ${?LOGGING_LEVEL_ROOT} - - "io.koraframework": "INFO" - "io.koraframework": ${?LOGGING_LEVEL_ALL} - "io.koraframework": ${?LOGGING_LEVEL_KORA} - "io.koraframework.http.server": ${?LOGGING_LEVEL_ALL} - "io.koraframework.http.server": ${?LOGGING_LEVEL_KORA_HTTP_SERVER} - "io.koraframework.database": ${?LOGGING_LEVEL_ALL} - "io.koraframework.database": ${?LOGGING_LEVEL_KORA_DATABASE} - "io.koraframework.cache": ${?LOGGING_LEVEL_ALL} - "io.koraframework.cache": ${?LOGGING_LEVEL_KORA_CACHE} - "io.koraframework.resilient": ${?LOGGING_LEVEL_ALL} - "io.koraframework.resilient": ${?LOGGING_LEVEL_KORA_RESILIENT} - - "io.koraframework.example": "INFO" - "io.koraframework.example": ${?LOGGING_LEVEL_ALL} - "io.koraframework.example": ${?LOGGING_LEVEL_APP} -} diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/resources/db/migration/V1__setup-tables.sql b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/resources/db/migration/V1__setup-tables.sql deleted file mode 100644 index 3e704998..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/resources/db/migration/V1__setup-tables.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE IF NOT EXISTS categories -( - id BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY, - name VARCHAR NOT NULL, - PRIMARY KEY (id) -); - - -CREATE TABLE IF NOT EXISTS pets -( - id BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY, - name VARCHAR NOT NULL, - status SMALLINT NOT NULL, - category_id BIGINT NOT NULL REFERENCES categories(id), - PRIMARY KEY (id) -); diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/resources/openapi/http-server.yaml b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/resources/openapi/http-server.yaml deleted file mode 100644 index 8d99f499..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/resources/openapi/http-server.yaml +++ /dev/null @@ -1,315 +0,0 @@ -openapi: 3.0.3 -info: - title: Swagger Petstore - OpenAPI 3.0 - description: |- - This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about - Swagger at [https://swagger.io](https://swagger.io). - license: - name: Apache 2.0 - url: https://www.apache.org/licenses/LICENSE-2.0.html - version: 1.0.11 -externalDocs: - description: Find out more about Swagger - url: https://swagger.io -tags: - - name: pet - description: Everything about your Pets - externalDocs: - description: Find out more - url: https://swagger.io -paths: - /v3/pets: - post: - tags: - - pet - summary: Add a new pet to the store - description: Add a new pet to the store - operationId: addPet - requestBody: - required: true - description: Create a new pet in the store - content: - application/json: - schema: - $ref: '#/components/schemas/PetCreateTO' - responses: - '200': - description: Invalid parameters supplier - content: - application/json: - schema: - $ref: '#/components/schemas/PetTO' - '400': - description: Invalid parameters supplier - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '408': - description: Timeout exception - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '429': - description: Too many requests - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - /v3/pets/{id}: - get: - tags: - - pet - summary: Find pet by ID - description: Returns a single pet - operationId: getPetById - parameters: - - name: id - in: path - description: ID of pet to return - required: true - schema: - type: integer - format: int64 - nullable: false - minimum: 1 - responses: - '200': - description: successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/PetTO' - '400': - description: Invalid parameters supplier - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '404': - description: Pet not found - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '408': - description: Timeout exception - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '429': - description: Too many requests - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - put: - tags: - - pet - summary: Update an existing pet - description: Update an existing pet by Id - operationId: updatePet - parameters: - - name: id - in: path - description: Pet id to update - required: true - schema: - type: integer - format: int64 - requestBody: - description: Update an existent pet in the store - content: - application/json: - schema: - $ref: '#/components/schemas/PetUpdateTO' - required: true - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/PetTO' - '400': - description: Invalid parameters supplier - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '404': - description: Pet not found - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '408': - description: Timeout exception - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '429': - description: Too many requests - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - delete: - tags: - - pet - summary: Deletes a pet - description: delete a pet - operationId: deletePet - parameters: - - name: id - in: path - description: Pet id to delete - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '400': - description: Invalid parameters supplier - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '404': - description: Pet not found - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '408': - description: Timeout exception - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '429': - description: Too many requests - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' -components: - schemas: - MessageTO: - type: object - properties: - message: - type: string - CategoryTO: - required: - - id - - name - type: object - properties: - id: - type: integer - format: int64 - example: 1 - name: - type: string - example: Dogs - CategoryCreateTO: - required: - - name - type: object - properties: - name: - type: string - example: Dogs - PetStatusTO: - properties: - status: - type: string - description: pet status in the store - enum: - - available - - pending - - sold - PetTO: - allOf: - - $ref: '#/components/schemas/PetStatusTO' - - type: object - required: - - id - - name - properties: - id: - type: integer - format: int64 - example: 10 - nullable: false - name: - type: string - example: doggie - nullable: false - minLength: 1 - maxLength: 50 - category: - $ref: '#/components/schemas/CategoryTO' - PetCreateTO: - required: - - name - - category - type: object - properties: - name: - type: string - example: doggie - nullable: false - minLength: 1 - maxLength: 50 - category: - $ref: '#/components/schemas/CategoryCreateTO' - PetUpdateTO: - allOf: - - $ref: '#/components/schemas/PetStatusTO' - - type: object - required: - - name - properties: - name: - type: string - example: doggie - nullable: false - minLength: 1 - maxLength: 50 - category: - $ref: '#/components/schemas/CategoryCreateTO' diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/test/java/io/koraframework/example/graalvm/crud/r2dbc/AppContainer.java b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/test/java/io/koraframework/example/graalvm/crud/r2dbc/AppContainer.java deleted file mode 100644 index 0f98d4f3..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/test/java/io/koraframework/example/graalvm/crud/r2dbc/AppContainer.java +++ /dev/null @@ -1,47 +0,0 @@ -package io.koraframework.example.graalvm.crud.r2dbc; - -import java.net.URI; -import java.nio.file.Paths; -import java.time.Duration; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.images.builder.ImageFromDockerfile; -import org.testcontainers.utility.DockerImageName; - -public final class AppContainer extends GenericContainer { - - private AppContainer() { - super(new ImageFromDockerfile("kora-java-graalvm-crud-r2dbc") - .withDockerfile(Paths.get("Dockerfile").toAbsolutePath())); - } - - private AppContainer(DockerImageName image) { - super(image); - } - - public static AppContainer build() { - final String appImage = System.getenv("IMAGE_KORA_JAVA_GRAALVM_CRUD_R2DBC"); - return (appImage != null && !appImage.isBlank()) - ? new AppContainer(DockerImageName.parse(appImage)) - : new AppContainer(); - } - - @Override - protected void configure() { - super.configure(); - withExposedPorts(8080, 8085); - withStartupTimeout(Duration.ofSeconds(50)); - withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(AppContainer.class))); - waitingFor(Wait.forHttp("/system/readiness").forPort(8085).forStatusCode(200).withStartupTimeout(Duration.ofSeconds(20))); - } - - public int getPort() { - return getMappedPort(8080); - } - - public URI getURI() { - return URI.create(String.format("http://%s:%s", getHost(), getPort())); - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/test/java/io/koraframework/example/graalvm/crud/r2dbc/BlackBoxTests.java b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/test/java/io/koraframework/example/graalvm/crud/r2dbc/BlackBoxTests.java deleted file mode 100644 index f46eb41a..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/test/java/io/koraframework/example/graalvm/crud/r2dbc/BlackBoxTests.java +++ /dev/null @@ -1,236 +0,0 @@ -package io.koraframework.example.graalvm.crud.r2dbc; - -import static org.junit.jupiter.api.Assertions.*; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.Network; -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL; -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; -import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; -import java.util.Map; -import org.json.JSONObject; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; - -@TestcontainersPostgreSQL( - network = @Network(shared = true), - mode = ContainerMode.PER_RUN, - migration = @Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD)) -class BlackBoxTests { - - private static final AppContainer container = AppContainer.build() - .withNetwork(org.testcontainers.containers.Network.SHARED); - - @ConnectionPostgreSQL - private JdbcConnection connection; - - @BeforeEach - public void setup(@ConnectionPostgreSQL JdbcConnection connection) { - if (!container.isRunning()) { - var params = connection.paramsInNetwork().orElseThrow(); - container.withEnv(Map.ofEntries( - Map.entry( - "POSTGRES_R2DBC_URL", - "r2dbc:postgresql://%s:%s/%s".formatted(params.host(), params.port(), params.database())), - Map.entry("POSTGRES_USER", params.username()), - Map.entry("POSTGRES_PASS", params.password()), - Map.entry("CACHE_MAX_SIZE", "0"), - Map.entry("RETRY_ATTEMPTS", "0"), - Map.entry("LOGGING_LEVEL_KORA", "INFO"), - Map.entry("LOGGING_LEVEL_APP", "DEBUG"), - Map.entry("LOGGING_LEVEL_KORA_HTTP_SERVER", "TRACE"), - Map.entry("LOGGING_LEVEL_KORA_DATABASE", "TRACE"), - Map.entry("LOGGING_LEVEL_KORA_RESILIENT", "TRACE"), - Map.entry("LOGGING_LEVEL_KORA_CACHE", "TRACE"))); - - container.start(); - } - } - - @AfterAll - public static void cleanup() { - container.stop(); - } - - @Test - void addPet() throws Exception { - // given - var httpClient = HttpClient.newHttpClient(); - var requestBody = new JSONObject() - .put("name", "doggie") - .put("category", new JSONObject() - .put("name", "Dogs")); - - // when - var request = HttpRequest.newBuilder() - .POST(HttpRequest.BodyPublishers.ofString(requestBody.toString())) - .uri(container.getURI().resolve("/v3/pets")) - .timeout(Duration.ofSeconds(5)) - .build(); - - var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - assertEquals(200, response.statusCode(), response.body()); - - // then - connection.assertCountsEquals(1, "pets"); - connection.assertCountsEquals(1, "categories"); - var responseBody = new JSONObject(response.body()); - assertNotNull(responseBody.query("/id")); - assertNotEquals(0L, responseBody.query("/id")); - assertNotNull(responseBody.query("/status")); - assertEquals(requestBody.query("/name"), responseBody.query("/name")); - assertNotNull(responseBody.query("/category/id")); - assertEquals(requestBody.query("/category/name"), responseBody.query("/category/name")); - } - - @Test - void getPet() throws Exception { - // given - var httpClient = HttpClient.newHttpClient(); - var createRequestBody = new JSONObject() - .put("name", "doggie") - .put("category", new JSONObject() - .put("name", "Dogs")); - - // when - var createRequest = HttpRequest.newBuilder() - .POST(HttpRequest.BodyPublishers.ofString(createRequestBody.toString())) - .uri(container.getURI().resolve("/v3/pets")) - .timeout(Duration.ofSeconds(5)) - .build(); - - var createResponse = httpClient.send(createRequest, HttpResponse.BodyHandlers.ofString()); - assertEquals(200, createResponse.statusCode(), createResponse.body()); - connection.assertCountsEquals(1, "pets"); - connection.assertCountsEquals(1, "categories"); - var createResponseBody = new JSONObject(createResponse.body()); - - // then - var getRequest = HttpRequest.newBuilder() - .GET() - .uri(container.getURI().resolve("/v3/pets/" + createResponseBody.query("/id"))) - .timeout(Duration.ofSeconds(5)) - .build(); - - var getResponse = httpClient.send(getRequest, HttpResponse.BodyHandlers.ofString()); - assertEquals(200, getResponse.statusCode(), getResponse.body()); - - var getResponseBody = new JSONObject(getResponse.body()); - JSONAssert.assertEquals(createResponseBody.toString(), getResponseBody.toString(), JSONCompareMode.LENIENT); - } - - @Test - void getPetNotFound() throws Exception { - // given - var httpClient = HttpClient.newHttpClient(); - - // when - var getRequest = HttpRequest.newBuilder() - .GET() - .uri(container.getURI().resolve("/v3/pets/1")) - .timeout(Duration.ofSeconds(5)) - .build(); - - // then - var getResponse = httpClient.send(getRequest, HttpResponse.BodyHandlers.ofString()); - assertEquals(404, getResponse.statusCode(), getResponse.body()); - } - - @Test - void updatePet() throws Exception { - // given - var httpClient = HttpClient.newHttpClient(); - var createRequestBody = new JSONObject() - .put("name", "doggie") - .put("category", new JSONObject() - .put("name", "Dogs")); - - var createRequest = HttpRequest.newBuilder() - .POST(HttpRequest.BodyPublishers.ofString(createRequestBody.toString())) - .uri(container.getURI().resolve("/v3/pets")) - .timeout(Duration.ofSeconds(5)) - .build(); - - var createResponse = httpClient.send(createRequest, HttpResponse.BodyHandlers.ofString()); - assertEquals(200, createResponse.statusCode(), createResponse.body()); - connection.assertCountsEquals(1, "pets"); - connection.assertCountsEquals(1, "categories"); - var createResponseBody = new JSONObject(createResponse.body()); - - // when - var updateRequestBody = new JSONObject() - .put("name", "doggie2") - .put("status", "pending") - .put("category", new JSONObject() - .put("name", "Dogs2")); - - var updateRequest = HttpRequest.newBuilder() - .PUT(HttpRequest.BodyPublishers.ofString(updateRequestBody.toString())) - .uri(container.getURI().resolve("/v3/pets/" + createResponseBody.query("/id"))) - .timeout(Duration.ofSeconds(5)) - .build(); - - var updateResponse = httpClient.send(updateRequest, HttpResponse.BodyHandlers.ofString()); - assertEquals(200, updateResponse.statusCode(), updateResponse.body()); - var updateResponseBody = new JSONObject(updateResponse.body()); - - // then - var getRequest = HttpRequest.newBuilder() - .GET() - .uri(container.getURI().resolve("/v3/pets/" + createResponseBody.query("/id"))) - .timeout(Duration.ofSeconds(5)) - .build(); - - var getResponse = httpClient.send(getRequest, HttpResponse.BodyHandlers.ofString()); - assertEquals(200, createResponse.statusCode(), getResponse.body()); - - var getResponseBody = new JSONObject(getResponse.body()); - JSONAssert.assertEquals(updateResponseBody.toString(), getResponseBody.toString(), JSONCompareMode.LENIENT); - } - - @Test - void deletePet() throws Exception { - // given - var httpClient = HttpClient.newHttpClient(); - var createRequestBody = new JSONObject() - .put("name", "doggie") - .put("category", new JSONObject() - .put("name", "Dogs")); - - var createRequest = HttpRequest.newBuilder() - .POST(HttpRequest.BodyPublishers.ofString(createRequestBody.toString())) - .uri(container.getURI().resolve("/v3/pets")) - .timeout(Duration.ofSeconds(5)) - .build(); - - var createResponse = httpClient.send(createRequest, HttpResponse.BodyHandlers.ofString()); - assertEquals(200, createResponse.statusCode(), createResponse.body()); - connection.assertCountsEquals(1, "pets"); - connection.assertCountsEquals(1, "categories"); - var createResponseBody = new JSONObject(createResponse.body()); - - // when - var deleteRequest = HttpRequest.newBuilder() - .DELETE() - .uri(container.getURI().resolve("/v3/pets/" + createResponseBody.query("/id"))) - .timeout(Duration.ofSeconds(5)) - .build(); - - var deleteResponse = httpClient.send(deleteRequest, HttpResponse.BodyHandlers.ofString()); - assertEquals(200, deleteResponse.statusCode(), deleteResponse.body()); - - // then - connection.assertCountsEquals(0, "pets"); - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/test/java/io/koraframework/example/graalvm/crud/r2dbc/ComponentTests.java b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/test/java/io/koraframework/example/graalvm/crud/r2dbc/ComponentTests.java deleted file mode 100644 index 1dbab3de..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/test/java/io/koraframework/example/graalvm/crud/r2dbc/ComponentTests.java +++ /dev/null @@ -1,127 +0,0 @@ -package io.koraframework.example.graalvm.crud.r2dbc; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; - -import java.util.Collections; -import java.util.Map; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.Mockito; -import reactor.core.publisher.Mono; -import io.koraframework.example.graalvm.crud.openapi.server.model.CategoryCreateTO; -import io.koraframework.example.graalvm.crud.openapi.server.model.PetCreateTO; -import io.koraframework.example.graalvm.crud.openapi.server.model.PetUpdateTO; -import io.koraframework.example.graalvm.crud.r2dbc.repository.CategoryRepository; -import io.koraframework.example.graalvm.crud.r2dbc.repository.PetRepository; -import io.koraframework.example.graalvm.crud.r2dbc.service.PetCache; -import io.koraframework.example.graalvm.crud.r2dbc.service.PetService; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@KoraAppTest(Application.class) -class ComponentTests implements KoraAppTestConfigModifier { - - @Mock - @TestComponent - private PetCache petCache; - @Mock - @TestComponent - private PetRepository petRepository; - @Mock - @TestComponent - private CategoryRepository categoryRepository; - - @TestComponent - private PetService petService; - - @NotNull - @Override - public KoraConfigModification config() { - return KoraConfigModification.ofString(""" - resilient { - circuitbreaker.pet { - slidingWindowSize = 2 - minimumRequiredCalls = 2 - failureRateThreshold = 100 - permittedCallsInHalfOpenState = 1 - waitDurationInOpenState = 15s - } - timeout.pet { - duration = 5000ms - } - retry.pet { - delay = 100ms - attempts = 0 - } - } - """); - } - - @Test - void updatePetWithNewCategoryCreated() { - // given - mockCache(); - mockRepository(Map.of("dog", 1L, "cat", 2L)); - - var added = petService.add(new PetCreateTO("dog", new CategoryCreateTO("dog"))).block(); - assertEquals(1, added.id()); - assertEquals(1, added.category().id()); - - // when - Mockito.when(petRepository.findById(anyLong())).thenReturn(Mono.just(added)); - Mockito.when(petRepository.update(any())).thenReturn(Mono.empty()); - var updated = petService.update(added.id(), - new PetUpdateTO(PetUpdateTO.StatusEnum.PENDING, "cat", new CategoryCreateTO("cat"))) - .blockOptional(); - assertTrue(updated.isPresent()); - assertEquals(1, updated.get().id()); - assertEquals(2, updated.get().category().id()); - - // then - Mockito.verify(petRepository).insert(any()); - Mockito.verify(categoryRepository, Mockito.times(2)).insert(any()); - } - - @Test - void updatePetWithSameCategory() { - // given - mockCache(); - mockRepository(Map.of("dog", 1L)); - - var added = petService.add(new PetCreateTO("dog", new CategoryCreateTO("dog"))).block(); - assertEquals(1, added.id()); - assertEquals(1, added.category().id()); - - // when - Mockito.when(petRepository.findById(anyLong())).thenReturn(Mono.just(added)); - Mockito.when(petRepository.update(any())).thenReturn(Mono.empty()); - Mockito.when(categoryRepository.findByName(any())).thenReturn(Mono.just(added.category())); - var updated = petService.update(added.id(), - new PetUpdateTO(PetUpdateTO.StatusEnum.PENDING, "cat", new CategoryCreateTO("dog"))) - .blockOptional(); - assertTrue(updated.isPresent()); - assertNotEquals(0, updated.get().id()); - assertNotEquals(0, updated.get().category().id()); - - // then - Mockito.verify(petRepository).insert(any()); - Mockito.verify(categoryRepository).insert(any()); - } - - private void mockCache() { - Mockito.when(petCache.get(anyLong())).thenReturn(null); - Mockito.when(petCache.put(anyLong(), any())).then(invocation -> invocation.getArguments()[1]); - Mockito.when(petCache.get(anyCollection())).thenReturn(Collections.emptyMap()); - } - - private void mockRepository(Map categoryNameToId) { - categoryNameToId.forEach((k, v) -> Mockito.when(categoryRepository.insert(k)).thenReturn(Mono.just(v))); - Mockito.when(categoryRepository.findByName(any())).thenReturn(Mono.empty()); - Mockito.when(petRepository.insert(any())).thenReturn(Mono.just(1L)); - Mockito.when(petRepository.findById(anyLong())).thenReturn(Mono.empty()); - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/test/resources/logback-test.xml b/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/test/resources/logback-test.xml deleted file mode 100644 index 499ca299..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/test/resources/logback-test.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - UTF-8 - %cyan(%d{HH:mm:ss.SSS}) %highlight(%-5level) [%thread] %logger{36} - %msg%n - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/Dockerfile b/examples/graalvm/kora-java-graalvm-crud-vertx/Dockerfile deleted file mode 100644 index 26bb3f57..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -FROM ghcr.io/graalvm/native-image-community:21 as builder - -ARG TARGET_DIR=/opt/app -ARG SOURCE_DIR=build/libs -WORKDIR $TARGET_DIR - -COPY $SOURCE_DIR/*-all.jar $TARGET_DIR/application.jar - -RUN native-image --no-fallback -classpath $TARGET_DIR/application.jar - -FROM ubuntu:noble-20240212 as runner - -ARG TARGET_DIR=/opt/app -WORKDIR $TARGET_DIR - -COPY --from=builder $TARGET_DIR/application $TARGET_DIR/application - -ARG DOCKER_USER=app -RUN groupadd -r $DOCKER_USER && useradd -rg $DOCKER_USER $DOCKER_USER -RUN chmod +x application -USER $DOCKER_USER - -EXPOSE 8080/tcp -EXPOSE 8085/tcp -CMD "/opt/app/application" \ No newline at end of file diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/README.md b/examples/graalvm/kora-java-graalvm-crud-vertx/README.md deleted file mode 100644 index c48fbc00..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/README.md +++ /dev/null @@ -1,88 +0,0 @@ -# Kora Java GraalVM CRUD Vertx Service - -Пример сервиса реализованного на Kora с упрощенным HTTP [CRUD](https://github.com/swagger-api/swagger-petstore) API -и использованием [GraalVM Native Image](https://www.graalvm.org/latest/reference-manual/native-image/), -в качестве базы данных выступает Postgres, используется кэш Caffeine, а также другие модули которые использовались бы в реальном приложении в бою. - -В примере использовались модули: -- [HTTP Server](https://kora-projects.github.io/kora-docs/ru/documentation/http-server/) -- [HTTP Server OpenAPI Generation](https://kora-projects.github.io/kora-docs/ru/documentation/openapi-codegen/) -- [Probes](https://kora-projects.github.io/kora-docs/ru/documentation/probes/) -- [Metrics](https://kora-projects.github.io/kora-docs/ru/documentation/metrics/) -- [Database Vertx](https://kora-projects.github.io/kora-docs/ru/documentation/database-vertx/) -- [JSON](https://kora-projects.github.io/kora-docs/ru/documentation/json/) -- [Resilient](https://kora-projects.github.io/kora-docs/ru/documentation/resilient/) -- [Validation](https://kora-projects.github.io/kora-docs/ru/documentation/validation/) -- [Cache Caffeine](https://kora-projects.github.io/kora-docs/ru/documentation/cache/#caffeine) - -Скомпилирован с помощью [GraalVM](https://www.graalvm.org/release-notes/JDK_21/) - -## Build - -Собрать классы: - -```shell -./gradlew classes -``` - -Собрать Java артефакт: - -```shell -./gradlew shadowJar -``` - -### Generate - -Сгенерировать API для HTTP Server: -```shell -./gradlew openApiGenerateHttpServer -``` - -### Image - -Собрать образ приложения: -```shell -docker build -t kora-java-graalvm-crud-vertx . -``` - -Получить GraalVM Native Image артефакт: - -```shell -docker run --rm --entrypoint cat kora-java-graalvm-crud-vertx /opt/app/application > application -``` - -## Migration - -Миграции вызываются с помощью Flyway Gradle Plugin: -```shell -./gradlew flywayMigrate -``` - -## Run - -Запустить Java локально: -```shell -./gradlew run -``` - -## Run Docker-Compose - -Требуется сначала собрать артефакт: - -```shell -./gradlew shadowJar -``` - -Запустить как docker-compose: -```shell -docker-compose up -``` - -## Test - -Тесты используют [Testcontainers](https://java.testcontainers.org/), требуется [Docker](https://docs.docker.com/engine/install/) окружение для запуска тестов или аналогичные контейнерные окружения ([colima](https://github.com/abiosoft/colima) / итп) - -Протестировать локально: -```shell -./gradlew test -``` diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/build.gradle b/examples/graalvm/kora-java-graalvm-crud-vertx/build.gradle deleted file mode 100644 index 7cfde40a..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/build.gradle +++ /dev/null @@ -1,189 +0,0 @@ -import org.openapitools.generator.gradle.plugin.tasks.GenerateTask - -buildscript { - repositories { - mavenLocal() - mavenCentral() - } - dependencies { - classpath("io.koraframework:openapi-generator:$koraVersion") - } -} - -plugins { - id "java" - id "jacoco" - id "application" - - id "org.openapi.generator" version "7.23.0" - id "org.flywaydb.flyway" version "8.4.2" - id "com.gradleup.shadow" version "9.4.1" - id "org.graalvm.buildtools.native" -} - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(25) - vendor = JvmVendorSpec.ADOPTIUM - } -} - -configurations { - koraBom - annotationProcessor.extendsFrom(koraBom); compileOnly.extendsFrom(koraBom); implementation.extendsFrom(koraBom) - api.extendsFrom(koraBom); testImplementation.extendsFrom(koraBom); testAnnotationProcessor.extendsFrom(koraBom) -} - -dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") - annotationProcessor "org.mapstruct:mapstruct-processor:1.5.5.Final" - annotationProcessor "io.koraframework:annotation-processors" - annotationProcessor "io.goodforgod:graalvm-hint-processor:1.2.0" - compileOnly "io.goodforgod:graalvm-hint-annotations:1.2.0" - compileOnly "org.postgresql:postgresql:42.7.7" - - implementation "io.koraframework:http-server-undertow" - implementation "io.koraframework:database-vertx" - implementation "io.koraframework:micrometer-module" - implementation "io.koraframework:json-common" - implementation "io.koraframework:validation-module" - implementation "io.koraframework:cache-caffeine" - implementation "io.koraframework:resilient-kora" - implementation "io.koraframework:config-hocon" - implementation "io.koraframework:openapi-management" - implementation "io.koraframework:logging-logback" - - implementation "org.mapstruct:mapstruct:1.5.5.Final" - implementation "io.vertx:vertx-pg-client:4.3.8" - implementation "com.ongres.scram:client:2.1" - implementation "io.projectreactor:reactor-core:3.6.18" // For reactive examples (optional) - - testImplementation "org.json:json:20231013" - testImplementation "org.skyscreamer:jsonassert:1.5.1" - testImplementation "org.postgresql:postgresql:42.7.7" - - testImplementation "org.mockito:mockito-core:5.18.0" - testImplementation "io.koraframework:test-junit5" - testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.13.1" - testImplementation "org.testcontainers:junit-jupiter:1.21.4" -} - -application { - mainClass = "io.koraframework.example.graalvm.crud.vertx.Application" -} - -def openApiGenerateHttpServer = tasks.register("openApiGenerateHttpServer", GenerateTask) { - generatorName = "kora" - group = "openapi tools" - inputSpec = layout.projectDirectory.file("src/main/resources/openapi/http-server.yaml") - outputDir = layout.buildDirectory.dir("generated/openapi") - def corePackage = "io.koraframework.example.graalvm.crud.openapi.server" - apiPackage = "${corePackage}.api" - modelPackage = "${corePackage}.model" - invokerPackage = "${corePackage}.invoker" - configOptions = [ - mode : "java-server", // так же есть java-server вариация HTTP Server"а - enableServerValidation: "true" - ] -} -sourceSets.main { java.srcDirs += openApiGenerateHttpServer.get().outputDir } -compileJava.dependsOn openApiGenerateHttpServer - -graalvmNative { - binaries { - main { - imageName = "$project.name" - mainClass = "$application.mainClass" - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(25) - vendor = JvmVendorSpec.matching("GraalVM Community") - } - } - } - metadataRepository { - enabled = true - } -} - -processResources.dependsOn tasks.collectReachabilityMetadata -sourceSets.main { resources.srcDirs += "$buildDir/native-reachability-metadata" } - -//noinspection GroovyAssignabilityCheck -run { - environment([ - "POSTGRES_VERTX_URL": "postgresql://$postgresHost:$postgresPort/$postgresDatabase", - "POSTGRES_USER" : "$postgresUser", - "POSTGRES_PASS" : "$postgresPassword", - ]) -} - -jar.enabled = false -shadowJar { - mergeServiceFiles() - manifest { - attributes "Main-Class": application.mainClass - attributes "Implementation-Version": koraVersion - } -} - -assemble.dependsOn shadowJar - -test { - dependsOn tasks.shadowJar - - jvmArgs += [ - "-XX:+TieredCompilation", - "-XX:TieredStopAtLevel=1", - ] - - environment([ - "": "" - ]) - - useJUnitPlatform() - testLogging { - showStandardStreams = true - events("passed", "skipped", "failed") - exceptionFormat = "full" - } - - exclude("**/\$*") - - jacoco { - excludes += ["**/generated/**", "**/Application*", "**/\$*"] - } - - reports { - html.required = false - junitXml.required = false - } -} - -flyway { - url = "jdbc:postgresql://$postgresHost:$postgresPort/$postgresDatabase" - user = "$postgresUser" - password = "$postgresPassword" - locations = ["classpath:db/migration"] -} - -compileJava { - options.encoding = "UTF-8" - options.incremental = true - options.fork = false -} - -check.dependsOn jacocoTestReport -jacocoTestReport { - reports { - xml.required = true - html.outputLocation = layout.buildDirectory.dir("jacocoHtml") - } - classDirectories = files(classDirectories.files.collect { fileTree(dir: it, excludes: test.jacoco.excludes) }) -} - -javadoc { - options.encoding = "UTF-8" - if (JavaVersion.current().isJava9Compatible()) { - options.addBooleanOption("html5", true) - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/docker-compose.yml b/examples/graalvm/kora-java-graalvm-crud-vertx/docker-compose.yml deleted file mode 100644 index 5b0e00ac..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/docker-compose.yml +++ /dev/null @@ -1,38 +0,0 @@ -services: - postgres: - image: postgres:16.4-alpine - restart: unless-stopped - ports: - - '5432:5432' - environment: - POSTGRES_DATABASE: postgres - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - - - flyway: - image: flyway/flyway:10.2-alpine - restart: no - command: -url=jdbc:postgresql://postgres:5432/postgres -schemas=public -user=postgres -password=postgres -connectRetries=60 migrate - volumes: - - ./src/main/resources/db/migration:/flyway/sql - depends_on: - - postgres - - -# Можно закомментировать секцию application и использовать docker-compose -# как окружения для запуска сервиса через gradle run - application: - image: kora-java-graalvm-crud-vertx - build: . - restart: unless-stopped - ports: - - '8080:8080' - - '8085:8085' - environment: - POSTGRES_VERTX_URL: jdbc:postgresql://postgres:5432/postgres - POSTGRES_USER: postgres - POSTGRES_PASS: postgres - depends_on: - - postgres - - flyway diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/gradle.properties b/examples/graalvm/kora-java-graalvm-crud-vertx/gradle.properties deleted file mode 100644 index 8e7a5091..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/gradle.properties +++ /dev/null @@ -1,6 +0,0 @@ -##### POSTGRES ##### -postgresHost=localhost -postgresPort=5432 -postgresUser=postgres -postgresPassword=postgres -postgresDatabase=postgres diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/Application.java b/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/Application.java deleted file mode 100644 index 48b27a67..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/Application.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.koraframework.example.graalvm.crud.vertx; - -import io.goodforgod.graalvm.hint.annotation.NativeImageHint; -import io.goodforgod.graalvm.hint.annotation.ResourceHint; -import io.koraframework.application.graph.KoraApplication; -import io.koraframework.cache.caffeine.CaffeineCacheModule; -import io.koraframework.common.annotation.KoraApp; -import io.koraframework.config.hocon.HoconConfigModule; -import io.koraframework.database.vertx.VertxDatabaseModule; -import io.koraframework.http.server.undertow.UndertowPublicHttpServerModule; -import io.koraframework.json.common.JsonModule; -import io.koraframework.logging.logback.LogbackModule; -import io.koraframework.micrometer.module.MetricsModule; -import io.koraframework.openapi.management.OpenApiManagementModule; -import io.koraframework.resilient.ResilientModule; -import io.koraframework.validation.module.ValidationModule; - -@ResourceHint(include = { "openapi/http-server.yaml" }) -@NativeImageHint(name = "application", entrypoint = Application.class) -@KoraApp -public interface Application extends - HoconConfigModule, - LogbackModule, - VertxDatabaseModule, - ValidationModule, - JsonModule, - CaffeineCacheModule, - ResilientModule, - MetricsModule, - OpenApiManagementModule, - UndertowPublicHttpServerModule { - - static void main(String[] args) { - KoraApplication.run(ApplicationGraph::graph); - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/controller/HttpExceptionHandler.java b/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/controller/HttpExceptionHandler.java deleted file mode 100644 index f1f329fc..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/controller/HttpExceptionHandler.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.koraframework.example.graalvm.crud.vertx.controller; - -import io.micrometer.core.instrument.config.validate.ValidationException; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.TimeoutException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import io.koraframework.common.annotation.Component; -import io.koraframework.common.Context; -import io.koraframework.common.annotation.Tag; -import io.koraframework.example.graalvm.crud.openapi.server.model.MessageTO; -import io.koraframework.http.common.body.HttpBody; -import io.koraframework.http.server.common.*; -import io.koraframework.json.common.JsonWriter; - -@Tag(HttpServerModule.class) -@Component -public final class HttpExceptionHandler implements HttpServerInterceptor { - - private static final Logger logger = LoggerFactory.getLogger(HttpExceptionHandler.class); - - private final JsonWriter errorJsonWriter; - - public HttpExceptionHandler(JsonWriter errorJsonWriter) { - this.errorJsonWriter = errorJsonWriter; - } - - @Override - public CompletionStage intercept(Context context, HttpServerRequest request, InterceptChain chain) - throws Exception { - return chain.process(context, request).exceptionally(e -> { - if (e instanceof HttpServerResponseException ex) { - return ex; - } - - e.printStackTrace(); - var body = HttpBody.json(errorJsonWriter.toByteArrayUnchecked(new MessageTO(e.getMessage()))); - if (e instanceof IllegalArgumentException || e instanceof ValidationException) { - return HttpServerResponse.of(400, body); - } else if (e instanceof TimeoutException) { - return HttpServerResponse.of(408, body); - } else { - logger.error("Request '{} {}' failed", request.method(), request.path(), e); - return HttpServerResponse.of(500, body); - } - }); - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/controller/PetDelegate.java b/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/controller/PetDelegate.java deleted file mode 100644 index 533060d9..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/controller/PetDelegate.java +++ /dev/null @@ -1,93 +0,0 @@ -package io.koraframework.example.graalvm.crud.vertx.controller; - -import static io.koraframework.example.graalvm.crud.openapi.server.api.PetApiResponses.*; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Mono; -import io.koraframework.common.annotation.Component; -import io.koraframework.example.graalvm.crud.openapi.server.api.PetApiDelegate; -import io.koraframework.example.graalvm.crud.openapi.server.model.MessageTO; -import io.koraframework.example.graalvm.crud.openapi.server.model.PetCreateTO; -import io.koraframework.example.graalvm.crud.openapi.server.model.PetUpdateTO; -import io.koraframework.example.graalvm.crud.vertx.model.mapper.PetMapper; -import io.koraframework.example.graalvm.crud.vertx.service.PetService; - -@Component -public final class PetDelegate implements PetApiDelegate { - - private final PetMapper petMapper; - private final PetService petService; - - public PetDelegate(PetMapper petMapper, PetService petService) { - this.petMapper = petMapper; - this.petService = petService; - } - - @Override - public Mono getPetById(long petId) { - if (petId < 0) { - return Mono.just(new GetPetByIdApiResponse.GetPetById400ApiResponse(malformedId(petId))); - } - - return petService.findByID(petId) - .map(pet -> { - var body = petMapper.asDTO(pet); - return ((GetPetByIdApiResponse) new GetPetByIdApiResponse.GetPetById200ApiResponse(body)); - }) - .switchIfEmpty(Mono.fromSupplier(() -> new GetPetByIdApiResponse.GetPetById404ApiResponse(notFound(petId)))); - } - - @Override - public Mono addPet(PetCreateTO petCreateTO) { - Logger logger = LoggerFactory.getLogger(getClass()); - return petService.add(petCreateTO) - .doOnSuccess(r -> { - if (r == null) { - logger.warn("INSERT NULL"); - } else { - logger.warn("INSERT NOT NULL"); - } - }) - .doOnError(e -> { logger.warn("ERROR - " + e); }) - .map(pet -> { - var body = petMapper.asDTO(pet); - return new AddPetApiResponse.AddPet200ApiResponse(body); - }); - } - - @Override - public Mono updatePet(long petId, PetUpdateTO petUpdateTO) { - if (petId < 0) { - return Mono.just(new UpdatePetApiResponse.UpdatePet400ApiResponse(malformedId(petId))); - } - - return petService.update(petId, petUpdateTO) - .map(pet -> { - var body = petMapper.asDTO(pet); - return ((UpdatePetApiResponse) new UpdatePetApiResponse.UpdatePet200ApiResponse(body)); - }) - .switchIfEmpty(Mono.fromSupplier(() -> new UpdatePetApiResponse.UpdatePet404ApiResponse(notFound(petId)))); - } - - @Override - public Mono deletePet(long petId) { - if (petId < 0) { - return Mono.just(new DeletePetApiResponse.DeletePet400ApiResponse(malformedId(petId))); - } - - return petService.delete(petId) - .map(isDeleted -> (isDeleted) - ? new DeletePetApiResponse.DeletePet200ApiResponse( - new MessageTO("Successfully deleted pet with ID: " + petId)) - : new DeletePetApiResponse.DeletePet404ApiResponse(notFound(petId))); - } - - private static MessageTO notFound(long petId) { - return new MessageTO("Pet not found for ID: " + petId); - } - - private static MessageTO malformedId(long petId) { - return new MessageTO("Pet malformed ID: " + petId); - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/model/dao/Pet.java b/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/model/dao/Pet.java deleted file mode 100644 index 799db874..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/model/dao/Pet.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.koraframework.example.graalvm.crud.vertx.model.dao; - -import io.koraframework.database.common.annotation.Column; -import io.koraframework.database.common.annotation.Id; -import io.koraframework.database.common.annotation.Table; - -@Table("pets") -public record Pet(@Id @Column("id") long id, - @Column("name") String name, - @Column("status") Status status, - @Column("category_id") long categoryId) { - - public enum Status { - - AVAILABLE(0), - PENDING(10), - SOLD(20); - - public final int code; - - Status(int code) { - this.code = code; - } - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/model/dao/PetCategory.java b/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/model/dao/PetCategory.java deleted file mode 100644 index 9b0ddc79..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/model/dao/PetCategory.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.koraframework.example.graalvm.crud.vertx.model.dao; - -import io.koraframework.database.common.annotation.Id; -import io.koraframework.database.common.annotation.Table; - -@Table("categories") -public record PetCategory(@Id long id, - String name) {} diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/model/dao/PetWithCategory.java b/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/model/dao/PetWithCategory.java deleted file mode 100644 index 017d01ee..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/model/dao/PetWithCategory.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.koraframework.example.graalvm.crud.vertx.model.dao; - -import io.koraframework.database.common.annotation.Column; -import io.koraframework.database.common.annotation.Embedded; - -public record PetWithCategory(@Column("id") long id, - @Column("name") String name, - @Column("status") Pet.Status status, - @Embedded("category_") PetCategory category) { - - public Pet getPet() { - return new Pet(id, name, status, category.id()); - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/model/mapper/PetMapper.java b/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/model/mapper/PetMapper.java deleted file mode 100644 index a57c26dc..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/model/mapper/PetMapper.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.koraframework.example.graalvm.crud.vertx.model.mapper; - -import org.mapstruct.Mapper; -import org.mapstruct.ReportingPolicy; -import io.koraframework.example.graalvm.crud.openapi.server.model.CategoryTO; -import io.koraframework.example.graalvm.crud.openapi.server.model.PetTO; -import io.koraframework.example.graalvm.crud.vertx.model.dao.PetCategory; -import io.koraframework.example.graalvm.crud.vertx.model.dao.PetWithCategory; - -@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE) -public interface PetMapper { - - PetTO asDTO(PetWithCategory pet); - - CategoryTO asDTO(PetCategory category); -} diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/repository/CategoryRepository.java b/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/repository/CategoryRepository.java deleted file mode 100644 index 333d894f..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/repository/CategoryRepository.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.koraframework.example.graalvm.crud.vertx.repository; - -import reactor.core.publisher.Mono; -import io.koraframework.database.common.UpdateCount; -import io.koraframework.database.common.annotation.Query; -import io.koraframework.database.common.annotation.Repository; -import io.koraframework.database.vertx.VertxRepository; -import io.koraframework.example.graalvm.crud.vertx.model.dao.PetCategory; - -@Repository -public interface CategoryRepository extends VertxRepository { - - @Query("SELECT %{return#selects} FROM %{return#table} WHERE name = :name") - Mono findByName(String name); - - @Query("INSERT INTO categories(name) VALUES (:categoryName) RETURNING id") - Mono insert(String categoryName); - - @Query("DELETE FROM categories WHERE id = :id") - Mono deleteById(long id); -} diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/repository/PetRepository.java b/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/repository/PetRepository.java deleted file mode 100644 index ff92377c..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/repository/PetRepository.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.koraframework.example.graalvm.crud.vertx.repository; - -import reactor.core.publisher.Mono; -import io.koraframework.database.common.UpdateCount; -import io.koraframework.database.common.annotation.Query; -import io.koraframework.database.common.annotation.Repository; -import io.koraframework.database.vertx.VertxRepository; -import io.koraframework.example.graalvm.crud.vertx.model.dao.Pet; -import io.koraframework.example.graalvm.crud.vertx.model.dao.PetWithCategory; - -@Repository -public interface PetRepository extends VertxRepository { - - @Query(""" - SELECT p.id, p.name, p.status, p.category_id, c.name as category_name - FROM pets p - JOIN categories c on c.id = p.category_id - WHERE p.id = :id - """) - Mono findById(long id); - - @Query("INSERT INTO %{entity#inserts -= id} RETURNING id") - Mono insert(Pet entity); - - @Query("UPDATE %{entity#table} SET %{entity#updates} WHERE %{entity#where = @id}") - Mono update(Pet entity); - - @Query("DELETE FROM pets WHERE id = :id") - Mono deleteById(long id); -} diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/repository/mapper/PetStatusParameterMapper.java b/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/repository/mapper/PetStatusParameterMapper.java deleted file mode 100644 index a721f1ca..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/repository/mapper/PetStatusParameterMapper.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.koraframework.example.graalvm.crud.vertx.repository.mapper; - -import org.jspecify.annotations.Nullable; -import io.koraframework.common.annotation.Component; -import io.koraframework.database.vertx.mapper.parameter.VertxParameterColumnMapper; -import io.koraframework.example.graalvm.crud.vertx.model.dao.Pet; - -@Component -public final class PetStatusParameterMapper implements VertxParameterColumnMapper { - - @Override - public Object apply(@Nullable Pet.Status value) { - if (value == null) { - return null; - } else { - return value.code; - } - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/repository/mapper/PetStatusResultMapper.java b/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/repository/mapper/PetStatusResultMapper.java deleted file mode 100644 index c2eb25dc..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/repository/mapper/PetStatusResultMapper.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.koraframework.example.graalvm.crud.vertx.repository.mapper; - -import io.koraframework.common.annotation.Component; -import io.koraframework.database.vertx.mapper.result.VertxResultColumnMapper; -import io.koraframework.example.graalvm.crud.vertx.model.dao.Pet; - -@Component -public final class PetStatusResultMapper implements VertxResultColumnMapper { - - private final Pet.Status[] statuses = Pet.Status.values(); - - @Override - public Pet.Status apply(io.vertx.sqlclient.Row row, int index) { - final int code = row.get(Integer.class, index); - for (Pet.Status status : statuses) { - if (code == status.code) { - return status; - } - } - - throw new IllegalStateException("Unknown code: " + code); - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/service/PetCache.java b/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/service/PetCache.java deleted file mode 100644 index 6eb2f5bb..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/service/PetCache.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.koraframework.example.graalvm.crud.vertx.service; - -import io.koraframework.cache.annotation.Cache; -import io.koraframework.cache.caffeine.CaffeineCache; -import io.koraframework.example.graalvm.crud.vertx.model.dao.PetWithCategory; - -@Cache("pet-cache") -public interface PetCache extends CaffeineCache { - -} diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/service/PetService.java b/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/service/PetService.java deleted file mode 100644 index c4aada08..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/java/io/koraframework/example/graalvm/crud/vertx/service/PetService.java +++ /dev/null @@ -1,91 +0,0 @@ -package io.koraframework.example.graalvm.crud.vertx.service; - -import reactor.core.publisher.Mono; -import io.koraframework.cache.annotation.CacheInvalidate; -import io.koraframework.cache.annotation.CachePut; -import io.koraframework.cache.annotation.Cacheable; -import io.koraframework.common.annotation.Component; -import io.koraframework.example.graalvm.crud.openapi.server.model.PetCreateTO; -import io.koraframework.example.graalvm.crud.openapi.server.model.PetUpdateTO; -import io.koraframework.example.graalvm.crud.vertx.model.dao.Pet; -import io.koraframework.example.graalvm.crud.vertx.model.dao.PetCategory; -import io.koraframework.example.graalvm.crud.vertx.model.dao.PetWithCategory; -import io.koraframework.example.graalvm.crud.vertx.repository.CategoryRepository; -import io.koraframework.example.graalvm.crud.vertx.repository.PetRepository; -import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreaker; -import io.koraframework.resilient.retry.annotation.Retry; -import io.koraframework.resilient.timeout.annotation.Timeout; - -@Component -public class PetService { - - private final PetRepository petRepository; - private final CategoryRepository categoryRepository; - - public PetService(PetRepository petRepository, CategoryRepository categoryRepository) { - this.petRepository = petRepository; - this.categoryRepository = categoryRepository; - } - - @Cacheable(PetCache.class) - @CircuitBreaker("pet") - @Retry("pet") - @Timeout("pet") - public Mono findByID(long petId) { - return petRepository.findById(petId); - } - - @CircuitBreaker("pet") - @Timeout("pet") - public Mono add(PetCreateTO createTO) { - return categoryRepository.findByName(createTO.category().name()) - .map(PetCategory::id) - .switchIfEmpty(categoryRepository.insert(createTO.category().name())) - .flatMap(categoryId -> { - var pet = new Pet(0, createTO.name(), Pet.Status.AVAILABLE, categoryId); - return petRepository.insert(pet) - .map(petId -> new PetWithCategory(petId, pet.name(), pet.status(), - new PetCategory(categoryId, createTO.category().name()))); - }); - } - - @CircuitBreaker("pet") - @Timeout("pet") - @CachePut(value = PetCache.class, args = "id") - public Mono update(long id, PetUpdateTO updateTO) { - return petRepository.findById(id) - .flatMap(existingPet -> { - var categoryMono = Mono.just(existingPet.category()); - if (updateTO.category() != null) { - categoryMono = categoryRepository.findByName(updateTO.category().name()) - .switchIfEmpty(Mono.defer(() -> categoryRepository.insert(updateTO.category().name()) - .map(newCategoryId -> new PetCategory(newCategoryId, updateTO.category().name())))); - } - - var status = (updateTO.status() == null) - ? existingPet.status() - : toStatus(updateTO.status()); - - return categoryMono - .flatMap(category -> { - var result = new PetWithCategory(existingPet.id(), updateTO.name(), status, category); - return petRepository.update(result.getPet()).then(Mono.just(result)); - }); - }); - } - - @CircuitBreaker("pet") - @Timeout("pet") - @CacheInvalidate(PetCache.class) - public Mono delete(long petId) { - return petRepository.deleteById(petId).map(counter -> counter.value() == 1); - } - - private static Pet.Status toStatus(PetUpdateTO.StatusEnum statusEnum) { - return switch (statusEnum) { - case AVAILABLE -> Pet.Status.AVAILABLE; - case PENDING -> Pet.Status.PENDING; - case SOLD -> Pet.Status.SOLD; - }; - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/resources/META-INF/native-image/ru.tinkoff.kora.examples/logback/reflect-config.json b/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/resources/META-INF/native-image/ru.tinkoff.kora.examples/logback/reflect-config.json deleted file mode 100644 index fe31c4e4..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/resources/META-INF/native-image/ru.tinkoff.kora.examples/logback/reflect-config.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "name": "io.koraframework.logging.logback.ConsoleTextRecordEncoder", - "allDeclaredConstructors": true, - "allPublicMethods": true - }, - { - "name": "io.koraframework.logging.logback.KoraAsyncAppender", - "allDeclaredConstructors": true, - "allPublicMethods": true - }, - { - "name": "ch.qos.logback.core.status.NopStatusListener", - "allDeclaredConstructors": true - } -] diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/resources/META-INF/native-image/ru.tinkoff.kora.examples/logback/resource-config.json b/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/resources/META-INF/native-image/ru.tinkoff.kora.examples/logback/resource-config.json deleted file mode 100644 index bd2d3ecc..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/resources/META-INF/native-image/ru.tinkoff.kora.examples/logback/resource-config.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "resources": { - "includes": [ - { - "pattern": "\\Qlogback.xml\\E" - }, - { - "pattern": "\\Qapplication.conf\\E" - } - ] - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/resources/application.conf b/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/resources/application.conf deleted file mode 100644 index 34efaa7a..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/resources/application.conf +++ /dev/null @@ -1,80 +0,0 @@ -httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 - telemetry.logging.enabled = true -} - - -db { - connectionUri = ${POSTGRES_VERTX_URL} - username = ${POSTGRES_USER} - password = ${POSTGRES_PASS} - maxPoolSize = 10 - poolName = "kora" - initializationFailTimeout = "10s" - telemetry.logging.enabled = true -} - - -pet-cache { - maximumSize = 1000 - maximumSize = ${?CACHE_MAX_SIZE} - expireAfterWrite = ${?CACHE_EXPIRE_WRITE} -} - - -openapi { - management { - enabled = true - file = "openapi/http-server.yaml" - swaggerui { - enabled = true - } - rapidoc { - enabled = true - } - } -} - - -resilient { - circuitbreaker.pet { - slidingWindowSize = 50 - minimumRequiredCalls = 25 - failureRateThreshold = 50 - waitDurationInOpenState = "25s" - permittedCallsInHalfOpenState = 10 - } - timeout.pet { - duration = "5000ms" - } - retry.pet { - delay = "500ms" - delayStep = "5s" - attempts = 3 - attempts = ${?RETRY_ATTEMPTS} - } -} - - -logging.level { - "root": "WARN" - "root": ${?LOGGING_LEVEL_ALL} - "root": ${?LOGGING_LEVEL_ROOT} - - "io.koraframework": "INFO" - "io.koraframework": ${?LOGGING_LEVEL_ALL} - "io.koraframework": ${?LOGGING_LEVEL_KORA} - "io.koraframework.http.server": ${?LOGGING_LEVEL_ALL} - "io.koraframework.http.server": ${?LOGGING_LEVEL_KORA_HTTP_SERVER} - "io.koraframework.database": ${?LOGGING_LEVEL_ALL} - "io.koraframework.database": ${?LOGGING_LEVEL_KORA_DATABASE} - "io.koraframework.cache": ${?LOGGING_LEVEL_ALL} - "io.koraframework.cache": ${?LOGGING_LEVEL_KORA_CACHE} - "io.koraframework.resilient": ${?LOGGING_LEVEL_ALL} - "io.koraframework.resilient": ${?LOGGING_LEVEL_KORA_RESILIENT} - - "io.koraframework.example": "INFO" - "io.koraframework.example": ${?LOGGING_LEVEL_ALL} - "io.koraframework.example": ${?LOGGING_LEVEL_APP} -} diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/resources/db/migration/V1__setup-tables.sql b/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/resources/db/migration/V1__setup-tables.sql deleted file mode 100644 index 3e704998..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/resources/db/migration/V1__setup-tables.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE IF NOT EXISTS categories -( - id BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY, - name VARCHAR NOT NULL, - PRIMARY KEY (id) -); - - -CREATE TABLE IF NOT EXISTS pets -( - id BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY, - name VARCHAR NOT NULL, - status SMALLINT NOT NULL, - category_id BIGINT NOT NULL REFERENCES categories(id), - PRIMARY KEY (id) -); diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/resources/openapi/http-server.yaml b/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/resources/openapi/http-server.yaml deleted file mode 100644 index 8d99f499..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/resources/openapi/http-server.yaml +++ /dev/null @@ -1,315 +0,0 @@ -openapi: 3.0.3 -info: - title: Swagger Petstore - OpenAPI 3.0 - description: |- - This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about - Swagger at [https://swagger.io](https://swagger.io). - license: - name: Apache 2.0 - url: https://www.apache.org/licenses/LICENSE-2.0.html - version: 1.0.11 -externalDocs: - description: Find out more about Swagger - url: https://swagger.io -tags: - - name: pet - description: Everything about your Pets - externalDocs: - description: Find out more - url: https://swagger.io -paths: - /v3/pets: - post: - tags: - - pet - summary: Add a new pet to the store - description: Add a new pet to the store - operationId: addPet - requestBody: - required: true - description: Create a new pet in the store - content: - application/json: - schema: - $ref: '#/components/schemas/PetCreateTO' - responses: - '200': - description: Invalid parameters supplier - content: - application/json: - schema: - $ref: '#/components/schemas/PetTO' - '400': - description: Invalid parameters supplier - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '408': - description: Timeout exception - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '429': - description: Too many requests - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - /v3/pets/{id}: - get: - tags: - - pet - summary: Find pet by ID - description: Returns a single pet - operationId: getPetById - parameters: - - name: id - in: path - description: ID of pet to return - required: true - schema: - type: integer - format: int64 - nullable: false - minimum: 1 - responses: - '200': - description: successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/PetTO' - '400': - description: Invalid parameters supplier - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '404': - description: Pet not found - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '408': - description: Timeout exception - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '429': - description: Too many requests - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - put: - tags: - - pet - summary: Update an existing pet - description: Update an existing pet by Id - operationId: updatePet - parameters: - - name: id - in: path - description: Pet id to update - required: true - schema: - type: integer - format: int64 - requestBody: - description: Update an existent pet in the store - content: - application/json: - schema: - $ref: '#/components/schemas/PetUpdateTO' - required: true - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/PetTO' - '400': - description: Invalid parameters supplier - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '404': - description: Pet not found - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '408': - description: Timeout exception - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '429': - description: Too many requests - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - delete: - tags: - - pet - summary: Deletes a pet - description: delete a pet - operationId: deletePet - parameters: - - name: id - in: path - description: Pet id to delete - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '400': - description: Invalid parameters supplier - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '404': - description: Pet not found - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '408': - description: Timeout exception - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '429': - description: Too many requests - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/MessageTO' -components: - schemas: - MessageTO: - type: object - properties: - message: - type: string - CategoryTO: - required: - - id - - name - type: object - properties: - id: - type: integer - format: int64 - example: 1 - name: - type: string - example: Dogs - CategoryCreateTO: - required: - - name - type: object - properties: - name: - type: string - example: Dogs - PetStatusTO: - properties: - status: - type: string - description: pet status in the store - enum: - - available - - pending - - sold - PetTO: - allOf: - - $ref: '#/components/schemas/PetStatusTO' - - type: object - required: - - id - - name - properties: - id: - type: integer - format: int64 - example: 10 - nullable: false - name: - type: string - example: doggie - nullable: false - minLength: 1 - maxLength: 50 - category: - $ref: '#/components/schemas/CategoryTO' - PetCreateTO: - required: - - name - - category - type: object - properties: - name: - type: string - example: doggie - nullable: false - minLength: 1 - maxLength: 50 - category: - $ref: '#/components/schemas/CategoryCreateTO' - PetUpdateTO: - allOf: - - $ref: '#/components/schemas/PetStatusTO' - - type: object - required: - - name - properties: - name: - type: string - example: doggie - nullable: false - minLength: 1 - maxLength: 50 - category: - $ref: '#/components/schemas/CategoryCreateTO' diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/test/java/io/koraframework/example/graalvm/crud/vertx/AppContainer.java b/examples/graalvm/kora-java-graalvm-crud-vertx/src/test/java/io/koraframework/example/graalvm/crud/vertx/AppContainer.java deleted file mode 100644 index ba263bd3..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/test/java/io/koraframework/example/graalvm/crud/vertx/AppContainer.java +++ /dev/null @@ -1,47 +0,0 @@ -package io.koraframework.example.graalvm.crud.vertx; - -import java.net.URI; -import java.nio.file.Paths; -import java.time.Duration; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.images.builder.ImageFromDockerfile; -import org.testcontainers.utility.DockerImageName; - -public final class AppContainer extends GenericContainer { - - private AppContainer() { - super(new ImageFromDockerfile("kora-java-graalvm-crud-r2dbc") - .withDockerfile(Paths.get("Dockerfile").toAbsolutePath())); - } - - private AppContainer(DockerImageName image) { - super(image); - } - - public static AppContainer build() { - final String appImage = System.getenv("IMAGE_KORA_JAVA_GRAALVM_CRUD_R2DBC"); - return (appImage != null && !appImage.isBlank()) - ? new AppContainer(DockerImageName.parse(appImage)) - : new AppContainer(); - } - - @Override - protected void configure() { - super.configure(); - withExposedPorts(8080, 8085); - withStartupTimeout(Duration.ofSeconds(50)); - withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(AppContainer.class))); - waitingFor(Wait.forHttp("/system/readiness").forPort(8085).forStatusCode(200).withStartupTimeout(Duration.ofSeconds(20))); - } - - public int getPort() { - return getMappedPort(8080); - } - - public URI getURI() { - return URI.create(String.format("http://%s:%s", getHost(), getPort())); - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/test/java/io/koraframework/example/graalvm/crud/vertx/BlackBoxTests.java b/examples/graalvm/kora-java-graalvm-crud-vertx/src/test/java/io/koraframework/example/graalvm/crud/vertx/BlackBoxTests.java deleted file mode 100644 index 04360b58..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/test/java/io/koraframework/example/graalvm/crud/vertx/BlackBoxTests.java +++ /dev/null @@ -1,236 +0,0 @@ -package io.koraframework.example.graalvm.crud.vertx; - -import static org.junit.jupiter.api.Assertions.*; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.Network; -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL; -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; -import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; -import java.util.Map; -import org.json.JSONObject; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; - -@TestcontainersPostgreSQL( - network = @Network(shared = true), - mode = ContainerMode.PER_RUN, - migration = @Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD)) -class BlackBoxTests { - - private static final AppContainer container = AppContainer.build() - .withNetwork(org.testcontainers.containers.Network.SHARED); - - @ConnectionPostgreSQL - private JdbcConnection connection; - - @BeforeEach - public void setup(@ConnectionPostgreSQL JdbcConnection connection) { - if (!container.isRunning()) { - var params = connection.paramsInNetwork().orElseThrow(); - container.withEnv(Map.ofEntries( - Map.entry( - "POSTGRES_VERTX_URL", - "postgresql://%s:%s/%s".formatted(params.host(), params.port(), params.database())), - Map.entry("POSTGRES_USER", params.username()), - Map.entry("POSTGRES_PASS", params.password()), - Map.entry("CACHE_MAX_SIZE", "0"), - Map.entry("RETRY_ATTEMPTS", "0"), - Map.entry("LOGGING_LEVEL_KORA", "INFO"), - Map.entry("LOGGING_LEVEL_APP", "DEBUG"), - Map.entry("LOGGING_LEVEL_KORA_HTTP_SERVER", "TRACE"), - Map.entry("LOGGING_LEVEL_KORA_DATABASE", "TRACE"), - Map.entry("LOGGING_LEVEL_KORA_RESILIENT", "TRACE"), - Map.entry("LOGGING_LEVEL_KORA_CACHE", "TRACE"))); - - container.start(); - } - } - - @AfterAll - public static void cleanup() { - container.stop(); - } - - @Test - void addPet() throws Exception { - // given - var httpClient = HttpClient.newHttpClient(); - var requestBody = new JSONObject() - .put("name", "doggie") - .put("category", new JSONObject() - .put("name", "Dogs")); - - // when - var request = HttpRequest.newBuilder() - .POST(HttpRequest.BodyPublishers.ofString(requestBody.toString())) - .uri(container.getURI().resolve("/v3/pets")) - .timeout(Duration.ofSeconds(5)) - .build(); - - var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - assertEquals(200, response.statusCode(), response.body()); - - // then - connection.assertCountsEquals(1, "pets"); - connection.assertCountsEquals(1, "categories"); - var responseBody = new JSONObject(response.body()); - assertNotNull(responseBody.query("/id")); - assertNotEquals(0L, responseBody.query("/id")); - assertNotNull(responseBody.query("/status")); - assertEquals(requestBody.query("/name"), responseBody.query("/name")); - assertNotNull(responseBody.query("/category/id")); - assertEquals(requestBody.query("/category/name"), responseBody.query("/category/name")); - } - - @Test - void getPet() throws Exception { - // given - var httpClient = HttpClient.newHttpClient(); - var createRequestBody = new JSONObject() - .put("name", "doggie") - .put("category", new JSONObject() - .put("name", "Dogs")); - - // when - var createRequest = HttpRequest.newBuilder() - .POST(HttpRequest.BodyPublishers.ofString(createRequestBody.toString())) - .uri(container.getURI().resolve("/v3/pets")) - .timeout(Duration.ofSeconds(5)) - .build(); - - var createResponse = httpClient.send(createRequest, HttpResponse.BodyHandlers.ofString()); - assertEquals(200, createResponse.statusCode(), createResponse.body()); - connection.assertCountsEquals(1, "pets"); - connection.assertCountsEquals(1, "categories"); - var createResponseBody = new JSONObject(createResponse.body()); - - // then - var getRequest = HttpRequest.newBuilder() - .GET() - .uri(container.getURI().resolve("/v3/pets/" + createResponseBody.query("/id"))) - .timeout(Duration.ofSeconds(5)) - .build(); - - var getResponse = httpClient.send(getRequest, HttpResponse.BodyHandlers.ofString()); - assertEquals(200, getResponse.statusCode(), getResponse.body()); - - var getResponseBody = new JSONObject(getResponse.body()); - JSONAssert.assertEquals(createResponseBody.toString(), getResponseBody.toString(), JSONCompareMode.LENIENT); - } - - @Test - void getPetNotFound() throws Exception { - // given - var httpClient = HttpClient.newHttpClient(); - - // when - var getRequest = HttpRequest.newBuilder() - .GET() - .uri(container.getURI().resolve("/v3/pets/1")) - .timeout(Duration.ofSeconds(5)) - .build(); - - // then - var getResponse = httpClient.send(getRequest, HttpResponse.BodyHandlers.ofString()); - assertEquals(404, getResponse.statusCode(), getResponse.body()); - } - - @Test - void updatePet() throws Exception { - // given - var httpClient = HttpClient.newHttpClient(); - var createRequestBody = new JSONObject() - .put("name", "doggie") - .put("category", new JSONObject() - .put("name", "Dogs")); - - var createRequest = HttpRequest.newBuilder() - .POST(HttpRequest.BodyPublishers.ofString(createRequestBody.toString())) - .uri(container.getURI().resolve("/v3/pets")) - .timeout(Duration.ofSeconds(5)) - .build(); - - var createResponse = httpClient.send(createRequest, HttpResponse.BodyHandlers.ofString()); - assertEquals(200, createResponse.statusCode(), createResponse.body()); - connection.assertCountsEquals(1, "pets"); - connection.assertCountsEquals(1, "categories"); - var createResponseBody = new JSONObject(createResponse.body()); - - // when - var updateRequestBody = new JSONObject() - .put("name", "doggie2") - .put("status", "pending") - .put("category", new JSONObject() - .put("name", "Dogs2")); - - var updateRequest = HttpRequest.newBuilder() - .PUT(HttpRequest.BodyPublishers.ofString(updateRequestBody.toString())) - .uri(container.getURI().resolve("/v3/pets/" + createResponseBody.query("/id"))) - .timeout(Duration.ofSeconds(5)) - .build(); - - var updateResponse = httpClient.send(updateRequest, HttpResponse.BodyHandlers.ofString()); - assertEquals(200, updateResponse.statusCode(), updateResponse.body()); - var updateResponseBody = new JSONObject(updateResponse.body()); - - // then - var getRequest = HttpRequest.newBuilder() - .GET() - .uri(container.getURI().resolve("/v3/pets/" + createResponseBody.query("/id"))) - .timeout(Duration.ofSeconds(5)) - .build(); - - var getResponse = httpClient.send(getRequest, HttpResponse.BodyHandlers.ofString()); - assertEquals(200, createResponse.statusCode(), getResponse.body()); - - var getResponseBody = new JSONObject(getResponse.body()); - JSONAssert.assertEquals(updateResponseBody.toString(), getResponseBody.toString(), JSONCompareMode.LENIENT); - } - - @Test - void deletePet() throws Exception { - // given - var httpClient = HttpClient.newHttpClient(); - var createRequestBody = new JSONObject() - .put("name", "doggie") - .put("category", new JSONObject() - .put("name", "Dogs")); - - var createRequest = HttpRequest.newBuilder() - .POST(HttpRequest.BodyPublishers.ofString(createRequestBody.toString())) - .uri(container.getURI().resolve("/v3/pets")) - .timeout(Duration.ofSeconds(5)) - .build(); - - var createResponse = httpClient.send(createRequest, HttpResponse.BodyHandlers.ofString()); - assertEquals(200, createResponse.statusCode(), createResponse.body()); - connection.assertCountsEquals(1, "pets"); - connection.assertCountsEquals(1, "categories"); - var createResponseBody = new JSONObject(createResponse.body()); - - // when - var deleteRequest = HttpRequest.newBuilder() - .DELETE() - .uri(container.getURI().resolve("/v3/pets/" + createResponseBody.query("/id"))) - .timeout(Duration.ofSeconds(5)) - .build(); - - var deleteResponse = httpClient.send(deleteRequest, HttpResponse.BodyHandlers.ofString()); - assertEquals(200, deleteResponse.statusCode(), deleteResponse.body()); - - // then - connection.assertCountsEquals(0, "pets"); - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/test/java/io/koraframework/example/graalvm/crud/vertx/ComponentTests.java b/examples/graalvm/kora-java-graalvm-crud-vertx/src/test/java/io/koraframework/example/graalvm/crud/vertx/ComponentTests.java deleted file mode 100644 index ea0c35f3..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/test/java/io/koraframework/example/graalvm/crud/vertx/ComponentTests.java +++ /dev/null @@ -1,127 +0,0 @@ -package io.koraframework.example.graalvm.crud.vertx; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; - -import java.util.Collections; -import java.util.Map; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.Mockito; -import reactor.core.publisher.Mono; -import io.koraframework.example.graalvm.crud.openapi.server.model.CategoryCreateTO; -import io.koraframework.example.graalvm.crud.openapi.server.model.PetCreateTO; -import io.koraframework.example.graalvm.crud.openapi.server.model.PetUpdateTO; -import io.koraframework.example.graalvm.crud.vertx.repository.CategoryRepository; -import io.koraframework.example.graalvm.crud.vertx.repository.PetRepository; -import io.koraframework.example.graalvm.crud.vertx.service.PetCache; -import io.koraframework.example.graalvm.crud.vertx.service.PetService; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@KoraAppTest(Application.class) -class ComponentTests implements KoraAppTestConfigModifier { - - @Mock - @TestComponent - private PetCache petCache; - @Mock - @TestComponent - private PetRepository petRepository; - @Mock - @TestComponent - private CategoryRepository categoryRepository; - - @TestComponent - private PetService petService; - - @NotNull - @Override - public KoraConfigModification config() { - return KoraConfigModification.ofString(""" - resilient { - circuitbreaker.pet { - slidingWindowSize = 2 - minimumRequiredCalls = 2 - failureRateThreshold = 100 - permittedCallsInHalfOpenState = 1 - waitDurationInOpenState = 15s - } - timeout.pet { - duration = 5000ms - } - retry.pet { - delay = 100ms - attempts = 0 - } - } - """); - } - - @Test - void updatePetWithNewCategoryCreated() { - // given - mockCache(); - mockRepository(Map.of("dog", 1L, "cat", 2L)); - - var added = petService.add(new PetCreateTO("dog", new CategoryCreateTO("dog"))).block(); - assertEquals(1, added.id()); - assertEquals(1, added.category().id()); - - // when - Mockito.when(petRepository.findById(anyLong())).thenReturn(Mono.just(added)); - Mockito.when(petRepository.update(any())).thenReturn(Mono.empty()); - var updated = petService.update(added.id(), - new PetUpdateTO(PetUpdateTO.StatusEnum.PENDING, "cat", new CategoryCreateTO("cat"))) - .blockOptional(); - assertTrue(updated.isPresent()); - assertEquals(1, updated.get().id()); - assertEquals(2, updated.get().category().id()); - - // then - Mockito.verify(petRepository).insert(any()); - Mockito.verify(categoryRepository, Mockito.times(2)).insert(any()); - } - - @Test - void updatePetWithSameCategory() { - // given - mockCache(); - mockRepository(Map.of("dog", 1L)); - - var added = petService.add(new PetCreateTO("dog", new CategoryCreateTO("dog"))).block(); - assertEquals(1, added.id()); - assertEquals(1, added.category().id()); - - // when - Mockito.when(petRepository.findById(anyLong())).thenReturn(Mono.just(added)); - Mockito.when(petRepository.update(any())).thenReturn(Mono.empty()); - Mockito.when(categoryRepository.findByName(any())).thenReturn(Mono.just(added.category())); - var updated = petService.update(added.id(), - new PetUpdateTO(PetUpdateTO.StatusEnum.PENDING, "cat", new CategoryCreateTO("dog"))) - .blockOptional(); - assertTrue(updated.isPresent()); - assertNotEquals(0, updated.get().id()); - assertNotEquals(0, updated.get().category().id()); - - // then - Mockito.verify(petRepository).insert(any()); - Mockito.verify(categoryRepository).insert(any()); - } - - private void mockCache() { - Mockito.when(petCache.get(anyLong())).thenReturn(null); - Mockito.when(petCache.put(anyLong(), any())).then(invocation -> invocation.getArguments()[1]); - Mockito.when(petCache.get(anyCollection())).thenReturn(Collections.emptyMap()); - } - - private void mockRepository(Map categoryNameToId) { - categoryNameToId.forEach((k, v) -> Mockito.when(categoryRepository.insert(k)).thenReturn(Mono.just(v))); - Mockito.when(categoryRepository.findByName(any())).thenReturn(Mono.empty()); - Mockito.when(petRepository.insert(any())).thenReturn(Mono.just(1L)); - Mockito.when(petRepository.findById(anyLong())).thenReturn(Mono.empty()); - } -} diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/test/resources/logback-test.xml b/examples/graalvm/kora-java-graalvm-crud-vertx/src/test/resources/logback-test.xml deleted file mode 100644 index 499ca299..00000000 --- a/examples/graalvm/kora-java-graalvm-crud-vertx/src/test/resources/logback-test.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - UTF-8 - %cyan(%d{HH:mm:ss.SSS}) %highlight(%-5level) [%thread] %logger{36} - %msg%n - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/graalvm/kora-java-graalvm-kafka/Dockerfile b/examples/graalvm/kora-java-graalvm-kafka/Dockerfile index 26bb3f57..501a0108 100644 --- a/examples/graalvm/kora-java-graalvm-kafka/Dockerfile +++ b/examples/graalvm/kora-java-graalvm-kafka/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/graalvm/native-image-community:21 as builder +FROM ghcr.io/graalvm/native-image-community:25 as builder ARG TARGET_DIR=/opt/app ARG SOURCE_DIR=build/libs diff --git a/examples/graalvm/kora-java-graalvm-kafka/README.md b/examples/graalvm/kora-java-graalvm-kafka/README.md index 4e75833f..8b0209ca 100644 --- a/examples/graalvm/kora-java-graalvm-kafka/README.md +++ b/examples/graalvm/kora-java-graalvm-kafka/README.md @@ -7,7 +7,7 @@ - [Kafka](https://kora-projects.github.io/kora-docs/ru/documentation/kafka/) - [JSON](https://kora-projects.github.io/kora-docs/ru/documentation/json/) -Скомпилирован с помощью [GraalVM](https://www.graalvm.org/release-notes/JDK_21/) +Скомпилирован с помощью [GraalVM](https://www.graalvm.org/release-notes/JDK_25/) ## Build diff --git a/examples/graalvm/kora-java-graalvm-kafka/build.gradle b/examples/graalvm/kora-java-graalvm-kafka/build.gradle index 4afdab35..38ef0d87 100644 --- a/examples/graalvm/kora-java-graalvm-kafka/build.gradle +++ b/examples/graalvm/kora-java-graalvm-kafka/build.gradle @@ -21,7 +21,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" annotationProcessor "io.goodforgod:graalvm-hint-processor:1.2.0" compileOnly "io.goodforgod:graalvm-hint-annotations:1.2.0" @@ -36,7 +36,7 @@ dependencies { testImplementation "org.json:json:20231013" testImplementation "org.mockito:mockito-core:5.18.0" testImplementation "io.koraframework:test-junit5" - testImplementation "io.goodforgod:testcontainers-extensions-kafka:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-kafka:0.15.0" testImplementation "org.testcontainers:junit-jupiter:1.21.4" } @@ -47,8 +47,8 @@ application { graalvmNative { binaries { main { - imageName = "$project.name" - mainClass = "$application.mainClass" + imageName = project.name + mainClass = application.mainClass javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) vendor = JvmVendorSpec.matching("GraalVM Community") @@ -70,7 +70,7 @@ run { ]) } -jar.enabled = false +// nativeCompile takes the project's own artifact as its classpath entry, so the plain jar must stay enabled shadowJar { mergeServiceFiles() manifest { @@ -89,10 +89,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/graalvm/kora-java-graalvm-kafka/src/main/java/io/koraframework/example/graalvm/kafka/Application.java b/examples/graalvm/kora-java-graalvm-kafka/src/main/java/io/koraframework/example/graalvm/kafka/Application.java index 5814c5d8..21d9707e 100644 --- a/examples/graalvm/kora-java-graalvm-kafka/src/main/java/io/koraframework/example/graalvm/kafka/Application.java +++ b/examples/graalvm/kora-java-graalvm-kafka/src/main/java/io/koraframework/example/graalvm/kafka/Application.java @@ -4,7 +4,7 @@ import io.koraframework.application.graph.KoraApplication; import io.koraframework.common.annotation.KoraApp; import io.koraframework.config.yaml.YamlConfigModule; -import io.koraframework.http.server.undertow.UndertowModule; +import io.koraframework.http.server.undertow.UndertowSystemHttpServerModule; import io.koraframework.json.common.JsonModule; import io.koraframework.kafka.common.KafkaModule; import io.koraframework.logging.logback.LogbackModule; @@ -16,7 +16,7 @@ public interface Application extends YamlConfigModule, LogbackModule, JsonModule, - UndertowModule, + UndertowSystemHttpServerModule, KafkaModule, MetricsModule { diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/resources/META-INF/native-image/ru.tinkoff.kora.examples/logback/reflect-config.json b/examples/graalvm/kora-java-graalvm-kafka/src/main/resources/META-INF/native-image/io.koraframework.examples/logback/reflect-config.json similarity index 100% rename from examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/resources/META-INF/native-image/ru.tinkoff.kora.examples/logback/reflect-config.json rename to examples/graalvm/kora-java-graalvm-kafka/src/main/resources/META-INF/native-image/io.koraframework.examples/logback/reflect-config.json diff --git a/examples/graalvm/kora-java-graalvm-kafka/src/main/resources/META-INF/native-image/ru.tinkoff.kora.examples/logback/resource-config.json b/examples/graalvm/kora-java-graalvm-kafka/src/main/resources/META-INF/native-image/io.koraframework.examples/logback/resource-config.json similarity index 100% rename from examples/graalvm/kora-java-graalvm-kafka/src/main/resources/META-INF/native-image/ru.tinkoff.kora.examples/logback/resource-config.json rename to examples/graalvm/kora-java-graalvm-kafka/src/main/resources/META-INF/native-image/io.koraframework.examples/logback/resource-config.json diff --git a/examples/graalvm/kora-java-graalvm-kafka/src/main/resources/META-INF/native-image/ru.tinkoff.kora.examples/logback/reflect-config.json b/examples/graalvm/kora-java-graalvm-kafka/src/main/resources/META-INF/native-image/ru.tinkoff.kora.examples/logback/reflect-config.json deleted file mode 100644 index fe31c4e4..00000000 --- a/examples/graalvm/kora-java-graalvm-kafka/src/main/resources/META-INF/native-image/ru.tinkoff.kora.examples/logback/reflect-config.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "name": "io.koraframework.logging.logback.ConsoleTextRecordEncoder", - "allDeclaredConstructors": true, - "allPublicMethods": true - }, - { - "name": "io.koraframework.logging.logback.KoraAsyncAppender", - "allDeclaredConstructors": true, - "allPublicMethods": true - }, - { - "name": "ch.qos.logback.core.status.NopStatusListener", - "allDeclaredConstructors": true - } -] diff --git a/examples/graalvm/kora-java-graalvm-kafka/src/main/resources/application.yaml b/examples/graalvm/kora-java-graalvm-kafka/src/main/resources/application.yaml index e9ff476a..5934dcb5 100644 --- a/examples/graalvm/kora-java-graalvm-kafka/src/main/resources/application.yaml +++ b/examples/graalvm/kora-java-graalvm-kafka/src/main/resources/application.yaml @@ -19,9 +19,9 @@ kafka: logging: - level: - root: ${?LOGGING_LEVEL_ROOT:WARN} - io.koraframework: ${?LOGGING_LEVEL_KORA:INFO} - io.koraframework.kafka: ${?LOGGING_LEVEL_KORA_KAFKA:INFO} - io.koraframework.example: ${?LOGGING_LEVEL_APP:INFO} - io.koraframework.application.graph.internal.loom.VirtualThreadExecutorHolder: ${?LOGGING_LEVEL_VIRTUAL_THREAD_EXECUTOR:DEBUG} + levels: + root: ${LOGGING_LEVEL_ROOT:WARN} + io.koraframework: ${LOGGING_LEVEL_KORA:INFO} + io.koraframework.kafka: ${LOGGING_LEVEL_KORA_KAFKA:INFO} + io.koraframework.example: ${LOGGING_LEVEL_APP:INFO} + io.koraframework.application.graph.internal.loom.VirtualThreadExecutorHolder: ${LOGGING_LEVEL_VIRTUAL_THREAD_EXECUTOR:DEBUG} diff --git a/examples/graalvm/kora-java-graalvm-kafka/src/test/java/io/koraframework/example/graalvm/kafka/AppContainer.java b/examples/graalvm/kora-java-graalvm-kafka/src/test/java/io/koraframework/example/graalvm/kafka/AppContainer.java index e5c219bc..0f2d98e3 100644 --- a/examples/graalvm/kora-java-graalvm-kafka/src/test/java/io/koraframework/example/graalvm/kafka/AppContainer.java +++ b/examples/graalvm/kora-java-graalvm-kafka/src/test/java/io/koraframework/example/graalvm/kafka/AppContainer.java @@ -32,9 +32,14 @@ public static AppContainer build() { protected void configure() { super.configure(); withExposedPorts(8080, 8085); - withStartupTimeout(Duration.ofSeconds(20)); + withStartupTimeout(Duration.ofSeconds(60)); withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(AppContainer.class))); - waitingFor(Wait.forLogMessage(".*Kafka Consumer '.*' first poll.*", 1).withStartupTimeout(Duration.ofSeconds(20))); + // wait on the readiness probe rather than on a log line: the wording of Kora's startup + // messages is not part of its contract, and this example only exposes the system server + waitingFor(Wait.forHttp("/system/readiness") + .forPort(8085) + .forStatusCode(200) + .withStartupTimeout(Duration.ofSeconds(60))); } public int getPort() { diff --git a/examples/graalvm/kora-java-graalvm-kafka/src/test/java/io/koraframework/example/graalvm/kafka/BlackBoxTests.java b/examples/graalvm/kora-java-graalvm-kafka/src/test/java/io/koraframework/example/graalvm/kafka/BlackBoxTests.java index c648fe10..bc4294c3 100644 --- a/examples/graalvm/kora-java-graalvm-kafka/src/test/java/io/koraframework/example/graalvm/kafka/BlackBoxTests.java +++ b/examples/graalvm/kora-java-graalvm-kafka/src/test/java/io/koraframework/example/graalvm/kafka/BlackBoxTests.java @@ -25,9 +25,10 @@ class BlackBoxTests { @BeforeAll public static void setup(@ConnectionKafka KafkaConnection connection) { var params = connection.paramsInNetwork().orElseThrow(); - System.out.println(params); - System.out.println("BOOTS - " + params.bootstrapServers()); container.withEnv(Map.of( + // the broker advertises :9092 to the host and its BROKER listener on :9093 inside + // the network, so a sibling container has to bootstrap on the latter - otherwise it + // connects and then receives metadata pointing back at localhost "KAFKA_BOOTSTRAP", params.bootstrapServers().replace(":9092", ":9093"), "LOGGING_LEVEL_KORA", "INFO", "LOGGING_LEVEL_APP", "DEBUG", diff --git a/examples/java/kora-java-cache-caffeine/build.gradle b/examples/java/kora-java-cache-caffeine/build.gradle index fdd8eccc..bd774f53 100644 --- a/examples/java/kora-java-cache-caffeine/build.gradle +++ b/examples/java/kora-java-cache-caffeine/build.gradle @@ -18,7 +18,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:cache-caffeine" @@ -37,9 +37,6 @@ application { //noinspection GroovyAssignabilityCheck run { - environment([ - "": "" - ]) } distTar { @@ -54,10 +51,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/java/kora-java-cache-redis/build.gradle b/examples/java/kora-java-cache-redis/build.gradle index 32aabac9..8cf9c06b 100644 --- a/examples/java/kora-java-cache-redis/build.gradle +++ b/examples/java/kora-java-cache-redis/build.gradle @@ -18,7 +18,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:cache-redis-lettuce" @@ -27,7 +27,7 @@ dependencies { implementation "io.koraframework:config-hocon" testImplementation "io.koraframework:test-junit5" - testImplementation "io.goodforgod:testcontainers-extensions-redis:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-redis:0.15.0" testImplementation "redis.clients:jedis:4.4.3" } @@ -39,9 +39,6 @@ application { //noinspection GroovyAssignabilityCheck run { - environment([ - "": "" - ]) } distTar { @@ -56,10 +53,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/java/kora-java-camunda-engine/build.gradle b/examples/java/kora-java-camunda-engine/build.gradle index 79f0d489..8bb51e78 100644 --- a/examples/java/kora-java-camunda-engine/build.gradle +++ b/examples/java/kora-java-camunda-engine/build.gradle @@ -18,7 +18,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:http-server-undertow" @@ -33,7 +33,7 @@ dependencies { testImplementation "org.mockito:mockito-core:5.18.0" testImplementation "io.koraframework:test-junit5" - testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.15.0" testRuntimeOnly "com.h2database:h2:2.2.224" testImplementation "org.camunda.bpm:camunda-bpm-assert:7.21.0" testImplementation "org.assertj:assertj-core:3.26.0" @@ -66,10 +66,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/java/kora-java-camunda-engine/src/main/resources/application.conf b/examples/java/kora-java-camunda-engine/src/main/resources/application.conf index f8f963b1..71610687 100644 --- a/examples/java/kora-java-camunda-engine/src/main/resources/application.conf +++ b/examples/java/kora-java-camunda-engine/src/main/resources/application.conf @@ -1,4 +1,4 @@ -db { +jdbc { jdbcUrl = ${POSTGRES_JDBC_URL} username = ${POSTGRES_USER} password = ${POSTGRES_PASS} diff --git a/examples/java/kora-java-camunda-zeebe-worker/build.gradle b/examples/java/kora-java-camunda-zeebe-worker/build.gradle index 466dce6c..418fac75 100644 --- a/examples/java/kora-java-camunda-zeebe-worker/build.gradle +++ b/examples/java/kora-java-camunda-zeebe-worker/build.gradle @@ -24,7 +24,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework.experimental:camunda-zeebe-worker" @@ -34,7 +34,7 @@ dependencies { testImplementation "org.mockito:mockito-core:5.18.0" testImplementation "io.koraframework:test-junit5" - testImplementation "io.camunda:zeebe-process-test-extension-testcontainer:8.8.24" + testImplementation "io.camunda:zeebe-process-test-extension-testcontainer:8.9.14" testImplementation "org.testcontainers:junit-jupiter:1.21.4" } @@ -48,6 +48,7 @@ application { run { environment([ "ZEEBE_GRPC_URL": "http://$zeebeHost:$zeebePort", + "ZEEBE_REST_URL": "http://$zeebeHost:$zeebeRestPort", ]) } @@ -63,10 +64,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/java/kora-java-camunda-zeebe-worker/gradle.properties b/examples/java/kora-java-camunda-zeebe-worker/gradle.properties index f49a78ff..2cea84bd 100644 --- a/examples/java/kora-java-camunda-zeebe-worker/gradle.properties +++ b/examples/java/kora-java-camunda-zeebe-worker/gradle.properties @@ -1,3 +1,4 @@ ##### ZEEBE ##### zeebeHost=localhost zeebePort=26500 +zeebeRestPort=8080 diff --git a/examples/java/kora-java-camunda-zeebe-worker/src/main/java/io/koraframework/example/camunda/zeebe/ProcessScheduler.java b/examples/java/kora-java-camunda-zeebe-worker/src/main/java/io/koraframework/example/camunda/zeebe/ProcessScheduler.java index 9bb5db9c..d2a33bf8 100644 --- a/examples/java/kora-java-camunda-zeebe-worker/src/main/java/io/koraframework/example/camunda/zeebe/ProcessScheduler.java +++ b/examples/java/kora-java-camunda-zeebe-worker/src/main/java/io/koraframework/example/camunda/zeebe/ProcessScheduler.java @@ -1,7 +1,7 @@ package io.koraframework.example.camunda.zeebe; -import io.camunda.zeebe.client.ZeebeClient; -import io.camunda.zeebe.client.api.response.ProcessInstanceEvent; +import io.camunda.client.CamundaClient; +import io.camunda.client.api.response.ProcessInstanceEvent; import java.util.Date; import java.util.UUID; import org.slf4j.Logger; @@ -14,9 +14,9 @@ public final class ProcessScheduler { private static final Logger logger = LoggerFactory.getLogger(ProcessScheduler.class); - private final ZeebeClient client; + private final CamundaClient client; - public ProcessScheduler(ZeebeClient client) { + public ProcessScheduler(CamundaClient client) { this.client = client; } diff --git a/examples/java/kora-java-camunda-zeebe-worker/src/main/java/io/koraframework/example/camunda/zeebe/Step3JobWorker.java b/examples/java/kora-java-camunda-zeebe-worker/src/main/java/io/koraframework/example/camunda/zeebe/Step3JobWorker.java index c73e3add..02ca1b83 100644 --- a/examples/java/kora-java-camunda-zeebe-worker/src/main/java/io/koraframework/example/camunda/zeebe/Step3JobWorker.java +++ b/examples/java/kora-java-camunda-zeebe-worker/src/main/java/io/koraframework/example/camunda/zeebe/Step3JobWorker.java @@ -5,7 +5,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.koraframework.camunda.zeebe.worker.JobContext; -import io.koraframework.camunda.zeebe.worker.JobWorkerException; +import io.koraframework.camunda.zeebe.worker.exception.JobWorkerException; import io.koraframework.camunda.zeebe.worker.annotation.JobVariable; import io.koraframework.camunda.zeebe.worker.annotation.JobWorker; import io.koraframework.common.annotation.Component; diff --git a/examples/java/kora-java-camunda-zeebe-worker/src/main/java/io/koraframework/example/camunda/zeebe/WorkerUtils.java b/examples/java/kora-java-camunda-zeebe-worker/src/main/java/io/koraframework/example/camunda/zeebe/WorkerUtils.java index 9b3dae3e..2106f7c8 100644 --- a/examples/java/kora-java-camunda-zeebe-worker/src/main/java/io/koraframework/example/camunda/zeebe/WorkerUtils.java +++ b/examples/java/kora-java-camunda-zeebe-worker/src/main/java/io/koraframework/example/camunda/zeebe/WorkerUtils.java @@ -1,7 +1,5 @@ package io.koraframework.example.camunda.zeebe; -import io.camunda.zeebe.client.api.response.ActivatedJob; -import java.time.Instant; import org.slf4j.Logger; import io.koraframework.camunda.zeebe.worker.JobContext; @@ -9,17 +7,6 @@ public final class WorkerUtils { private WorkerUtils() {} - public static void logJob(Logger logger, ActivatedJob job) { - logger.info(""" - Complete Job - {} - [processKey: {}] - [elementId: {}] - [deadline: {}] - [variables: {}] - """, job.getType(), job.getProcessInstanceKey(), job.getElementId(), - Instant.ofEpochMilli(job.getDeadline()), job.getVariables()); - } - public static void logJob(Logger logger, JobContext job) { logger.info(""" Complete Job - {} diff --git a/examples/java/kora-java-camunda-zeebe-worker/src/main/resources/application.conf b/examples/java/kora-java-camunda-zeebe-worker/src/main/resources/application.conf index d5006366..e5f99aca 100644 --- a/examples/java/kora-java-camunda-zeebe-worker/src/main/resources/application.conf +++ b/examples/java/kora-java-camunda-zeebe-worker/src/main/resources/application.conf @@ -1,6 +1,7 @@ zeebe.client { initializationFailTimeout = "10s" grpc.url = ${ZEEBE_GRPC_URL} + rest.url = ${ZEEBE_REST_URL} deployment.resources = "classpath:bpm" telemetry.logging.enabled = true diff --git a/examples/java/kora-java-camunda-zeebe-worker/src/test/java/io/koraframework/example/bpmn/camunda8/ZeebeMockedTests.java b/examples/java/kora-java-camunda-zeebe-worker/src/test/java/io/koraframework/example/bpmn/camunda8/ZeebeMockedTests.java index 514b8e8c..406ce929 100644 --- a/examples/java/kora-java-camunda-zeebe-worker/src/test/java/io/koraframework/example/bpmn/camunda8/ZeebeMockedTests.java +++ b/examples/java/kora-java-camunda-zeebe-worker/src/test/java/io/koraframework/example/bpmn/camunda8/ZeebeMockedTests.java @@ -20,15 +20,14 @@ import io.koraframework.test.extension.junit5.KoraAppTest; import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; @ZeebeProcessTest @KoraAppTest(value = Application.class, components = KoraZeebeJobWorkerEngine.class) class ZeebeMockedTests implements KoraAppTestConfigModifier { - // Is injected by ZeebeProcessTest + // Is injected by ZeebeProcessTest. zeebe-process-test still exposes the legacy ZeebeClient + // and its assertions accept only its response types; the application itself runs on CamundaClient. @Spy - @TestComponent private ZeebeClient client; // Is injected by ZeebeProcessTest @@ -37,7 +36,9 @@ class ZeebeMockedTests implements KoraAppTestConfigModifier { @NotNull @Override public KoraConfigModification config() { - return KoraConfigModification.ofSystemProperty("ZEEBE_GRPC_URL", client.getConfiguration().getGrpcAddress().toString()); + return KoraConfigModification + .ofSystemProperty("ZEEBE_GRPC_URL", client.getConfiguration().getGrpcAddress().toString()) + .withSystemProperty("ZEEBE_REST_URL", client.getConfiguration().getRestAddress().toString()); } @Test diff --git a/examples/java/kora-java-config-hocon/build.gradle b/examples/java/kora-java-config-hocon/build.gradle index bdbc73d7..fb623a24 100644 --- a/examples/java/kora-java-config-hocon/build.gradle +++ b/examples/java/kora-java-config-hocon/build.gradle @@ -18,7 +18,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:logging-logback" diff --git a/examples/java/kora-java-config-yaml/build.gradle b/examples/java/kora-java-config-yaml/build.gradle index 41ec5814..7df87250 100644 --- a/examples/java/kora-java-config-yaml/build.gradle +++ b/examples/java/kora-java-config-yaml/build.gradle @@ -18,7 +18,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:logging-logback" diff --git a/examples/java/kora-java-crud-submodule/build.gradle b/examples/java/kora-java-crud-submodule/build.gradle index da1056eb..b0a5ad49 100644 --- a/examples/java/kora-java-crud-submodule/build.gradle +++ b/examples/java/kora-java-crud-submodule/build.gradle @@ -16,7 +16,7 @@ subprojects { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" testImplementation "org.json:json:20231013" @@ -32,10 +32,6 @@ subprojects { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() failOnNoDiscoveredTests = false testLogging { diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/build.gradle b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/build.gradle index 799e11fc..df2e0908 100644 --- a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/build.gradle +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/build.gradle @@ -47,7 +47,7 @@ dependencies { // access to flyway migrations in tests testRuntimeOnly project(":examples:java:kora-java-crud-submodule:kora-java-crud-submodule-common") - testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.15.0" testImplementation "org.testcontainers:junit-jupiter:1.21.4" } diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/src/main/java/io/koraframework/example/submodule/app/controller/HttpExceptionHandler.java b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/src/main/java/io/koraframework/example/submodule/app/controller/HttpExceptionHandler.java index 95f20b5c..52c8bc15 100644 --- a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/src/main/java/io/koraframework/example/submodule/app/controller/HttpExceptionHandler.java +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/src/main/java/io/koraframework/example/submodule/app/controller/HttpExceptionHandler.java @@ -1,19 +1,21 @@ package io.koraframework.example.submodule.app.controller; import io.micrometer.core.instrument.config.validate.ValidationException; -import java.util.concurrent.CompletionStage; import java.util.concurrent.TimeoutException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.koraframework.common.annotation.Component; -import io.koraframework.common.Context; import io.koraframework.common.annotation.Tag; import io.koraframework.example.submodule.openapi.http.server.model.MessageTO; import io.koraframework.http.common.body.HttpBody; -import io.koraframework.http.server.common.*; +import io.koraframework.http.server.common.HttpServer; +import io.koraframework.http.server.common.interceptor.HttpServerInterceptor; +import io.koraframework.http.server.common.request.HttpServerRequest; +import io.koraframework.http.server.common.response.HttpServerResponse; +import io.koraframework.http.server.common.response.HttpServerResponseException; import io.koraframework.json.common.JsonWriter; -@Tag(HttpServerModule.class) +@Tag(HttpServer.class) @Component public final class HttpExceptionHandler implements HttpServerInterceptor { @@ -26,14 +28,15 @@ public HttpExceptionHandler(JsonWriter errorJsonWriter) { } @Override - public CompletionStage intercept(Context context, HttpServerRequest request, InterceptChain chain) - throws Exception { - return chain.process(context, request).exceptionally(e -> { + public HttpServerResponse intercept(HttpServerRequest request, InterceptChain chain) { + try { + return chain.process(request); + } catch (Exception e) { if (e instanceof HttpServerResponseException ex) { return ex; } - var body = HttpBody.json(errorJsonWriter.toByteArrayUnchecked(new MessageTO(e.getMessage()))); + var body = HttpBody.json(errorJsonWriter.toByteArray(new MessageTO(e.getMessage()))); if (e instanceof IllegalArgumentException || e instanceof ValidationException) { return HttpServerResponse.of(400, body); } else if (e instanceof TimeoutException) { @@ -42,6 +45,6 @@ public CompletionStage intercept(Context context, HttpServer logger.error("Request '{} {}' failed", request.method(), request.path(), e); return HttpServerResponse.of(500, body); } - }); + } } } diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/src/main/java/io/koraframework/example/submodule/app/controller/PetDelegate.java b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/src/main/java/io/koraframework/example/submodule/app/controller/PetDelegate.java index 37cb094c..d213d67f 100644 --- a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/src/main/java/io/koraframework/example/submodule/app/controller/PetDelegate.java +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/src/main/java/io/koraframework/example/submodule/app/controller/PetDelegate.java @@ -76,8 +76,7 @@ public DeletePetApiResponse deletePet(long petId) { } } - @Nullable - private static Pet.Status toStatus(@Nullable PetUpdateTO.StatusEnum statusEnum) { + private static Pet.@Nullable Status toStatus(PetUpdateTO.@Nullable StatusEnum statusEnum) { if (statusEnum == null) { return null; } diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/src/main/java/io/koraframework/example/submodule/app/controller/VetDelegate.java b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/src/main/java/io/koraframework/example/submodule/app/controller/VetDelegate.java index d1a9bbf7..7af55d37 100644 --- a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/src/main/java/io/koraframework/example/submodule/app/controller/VetDelegate.java +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/src/main/java/io/koraframework/example/submodule/app/controller/VetDelegate.java @@ -7,7 +7,6 @@ import io.koraframework.example.submodule.openapi.http.server.api.VetApiDelegate; import io.koraframework.example.submodule.openapi.http.server.model.MessageTO; import io.koraframework.example.submodule.openapi.http.server.model.VetCreateTO; -import io.koraframework.example.submodule.openapi.http.server.model.VetUpdateTO; import io.koraframework.example.submodule.vet.service.VetService; @Component @@ -54,7 +53,7 @@ public AddVetApiResponse addVet(VetCreateTO vetCreateTO) { } @Override - public UpdateVetApiResponse updateVet(long vetId, VetUpdateTO vetUpdateTO) { + public UpdateVetApiResponse updateVet(long vetId, VetCreateTO vetUpdateTO) { if (vetId < 0) { return new UpdateVetApiResponse.UpdateVet400ApiResponse(malformedId(vetId)); } diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/src/main/resources/application.conf b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/src/main/resources/application.conf index c05167a9..c0babf18 100644 --- a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/src/main/resources/application.conf +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/src/main/resources/application.conf @@ -1,11 +1,11 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } -db { +jdbc { jdbcUrl = ${POSTGRES_JDBC_URL} username = ${POSTGRES_USER} password = ${POSTGRES_PASS} @@ -19,11 +19,11 @@ db { openapi { management { enabled = true - file = "openapi/http-server.yaml" + files = "openapi/http-server.yaml" swaggerui { enabled = true } - rapidoc { + scalar { enabled = true } } diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/src/test/java/io/koraframework/example/submodule/app/IntegrationTests.java b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/src/test/java/io/koraframework/example/submodule/app/IntegrationTests.java index 73d75c41..e2598a77 100644 --- a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/src/test/java/io/koraframework/example/submodule/app/IntegrationTests.java +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app/src/test/java/io/koraframework/example/submodule/app/IntegrationTests.java @@ -64,7 +64,7 @@ class IntegrationTests implements KoraAppTestConfigModifier { @Override public KoraConfigModification config() { return KoraConfigModification.ofString(""" - db { + jdbc { jdbcUrl = ${POSTGRES_JDBC_URL} username = ${POSTGRES_USER} password = ${POSTGRES_PASS} @@ -73,7 +73,8 @@ public KoraConfigModification config() { pet-cache.maximumSize = 0 resilient { circuitbreaker.pet { - slidingWindowSize = 2 + type = FIXED_WINDOW + countBased.windowSize = 2 minimumRequiredCalls = 2 failureRateThreshold = 100 permittedCallsInHalfOpenState = 1 diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-common/build.gradle b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-common/build.gradle index 671b138b..148f747e 100644 --- a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-common/build.gradle +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-common/build.gradle @@ -10,7 +10,7 @@ java { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:common" diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/build.gradle b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/build.gradle index b17f0c66..d34eb523 100644 --- a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/build.gradle +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/build.gradle @@ -20,5 +20,5 @@ dependencies { testImplementation "io.koraframework:config-hocon" - testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.15.0" } diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/java/io/koraframework/example/submodule/pet/model/dao/PetWithCategory.java b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/java/io/koraframework/example/submodule/pet/model/dao/PetWithCategory.java index f19e0a90..e80dd500 100644 --- a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/java/io/koraframework/example/submodule/pet/model/dao/PetWithCategory.java +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/java/io/koraframework/example/submodule/pet/model/dao/PetWithCategory.java @@ -3,11 +3,12 @@ import io.koraframework.database.common.annotation.Column; import io.koraframework.database.common.annotation.Embedded; import io.koraframework.database.jdbc.annotation.EntityJdbc; +import io.koraframework.example.submodule.pet.model.dao.Pet.Status; @EntityJdbc public record PetWithCategory(@Column("id") long id, @Column("name") String name, - @Column("status") Pet.Status status, + @Column("status") Status status, @Embedded("category_") PetCategory category) { public Pet getPet() { diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/java/io/koraframework/example/submodule/pet/service/PetCircuitBreaker.java b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/java/io/koraframework/example/submodule/pet/service/PetCircuitBreaker.java new file mode 100644 index 00000000..a6cb8e7e --- /dev/null +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/java/io/koraframework/example/submodule/pet/service/PetCircuitBreaker.java @@ -0,0 +1,9 @@ +package io.koraframework.example.submodule.pet.service; + +import io.koraframework.resilient.circuitbreaker.CircuitBreaker; +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakerSpec; + +@CircuitBreakerSpec("resilient.circuitbreaker.pet") +public interface PetCircuitBreaker extends CircuitBreaker { + +} diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/java/io/koraframework/example/submodule/pet/service/PetRetry.java b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/java/io/koraframework/example/submodule/pet/service/PetRetry.java new file mode 100644 index 00000000..ec4422aa --- /dev/null +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/java/io/koraframework/example/submodule/pet/service/PetRetry.java @@ -0,0 +1,9 @@ +package io.koraframework.example.submodule.pet.service; + +import io.koraframework.resilient.retry.Retry; +import io.koraframework.resilient.retry.annotation.RetrySpec; + +@RetrySpec("resilient.retry.pet") +public interface PetRetry extends Retry { + +} diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/java/io/koraframework/example/submodule/pet/service/PetService.java b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/java/io/koraframework/example/submodule/pet/service/PetService.java index 277c8004..ff54bbfd 100644 --- a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/java/io/koraframework/example/submodule/pet/service/PetService.java +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/java/io/koraframework/example/submodule/pet/service/PetService.java @@ -11,8 +11,8 @@ import io.koraframework.example.submodule.pet.model.dao.PetWithCategory; import io.koraframework.example.submodule.pet.repository.CategoryRepository; import io.koraframework.example.submodule.pet.repository.PetRepository; -import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreaker; -import io.koraframework.resilient.retry.annotation.Retry; +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakable; +import io.koraframework.resilient.retry.annotation.Retryable; import io.koraframework.resilient.timeout.annotation.Timeout; @Component @@ -27,15 +27,15 @@ public PetService(PetRepository petRepository, CategoryRepository categoryReposi } @Cacheable(PetCache.class) - @CircuitBreaker("pet") - @Retry("pet") - @Timeout("pet") + @CircuitBreakable(PetCircuitBreaker.class) + @Retryable(PetRetry.class) + @Timeout(PetTimeouter.class) public Optional findByID(long petId) { return petRepository.findById(petId); } - @CircuitBreaker("pet") - @Timeout("pet") + @CircuitBreakable(PetCircuitBreaker.class) + @Timeout(PetTimeouter.class) public PetWithCategory add(String petName, String categoryName) { final long petCategoryId = categoryRepository.findByName(categoryName) .map(PetCategory::id) @@ -48,13 +48,13 @@ public PetWithCategory add(String petName, String categoryName) { new PetCategory(petCategoryId, categoryName)); } - @CircuitBreaker("pet") - @Timeout("pet") + @CircuitBreakable(PetCircuitBreaker.class) + @Timeout(PetTimeouter.class) @CachePut(value = PetCache.class, args = "petId") public Optional update(long petId, @Nullable String petNameUpdate, @Nullable String petCategoryUpdate, - @Nullable Pet.Status petStatusUpdate) { + Pet.@Nullable Status petStatusUpdate) { final Optional existing = petRepository.findById(petId); if (existing.isEmpty()) { return Optional.empty(); @@ -80,8 +80,8 @@ public Optional update(long petId, return Optional.of(result); } - @CircuitBreaker("pet") - @Timeout("pet") + @CircuitBreakable(PetCircuitBreaker.class) + @Timeout(PetTimeouter.class) @CacheInvalidate(PetCache.class) public boolean delete(long petId) { return petRepository.deleteById(petId).value() == 1; diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/java/io/koraframework/example/submodule/pet/service/PetTimeouter.java b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/java/io/koraframework/example/submodule/pet/service/PetTimeouter.java new file mode 100644 index 00000000..e0a96449 --- /dev/null +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/java/io/koraframework/example/submodule/pet/service/PetTimeouter.java @@ -0,0 +1,9 @@ +package io.koraframework.example.submodule.pet.service; + +import io.koraframework.resilient.timeout.Timeouter; +import io.koraframework.resilient.timeout.annotation.TimeoutSpec; + +@TimeoutSpec("resilient.timeout.pet") +public interface PetTimeouter extends Timeouter { + +} diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/resources/reference.conf b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/resources/reference.conf index ed893364..5e219183 100644 --- a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/resources/reference.conf +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/main/resources/reference.conf @@ -7,7 +7,8 @@ pet-cache { resilient { circuitbreaker.pet { - slidingWindowSize = 50 + type = FIXED_WINDOW + countBased.windowSize = 50 minimumRequiredCalls = 25 failureRateThreshold = 50 waitDurationInOpenState = "25s" diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/test/java/io/koraframework/example/submodule/pet/PetComponentTests.java b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/test/java/io/koraframework/example/submodule/pet/PetComponentTests.java index e9482966..03b9617f 100644 --- a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/test/java/io/koraframework/example/submodule/pet/PetComponentTests.java +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api/src/test/java/io/koraframework/example/submodule/pet/PetComponentTests.java @@ -42,7 +42,8 @@ public KoraConfigModification config() { return KoraConfigModification.ofString(""" resilient { circuitbreaker.pet { - slidingWindowSize = 2 + type = FIXED_WINDOW + countBased.windowSize = 2 minimumRequiredCalls = 2 failureRateThreshold = 100 permittedCallsInHalfOpenState = 1 diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/build.gradle b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/build.gradle index b17f0c66..d34eb523 100644 --- a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/build.gradle +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/build.gradle @@ -20,5 +20,5 @@ dependencies { testImplementation "io.koraframework:config-hocon" - testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.15.0" } diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/main/java/io/koraframework/example/submodule/vet/service/VetCircuitBreaker.java b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/main/java/io/koraframework/example/submodule/vet/service/VetCircuitBreaker.java new file mode 100644 index 00000000..99f9200e --- /dev/null +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/main/java/io/koraframework/example/submodule/vet/service/VetCircuitBreaker.java @@ -0,0 +1,9 @@ +package io.koraframework.example.submodule.vet.service; + +import io.koraframework.resilient.circuitbreaker.CircuitBreaker; +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakerSpec; + +@CircuitBreakerSpec("resilient.circuitbreaker.vet") +public interface VetCircuitBreaker extends CircuitBreaker { + +} diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/main/java/io/koraframework/example/submodule/vet/service/VetRetry.java b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/main/java/io/koraframework/example/submodule/vet/service/VetRetry.java new file mode 100644 index 00000000..1ea97fc8 --- /dev/null +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/main/java/io/koraframework/example/submodule/vet/service/VetRetry.java @@ -0,0 +1,9 @@ +package io.koraframework.example.submodule.vet.service; + +import io.koraframework.resilient.retry.Retry; +import io.koraframework.resilient.retry.annotation.RetrySpec; + +@RetrySpec("resilient.retry.vet") +public interface VetRetry extends Retry { + +} diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/main/java/io/koraframework/example/submodule/vet/service/VetService.java b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/main/java/io/koraframework/example/submodule/vet/service/VetService.java index feaa41cd..89eb9193 100644 --- a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/main/java/io/koraframework/example/submodule/vet/service/VetService.java +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/main/java/io/koraframework/example/submodule/vet/service/VetService.java @@ -8,8 +8,8 @@ import io.koraframework.common.annotation.Component; import io.koraframework.example.submodule.vet.model.dao.Vet; import io.koraframework.example.submodule.vet.repository.VetRepository; -import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreaker; -import io.koraframework.resilient.retry.annotation.Retry; +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakable; +import io.koraframework.resilient.retry.annotation.Retryable; import io.koraframework.resilient.timeout.annotation.Timeout; @Component @@ -21,31 +21,31 @@ public VetService(VetRepository vetRepository) { this.vetRepository = vetRepository; } - @CircuitBreaker("vet") - @Retry("vet") - @Timeout("vet") + @CircuitBreakable(VetCircuitBreaker.class) + @Retryable(VetRetry.class) + @Timeout(VetTimeouter.class) public List findAll() { return vetRepository.findAll(); } @Cacheable(VetCache.class) - @CircuitBreaker("vet") - @Retry("vet") - @Timeout("vet") + @CircuitBreakable(VetCircuitBreaker.class) + @Retryable(VetRetry.class) + @Timeout(VetTimeouter.class) public Optional findByID(long vetId) { return vetRepository.findById(vetId); } - @CircuitBreaker("vet") - @Timeout("vet") + @CircuitBreakable(VetCircuitBreaker.class) + @Timeout(VetTimeouter.class) public Vet add(String name, String surname) { var vet = new Vet(0, name, surname); var vetId = vetRepository.insert(vet); return new Vet(vetId, vet.name(), vet.surname()); } - @CircuitBreaker("vet") - @Timeout("vet") + @CircuitBreakable(VetCircuitBreaker.class) + @Timeout(VetTimeouter.class) @CachePut(value = VetCache.class, args = "id") public Optional update(long id, String name, String surname) { final Optional existing = vetRepository.findById(id); @@ -58,8 +58,8 @@ public Optional update(long id, String name, String surname) { return Optional.of(result); } - @CircuitBreaker("vet") - @Timeout("vet") + @CircuitBreakable(VetCircuitBreaker.class) + @Timeout(VetTimeouter.class) @CacheInvalidate(VetCache.class) public boolean delete(long vetId) { return vetRepository.deleteById(vetId).value() == 1; diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/main/java/io/koraframework/example/submodule/vet/service/VetTimeouter.java b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/main/java/io/koraframework/example/submodule/vet/service/VetTimeouter.java new file mode 100644 index 00000000..15dfc0b3 --- /dev/null +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/main/java/io/koraframework/example/submodule/vet/service/VetTimeouter.java @@ -0,0 +1,9 @@ +package io.koraframework.example.submodule.vet.service; + +import io.koraframework.resilient.timeout.Timeouter; +import io.koraframework.resilient.timeout.annotation.TimeoutSpec; + +@TimeoutSpec("resilient.timeout.vet") +public interface VetTimeouter extends Timeouter { + +} diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/main/resources/reference.conf b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/main/resources/reference.conf index 5755981e..3c814ec6 100644 --- a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/main/resources/reference.conf +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/main/resources/reference.conf @@ -7,7 +7,8 @@ vet-cache { resilient { circuitbreaker.vet { - slidingWindowSize = 50 + type = FIXED_WINDOW + countBased.windowSize = 50 minimumRequiredCalls = 25 failureRateThreshold = 50 waitDurationInOpenState = "25s" diff --git a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/test/java/io/koraframework/example/submodule/vet/VetComponentTests.java b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/test/java/io/koraframework/example/submodule/vet/VetComponentTests.java index 06336a30..7dfa52c9 100644 --- a/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/test/java/io/koraframework/example/submodule/vet/VetComponentTests.java +++ b/examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api/src/test/java/io/koraframework/example/submodule/vet/VetComponentTests.java @@ -36,7 +36,8 @@ public KoraConfigModification config() { return KoraConfigModification.ofString(""" resilient { circuitbreaker.vet { - slidingWindowSize = 2 + type = FIXED_WINDOW + countBased.windowSize = 2 minimumRequiredCalls = 2 failureRateThreshold = 100 permittedCallsInHalfOpenState = 1 diff --git a/examples/java/kora-java-crud/build.gradle b/examples/java/kora-java-crud/build.gradle index 6fcbfa5d..b956d174 100644 --- a/examples/java/kora-java-crud/build.gradle +++ b/examples/java/kora-java-crud/build.gradle @@ -34,7 +34,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "org.mapstruct:mapstruct-processor:1.5.5.Final" annotationProcessor "io.koraframework:annotation-processors" @@ -59,7 +59,7 @@ dependencies { testImplementation "org.mockito:mockito-core:5.18.0" testImplementation "io.koraframework:test-junit5" - testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.15.0" testImplementation "org.testcontainers:junit-jupiter:1.21.4" } @@ -107,10 +107,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true @@ -162,4 +158,4 @@ javadoc { if (JavaVersion.current().isJava9Compatible()) { options.addBooleanOption("html5", true) } -} \ No newline at end of file +} diff --git a/examples/java/kora-java-database-cassandra/build.gradle b/examples/java/kora-java-database-cassandra/build.gradle index 9e53c9b9..f53b9f4a 100644 --- a/examples/java/kora-java-database-cassandra/build.gradle +++ b/examples/java/kora-java-database-cassandra/build.gradle @@ -18,17 +18,16 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:database-cassandra" - implementation "io.projectreactor:reactor-core:3.6.18" // For reactive examples (optional) implementation "io.koraframework:logging-logback" implementation "io.koraframework:config-hocon" testImplementation "io.koraframework:test-junit5" - testImplementation "io.goodforgod:testcontainers-extensions-scylla:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-scylla:0.15.0" } application { @@ -60,10 +59,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraCrudReactorRepository.java b/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraCrudReactorRepository.java deleted file mode 100644 index 3fb82d4b..00000000 --- a/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraCrudReactorRepository.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.koraframework.example.cassandra; - -import org.jspecify.annotations.Nullable; -import java.util.List; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import io.koraframework.database.cassandra.CassandraRepository; -import io.koraframework.database.cassandra.annotation.EntityCassandra; -import io.koraframework.database.common.annotation.*; - -@Repository -public interface CassandraCrudReactorRepository extends CassandraRepository { - - @EntityCassandra - record Entity(String id, - @Column("value1") int field1, - String value2, - @Nullable String value3) {} - - @Query("SELECT * FROM entities WHERE id = :id") - Mono findById(String id); - - @Query("SELECT * FROM entities") - Flux findAll(); - - @Query("SELECT * FROM entities") - Mono> findAllMono(); - - @Query(""" - INSERT INTO entities(id, value1, value2, value3) - VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3) - """) - Mono insert(Entity entity); - - @Query(""" - INSERT INTO entities(id, value1, value2, value3) - VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3) - """) - Mono insertBatch(@Batch List entity); - - @Query(""" - UPDATE entities - SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 - WHERE id = :entity.id - """) - Mono update(Entity entity); - - @Query(""" - UPDATE entities - SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 - WHERE id = :entity.id - """) - Mono updateBatch(@Batch List entity); - - @Query("DELETE FROM entities WHERE id = :id") - Mono deleteById(String id); - - @Query("TRUNCATE entities") - Mono deleteAll(); -} diff --git a/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraMapperParameterRepository.java b/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraMapperParameterRepository.java index 267ec8d4..86d73871 100644 --- a/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraMapperParameterRepository.java +++ b/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraMapperParameterRepository.java @@ -2,6 +2,7 @@ import com.datastax.oss.driver.api.core.data.SettableByName; import org.jspecify.annotations.Nullable; +import io.koraframework.common.annotation.Component; import io.koraframework.common.annotation.Mapping; import io.koraframework.database.cassandra.CassandraRepository; import io.koraframework.database.cassandra.mapper.parameter.CassandraParameterColumnMapper; @@ -10,10 +11,12 @@ @Repository public interface CassandraMapperParameterRepository extends CassandraRepository { + @Component + final class EntityFieldTypeParameterMapper implements CassandraParameterColumnMapper { @Override - public void apply(SettableByName stmt, int index, @Nullable Entity.FieldType value) { + public void apply(SettableByName stmt, int index, Entity.@Nullable FieldType value) { if (value != null) { stmt.setInt(index, value.code()); } diff --git a/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraMapperResultSetReactiveRepository.java b/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraMapperResultSetReactiveRepository.java deleted file mode 100644 index 68f1fbdb..00000000 --- a/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraMapperResultSetReactiveRepository.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.koraframework.example.cassandra; - -import com.datastax.dse.driver.api.core.cql.reactive.ReactiveResultSet; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import io.koraframework.common.annotation.Mapping; -import io.koraframework.database.cassandra.CassandraRepository; -import io.koraframework.database.cassandra.mapper.result.CassandraReactiveResultSetMapper; -import io.koraframework.database.common.annotation.Query; -import io.koraframework.database.common.annotation.Repository; - -@Repository -public interface CassandraMapperResultSetReactiveRepository extends CassandraRepository { - - record EntityPart(String id, int field1) {} - - final class EntityPartResultSetMapper implements - CassandraReactiveResultSetMapper>, Mono>>> { - - @Override - public Mono>> apply(ReactiveResultSet rows) { - return Flux.from(rows) - .map(row -> new EntityPart(row.getString(0), row.getInt(1))) - .collect(HashMap::new, (collector, value) -> { - var parts = collector.computeIfAbsent(value.field1(), k -> new ArrayList<>()); - parts.add(value); - }); - } - } - - @Mapping(EntityPartResultSetMapper.class) - @Query("SELECT id, value1 FROM entities") - Mono>> findAllParts(); -} diff --git a/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraMapperResultSetRepository.java b/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraMapperResultSetRepository.java index 5c10f483..b5183a88 100644 --- a/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraMapperResultSetRepository.java +++ b/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraMapperResultSetRepository.java @@ -6,6 +6,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import io.koraframework.common.annotation.Component; import io.koraframework.common.annotation.Mapping; import io.koraframework.database.cassandra.CassandraRepository; import io.koraframework.database.cassandra.mapper.result.CassandraResultSetMapper; @@ -17,6 +18,8 @@ public interface CassandraMapperResultSetRepository extends CassandraRepository record EntityPart(String id, int field1) {} + @Component + final class EntityPartResultSetMapper implements CassandraResultSetMapper>> { @Override diff --git a/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraMapperRowColumnRepository.java b/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraMapperRowColumnRepository.java index f425869f..7d3f5a13 100644 --- a/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraMapperRowColumnRepository.java +++ b/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraMapperRowColumnRepository.java @@ -4,7 +4,9 @@ import com.datastax.oss.driver.api.core.data.SettableByName; import org.jspecify.annotations.Nullable; import java.util.List; +import io.koraframework.common.annotation.Component; import io.koraframework.common.annotation.Mapping; +import io.koraframework.database.cassandra.annotation.EntityCassandra; import io.koraframework.database.cassandra.CassandraRepository; import io.koraframework.database.cassandra.mapper.parameter.CassandraParameterColumnMapper; import io.koraframework.database.cassandra.mapper.result.CassandraRowColumnMapper; @@ -13,6 +15,8 @@ @Repository public interface CassandraMapperRowColumnRepository extends CassandraRepository { + @Component + final class EntityFieldTypeResultMapper implements CassandraRowColumnMapper { private static final Entity.FieldType[] ALL = Entity.FieldType.values(); @@ -30,18 +34,21 @@ public Entity.FieldType apply(GettableByName row, int index) { } } + @Component + final class EntityFieldTypeParameterMapper implements CassandraParameterColumnMapper { @Override public void - apply(SettableByName stmt, int index, @Nullable CassandraMapperParameterRepository.Entity.FieldType value) { + apply(SettableByName stmt, int index, CassandraMapperParameterRepository.Entity.@Nullable FieldType value) { if (value != null) { stmt.setInt(index, value.code()); } } } + @EntityCassandra record Entity(String id, @Mapping(EntityFieldTypeResultMapper.class) @Mapping(EntityFieldTypeParameterMapper.class) diff --git a/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraMapperRowRepository.java b/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraMapperRowRepository.java index 54733e7f..c0ef8529 100644 --- a/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraMapperRowRepository.java +++ b/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/CassandraMapperRowRepository.java @@ -2,6 +2,7 @@ import com.datastax.oss.driver.api.core.cql.Row; import java.util.List; +import io.koraframework.common.annotation.Component; import io.koraframework.common.annotation.Mapping; import io.koraframework.database.cassandra.CassandraRepository; import io.koraframework.database.cassandra.mapper.result.CassandraRowMapper; @@ -13,6 +14,8 @@ public interface CassandraMapperRowRepository extends CassandraRepository { record EntityPart(String id, int field1) {} + @Component + final class EntityPartRowMapper implements CassandraRowMapper { @Override diff --git a/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/RootService.java b/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/RootService.java index 09cde5a5..0f654502 100644 --- a/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/RootService.java +++ b/examples/java/kora-java-database-cassandra/src/main/java/io/koraframework/example/cassandra/RootService.java @@ -9,9 +9,7 @@ public final class RootService { private final CassandraCrudSyncRepository crudSyncRepository; private final CassandraCrudAsyncRepository crudAsyncRepository; - private final CassandraCrudReactorRepository crudReactorRepository; private final CassandraMapperResultSetRepository mapperResultSetRepository; - private final CassandraMapperResultSetReactiveRepository mapperResultSetReactiveRepository; private final CassandraMapperRowRepository mapperRowRepository; private final CassandraMapperRowColumnRepository mapperRowColumnRepository; private final CassandraMapperParameterRepository mapperParameterRepository; @@ -19,18 +17,14 @@ public final class RootService { public RootService(CassandraCrudSyncRepository crudSyncRepository, CassandraCrudAsyncRepository crudAsyncRepository, - CassandraCrudReactorRepository crudReactorRepository, CassandraMapperResultSetRepository mapperResultSetRepository, - CassandraMapperResultSetReactiveRepository mapperResultSetReactiveRepository, CassandraMapperRowRepository mapperRowRepository, CassandraMapperRowColumnRepository mapperRowColumnRepository, CassandraMapperParameterRepository mapperParameterRepository, CassandraUdtRepository cassandraUdtRepository) { this.crudSyncRepository = crudSyncRepository; this.crudAsyncRepository = crudAsyncRepository; - this.crudReactorRepository = crudReactorRepository; this.mapperResultSetRepository = mapperResultSetRepository; - this.mapperResultSetReactiveRepository = mapperResultSetReactiveRepository; this.mapperRowRepository = mapperRowRepository; this.mapperRowColumnRepository = mapperRowColumnRepository; this.mapperParameterRepository = mapperParameterRepository; diff --git a/examples/java/kora-java-database-cassandra/src/test/java/io/koraframework/example/cassandra/CassandraCrudReactorTests.java b/examples/java/kora-java-database-cassandra/src/test/java/io/koraframework/example/cassandra/CassandraCrudReactorTests.java deleted file mode 100644 index 6cd73e7a..00000000 --- a/examples/java/kora-java-database-cassandra/src/test/java/io/koraframework/example/cassandra/CassandraCrudReactorTests.java +++ /dev/null @@ -1,124 +0,0 @@ -package io.koraframework.example.cassandra; - -import static org.junit.jupiter.api.Assertions.*; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.scylla.ConnectionScylla; -import io.goodforgod.testcontainers.extensions.scylla.Migration; -import io.goodforgod.testcontainers.extensions.scylla.ScyllaConnection; -import io.goodforgod.testcontainers.extensions.scylla.TestcontainersScylla; -import java.time.Duration; -import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersScylla( - mode = ContainerMode.PER_RUN, - migration = @Migration( - engine = Migration.Engines.SCRIPTS, - locations = { "migrations" }, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class CassandraCrudReactorTests implements KoraAppTestConfigModifier { - - @ConnectionScylla - private ScyllaConnection connection; - - @TestComponent - private CassandraCrudReactorRepository repository; - - @NotNull - @Override - public KoraConfigModification config() { - return KoraConfigModification - .ofSystemProperty("CASSANDRA_CONTACT_POINTS", connection.params().contactPoint()) - .withSystemProperty("CASSANDRA_USER", connection.params().username()) - .withSystemProperty("CASSANDRA_PASS", connection.params().password()) - .withSystemProperty("CASSANDRA_DC", connection.params().datacenter()) - .withSystemProperty("CASSANDRA_KEYSPACE", connection.params().keyspace()); - } - - @Test - void monoSingleSuccess() { - // given - var entityCreate = new CassandraCrudReactorRepository.Entity("1", 1, "2", null); - repository.insert(entityCreate).block(Duration.ofSeconds(15)); - - var foundCreated = repository.findById("1").block(Duration.ofSeconds(15)); - assertNotNull(foundCreated); - assertEquals("1", foundCreated.id()); - assertEquals(1, foundCreated.field1()); - assertEquals("2", foundCreated.value2()); - assertNull(foundCreated.value3()); - - // when - var entityUpdate = new CassandraCrudReactorRepository.Entity("1", 5, "6", null); - repository.update(entityUpdate).block(Duration.ofSeconds(15)); - - var foundUpdated = repository.findById("1").block(Duration.ofSeconds(15)); - assertNotNull(foundUpdated); - assertEquals("1", foundUpdated.id()); - assertEquals(5, foundUpdated.field1()); - assertEquals("6", foundUpdated.value2()); - assertNull(foundUpdated.value3()); - - // then - repository.deleteById("1").block(Duration.ofSeconds(15)); - assertNull(repository.findById("1").block(Duration.ofSeconds(15))); - } - - @Test - void monoBatchSuccess() { - // given - var entityCreate1 = new CassandraCrudReactorRepository.Entity("1", 1, "2", null); - var entityCreate2 = new CassandraCrudReactorRepository.Entity("2", 3, "4", null); - repository.insertBatch(List.of(entityCreate1, entityCreate2)).block(Duration.ofSeconds(15)); - - var foundCreated = repository.findAllMono().block(Duration.ofSeconds(15)); - assertNotNull(foundCreated); - assertEquals(2, foundCreated.size()); - for (var entity : foundCreated) { - if (entity.id().equals("1")) { - assertEquals("1", entity.id()); - assertEquals(1, entity.field1()); - assertEquals("2", entity.value2()); - assertNull(entity.value3()); - } else { - assertEquals("2", entity.id()); - assertEquals(3, entity.field1()); - assertEquals("4", entity.value2()); - assertNull(entity.value3()); - } - } - - // when - var entityUpdate1 = new CassandraCrudReactorRepository.Entity("1", 5, "6", null); - var entityUpdate2 = new CassandraCrudReactorRepository.Entity("2", 7, "8", null); - repository.updateBatch(List.of(entityUpdate1, entityUpdate2)).block(Duration.ofSeconds(15)); - - var foundUpdated = repository.findAll().collectList().block(Duration.ofSeconds(15)); - assertEquals(2, foundUpdated.size()); - for (var entity : foundUpdated) { - if (entity.id().equals("1")) { - assertEquals("1", entity.id()); - assertEquals(5, entity.field1()); - assertEquals("6", entity.value2()); - assertNull(entity.value3()); - } else { - assertEquals("2", entity.id()); - assertEquals(7, entity.field1()); - assertEquals("8", entity.value2()); - assertNull(entity.value3()); - } - } - - // then - repository.deleteAll().block(Duration.ofSeconds(15)); - assertTrue(repository.findAllMono().block(Duration.ofSeconds(15)).isEmpty()); - } -} diff --git a/examples/java/kora-java-database-cassandra/src/test/java/io/koraframework/example/cassandra/CassandraMapperResultSetReactiveTests.java b/examples/java/kora-java-database-cassandra/src/test/java/io/koraframework/example/cassandra/CassandraMapperResultSetReactiveTests.java deleted file mode 100644 index 32651bfc..00000000 --- a/examples/java/kora-java-database-cassandra/src/test/java/io/koraframework/example/cassandra/CassandraMapperResultSetReactiveTests.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.koraframework.example.cassandra; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.scylla.ConnectionScylla; -import io.goodforgod.testcontainers.extensions.scylla.Migration; -import io.goodforgod.testcontainers.extensions.scylla.ScyllaConnection; -import io.goodforgod.testcontainers.extensions.scylla.TestcontainersScylla; -import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersScylla( - mode = ContainerMode.PER_RUN, - migration = @Migration( - engine = Migration.Engines.SCRIPTS, - locations = { "migrations" }, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class CassandraMapperResultSetReactiveTests implements KoraAppTestConfigModifier { - - @ConnectionScylla - private ScyllaConnection connection; - - @TestComponent - private CassandraCrudSyncRepository crudRepository; - @TestComponent - private CassandraMapperResultSetRepository mapperRepository; - - @NotNull - @Override - public KoraConfigModification config() { - return KoraConfigModification - .ofSystemProperty("CASSANDRA_CONTACT_POINTS", connection.params().contactPoint()) - .withSystemProperty("CASSANDRA_USER", connection.params().username()) - .withSystemProperty("CASSANDRA_PASS", connection.params().password()) - .withSystemProperty("CASSANDRA_DC", connection.params().datacenter()) - .withSystemProperty("CASSANDRA_KEYSPACE", connection.params().keyspace()); - } - - @Test - void resultSetMapperSuccess() { - // given - var entity1 = new CassandraCrudSyncRepository.Entity("1", 1, "2", null); - var entity2 = new CassandraCrudSyncRepository.Entity("2", 3, "4", null); - - // when - crudRepository.insertBatch(List.of(entity1, entity2)); - - // then - var found = mapperRepository.findAllParts(); - var fieldOne = found.get(1); - assertNotNull(fieldOne); - assertEquals(1, fieldOne.size()); - assertEquals(entity1.id(), fieldOne.get(0).id()); - - var fieldUnknown = found.get(3); - assertNotNull(fieldUnknown); - assertEquals(1, fieldUnknown.size()); - assertEquals(entity2.id(), fieldUnknown.get(0).id()); - } -} diff --git a/examples/java/kora-java-database-jdbc/build.gradle b/examples/java/kora-java-database-jdbc/build.gradle index 458636f4..dae4d5d9 100644 --- a/examples/java/kora-java-database-jdbc/build.gradle +++ b/examples/java/kora-java-database-jdbc/build.gradle @@ -18,18 +18,17 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "org.postgresql:postgresql:42.7.7" implementation "io.koraframework:database-jdbc" - implementation "io.projectreactor:reactor-core:3.6.18" // For reactive examples (optional) implementation "io.koraframework:logging-logback" implementation "io.koraframework:config-hocon" testImplementation "io.koraframework:test-junit5" - testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.15.0" } application { @@ -59,10 +58,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/Application.java b/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/Application.java index e363f7e8..575eb935 100644 --- a/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/Application.java +++ b/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/Application.java @@ -1,30 +1,20 @@ package io.koraframework.example.jdbc; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; import io.koraframework.application.graph.KoraApplication; import io.koraframework.common.annotation.KoraApp; -import io.koraframework.common.annotation.Tag; import io.koraframework.config.hocon.HoconConfigModule; -import io.koraframework.database.jdbc.JdbcDatabase; import io.koraframework.database.jdbc.JdbcDatabaseModule; -import io.koraframework.json.common.JsonCommonModule; +import io.koraframework.json.common.JsonModule; import io.koraframework.logging.logback.LogbackModule; @KoraApp public interface Application extends HoconConfigModule, LogbackModule, - JsonCommonModule, + JsonModule, JdbcDatabaseModule { static void main(String[] args) { KoraApplication.run(ApplicationGraph::graph); } - - // Also when using UndertowHttpServer, you can use XnioWorker as Executor - @Tag(JdbcDatabase.class) - default Executor jdbcExecutor() { - return Executors.newFixedThreadPool(Math.max(Runtime.getRuntime().availableProcessors(), 2) * 2); - } } diff --git a/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/JdbcCrudAsyncRepository.java b/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/JdbcCrudAsyncRepository.java deleted file mode 100644 index 2e473385..00000000 --- a/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/JdbcCrudAsyncRepository.java +++ /dev/null @@ -1,59 +0,0 @@ -package io.koraframework.example.jdbc; - -import org.jspecify.annotations.Nullable; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import io.koraframework.database.common.UpdateCount; -import io.koraframework.database.common.annotation.*; -import io.koraframework.database.jdbc.annotation.EntityJdbc; -import io.koraframework.database.jdbc.JdbcRepository; - -@Repository -public interface JdbcCrudAsyncRepository extends JdbcRepository { - - @EntityJdbc - @Table("entities") - record Entity(@Id String id, - @Column("value1") int field1, - String value2, - @Nullable String value3) {} - - @Query("SELECT * FROM entities WHERE id = :id") - CompletionStage findById(String id); - - @Query("SELECT * FROM entities") - CompletionStage> findAll(); - - @Query(""" - INSERT INTO entities(id, value1, value2, value3) - VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3) - """) - CompletionStage insert(Entity entity); - - @Query(""" - INSERT INTO entities(id, value1, value2, value3) - VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3) - """) - CompletionStage insertBatch(@Batch List entity); - - @Query(""" - UPDATE entities - SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 - WHERE id = :entity.id - """) - CompletionStage update(Entity entity); - - @Query(""" - UPDATE entities - SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 - WHERE id = :entity.id - """) - CompletionStage updateBatch(@Batch List entity); - - @Query("DELETE FROM entities WHERE id = :id") - CompletableFuture deleteById(String id); - - @Query("DELETE FROM entities") - CompletableFuture deleteAll(); -} diff --git a/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/JdbcCrudReactorRepository.java b/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/JdbcCrudReactorRepository.java deleted file mode 100644 index e11bc571..00000000 --- a/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/JdbcCrudReactorRepository.java +++ /dev/null @@ -1,107 +0,0 @@ -package io.koraframework.example.jdbc; - -import org.jspecify.annotations.Nullable; -import java.util.List; -import reactor.core.publisher.Mono; -import io.koraframework.database.common.UpdateCount; -import io.koraframework.database.common.annotation.Batch; -import io.koraframework.database.common.annotation.Column; -import io.koraframework.database.common.annotation.Query; -import io.koraframework.database.common.annotation.Repository; -import io.koraframework.database.jdbc.annotation.EntityJdbc; -import io.koraframework.database.jdbc.JdbcRepository; - -@Repository -public interface JdbcCrudReactorRepository extends JdbcRepository { - - @EntityJdbc - class Entity { - - private String id; - @Column("value1") - private int field1; - private String value2; - @Nullable - private String value3; - - public Entity() {} - - public Entity(String id, int field1, String value2, @Nullable String value3) { - this.id = id; - this.field1 = field1; - this.value2 = value2; - this.value3 = value3; - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public int getField1() { - return field1; - } - - public void setField1(int field1) { - this.field1 = field1; - } - - public String getValue2() { - return value2; - } - - public void setValue2(String value2) { - this.value2 = value2; - } - - @Nullable - public String getValue3() { - return value3; - } - - public void setValue3(@Nullable String value3) { - this.value3 = value3; - } - } - - @Query("SELECT * FROM entities WHERE id = :id") - Mono findById(String id); - - @Query("SELECT * FROM entities") - Mono> findAll(); - - @Query(""" - INSERT INTO entities(id, value1, value2, value3) - VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3) - """) - Mono insert(Entity entity); - - @Query(""" - INSERT INTO entities(id, value1, value2, value3) - VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3) - """) - Mono insertBatch(@Batch List entity); - - @Query(""" - UPDATE entities - SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 - WHERE id = :entity.id - """) - Mono update(Entity entity); - - @Query(""" - UPDATE entities - SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 - WHERE id = :entity.id - """) - Mono updateBatch(@Batch List entity); - - @Query("DELETE FROM entities WHERE id = :id") - Mono deleteById(String id); - - @Query("DELETE FROM entities") - Mono deleteAll(); -} diff --git a/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/JdbcJsonbMapperModule.java b/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/JdbcJsonbMapperModule.java index dceed8d7..52c8e497 100644 --- a/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/JdbcJsonbMapperModule.java +++ b/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/JdbcJsonbMapperModule.java @@ -18,7 +18,7 @@ default JdbcParameterColumnMapper jdbcJsonParameterColumnMapper(JsonWrite if (value != null) { PGobject jsonb = new PGobject(); jsonb.setType("jsonb"); - jsonb.setValue(writer.toStringUnchecked(value)); + jsonb.setValue(writer.toString(value)); stmt.setObject(index, jsonb); } else { stmt.setNull(index, Types.NULL); @@ -33,7 +33,7 @@ default JdbcResultColumnMapper jdbcJsonResultColumnMapper(JsonReader r if (value == null) { return null; } else { - return reader.readUnchecked(value); + return reader.read(value); } }; } diff --git a/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/JdbcMapperColumnRepository.java b/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/JdbcMapperColumnRepository.java index 456eebed..cd82be18 100644 --- a/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/JdbcMapperColumnRepository.java +++ b/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/JdbcMapperColumnRepository.java @@ -37,7 +37,7 @@ public Entity.FieldType apply(ResultSet rs, int index) throws SQLException { final class EntityFieldTypeParameterMapper implements JdbcParameterColumnMapper { @Override - public void set(PreparedStatement stmt, int index, @Nullable Entity.FieldType value) throws SQLException { + public void set(PreparedStatement stmt, int index, Entity.@Nullable FieldType value) throws SQLException { if (value != null) { stmt.setInt(index, value.code()); } diff --git a/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/JdbcMapperParameterRepository.java b/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/JdbcMapperParameterRepository.java index 130a6c98..1a7fe972 100644 --- a/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/JdbcMapperParameterRepository.java +++ b/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/JdbcMapperParameterRepository.java @@ -17,7 +17,7 @@ public interface JdbcMapperParameterRepository extends JdbcRepository { final class EntityFieldTypeParameterMapper implements JdbcParameterColumnMapper { @Override - public void set(PreparedStatement stmt, int index, @Nullable Entity.FieldType value) + public void set(PreparedStatement stmt, int index, Entity.@Nullable FieldType value) throws SQLException { if (value != null) { stmt.setInt(index, value.code()); diff --git a/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/RootService.java b/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/RootService.java index 5182408d..de1aedb5 100644 --- a/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/RootService.java +++ b/examples/java/kora-java-database-jdbc/src/main/java/io/koraframework/example/jdbc/RootService.java @@ -8,8 +8,6 @@ public final class RootService { private final JdbcCrudSyncRepository jdbcCrudSyncRepository; - private final JdbcCrudAsyncRepository jdbcCrudAsyncRepository; - private final JdbcCrudReactorRepository jdbcCrudReactorRepository; private final JdbcMapperResultSetRepository jdbcMapperResultSetRepository; private final JdbcMapperRowRepository jdbcMapperRowRepository; private final JdbcMapperColumnRepository jdbcMapperColumnRepository; @@ -25,8 +23,6 @@ public final class RootService { private final JdbcCrudExtendedCompositeRepository jdbcCrudExtendedCompositeRepository; public RootService(JdbcCrudSyncRepository jdbcCrudSyncRepository, - JdbcCrudAsyncRepository jdbcCrudAsyncRepository, - JdbcCrudReactorRepository jdbcCrudReactorRepository, JdbcMapperResultSetRepository jdbcMapperResultSetRepository, JdbcMapperRowRepository jdbcMapperRowRepository, JdbcMapperColumnRepository jdbcMapperColumnRepository, @@ -41,8 +37,6 @@ public RootService(JdbcCrudSyncRepository jdbcCrudSyncRepository, JdbcCrudExtendedRepository jdbcCrudExtendedRepository, JdbcCrudExtendedCompositeRepository jdbcCrudExtendedCompositeRepository) { this.jdbcCrudSyncRepository = jdbcCrudSyncRepository; - this.jdbcCrudAsyncRepository = jdbcCrudAsyncRepository; - this.jdbcCrudReactorRepository = jdbcCrudReactorRepository; this.jdbcMapperResultSetRepository = jdbcMapperResultSetRepository; this.jdbcMapperRowRepository = jdbcMapperRowRepository; this.jdbcMapperColumnRepository = jdbcMapperColumnRepository; diff --git a/examples/java/kora-java-database-jdbc/src/main/resources/application.conf b/examples/java/kora-java-database-jdbc/src/main/resources/application.conf index 617290d1..bb1a8bc4 100644 --- a/examples/java/kora-java-database-jdbc/src/main/resources/application.conf +++ b/examples/java/kora-java-database-jdbc/src/main/resources/application.conf @@ -1,4 +1,4 @@ -db { +jdbc { jdbcUrl = ${POSTGRES_JDBC_URL} username = ${POSTGRES_USER} password = ${POSTGRES_PASS} diff --git a/examples/java/kora-java-database-jdbc/src/test/java/io/koraframework/example/jdbc/JdbcCrudAsyncTests.java b/examples/java/kora-java-database-jdbc/src/test/java/io/koraframework/example/jdbc/JdbcCrudAsyncTests.java deleted file mode 100644 index 041173ec..00000000 --- a/examples/java/kora-java-database-jdbc/src/test/java/io/koraframework/example/jdbc/JdbcCrudAsyncTests.java +++ /dev/null @@ -1,119 +0,0 @@ -package io.koraframework.example.jdbc; - -import static org.junit.jupiter.api.Assertions.*; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL; -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; -import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL; -import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = @Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class JdbcCrudAsyncTests implements KoraAppTestConfigModifier { - - @ConnectionPostgreSQL - private JdbcConnection connection; - - @TestComponent - private JdbcCrudAsyncRepository repository; - - @NotNull - @Override - public KoraConfigModification config() { - return KoraConfigModification.ofSystemProperty("POSTGRES_JDBC_URL", connection.params().jdbcUrl()) - .withSystemProperty("POSTGRES_USER", connection.params().username()) - .withSystemProperty("POSTGRES_PASS", connection.params().password()); - } - - @Test - void asyncSingle() { - // given - var entityCreate = new JdbcCrudAsyncRepository.Entity("1", 1, "2", null); - repository.insert(entityCreate).toCompletableFuture().join(); - - var foundCreated = repository.findById("1").toCompletableFuture().join(); - assertNotNull(foundCreated); - assertEquals("1", foundCreated.id()); - assertEquals(1, foundCreated.field1()); - assertEquals("2", foundCreated.value2()); - assertNull(foundCreated.value3()); - - // when - var entityUpdate = new JdbcCrudAsyncRepository.Entity("1", 5, "6", null); - repository.update(entityUpdate).toCompletableFuture().join(); - - var foundUpdated = repository.findById("1").toCompletableFuture().join(); - assertNotNull(foundUpdated); - assertEquals("1", foundUpdated.id()); - assertEquals(5, foundUpdated.field1()); - assertEquals("6", foundUpdated.value2()); - assertNull(foundUpdated.value3()); - - // then - repository.deleteById("1").join(); - assertNull(repository.findById("1").toCompletableFuture().join()); - } - - @Test - void asyncBatch() { - // given - var entityCreate1 = new JdbcCrudAsyncRepository.Entity("1", 1, "2", null); - var entityCreate2 = new JdbcCrudAsyncRepository.Entity("2", 3, "4", null); - assertEquals(2L, repository.insertBatch(List.of(entityCreate1, entityCreate2)).toCompletableFuture().join().value()); - - var foundCreated = repository.findAll().toCompletableFuture().join(); - assertNotNull(foundCreated); - assertEquals(2, foundCreated.size()); - for (var entity : foundCreated) { - if (entity.id().equals("1")) { - assertEquals("1", entity.id()); - assertEquals(1, entity.field1()); - assertEquals("2", entity.value2()); - assertNull(entity.value3()); - } else { - assertEquals("2", entity.id()); - assertEquals(3, entity.field1()); - assertEquals("4", entity.value2()); - assertNull(entity.value3()); - } - } - - // when - var entityUpdate1 = new JdbcCrudAsyncRepository.Entity("1", 5, "6", null); - var entityUpdate2 = new JdbcCrudAsyncRepository.Entity("2", 7, "8", null); - assertEquals(2L, repository.updateBatch(List.of(entityUpdate1, entityUpdate2)).toCompletableFuture().join().value()); - - var foundUpdated = repository.findAll().toCompletableFuture().join(); - assertEquals(2, foundUpdated.size()); - for (var entity : foundUpdated) { - if (entity.id().equals("1")) { - assertEquals("1", entity.id()); - assertEquals(5, entity.field1()); - assertEquals("6", entity.value2()); - assertNull(entity.value3()); - } else { - assertEquals("2", entity.id()); - assertEquals(7, entity.field1()); - assertEquals("8", entity.value2()); - assertNull(entity.value3()); - } - } - - // then - assertEquals(2L, repository.deleteAll().join().value()); - assertTrue(repository.findAll().toCompletableFuture().join().isEmpty()); - } -} diff --git a/examples/java/kora-java-database-jdbc/src/test/java/io/koraframework/example/jdbc/JdbcCrudReactorTests.java b/examples/java/kora-java-database-jdbc/src/test/java/io/koraframework/example/jdbc/JdbcCrudReactorTests.java deleted file mode 100644 index 4865aff3..00000000 --- a/examples/java/kora-java-database-jdbc/src/test/java/io/koraframework/example/jdbc/JdbcCrudReactorTests.java +++ /dev/null @@ -1,120 +0,0 @@ -package io.koraframework.example.jdbc; - -import static org.junit.jupiter.api.Assertions.*; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL; -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; -import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL; -import java.time.Duration; -import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = @Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class JdbcCrudReactorTests implements KoraAppTestConfigModifier { - - @ConnectionPostgreSQL - private JdbcConnection connection; - - @TestComponent - private JdbcCrudReactorRepository repository; - - @NotNull - @Override - public KoraConfigModification config() { - return KoraConfigModification.ofSystemProperty("POSTGRES_JDBC_URL", connection.params().jdbcUrl()) - .withSystemProperty("POSTGRES_USER", connection.params().username()) - .withSystemProperty("POSTGRES_PASS", connection.params().password()); - } - - @Test - void monoSingle() { - // given - var entityCreate = new JdbcCrudReactorRepository.Entity("1", 1, "2", null); - repository.insert(entityCreate).block(Duration.ofSeconds(15)); - - var foundCreated = repository.findById("1").block(Duration.ofSeconds(15)); - assertNotNull(foundCreated); - assertEquals("1", foundCreated.getId()); - assertEquals(1, foundCreated.getField1()); - assertEquals("2", foundCreated.getValue2()); - assertNull(foundCreated.getValue3()); - - // when - var entityUpdate = new JdbcCrudReactorRepository.Entity("1", 5, "6", null); - repository.update(entityUpdate).block(Duration.ofSeconds(15)); - - var foundUpdated = repository.findById("1").block(Duration.ofSeconds(15)); - assertNotNull(foundUpdated); - assertEquals("1", foundUpdated.getId()); - assertEquals(5, foundUpdated.getField1()); - assertEquals("6", foundUpdated.getValue2()); - assertNull(foundUpdated.getValue3()); - - // then - repository.deleteById("1").block(Duration.ofSeconds(15)); - assertNull(repository.findById("1").block(Duration.ofSeconds(15))); - } - - @Test - void monoBatch() { - // given - var entityCreate1 = new JdbcCrudReactorRepository.Entity("1", 1, "2", null); - var entityCreate2 = new JdbcCrudReactorRepository.Entity("2", 3, "4", null); - assertEquals(2L, repository.insertBatch(List.of(entityCreate1, entityCreate2)).block(Duration.ofSeconds(15)).value()); - - var foundCreated = repository.findAll().block(Duration.ofSeconds(15)); - assertNotNull(foundCreated); - assertEquals(2, foundCreated.size()); - for (var entity : foundCreated) { - if (entity.getId().equals("1")) { - assertEquals("1", entity.getId()); - assertEquals(1, entity.getField1()); - assertEquals("2", entity.getValue2()); - assertNull(entity.getValue3()); - } else { - assertEquals("2", entity.getId()); - assertEquals(3, entity.getField1()); - assertEquals("4", entity.getValue2()); - assertNull(entity.getValue3()); - } - } - - // when - var entityUpdate1 = new JdbcCrudReactorRepository.Entity("1", 5, "6", null); - var entityUpdate2 = new JdbcCrudReactorRepository.Entity("2", 7, "8", null); - assertEquals(2L, repository.updateBatch(List.of(entityUpdate1, entityUpdate2)).block(Duration.ofSeconds(15)).value()); - - var foundUpdated = repository.findAll().block(Duration.ofSeconds(15)); - assertEquals(2, foundUpdated.size()); - for (var entity : foundUpdated) { - if (entity.getId().equals("1")) { - assertEquals("1", entity.getId()); - assertEquals(5, entity.getField1()); - assertEquals("6", entity.getValue2()); - assertNull(entity.getValue3()); - } else { - assertEquals("2", entity.getId()); - assertEquals(7, entity.getField1()); - assertEquals("8", entity.getValue2()); - assertNull(entity.getValue3()); - } - } - - // then - assertEquals(2L, repository.deleteAll().block(Duration.ofSeconds(15)).value()); - assertTrue(repository.findAll().block(Duration.ofSeconds(15)).isEmpty()); - } -} diff --git a/examples/java/kora-java-database-r2dbc/README.md b/examples/java/kora-java-database-r2dbc/README.md deleted file mode 100644 index fda9aa06..00000000 --- a/examples/java/kora-java-database-r2dbc/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Kora Java Database R2DBC - -Пример модуля Database R2DBC в Kora. - -В примере использовались модули: -- [R2dbc Database](https://kora-projects.github.io/kora-docs/ru/documentation/database-r2dbc) - -## Build - -Собрать классы: - -```shell -./gradlew classes -``` - -Собрать артефакт: - -```shell -./gradlew distTar -``` - -## Run - -Запустить локально: -```shell -./gradlew run -``` - -## Test - -Тесты используют [Testcontainers](https://java.testcontainers.org/), требуется [Docker](https://docs.docker.com/engine/install/) окружение для запуска тестов или аналогичные контейнерные окружения ([colima](https://github.com/abiosoft/colima) / итп) - -Протестировать локально: -```shell -./gradlew test -``` diff --git a/examples/java/kora-java-database-r2dbc/build.gradle b/examples/java/kora-java-database-r2dbc/build.gradle deleted file mode 100644 index 2eb3abbf..00000000 --- a/examples/java/kora-java-database-r2dbc/build.gradle +++ /dev/null @@ -1,106 +0,0 @@ -plugins { - id "java" - id "jacoco" - id "application" -} - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(25) - vendor = JvmVendorSpec.ADOPTIUM - } -} - -configurations { - koraBom - annotationProcessor.extendsFrom(koraBom); compileOnly.extendsFrom(koraBom); implementation.extendsFrom(koraBom) - api.extendsFrom(koraBom); testImplementation.extendsFrom(koraBom); testAnnotationProcessor.extendsFrom(koraBom) -} - -dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") - annotationProcessor "io.koraframework:annotation-processors" - - runtimeOnly "io.netty:netty-transport-native-epoll:4.1.109.Final:linux-x86_64" - implementation "org.postgresql:r2dbc-postgresql:1.0.4.RELEASE" - implementation "io.koraframework:database-r2dbc" - - implementation "io.koraframework:logging-logback" - implementation "io.koraframework:config-hocon" - - testImplementation "io.koraframework:test-junit5" - testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.13.1" - testImplementation "org.postgresql:postgresql:42.7.7" -} - -application { - applicationName = "application" - mainClass = "io.koraframework.example.r2dbc.Application" - applicationDefaultJvmArgs = ["-Dfile.encoding=UTF-8"] -} - -//noinspection GroovyAssignabilityCheck -run { - environment([ - "POSTGRES_JDBC_URL": "r2dbc:postgresql://$postgresHost:$postgresPort/$postgresDatabase", - "POSTGRES_USER" : "$postgresUser", - "POSTGRES_PASS" : "$postgresPassword", - ]) -} - -distTar { - archiveFileName = "application.tar" -} - -test { - dependsOn tasks.distTar - - jvmArgs += [ - "-XX:+TieredCompilation", - "-XX:TieredStopAtLevel=1", - ] - - environment([ - "": "" - ]) - - useJUnitPlatform() - testLogging { - showStandardStreams = true - events("passed", "skipped", "failed") - exceptionFormat = "full" - } - - exclude("**/\$*") - - jacoco { - excludes += ["**/generated/**", "**/Application*", "**/\$*"] - } - - reports { - html.required = false - junitXml.required = false - } -} - -compileJava { - options.encoding = "UTF-8" - options.incremental = true - options.fork = false -} - -check.dependsOn jacocoTestReport -jacocoTestReport { - reports { - xml.required = true - html.outputLocation = layout.buildDirectory.dir("jacocoHtml") - } - classDirectories = files(classDirectories.files.collect { fileTree(dir: it, excludes: test.jacoco.excludes) }) -} - -javadoc { - options.encoding = "UTF-8" - if (JavaVersion.current().isJava9Compatible()) { - options.addBooleanOption("html5", true) - } -} \ No newline at end of file diff --git a/examples/java/kora-java-database-r2dbc/gradle.properties b/examples/java/kora-java-database-r2dbc/gradle.properties deleted file mode 100644 index 8e7a5091..00000000 --- a/examples/java/kora-java-database-r2dbc/gradle.properties +++ /dev/null @@ -1,6 +0,0 @@ -##### POSTGRES ##### -postgresHost=localhost -postgresPort=5432 -postgresUser=postgres -postgresPassword=postgres -postgresDatabase=postgres diff --git a/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/Application.java b/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/Application.java deleted file mode 100644 index 8dacffbc..00000000 --- a/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/Application.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.koraframework.example.r2dbc; - -import io.koraframework.application.graph.KoraApplication; -import io.koraframework.common.annotation.KoraApp; -import io.koraframework.config.hocon.HoconConfigModule; -import io.koraframework.database.r2dbc.R2dbcDatabaseModule; -import io.koraframework.logging.logback.LogbackModule; - -@KoraApp -public interface Application extends - HoconConfigModule, - LogbackModule, - R2dbcDatabaseModule { - - static void main(String[] args) { - KoraApplication.run(ApplicationGraph::graph); - } -} diff --git a/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcCrudMacrosRepository.java b/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcCrudMacrosRepository.java deleted file mode 100644 index 0c8ac67e..00000000 --- a/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcCrudMacrosRepository.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.koraframework.example.r2dbc; - -import org.jspecify.annotations.Nullable; -import java.util.List; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import io.koraframework.database.common.UpdateCount; -import io.koraframework.database.common.annotation.*; -import io.koraframework.database.r2dbc.R2dbcRepository; - -@Repository -public interface R2dbcCrudMacrosRepository extends R2dbcRepository { - - @Table("entities") - record Entity(@Id String id, - @Column("value1") int field1, - String value2, - @Nullable String value3) {} - - @Query("SELECT %{return#selects} FROM %{return#table} WHERE id = :id") - Mono findById(String id); - - @Query("SELECT %{return#selects} FROM %{return#table}") - Flux findAll(); - - @Query("SELECT %{return#selects} FROM %{return#table}") - Mono> findAllMono(); - - @Query("INSERT INTO %{entity#inserts}") - Mono insert(Entity entity); - - @Query("INSERT INTO %{entity#inserts}") - Mono insertBatch(@Batch List entity); - - @Query("UPDATE %{entity#table} SET %{entity#updates} WHERE %{entity#where = @id}") - Mono update(Entity entity); - - @Query("UPDATE %{entity#table} SET %{entity#updates} WHERE %{entity#where = @id}") - Mono updateBatch(@Batch List entity); - - @Query("INSERT INTO %{entity#inserts} ON CONFLICT (id) DO UPDATE SET %{entity#updates}") - Mono upsert(Entity entity); - - @Query("DELETE FROM entities WHERE id = :id") - Mono deleteById(String id); - - @Query("DELETE FROM entities") - Mono deleteAll(); -} diff --git a/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcCrudRepository.java b/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcCrudRepository.java deleted file mode 100644 index b7086c52..00000000 --- a/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcCrudRepository.java +++ /dev/null @@ -1,59 +0,0 @@ -package io.koraframework.example.r2dbc; - -import org.jspecify.annotations.Nullable; -import java.util.List; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import io.koraframework.database.common.UpdateCount; -import io.koraframework.database.common.annotation.*; -import io.koraframework.database.r2dbc.R2dbcRepository; - -@Repository -public interface R2dbcCrudRepository extends R2dbcRepository { - - record Entity(String id, - @Column("value1") int field1, - String value2, - @Nullable String value3) {} - - @Query("SELECT * FROM entities WHERE id = :id") - Mono findById(String id); - - @Query("SELECT * FROM entities") - Flux findAll(); - - @Query("SELECT * FROM entities") - Mono> findAllMono(); - - @Query(""" - INSERT INTO entities(id, value1, value2, value3) - VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3) - """) - Mono insert(Entity entity); - - @Query(""" - INSERT INTO entities(id, value1, value2, value3) - VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3) - """) - Mono insertBatch(@Batch List entity); - - @Query(""" - UPDATE entities - SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 - WHERE id = :entity.id - """) - Mono update(Entity entity); - - @Query(""" - UPDATE entities - SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 - WHERE id = :entity.id - """) - Mono updateBatch(@Batch List entity); - - @Query("DELETE FROM entities WHERE id = :id") - Mono deleteById(String id); - - @Query("DELETE FROM entities") - Mono deleteAll(); -} diff --git a/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcCrudSyncRepository.java b/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcCrudSyncRepository.java deleted file mode 100644 index 81aa3e48..00000000 --- a/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcCrudSyncRepository.java +++ /dev/null @@ -1,58 +0,0 @@ -package io.koraframework.example.r2dbc; - -import org.jspecify.annotations.Nullable; -import java.util.List; -import io.koraframework.database.common.UpdateCount; -import io.koraframework.database.common.annotation.Batch; -import io.koraframework.database.common.annotation.Column; -import io.koraframework.database.common.annotation.Query; -import io.koraframework.database.common.annotation.Repository; -import io.koraframework.database.r2dbc.R2dbcRepository; - -@Repository -public interface R2dbcCrudSyncRepository extends R2dbcRepository { - - record Entity(String id, - @Column("value1") int field1, - String value2, - @Nullable String value3) {} - - @Query("SELECT * FROM entities WHERE id = :id") - @Nullable - Entity findById(String id); - - @Query("SELECT * FROM entities") - List findAll(); - - @Query(""" - INSERT INTO entities(id, value1, value2, value3) - VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3) - """) - void insert(Entity entity); - - @Query(""" - INSERT INTO entities(id, value1, value2, value3) - VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3) - """) - UpdateCount insertBatch(@Batch List entity); - - @Query(""" - UPDATE entities - SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 - WHERE id = :entity.id - """) - void update(Entity entity); - - @Query(""" - UPDATE entities - SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 - WHERE id = :entity.id - """) - UpdateCount updateBatch(@Batch List entity); - - @Query("DELETE FROM entities WHERE id = :id") - void deleteById(String id); - - @Query("DELETE FROM entities") - UpdateCount deleteAll(); -} diff --git a/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcIdRandomRepository.java b/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcIdRandomRepository.java deleted file mode 100644 index 4002cea5..00000000 --- a/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcIdRandomRepository.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.koraframework.example.r2dbc; - -import java.util.UUID; -import reactor.core.publisher.Mono; -import io.koraframework.database.common.UpdateCount; -import io.koraframework.database.common.annotation.Column; -import io.koraframework.database.common.annotation.Query; -import io.koraframework.database.common.annotation.Repository; -import io.koraframework.database.r2dbc.R2dbcRepository; - -@Repository -public interface R2dbcIdRandomRepository extends R2dbcRepository { - - record Entity(UUID id, - @Column("name") String name) { - - public Entity(String name) { - this(UUID.randomUUID(), name); - } - } - - @Query("SELECT * FROM entities_uuid WHERE id = :id") - Mono findById(UUID id); - - @Query(""" - INSERT INTO entities_uuid(id, name) - VALUES (:entity.id, :entity.name) - """) - Mono insert(Entity entity); -} diff --git a/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcIdSequenceRepository.java b/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcIdSequenceRepository.java deleted file mode 100644 index d1051a4b..00000000 --- a/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcIdSequenceRepository.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.koraframework.example.r2dbc; - -import org.jspecify.annotations.Nullable; -import java.util.List; -import reactor.core.publisher.Mono; -import io.koraframework.database.common.annotation.*; -import io.koraframework.database.r2dbc.R2dbcRepository; - -@Repository -public interface R2dbcIdSequenceRepository extends R2dbcRepository { - - record Entity(Long id, - @Column("name") String name) { - - public Entity(String name) { - this(null, name); - } - } - - @Query("SELECT * FROM entities_sequence WHERE id = :id") - @Nullable - Mono findById(long id); - - @Query(""" - INSERT INTO entities_sequence(name) - VALUES (:entity.name) - RETURNING id - """) - Mono insert(Entity entity); - - @Query(""" - INSERT INTO entities_sequence(name) - VALUES (:entity.name) - """) - @Id - Mono createGenerated(Entity entity); - - @Query(""" - INSERT INTO entities_sequence(name) - VALUES (:entity.name) - """) - @Id - Mono> createGenerated(@Batch List entity); -} diff --git a/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcMapperColumnRepository.java b/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcMapperColumnRepository.java deleted file mode 100644 index 285efa14..00000000 --- a/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcMapperColumnRepository.java +++ /dev/null @@ -1,75 +0,0 @@ -package io.koraframework.example.r2dbc; - -import io.r2dbc.spi.Row; -import io.r2dbc.spi.Statement; -import org.jspecify.annotations.Nullable; -import reactor.core.publisher.Flux; -import io.koraframework.common.annotation.Mapping; -import io.koraframework.database.common.annotation.*; -import io.koraframework.database.r2dbc.R2dbcRepository; -import io.koraframework.database.r2dbc.mapper.parameter.R2dbcParameterColumnMapper; -import io.koraframework.database.r2dbc.mapper.result.R2dbcResultColumnMapper; - -@Repository -public interface R2dbcMapperColumnRepository extends R2dbcRepository { - - final class EntityFieldTypeResultMapper implements R2dbcResultColumnMapper { - - private static final Entity.FieldType[] ALL = Entity.FieldType.values(); - - @Nullable - @Override - public Entity.FieldType apply(Row row, String label) { - var fieldAsInt = row.get(label, Integer.class); - if (fieldAsInt == null) { - return null; - } - - for (var type : ALL) { - if (type.code() == fieldAsInt) { - return type; - } - } - - return Entity.FieldType.UNKNOWN; - } - } - - final class EntityFieldTypeParameterMapper implements R2dbcParameterColumnMapper { - - @Override - public void apply(Statement stmt, int index, @Nullable Entity.FieldType value) { - if (value != null) { - stmt.bind(index, value.code()); - } - } - } - - record Entity(String id, - @Mapping(EntityFieldTypeResultMapper.class) - @Mapping(EntityFieldTypeParameterMapper.class) - @Column("value1") FieldType field1, - String value2, - @Nullable String value3) { - - enum FieldType { - - UNKNOWN(-10), - ONE(1), - TWO(2); - - private final int code; - - FieldType(int code) { - this.code = code; - } - - public int code() { - return code; - } - } - } - - @Query("SELECT * FROM entities") - Flux findAll(); -} diff --git a/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcMapperParameterRepository.java b/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcMapperParameterRepository.java deleted file mode 100644 index 51841739..00000000 --- a/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcMapperParameterRepository.java +++ /dev/null @@ -1,55 +0,0 @@ -package io.koraframework.example.r2dbc; - -import io.r2dbc.spi.Statement; -import org.jspecify.annotations.Nullable; -import reactor.core.publisher.Mono; -import io.koraframework.common.annotation.Mapping; -import io.koraframework.database.common.UpdateCount; -import io.koraframework.database.common.annotation.*; -import io.koraframework.database.r2dbc.R2dbcRepository; -import io.koraframework.database.r2dbc.mapper.parameter.R2dbcParameterColumnMapper; - -@Repository -public interface R2dbcMapperParameterRepository extends R2dbcRepository { - - final class EntityFieldTypeParameterMapper implements R2dbcParameterColumnMapper { - - @Override - public void apply(Statement stmt, int index, @Nullable Entity.FieldType value) { - if (value != null) { - stmt.bind(index, value.code()); - } - } - } - - record Entity(String id, - @Column("value1") int field1, - String value2, - @Nullable String value3) { - - enum FieldType { - - UNKNOWN(-10), - ONE(1), - TWO(2); - - private final int code; - - FieldType(int code) { - this.code = code; - } - - public int code() { - return code; - } - } - } - - @Query(""" - UPDATE entities - SET value1 = :fieldType - WHERE id = :id - """) - Mono updateFieldType(String id, - @Mapping(EntityFieldTypeParameterMapper.class) Entity.FieldType fieldType); -} diff --git a/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcMapperRowRepository.java b/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcMapperRowRepository.java deleted file mode 100644 index 7306fb76..00000000 --- a/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/R2dbcMapperRowRepository.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.koraframework.example.r2dbc; - -import io.r2dbc.spi.Row; -import reactor.core.publisher.Flux; -import io.koraframework.common.annotation.Mapping; -import io.koraframework.database.common.annotation.Query; -import io.koraframework.database.common.annotation.Repository; -import io.koraframework.database.r2dbc.R2dbcRepository; -import io.koraframework.database.r2dbc.mapper.result.R2dbcRowMapper; - -@Repository -public interface R2dbcMapperRowRepository extends R2dbcRepository { - - record EntityPart(String id, int field1) {} - - final class EntityPartRowMapper implements R2dbcRowMapper { - - @Override - public EntityPart apply(Row row) { - return new EntityPart(row.get(0, String.class), row.get(1, Integer.class)); - } - } - - @Mapping(EntityPartRowMapper.class) - @Query("SELECT id, value1 FROM entities") - Flux findAllParts(); -} diff --git a/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/RootService.java b/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/RootService.java deleted file mode 100644 index 23100970..00000000 --- a/examples/java/kora-java-database-r2dbc/src/main/java/io/koraframework/example/r2dbc/RootService.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.koraframework.example.r2dbc; - -import io.koraframework.common.annotation.Component; -import io.koraframework.common.annotation.Root; - -@Root -@Component -public final class RootService { - - private final R2dbcCrudRepository r2dbcCrudRepository; - private final R2dbcIdRandomRepository r2dbcIdRandomRepository; - private final R2dbcIdSequenceRepository r2dbcIdSequenceRepository; - private final R2dbcMapperColumnRepository r2dbcMapperColumnRepository; - private final R2dbcMapperRowRepository r2dbcMapperRowRepository; - private final R2dbcMapperParameterRepository r2dbcMapperParameterRepository; - private final R2dbcCrudMacrosRepository r2dbcCrudMacrosRepository; - private final R2dbcCrudSyncRepository r2dbcCrudSyncRepository; - - public RootService(R2dbcCrudRepository r2dbcCrudRepository, - R2dbcIdRandomRepository r2dbcIdRandomRepository, - R2dbcIdSequenceRepository r2dbcIdSequenceRepository, - R2dbcMapperColumnRepository r2dbcMapperColumnRepository, - R2dbcMapperRowRepository r2dbcMapperRowRepository, - R2dbcMapperParameterRepository r2dbcMapperParameterRepository, - R2dbcCrudMacrosRepository r2dbcCrudMacrosRepository, - R2dbcCrudSyncRepository r2dbcCrudSyncRepository) { - this.r2dbcCrudRepository = r2dbcCrudRepository; - this.r2dbcIdRandomRepository = r2dbcIdRandomRepository; - this.r2dbcIdSequenceRepository = r2dbcIdSequenceRepository; - this.r2dbcMapperColumnRepository = r2dbcMapperColumnRepository; - this.r2dbcMapperRowRepository = r2dbcMapperRowRepository; - this.r2dbcMapperParameterRepository = r2dbcMapperParameterRepository; - this.r2dbcCrudMacrosRepository = r2dbcCrudMacrosRepository; - this.r2dbcCrudSyncRepository = r2dbcCrudSyncRepository; - } -} diff --git a/examples/java/kora-java-database-r2dbc/src/main/resources/application.conf b/examples/java/kora-java-database-r2dbc/src/main/resources/application.conf deleted file mode 100644 index e8d51862..00000000 --- a/examples/java/kora-java-database-r2dbc/src/main/resources/application.conf +++ /dev/null @@ -1,15 +0,0 @@ -db { - r2dbcUrl = ${POSTGRES_R2DBC_URL} - username = ${POSTGRES_USER} - password = ${POSTGRES_PASS} - maxPoolSize = 10 - poolName = "kora" - telemetry.logging.enabled = true -} - - -logging.level { - "root": "WARN" - "io.koraframework": "INFO" - "io.koraframework.example": "INFO" -} diff --git a/examples/java/kora-java-database-r2dbc/src/main/resources/db/migration/V1__setup-tables.sql b/examples/java/kora-java-database-r2dbc/src/main/resources/db/migration/V1__setup-tables.sql deleted file mode 100644 index 149017c9..00000000 --- a/examples/java/kora-java-database-r2dbc/src/main/resources/db/migration/V1__setup-tables.sql +++ /dev/null @@ -1,33 +0,0 @@ -CREATE TABLE IF NOT EXISTS entities -( - id VARCHAR NOT NULL, - value1 INT NOT NULL, - value2 VARCHAR NOT NULL, - value3 VARCHAR, - PRIMARY KEY (id) -); - - -CREATE TABLE IF NOT EXISTS entities_sequence -( - id BIGINT GENERATED ALWAYS AS IDENTITY, - name VARCHAR NOT NULL, - code VARCHAR NOT NULL DEFAULT '1', - PRIMARY KEY (id) -); - - -CREATE TABLE IF NOT EXISTS entities_uuid -( - id UUID NOT NULL, - name VARCHAR NOT NULL, - PRIMARY KEY (id) -); - - -CREATE TABLE IF NOT EXISTS entities_jsonb -( - id UUID NOT NULL, - value JSONB NOT NULL, - PRIMARY KEY (id) -); diff --git a/examples/java/kora-java-database-r2dbc/src/main/resources/logback.xml b/examples/java/kora-java-database-r2dbc/src/main/resources/logback.xml deleted file mode 100644 index 184638b5..00000000 --- a/examples/java/kora-java-database-r2dbc/src/main/resources/logback.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcCrudMacrosTests.java b/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcCrudMacrosTests.java deleted file mode 100644 index 0a3d957f..00000000 --- a/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcCrudMacrosTests.java +++ /dev/null @@ -1,122 +0,0 @@ -package io.koraframework.example.r2dbc; - -import static org.junit.jupiter.api.Assertions.*; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL; -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; -import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL; -import java.time.Duration; -import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = @Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class R2dbcCrudMacrosTests implements KoraAppTestConfigModifier { - - @ConnectionPostgreSQL - private JdbcConnection connection; - - @TestComponent - private R2dbcCrudMacrosRepository repository; - - @NotNull - @Override - public KoraConfigModification config() { - var params = connection.params(); - var url = "r2dbc:postgresql://%s:%s/%s".formatted(params.host(), params.port(), params.database()); - return KoraConfigModification.ofSystemProperty("POSTGRES_R2DBC_URL", url) - .withSystemProperty("POSTGRES_USER", params.username()) - .withSystemProperty("POSTGRES_PASS", params.password()); - } - - @Test - void monoSingleSuccess() { - // given - var entityCreate = new R2dbcCrudMacrosRepository.Entity("1", 1, "2", null); - repository.insert(entityCreate).block(Duration.ofSeconds(15)); - - var foundCreated = repository.findById("1").block(Duration.ofSeconds(15)); - assertNotNull(foundCreated); - assertEquals("1", foundCreated.id()); - assertEquals(1, foundCreated.field1()); - assertEquals("2", foundCreated.value2()); - assertNull(foundCreated.value3()); - - // when - var entityUpdate = new R2dbcCrudMacrosRepository.Entity("1", 5, "6", null); - repository.update(entityUpdate).block(Duration.ofSeconds(15)); - - var foundUpdated = repository.findById("1").block(Duration.ofSeconds(15)); - assertNotNull(foundUpdated); - assertEquals("1", foundUpdated.id()); - assertEquals(5, foundUpdated.field1()); - assertEquals("6", foundUpdated.value2()); - assertNull(foundUpdated.value3()); - - // then - repository.deleteById("1").block(Duration.ofSeconds(15)); - assertNull(repository.findById("1").block(Duration.ofSeconds(15))); - } - - @Test - void monoBatchSuccess() { - // given - var entityCreate1 = new R2dbcCrudMacrosRepository.Entity("1", 1, "2", null); - var entityCreate2 = new R2dbcCrudMacrosRepository.Entity("2", 3, "4", null); - assertEquals(2L, repository.insertBatch(List.of(entityCreate1, entityCreate2)).block(Duration.ofSeconds(15)).value()); - - var foundCreated = repository.findAllMono().block(Duration.ofSeconds(15)); - assertNotNull(foundCreated); - assertEquals(2, foundCreated.size()); - for (var entity : foundCreated) { - if (entity.id().equals("1")) { - assertEquals("1", entity.id()); - assertEquals(1, entity.field1()); - assertEquals("2", entity.value2()); - assertNull(entity.value3()); - } else { - assertEquals("2", entity.id()); - assertEquals(3, entity.field1()); - assertEquals("4", entity.value2()); - assertNull(entity.value3()); - } - } - - // when - var entityUpdate1 = new R2dbcCrudMacrosRepository.Entity("1", 5, "6", null); - var entityUpdate2 = new R2dbcCrudMacrosRepository.Entity("2", 7, "8", null); - assertEquals(2L, repository.updateBatch(List.of(entityUpdate1, entityUpdate2)).block(Duration.ofSeconds(15)).value()); - - var foundUpdated = repository.findAll().collectList().block(Duration.ofSeconds(15)); - assertEquals(2, foundUpdated.size()); - for (var entity : foundUpdated) { - if (entity.id().equals("1")) { - assertEquals("1", entity.id()); - assertEquals(5, entity.field1()); - assertEquals("6", entity.value2()); - assertNull(entity.value3()); - } else { - assertEquals("2", entity.id()); - assertEquals(7, entity.field1()); - assertEquals("8", entity.value2()); - assertNull(entity.value3()); - } - } - - // then - assertEquals(2L, repository.deleteAll().block(Duration.ofSeconds(15)).value()); - assertTrue(repository.findAll().collectList().block(Duration.ofSeconds(15)).isEmpty()); - } -} diff --git a/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcCrudSyncTests.java b/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcCrudSyncTests.java deleted file mode 100644 index b9b479f9..00000000 --- a/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcCrudSyncTests.java +++ /dev/null @@ -1,121 +0,0 @@ -package io.koraframework.example.r2dbc; - -import static org.junit.jupiter.api.Assertions.*; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL; -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; -import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL; -import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = @Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class R2dbcCrudSyncTests implements KoraAppTestConfigModifier { - - @ConnectionPostgreSQL - private JdbcConnection connection; - - @TestComponent - private R2dbcCrudSyncRepository repository; - - @NotNull - @Override - public KoraConfigModification config() { - var params = connection.params(); - var url = "r2dbc:postgresql://%s:%s/%s".formatted(params.host(), params.port(), params.database()); - return KoraConfigModification.ofSystemProperty("POSTGRES_R2DBC_URL", url) - .withSystemProperty("POSTGRES_USER", params.username()) - .withSystemProperty("POSTGRES_PASS", params.password()); - } - - @Test - void monoSingleSuccess() { - // given - var entityCreate = new R2dbcCrudSyncRepository.Entity("1", 1, "2", null); - repository.insert(entityCreate); - - var foundCreated = repository.findById("1"); - assertNotNull(foundCreated); - assertEquals("1", foundCreated.id()); - assertEquals(1, foundCreated.field1()); - assertEquals("2", foundCreated.value2()); - assertNull(foundCreated.value3()); - - // when - var entityUpdate = new R2dbcCrudSyncRepository.Entity("1", 5, "6", null); - repository.update(entityUpdate); - - var foundUpdated = repository.findById("1"); - assertNotNull(foundUpdated); - assertEquals("1", foundUpdated.id()); - assertEquals(5, foundUpdated.field1()); - assertEquals("6", foundUpdated.value2()); - assertNull(foundUpdated.value3()); - - // then - repository.deleteById("1"); - assertNull(repository.findById("1")); - } - - @Test - void monoBatchSuccess() { - // given - var entityCreate1 = new R2dbcCrudSyncRepository.Entity("1", 1, "2", null); - var entityCreate2 = new R2dbcCrudSyncRepository.Entity("2", 3, "4", null); - assertEquals(2L, repository.insertBatch(List.of(entityCreate1, entityCreate2)).value()); - - var foundCreated = repository.findAll(); - assertNotNull(foundCreated); - assertEquals(2, foundCreated.size()); - for (var entity : foundCreated) { - if (entity.id().equals("1")) { - assertEquals("1", entity.id()); - assertEquals(1, entity.field1()); - assertEquals("2", entity.value2()); - assertNull(entity.value3()); - } else { - assertEquals("2", entity.id()); - assertEquals(3, entity.field1()); - assertEquals("4", entity.value2()); - assertNull(entity.value3()); - } - } - - // when - var entityUpdate1 = new R2dbcCrudSyncRepository.Entity("1", 5, "6", null); - var entityUpdate2 = new R2dbcCrudSyncRepository.Entity("2", 7, "8", null); - assertEquals(2L, repository.updateBatch(List.of(entityUpdate1, entityUpdate2)).value()); - - var foundUpdated = repository.findAll(); - assertEquals(2, foundUpdated.size()); - for (var entity : foundUpdated) { - if (entity.id().equals("1")) { - assertEquals("1", entity.id()); - assertEquals(5, entity.field1()); - assertEquals("6", entity.value2()); - assertNull(entity.value3()); - } else { - assertEquals("2", entity.id()); - assertEquals(7, entity.field1()); - assertEquals("8", entity.value2()); - assertNull(entity.value3()); - } - } - - // then - assertEquals(2L, repository.deleteAll().value()); - assertTrue(repository.findAll().isEmpty()); - } -} diff --git a/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcCrudTests.java b/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcCrudTests.java deleted file mode 100644 index f2dea733..00000000 --- a/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcCrudTests.java +++ /dev/null @@ -1,122 +0,0 @@ -package io.koraframework.example.r2dbc; - -import static org.junit.jupiter.api.Assertions.*; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL; -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; -import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL; -import java.time.Duration; -import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = @Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class R2dbcCrudTests implements KoraAppTestConfigModifier { - - @ConnectionPostgreSQL - private JdbcConnection connection; - - @TestComponent - private R2dbcCrudRepository repository; - - @NotNull - @Override - public KoraConfigModification config() { - var params = connection.params(); - var url = "r2dbc:postgresql://%s:%s/%s".formatted(params.host(), params.port(), params.database()); - return KoraConfigModification.ofSystemProperty("POSTGRES_R2DBC_URL", url) - .withSystemProperty("POSTGRES_USER", params.username()) - .withSystemProperty("POSTGRES_PASS", params.password()); - } - - @Test - void monoSingleSuccess() { - // given - var entityCreate = new R2dbcCrudRepository.Entity("1", 1, "2", null); - repository.insert(entityCreate).block(Duration.ofSeconds(15)); - - var foundCreated = repository.findById("1").block(Duration.ofSeconds(15)); - assertNotNull(foundCreated); - assertEquals("1", foundCreated.id()); - assertEquals(1, foundCreated.field1()); - assertEquals("2", foundCreated.value2()); - assertNull(foundCreated.value3()); - - // when - var entityUpdate = new R2dbcCrudRepository.Entity("1", 5, "6", null); - repository.update(entityUpdate).block(Duration.ofSeconds(15)); - - var foundUpdated = repository.findById("1").block(Duration.ofSeconds(15)); - assertNotNull(foundUpdated); - assertEquals("1", foundUpdated.id()); - assertEquals(5, foundUpdated.field1()); - assertEquals("6", foundUpdated.value2()); - assertNull(foundUpdated.value3()); - - // then - repository.deleteById("1").block(Duration.ofSeconds(15)); - assertNull(repository.findById("1").block(Duration.ofSeconds(15))); - } - - @Test - void monoBatchSuccess() { - // given - var entityCreate1 = new R2dbcCrudRepository.Entity("1", 1, "2", null); - var entityCreate2 = new R2dbcCrudRepository.Entity("2", 3, "4", null); - assertEquals(2L, repository.insertBatch(List.of(entityCreate1, entityCreate2)).block(Duration.ofSeconds(15)).value()); - - var foundCreated = repository.findAllMono().block(Duration.ofSeconds(15)); - assertNotNull(foundCreated); - assertEquals(2, foundCreated.size()); - for (var entity : foundCreated) { - if (entity.id().equals("1")) { - assertEquals("1", entity.id()); - assertEquals(1, entity.field1()); - assertEquals("2", entity.value2()); - assertNull(entity.value3()); - } else { - assertEquals("2", entity.id()); - assertEquals(3, entity.field1()); - assertEquals("4", entity.value2()); - assertNull(entity.value3()); - } - } - - // when - var entityUpdate1 = new R2dbcCrudRepository.Entity("1", 5, "6", null); - var entityUpdate2 = new R2dbcCrudRepository.Entity("2", 7, "8", null); - assertEquals(2L, repository.updateBatch(List.of(entityUpdate1, entityUpdate2)).block(Duration.ofSeconds(15)).value()); - - var foundUpdated = repository.findAll().collectList().block(Duration.ofSeconds(15)); - assertEquals(2, foundUpdated.size()); - for (var entity : foundUpdated) { - if (entity.id().equals("1")) { - assertEquals("1", entity.id()); - assertEquals(5, entity.field1()); - assertEquals("6", entity.value2()); - assertNull(entity.value3()); - } else { - assertEquals("2", entity.id()); - assertEquals(7, entity.field1()); - assertEquals("8", entity.value2()); - assertNull(entity.value3()); - } - } - - // then - assertEquals(2L, repository.deleteAll().block(Duration.ofSeconds(15)).value()); - assertTrue(repository.findAll().collectList().block(Duration.ofSeconds(15)).isEmpty()); - } -} diff --git a/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcIdRandomTests.java b/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcIdRandomTests.java deleted file mode 100644 index a58534b7..00000000 --- a/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcIdRandomTests.java +++ /dev/null @@ -1,57 +0,0 @@ -package io.koraframework.example.r2dbc; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL; -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; -import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = @Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class R2dbcIdRandomTests implements KoraAppTestConfigModifier { - - @ConnectionPostgreSQL - private JdbcConnection connection; - - @TestComponent - private R2dbcIdRandomRepository repository; - - @NotNull - @Override - public KoraConfigModification config() { - var params = connection.params(); - var url = "r2dbc:postgresql://%s:%s/%s".formatted(params.host(), params.port(), params.database()); - return KoraConfigModification.ofSystemProperty("POSTGRES_R2DBC_URL", url) - .withSystemProperty("POSTGRES_USER", params.username()) - .withSystemProperty("POSTGRES_PASS", params.password()); - } - - @Test - void syncSingleSuccess() { - // given - var entityCreate = new R2dbcIdRandomRepository.Entity("Ivan"); - - // when - repository.insert(entityCreate).block(); - - // then - var foundCreated = repository.findById(entityCreate.id()).block(); - assertNotNull(foundCreated); - assertEquals(entityCreate.id(), foundCreated.id()); - assertEquals(entityCreate.name(), foundCreated.name()); - } -} diff --git a/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcIdSequenceTests.java b/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcIdSequenceTests.java deleted file mode 100644 index bb2062b6..00000000 --- a/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcIdSequenceTests.java +++ /dev/null @@ -1,68 +0,0 @@ -package io.koraframework.example.r2dbc; - -import static org.junit.jupiter.api.Assertions.*; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL; -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; -import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL; -import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = @Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class R2dbcIdSequenceTests implements KoraAppTestConfigModifier { - - @ConnectionPostgreSQL - private JdbcConnection connection; - - @TestComponent - private R2dbcIdSequenceRepository repository; - - @NotNull - @Override - public KoraConfigModification config() { - var params = connection.params(); - var url = "r2dbc:postgresql://%s:%s/%s".formatted(params.host(), params.port(), params.database()); - return KoraConfigModification.ofSystemProperty("POSTGRES_R2DBC_URL", url) - .withSystemProperty("POSTGRES_USER", params.username()) - .withSystemProperty("POSTGRES_PASS", params.password()); - } - - @Test - void syncSingleSuccess() { - // given - var entityCreate = new R2dbcIdSequenceRepository.Entity("Ivan"); - - // when - long id = repository.insert(entityCreate).block(); - assertNotEquals(0, id); - - var ids = repository - .createGenerated(List.of(new R2dbcIdSequenceRepository.Entity("b1"), new R2dbcIdSequenceRepository.Entity("b2"))) - .block(); - for (Long id2 : ids) { - assertNotEquals(0, id2); - } - - var id3 = repository.createGenerated(new R2dbcIdSequenceRepository.Entity("b3")).block(); - assertNotEquals(0, id3); - - // then - var foundCreated = repository.findById(id).block(); - assertNotNull(foundCreated); - assertEquals(id, foundCreated.id()); - assertEquals(entityCreate.name(), foundCreated.name()); - } -} diff --git a/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcMapperColumnTests.java b/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcMapperColumnTests.java deleted file mode 100644 index 2946182e..00000000 --- a/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcMapperColumnTests.java +++ /dev/null @@ -1,68 +0,0 @@ -package io.koraframework.example.r2dbc; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL; -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; -import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL; -import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = @Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class R2dbcMapperColumnTests implements KoraAppTestConfigModifier { - - @ConnectionPostgreSQL - private JdbcConnection connection; - - @TestComponent - private R2dbcCrudRepository crudRepository; - @TestComponent - private R2dbcMapperColumnRepository mapperRepository; - - @NotNull - @Override - public KoraConfigModification config() { - var params = connection.params(); - var url = "r2dbc:postgresql://%s:%s/%s".formatted(params.host(), params.port(), params.database()); - return KoraConfigModification.ofSystemProperty("POSTGRES_R2DBC_URL", url) - .withSystemProperty("POSTGRES_USER", params.username()) - .withSystemProperty("POSTGRES_PASS", params.password()); - } - - @Test - void columnMapperSuccess() { - // given - var entity1 = new R2dbcCrudRepository.Entity("1", 1, "2", null); - var entity2 = new R2dbcCrudRepository.Entity("2", 3, "4", null); - - // when - assertEquals(2, crudRepository.insertBatch(List.of(entity1, entity2)).block().value()); - - // then - var parts = mapperRepository.findAll().collectList().block(); - assertEquals(2, parts.size()); - for (var part : parts) { - if (part.id().equals("1")) { - assertEquals("1", part.id()); - Assertions.assertEquals(R2dbcMapperColumnRepository.Entity.FieldType.ONE, part.field1()); - } else { - assertEquals("2", part.id()); - Assertions.assertEquals(R2dbcMapperColumnRepository.Entity.FieldType.UNKNOWN, part.field1()); - } - } - } -} diff --git a/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcMapperParameterTests.java b/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcMapperParameterTests.java deleted file mode 100644 index 4dd8ec08..00000000 --- a/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcMapperParameterTests.java +++ /dev/null @@ -1,61 +0,0 @@ -package io.koraframework.example.r2dbc; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL; -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; -import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL; -import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = @Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class R2dbcMapperParameterTests implements KoraAppTestConfigModifier { - - @ConnectionPostgreSQL - private JdbcConnection connection; - - @TestComponent - private R2dbcCrudRepository crudRepository; - @TestComponent - private R2dbcMapperParameterRepository mapperRepository; - - @NotNull - @Override - public KoraConfigModification config() { - var params = connection.params(); - var url = "r2dbc:postgresql://%s:%s/%s".formatted(params.host(), params.port(), params.database()); - return KoraConfigModification.ofSystemProperty("POSTGRES_R2DBC_URL", url) - .withSystemProperty("POSTGRES_USER", params.username()) - .withSystemProperty("POSTGRES_PASS", params.password()); - } - - @Test - void parameterMapperSuccess() { - // given - var entity1 = new R2dbcCrudRepository.Entity("1", R2dbcMapperParameterRepository.Entity.FieldType.ONE.code(), "2", null); - var entity2 = new R2dbcCrudRepository.Entity("2", 3, "4", null); - assertEquals(2, crudRepository.insertBatch(List.of(entity1, entity2)).block().value()); - - // when - mapperRepository.updateFieldType(entity1.id(), R2dbcMapperParameterRepository.Entity.FieldType.TWO).block(); - - // then - var found = crudRepository.findById(entity1.id()).block(); - assertNotNull(found); - assertEquals(R2dbcMapperParameterRepository.Entity.FieldType.TWO.code(), found.field1()); - } -} diff --git a/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcMapperRowTests.java b/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcMapperRowTests.java deleted file mode 100644 index 0d09f50a..00000000 --- a/examples/java/kora-java-database-r2dbc/src/test/java/io/koraframework/example/r2dbc/R2dbcMapperRowTests.java +++ /dev/null @@ -1,58 +0,0 @@ -package io.koraframework.example.r2dbc; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL; -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; -import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL; -import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = @Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class R2dbcMapperRowTests implements KoraAppTestConfigModifier { - - @ConnectionPostgreSQL - private JdbcConnection connection; - - @TestComponent - private R2dbcCrudRepository crudRepository; - @TestComponent - private R2dbcMapperRowRepository mapperRepository; - - @NotNull - @Override - public KoraConfigModification config() { - var params = connection.params(); - var url = "r2dbc:postgresql://%s:%s/%s".formatted(params.host(), params.port(), params.database()); - return KoraConfigModification.ofSystemProperty("POSTGRES_R2DBC_URL", url) - .withSystemProperty("POSTGRES_USER", params.username()) - .withSystemProperty("POSTGRES_PASS", params.password()); - } - - @Test - void rowMapperSuccess() { - // given - var entity1 = new R2dbcCrudRepository.Entity("1", 1, "2", null); - var entity2 = new R2dbcCrudRepository.Entity("2", 3, "4", null); - - // when - assertEquals(2, crudRepository.insertBatch(List.of(entity1, entity2)).block().value()); - - // then - var found = mapperRepository.findAllParts().collectList().block(); - assertEquals(2, found.size()); - } -} diff --git a/examples/java/kora-java-database-r2dbc/src/test/resources/logback-test.xml b/examples/java/kora-java-database-r2dbc/src/test/resources/logback-test.xml deleted file mode 100644 index bac04fb1..00000000 --- a/examples/java/kora-java-database-r2dbc/src/test/resources/logback-test.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - UTF-8 - %cyan(%d{HH:mm:ss.SSS}) %highlight(%-5level) [%thread] %logger{36} - %msg%n - - - - - - - - - - - - - - - - - - - - diff --git a/examples/java/kora-java-database-vertx/README.md b/examples/java/kora-java-database-vertx/README.md deleted file mode 100644 index 30013917..00000000 --- a/examples/java/kora-java-database-vertx/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Kora Java Database Vertx - -Пример модуля Database Vertx в Kora. - -В примере использовались модули: -- [Vertx Database](https://kora-projects.github.io/kora-docs/ru/documentation/database-vertx) - -## Build - -Собрать классы: - -```shell -./gradlew classes -``` - -Собрать артефакт: - -```shell -./gradlew distTar -``` - -## Run - -Запустить локально: -```shell -./gradlew run -``` - -## Test - -Тесты используют [Testcontainers](https://java.testcontainers.org/), требуется [Docker](https://docs.docker.com/engine/install/) окружение для запуска тестов или аналогичные контейнерные окружения ([colima](https://github.com/abiosoft/colima) / итп) - -Протестировать локально: -```shell -./gradlew test -``` diff --git a/examples/java/kora-java-database-vertx/build.gradle b/examples/java/kora-java-database-vertx/build.gradle deleted file mode 100644 index f471a76b..00000000 --- a/examples/java/kora-java-database-vertx/build.gradle +++ /dev/null @@ -1,107 +0,0 @@ -plugins { - id "java" - id "jacoco" - id "application" -} - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(25) - vendor = JvmVendorSpec.ADOPTIUM - } -} - -configurations { - koraBom - annotationProcessor.extendsFrom(koraBom); compileOnly.extendsFrom(koraBom); implementation.extendsFrom(koraBom) - api.extendsFrom(koraBom); testImplementation.extendsFrom(koraBom); testAnnotationProcessor.extendsFrom(koraBom) -} - -dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") - annotationProcessor "io.koraframework:annotation-processors" - - implementation "io.koraframework:database-vertx" - implementation "io.vertx:vertx-pg-client:4.3.8" - implementation "com.ongres.scram:client:2.1" - implementation "io.projectreactor:reactor-core:3.6.18" // For reactive examples (optional) - - implementation "io.koraframework:logging-logback" - implementation "io.koraframework:config-hocon" - - testImplementation "io.koraframework:test-junit5" - testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.13.1" - testImplementation "org.postgresql:postgresql:42.7.7" -} - -application { - applicationName = "application" - mainClass = "io.koraframework.example.vertx.Application" - applicationDefaultJvmArgs = ["-Dfile.encoding=UTF-8"] -} - -//noinspection GroovyAssignabilityCheck -run { - environment([ - "POSTGRES_VERTX_URI": "postgresql://$postgresHost:$postgresPort/$postgresDatabase", - "POSTGRES_USER" : "$postgresUser", - "POSTGRES_PASS" : "$postgresPassword", - ]) -} - -distTar { - archiveFileName = "application.tar" -} - -test { - dependsOn tasks.distTar - - jvmArgs += [ - "-XX:+TieredCompilation", - "-XX:TieredStopAtLevel=1", - ] - - environment([ - "": "" - ]) - - useJUnitPlatform() - testLogging { - showStandardStreams = true - events("passed", "skipped", "failed") - exceptionFormat = "full" - } - - exclude("**/\$*") - - jacoco { - excludes += ["**/generated/**", "**/Application*", "**/\$*"] - } - - reports { - html.required = false - junitXml.required = false - } -} - -compileJava { - options.encoding = "UTF-8" - options.incremental = true - options.fork = false -} - -check.dependsOn jacocoTestReport -jacocoTestReport { - reports { - xml.required = true - html.outputLocation = layout.buildDirectory.dir("jacocoHtml") - } - classDirectories = files(classDirectories.files.collect { fileTree(dir: it, excludes: test.jacoco.excludes) }) -} - -javadoc { - options.encoding = "UTF-8" - if (JavaVersion.current().isJava9Compatible()) { - options.addBooleanOption("html5", true) - } -} \ No newline at end of file diff --git a/examples/java/kora-java-database-vertx/gradle.properties b/examples/java/kora-java-database-vertx/gradle.properties deleted file mode 100644 index 8e7a5091..00000000 --- a/examples/java/kora-java-database-vertx/gradle.properties +++ /dev/null @@ -1,6 +0,0 @@ -##### POSTGRES ##### -postgresHost=localhost -postgresPort=5432 -postgresUser=postgres -postgresPassword=postgres -postgresDatabase=postgres diff --git a/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/Application.java b/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/Application.java deleted file mode 100644 index 0f41da07..00000000 --- a/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/Application.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.koraframework.example.vertx; - -import io.koraframework.application.graph.KoraApplication; -import io.koraframework.common.annotation.KoraApp; -import io.koraframework.config.hocon.HoconConfigModule; -import io.koraframework.database.vertx.VertxDatabaseModule; -import io.koraframework.logging.logback.LogbackModule; - -@KoraApp -public interface Application extends - HoconConfigModule, - LogbackModule, - VertxDatabaseModule { - - static void main(String[] args) { - KoraApplication.run(ApplicationGraph::graph); - } -} diff --git a/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/RootService.java b/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/RootService.java deleted file mode 100644 index ca42b6c2..00000000 --- a/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/RootService.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.koraframework.example.vertx; - -import io.koraframework.common.annotation.Component; -import io.koraframework.common.annotation.Root; - -@Root -@Component -public final class RootService { - - private final VertxCrudSyncRepository vertxCrudRepository; - private final VertxMapperColumnRepository vertxMapperColumnRepository; - private final VertxMapperParameterRepository vertxMapperParameterRepository; - private final VertxMapperRowSetRepository vertxMapperRowSetRepository; - private final VertxMapperRowRepository vertxMapperRowRepository; - private final VertxCrudReactorRepository vertxCrudReactorRepository; - - public RootService(VertxCrudSyncRepository vertxCrudRepository, - VertxMapperColumnRepository vertxMapperColumnRepository, - VertxMapperParameterRepository vertxMapperParameterRepository, - VertxMapperRowSetRepository vertxMapperRowSetRepository, - VertxMapperRowRepository vertxMapperRowRepository, - VertxCrudReactorRepository vertxCrudReactorRepository) { - this.vertxCrudRepository = vertxCrudRepository; - this.vertxMapperColumnRepository = vertxMapperColumnRepository; - this.vertxMapperParameterRepository = vertxMapperParameterRepository; - this.vertxMapperRowSetRepository = vertxMapperRowSetRepository; - this.vertxMapperRowRepository = vertxMapperRowRepository; - this.vertxCrudReactorRepository = vertxCrudReactorRepository; - } -} diff --git a/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/VertxCrudReactorRepository.java b/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/VertxCrudReactorRepository.java deleted file mode 100644 index 13780c2e..00000000 --- a/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/VertxCrudReactorRepository.java +++ /dev/null @@ -1,59 +0,0 @@ -package io.koraframework.example.vertx; - -import org.jspecify.annotations.Nullable; -import java.util.List; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import io.koraframework.database.common.UpdateCount; -import io.koraframework.database.common.annotation.*; -import io.koraframework.database.vertx.VertxRepository; - -@Repository -public interface VertxCrudReactorRepository extends VertxRepository { - - record Entity(String id, - @Column("value1") int field1, - String value2, - @Nullable String value3) {} - - @Query("SELECT * FROM entities WHERE id = :id") - Mono findById(String id); - - @Query("SELECT * FROM entities") - Flux findAll(); - - @Query("SELECT * FROM entities") - Mono> findAllMono(); - - @Query(""" - INSERT INTO entities(id, value1, value2, value3) - VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3) - """) - Mono insert(Entity entity); - - @Query(""" - INSERT INTO entities(id, value1, value2, value3) - VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3) - """) - Mono insertBatch(@Batch List entity); - - @Query(""" - UPDATE entities - SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 - WHERE id = :entity.id - """) - Mono update(Entity entity); - - @Query(""" - UPDATE entities - SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 - WHERE id = :entity.id - """) - Mono updateBatch(@Batch List entity); - - @Query("DELETE FROM entities WHERE id = :id") - Mono deleteById(String id); - - @Query("DELETE FROM entities") - Mono deleteAll(); -} diff --git a/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/VertxCrudSyncRepository.java b/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/VertxCrudSyncRepository.java deleted file mode 100644 index da9c29d6..00000000 --- a/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/VertxCrudSyncRepository.java +++ /dev/null @@ -1,54 +0,0 @@ -package io.koraframework.example.vertx; - -import org.jspecify.annotations.Nullable; -import java.util.List; -import io.koraframework.database.common.UpdateCount; -import io.koraframework.database.common.annotation.*; -import io.koraframework.database.vertx.VertxRepository; - -@Repository -public interface VertxCrudSyncRepository extends VertxRepository { - - record Entity(String id, - @Column("value1") int field1, - String value2, - @Nullable String value3) {} - - @Query("SELECT * FROM entities WHERE id = :id") - Entity findById(String id); - - @Query("SELECT * FROM entities") - List findAll(); - - @Query(""" - INSERT INTO entities(id, value1, value2, value3) - VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3) - """) - void insert(Entity entity); - - @Query(""" - INSERT INTO entities(id, value1, value2, value3) - VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3) - """) - UpdateCount insertBatch(@Batch List entity); - - @Query(""" - UPDATE entities - SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 - WHERE id = :entity.id - """) - void update(Entity entity); - - @Query(""" - UPDATE entities - SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 - WHERE id = :entity.id - """) - UpdateCount updateBatch(@Batch List entity); - - @Query("DELETE FROM entities WHERE id = :id") - void deleteById(String id); - - @Query("DELETE FROM entities") - UpdateCount deleteAll(); -} diff --git a/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/VertxMapperColumnRepository.java b/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/VertxMapperColumnRepository.java deleted file mode 100644 index 29479a2f..00000000 --- a/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/VertxMapperColumnRepository.java +++ /dev/null @@ -1,62 +0,0 @@ -package io.koraframework.example.vertx; - -import io.vertx.sqlclient.Row; -import org.jspecify.annotations.Nullable; -import java.util.List; -import io.koraframework.common.annotation.Mapping; -import io.koraframework.database.common.annotation.*; -import io.koraframework.database.vertx.VertxRepository; -import io.koraframework.database.vertx.mapper.result.VertxResultColumnMapper; - -@Repository -public interface VertxMapperColumnRepository extends VertxRepository { - - final class EntityFieldTypeResultMapper implements VertxResultColumnMapper { - - private static final Entity.FieldType[] ALL = Entity.FieldType.values(); - - @Nullable - @Override - public Entity.FieldType apply(Row row, int index) { - var fieldAsInt = row.get(Integer.class, index); - if (fieldAsInt == null) { - return null; - } - - for (var type : ALL) { - if (type.code() == fieldAsInt) { - return type; - } - } - - return Entity.FieldType.UNKNOWN; - } - } - - record Entity(String id, - @Mapping(EntityFieldTypeResultMapper.class) - @Column("value1") FieldType field1, - String value2, - @Nullable String value3) { - - enum FieldType { - - UNKNOWN(-10), - ONE(1), - TWO(2); - - private final int code; - - FieldType(int code) { - this.code = code; - } - - public int code() { - return code; - } - } - } - - @Query("SELECT * FROM entities") - List findAll(); -} diff --git a/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/VertxMapperParameterRepository.java b/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/VertxMapperParameterRepository.java deleted file mode 100644 index 91cf40f0..00000000 --- a/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/VertxMapperParameterRepository.java +++ /dev/null @@ -1,54 +0,0 @@ -package io.koraframework.example.vertx; - -import org.jspecify.annotations.Nullable; -import io.koraframework.common.annotation.Mapping; -import io.koraframework.database.common.UpdateCount; -import io.koraframework.database.common.annotation.*; -import io.koraframework.database.vertx.VertxRepository; -import io.koraframework.database.vertx.mapper.parameter.VertxParameterColumnMapper; - -@Repository -public interface VertxMapperParameterRepository extends VertxRepository { - - final class EntityFieldTypeParameterMapper implements VertxParameterColumnMapper { - - @Nullable - @Override - public Object apply(@Nullable Entity.FieldType fieldType) { - return (fieldType == null) - ? null - : fieldType.code(); - } - } - - record Entity(String id, - @Column("value1") int field1, - String value2, - @Nullable String value3) { - - enum FieldType { - - UNKNOWN(-10), - ONE(1), - TWO(2); - - private final int code; - - FieldType(int code) { - this.code = code; - } - - public int code() { - return code; - } - } - } - - @Query(""" - UPDATE entities - SET value1 = :fieldType - WHERE id = :id - """) - UpdateCount updateFieldType(String id, - @Mapping(EntityFieldTypeParameterMapper.class) Entity.FieldType fieldType); -} diff --git a/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/VertxMapperRowRepository.java b/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/VertxMapperRowRepository.java deleted file mode 100644 index 27f7c4b0..00000000 --- a/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/VertxMapperRowRepository.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.koraframework.example.vertx; - -import io.vertx.sqlclient.Row; -import java.util.List; -import io.koraframework.common.annotation.Mapping; -import io.koraframework.database.common.annotation.Query; -import io.koraframework.database.common.annotation.Repository; -import io.koraframework.database.vertx.VertxRepository; -import io.koraframework.database.vertx.mapper.result.VertxRowMapper; - -@Repository -public interface VertxMapperRowRepository extends VertxRepository { - - record EntityPart(String id, int field1) {} - - final class EntityPartRowMapper implements VertxRowMapper { - - @Override - public EntityPart apply(Row row) { - return new EntityPart(row.get(String.class, 0), row.get(Integer.class, 1)); - } - } - - @Mapping(EntityPartRowMapper.class) - @Query("SELECT id, value1 FROM entities") - List findAllParts(); -} diff --git a/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/VertxMapperRowSetRepository.java b/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/VertxMapperRowSetRepository.java deleted file mode 100644 index ffd3e792..00000000 --- a/examples/java/kora-java-database-vertx/src/main/java/io/koraframework/example/vertx/VertxMapperRowSetRepository.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.koraframework.example.vertx; - -import io.vertx.sqlclient.Row; -import io.vertx.sqlclient.RowSet; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import io.koraframework.common.annotation.Mapping; -import io.koraframework.database.common.annotation.Query; -import io.koraframework.database.common.annotation.Repository; -import io.koraframework.database.vertx.VertxRepository; -import io.koraframework.database.vertx.mapper.result.VertxRowSetMapper; - -@Repository -public interface VertxMapperRowSetRepository extends VertxRepository { - - record EntityPart(String id, int field1) {} - - final class EntityPartRowMapper implements VertxRowSetMapper>> { - - @Override - public Map> apply(RowSet rows) { - var result = new LinkedHashMap>(rows.size()); - for (Row row : rows) { - var entityPart = new EntityPart(row.getString(0), row.getInteger(1)); - var entityParts = result.computeIfAbsent(entityPart.field1(), k -> new ArrayList<>()); - entityParts.add(entityPart); - } - return result; - } - } - - @Mapping(EntityPartRowMapper.class) - @Query("SELECT id, value1 FROM entities") - Map> findAllParts(); -} diff --git a/examples/java/kora-java-database-vertx/src/main/resources/application.conf b/examples/java/kora-java-database-vertx/src/main/resources/application.conf deleted file mode 100644 index f4ab9e4c..00000000 --- a/examples/java/kora-java-database-vertx/src/main/resources/application.conf +++ /dev/null @@ -1,15 +0,0 @@ -db { - connectionUri = ${POSTGRES_VERTX_URI} - username = ${POSTGRES_USER} - password = ${POSTGRES_PASS} - maxPoolSize = 10 - poolName = "kora" - telemetry.logging.enabled = true -} - - -logging.level { - "root": "WARN" - "io.koraframework": "INFO" - "io.koraframework.example": "INFO" -} diff --git a/examples/java/kora-java-database-vertx/src/main/resources/db/migration/V1__setup-tables.sql b/examples/java/kora-java-database-vertx/src/main/resources/db/migration/V1__setup-tables.sql deleted file mode 100644 index 20ac2f10..00000000 --- a/examples/java/kora-java-database-vertx/src/main/resources/db/migration/V1__setup-tables.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE IF NOT EXISTS entities -( - id VARCHAR NOT NULL, - value1 INT NOT NULL, - value2 VARCHAR NOT NULL, - value3 VARCHAR, - PRIMARY KEY (id) -); diff --git a/examples/java/kora-java-database-vertx/src/main/resources/logback.xml b/examples/java/kora-java-database-vertx/src/main/resources/logback.xml deleted file mode 100644 index 184638b5..00000000 --- a/examples/java/kora-java-database-vertx/src/main/resources/logback.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/examples/java/kora-java-database-vertx/src/test/java/io/koraframework/example/vertx/VertxCrudReactorTests.java b/examples/java/kora-java-database-vertx/src/test/java/io/koraframework/example/vertx/VertxCrudReactorTests.java deleted file mode 100644 index 86bdfa4d..00000000 --- a/examples/java/kora-java-database-vertx/src/test/java/io/koraframework/example/vertx/VertxCrudReactorTests.java +++ /dev/null @@ -1,122 +0,0 @@ -package io.koraframework.example.vertx; - -import static org.junit.jupiter.api.Assertions.*; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL; -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; -import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL; -import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = @Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class VertxCrudReactorTests implements KoraAppTestConfigModifier { - - @ConnectionPostgreSQL - private JdbcConnection connection; - - @TestComponent - private VertxCrudReactorRepository repository; - - @NotNull - @Override - public KoraConfigModification config() { - var params = connection.params(); - return KoraConfigModification - .ofSystemProperty("POSTGRES_VERTX_URI", - "postgresql://%s:%s/%s".formatted(params.host(), params.port(), params.database())) - .withSystemProperty("POSTGRES_USER", params.username()) - .withSystemProperty("POSTGRES_PASS", params.password()); - } - - @Test - void monoSingleSuccess() { - // given - var entityCreate = new VertxCrudReactorRepository.Entity("1", 1, "2", null); - repository.insert(entityCreate).block(); - - var foundCreated = repository.findById("1").block(); - assertNotNull(foundCreated); - assertEquals("1", foundCreated.id()); - assertEquals(1, foundCreated.field1()); - assertEquals("2", foundCreated.value2()); - assertNull(foundCreated.value3()); - - // when - var entityUpdate = new VertxCrudReactorRepository.Entity("1", 5, "6", null); - repository.update(entityUpdate).block(); - - var foundUpdated = repository.findById("1").block(); - assertNotNull(foundUpdated); - assertEquals("1", foundUpdated.id()); - assertEquals(5, foundUpdated.field1()); - assertEquals("6", foundUpdated.value2()); - assertNull(foundUpdated.value3()); - - // then - repository.deleteById("1").block(); - assertNull(repository.findById("1").block()); - } - - @Test - void monoBatchSuccess() { - // given - var entity1 = new VertxCrudReactorRepository.Entity("1", 1, "2", null); - var entity2 = new VertxCrudReactorRepository.Entity("2", 3, "4", null); - assertEquals(2L, repository.insertBatch(List.of(entity1, entity2)).block().value()); - - var foundCreated = repository.findAll().collectList().block(); - assertNotNull(foundCreated); - assertEquals(2, foundCreated.size()); - for (var entity : foundCreated) { - if (entity.id().equals("1")) { - assertEquals("1", entity.id()); - assertEquals(1, entity.field1()); - assertEquals("2", entity.value2()); - assertNull(entity.value3()); - } else { - assertEquals("2", entity.id()); - assertEquals(3, entity.field1()); - assertEquals("4", entity.value2()); - assertNull(entity.value3()); - } - } - - // when - var entityUpdate1 = new VertxCrudReactorRepository.Entity("1", 5, "6", null); - var entityUpdate2 = new VertxCrudReactorRepository.Entity("2", 7, "8", null); - assertEquals(2L, repository.updateBatch(List.of(entityUpdate1, entityUpdate2)).block().value()); - - var foundUpdated = repository.findAllMono().block(); - assertEquals(2, foundUpdated.size()); - for (var entity : foundUpdated) { - if (entity.id().equals("1")) { - assertEquals("1", entity.id()); - assertEquals(5, entity.field1()); - assertEquals("6", entity.value2()); - assertNull(entity.value3()); - } else { - assertEquals("2", entity.id()); - assertEquals(7, entity.field1()); - assertEquals("8", entity.value2()); - assertNull(entity.value3()); - } - } - - // then - assertEquals(2L, repository.deleteAll().block().value()); - assertTrue(repository.findAll().collectList().block().isEmpty()); - } -} diff --git a/examples/java/kora-java-database-vertx/src/test/java/io/koraframework/example/vertx/VertxCrudSyncTests.java b/examples/java/kora-java-database-vertx/src/test/java/io/koraframework/example/vertx/VertxCrudSyncTests.java deleted file mode 100644 index 22525640..00000000 --- a/examples/java/kora-java-database-vertx/src/test/java/io/koraframework/example/vertx/VertxCrudSyncTests.java +++ /dev/null @@ -1,122 +0,0 @@ -package io.koraframework.example.vertx; - -import static org.junit.jupiter.api.Assertions.*; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL; -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; -import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL; -import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = @Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class VertxCrudSyncTests implements KoraAppTestConfigModifier { - - @ConnectionPostgreSQL - private JdbcConnection connection; - - @TestComponent - private VertxCrudSyncRepository repository; - - @NotNull - @Override - public KoraConfigModification config() { - var params = connection.params(); - return KoraConfigModification - .ofSystemProperty("POSTGRES_VERTX_URI", - "postgresql://%s:%s/%s".formatted(params.host(), params.port(), params.database())) - .withSystemProperty("POSTGRES_USER", params.username()) - .withSystemProperty("POSTGRES_PASS", params.password()); - } - - @Test - void monoSingleSuccess() { - // given - var entityCreate = new VertxCrudSyncRepository.Entity("1", 1, "2", null); - repository.insert(entityCreate); - - var foundCreated = repository.findById("1"); - assertNotNull(foundCreated); - assertEquals("1", foundCreated.id()); - assertEquals(1, foundCreated.field1()); - assertEquals("2", foundCreated.value2()); - assertNull(foundCreated.value3()); - - // when - var entityUpdate = new VertxCrudSyncRepository.Entity("1", 5, "6", null); - repository.update(entityUpdate); - - var foundUpdated = repository.findById("1"); - assertNotNull(foundUpdated); - assertEquals("1", foundUpdated.id()); - assertEquals(5, foundUpdated.field1()); - assertEquals("6", foundUpdated.value2()); - assertNull(foundUpdated.value3()); - - // then - repository.deleteById("1"); - assertNull(repository.findById("1")); - } - - @Test - void monoBatchSuccess() { - // given - var entity1 = new VertxCrudSyncRepository.Entity("1", 1, "2", null); - var entity2 = new VertxCrudSyncRepository.Entity("2", 3, "4", null); - assertEquals(2L, repository.insertBatch(List.of(entity1, entity2)).value()); - - var foundCreated = repository.findAll(); - assertNotNull(foundCreated); - assertEquals(2, foundCreated.size()); - for (var entity : foundCreated) { - if (entity.id().equals("1")) { - assertEquals("1", entity.id()); - assertEquals(1, entity.field1()); - assertEquals("2", entity.value2()); - assertNull(entity.value3()); - } else { - assertEquals("2", entity.id()); - assertEquals(3, entity.field1()); - assertEquals("4", entity.value2()); - assertNull(entity.value3()); - } - } - - // when - var entityUpdate1 = new VertxCrudSyncRepository.Entity("1", 5, "6", null); - var entityUpdate2 = new VertxCrudSyncRepository.Entity("2", 7, "8", null); - assertEquals(2L, repository.updateBatch(List.of(entityUpdate1, entityUpdate2)).value()); - - var foundUpdated = repository.findAll(); - assertEquals(2, foundUpdated.size()); - for (var entity : foundUpdated) { - if (entity.id().equals("1")) { - assertEquals("1", entity.id()); - assertEquals(5, entity.field1()); - assertEquals("6", entity.value2()); - assertNull(entity.value3()); - } else { - assertEquals("2", entity.id()); - assertEquals(7, entity.field1()); - assertEquals("8", entity.value2()); - assertNull(entity.value3()); - } - } - - // then - assertEquals(2L, repository.deleteAll().value()); - assertTrue(repository.findAll().isEmpty()); - } -} diff --git a/examples/java/kora-java-database-vertx/src/test/java/io/koraframework/example/vertx/VertxMapperColumnTests.java b/examples/java/kora-java-database-vertx/src/test/java/io/koraframework/example/vertx/VertxMapperColumnTests.java deleted file mode 100644 index 0c6bc9c5..00000000 --- a/examples/java/kora-java-database-vertx/src/test/java/io/koraframework/example/vertx/VertxMapperColumnTests.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.koraframework.example.vertx; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL; -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; -import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL; -import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = @Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class VertxMapperColumnTests implements KoraAppTestConfigModifier { - - @ConnectionPostgreSQL - private JdbcConnection connection; - - @TestComponent - private VertxCrudSyncRepository crudRepository; - @TestComponent - private VertxMapperColumnRepository mapperRepository; - - @NotNull - @Override - public KoraConfigModification config() { - var params = connection.params(); - return KoraConfigModification - .ofSystemProperty("POSTGRES_VERTX_URI", - "postgresql://%s:%s/%s".formatted(params.host(), params.port(), params.database())) - .withSystemProperty("POSTGRES_USER", params.username()) - .withSystemProperty("POSTGRES_PASS", params.password()); - } - - @Test - void columnMapperSuccess() { - // given - var entity1 = new VertxCrudSyncRepository.Entity("1", 1, "2", null); - var entity2 = new VertxCrudSyncRepository.Entity("2", 3, "4", null); - - // when - assertEquals(2, crudRepository.insertBatch(List.of(entity1, entity2)).value()); - - // then - var parts = mapperRepository.findAll(); - assertEquals(2, parts.size()); - for (var part : parts) { - if (part.id().equals("1")) { - assertEquals("1", part.id()); - Assertions.assertEquals(VertxMapperColumnRepository.Entity.FieldType.ONE, part.field1()); - } else { - assertEquals("2", part.id()); - Assertions.assertEquals(VertxMapperColumnRepository.Entity.FieldType.UNKNOWN, part.field1()); - } - } - } -} diff --git a/examples/java/kora-java-database-vertx/src/test/java/io/koraframework/example/vertx/VertxMapperParameterTests.java b/examples/java/kora-java-database-vertx/src/test/java/io/koraframework/example/vertx/VertxMapperParameterTests.java deleted file mode 100644 index e8f4a2f6..00000000 --- a/examples/java/kora-java-database-vertx/src/test/java/io/koraframework/example/vertx/VertxMapperParameterTests.java +++ /dev/null @@ -1,64 +0,0 @@ -package io.koraframework.example.vertx; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL; -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; -import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL; -import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = @Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class VertxMapperParameterTests implements KoraAppTestConfigModifier { - - @ConnectionPostgreSQL - private JdbcConnection connection; - - @TestComponent - private VertxCrudSyncRepository crudRepository; - @TestComponent - private VertxMapperParameterRepository mapperRepository; - - @NotNull - @Override - public KoraConfigModification config() { - var params = connection.params(); - return KoraConfigModification - .ofSystemProperty("POSTGRES_VERTX_URI", - "postgresql://%s:%s/%s".formatted(params.host(), params.port(), params.database())) - .withSystemProperty("POSTGRES_USER", params.username()) - .withSystemProperty("POSTGRES_PASS", params.password()); - } - - @Test - void parameterMapperSuccess() { - // given - var entity1 = new VertxCrudSyncRepository.Entity("1", VertxMapperParameterRepository.Entity.FieldType.ONE.code(), "2", - null); - var entity2 = new VertxCrudSyncRepository.Entity("2", 3, "4", null); - assertEquals(2, crudRepository.insertBatch(List.of(entity1, entity2)).value()); - - // when - mapperRepository.updateFieldType(entity1.id(), VertxMapperParameterRepository.Entity.FieldType.TWO); - - // then - var found = crudRepository.findById(entity1.id()); - assertNotNull(found); - Assertions.assertEquals(VertxMapperParameterRepository.Entity.FieldType.TWO.code(), found.field1()); - } -} diff --git a/examples/java/kora-java-database-vertx/src/test/java/io/koraframework/example/vertx/VertxMapperResultSetTests.java b/examples/java/kora-java-database-vertx/src/test/java/io/koraframework/example/vertx/VertxMapperResultSetTests.java deleted file mode 100644 index e6321a96..00000000 --- a/examples/java/kora-java-database-vertx/src/test/java/io/koraframework/example/vertx/VertxMapperResultSetTests.java +++ /dev/null @@ -1,68 +0,0 @@ -package io.koraframework.example.vertx; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL; -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; -import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL; -import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = @Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class VertxMapperResultSetTests implements KoraAppTestConfigModifier { - - @ConnectionPostgreSQL - private JdbcConnection connection; - - @TestComponent - private VertxCrudSyncRepository crudRepository; - @TestComponent - private VertxMapperRowSetRepository mapperRepository; - - @NotNull - @Override - public KoraConfigModification config() { - var params = connection.params(); - return KoraConfigModification - .ofSystemProperty("POSTGRES_VERTX_URI", - "postgresql://%s:%s/%s".formatted(params.host(), params.port(), params.database())) - .withSystemProperty("POSTGRES_USER", params.username()) - .withSystemProperty("POSTGRES_PASS", params.password()); - } - - @Test - void resultSetMapperSuccess() { - // given - var entity1 = new VertxCrudSyncRepository.Entity("1", 1, "2", null); - var entity2 = new VertxCrudSyncRepository.Entity("2", 3, "4", null); - - // when - assertEquals(2, crudRepository.insertBatch(List.of(entity1, entity2)).value()); - - // then - var found = mapperRepository.findAllParts(); - var fieldOne = found.get(1); - assertNotNull(fieldOne); - assertEquals(1, fieldOne.size()); - assertEquals(entity1.id(), fieldOne.get(0).id()); - - var fieldUnknown = found.get(3); - assertNotNull(fieldUnknown); - assertEquals(1, fieldUnknown.size()); - assertEquals(entity2.id(), fieldUnknown.get(0).id()); - } -} diff --git a/examples/java/kora-java-database-vertx/src/test/java/io/koraframework/example/vertx/VertxMapperRowTests.java b/examples/java/kora-java-database-vertx/src/test/java/io/koraframework/example/vertx/VertxMapperRowTests.java deleted file mode 100644 index 9d75629b..00000000 --- a/examples/java/kora-java-database-vertx/src/test/java/io/koraframework/example/vertx/VertxMapperRowTests.java +++ /dev/null @@ -1,59 +0,0 @@ -package io.koraframework.example.vertx; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL; -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; -import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL; -import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = @Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class VertxMapperRowTests implements KoraAppTestConfigModifier { - - @ConnectionPostgreSQL - private JdbcConnection connection; - - @TestComponent - private VertxCrudSyncRepository crudRepository; - @TestComponent - private VertxMapperRowRepository mapperRepository; - - @NotNull - @Override - public KoraConfigModification config() { - var params = connection.params(); - return KoraConfigModification - .ofSystemProperty("POSTGRES_VERTX_URI", - "postgresql://%s:%s/%s".formatted(params.host(), params.port(), params.database())) - .withSystemProperty("POSTGRES_USER", params.username()) - .withSystemProperty("POSTGRES_PASS", params.password()); - } - - @Test - void rowMapperSuccess() { - // given - var entity1 = new VertxCrudSyncRepository.Entity("1", 1, "2", null); - var entity2 = new VertxCrudSyncRepository.Entity("2", 3, "4", null); - - // when - assertEquals(2, crudRepository.insertBatch(List.of(entity1, entity2)).value()); - - // then - var found = mapperRepository.findAllParts(); - assertEquals(2, found.size()); - } -} diff --git a/examples/java/kora-java-database-vertx/src/test/resources/logback-test.xml b/examples/java/kora-java-database-vertx/src/test/resources/logback-test.xml deleted file mode 100644 index bac04fb1..00000000 --- a/examples/java/kora-java-database-vertx/src/test/resources/logback-test.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - UTF-8 - %cyan(%d{HH:mm:ss.SSS}) %highlight(%-5level) [%thread] %logger{36} - %msg%n - - - - - - - - - - - - - - - - - - - - diff --git a/examples/java/kora-java-grpc-client/build.gradle b/examples/java/kora-java-grpc-client/build.gradle index e0c4f3da..872a12eb 100644 --- a/examples/java/kora-java-grpc-client/build.gradle +++ b/examples/java/kora-java-grpc-client/build.gradle @@ -20,11 +20,11 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:grpc-client" - implementation "io.grpc:grpc-protobuf:1.74.0" + implementation "io.grpc:grpc-protobuf:1.83.1" implementation "javax.annotation:javax.annotation-api:1.3.2" implementation "io.koraframework:logging-logback" @@ -42,15 +42,12 @@ application { //noinspection GroovyAssignabilityCheck run { - environment([ - "": "", - ]) } protobuf { protoc { artifact = "com.google.protobuf:protoc:3.25.3" } plugins { - grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.74.0" } + grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.83.1" } } generateProtoTasks { @@ -72,10 +69,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/java/kora-java-grpc-client/src/main/java/io/koraframework/example/grpc/client/Application.java b/examples/java/kora-java-grpc-client/src/main/java/io/koraframework/example/grpc/client/Application.java index b7a7fe71..a5c24fe0 100644 --- a/examples/java/kora-java-grpc-client/src/main/java/io/koraframework/example/grpc/client/Application.java +++ b/examples/java/kora-java-grpc-client/src/main/java/io/koraframework/example/grpc/client/Application.java @@ -1,6 +1,6 @@ package io.koraframework.example.grpc.client; -import ru.tinkoff.grpc.client.GrpcClientModule; +import io.koraframework.grpc.client.GrpcClientModule; import io.koraframework.application.graph.KoraApplication; import io.koraframework.common.annotation.KoraApp; import io.koraframework.config.hocon.HoconConfigModule; diff --git a/examples/java/kora-java-grpc-server/build.gradle b/examples/java/kora-java-grpc-server/build.gradle index 4dc9aa83..ab7a236c 100644 --- a/examples/java/kora-java-grpc-server/build.gradle +++ b/examples/java/kora-java-grpc-server/build.gradle @@ -20,11 +20,11 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:grpc-server" - implementation "io.grpc:grpc-protobuf:1.74.0" + implementation "io.grpc:grpc-protobuf:1.83.1" implementation "javax.annotation:javax.annotation-api:1.3.2" implementation "io.koraframework:logging-logback" @@ -51,7 +51,7 @@ run { protobuf { protoc { artifact = "com.google.protobuf:protoc:3.25.3" } plugins { - grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.74.0" } + grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.83.1" } } generateProtoTasks { @@ -73,10 +73,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/java/kora-java-helloworld/build.gradle b/examples/java/kora-java-helloworld/build.gradle index 30e2622f..2d405911 100644 --- a/examples/java/kora-java-helloworld/build.gradle +++ b/examples/java/kora-java-helloworld/build.gradle @@ -18,7 +18,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:http-server-undertow" @@ -39,9 +39,6 @@ application { //noinspection GroovyAssignabilityCheck run { - environment([ - "": "", - ]) } distTar { @@ -56,10 +53,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/java/kora-java-helloworld/src/main/resources/application.conf b/examples/java/kora-java-helloworld/src/main/resources/application.conf index fa02544d..fedbe377 100644 --- a/examples/java/kora-java-helloworld/src/main/resources/application.conf +++ b/examples/java/kora-java-helloworld/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 } diff --git a/examples/java/kora-java-http-client/build.gradle b/examples/java/kora-java-http-client/build.gradle index d204825d..827d01b7 100644 --- a/examples/java/kora-java-http-client/build.gradle +++ b/examples/java/kora-java-http-client/build.gradle @@ -19,18 +19,17 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:http-client-jdk" implementation "io.koraframework:json-common" - implementation "io.projectreactor:reactor-core:3.6.18" // For reactive examples (optional) implementation "io.koraframework:logging-logback" implementation "io.koraframework:config-hocon" testImplementation "io.koraframework:test-junit5" - testImplementation "io.goodforgod:testcontainers-extensions-mockserver:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-mockserver:0.15.0" } application { @@ -58,10 +57,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/FormHttpClient.java b/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/FormHttpClient.java index e3bdaa17..e13b1d99 100644 --- a/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/FormHttpClient.java +++ b/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/FormHttpClient.java @@ -7,7 +7,7 @@ import io.koraframework.http.common.form.FormMultipart; import io.koraframework.http.common.form.FormUrlEncoded; -@HttpClient(configPath = "httpClient.default") +@HttpClient("httpClient.default") public interface FormHttpClient { @HttpRoute(method = HttpMethod.POST, path = "/form/encoded") diff --git a/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/InterceptedHttpClient.java b/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/InterceptedHttpClient.java index fa8ac5ef..a29cc83d 100644 --- a/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/InterceptedHttpClient.java +++ b/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/InterceptedHttpClient.java @@ -1,9 +1,8 @@ package io.koraframework.example.http.client; -import java.util.concurrent.CompletionStage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.koraframework.common.Context; +import io.koraframework.common.annotation.Component; import io.koraframework.http.client.common.annotation.HttpClient; import io.koraframework.http.client.common.interceptor.HttpClientInterceptor; import io.koraframework.http.client.common.request.HttpClientRequest; @@ -14,30 +13,30 @@ import io.koraframework.http.common.annotation.InterceptWith; @InterceptWith(InterceptedHttpClient.ClientInterceptor.class) -@HttpClient(configPath = "httpClient.default") +@HttpClient("httpClient.default") public interface InterceptedHttpClient { + @Component final class ClientInterceptor implements HttpClientInterceptor { private static final Logger logger = LoggerFactory.getLogger(ClientInterceptor.class); @Override - public CompletionStage processRequest(Context ctx, InterceptChain chain, HttpClientRequest request) - throws Exception { + public HttpClientResponse processRequest(InterceptChain chain, HttpClientRequest request) throws Exception { logger.info("Client Level Interceptor"); - return chain.process(ctx, request); + return chain.process(request); } } + @Component final class MethodInterceptor implements HttpClientInterceptor { private static final Logger logger = LoggerFactory.getLogger(MethodInterceptor.class); @Override - public CompletionStage processRequest(Context ctx, InterceptChain chain, HttpClientRequest request) - throws Exception { + public HttpClientResponse processRequest(InterceptChain chain, HttpClientRequest request) throws Exception { logger.info("Method Level Interceptor"); - return chain.process(ctx, request); + return chain.process(request); } } diff --git a/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/JsonHttpClient.java b/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/JsonHttpClient.java index dc1c6062..9e20b862 100644 --- a/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/JsonHttpClient.java +++ b/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/JsonHttpClient.java @@ -5,7 +5,7 @@ import io.koraframework.http.common.annotation.HttpRoute; import io.koraframework.json.common.annotation.Json; -@HttpClient(configPath = "httpClient.default") +@HttpClient("httpClient.default") public interface JsonHttpClient { @Json diff --git a/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/MapperRequestHttpClient.java b/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/MapperRequestHttpClient.java index 3d674b7d..150a310d 100644 --- a/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/MapperRequestHttpClient.java +++ b/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/MapperRequestHttpClient.java @@ -1,6 +1,6 @@ package io.koraframework.example.http.client; -import io.koraframework.common.Context; +import io.koraframework.common.annotation.Component; import io.koraframework.common.annotation.Mapping; import io.koraframework.http.client.common.annotation.HttpClient; import io.koraframework.http.client.common.request.HttpClientRequestMapper; @@ -10,15 +10,16 @@ import io.koraframework.http.common.body.HttpBody; import io.koraframework.http.common.body.HttpBodyOutput; -@HttpClient(configPath = "httpClient.default") +@HttpClient("httpClient.default") public interface MapperRequestHttpClient { record UserBody(String id) {} + @Component final class UserRequestMapper implements HttpClientRequestMapper { @Override - public HttpBodyOutput apply(Context ctx, UserBody value) { + public HttpBodyOutput apply(UserBody value) { return HttpBody.plaintext(value.id()); } } diff --git a/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/MapperResponseHttpClient.java b/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/MapperResponseHttpClient.java index 0df03b9b..02ebe94d 100644 --- a/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/MapperResponseHttpClient.java +++ b/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/MapperResponseHttpClient.java @@ -13,7 +13,7 @@ import io.koraframework.http.common.annotation.HttpRoute; import io.koraframework.http.common.annotation.Path; -@HttpClient(configPath = "httpClient.default") +@HttpClient("httpClient.default") public interface MapperResponseHttpClient { final class ResponseSuccessMapper implements HttpClientResponseMapper { diff --git a/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/ParametersHttpClient.java b/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/ParametersHttpClient.java index 3449b2a7..d5f20367 100644 --- a/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/ParametersHttpClient.java +++ b/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/ParametersHttpClient.java @@ -10,7 +10,7 @@ import io.koraframework.http.common.annotation.Path; import io.koraframework.http.common.annotation.Query; -@HttpClient(configPath = "httpClient.default") +@HttpClient("httpClient.default") public interface ParametersHttpClient { @HttpRoute(method = HttpMethod.POST, path = "/parameters/{path}") diff --git a/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/ReactorHttpClient.java b/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/ReactorHttpClient.java deleted file mode 100644 index ffedf631..00000000 --- a/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/ReactorHttpClient.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.koraframework.example.http.client; - -import org.jspecify.annotations.Nullable; -import reactor.core.publisher.Mono; -import io.koraframework.http.client.common.annotation.HttpClient; -import io.koraframework.http.common.HttpMethod; -import io.koraframework.http.common.annotation.Header; -import io.koraframework.http.common.annotation.HttpRoute; -import io.koraframework.http.common.annotation.Path; -import io.koraframework.http.common.annotation.Query; - -@HttpClient(configPath = "httpClient.default") -public interface ReactorHttpClient { - - @HttpRoute(method = HttpMethod.GET, path = "/reactor/{path}") - Mono get(@Path String path, - @Nullable @Query String query, - @Nullable @Header String header); -} diff --git a/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/RootService.java b/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/RootService.java index 83a91c48..587541b1 100644 --- a/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/RootService.java +++ b/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/RootService.java @@ -12,7 +12,6 @@ public final class RootService { private final MapperRequestHttpClient mapperRequestHttpClient; private final MapperResponseHttpClient mapperResponseHttpClient; private final ParametersHttpClient parametersHttpClient; - private final ReactorHttpClient reactorHttpClient; private final VoidHttpClient voidHttpClient; private final FormHttpClient formHttpClient; @@ -21,7 +20,6 @@ public RootService(InterceptedHttpClient interceptedHttpClient, MapperRequestHttpClient mapperRequestHttpClient, MapperResponseHttpClient mapperResponseHttpClient, ParametersHttpClient parametersHttpClient, - ReactorHttpClient reactorHttpClient, VoidHttpClient voidHttpClient, FormHttpClient formHttpClient) { this.interceptedHttpClient = interceptedHttpClient; @@ -29,7 +27,6 @@ public RootService(InterceptedHttpClient interceptedHttpClient, this.mapperRequestHttpClient = mapperRequestHttpClient; this.mapperResponseHttpClient = mapperResponseHttpClient; this.parametersHttpClient = parametersHttpClient; - this.reactorHttpClient = reactorHttpClient; this.voidHttpClient = voidHttpClient; this.formHttpClient = formHttpClient; } diff --git a/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/VoidHttpClient.java b/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/VoidHttpClient.java index a0ba5246..afe72eeb 100644 --- a/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/VoidHttpClient.java +++ b/examples/java/kora-java-http-client/src/main/java/io/koraframework/example/http/client/VoidHttpClient.java @@ -1,16 +1,12 @@ package io.koraframework.example.http.client; -import reactor.core.publisher.Mono; import io.koraframework.http.client.common.annotation.HttpClient; import io.koraframework.http.common.HttpMethod; import io.koraframework.http.common.annotation.HttpRoute; -@HttpClient(configPath = "httpClient.default") +@HttpClient("httpClient.default") public interface VoidHttpClient { @HttpRoute(method = HttpMethod.POST, path = "/void") void sync(); - - @HttpRoute(method = HttpMethod.POST, path = "/void") - Mono reactor(); } diff --git a/examples/java/kora-java-http-client/src/test/java/io/koraframework/example/http/client/FormHttpClientTests.java b/examples/java/kora-java-http-client/src/test/java/io/koraframework/example/http/client/FormHttpClientTests.java index ecafde8a..aff00ac0 100644 --- a/examples/java/kora-java-http-client/src/test/java/io/koraframework/example/http/client/FormHttpClientTests.java +++ b/examples/java/kora-java-http-client/src/test/java/io/koraframework/example/http/client/FormHttpClientTests.java @@ -66,7 +66,7 @@ void formMultipartHttpClient() { var requestBody = """ --blob:.*\\r content-disposition: form-data; name="field1"\\r - content-type: text/plain; charset=utf-8\\r + content-type: text/plain;charset=utf-8\\r \\r some data content\\r --blob:.*\\r @@ -93,7 +93,7 @@ void formMultipartHttpClient() { "some file content".getBytes(StandardCharsets.UTF_8))))); HttpClientRequest request = new HttpClientRequestBuilderImpl("POST", "http://localhost:8090/pets/{petId}") - .templateParam("petId", "1") + .pathParam("petId", "1") .queryParam("page", 1) .header("token", "12345") .body(HttpBody.plaintext("refresh")) diff --git a/examples/java/kora-java-http-client/src/test/java/io/koraframework/example/http/client/ReactorHttpClientTests.java b/examples/java/kora-java-http-client/src/test/java/io/koraframework/example/http/client/ReactorHttpClientTests.java deleted file mode 100644 index f4815099..00000000 --- a/examples/java/kora-java-http-client/src/test/java/io/koraframework/example/http/client/ReactorHttpClientTests.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.koraframework.example.http.client; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.mockserver.ConnectionMockServer; -import io.goodforgod.testcontainers.extensions.mockserver.MockServerConnection; -import io.goodforgod.testcontainers.extensions.mockserver.TestcontainersMockServer; -import java.nio.charset.StandardCharsets; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersMockServer(mode = ContainerMode.PER_RUN) -@KoraAppTest(Application.class) -class ReactorHttpClientTests implements KoraAppTestConfigModifier { - - @ConnectionMockServer - private MockServerConnection mockserverConnection; - - @NotNull - @Override - public KoraConfigModification config() { - return KoraConfigModification.ofSystemProperty("HTTP_CLIENT_URL", mockserverConnection.params().uri().toString()); - } - - @TestComponent - private ReactorHttpClient httpClient; - - @Test - void reactorHttpClient() { - // given - mockserverConnection.client().when( - org.mockserver.model.HttpRequest.request() - .withMethod("GET") - .withPath("/reactor/path") - .withQueryStringParameter("query", "query") - .withHeader("header", "header")) - .respond( - org.mockserver.model.HttpResponse.response() - .withBody("OK")); - - // then - var response = httpClient.get("path", "query", "header").block(); - assertNotNull(response); - assertEquals("OK", new String(response, StandardCharsets.UTF_8)); - } -} diff --git a/examples/java/kora-java-http-client/src/test/java/io/koraframework/example/http/client/VoidHttpClientTests.java b/examples/java/kora-java-http-client/src/test/java/io/koraframework/example/http/client/VoidHttpClientTests.java index c671faf9..59773b45 100644 --- a/examples/java/kora-java-http-client/src/test/java/io/koraframework/example/http/client/VoidHttpClientTests.java +++ b/examples/java/kora-java-http-client/src/test/java/io/koraframework/example/http/client/VoidHttpClientTests.java @@ -4,7 +4,6 @@ import io.goodforgod.testcontainers.extensions.mockserver.ConnectionMockServer; import io.goodforgod.testcontainers.extensions.mockserver.MockServerConnection; import io.goodforgod.testcontainers.extensions.mockserver.TestcontainersMockServer; -import java.time.Duration; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import io.koraframework.test.extension.junit5.KoraAppTest; @@ -28,21 +27,6 @@ public KoraConfigModification config() { @TestComponent private VoidHttpClient httpClient; - @Test - void voidPostReactorRequestSuccess() { - // given - mockserverConnection.client().when( - org.mockserver.model.HttpRequest.request() - .withMethod("POST") - .withPath("/void")) - .respond( - org.mockserver.model.HttpResponse.response() - .withBody("OK")); - - // then - httpClient.reactor().block(Duration.ofSeconds(15)); - } - @Test void voidPostSyncRequestSuccess() { // given diff --git a/examples/java/kora-java-http-server/build.gradle b/examples/java/kora-java-http-server/build.gradle index c95de68a..7ab4b6fb 100644 --- a/examples/java/kora-java-http-server/build.gradle +++ b/examples/java/kora-java-http-server/build.gradle @@ -18,13 +18,12 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:http-server-undertow" implementation "io.koraframework:json-common" implementation "io.koraframework:validation-module" - implementation "io.projectreactor:reactor-core:3.6.18" // For reactive examples (optional) implementation "io.koraframework:logging-logback" implementation "io.koraframework:config-hocon" @@ -56,10 +55,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/java/kora-java-http-server/src/main/java/io/koraframework/example/http/server/ErrorHandlerController.java b/examples/java/kora-java-http-server/src/main/java/io/koraframework/example/http/server/ErrorHandlerController.java index b621fb68..3777073d 100644 --- a/examples/java/kora-java-http-server/src/main/java/io/koraframework/example/http/server/ErrorHandlerController.java +++ b/examples/java/kora-java-http-server/src/main/java/io/koraframework/example/http/server/ErrorHandlerController.java @@ -1,6 +1,5 @@ package io.koraframework.example.http.server; -import java.io.IOException; import io.koraframework.common.annotation.Component; import io.koraframework.http.common.HttpMethod; import io.koraframework.http.common.annotation.HttpRoute; @@ -54,11 +53,7 @@ public HttpServerResponse intercept(HttpServerRequest request, InterceptChain ch code = 500; } - try { - return HttpServerResponse.of(code, HttpBody.json(errorJsonWriter.toByteArray(error))); - } catch (IOException ex) { - return HttpServerResponse.of(500, HttpBody.plaintext(ex.getMessage())); - } + return HttpServerResponse.of(code, HttpBody.json(errorJsonWriter.toByteArray(error))); } } } diff --git a/examples/java/kora-java-http-server/src/main/java/io/koraframework/example/http/server/InterceptedController.java b/examples/java/kora-java-http-server/src/main/java/io/koraframework/example/http/server/InterceptedController.java index 6bd1834a..7302d68f 100644 --- a/examples/java/kora-java-http-server/src/main/java/io/koraframework/example/http/server/InterceptedController.java +++ b/examples/java/kora-java-http-server/src/main/java/io/koraframework/example/http/server/InterceptedController.java @@ -9,7 +9,7 @@ import io.koraframework.http.common.annotation.InterceptWith; import io.koraframework.http.common.body.HttpBody; import io.koraframework.http.server.common.interceptor.HttpServerInterceptor; -import io.koraframework.http.server.common.HttpServerModule; +import io.koraframework.http.server.common.HttpServer; import io.koraframework.http.server.common.request.HttpServerRequest; import io.koraframework.http.server.common.response.HttpServerResponse; import io.koraframework.http.server.common.annotation.HttpController; @@ -48,7 +48,7 @@ public HttpServerResponse intercept(HttpServerRequest request, InterceptChain ch } } - @Tag(HttpServerModule.class) + @Tag(HttpServer.class) @Component public static final class ServerInterceptor implements HttpServerInterceptor { diff --git a/examples/java/kora-java-http-server/src/main/java/io/koraframework/example/http/server/MapperRequestController.java b/examples/java/kora-java-http-server/src/main/java/io/koraframework/example/http/server/MapperRequestController.java index 968a6a38..eacd8e21 100644 --- a/examples/java/kora-java-http-server/src/main/java/io/koraframework/example/http/server/MapperRequestController.java +++ b/examples/java/kora-java-http-server/src/main/java/io/koraframework/example/http/server/MapperRequestController.java @@ -16,6 +16,7 @@ public final class MapperRequestController { public record UserContext(String userId, String traceId) {} + @Component public static final class UserContextRequestMapper implements HttpServerRequestMapper { @Override diff --git a/examples/java/kora-java-http-server/src/main/java/io/koraframework/example/http/server/ReactorController.java b/examples/java/kora-java-http-server/src/main/java/io/koraframework/example/http/server/ReactorController.java deleted file mode 100644 index ec6c12fe..00000000 --- a/examples/java/kora-java-http-server/src/main/java/io/koraframework/example/http/server/ReactorController.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.koraframework.example.http.server; - -import reactor.core.publisher.Mono; -import io.koraframework.common.annotation.Component; -import io.koraframework.http.common.HttpMethod; -import io.koraframework.http.common.annotation.HttpRoute; -import io.koraframework.http.common.body.HttpBody; -import io.koraframework.http.server.common.response.HttpServerResponse; -import io.koraframework.http.server.common.annotation.HttpController; - -/** - * @see Mono - Method is using Project Reactor reactive response type - */ -@Component -@HttpController -public final class ReactorController { - - @HttpRoute(method = HttpMethod.GET, path = "/reactor/mono") - public Mono get() { - return Mono.fromCallable(() -> HttpServerResponse.of(200, HttpBody.plaintext("Hello world"))); - } -} diff --git a/examples/java/kora-java-http-server/src/main/resources/application.conf b/examples/java/kora-java-http-server/src/main/resources/application.conf index 8aad81ab..1e9e6cd3 100644 --- a/examples/java/kora-java-http-server/src/main/resources/application.conf +++ b/examples/java/kora-java-http-server/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } diff --git a/examples/java/kora-java-http-server/src/test/java/io/koraframework/example/httpserver/ReactorControllerTests.java b/examples/java/kora-java-http-server/src/test/java/io/koraframework/example/httpserver/ReactorControllerTests.java deleted file mode 100644 index b5f3e661..00000000 --- a/examples/java/kora-java-http-server/src/test/java/io/koraframework/example/httpserver/ReactorControllerTests.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.koraframework.example.httpserver; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; -import org.junit.jupiter.api.Test; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -@Testcontainers -class ReactorControllerTests { - - @Container - private static final AppContainer container = AppContainer.build(); - - @Test - void reactorController() throws Exception { - // given - var httpClient = HttpClient.newHttpClient(); - - // then - var request = HttpRequest.newBuilder() - .GET() - .uri(container.getURI().resolve("/reactor/mono")) - .timeout(Duration.ofSeconds(5)) - .build(); - - var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - assertEquals(200, response.statusCode(), response.body()); - assertEquals("Hello world", response.body(), response.body()); - } -} diff --git a/examples/java/kora-java-http-server/src/test/java/io/koraframework/example/httpserver/TestApplication.java b/examples/java/kora-java-http-server/src/test/java/io/koraframework/example/httpserver/TestApplication.java index 8e70804b..f0f8136b 100644 --- a/examples/java/kora-java-http-server/src/test/java/io/koraframework/example/httpserver/TestApplication.java +++ b/examples/java/kora-java-http-server/src/test/java/io/koraframework/example/httpserver/TestApplication.java @@ -36,7 +36,7 @@ public interface TestApplication extends Application, JdkHttpClientModule { @Root @Component - @HttpClient(configPath = "testHttpClient") + @HttpClient("testHttpClient") interface JsonHttpClient { @HttpRoute(method = HttpMethod.POST, path = "/json") diff --git a/examples/java/kora-java-kafka/build.gradle b/examples/java/kora-java-kafka/build.gradle index a7b0fdff..5b622a9f 100644 --- a/examples/java/kora-java-kafka/build.gradle +++ b/examples/java/kora-java-kafka/build.gradle @@ -18,7 +18,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:kafka" @@ -28,7 +28,7 @@ dependencies { implementation "io.koraframework:config-hocon" testImplementation "io.koraframework:test-junit5" - testImplementation "io.goodforgod:testcontainers-extensions-kafka:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-kafka:0.15.0" } application { @@ -56,10 +56,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/java/kora-java-kafka/src/main/java/io/koraframework/example/kafka/listener/AutoCommitRecordMapperListener.java b/examples/java/kora-java-kafka/src/main/java/io/koraframework/example/kafka/listener/AutoCommitRecordMapperListener.java index 2c1254d9..3b2963f1 100644 --- a/examples/java/kora-java-kafka/src/main/java/io/koraframework/example/kafka/listener/AutoCommitRecordMapperListener.java +++ b/examples/java/kora-java-kafka/src/main/java/io/koraframework/example/kafka/listener/AutoCommitRecordMapperListener.java @@ -1,6 +1,5 @@ package io.koraframework.example.kafka.listener; -import java.io.IOException; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.common.serialization.Deserializer; import io.koraframework.common.annotation.Component; @@ -27,11 +26,7 @@ public MyDeserializer(JsonReader reader) { @Override public MyEvent deserialize(String topic, byte[] data) { - try { - return reader.read(data); - } catch (IOException e) { - throw new IllegalStateException(e); - } + return reader.read(data); } } diff --git a/examples/java/kora-java-kafka/src/main/java/io/koraframework/example/kafka/listener/AutoCommitRecordsTelemetryListener.java b/examples/java/kora-java-kafka/src/main/java/io/koraframework/example/kafka/listener/AutoCommitRecordsTelemetryListener.java deleted file mode 100644 index d09df153..00000000 --- a/examples/java/kora-java-kafka/src/main/java/io/koraframework/example/kafka/listener/AutoCommitRecordsTelemetryListener.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.koraframework.example.kafka.listener; - -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.consumer.ConsumerRecords; -import io.koraframework.common.annotation.Component; -import io.koraframework.kafka.common.annotation.KafkaListener; -import io.koraframework.kafka.common.consumer.telemetry.KafkaConsumerTelemetry; - -@Component -public final class AutoCommitRecordsTelemetryListener extends AbstractListener { - - @KafkaListener("kafka.consumer.my-listener") - void process(ConsumerRecords records, - KafkaConsumerTelemetry.KafkaConsumerRecordsTelemetryContext ctx) { - for (ConsumerRecord record : records) { - var telemetryContext = ctx.get(record); - success(record.value()); - telemetryContext.close(null); - } - } -} diff --git a/examples/java/kora-java-kafka/src/main/java/io/koraframework/example/kafka/listener/AutoCommitValueMapperListener.java b/examples/java/kora-java-kafka/src/main/java/io/koraframework/example/kafka/listener/AutoCommitValueMapperListener.java index e410acf0..ac4b15de 100644 --- a/examples/java/kora-java-kafka/src/main/java/io/koraframework/example/kafka/listener/AutoCommitValueMapperListener.java +++ b/examples/java/kora-java-kafka/src/main/java/io/koraframework/example/kafka/listener/AutoCommitValueMapperListener.java @@ -1,6 +1,5 @@ package io.koraframework.example.kafka.listener; -import java.io.IOException; import org.apache.kafka.common.serialization.Deserializer; import io.koraframework.common.annotation.Component; import io.koraframework.common.annotation.Tag; @@ -26,11 +25,7 @@ public MyDeserializer(JsonReader reader) { @Override public MyEvent deserialize(String topic, byte[] data) { - try { - return reader.read(data); - } catch (IOException e) { - throw new IllegalArgumentException(e); - } + return reader.read(data); } } diff --git a/examples/java/kora-java-kafka/src/main/java/io/koraframework/example/kafka/publisher/ProducerMapperPublisher.java b/examples/java/kora-java-kafka/src/main/java/io/koraframework/example/kafka/publisher/ProducerMapperPublisher.java index 9f69b1a3..57fd3222 100644 --- a/examples/java/kora-java-kafka/src/main/java/io/koraframework/example/kafka/publisher/ProducerMapperPublisher.java +++ b/examples/java/kora-java-kafka/src/main/java/io/koraframework/example/kafka/publisher/ProducerMapperPublisher.java @@ -2,7 +2,6 @@ import static io.koraframework.example.kafka.publisher.ProducerMapperPublisher.MyEvent; -import java.io.IOException; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.common.serialization.Serializer; import io.koraframework.common.annotation.Component; @@ -29,11 +28,7 @@ public MySerializer(JsonWriter writer) { @Override public byte[] serialize(String topic, MyEvent data) { - try { - return writer.toByteArray(data); - } catch (IOException e) { - throw new IllegalArgumentException(e); - } + return writer.toByteArray(data); } } diff --git a/examples/java/kora-java-kafka/src/main/java/io/koraframework/example/kafka/publisher/TopicMapperPublisher.java b/examples/java/kora-java-kafka/src/main/java/io/koraframework/example/kafka/publisher/TopicMapperPublisher.java index 5b6ccb31..f318f0f4 100644 --- a/examples/java/kora-java-kafka/src/main/java/io/koraframework/example/kafka/publisher/TopicMapperPublisher.java +++ b/examples/java/kora-java-kafka/src/main/java/io/koraframework/example/kafka/publisher/TopicMapperPublisher.java @@ -1,6 +1,5 @@ package io.koraframework.example.kafka.publisher; -import java.io.IOException; import org.apache.kafka.common.serialization.Serializer; import io.koraframework.common.annotation.Component; import io.koraframework.common.annotation.Tag; @@ -27,11 +26,7 @@ public MySerializer(JsonWriter writer) { @Override public byte[] serialize(String topic, MyEvent data) { - try { - return writer.toByteArray(data); - } catch (IOException e) { - throw new IllegalArgumentException(e); - } + return writer.toByteArray(data); } } diff --git a/examples/java/kora-java-kafka/src/test/java/io/koraframework/example/kafka/listener/AutoCommitRecordsTelemetryListenerTests.java b/examples/java/kora-java-kafka/src/test/java/io/koraframework/example/kafka/listener/AutoCommitRecordsTelemetryListenerTests.java deleted file mode 100644 index 04189b85..00000000 --- a/examples/java/kora-java-kafka/src/test/java/io/koraframework/example/kafka/listener/AutoCommitRecordsTelemetryListenerTests.java +++ /dev/null @@ -1,55 +0,0 @@ -package io.koraframework.example.kafka.listener; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.kafka.*; -import java.time.Duration; -import java.util.concurrent.Executors; -import org.jetbrains.annotations.NotNull; -import org.json.JSONObject; -import org.junit.jupiter.api.Test; -import org.testcontainers.shaded.org.awaitility.Awaitility; -import io.koraframework.application.graph.Lifecycle; -import io.koraframework.common.annotation.Tag; -import io.koraframework.example.kafka.Application; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersKafka(mode = ContainerMode.PER_RUN, topics = @Topics({ "my-topic-consumer" })) -@KoraAppTest(Application.class) -class AutoCommitRecordsTelemetryListenerTests implements KoraAppTestConfigModifier { - - @ConnectionKafka - private KafkaConnection connection; - - @Tag(AutoCommitRecordsTelemetryListenerModule.AutoCommitRecordsTelemetryListenerProcessTag.class) - @TestComponent - private Lifecycle consumerLifecycle; - - @TestComponent - private AutoCommitRecordsTelemetryListener consumer; - - @NotNull - @Override - public KoraConfigModification config() { - return KoraConfigModification - .ofSystemProperty("KAFKA_BOOTSTRAP", connection.params().bootstrapServers()); - } - - @Test - void processed() { - // given - var topic = "my-topic-consumer"; - var event = new JSONObject().put("username", "Bob").put("code", 1); - - // when - connection.send(topic, Event.ofValueAndRandomKey(event)); - - // then - Awaitility.await() - .atMost(Duration.ofSeconds(15)) - .pollExecutorService(Executors.newSingleThreadExecutor()) - .until(() -> consumer.received().size() == 1); - } -} diff --git a/examples/java/kora-java-openapi-generator-http-client/build.gradle b/examples/java/kora-java-openapi-generator-http-client/build.gradle index a926b018..d6fd9495 100644 --- a/examples/java/kora-java-openapi-generator-http-client/build.gradle +++ b/examples/java/kora-java-openapi-generator-http-client/build.gradle @@ -32,7 +32,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:validation-module" @@ -46,7 +46,7 @@ dependencies { testImplementation "org.json:json:20231013" testImplementation "org.skyscreamer:jsonassert:1.5.1" testImplementation "io.koraframework:test-junit5" - testImplementation "io.goodforgod:testcontainers-extensions-mockserver:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-mockserver:0.15.0" } application { @@ -113,10 +113,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true @@ -156,4 +152,4 @@ javadoc { if (JavaVersion.current().isJava9Compatible()) { options.addBooleanOption("html5", true) } -} \ No newline at end of file +} diff --git a/examples/java/kora-java-openapi-generator-http-client/src/main/java/io/koraframework/example/openapi/http/client/Application.java b/examples/java/kora-java-openapi-generator-http-client/src/main/java/io/koraframework/example/openapi/http/client/Application.java index fd0cdc77..03510e54 100644 --- a/examples/java/kora-java-openapi-generator-http-client/src/main/java/io/koraframework/example/openapi/http/client/Application.java +++ b/examples/java/kora-java-openapi-generator-http-client/src/main/java/io/koraframework/example/openapi/http/client/Application.java @@ -2,7 +2,10 @@ import io.koraframework.application.graph.KoraApplication; import io.koraframework.common.annotation.KoraApp; +import io.koraframework.common.annotation.Tag; import io.koraframework.config.hocon.HoconConfigModule; +import io.koraframework.example.openapi.petV3.api.ApiSecurity; +import io.koraframework.http.client.common.auth.HttpClientTokenProvider; import io.koraframework.http.client.jdk.JdkHttpClientModule; import io.koraframework.json.common.JsonModule; import io.koraframework.logging.logback.LogbackModule; @@ -19,4 +22,18 @@ public interface Application extends static void main(String[] args) { KoraApplication.run(ApplicationGraph::graph); } + + // Сгенерированный ApiSecurity требует HttpClientTokenProvider под тегом каждой схемы, даже если + // приложение её не использует. Перехватчик перебирает схемы по порядку и берёт первую, чей + // провайдер вернул токен, поэтому неиспользуемая схема обязана вернуть null: иначе она перебьёт + // apiKeyAuth, и запрос уйдёт с чужим заголовком. + @Tag(ApiSecurity.BearerAuth.class) + default HttpClientTokenProvider bearerAuthTokenProvider() { + return request -> null; + } + + @Tag(ApiSecurity.OAuth.class) + default HttpClientTokenProvider oAuthTokenProvider() { + return request -> null; + } } diff --git a/examples/java/kora-java-openapi-generator-http-client/src/main/resources/application.conf b/examples/java/kora-java-openapi-generator-http-client/src/main/resources/application.conf index e3c455b2..0232f69a 100644 --- a/examples/java/kora-java-openapi-generator-http-client/src/main/resources/application.conf +++ b/examples/java/kora-java-openapi-generator-http-client/src/main/resources/application.conf @@ -1,4 +1,4 @@ -httpClient.petV2.PetApi { +httpClient.petV2.petApi { url = ${HTTP_CLIENT_PET_V2_URL} requestTimeout = 10s getValuesConfig { @@ -7,7 +7,7 @@ httpClient.petV2.PetApi { telemetry.logging.enabled = true } -httpClient.petV3.PetApi { +httpClient.petV3.petApi { url = ${HTTP_CLIENT_PET_V3_URL} requestTimeout = 10s getValuesConfig { @@ -16,7 +16,10 @@ httpClient.petV3.PetApi { telemetry.logging.enabled = true } -openapiAuth.apiKeyAuth = "MyAuthApiKey" +openapiAuth { + apiKeyAuth = "MyAuthApiKey" + basicAuth { username = "user", password = "pass" } +} logging.level { "root": "WARN" diff --git a/examples/java/kora-java-openapi-generator-http-client/src/test/java/io/koraframework/example/openapi/http/client/HttpClientPetV3Tests.java b/examples/java/kora-java-openapi-generator-http-client/src/test/java/io/koraframework/example/openapi/http/client/HttpClientPetV3Tests.java index 18ca0e34..455a04ab 100644 --- a/examples/java/kora-java-openapi-generator-http-client/src/test/java/io/koraframework/example/openapi/http/client/HttpClientPetV3Tests.java +++ b/examples/java/kora-java-openapi-generator-http-client/src/test/java/io/koraframework/example/openapi/http/client/HttpClientPetV3Tests.java @@ -69,7 +69,7 @@ void getRequestSuccess() { // when // then - var response = petApi.getPetById(1L).block(); + var response = petApi.getPetById(1L); if (response instanceof PetApiResponses.GetPetByIdApiResponse.GetPetById200ApiResponse r) { assertEquals(responseBody.getLong("id"), r.content().id()); assertEquals(responseBody.getString("name"), r.content().name()); @@ -110,7 +110,7 @@ void postRequestSuccess() { new Category(1L, "category"), List.of(new Tag(1L, "tag")), Pet.StatusEnum.AVAILABLE); - var response = petApi.addPet(request).block(); + var response = petApi.addPet(request); if (response instanceof PetApiResponses.AddPetApiResponse.AddPet200ApiResponse r) { assertEquals(request, r.content()); } else { diff --git a/examples/java/kora-java-openapi-generator-http-server/build.gradle b/examples/java/kora-java-openapi-generator-http-server/build.gradle index 2517cc1a..271db8a6 100644 --- a/examples/java/kora-java-openapi-generator-http-server/build.gradle +++ b/examples/java/kora-java-openapi-generator-http-server/build.gradle @@ -32,7 +32,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:validation-module" @@ -55,13 +55,6 @@ application { applicationDefaultJvmArgs = ["-Dfile.encoding=UTF-8"] } -//noinspection GroovyAssignabilityCheck -run { - environment([ - "": "" - ]) -} - // OpenAPI for V2 def openApiGeneratePetV2 = tasks.register("openApiGeneratePetV2", GenerateTask) { generatorName = "kora" @@ -110,10 +103,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true @@ -159,4 +148,4 @@ javadoc { if (JavaVersion.current().isJava9Compatible()) { options.addBooleanOption("html5", true) } -} \ No newline at end of file +} diff --git a/examples/java/kora-java-openapi-generator-http-server/src/main/java/io/koraframework/example/openapi/http/server/Application.java b/examples/java/kora-java-openapi-generator-http-server/src/main/java/io/koraframework/example/openapi/http/server/Application.java index 4c34cff1..28b91592 100644 --- a/examples/java/kora-java-openapi-generator-http-server/src/main/java/io/koraframework/example/openapi/http/server/Application.java +++ b/examples/java/kora-java-openapi-generator-http-server/src/main/java/io/koraframework/example/openapi/http/server/Application.java @@ -33,22 +33,22 @@ default ViolationExceptionHttpServerResponseMapper customViolationExceptionHttpS } @Tag(ApiSecurity.BearerAuth.class) - default HttpServerPrincipalExtractor bearerHttpServerPrincipalExtractor() { - return (request, value) -> CompletableFuture.completedFuture(new UserPrincipal("name")); + default HttpServerPrincipalExtractor bearerHttpServerPrincipalExtractor() { + return (request, value) -> new UserPrincipal("name"); } @Tag(ApiSecurity.BasicAuth.class) - default HttpServerPrincipalExtractor basicHttpServerPrincipalExtractor() { - return (request, value) -> CompletableFuture.completedFuture(new UserPrincipal("name")); + default HttpServerPrincipalExtractor basicHttpServerPrincipalExtractor() { + return (request, value) -> new UserPrincipal("name"); } @Tag(ApiSecurity.ApiKeyAuth.class) - default HttpServerPrincipalExtractor apiKeyHttpServerPrincipalExtractor() { - return (request, value) -> CompletableFuture.completedFuture(new UserPrincipal("name")); + default HttpServerPrincipalExtractor apiKeyHttpServerPrincipalExtractor() { + return (request, value) -> new UserPrincipal("name"); } @Tag(ApiSecurity.OAuth.class) - default HttpServerPrincipalExtractor oauthHttpServerPrincipalExtractor() { - return (request, value) -> CompletableFuture.completedFuture(new UserPrincipal("name")); + default HttpServerPrincipalExtractor oauthHttpServerPrincipalExtractor() { + return (request, value) -> new UserPrincipal("name"); } } diff --git a/examples/java/kora-java-openapi-generator-http-server/src/main/java/io/koraframework/example/openapi/http/server/PetV2Delegate.java b/examples/java/kora-java-openapi-generator-http-server/src/main/java/io/koraframework/example/openapi/http/server/PetV2Delegate.java index 2523e7c3..fb28f477 100644 --- a/examples/java/kora-java-openapi-generator-http-server/src/main/java/io/koraframework/example/openapi/http/server/PetV2Delegate.java +++ b/examples/java/kora-java-openapi-generator-http-server/src/main/java/io/koraframework/example/openapi/http/server/PetV2Delegate.java @@ -34,7 +34,7 @@ public PetApiResponses.DeletePetApiResponse deletePet(long petId, @Nullable Stri @Override public PetApiResponses.FindPetsByStatusApiResponse findPetsByStatus(List status) { final Set petStatuses = status.stream() - .map(Pet.StatusEnum::valueOf) + .map(Pet.StatusEnum::fromValue) .collect(Collectors.toSet()); final List pets = petMap.values().stream() @@ -93,7 +93,7 @@ public PetApiResponses.UpdatePetWithFormApiResponse updatePetWithForm(long petId final Pet updated = pet .withName(form.name()) - .withStatus(Pet.StatusEnum.valueOf(form.status())); + .withStatus(form.status() == null ? null : Pet.StatusEnum.fromValue(form.status())); petMap.put(updated.id(), updated); return new PetApiResponses.UpdatePetWithFormApiResponse.UpdatePetWithForm200ApiResponse(new Message("OK")); diff --git a/examples/java/kora-java-openapi-generator-http-server/src/main/java/io/koraframework/example/openapi/http/server/PetV3Delegate.java b/examples/java/kora-java-openapi-generator-http-server/src/main/java/io/koraframework/example/openapi/http/server/PetV3Delegate.java index f92261e5..8890f760 100644 --- a/examples/java/kora-java-openapi-generator-http-server/src/main/java/io/koraframework/example/openapi/http/server/PetV3Delegate.java +++ b/examples/java/kora-java-openapi-generator-http-server/src/main/java/io/koraframework/example/openapi/http/server/PetV3Delegate.java @@ -6,7 +6,6 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import reactor.core.publisher.Mono; import io.koraframework.common.annotation.Component; import io.koraframework.example.openapi.petV3.api.PetApiDelegate; import io.koraframework.example.openapi.petV3.api.PetApiResponses; @@ -19,33 +18,39 @@ public final class PetV3Delegate implements PetApiDelegate { private final Map petMap = new ConcurrentHashMap<>(); @Override - public Mono addPet(Pet body) { + public PetApiResponses.AddPetApiResponse addPet(Pet body) { petMap.put(body.id(), body); - return Mono.just(new PetApiResponses.AddPetApiResponse.AddPet200ApiResponse(body)); + return new PetApiResponses.AddPetApiResponse.AddPet200ApiResponse(body); } @Override - public Mono deletePet(long petId, @Nullable String apiKey) { + public PetApiResponses.DeletePetApiResponse deletePet(long petId, @Nullable String apiKey) { petMap.remove(petId); - return Mono.just(new PetApiResponses.DeletePetApiResponse.DeletePet200ApiResponse(new Message("OK"))); + return new PetApiResponses.DeletePetApiResponse.DeletePet200ApiResponse(new Message("OK")); } @Override - public Mono findPetsByStatus(@Nullable String status) { + public PetApiResponses.FindPetsByStatusApiResponse findPetsByStatus(@Nullable String status) { if (status == null) { - return Mono.just(new PetApiResponses.FindPetsByStatusApiResponse.FindPetsByStatus400ApiResponse()); + return new PetApiResponses.FindPetsByStatusApiResponse.FindPetsByStatus400ApiResponse(); + } + + final Pet.StatusEnum statusEnum; + try { + statusEnum = Pet.StatusEnum.fromValue(status); + } catch (IllegalArgumentException e) { + return new PetApiResponses.FindPetsByStatusApiResponse.FindPetsByStatus400ApiResponse(); } - final Pet.StatusEnum statusEnum = Pet.StatusEnum.valueOf(status); final List pets = petMap.values().stream() .filter(p -> statusEnum.equals(p.status())) .toList(); - return Mono.just(new PetApiResponses.FindPetsByStatusApiResponse.FindPetsByStatus200ApiResponse(pets)); + return new PetApiResponses.FindPetsByStatusApiResponse.FindPetsByStatus200ApiResponse(pets); } @Override - public Mono findPetsByTags(List tags) { + public PetApiResponses.FindPetsByTagsApiResponse findPetsByTags(List tags) { final Set petTags = new HashSet<>(tags); final List pets = petMap.values().stream() .filter(p -> p.tags() != null) @@ -53,42 +58,42 @@ public Mono findPetsByTags(List getPetById(long petId) { + public PetApiResponses.GetPetByIdApiResponse getPetById(long petId) { if (petId < 0) { - return Mono.just(new PetApiResponses.GetPetByIdApiResponse.GetPetById400ApiResponse()); + return new PetApiResponses.GetPetByIdApiResponse.GetPetById400ApiResponse(); } final Pet pet = petMap.get(petId); if (pet == null) { - return Mono.just(new PetApiResponses.GetPetByIdApiResponse.GetPetById404ApiResponse()); + return new PetApiResponses.GetPetByIdApiResponse.GetPetById404ApiResponse(); } else { - return Mono.just(new PetApiResponses.GetPetByIdApiResponse.GetPetById200ApiResponse(pet)); + return new PetApiResponses.GetPetByIdApiResponse.GetPetById200ApiResponse(pet); } } @Override - public Mono updatePet(Pet body) { + public PetApiResponses.UpdatePetApiResponse updatePet(Pet body) { if (!petMap.containsKey(body.id())) { - return Mono.just(new PetApiResponses.UpdatePetApiResponse.UpdatePet404ApiResponse()); + return new PetApiResponses.UpdatePetApiResponse.UpdatePet404ApiResponse(); } petMap.put(body.id(), body); - return Mono.just(new PetApiResponses.UpdatePetApiResponse.UpdatePet200ApiResponse(body)); + return new PetApiResponses.UpdatePetApiResponse.UpdatePet200ApiResponse(body); } @Override - public Mono + public PetApiResponses.UpdatePetWithFormApiResponse updatePetWithForm(long petId, @Nullable String name, @Nullable String status) { final Pet pet = petMap.get(petId); if (pet == null) { - return Mono.just(new PetApiResponses.UpdatePetWithFormApiResponse.UpdatePetWithForm404ApiResponse()); + return new PetApiResponses.UpdatePetWithFormApiResponse.UpdatePetWithForm404ApiResponse(); } Pet updated = pet; @@ -96,10 +101,10 @@ public Mono updatePet(Pet body) { updated = pet.withName(name); } if (status != null) { - updated = pet.withStatus(Pet.StatusEnum.valueOf(status)); + updated = pet.withStatus(Pet.StatusEnum.fromValue(status)); } petMap.put(updated.id(), updated); - return Mono.just(new PetApiResponses.UpdatePetWithFormApiResponse.UpdatePetWithForm200ApiResponse(new Message("OK"))); + return new PetApiResponses.UpdatePetWithFormApiResponse.UpdatePetWithForm200ApiResponse(new Message("OK")); } } diff --git a/examples/java/kora-java-openapi-generator-http-server/src/main/resources/application.conf b/examples/java/kora-java-openapi-generator-http-server/src/main/resources/application.conf index 650dccc8..6ad7300c 100644 --- a/examples/java/kora-java-openapi-generator-http-server/src/main/resources/application.conf +++ b/examples/java/kora-java-openapi-generator-http-server/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } diff --git a/examples/java/kora-java-resilient/build.gradle b/examples/java/kora-java-resilient/build.gradle index 45228063..e6a5cf89 100644 --- a/examples/java/kora-java-resilient/build.gradle +++ b/examples/java/kora-java-resilient/build.gradle @@ -18,7 +18,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:resilient-kora" @@ -37,9 +37,6 @@ application { //noinspection GroovyAssignabilityCheck run { - environment([ - "": "" - ]) } distTar { @@ -54,10 +51,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/java/kora-java-resilient/src/main/resources/application.conf b/examples/java/kora-java-resilient/src/main/resources/application.conf index 06c5db97..71d4ef16 100644 --- a/examples/java/kora-java-resilient/src/main/resources/application.conf +++ b/examples/java/kora-java-resilient/src/main/resources/application.conf @@ -1,17 +1,14 @@ resilient { circuitbreaker { - default { - type = FIXED_WINDOW - countBased.windowSize = 2 - minimumRequiredCalls = 2 - failureRateThreshold = 100 - permittedCallsInHalfOpenState = 1 - waitDurationInOpenState = 1s - } + # 2.0 does not merge a named section with `default` — the spec points at one explicit path, + # so every required value has to be present here my_cb { type = FIXED_WINDOW countBased.windowSize = 1 minimumRequiredCalls = 1 + failureRateThreshold = 100 + permittedCallsInHalfOpenState = 1 + waitDurationInOpenState = 1s } } timeout { diff --git a/examples/java/kora-java-s3-client-aws/README.md b/examples/java/kora-java-s3-client-aws/README.md index c1ad0953..70af1c87 100644 --- a/examples/java/kora-java-s3-client-aws/README.md +++ b/examples/java/kora-java-s3-client-aws/README.md @@ -3,7 +3,12 @@ Пример модуля S3 AWS в Kora. В примере использовались модули: -- [S3 AWS](https://kora-projects.github.io/kora-docs/ru/documentation/s3-client/#aws) +- `io.koraframework:s3-client-aws` + +> В Kora 2.0 модуль отдаёт в контейнер сам `software.amazon.awssdk.services.s3.S3Client`, +> работа с S3 идёт напрямую через API AWS SDK. Декларативные контракты `@S3.Client` +> в этот артефакт больше не входят — они переехали в `s3-client-kora`, +> см. `kora-java-s3-client-minio`. ## Build diff --git a/examples/java/kora-java-s3-client-aws/build.gradle b/examples/java/kora-java-s3-client-aws/build.gradle index 243831f0..54f770dc 100644 --- a/examples/java/kora-java-s3-client-aws/build.gradle +++ b/examples/java/kora-java-s3-client-aws/build.gradle @@ -18,7 +18,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:s3-client-aws" @@ -29,7 +29,7 @@ dependencies { testImplementation "org.testcontainers:junit-jupiter:1.21.4" testImplementation "org.testcontainers:minio:1.21.4" - testImplementation "io.goodforgod:testcontainers-extensions-minio:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-minio:0.15.0" testImplementation "io.koraframework:test-junit5" } @@ -61,10 +61,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/AsyncS3Client.java b/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/AsyncS3Client.java deleted file mode 100644 index dd58a4a8..00000000 --- a/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/AsyncS3Client.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.koraframework.example.s3.aws; - -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import io.koraframework.s3.client.annotation.S3; -import io.koraframework.s3.client.model.*; - -@S3.Client("my") -public interface AsyncS3Client { - - @S3.Get("pre-{key}") - CompletableFuture getObject(String key); - - @S3.Get - CompletableFuture getObjectMeta(String key); - - @S3.Get - CompletableFuture> getObjects(List keys); - - @S3.Get - CompletableFuture> getObjectMetas(List keys); - - @S3.List("pre-{prefix}") - CompletionStage listObject(String prefix); - - @S3.List(value = "pre-{prefix}", limit = 50) - CompletionStage listObjectMeta(String prefix); - - @S3.Put("pre-{key}") - CompletionStage putObject(String key, S3Body object); - - @S3.Delete("pre-{key}") - CompletionStage deleteObject(String key); - - @S3.Delete - CompletionStage deleteObjects(List keys); -} diff --git a/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/AwsS3Client.java b/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/AwsS3Client.java deleted file mode 100644 index cd1e3b44..00000000 --- a/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/AwsS3Client.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.koraframework.example.s3.aws; - -import java.util.List; -import io.koraframework.s3.client.annotation.S3; -import io.koraframework.s3.client.model.S3Body; -import software.amazon.awssdk.core.ResponseInputStream; -import software.amazon.awssdk.services.s3.model.*; - -@S3.Client("my") -public interface AwsS3Client { - - @S3.Get("pre-{key}") - ResponseInputStream getObject(String key); - - @S3.Get - GetObjectResponse getObjectMeta(String key); - - @S3.List(value = "pre-{prefix}", limit = 50) - ListObjectsV2Response listObjectMeta(String prefix); - - @S3.Put("pre-{key}") - PutObjectResponse putObject(String key, S3Body object); - - @S3.Delete("pre-{key}") - DeleteObjectResponse deleteObject(String key); - - @S3.Delete - DeleteObjectsResponse deleteObjects(List keys); -} diff --git a/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/AwsS3Service.java b/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/AwsS3Service.java new file mode 100644 index 00000000..afb82f79 --- /dev/null +++ b/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/AwsS3Service.java @@ -0,0 +1,59 @@ +package io.koraframework.example.s3.aws; + +import io.koraframework.common.annotation.Component; +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.HeadObjectResponse; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; +import software.amazon.awssdk.services.s3.model.ObjectIdentifier; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; + +import java.util.List; + +/** + * Kora 2.0 exposes the AWS SDK {@link S3Client} itself as a component, so working with S3 through + * this module means working with the AWS SDK API directly. The declarative {@code @S3.Client} + * contracts live in a different artifact now, see {@code kora-java-s3-client-minio}. + */ +@Component +public class AwsS3Service { + + private final S3Client s3Client; + private final String bucket; + + public AwsS3Service(S3Client s3Client, S3Config config) { + this.s3Client = s3Client; + this.bucket = config.bucket(); + } + + public PutObjectResponse putObject(String key, byte[] value) { + return s3Client.putObject(r -> r.bucket(bucket).key(key), RequestBody.fromBytes(value)); + } + + public ResponseInputStream getObject(String key) { + return s3Client.getObject(r -> r.bucket(bucket).key(key)); + } + + public HeadObjectResponse getObjectMeta(String key) { + return s3Client.headObject(r -> r.bucket(bucket).key(key)); + } + + public ListObjectsV2Response listObjects(String prefix) { + return s3Client.listObjectsV2(r -> r.bucket(bucket).prefix(prefix).maxKeys(50)); + } + + public void deleteObject(String key) { + s3Client.deleteObject(r -> r.bucket(bucket).key(key)); + } + + public DeleteObjectsResponse deleteObjects(List keys) { + var identifiers = keys.stream() + .map(key -> ObjectIdentifier.builder().key(key).build()) + .toList(); + + return s3Client.deleteObjects(r -> r.bucket(bucket).delete(d -> d.objects(identifiers))); + } +} diff --git a/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/ReactorS3Client.java b/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/ReactorS3Client.java deleted file mode 100644 index 04af3c86..00000000 --- a/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/ReactorS3Client.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.koraframework.example.s3.aws; - -import java.util.List; -import reactor.core.publisher.Mono; -import io.koraframework.s3.client.annotation.S3; -import io.koraframework.s3.client.model.*; - -@S3.Client("my") -public interface ReactorS3Client { - - @S3.Get("pre-{key}") - Mono getObject(String key); - - @S3.Get - Mono getObjectMeta(String key); - - @S3.Get - Mono> getObjects(List keys); - - @S3.Get - Mono> getObjectMetas(List keys); - - @S3.List("pre-{prefix}") - Mono listObject(String prefix); - - @S3.List(value = "pre-{prefix}", limit = 50) - Mono listObjectMeta(String prefix); - - @S3.Put("pre-{key}") - Mono putObject(String key, S3Body object); - - @S3.Delete("pre-{key}") - Mono deleteObject(String key); - - @S3.Delete - Mono deleteObjects(List keys); -} diff --git a/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/RootService.java b/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/RootService.java index 0f7784ff..f5e20a78 100644 --- a/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/RootService.java +++ b/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/RootService.java @@ -7,18 +7,9 @@ @Component public class RootService { - private final SyncS3Client syncS3Client; - private final AsyncS3Client asyncS3Client; - private final ReactorS3Client reactorS3Client; - private final AwsS3Client awsS3Client; + private final AwsS3Service awsS3Service; - public RootService(SyncS3Client syncS3Client, - AsyncS3Client asyncS3Client, - ReactorS3Client reactorS3Client, - AwsS3Client awsS3Client) { - this.syncS3Client = syncS3Client; - this.asyncS3Client = asyncS3Client; - this.reactorS3Client = reactorS3Client; - this.awsS3Client = awsS3Client; + public RootService(AwsS3Service awsS3Service) { + this.awsS3Service = awsS3Service; } } diff --git a/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/S3Config.java b/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/S3Config.java new file mode 100644 index 00000000..fc96b1b0 --- /dev/null +++ b/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/S3Config.java @@ -0,0 +1,9 @@ +package io.koraframework.example.s3.aws; + +import io.koraframework.config.common.annotation.ConfigSource; + +@ConfigSource("my") +public interface S3Config { + + String bucket(); +} diff --git a/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/SyncS3Client.java b/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/SyncS3Client.java deleted file mode 100644 index 79d8daf0..00000000 --- a/examples/java/kora-java-s3-client-aws/src/main/java/io/koraframework/example/s3/aws/SyncS3Client.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.koraframework.example.s3.aws; - -import java.util.List; -import io.koraframework.s3.client.annotation.S3; -import io.koraframework.s3.client.model.*; - -@S3.Client("my") -public interface SyncS3Client { - - @S3.Get("pre-{key}") - S3Object getObject(String key); - - @S3.Get - S3ObjectMeta getObjectMeta(String key); - - @S3.Get - List getObjects(List keys); - - @S3.Get - List getObjectMetas(List keys); - - @S3.List("pre-{prefix}") - S3ObjectList listObject(String prefix); - - @S3.List(value = "pre-{prefix}", limit = 50) - S3ObjectMetaList listObjectMeta(String prefix); - - @S3.Put("pre-{key}") - S3ObjectUpload putObject(String key, S3Body object); - - @S3.Delete("pre-{key}") - void deleteObject(String key); - - @S3.Delete - void deleteObjects(List keys); -} diff --git a/examples/java/kora-java-s3-client-aws/src/main/resources/application.conf b/examples/java/kora-java-s3-client-aws/src/main/resources/application.conf index c02cb9b6..fcd5ea27 100644 --- a/examples/java/kora-java-s3-client-aws/src/main/resources/application.conf +++ b/examples/java/kora-java-s3-client-aws/src/main/resources/application.conf @@ -1,8 +1,11 @@ -s3client { +s3client.aws { url = ${S3_URL} - accessKey = ${S3_ACCESS_KEY} - secretKey = ${S3_SECRET_KEY} - aws.requestTimeout = 5s + requestTimeout = 5s + + credentials { + accessKey = ${S3_ACCESS_KEY} + secretKey = ${S3_SECRET_KEY} + } } diff --git a/examples/java/kora-java-s3-client-aws/src/test/java/io/koraframework/example/s3/aws/AsyncS3ClientTests.java b/examples/java/kora-java-s3-client-aws/src/test/java/io/koraframework/example/s3/aws/AsyncS3ClientTests.java deleted file mode 100644 index da670fcc..00000000 --- a/examples/java/kora-java-s3-client-aws/src/test/java/io/koraframework/example/s3/aws/AsyncS3ClientTests.java +++ /dev/null @@ -1,179 +0,0 @@ -package io.koraframework.example.s3.aws; - -import static org.junit.jupiter.api.Assertions.*; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.minio.Bucket; -import io.goodforgod.testcontainers.extensions.minio.ConnectionMinio; -import io.goodforgod.testcontainers.extensions.minio.MinioConnection; -import io.goodforgod.testcontainers.extensions.minio.TestcontainersMinio; -import java.net.http.HttpRequest; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.concurrent.CompletionException; -import org.junit.jupiter.api.Test; -import io.koraframework.s3.client.S3NotFoundException; -import io.koraframework.s3.client.model.S3Body; -import io.koraframework.s3.client.model.S3Object; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; -import software.amazon.awssdk.services.s3.S3Client; - -@TestcontainersMinio( - mode = ContainerMode.PER_RUN, - bucket = @Bucket( - value = AsyncS3ClientTests.BUCKET, - create = Bucket.Mode.PER_METHOD, - drop = Bucket.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class AsyncS3ClientTests implements KoraAppTestConfigModifier { - - static final String BUCKET = "simple"; - - @ConnectionMinio - private MinioConnection minioConnection; - - @TestComponent - private AsyncS3Client client; - @TestComponent - private S3Client s3Client; - - @Override - public KoraConfigModification config() { - return KoraConfigModification - .ofSystemProperty("S3_URL", minioConnection.params().uri().toString()) - .withSystemProperty("S3_ACCESS_KEY", minioConnection.params().accessKey()) - .withSystemProperty("S3_SECRET_KEY", minioConnection.params().secretKey()) - .withSystemProperty("S3_BUCKET", BUCKET); - } - - @Test - void putAndGetObject() { - // given - var key = "k1"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofPublisher(HttpRequest.BodyPublishers.ofByteArray(value), value.length)) - .toCompletableFuture().join(); - - // when - var found = client.getObject(key).join(); - assertNotNull(found); - assertArrayEquals(value, found.body().asBytes()); - - // then - var ex = assertThrows(CompletionException.class, () -> client.getObject("k2").join()); - assertInstanceOf(S3NotFoundException.class, ex.getCause()); - } - - @Test - void putAndGetMeta() { - // given - var key = "k1"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofBytes(value)).toCompletableFuture().join(); - - // when - var found = client.getObjectMeta("pre-" + key).join(); - assertNotNull(found); - - // then - var ex = assertThrows(CompletionException.class, () -> client.getObject("k2").join()); - assertInstanceOf(S3NotFoundException.class, ex.getCause()); - } - - @Test - void putAndGetManyObjects() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)).toCompletableFuture().join(); - client.putObject(key2, S3Body.ofBytes(value)).toCompletableFuture().join(); - - // when - var found = client.getObjects(List.of("pre-" + key1, "pre-" + key2)).join(); - assertEquals(2, found.size()); - for (S3Object object : found) { - assertEquals("value", new String(object.body().asBytes(), StandardCharsets.UTF_8)); - } - } - - @Test - void putAndGetManyMetas() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)).toCompletableFuture().join(); - client.putObject(key2, S3Body.ofBytes(value)).toCompletableFuture().join(); - - // when - var found = client.getObjectMetas(List.of("pre-" + key1, "pre-" + key2)).join(); - assertEquals(2, found.size()); - } - - @Test - void putAndListObjects() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)).toCompletableFuture().join(); - client.putObject(key2, S3Body.ofBytes(value)).toCompletableFuture().join(); - - // when - var found = client.listObject("k").toCompletableFuture().join(); - assertEquals(2, found.metas().size()); - for (S3Object object : found.objects()) { - assertEquals("value", new String(object.body().asBytes(), StandardCharsets.UTF_8)); - } - } - - @Test - void putAndListMetas() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)).toCompletableFuture().join(); - client.putObject(key2, S3Body.ofBytes(value)).toCompletableFuture().join(); - - // when - var found = client.listObjectMeta("k").toCompletableFuture().join(); - assertEquals(2, found.metas().size()); - } - - @Test - void putAndDelete() { - // given - var key = "k1"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofBytes(value)).toCompletableFuture().join(); - - // when - client.deleteObject(key).toCompletableFuture().join(); - - // then - var ex = assertThrows(CompletionException.class, () -> client.getObject("k1").join()); - assertInstanceOf(S3NotFoundException.class, ex.getCause()); - } - - @Test - void putAndDeleteMany() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)).toCompletableFuture().join(); - client.putObject(key2, S3Body.ofBytes(value)).toCompletableFuture().join(); - - // when - client.deleteObjects(List.of("pre-k1", "pre-k2")).toCompletableFuture().join(); - var ex1 = assertThrows(CompletionException.class, () -> client.getObject("k1").join()); - assertInstanceOf(S3NotFoundException.class, ex1.getCause()); - var ex2 = assertThrows(CompletionException.class, () -> client.getObject("k2").join()); - assertInstanceOf(S3NotFoundException.class, ex2.getCause()); - } -} diff --git a/examples/java/kora-java-s3-client-aws/src/test/java/io/koraframework/example/s3/aws/AwsS3ClientTests.java b/examples/java/kora-java-s3-client-aws/src/test/java/io/koraframework/example/s3/aws/AwsS3ServiceTests.java similarity index 61% rename from examples/java/kora-java-s3-client-aws/src/test/java/io/koraframework/example/s3/aws/AwsS3ClientTests.java rename to examples/java/kora-java-s3-client-aws/src/test/java/io/koraframework/example/s3/aws/AwsS3ServiceTests.java index f352b3f3..b4e4326d 100644 --- a/examples/java/kora-java-s3-client-aws/src/test/java/io/koraframework/example/s3/aws/AwsS3ClientTests.java +++ b/examples/java/kora-java-s3-client-aws/src/test/java/io/koraframework/example/s3/aws/AwsS3ServiceTests.java @@ -7,25 +7,24 @@ import io.goodforgod.testcontainers.extensions.minio.ConnectionMinio; import io.goodforgod.testcontainers.extensions.minio.MinioConnection; import io.goodforgod.testcontainers.extensions.minio.TestcontainersMinio; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.List; import org.junit.jupiter.api.Test; -import io.koraframework.s3.client.model.S3Body; import io.koraframework.test.extension.junit5.KoraAppTest; import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; import io.koraframework.test.extension.junit5.KoraConfigModification; import io.koraframework.test.extension.junit5.TestComponent; -import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.NoSuchKeyException; @TestcontainersMinio( mode = ContainerMode.PER_RUN, bucket = @Bucket( - value = AwsS3ClientTests.BUCKET, + value = AwsS3ServiceTests.BUCKET, create = Bucket.Mode.PER_METHOD, drop = Bucket.Mode.PER_METHOD)) @KoraAppTest(Application.class) -class AwsS3ClientTests implements KoraAppTestConfigModifier { +class AwsS3ServiceTests implements KoraAppTestConfigModifier { static final String BUCKET = "simple"; @@ -33,9 +32,7 @@ class AwsS3ClientTests implements KoraAppTestConfigModifier { private MinioConnection minioConnection; @TestComponent - private AwsS3Client client; - @TestComponent - private S3Client s3Client; + private AwsS3Service service; @Override public KoraConfigModification config() { @@ -47,18 +44,19 @@ public KoraConfigModification config() { } @Test - void putAndGetObject() { + void putAndGetObject() throws IOException { // given var key = "k1"; var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofBytes(value)); + service.putObject(key, value); // when - var found = client.getObject(key); - assertNotNull(found); + try (var found = service.getObject(key)) { + assertArrayEquals(value, found.readAllBytes()); + } // then - assertThrows(NoSuchKeyException.class, () -> client.getObject("k2")); + assertThrows(NoSuchKeyException.class, () -> service.getObject("k2")); } @Test @@ -66,27 +64,27 @@ void putAndGetMeta() { // given var key = "k1"; var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofBytes(value)); + service.putObject(key, value); // when - var found = client.getObjectMeta("pre-" + key); - assertNotNull(found); + var found = service.getObjectMeta(key); + assertEquals(value.length, found.contentLength()); // then - assertThrows(NoSuchKeyException.class, () -> client.getObjectMeta("k2")); + assertThrows(NoSuchKeyException.class, () -> service.getObjectMeta("k2")); } @Test - void putAndListMetas() { + void putAndListObjects() { // given - var key1 = "k1"; - var key2 = "k2"; var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)); - client.putObject(key2, S3Body.ofBytes(value)); + service.putObject("k1", value); + service.putObject("k2", value); // when - var found = client.listObjectMeta("k"); + var found = service.listObjects("k"); + + // then assertEquals(2, found.contents().size()); } @@ -95,27 +93,27 @@ void putAndDelete() { // given var key = "k1"; var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofBytes(value)); + service.putObject(key, value); // when - client.deleteObject(key); + service.deleteObject(key); // then - assertThrows(NoSuchKeyException.class, () -> client.getObject("k1")); + assertThrows(NoSuchKeyException.class, () -> service.getObject(key)); } @Test void putAndDeleteMany() { // given - var key1 = "k1"; - var key2 = "k2"; var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)); - client.putObject(key2, S3Body.ofBytes(value)); + service.putObject("k1", value); + service.putObject("k2", value); // when - client.deleteObjects(List.of("pre-k1", "pre-k2")); - assertThrows(NoSuchKeyException.class, () -> client.getObject("k1")); - assertThrows(NoSuchKeyException.class, () -> client.getObject("k2")); + service.deleteObjects(List.of("k1", "k2")); + + // then + assertThrows(NoSuchKeyException.class, () -> service.getObject("k1")); + assertThrows(NoSuchKeyException.class, () -> service.getObject("k2")); } } diff --git a/examples/java/kora-java-s3-client-aws/src/test/java/io/koraframework/example/s3/aws/ReactorS3ClientTests.java b/examples/java/kora-java-s3-client-aws/src/test/java/io/koraframework/example/s3/aws/ReactorS3ClientTests.java deleted file mode 100644 index 36b21b07..00000000 --- a/examples/java/kora-java-s3-client-aws/src/test/java/io/koraframework/example/s3/aws/ReactorS3ClientTests.java +++ /dev/null @@ -1,177 +0,0 @@ -package io.koraframework.example.s3.aws; - -import static org.junit.jupiter.api.Assertions.*; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.minio.Bucket; -import io.goodforgod.testcontainers.extensions.minio.ConnectionMinio; -import io.goodforgod.testcontainers.extensions.minio.MinioConnection; -import io.goodforgod.testcontainers.extensions.minio.TestcontainersMinio; -import java.net.http.HttpRequest; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Optional; -import org.junit.jupiter.api.Test; -import io.koraframework.s3.client.model.S3Body; -import io.koraframework.s3.client.model.S3Object; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; -import software.amazon.awssdk.services.s3.S3Client; - -@TestcontainersMinio( - mode = ContainerMode.PER_RUN, - bucket = @Bucket( - value = ReactorS3ClientTests.BUCKET, - create = Bucket.Mode.PER_METHOD, - drop = Bucket.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class ReactorS3ClientTests implements KoraAppTestConfigModifier { - - static final String BUCKET = "simple"; - - @ConnectionMinio - private MinioConnection minioConnection; - - @TestComponent - private ReactorS3Client client; - @TestComponent - private S3Client s3Client; - - @Override - public KoraConfigModification config() { - return KoraConfigModification - .ofSystemProperty("S3_URL", minioConnection.params().uri().toString()) - .withSystemProperty("S3_ACCESS_KEY", minioConnection.params().accessKey()) - .withSystemProperty("S3_SECRET_KEY", minioConnection.params().secretKey()) - .withSystemProperty("S3_BUCKET", BUCKET); - } - - @Test - void putAndGetObject() { - // given - var key = "k1"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofPublisher(HttpRequest.BodyPublishers.ofByteArray(value), value.length)).block(); - - // when - var found = client.getObject(key).block(); - assertNotNull(found); - assertArrayEquals(value, found.body().asBytes()); - - // then - Optional k2 = client.getObject("k2").blockOptional(); - assertTrue(k2.isEmpty()); - } - - @Test - void putAndGetMeta() { - // given - var key = "k1"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofBytes(value)).block(); - - // when - var found = client.getObjectMeta("pre-" + key).block(); - assertNotNull(found); - - // then - Optional k2 = client.getObject("k2").blockOptional(); - assertTrue(k2.isEmpty()); - } - - @Test - void putAndGetManyObjects() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)).block(); - client.putObject(key2, S3Body.ofBytes(value)).block(); - - // when - var found = client.getObjects(List.of("pre-" + key1, "pre-" + key2)).block(); - assertEquals(2, found.size()); - for (S3Object object : found) { - assertEquals("value", new String(object.body().asBytes(), StandardCharsets.UTF_8)); - } - } - - @Test - void putAndGetManyMetas() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)).block(); - client.putObject(key2, S3Body.ofBytes(value)).block(); - - // when - var found = client.getObjectMetas(List.of("pre-" + key1, "pre-" + key2)).block(); - assertEquals(2, found.size()); - } - - @Test - void putAndListObjects() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)).block(); - client.putObject(key2, S3Body.ofBytes(value)).block(); - - // when - var found = client.listObject("k").block(); - assertEquals(2, found.metas().size()); - for (S3Object object : found.objects()) { - assertEquals("value", new String(object.body().asBytes(), StandardCharsets.UTF_8)); - } - } - - @Test - void putAndListMetas() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)).block(); - client.putObject(key2, S3Body.ofBytes(value)).block(); - - // when - var found = client.listObjectMeta("k").block(); - assertEquals(2, found.metas().size()); - } - - @Test - void putAndDelete() { - // given - var key = "k1"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofBytes(value)).block(); - - // when - client.deleteObject(key).block(); - - // then - Optional k1 = client.getObject("k1").blockOptional(); - assertTrue(k1.isEmpty()); - } - - @Test - void putAndDeleteMany() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)).block(); - client.putObject(key2, S3Body.ofBytes(value)).block(); - - // when - client.deleteObjects(List.of("pre-k1", "pre-k2")).block(); - Optional k1 = client.getObject("k1").blockOptional(); - assertTrue(k1.isEmpty()); - Optional k2 = client.getObject("k2").blockOptional(); - assertTrue(k2.isEmpty()); - } -} diff --git a/examples/java/kora-java-s3-client-aws/src/test/java/io/koraframework/example/s3/aws/SyncS3ClientTests.java b/examples/java/kora-java-s3-client-aws/src/test/java/io/koraframework/example/s3/aws/SyncS3ClientTests.java deleted file mode 100644 index e0864e34..00000000 --- a/examples/java/kora-java-s3-client-aws/src/test/java/io/koraframework/example/s3/aws/SyncS3ClientTests.java +++ /dev/null @@ -1,172 +0,0 @@ -package io.koraframework.example.s3.aws; - -import static org.junit.jupiter.api.Assertions.*; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.minio.Bucket; -import io.goodforgod.testcontainers.extensions.minio.ConnectionMinio; -import io.goodforgod.testcontainers.extensions.minio.MinioConnection; -import io.goodforgod.testcontainers.extensions.minio.TestcontainersMinio; -import java.net.http.HttpRequest; -import java.nio.charset.StandardCharsets; -import java.util.List; -import org.junit.jupiter.api.Test; -import io.koraframework.s3.client.S3NotFoundException; -import io.koraframework.s3.client.model.S3Body; -import io.koraframework.s3.client.model.S3Object; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; -import software.amazon.awssdk.services.s3.S3Client; - -@TestcontainersMinio( - mode = ContainerMode.PER_RUN, - bucket = @Bucket( - value = SyncS3ClientTests.BUCKET, - create = Bucket.Mode.PER_METHOD, - drop = Bucket.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class SyncS3ClientTests implements KoraAppTestConfigModifier { - - static final String BUCKET = "simple"; - - @ConnectionMinio - private MinioConnection minioConnection; - - @TestComponent - private SyncS3Client client; - @TestComponent - private S3Client s3Client; - - @Override - public KoraConfigModification config() { - return KoraConfigModification - .ofSystemProperty("S3_URL", minioConnection.params().uri().toString()) - .withSystemProperty("S3_ACCESS_KEY", minioConnection.params().accessKey()) - .withSystemProperty("S3_SECRET_KEY", minioConnection.params().secretKey()) - .withSystemProperty("S3_BUCKET", BUCKET); - } - - @Test - void putAndGetObject() { - // given - var key = "k1"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofPublisher(HttpRequest.BodyPublishers.ofByteArray(value), value.length)); - - // when - var found = client.getObject(key); - assertNotNull(found); - assertArrayEquals(value, found.body().asBytes()); - - // then - assertThrows(S3NotFoundException.class, () -> client.getObject("k2")); - } - - @Test - void putAndGetMeta() { - // given - var key = "k1"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofBytes(value)); - - // when - var found = client.getObjectMeta("pre-" + key); - assertNotNull(found); - - // then - assertThrows(S3NotFoundException.class, () -> client.getObjectMeta("k2")); - } - - @Test - void putAndGetManyObjects() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)); - client.putObject(key2, S3Body.ofBytes(value)); - - // when - var found = client.getObjects(List.of("pre-" + key1, "pre-" + key2)); - assertEquals(2, found.size()); - for (S3Object object : found) { - assertEquals("value", new String(object.body().asBytes(), StandardCharsets.UTF_8)); - } - } - - @Test - void putAndGetManyMetas() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)); - client.putObject(key2, S3Body.ofBytes(value)); - - // when - var found = client.getObjectMetas(List.of("pre-" + key1, "pre-" + key2)); - assertEquals(2, found.size()); - } - - @Test - void putAndListObjects() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)); - client.putObject(key2, S3Body.ofBytes(value)); - - // when - var found = client.listObject("k"); - assertEquals(2, found.metas().size()); - for (S3Object object : found.objects()) { - assertEquals("value", new String(object.body().asBytes(), StandardCharsets.UTF_8)); - } - } - - @Test - void putAndListMetas() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)); - client.putObject(key2, S3Body.ofBytes(value)); - - // when - var found = client.listObjectMeta("k"); - assertEquals(2, found.metas().size()); - } - - @Test - void putAndDelete() { - // given - var key = "k1"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofBytes(value)); - - // when - client.deleteObject(key); - - // then - assertThrows(S3NotFoundException.class, () -> client.getObject("k1")); - } - - @Test - void putAndDeleteMany() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)); - client.putObject(key2, S3Body.ofBytes(value)); - - // when - client.deleteObjects(List.of("pre-k1", "pre-k2")); - assertThrows(S3NotFoundException.class, () -> client.getObject("k1")); - assertThrows(S3NotFoundException.class, () -> client.getObject("k2")); - } -} diff --git a/examples/java/kora-java-database-r2dbc/Dockerfile b/examples/java/kora-java-s3-client-kora/Dockerfile similarity index 100% rename from examples/java/kora-java-database-r2dbc/Dockerfile rename to examples/java/kora-java-s3-client-kora/Dockerfile diff --git a/examples/java/kora-java-s3-client-kora/README.md b/examples/java/kora-java-s3-client-kora/README.md new file mode 100644 index 00000000..2cb6ee8c --- /dev/null +++ b/examples/java/kora-java-s3-client-kora/README.md @@ -0,0 +1,42 @@ +# Kora Java S3 Minio + +Пример декларативного S3 клиента Kora, который работает против Minio как S3-совместимого хранилища. + +В примере использовались модули: +- `io.koraframework.experimental:s3-client-kora` + +> В Kora 1.x декларативный клиент `@S3.Client` был реализован поверх SDK Minio +> (`s3-client-minio`). В Kora 2.0 этой реализации больше нет: декларативный клиент +> собран поверх собственного HTTP клиента Kora и живёт в артефакте `s3-client-kora`. +> Minio здесь остаётся только как хранилище, против которого гоняются тесты. +> Обёртка над AWS SDK — отдельный модуль, см. `kora-java-s3-client-aws`. + +## Build + +Собрать классы: + +```shell +./gradlew classes +``` + +Собрать артефакт: + +```shell +./gradlew distTar +``` + +## Run + +Запустить локально: +```shell +./gradlew run +``` + +## Test + +Тесты используют [Testcontainers](https://java.testcontainers.org/), требуется [Docker](https://docs.docker.com/engine/install/) окружение для запуска тестов или аналогичные контейнерные окружения ([colima](https://github.com/abiosoft/colima) / итп) + +Протестировать локально: +```shell +./gradlew test +``` diff --git a/examples/java/kora-java-s3-client-minio/build.gradle b/examples/java/kora-java-s3-client-kora/build.gradle similarity index 89% rename from examples/java/kora-java-s3-client-minio/build.gradle rename to examples/java/kora-java-s3-client-kora/build.gradle index 3b9d7ca0..0cff5d81 100644 --- a/examples/java/kora-java-s3-client-minio/build.gradle +++ b/examples/java/kora-java-s3-client-kora/build.gradle @@ -18,19 +18,18 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" - implementation "io.koraframework.experimental:s3-client-minio" + implementation "io.koraframework.experimental:s3-client-kora" implementation "io.koraframework:http-client-ok" - implementation "io.projectreactor:reactor-core:3.6.18" // For reactive examples (optional) implementation "io.koraframework:logging-logback" implementation "io.koraframework:config-hocon" testImplementation "org.testcontainers:junit-jupiter:1.21.4" testImplementation "org.testcontainers:minio:1.21.4" - testImplementation "io.goodforgod:testcontainers-extensions-minio:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-minio:0.15.0" testImplementation "io.koraframework:test-junit5" } @@ -62,10 +61,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/java/kora-java-s3-client-minio/docker-compose.yml b/examples/java/kora-java-s3-client-kora/docker-compose.yml similarity index 100% rename from examples/java/kora-java-s3-client-minio/docker-compose.yml rename to examples/java/kora-java-s3-client-kora/docker-compose.yml diff --git a/examples/java/kora-java-s3-client-minio/gradle.properties b/examples/java/kora-java-s3-client-kora/gradle.properties similarity index 100% rename from examples/java/kora-java-s3-client-minio/gradle.properties rename to examples/java/kora-java-s3-client-kora/gradle.properties diff --git a/examples/java/kora-java-s3-client-minio/src/main/java/io/koraframework/example/s3/minio/Application.java b/examples/java/kora-java-s3-client-kora/src/main/java/io/koraframework/example/s3/kora/Application.java similarity index 79% rename from examples/java/kora-java-s3-client-minio/src/main/java/io/koraframework/example/s3/minio/Application.java rename to examples/java/kora-java-s3-client-kora/src/main/java/io/koraframework/example/s3/kora/Application.java index f90db29a..1dd4bad3 100644 --- a/examples/java/kora-java-s3-client-minio/src/main/java/io/koraframework/example/s3/minio/Application.java +++ b/examples/java/kora-java-s3-client-kora/src/main/java/io/koraframework/example/s3/kora/Application.java @@ -1,17 +1,17 @@ -package io.koraframework.example.s3.minio; +package io.koraframework.example.s3.kora; import io.koraframework.application.graph.KoraApplication; import io.koraframework.common.annotation.KoraApp; import io.koraframework.config.hocon.HoconConfigModule; import io.koraframework.http.client.ok.OkHttpClientModule; import io.koraframework.logging.logback.LogbackModule; -import io.koraframework.s3.client.minio.MinioS3ClientModule; +import io.koraframework.s3.client.kora.KoraS3ClientModule; @KoraApp public interface Application extends HoconConfigModule, LogbackModule, - MinioS3ClientModule, + KoraS3ClientModule, OkHttpClientModule { static void main(String[] args) { diff --git a/examples/java/kora-java-s3-client-kora/src/main/java/io/koraframework/example/s3/kora/RootService.java b/examples/java/kora-java-s3-client-kora/src/main/java/io/koraframework/example/s3/kora/RootService.java new file mode 100644 index 00000000..ecf6a98a --- /dev/null +++ b/examples/java/kora-java-s3-client-kora/src/main/java/io/koraframework/example/s3/kora/RootService.java @@ -0,0 +1,15 @@ +package io.koraframework.example.s3.kora; + +import io.koraframework.common.annotation.Component; +import io.koraframework.common.annotation.Root; + +@Root +@Component +public class RootService { + + private final SyncS3Client syncS3Client; + + public RootService(SyncS3Client syncS3Client) { + this.syncS3Client = syncS3Client; + } +} diff --git a/examples/java/kora-java-s3-client-kora/src/main/java/io/koraframework/example/s3/kora/SyncS3Client.java b/examples/java/kora-java-s3-client-kora/src/main/java/io/koraframework/example/s3/kora/SyncS3Client.java new file mode 100644 index 00000000..f3fa0df9 --- /dev/null +++ b/examples/java/kora-java-s3-client-kora/src/main/java/io/koraframework/example/s3/kora/SyncS3Client.java @@ -0,0 +1,37 @@ +package io.koraframework.example.s3.kora; + +import java.util.List; +import io.koraframework.s3.client.kora.annotation.S3; +import io.koraframework.s3.client.kora.model.response.GetObjectResult; +import io.koraframework.s3.client.kora.model.response.HeadObjectResult; +import io.koraframework.s3.client.kora.model.response.ListBucketResult; + +/** + * Bucket name is read from the {@code my.bucket} configuration path: a leading dot in + * {@code @S3.Bucket} makes the path relative to the client path declared in {@code @S3.Client}. + */ +@S3.Client("my") +@S3.Bucket(".bucket") +public interface SyncS3Client { + + @S3.Get("pre-{key}") + GetObjectResult getObject(String key); + + @S3.Get("pre-{key}") + byte[] getObjectAsBytes(String key); + + @S3.Head("pre-{key}") + HeadObjectResult getObjectMeta(String key); + + @S3.List("pre-{prefix}") + ListBucketResult listObjects(String prefix); + + @S3.List("pre-{prefix}") + List listObjectKeys(String prefix); + + @S3.Put("pre-{key}") + String putObject(String key, byte[] value); + + @S3.Delete("pre-{key}") + void deleteObject(String key); +} diff --git a/examples/kotlin/kora-kotlin-s3-client-minio/src/main/resources/application.conf b/examples/java/kora-java-s3-client-kora/src/main/resources/application.conf similarity index 55% rename from examples/kotlin/kora-kotlin-s3-client-minio/src/main/resources/application.conf rename to examples/java/kora-java-s3-client-kora/src/main/resources/application.conf index 9a94ec10..9712519a 100644 --- a/examples/kotlin/kora-kotlin-s3-client-minio/src/main/resources/application.conf +++ b/examples/java/kora-java-s3-client-kora/src/main/resources/application.conf @@ -1,11 +1,12 @@ -s3client { - url = ${S3_URL} - accessKey = ${S3_ACCESS_KEY} - secretKey = ${S3_SECRET_KEY} -} - +my { + endpoint = ${S3_URL} + bucket = ${S3_BUCKET} -my.bucket = ${S3_BUCKET} + credentials { + accessKey = ${S3_ACCESS_KEY} + secretKey = ${S3_SECRET_KEY} + } +} httpClient { diff --git a/examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/resources/logback.xml b/examples/java/kora-java-s3-client-kora/src/main/resources/logback.xml similarity index 100% rename from examples/graalvm/kora-java-graalvm-crud-r2dbc/src/main/resources/logback.xml rename to examples/java/kora-java-s3-client-kora/src/main/resources/logback.xml diff --git a/examples/java/kora-java-s3-client-kora/src/test/java/io/koraframework/example/s3/kora/SyncS3ClientTests.java b/examples/java/kora-java-s3-client-kora/src/test/java/io/koraframework/example/s3/kora/SyncS3ClientTests.java new file mode 100644 index 00000000..8fe2a588 --- /dev/null +++ b/examples/java/kora-java-s3-client-kora/src/test/java/io/koraframework/example/s3/kora/SyncS3ClientTests.java @@ -0,0 +1,131 @@ +package io.koraframework.example.s3.kora; + +import static org.junit.jupiter.api.Assertions.*; + +import io.goodforgod.testcontainers.extensions.ContainerMode; +import io.goodforgod.testcontainers.extensions.minio.Bucket; +import io.goodforgod.testcontainers.extensions.minio.ConnectionMinio; +import io.goodforgod.testcontainers.extensions.minio.MinioConnection; +import io.goodforgod.testcontainers.extensions.minio.TestcontainersMinio; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; +import io.koraframework.s3.client.kora.exception.S3ClientNoSuchKeyException; +import io.koraframework.test.extension.junit5.KoraAppTest; +import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; +import io.koraframework.test.extension.junit5.KoraConfigModification; +import io.koraframework.test.extension.junit5.TestComponent; + +@TestcontainersMinio( + mode = ContainerMode.PER_RUN, + bucket = @Bucket( + value = SyncS3ClientTests.BUCKET, + create = Bucket.Mode.PER_METHOD, + drop = Bucket.Mode.PER_METHOD)) +@KoraAppTest(Application.class) +class SyncS3ClientTests implements KoraAppTestConfigModifier { + + static final String BUCKET = "simple"; + + @ConnectionMinio + private MinioConnection minioConnection; + + @TestComponent + private SyncS3Client client; + + @Override + public KoraConfigModification config() { + return KoraConfigModification + .ofSystemProperty("S3_URL", minioConnection.params().uri().toString()) + .withSystemProperty("S3_ACCESS_KEY", minioConnection.params().accessKey()) + .withSystemProperty("S3_SECRET_KEY", minioConnection.params().secretKey()) + .withSystemProperty("S3_BUCKET", BUCKET); + } + + @Test + void putAndGetObject() throws IOException { + // given + var key = "k1"; + var value = "value".getBytes(StandardCharsets.UTF_8); + client.putObject(key, value); + + // when + try (var found = client.getObject(key); var body = found.body().asInputStream()) { + assertArrayEquals(value, body.readAllBytes()); + } + + // then + assertThrows(S3ClientNoSuchKeyException.class, () -> client.getObject("k2")); + } + + @Test + void putAndGetObjectAsBytes() { + // given + var key = "k1"; + var value = "value".getBytes(StandardCharsets.UTF_8); + client.putObject(key, value); + + // when + var found = client.getObjectAsBytes(key); + + // then + assertArrayEquals(value, found); + } + + @Test + void putAndGetMeta() { + // given + var key = "k1"; + var value = "value".getBytes(StandardCharsets.UTF_8); + client.putObject(key, value); + + // when + var found = client.getObjectMeta(key); + assertEquals(value.length, found.size()); + + // then + assertThrows(S3ClientNoSuchKeyException.class, () -> client.getObjectMeta("k2")); + } + + @Test + void putAndListObjects() { + // given + var value = "value".getBytes(StandardCharsets.UTF_8); + client.putObject("k1", value); + client.putObject("k2", value); + + // when + var found = client.listObjects("k"); + + // then + assertEquals(2, found.items().size()); + } + + @Test + void putAndListObjectKeys() { + // given + var value = "value".getBytes(StandardCharsets.UTF_8); + client.putObject("k1", value); + client.putObject("k2", value); + + // when + var found = client.listObjectKeys("k"); + + // then + assertIterableEquals(java.util.List.of("pre-k1", "pre-k2"), found); + } + + @Test + void putAndDelete() { + // given + var key = "k1"; + var value = "value".getBytes(StandardCharsets.UTF_8); + client.putObject(key, value); + + // when + client.deleteObject(key); + + // then + assertThrows(S3ClientNoSuchKeyException.class, () -> client.getObject(key)); + } +} diff --git a/examples/java/kora-java-s3-client-minio/src/test/resources/logback-test.xml b/examples/java/kora-java-s3-client-kora/src/test/resources/logback-test.xml similarity index 100% rename from examples/java/kora-java-s3-client-minio/src/test/resources/logback-test.xml rename to examples/java/kora-java-s3-client-kora/src/test/resources/logback-test.xml diff --git a/examples/java/kora-java-s3-client-minio/Dockerfile b/examples/java/kora-java-s3-client-minio/Dockerfile deleted file mode 100644 index c22138f1..00000000 --- a/examples/java/kora-java-s3-client-minio/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -ARG RUN_IMAGE=eclipse-temurin:25-jre-jammy -FROM ${RUN_IMAGE} - -ARG TARGET_DIR=/opt/app -ARG SOURCE_DIR=build/distributions - -COPY $SOURCE_DIR/*.tar application.tar -RUN mkdir $TARGET_DIR -RUN tar -xf application.tar -C $TARGET_DIR -RUN rm application.tar - -ARG DOCKER_USER=app -RUN groupadd -r $DOCKER_USER && useradd -rg $DOCKER_USER $DOCKER_USER -USER $DOCKER_USER - -EXPOSE 8080/tcp -EXPOSE 8085/tcp -CMD [ "/opt/app/application/bin/application" ] \ No newline at end of file diff --git a/examples/java/kora-java-s3-client-minio/README.md b/examples/java/kora-java-s3-client-minio/README.md deleted file mode 100644 index 8241d15d..00000000 --- a/examples/java/kora-java-s3-client-minio/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Kora Java S3 Minio - -Пример модуля S3 Minio в Kora. - -В примере использовались модули: -- [S3 Minio](https://kora-projects.github.io/kora-docs/ru/documentation/s3-client/#minio) - -## Build - -Собрать классы: - -```shell -./gradlew classes -``` - -Собрать артефакт: - -```shell -./gradlew distTar -``` - -## Run - -Запустить локально: -```shell -./gradlew run -``` - -## Test - -Тесты используют [Testcontainers](https://java.testcontainers.org/), требуется [Docker](https://docs.docker.com/engine/install/) окружение для запуска тестов или аналогичные контейнерные окружения ([colima](https://github.com/abiosoft/colima) / итп) - -Протестировать локально: -```shell -./gradlew test -``` diff --git a/examples/java/kora-java-s3-client-minio/src/main/java/io/koraframework/example/s3/minio/AsyncS3Client.java b/examples/java/kora-java-s3-client-minio/src/main/java/io/koraframework/example/s3/minio/AsyncS3Client.java deleted file mode 100644 index cbb9bff9..00000000 --- a/examples/java/kora-java-s3-client-minio/src/main/java/io/koraframework/example/s3/minio/AsyncS3Client.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.koraframework.example.s3.minio; - -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import io.koraframework.s3.client.annotation.S3; -import io.koraframework.s3.client.model.*; - -@S3.Client("my") -public interface AsyncS3Client { - - @S3.Get("pre-{key}") - CompletableFuture getObject(String key); - - @S3.Get - CompletableFuture getObjectMeta(String key); - - @S3.Get - CompletableFuture> getObjects(List keys); - - @S3.Get - CompletableFuture> getObjectMetas(List keys); - - @S3.List("pre-{prefix}") - CompletionStage listObject(String prefix); - - @S3.List(value = "pre-{prefix}", limit = 50) - CompletionStage listObjectMeta(String prefix); - - @S3.Put("pre-{key}") - CompletionStage putObject(String key, S3Body object); - - @S3.Delete("pre-{key}") - CompletionStage deleteObject(String key); - - @S3.Delete - CompletionStage deleteObjects(List keys); -} diff --git a/examples/java/kora-java-s3-client-minio/src/main/java/io/koraframework/example/s3/minio/ReactorS3Client.java b/examples/java/kora-java-s3-client-minio/src/main/java/io/koraframework/example/s3/minio/ReactorS3Client.java deleted file mode 100644 index e5e39479..00000000 --- a/examples/java/kora-java-s3-client-minio/src/main/java/io/koraframework/example/s3/minio/ReactorS3Client.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.koraframework.example.s3.minio; - -import java.util.List; -import reactor.core.publisher.Mono; -import io.koraframework.s3.client.annotation.S3; -import io.koraframework.s3.client.model.*; - -@S3.Client("my") -public interface ReactorS3Client { - - @S3.Get("pre-{key}") - Mono getObject(String key); - - @S3.Get - Mono getObjectMeta(String key); - - @S3.Get - Mono> getObjects(List keys); - - @S3.Get - Mono> getObjectMetas(List keys); - - @S3.List("pre-{prefix}") - Mono listObject(String prefix); - - @S3.List(value = "pre-{prefix}", limit = 50) - Mono listObjectMeta(String prefix); - - @S3.Put("pre-{key}") - Mono putObject(String key, S3Body object); - - @S3.Delete("pre-{key}") - Mono deleteObject(String key); - - @S3.Delete - Mono deleteObjects(List keys); -} diff --git a/examples/java/kora-java-s3-client-minio/src/main/java/io/koraframework/example/s3/minio/RootService.java b/examples/java/kora-java-s3-client-minio/src/main/java/io/koraframework/example/s3/minio/RootService.java deleted file mode 100644 index 7a948ec2..00000000 --- a/examples/java/kora-java-s3-client-minio/src/main/java/io/koraframework/example/s3/minio/RootService.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.koraframework.example.s3.minio; - -import io.koraframework.common.annotation.Component; -import io.koraframework.common.annotation.Root; - -@Root -@Component -public class RootService { - - private final SyncS3Client syncS3Client; - private final AsyncS3Client asyncS3Client; - private final ReactorS3Client reactorS3Client; - - public RootService(SyncS3Client syncS3Client, AsyncS3Client asyncS3Client, ReactorS3Client reactorS3Client) { - this.syncS3Client = syncS3Client; - this.asyncS3Client = asyncS3Client; - this.reactorS3Client = reactorS3Client; - } -} diff --git a/examples/java/kora-java-s3-client-minio/src/main/java/io/koraframework/example/s3/minio/SyncS3Client.java b/examples/java/kora-java-s3-client-minio/src/main/java/io/koraframework/example/s3/minio/SyncS3Client.java deleted file mode 100644 index d454126b..00000000 --- a/examples/java/kora-java-s3-client-minio/src/main/java/io/koraframework/example/s3/minio/SyncS3Client.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.koraframework.example.s3.minio; - -import java.util.List; -import io.koraframework.s3.client.annotation.S3; -import io.koraframework.s3.client.model.*; - -@S3.Client("my") -public interface SyncS3Client { - - @S3.Get("pre-{key}") - S3Object getObject(String key); - - @S3.Get - S3ObjectMeta getObjectMeta(String key); - - @S3.Get - List getObjects(List keys); - - @S3.Get - List getObjectMetas(List keys); - - @S3.List("pre-{prefix}") - S3ObjectList listObject(String prefix); - - @S3.List(value = "pre-{prefix}", limit = 50) - S3ObjectMetaList listObjectMeta(String prefix); - - @S3.Put("pre-{key}") - S3ObjectUpload putObject(String key, S3Body object); - - @S3.Delete("pre-{key}") - void deleteObject(String key); - - @S3.Delete - void deleteObjects(List keys); -} diff --git a/examples/java/kora-java-s3-client-minio/src/main/resources/logback.xml b/examples/java/kora-java-s3-client-minio/src/main/resources/logback.xml deleted file mode 100644 index 184638b5..00000000 --- a/examples/java/kora-java-s3-client-minio/src/main/resources/logback.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/examples/java/kora-java-s3-client-minio/src/test/java/io/koraframework/example/s3/minio/AsyncS3ClientTests.java b/examples/java/kora-java-s3-client-minio/src/test/java/io/koraframework/example/s3/minio/AsyncS3ClientTests.java deleted file mode 100644 index 1663fb10..00000000 --- a/examples/java/kora-java-s3-client-minio/src/test/java/io/koraframework/example/s3/minio/AsyncS3ClientTests.java +++ /dev/null @@ -1,178 +0,0 @@ -package io.koraframework.example.s3.minio; - -import static org.junit.jupiter.api.Assertions.*; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.minio.Bucket; -import io.goodforgod.testcontainers.extensions.minio.ConnectionMinio; -import io.goodforgod.testcontainers.extensions.minio.MinioConnection; -import io.goodforgod.testcontainers.extensions.minio.TestcontainersMinio; -import io.minio.MinioClient; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.CompletionException; -import org.junit.jupiter.api.Test; -import io.koraframework.s3.client.S3NotFoundException; -import io.koraframework.s3.client.model.S3Body; -import io.koraframework.s3.client.model.S3Object; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersMinio( - mode = ContainerMode.PER_RUN, - bucket = @Bucket( - value = AsyncS3ClientTests.BUCKET, - create = Bucket.Mode.PER_METHOD, - drop = Bucket.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class AsyncS3ClientTests implements KoraAppTestConfigModifier { - - static final String BUCKET = "simple"; - - @ConnectionMinio - private MinioConnection minioConnection; - - @TestComponent - private AsyncS3Client client; - @TestComponent - private MinioClient s3Client; - - @Override - public KoraConfigModification config() { - return KoraConfigModification - .ofSystemProperty("S3_URL", minioConnection.params().uri().toString()) - .withSystemProperty("S3_ACCESS_KEY", minioConnection.params().accessKey()) - .withSystemProperty("S3_SECRET_KEY", minioConnection.params().secretKey()) - .withSystemProperty("S3_BUCKET", BUCKET); - } - - @Test - void putAndGetObject() { - // given - var key = "k1"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofBytes(value)).toCompletableFuture().join(); - - // when - var found = client.getObject(key).join(); - assertNotNull(found); - assertTrue(Arrays.equals(value, found.body().asBytes())); - - // then - var ex = assertThrows(CompletionException.class, () -> client.getObject("k2").join()); - assertInstanceOf(S3NotFoundException.class, ex.getCause()); - } - - @Test - void putAndGetMeta() { - // given - var key = "k1"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofBytes(value)).toCompletableFuture().join(); - - // when - var found = client.getObjectMeta("pre-" + key).join(); - assertNotNull(found); - - // then - var ex = assertThrows(CompletionException.class, () -> client.getObject("k2").join()); - assertInstanceOf(S3NotFoundException.class, ex.getCause()); - } - - @Test - void putAndGetManyObjects() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)).toCompletableFuture().join(); - client.putObject(key2, S3Body.ofBytes(value)).toCompletableFuture().join(); - - // when - var found = client.getObjects(List.of("pre-" + key1, "pre-" + key2)).join(); - assertEquals(2, found.size()); - for (S3Object object : found) { - assertEquals("value", new String(object.body().asBytes(), StandardCharsets.UTF_8)); - } - } - - @Test - void putAndGetManyMetas() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)).toCompletableFuture().join(); - client.putObject(key2, S3Body.ofBytes(value)).toCompletableFuture().join(); - - // when - var found = client.getObjectMetas(List.of("pre-" + key1, "pre-" + key2)).join(); - assertEquals(2, found.size()); - } - - @Test - void putAndListObjects() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)).toCompletableFuture().join(); - client.putObject(key2, S3Body.ofBytes(value)).toCompletableFuture().join(); - - // when - var found = client.listObject("k").toCompletableFuture().join(); - assertEquals(2, found.metas().size()); - for (S3Object object : found.objects()) { - assertEquals("value", new String(object.body().asBytes(), StandardCharsets.UTF_8)); - } - } - - @Test - void putAndListMetas() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)).toCompletableFuture().join(); - client.putObject(key2, S3Body.ofBytes(value)).toCompletableFuture().join(); - - // when - var found = client.listObjectMeta("k").toCompletableFuture().join(); - assertEquals(2, found.metas().size()); - } - - @Test - void putAndDelete() { - // given - var key = "k1"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofBytes(value)).toCompletableFuture().join(); - - // when - client.deleteObject(key).toCompletableFuture().join(); - - // then - var ex = assertThrows(CompletionException.class, () -> client.getObject("k1").join()); - assertInstanceOf(S3NotFoundException.class, ex.getCause()); - } - - @Test - void putAndDeleteMany() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)).toCompletableFuture().join(); - client.putObject(key2, S3Body.ofBytes(value)).toCompletableFuture().join(); - - // when - client.deleteObjects(List.of("pre-k1", "pre-k2")).toCompletableFuture().join(); - var ex1 = assertThrows(CompletionException.class, () -> client.getObject("k1").join()); - assertInstanceOf(S3NotFoundException.class, ex1.getCause()); - var ex2 = assertThrows(CompletionException.class, () -> client.getObject("k2").join()); - assertInstanceOf(S3NotFoundException.class, ex2.getCause()); - } -} diff --git a/examples/java/kora-java-s3-client-minio/src/test/java/io/koraframework/example/s3/minio/ReactorS3ClientTests.java b/examples/java/kora-java-s3-client-minio/src/test/java/io/koraframework/example/s3/minio/ReactorS3ClientTests.java deleted file mode 100644 index 390e0c3a..00000000 --- a/examples/java/kora-java-s3-client-minio/src/test/java/io/koraframework/example/s3/minio/ReactorS3ClientTests.java +++ /dev/null @@ -1,177 +0,0 @@ -package io.koraframework.example.s3.minio; - -import static org.junit.jupiter.api.Assertions.*; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.minio.Bucket; -import io.goodforgod.testcontainers.extensions.minio.ConnectionMinio; -import io.goodforgod.testcontainers.extensions.minio.MinioConnection; -import io.goodforgod.testcontainers.extensions.minio.TestcontainersMinio; -import io.minio.MinioClient; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import org.junit.jupiter.api.Test; -import io.koraframework.s3.client.model.S3Body; -import io.koraframework.s3.client.model.S3Object; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersMinio( - mode = ContainerMode.PER_RUN, - bucket = @Bucket( - value = ReactorS3ClientTests.BUCKET, - create = Bucket.Mode.PER_METHOD, - drop = Bucket.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class ReactorS3ClientTests implements KoraAppTestConfigModifier { - - static final String BUCKET = "simple"; - - @ConnectionMinio - private MinioConnection minioConnection; - - @TestComponent - private ReactorS3Client client; - @TestComponent - private MinioClient s3Client; - - @Override - public KoraConfigModification config() { - return KoraConfigModification - .ofSystemProperty("S3_URL", minioConnection.params().uri().toString()) - .withSystemProperty("S3_ACCESS_KEY", minioConnection.params().accessKey()) - .withSystemProperty("S3_SECRET_KEY", minioConnection.params().secretKey()) - .withSystemProperty("S3_BUCKET", BUCKET); - } - - @Test - void putAndGetObject() { - // given - var key = "k1"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofBytes(value)).block(); - - // when - var found = client.getObject(key).block(); - assertNotNull(found); - assertTrue(Arrays.equals(value, found.body().asBytes())); - - // then - Optional k2 = client.getObject("k2").blockOptional(); - assertTrue(k2.isEmpty()); - } - - @Test - void putAndGetMeta() { - // given - var key = "k1"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofBytes(value)).block(); - - // when - var found = client.getObjectMeta("pre-" + key).block(); - assertNotNull(found); - - // then - Optional k2 = client.getObject("k2").blockOptional(); - assertTrue(k2.isEmpty()); - } - - @Test - void putAndGetManyObjects() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)).block(); - client.putObject(key2, S3Body.ofBytes(value)).block(); - - // when - var found = client.getObjects(List.of("pre-" + key1, "pre-" + key2)).block(); - assertEquals(2, found.size()); - for (S3Object object : found) { - assertEquals("value", new String(object.body().asBytes(), StandardCharsets.UTF_8)); - } - } - - @Test - void putAndGetManyMetas() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)).block(); - client.putObject(key2, S3Body.ofBytes(value)).block(); - - // when - var found = client.getObjectMetas(List.of("pre-" + key1, "pre-" + key2)).block(); - assertEquals(2, found.size()); - } - - @Test - void putAndListObjects() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)).block(); - client.putObject(key2, S3Body.ofBytes(value)).block(); - - // when - var found = client.listObject("k").block(); - assertEquals(2, found.metas().size()); - for (S3Object object : found.objects()) { - assertEquals("value", new String(object.body().asBytes(), StandardCharsets.UTF_8)); - } - } - - @Test - void putAndListMetas() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)).block(); - client.putObject(key2, S3Body.ofBytes(value)).block(); - - // when - var found = client.listObjectMeta("k").block(); - assertEquals(2, found.metas().size()); - } - - @Test - void putAndDelete() { - // given - var key = "k1"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofBytes(value)).block(); - - // when - client.deleteObject(key).block(); - - // then - Optional k1 = client.getObject("k1").blockOptional(); - assertTrue(k1.isEmpty()); - } - - @Test - void putAndDeleteMany() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)).block(); - client.putObject(key2, S3Body.ofBytes(value)).block(); - - // when - client.deleteObjects(List.of("pre-k1", "pre-k2")).block(); - Optional k1 = client.getObject("k1").blockOptional(); - assertTrue(k1.isEmpty()); - Optional k2 = client.getObject("k2").blockOptional(); - assertTrue(k2.isEmpty()); - } -} diff --git a/examples/java/kora-java-s3-client-minio/src/test/java/io/koraframework/example/s3/minio/SyncS3ClientTests.java b/examples/java/kora-java-s3-client-minio/src/test/java/io/koraframework/example/s3/minio/SyncS3ClientTests.java deleted file mode 100644 index adf60752..00000000 --- a/examples/java/kora-java-s3-client-minio/src/test/java/io/koraframework/example/s3/minio/SyncS3ClientTests.java +++ /dev/null @@ -1,172 +0,0 @@ -package io.koraframework.example.s3.minio; - -import static org.junit.jupiter.api.Assertions.*; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.minio.Bucket; -import io.goodforgod.testcontainers.extensions.minio.ConnectionMinio; -import io.goodforgod.testcontainers.extensions.minio.MinioConnection; -import io.goodforgod.testcontainers.extensions.minio.TestcontainersMinio; -import io.minio.MinioClient; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.List; -import org.junit.jupiter.api.Test; -import io.koraframework.s3.client.S3NotFoundException; -import io.koraframework.s3.client.model.S3Body; -import io.koraframework.s3.client.model.S3Object; -import io.koraframework.test.extension.junit5.KoraAppTest; -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; -import io.koraframework.test.extension.junit5.KoraConfigModification; -import io.koraframework.test.extension.junit5.TestComponent; - -@TestcontainersMinio( - mode = ContainerMode.PER_RUN, - bucket = @Bucket( - value = SyncS3ClientTests.BUCKET, - create = Bucket.Mode.PER_METHOD, - drop = Bucket.Mode.PER_METHOD)) -@KoraAppTest(Application.class) -class SyncS3ClientTests implements KoraAppTestConfigModifier { - - static final String BUCKET = "simple"; - - @ConnectionMinio - private MinioConnection minioConnection; - - @TestComponent - private SyncS3Client client; - @TestComponent - private MinioClient s3Client; - - @Override - public KoraConfigModification config() { - return KoraConfigModification - .ofSystemProperty("S3_URL", minioConnection.params().uri().toString()) - .withSystemProperty("S3_ACCESS_KEY", minioConnection.params().accessKey()) - .withSystemProperty("S3_SECRET_KEY", minioConnection.params().secretKey()) - .withSystemProperty("S3_BUCKET", BUCKET); - } - - @Test - void putAndGetObject() { - // given - var key = "k1"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofBytes(value)); - - // when - var found = client.getObject(key); - assertNotNull(found); - assertTrue(Arrays.equals(value, found.body().asBytes())); - - // then - assertThrows(S3NotFoundException.class, () -> client.getObject("k2")); - } - - @Test - void putAndGetMeta() { - // given - var key = "k1"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofBytes(value)); - - // when - var found = client.getObjectMeta("pre-" + key); - assertNotNull(found); - - // then - assertThrows(S3NotFoundException.class, () -> client.getObjectMeta("k2")); - } - - @Test - void putAndGetManyObjects() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)); - client.putObject(key2, S3Body.ofBytes(value)); - - // when - var found = client.getObjects(List.of("pre-" + key1, "pre-" + key2)); - assertEquals(2, found.size()); - for (S3Object object : found) { - assertEquals("value", new String(object.body().asBytes(), StandardCharsets.UTF_8)); - } - } - - @Test - void putAndGetManyMetas() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)); - client.putObject(key2, S3Body.ofBytes(value)); - - // when - var found = client.getObjectMetas(List.of("pre-" + key1, "pre-" + key2)); - assertEquals(2, found.size()); - } - - @Test - void putAndListObjects() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)); - client.putObject(key2, S3Body.ofBytes(value)); - - // when - var found = client.listObject("k"); - assertEquals(2, found.metas().size()); - for (S3Object object : found.objects()) { - assertEquals("value", new String(object.body().asBytes(), StandardCharsets.UTF_8)); - } - } - - @Test - void putAndListMetas() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)); - client.putObject(key2, S3Body.ofBytes(value)); - - // when - var found = client.listObjectMeta("k"); - assertEquals(2, found.metas().size()); - } - - @Test - void putAndDelete() { - // given - var key = "k1"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key, S3Body.ofBytes(value)); - - // when - client.deleteObject(key); - - // then - assertThrows(S3NotFoundException.class, () -> client.getObject("k1")); - } - - @Test - void putAndDeleteMany() { - // given - var key1 = "k1"; - var key2 = "k2"; - var value = "value".getBytes(StandardCharsets.UTF_8); - client.putObject(key1, S3Body.ofBytes(value)); - client.putObject(key2, S3Body.ofBytes(value)); - - // when - client.deleteObjects(List.of("pre-k1", "pre-k2")); - assertThrows(S3NotFoundException.class, () -> client.getObject("k1")); - assertThrows(S3NotFoundException.class, () -> client.getObject("k2")); - } -} diff --git a/examples/java/kora-java-scheduling-jdk/build.gradle b/examples/java/kora-java-scheduling-jdk/build.gradle index 7c29023d..f9cb9707 100644 --- a/examples/java/kora-java-scheduling-jdk/build.gradle +++ b/examples/java/kora-java-scheduling-jdk/build.gradle @@ -18,7 +18,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:scheduling-jdk" @@ -38,9 +38,6 @@ application { //noinspection GroovyAssignabilityCheck run { - environment([ - "": "", - ]) } distTar { @@ -55,10 +52,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/java/kora-java-scheduling-quartz/build.gradle b/examples/java/kora-java-scheduling-quartz/build.gradle index 6d943bb9..04c32d1e 100644 --- a/examples/java/kora-java-scheduling-quartz/build.gradle +++ b/examples/java/kora-java-scheduling-quartz/build.gradle @@ -18,7 +18,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:scheduling-quartz" @@ -38,9 +38,6 @@ application { //noinspection GroovyAssignabilityCheck run { - environment([ - "": "", - ]) } distTar { @@ -55,10 +52,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/java/kora-java-soap-client/build.gradle b/examples/java/kora-java-soap-client/build.gradle index 35e80a26..444b113b 100644 --- a/examples/java/kora-java-soap-client/build.gradle +++ b/examples/java/kora-java-soap-client/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:json-common" @@ -38,7 +38,7 @@ dependencies { implementation "io.koraframework:config-hocon" testImplementation "io.koraframework:test-junit5" - testImplementation "io.goodforgod:testcontainers-extensions-mockserver:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-mockserver:0.15.0" } application { @@ -66,10 +66,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/java/kora-java-telemetry/build.gradle b/examples/java/kora-java-telemetry/build.gradle index 64b61adf..21161b3d 100644 --- a/examples/java/kora-java-telemetry/build.gradle +++ b/examples/java/kora-java-telemetry/build.gradle @@ -18,7 +18,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:micrometer-module" @@ -50,10 +50,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/java/kora-java-telemetry/src/main/resources/application.conf b/examples/java/kora-java-telemetry/src/main/resources/application.conf index f1684316..acbd43cc 100644 --- a/examples/java/kora-java-telemetry/src/main/resources/application.conf +++ b/examples/java/kora-java-telemetry/src/main/resources/application.conf @@ -1,8 +1,8 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 - privateApiHttpLivenessPath = "/liveness" - privateApiHttpReadinessPath = "/readiness" + port = 8080 + system.port = 8085 + system.livenessPath = "/liveness" + system.readinessPath = "/readiness" telemetry.logging.enabled = true } diff --git a/examples/java/kora-java-validation/build.gradle b/examples/java/kora-java-validation/build.gradle index 2990c863..9f598d7d 100644 --- a/examples/java/kora-java-validation/build.gradle +++ b/examples/java/kora-java-validation/build.gradle @@ -18,7 +18,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" implementation "io.koraframework:validation-common" @@ -47,10 +47,6 @@ test { "-XX:TieredStopAtLevel=1", ] - environment([ - "": "" - ]) - useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/examples/kotlin/kora-kotlin-cache-caffeine/build.gradle.kts b/examples/kotlin/kora-kotlin-cache-caffeine/build.gradle.kts index 552455ab..bc4417df 100644 --- a/examples/kotlin/kora-kotlin-cache-caffeine/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-cache-caffeine/build.gradle.kts @@ -7,16 +7,9 @@ plugins { id("com.google.devtools.ksp") version ("2.3.11") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:cache-caffeine") implementation("io.koraframework:config-hocon") implementation("io.koraframework:logging-logback") diff --git a/examples/kotlin/kora-kotlin-cache-redis/build.gradle.kts b/examples/kotlin/kora-kotlin-cache-redis/build.gradle.kts index 8ac9f154..24db604e 100644 --- a/examples/kotlin/kora-kotlin-cache-redis/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-cache-redis/build.gradle.kts @@ -7,22 +7,14 @@ plugins { id("com.google.devtools.ksp") version ("2.3.11") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:cache-redis-lettuce") implementation("io.koraframework:config-hocon") implementation("io.koraframework:logging-logback") testImplementation("io.koraframework:test-junit5") - testImplementation("io.goodforgod:testcontainers-extensions-redis:0.13.1") + testImplementation("io.goodforgod:testcontainers-extensions-redis:0.15.0") testImplementation("redis.clients:jedis:4.4.3") } diff --git a/examples/kotlin/kora-kotlin-camunda-engine/build.gradle.kts b/examples/kotlin/kora-kotlin-camunda-engine/build.gradle.kts index 2b147344..999c2cfa 100644 --- a/examples/kotlin/kora-kotlin-camunda-engine/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-camunda-engine/build.gradle.kts @@ -7,17 +7,9 @@ plugins { id("com.google.devtools.ksp") version ("2.3.11") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework.experimental:camunda-engine-bpmn") @@ -28,9 +20,11 @@ dependencies { implementation("io.koraframework:logging-logback") implementation("io.koraframework:config-hocon") + // mockito-kotlin 5.4.0 pins an older mockito-core whose Byte Buddy rejects Java 25 class files + testImplementation("org.mockito:mockito-core:5.18.0") testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0") testImplementation("io.koraframework:test-junit5") - testImplementation("io.goodforgod:testcontainers-extensions-postgres:0.13.1") + testImplementation("io.goodforgod:testcontainers-extensions-postgres:0.15.0") testRuntimeOnly("com.h2database:h2:2.2.224") testImplementation("org.camunda.bpm:camunda-bpm-assert:7.21.0") testImplementation("org.assertj:assertj-core:3.26.0") diff --git a/examples/kotlin/kora-kotlin-camunda-engine/src/main/resources/application.conf b/examples/kotlin/kora-kotlin-camunda-engine/src/main/resources/application.conf index f8f963b1..71610687 100644 --- a/examples/kotlin/kora-kotlin-camunda-engine/src/main/resources/application.conf +++ b/examples/kotlin/kora-kotlin-camunda-engine/src/main/resources/application.conf @@ -1,4 +1,4 @@ -db { +jdbc { jdbcUrl = ${POSTGRES_JDBC_URL} username = ${POSTGRES_USER} password = ${POSTGRES_PASS} diff --git a/examples/kotlin/kora-kotlin-camunda-zeebe-worker/build.gradle.kts b/examples/kotlin/kora-kotlin-camunda-zeebe-worker/build.gradle.kts index 58230a8f..b408aa33 100644 --- a/examples/kotlin/kora-kotlin-camunda-zeebe-worker/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-camunda-zeebe-worker/build.gradle.kts @@ -7,17 +7,9 @@ plugins { id("com.google.devtools.ksp") version ("2.3.11") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework.experimental:camunda-zeebe-worker") implementation("io.koraframework:scheduling-jdk") @@ -25,6 +17,8 @@ dependencies { implementation("io.koraframework:config-hocon") implementation("io.koraframework:json-common") + // mockito-kotlin 5.4.0 pins an older mockito-core whose Byte Buddy rejects Java 25 class files + testImplementation("org.mockito:mockito-core:5.18.0") testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0") testImplementation("io.koraframework:test-junit5") testImplementation("io.camunda:zeebe-process-test-extension-testcontainer:8.8.24") diff --git a/examples/kotlin/kora-kotlin-camunda-zeebe-worker/src/main/kotlin/io/koraframework/kotlin/example/camunda/zeebe/ProcessScheduler.kt b/examples/kotlin/kora-kotlin-camunda-zeebe-worker/src/main/kotlin/io/koraframework/kotlin/example/camunda/zeebe/ProcessScheduler.kt index bbd69de1..cff1d1d8 100644 --- a/examples/kotlin/kora-kotlin-camunda-zeebe-worker/src/main/kotlin/io/koraframework/kotlin/example/camunda/zeebe/ProcessScheduler.kt +++ b/examples/kotlin/kora-kotlin-camunda-zeebe-worker/src/main/kotlin/io/koraframework/kotlin/example/camunda/zeebe/ProcessScheduler.kt @@ -1,6 +1,6 @@ -package io.koraframework.kotlin.example.camunda.zeebe +package io.koraframework.kotlin.example.camunda.zeebe -import io.camunda.zeebe.client.ZeebeClient +import io.camunda.client.CamundaClient import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component import io.koraframework.scheduling.jdk.annotation.ScheduleAtFixedRate @@ -8,7 +8,7 @@ import java.util.Date import java.util.UUID @Component -class ProcessScheduler(private val client: ZeebeClient) { +class ProcessScheduler(private val client: CamundaClient) { private val logger = LoggerFactory.getLogger(ProcessScheduler::class.java) @ScheduleAtFixedRate(period = 5000L, initialDelay = 500L) @@ -34,4 +34,3 @@ class ProcessScheduler(private val client: ZeebeClient) { ) } } - diff --git a/examples/kotlin/kora-kotlin-camunda-zeebe-worker/src/main/kotlin/io/koraframework/kotlin/example/camunda/zeebe/Step3JobWorker.kt b/examples/kotlin/kora-kotlin-camunda-zeebe-worker/src/main/kotlin/io/koraframework/kotlin/example/camunda/zeebe/Step3JobWorker.kt index 3ff72d5f..4b5f25fb 100644 --- a/examples/kotlin/kora-kotlin-camunda-zeebe-worker/src/main/kotlin/io/koraframework/kotlin/example/camunda/zeebe/Step3JobWorker.kt +++ b/examples/kotlin/kora-kotlin-camunda-zeebe-worker/src/main/kotlin/io/koraframework/kotlin/example/camunda/zeebe/Step3JobWorker.kt @@ -2,7 +2,7 @@ package io.koraframework.kotlin.example.camunda.zeebe import org.slf4j.LoggerFactory import io.koraframework.camunda.zeebe.worker.JobContext -import io.koraframework.camunda.zeebe.worker.JobWorkerException +import io.koraframework.camunda.zeebe.worker.exception.JobWorkerException import io.koraframework.camunda.zeebe.worker.annotation.JobVariable import io.koraframework.camunda.zeebe.worker.annotation.JobWorker import io.koraframework.common.annotation.Component diff --git a/examples/kotlin/kora-kotlin-camunda-zeebe-worker/src/main/kotlin/io/koraframework/kotlin/example/camunda/zeebe/WorkerUtils.kt b/examples/kotlin/kora-kotlin-camunda-zeebe-worker/src/main/kotlin/io/koraframework/kotlin/example/camunda/zeebe/WorkerUtils.kt index 857e19b8..ac44a587 100644 --- a/examples/kotlin/kora-kotlin-camunda-zeebe-worker/src/main/kotlin/io/koraframework/kotlin/example/camunda/zeebe/WorkerUtils.kt +++ b/examples/kotlin/kora-kotlin-camunda-zeebe-worker/src/main/kotlin/io/koraframework/kotlin/example/camunda/zeebe/WorkerUtils.kt @@ -1,28 +1,9 @@ package io.koraframework.kotlin.example.camunda.zeebe -import io.camunda.zeebe.client.api.response.ActivatedJob import org.slf4j.Logger import io.koraframework.camunda.zeebe.worker.JobContext -import java.time.Instant object WorkerUtils { - fun logJob(logger: Logger, job: ActivatedJob) { - logger.info( - """ - Complete Job - {} - [processKey: {}] - [elementId: {}] - [deadline: {}] - [variables: {}] - """.trimIndent(), - job.type, - job.processInstanceKey, - job.elementId, - Instant.ofEpochMilli(job.deadline), - job.variables, - ) - } - fun logJob(logger: Logger, job: JobContext) { logger.info( """ diff --git a/examples/kotlin/kora-kotlin-camunda-zeebe-worker/src/main/resources/application.conf b/examples/kotlin/kora-kotlin-camunda-zeebe-worker/src/main/resources/application.conf index d5006366..e5f99aca 100644 --- a/examples/kotlin/kora-kotlin-camunda-zeebe-worker/src/main/resources/application.conf +++ b/examples/kotlin/kora-kotlin-camunda-zeebe-worker/src/main/resources/application.conf @@ -1,6 +1,7 @@ zeebe.client { initializationFailTimeout = "10s" grpc.url = ${ZEEBE_GRPC_URL} + rest.url = ${ZEEBE_REST_URL} deployment.resources = "classpath:bpm" telemetry.logging.enabled = true diff --git a/examples/kotlin/kora-kotlin-camunda-zeebe-worker/src/test/kotlin/io/koraframework/kotlin/example/bpmn/camunda8/ZeebeMockedTests.kt b/examples/kotlin/kora-kotlin-camunda-zeebe-worker/src/test/kotlin/io/koraframework/kotlin/example/bpmn/camunda8/ZeebeMockedTests.kt index 594b4d07..ab3f85f3 100644 --- a/examples/kotlin/kora-kotlin-camunda-zeebe-worker/src/test/kotlin/io/koraframework/kotlin/example/bpmn/camunda8/ZeebeMockedTests.kt +++ b/examples/kotlin/kora-kotlin-camunda-zeebe-worker/src/test/kotlin/io/koraframework/kotlin/example/bpmn/camunda8/ZeebeMockedTests.kt @@ -12,7 +12,6 @@ import io.koraframework.kotlin.example.camunda.zeebe.Application import io.koraframework.test.extension.junit5.KoraAppTest import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier import io.koraframework.test.extension.junit5.KoraConfigModification -import io.koraframework.test.extension.junit5.TestComponent import java.time.Duration import java.util.Date import java.util.UUID @@ -21,14 +20,19 @@ import java.util.UUID @KoraAppTest(value = Application::class, components = [KoraZeebeJobWorkerEngine::class]) class ZeebeMockedTests : KoraAppTestConfigModifier { + // Is injected by ZeebeProcessTest. zeebe-process-test still exposes the legacy ZeebeClient + // and its assertions accept only its response types; the application itself runs on CamundaClient, + // so this must not be declared as a @TestComponent — it is not a component of the graph. @Spy - @TestComponent lateinit var client: ZeebeClient + // Is injected by ZeebeProcessTest lateinit var engine: ZeebeTestEngine override fun config(): KoraConfigModification = - KoraConfigModification.ofSystemProperty("ZEEBE_GRPC_URL", client.configuration.grpcAddress.toString()) + KoraConfigModification + .ofSystemProperty("ZEEBE_GRPC_URL", client.configuration.grpcAddress.toString()) + .withSystemProperty("ZEEBE_REST_URL", client.configuration.restAddress.toString()) @Test fun processDemoSuccess() { diff --git a/examples/kotlin/kora-kotlin-config-hocon/build.gradle.kts b/examples/kotlin/kora-kotlin-config-hocon/build.gradle.kts index 926ac733..b46f2842 100644 --- a/examples/kotlin/kora-kotlin-config-hocon/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-config-hocon/build.gradle.kts @@ -7,17 +7,9 @@ plugins { id("com.google.devtools.ksp") version ("2.3.11") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:logging-logback") testImplementation("io.koraframework:test-junit5") diff --git a/examples/kotlin/kora-kotlin-config-yaml/build.gradle.kts b/examples/kotlin/kora-kotlin-config-yaml/build.gradle.kts index 3b1845aa..75f7f3ed 100644 --- a/examples/kotlin/kora-kotlin-config-yaml/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-config-yaml/build.gradle.kts @@ -7,17 +7,9 @@ plugins { id("com.google.devtools.ksp") version ("2.3.11") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-yaml") implementation("io.koraframework:logging-logback") testImplementation("io.koraframework:test-junit5") diff --git a/examples/kotlin/kora-kotlin-crud-submodule/build.gradle.kts b/examples/kotlin/kora-kotlin-crud-submodule/build.gradle.kts index 00f34094..3eadbda6 100644 --- a/examples/kotlin/kora-kotlin-crud-submodule/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-crud-submodule/build.gradle.kts @@ -22,20 +22,15 @@ subprojects { } } - val koraBom: Configuration by configurations.creating - configurations { - listOf("ksp", "kspTest", "compileOnly", "api", "implementation", "testImplementation").forEach { name -> - named(name) { extendsFrom(koraBom) } - } - } - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - add("ksp", "io.koraframework:symbol-processors") + add("implementation", platform("io.koraframework:kora-bom:${property("koraVersion")}")) + add("ksp", "io.koraframework:symbol-processors:${property("koraVersion")}") add("testImplementation", "org.json:json:20231013") add("testImplementation", "org.skyscreamer:jsonassert:1.5.1") add("testImplementation", "io.koraframework:test-junit5") + // mockito-kotlin 5.4.0 pins an older mockito-core whose Byte Buddy rejects Java 25 class files + add("testImplementation", "org.mockito:mockito-core:5.18.0") add("testImplementation", "org.mockito.kotlin:mockito-kotlin:5.4.0") } diff --git a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/build.gradle.kts b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/build.gradle.kts index 66039baf..b54b93be 100644 --- a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/build.gradle.kts @@ -17,6 +17,7 @@ plugins { } dependencies { + kspTest("io.koraframework:symbol-processors:${property("koraVersion")}") implementation(project(":examples:kotlin:kora-kotlin-crud-submodule:kora-kotlin-crud-submodule-pet-api")) implementation(project(":examples:kotlin:kora-kotlin-crud-submodule:kora-kotlin-crud-submodule-vet-api")) implementation("io.koraframework:http-server-undertow") @@ -28,10 +29,8 @@ dependencies { implementation("io.koraframework:openapi-management") implementation("org.postgresql:postgresql:42.7.7") - kspTest("io.koraframework:symbol-processors") - testRuntimeOnly(project(":examples:kotlin:kora-kotlin-crud-submodule:kora-kotlin-crud-submodule-common")) - testImplementation("io.goodforgod:testcontainers-extensions-postgres:0.13.1") + testImplementation("io.goodforgod:testcontainers-extensions-postgres:0.15.0") testImplementation("org.testcontainers:junit-jupiter:1.21.4") } diff --git a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/src/main/kotlin/io/koraframework/kotlin/example/submodule/app/controller/HttpExceptionHandler.kt b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/src/main/kotlin/io/koraframework/kotlin/example/submodule/app/controller/HttpExceptionHandler.kt index 7d68d8a8..3eea66a0 100644 --- a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/src/main/kotlin/io/koraframework/kotlin/example/submodule/app/controller/HttpExceptionHandler.kt +++ b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/src/main/kotlin/io/koraframework/kotlin/example/submodule/app/controller/HttpExceptionHandler.kt @@ -3,30 +3,31 @@ package io.koraframework.kotlin.example.submodule.app.controller import io.micrometer.core.instrument.config.validate.ValidationException import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component -import io.koraframework.common.Context import io.koraframework.common.annotation.Tag import io.koraframework.http.common.body.HttpBody -import io.koraframework.http.server.common.* +import io.koraframework.http.server.common.HttpServer +import io.koraframework.http.server.common.interceptor.HttpServerInterceptor +import io.koraframework.http.server.common.request.HttpServerRequest +import io.koraframework.http.server.common.response.HttpServerResponse +import io.koraframework.http.server.common.response.HttpServerResponseException import io.koraframework.json.common.JsonWriter import io.koraframework.kotlin.example.submodule.openapi.http.server.model.MessageTO -import java.util.concurrent.CompletionStage import java.util.concurrent.TimeoutException -@Tag(HttpServerModule::class) +@Tag(HttpServer::class) @Component class HttpExceptionHandler(private val errorJsonWriter: JsonWriter) : HttpServerInterceptor { - override fun intercept( - context: Context, - request: HttpServerRequest, - chain: HttpServerInterceptor.InterceptChain, - ): CompletionStage { - return chain.process(context, request).exceptionally { e -> + + override fun intercept(request: HttpServerRequest, chain: HttpServerInterceptor.InterceptChain): HttpServerResponse { + try { + return chain.process(request) + } catch (e: Exception) { if (e is HttpServerResponseException) { - return@exceptionally e + return e } - val body = HttpBody.json(errorJsonWriter.toByteArrayUnchecked(MessageTO(e.message))) - when (e) { + val body = HttpBody.json(errorJsonWriter.toByteArray(MessageTO(e.message))) + return when (e) { is ValidationException, is IllegalArgumentException -> HttpServerResponse.of(400, body) is TimeoutException -> HttpServerResponse.of(408, body) else -> { diff --git a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/src/main/kotlin/io/koraframework/kotlin/example/submodule/app/controller/VetDelegate.kt b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/src/main/kotlin/io/koraframework/kotlin/example/submodule/app/controller/VetDelegate.kt index 4e08dfb5..4f24698e 100644 --- a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/src/main/kotlin/io/koraframework/kotlin/example/submodule/app/controller/VetDelegate.kt +++ b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/src/main/kotlin/io/koraframework/kotlin/example/submodule/app/controller/VetDelegate.kt @@ -6,7 +6,6 @@ import io.koraframework.kotlin.example.submodule.openapi.http.server.api.VetApiD import io.koraframework.kotlin.example.submodule.openapi.http.server.api.VetApiResponses import io.koraframework.kotlin.example.submodule.openapi.http.server.model.MessageTO import io.koraframework.kotlin.example.submodule.openapi.http.server.model.VetCreateTO -import io.koraframework.kotlin.example.submodule.openapi.http.server.model.VetUpdateTO import io.koraframework.kotlin.example.submodule.vet.service.VetService @Component @@ -37,7 +36,7 @@ class VetDelegate( return VetApiResponses.AddVetApiResponse.AddVet200ApiResponse(vetMapper.asDTO(vet)) } - override fun updateVet(vetId: Long, vetUpdateTO: VetUpdateTO): VetApiResponses.UpdateVetApiResponse { + override fun updateVet(vetId: Long, vetUpdateTO: VetCreateTO): VetApiResponses.UpdateVetApiResponse { if (vetId < 0) { return VetApiResponses.UpdateVetApiResponse.UpdateVet400ApiResponse(malformedId(vetId)) } diff --git a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/src/main/kotlin/io/koraframework/kotlin/example/submodule/app/model/PetMapper.kt b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/src/main/kotlin/io/koraframework/kotlin/example/submodule/app/model/PetMapper.kt index 7d08fbb5..ff238a34 100644 --- a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/src/main/kotlin/io/koraframework/kotlin/example/submodule/app/model/PetMapper.kt +++ b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/src/main/kotlin/io/koraframework/kotlin/example/submodule/app/model/PetMapper.kt @@ -1,4 +1,4 @@ -package io.koraframework.kotlin.example.submodule.app.model +package io.koraframework.kotlin.example.submodule.app.model import io.koraframework.common.annotation.Component import io.koraframework.kotlin.example.submodule.openapi.http.server.model.CategoryTO @@ -20,8 +20,9 @@ class ManualPetMapper : PetMapper { Pet.Status.PENDING -> PetTO.StatusEnum.PENDING Pet.Status.SOLD -> PetTO.StatusEnum.SOLD } - return PetTO(pet.id, pet.name, status, asDTO(pet.category)) + // the 2.0 generator orders the constructor by optionality, so the arguments are named + return PetTO(status = status, id = pet.id, name = pet.name, category = asDTO(pet.category)) } - override fun asDTO(category: PetCategory): CategoryTO = CategoryTO(category.id, category.name) + override fun asDTO(category: PetCategory): CategoryTO = CategoryTO(id = category.id, name = category.name) } diff --git a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/src/main/resources/application.conf b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/src/main/resources/application.conf index c05167a9..c0babf18 100644 --- a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/src/main/resources/application.conf +++ b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/src/main/resources/application.conf @@ -1,11 +1,11 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } -db { +jdbc { jdbcUrl = ${POSTGRES_JDBC_URL} username = ${POSTGRES_USER} password = ${POSTGRES_PASS} @@ -19,11 +19,11 @@ db { openapi { management { enabled = true - file = "openapi/http-server.yaml" + files = "openapi/http-server.yaml" swaggerui { enabled = true } - rapidoc { + scalar { enabled = true } } diff --git a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/src/test/kotlin/io/koraframework/kotlin/example/submodule/app/IntegrationTests.kt b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/src/test/kotlin/io/koraframework/kotlin/example/submodule/app/IntegrationTests.kt index 980ae7c0..ee57e50e 100644 --- a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/src/test/kotlin/io/koraframework/kotlin/example/submodule/app/IntegrationTests.kt +++ b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app/src/test/kotlin/io/koraframework/kotlin/example/submodule/app/IntegrationTests.kt @@ -43,7 +43,7 @@ class IntegrationTests : KoraAppTestConfigModifier { override fun config(): KoraConfigModification { return KoraConfigModification.ofString( """ - db { + jdbc { jdbcUrl = ${'$'}{POSTGRES_JDBC_URL} username = ${'$'}{POSTGRES_USER} password = ${'$'}{POSTGRES_PASS} @@ -52,7 +52,8 @@ class IntegrationTests : KoraAppTestConfigModifier { pet-cache.maximumSize = 0 resilient { circuitbreaker.pet { - slidingWindowSize = 2 + type = FIXED_WINDOW + countBased.windowSize = 2 minimumRequiredCalls = 2 failureRateThreshold = 100 permittedCallsInHalfOpenState = 1 diff --git a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/build.gradle.kts b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/build.gradle.kts index 49e2e970..beeedb4d 100644 --- a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/build.gradle.kts @@ -3,11 +3,12 @@ plugins { } dependencies { + kspTest("io.koraframework:symbol-processors:${property("koraVersion")}") api(project(":examples:kotlin:kora-kotlin-crud-submodule:kora-kotlin-crud-submodule-common")) api("io.koraframework:database-jdbc") api("io.koraframework:cache-caffeine") api("io.koraframework:resilient-kora") testImplementation("io.koraframework:config-hocon") - testImplementation("io.goodforgod:testcontainers-extensions-postgres:0.13.1") + testImplementation("io.goodforgod:testcontainers-extensions-postgres:0.15.0") } diff --git a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/pet/service/PetCircuitBreaker.kt b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/pet/service/PetCircuitBreaker.kt new file mode 100644 index 00000000..20781778 --- /dev/null +++ b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/pet/service/PetCircuitBreaker.kt @@ -0,0 +1,7 @@ +package io.koraframework.kotlin.example.submodule.pet.service + +import io.koraframework.resilient.circuitbreaker.CircuitBreaker +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakerSpec + +@CircuitBreakerSpec("resilient.circuitbreaker.pet") +interface PetCircuitBreaker : CircuitBreaker diff --git a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/pet/service/PetRetry.kt b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/pet/service/PetRetry.kt new file mode 100644 index 00000000..01654891 --- /dev/null +++ b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/pet/service/PetRetry.kt @@ -0,0 +1,7 @@ +package io.koraframework.kotlin.example.submodule.pet.service + +import io.koraframework.resilient.retry.Retry +import io.koraframework.resilient.retry.annotation.RetrySpec + +@RetrySpec("resilient.retry.pet") +interface PetRetry : Retry diff --git a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/pet/service/PetService.kt b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/pet/service/PetService.kt index edbfa418..9ed11338 100644 --- a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/pet/service/PetService.kt +++ b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/pet/service/PetService.kt @@ -9,8 +9,8 @@ import io.koraframework.kotlin.example.submodule.pet.model.dao.PetCategory import io.koraframework.kotlin.example.submodule.pet.model.dao.PetWithCategory import io.koraframework.kotlin.example.submodule.pet.repository.CategoryRepository import io.koraframework.kotlin.example.submodule.pet.repository.PetRepository -import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreaker -import io.koraframework.resilient.retry.annotation.Retry +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakable +import io.koraframework.resilient.retry.annotation.Retryable import io.koraframework.resilient.timeout.annotation.Timeout @Component @@ -19,13 +19,13 @@ open class PetService( private val categoryRepository: CategoryRepository, ) { @Cacheable(PetCache::class) - @CircuitBreaker("pet") - @Retry("pet") - @Timeout("pet") + @CircuitBreakable(PetCircuitBreaker::class) + @Retryable(PetRetry::class) + @Timeout(PetTimeouter::class) open fun findByID(petId: Long): PetWithCategory? = petRepository.findById(petId) - @CircuitBreaker("pet") - @Timeout("pet") + @CircuitBreakable(PetCircuitBreaker::class) + @Timeout(PetTimeouter::class) open fun add(petName: String, categoryName: String): PetWithCategory { val petCategoryId = categoryRepository.findByName(categoryName)?.id ?: categoryRepository.insert(categoryName) @@ -34,8 +34,8 @@ open class PetService( return PetWithCategory(petId, pet.name, pet.status, PetCategory(petCategoryId, categoryName)) } - @CircuitBreaker("pet") - @Timeout("pet") + @CircuitBreakable(PetCircuitBreaker::class) + @Timeout(PetTimeouter::class) @CachePut(value = PetCache::class, args = ["petId"]) open fun update( petId: Long, @@ -57,8 +57,8 @@ open class PetService( return result } - @CircuitBreaker("pet") - @Timeout("pet") + @CircuitBreakable(PetCircuitBreaker::class) + @Timeout(PetTimeouter::class) @CacheInvalidate(PetCache::class) open fun delete(petId: Long): Boolean = petRepository.deleteById(petId).value() == 1L } diff --git a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/pet/service/PetTimeouter.kt b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/pet/service/PetTimeouter.kt new file mode 100644 index 00000000..24980f60 --- /dev/null +++ b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/pet/service/PetTimeouter.kt @@ -0,0 +1,7 @@ +package io.koraframework.kotlin.example.submodule.pet.service + +import io.koraframework.resilient.timeout.Timeouter +import io.koraframework.resilient.timeout.annotation.TimeoutSpec + +@TimeoutSpec("resilient.timeout.pet") +interface PetTimeouter : Timeouter diff --git a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/main/resources/reference.conf b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/main/resources/reference.conf index ed893364..5e219183 100644 --- a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/main/resources/reference.conf +++ b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/main/resources/reference.conf @@ -7,7 +7,8 @@ pet-cache { resilient { circuitbreaker.pet { - slidingWindowSize = 50 + type = FIXED_WINDOW + countBased.windowSize = 50 minimumRequiredCalls = 25 failureRateThreshold = 50 waitDurationInOpenState = "25s" diff --git a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/test/kotlin/io/koraframework/kotlin/example/submodule/pet/PetComponentTests.kt b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/test/kotlin/io/koraframework/kotlin/example/submodule/pet/PetComponentTests.kt index faa7d4a6..2c67fe70 100644 --- a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/test/kotlin/io/koraframework/kotlin/example/submodule/pet/PetComponentTests.kt +++ b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api/src/test/kotlin/io/koraframework/kotlin/example/submodule/pet/PetComponentTests.kt @@ -38,7 +38,8 @@ class PetComponentTests : KoraAppTestConfigModifier { """ resilient { circuitbreaker.pet { - slidingWindowSize = 2 + type = FIXED_WINDOW + countBased.windowSize = 2 minimumRequiredCalls = 2 failureRateThreshold = 100 permittedCallsInHalfOpenState = 1 diff --git a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/build.gradle.kts b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/build.gradle.kts index 49e2e970..beeedb4d 100644 --- a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/build.gradle.kts @@ -3,11 +3,12 @@ plugins { } dependencies { + kspTest("io.koraframework:symbol-processors:${property("koraVersion")}") api(project(":examples:kotlin:kora-kotlin-crud-submodule:kora-kotlin-crud-submodule-common")) api("io.koraframework:database-jdbc") api("io.koraframework:cache-caffeine") api("io.koraframework:resilient-kora") testImplementation("io.koraframework:config-hocon") - testImplementation("io.goodforgod:testcontainers-extensions-postgres:0.13.1") + testImplementation("io.goodforgod:testcontainers-extensions-postgres:0.15.0") } diff --git a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/vet/service/VetCircuitBreaker.kt b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/vet/service/VetCircuitBreaker.kt new file mode 100644 index 00000000..2203f4bb --- /dev/null +++ b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/vet/service/VetCircuitBreaker.kt @@ -0,0 +1,7 @@ +package io.koraframework.kotlin.example.submodule.vet.service + +import io.koraframework.resilient.circuitbreaker.CircuitBreaker +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakerSpec + +@CircuitBreakerSpec("resilient.circuitbreaker.vet") +interface VetCircuitBreaker : CircuitBreaker diff --git a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/vet/service/VetRetry.kt b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/vet/service/VetRetry.kt new file mode 100644 index 00000000..7e64adf6 --- /dev/null +++ b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/vet/service/VetRetry.kt @@ -0,0 +1,7 @@ +package io.koraframework.kotlin.example.submodule.vet.service + +import io.koraframework.resilient.retry.Retry +import io.koraframework.resilient.retry.annotation.RetrySpec + +@RetrySpec("resilient.retry.vet") +interface VetRetry : Retry diff --git a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/vet/service/VetService.kt b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/vet/service/VetService.kt index e73ee7e4..31b425ca 100644 --- a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/vet/service/VetService.kt +++ b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/vet/service/VetService.kt @@ -6,32 +6,32 @@ import io.koraframework.cache.annotation.Cacheable import io.koraframework.common.annotation.Component import io.koraframework.kotlin.example.submodule.vet.model.dao.Vet import io.koraframework.kotlin.example.submodule.vet.repository.VetRepository -import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreaker -import io.koraframework.resilient.retry.annotation.Retry +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakable +import io.koraframework.resilient.retry.annotation.Retryable import io.koraframework.resilient.timeout.annotation.Timeout @Component open class VetService(private val vetRepository: VetRepository) { - @CircuitBreaker("vet") - @Retry("vet") - @Timeout("vet") + @CircuitBreakable(VetCircuitBreaker::class) + @Retryable(VetRetry::class) + @Timeout(VetTimeouter::class) open fun findAll(): List = vetRepository.findAll() @Cacheable(VetCache::class) - @CircuitBreaker("vet") - @Retry("vet") - @Timeout("vet") + @CircuitBreakable(VetCircuitBreaker::class) + @Retryable(VetRetry::class) + @Timeout(VetTimeouter::class) open fun findByID(vetId: Long): Vet? = vetRepository.findById(vetId) - @CircuitBreaker("vet") - @Timeout("vet") + @CircuitBreakable(VetCircuitBreaker::class) + @Timeout(VetTimeouter::class) open fun add(name: String, surname: String): Vet { val vet = Vet(0, name, surname) return vet.copy(id = vetRepository.insert(vet)) } - @CircuitBreaker("vet") - @Timeout("vet") + @CircuitBreakable(VetCircuitBreaker::class) + @Timeout(VetTimeouter::class) @CachePut(value = VetCache::class, args = ["id"]) open fun update(id: Long, name: String, surname: String): Vet? { val existing = vetRepository.findById(id) ?: return null @@ -40,8 +40,8 @@ open class VetService(private val vetRepository: VetRepository) { return result } - @CircuitBreaker("vet") - @Timeout("vet") + @CircuitBreakable(VetCircuitBreaker::class) + @Timeout(VetTimeouter::class) @CacheInvalidate(VetCache::class) open fun delete(vetId: Long): Boolean = vetRepository.deleteById(vetId).value() == 1L } diff --git a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/vet/service/VetTimeouter.kt b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/vet/service/VetTimeouter.kt new file mode 100644 index 00000000..d5d38754 --- /dev/null +++ b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/main/kotlin/io/koraframework/kotlin/example/submodule/vet/service/VetTimeouter.kt @@ -0,0 +1,7 @@ +package io.koraframework.kotlin.example.submodule.vet.service + +import io.koraframework.resilient.timeout.Timeouter +import io.koraframework.resilient.timeout.annotation.TimeoutSpec + +@TimeoutSpec("resilient.timeout.vet") +interface VetTimeouter : Timeouter diff --git a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/main/resources/reference.conf b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/main/resources/reference.conf index 5755981e..3c814ec6 100644 --- a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/main/resources/reference.conf +++ b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/main/resources/reference.conf @@ -7,7 +7,8 @@ vet-cache { resilient { circuitbreaker.vet { - slidingWindowSize = 50 + type = FIXED_WINDOW + countBased.windowSize = 50 minimumRequiredCalls = 25 failureRateThreshold = 50 waitDurationInOpenState = "25s" diff --git a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/test/kotlin/io/koraframework/kotlin/example/submodule/vet/VetComponentTests.kt b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/test/kotlin/io/koraframework/kotlin/example/submodule/vet/VetComponentTests.kt index 00d5f3f5..ae189ffd 100644 --- a/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/test/kotlin/io/koraframework/kotlin/example/submodule/vet/VetComponentTests.kt +++ b/examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api/src/test/kotlin/io/koraframework/kotlin/example/submodule/vet/VetComponentTests.kt @@ -32,7 +32,8 @@ class VetComponentTests : KoraAppTestConfigModifier { """ resilient { circuitbreaker.vet { - slidingWindowSize = 2 + type = FIXED_WINDOW + countBased.windowSize = 2 minimumRequiredCalls = 2 failureRateThreshold = 100 permittedCallsInHalfOpenState = 1 diff --git a/examples/kotlin/kora-kotlin-crud/build.gradle.kts b/examples/kotlin/kora-kotlin-crud/build.gradle.kts index f5fc6662..6918777b 100644 --- a/examples/kotlin/kora-kotlin-crud/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-crud/build.gradle.kts @@ -14,24 +14,16 @@ buildscript { plugins { id("application") id("jacoco") -// kotlin("kapt") version ("2.4.10") // KAPT & KSP broken since 1.9.11 kotlin("jvm") version ("2.4.10") id("com.google.devtools.ksp") version ("2.3.11") id("org.openapi.generator") version ("7.24.0") id("org.flywaydb.flyway") version ("8.4.2") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) -// kapt("org.mapstruct:mapstruct-processor:1.5.5.Final") // KAPT & KSP broken since 1.9.11 - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:http-client-ok") @@ -46,16 +38,14 @@ dependencies { implementation("io.koraframework:logging-logback") implementation("org.postgresql:postgresql:42.7.7") -// implementation("org.mapstruct:mapstruct:1.5.5.Final") // KAPT & KSP broken since 1.9.11 - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.8.1") - kspTest("io.koraframework:symbol-processors") + kspTest("io.koraframework:symbol-processors:${property("koraVersion")}") testImplementation("org.json:json:20231013") testImplementation("org.skyscreamer:jsonassert:1.5.1") - testImplementation("io.mockk:mockk:1.13.8") + testImplementation("io.mockk:mockk:1.14.11") testImplementation("io.koraframework:test-junit5") - testImplementation("io.goodforgod:testcontainers-extensions-postgres:0.13.1") + testImplementation("io.goodforgod:testcontainers-extensions-postgres:0.15.0") testImplementation("org.testcontainers:junit-jupiter:1.21.4") } @@ -64,7 +54,6 @@ kotlin { languageVersion.set(JavaLanguageVersion.of(25)) vendor.set(JvmVendorSpec.ADOPTIUM) } -// sourceSets.main { kotlin.srcDir("build/generated/source/kapt/main") } // KAPT & KSP broken since 1.9.11 } @@ -110,11 +99,7 @@ ksp { arg("kora.app.submodule.enabled", "true") // Only for integration tests } -// Run KAPT before KSP for MapStruct broken since 1.9.11 cause its Kotlin //tasks.withType { -// dependsOn.removeIf { (it as Named).name.contains("kapt", true) } -// dependsOn(tasks.named("kaptGenerateStubsKotlin").get()) -// dependsOn(tasks.named("kaptKotlin").get()) //} tasks.distTar { diff --git a/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/controller/HttpExceptionHandler.kt b/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/controller/HttpExceptionHandler.kt index ca5392c8..b753a1be 100644 --- a/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/controller/HttpExceptionHandler.kt +++ b/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/controller/HttpExceptionHandler.kt @@ -3,16 +3,18 @@ package io.koraframework.kotlin.example.crud.controller import io.micrometer.core.instrument.config.validate.ValidationException import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component -import io.koraframework.common.Context import io.koraframework.common.annotation.Tag import io.koraframework.example.crud.openapi.http.server.model.MessageTO import io.koraframework.http.common.body.HttpBody -import io.koraframework.http.server.common.* +import io.koraframework.http.server.common.HttpServer +import io.koraframework.http.server.common.interceptor.HttpServerInterceptor +import io.koraframework.http.server.common.request.HttpServerRequest +import io.koraframework.http.server.common.response.HttpServerResponse +import io.koraframework.http.server.common.response.HttpServerResponseException import io.koraframework.json.common.JsonWriter -import java.util.concurrent.CompletionStage import java.util.concurrent.TimeoutException -@Tag(HttpServerModule::class) +@Tag(HttpServer::class) @Component class HttpExceptionHandler(private val errorJsonWriter: JsonWriter) : HttpServerInterceptor { @@ -20,18 +22,16 @@ class HttpExceptionHandler(private val errorJsonWriter: JsonWriter) : val logger = LoggerFactory.getLogger(HttpExceptionHandler::class.java)!! } - override fun intercept( - context: Context, - request: HttpServerRequest, - chain: HttpServerInterceptor.InterceptChain - ): CompletionStage { - return chain.process(context, request).exceptionally { e -> + override fun intercept(request: HttpServerRequest, chain: HttpServerInterceptor.InterceptChain): HttpServerResponse { + try { + return chain.process(request) + } catch (e: Exception) { if (e is HttpServerResponseException) { - return@exceptionally e + return e } - val body = HttpBody.json(errorJsonWriter.toByteArrayUnchecked(MessageTO(e.message))) - when (e) { + val body = HttpBody.json(errorJsonWriter.toByteArray(MessageTO(e.message))) + return when (e) { is ValidationException -> HttpServerResponse.of(400, body) is IllegalArgumentException -> HttpServerResponse.of(400, body) is TimeoutException -> HttpServerResponse.of(408, body) diff --git a/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/model/PetMapper.kt b/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/model/PetMapper.kt index 56ed96c4..a07cdc7d 100644 --- a/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/model/PetMapper.kt +++ b/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/model/PetMapper.kt @@ -1,4 +1,4 @@ -package io.koraframework.kotlin.example.crud.model +package io.koraframework.kotlin.example.crud.model import io.koraframework.common.annotation.Component import io.koraframework.example.crud.openapi.http.server.model.CategoryTO @@ -21,10 +21,11 @@ class ManualPetMapper : PetMapper { Pet.Status.PENDING -> StatusEnum.PENDING Pet.Status.SOLD -> StatusEnum.SOLD } - return PetTO(pet.id, pet.name, enum, petCategoryToCategoryTO(pet.category)) + // the 2.0 generator orders the constructor by optionality, so the arguments are named + return PetTO(status = enum, id = pet.id, name = pet.name, category = petCategoryToCategoryTO(pet.category)) } override fun petCategoryToCategoryTO(category: PetCategory): CategoryTO { - return CategoryTO(category.id, category.name) + return CategoryTO(id = category.id, name = category.name) } } diff --git a/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/service/PetCircuitBreaker.kt b/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/service/PetCircuitBreaker.kt new file mode 100644 index 00000000..7b18b0a5 --- /dev/null +++ b/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/service/PetCircuitBreaker.kt @@ -0,0 +1,7 @@ +package io.koraframework.kotlin.example.crud.service + +import io.koraframework.resilient.circuitbreaker.CircuitBreaker +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakerSpec + +@CircuitBreakerSpec("resilient.circuitbreaker.pet") +interface PetCircuitBreaker : CircuitBreaker diff --git a/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/service/PetRetry.kt b/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/service/PetRetry.kt new file mode 100644 index 00000000..4b092049 --- /dev/null +++ b/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/service/PetRetry.kt @@ -0,0 +1,7 @@ +package io.koraframework.kotlin.example.crud.service + +import io.koraframework.resilient.retry.Retry +import io.koraframework.resilient.retry.annotation.RetrySpec + +@RetrySpec("resilient.retry.pet") +interface PetRetry : Retry diff --git a/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/service/PetService.kt b/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/service/PetService.kt index 27164627..578c062e 100644 --- a/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/service/PetService.kt +++ b/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/service/PetService.kt @@ -11,8 +11,8 @@ import io.koraframework.kotlin.example.crud.model.PetCategory import io.koraframework.kotlin.example.crud.model.PetWithCategory import io.koraframework.kotlin.example.crud.repository.CategoryRepository import io.koraframework.kotlin.example.crud.repository.PetRepository -import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreaker -import io.koraframework.resilient.retry.annotation.Retry +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakable +import io.koraframework.resilient.retry.annotation.Retryable import io.koraframework.resilient.timeout.annotation.Timeout @Component @@ -22,15 +22,15 @@ open class PetService( ) { @Cacheable(PetCache::class) - @CircuitBreaker("pet") - @Retry("pet") - @Timeout("pet") + @CircuitBreakable(PetCircuitBreaker::class) + @Retryable(PetRetry::class) + @Timeout(PetTimeouter::class) open fun findByID(petId: Long): PetWithCategory? { return petRepository.findById(petId) } - @CircuitBreaker("pet") - @Timeout("pet") + @CircuitBreakable(PetCircuitBreaker::class) + @Timeout(PetTimeouter::class) open fun add(createTO: PetCreateTO): PetWithCategory { val petCategoryId = categoryRepository.findByName(createTO.category.name)?.id ?: categoryRepository.insert(createTO.category.name) @@ -44,8 +44,8 @@ open class PetService( ) } - @CircuitBreaker("pet") - @Timeout("pet") + @CircuitBreakable(PetCircuitBreaker::class) + @Timeout(PetTimeouter::class) @CachePut(value = PetCache::class, args = ["id"]) open fun update(id: Long, updateTO: PetUpdateTO): PetWithCategory? { val existing = petRepository.findById(id) ?: return null @@ -62,8 +62,8 @@ open class PetService( return result } - @CircuitBreaker("pet") - @Timeout("pet") + @CircuitBreakable(PetCircuitBreaker::class) + @Timeout(PetTimeouter::class) @CacheInvalidate(PetCache::class) open fun delete(petId: Long): Boolean { return petRepository.deleteById(petId).value() == 1L diff --git a/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/service/PetTimeouter.kt b/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/service/PetTimeouter.kt new file mode 100644 index 00000000..7885d8e7 --- /dev/null +++ b/examples/kotlin/kora-kotlin-crud/src/main/kotlin/io/koraframework/kotlin/example/crud/service/PetTimeouter.kt @@ -0,0 +1,7 @@ +package io.koraframework.kotlin.example.crud.service + +import io.koraframework.resilient.timeout.Timeouter +import io.koraframework.resilient.timeout.annotation.TimeoutSpec + +@TimeoutSpec("resilient.timeout.pet") +interface PetTimeouter : Timeouter diff --git a/examples/kotlin/kora-kotlin-crud/src/main/resources/application.conf b/examples/kotlin/kora-kotlin-crud/src/main/resources/application.conf index b7d07544..31e63060 100644 --- a/examples/kotlin/kora-kotlin-crud/src/main/resources/application.conf +++ b/examples/kotlin/kora-kotlin-crud/src/main/resources/application.conf @@ -1,11 +1,11 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } -db { +jdbc { jdbcUrl = ${POSTGRES_JDBC_URL} username = ${POSTGRES_USER} password = ${POSTGRES_PASS} @@ -26,16 +26,17 @@ pet-cache { openapi { management { enabled = true - file = "openapi/http-server.yaml" + files = "openapi/http-server.yaml" swaggerui.enabled = true - rapidoc.enabled = true + scalar.enabled = true } } resilient { circuitbreaker.pet { - slidingWindowSize = 50 + type = FIXED_WINDOW + countBased.windowSize = 50 minimumRequiredCalls = 25 failureRateThreshold = 50 waitDurationInOpenState = "25s" diff --git a/examples/kotlin/kora-kotlin-crud/src/test/kotlin/io/koraframework/kotlin/example/crud/ComponentTests.kt b/examples/kotlin/kora-kotlin-crud/src/test/kotlin/io/koraframework/kotlin/example/crud/ComponentTests.kt index 648fc95f..c1ef058e 100644 --- a/examples/kotlin/kora-kotlin-crud/src/test/kotlin/io/koraframework/kotlin/example/crud/ComponentTests.kt +++ b/examples/kotlin/kora-kotlin-crud/src/test/kotlin/io/koraframework/kotlin/example/crud/ComponentTests.kt @@ -41,7 +41,8 @@ class ComponentTests : KoraAppTestConfigModifier { """ resilient { circuitbreaker.pet { - slidingWindowSize = 2 + type = FIXED_WINDOW + countBased.windowSize = 2 minimumRequiredCalls = 2 failureRateThreshold = 100 permittedCallsInHalfOpenState = 1 @@ -74,7 +75,7 @@ class ComponentTests : KoraAppTestConfigModifier { every { petRepository.findById(any()) } returns added val updated = petService.update( added.id, - PetUpdateTO("cat", PetUpdateTO.StatusEnum.PENDING, CategoryCreateTO("cat")) + PetUpdateTO(status = PetUpdateTO.StatusEnum.PENDING, name = "cat", category = CategoryCreateTO("cat")) ) assertNotNull(updated) assertEquals(1, updated!!.id) @@ -102,7 +103,7 @@ class ComponentTests : KoraAppTestConfigModifier { every { categoryRepository.findByName(any()) } returns added.category val updated = petService.update( added.id, - PetUpdateTO("cat", PetUpdateTO.StatusEnum.PENDING, CategoryCreateTO("dog")) + PetUpdateTO(status = PetUpdateTO.StatusEnum.PENDING, name = "cat", category = CategoryCreateTO("dog")) ) assertNotNull(updated) assertEquals(1, updated!!.id) diff --git a/examples/kotlin/kora-kotlin-crud/src/test/kotlin/io/koraframework/kotlin/example/crud/IntegrationTests.kt b/examples/kotlin/kora-kotlin-crud/src/test/kotlin/io/koraframework/kotlin/example/crud/IntegrationTests.kt index 30d50997..aec6f64e 100644 --- a/examples/kotlin/kora-kotlin-crud/src/test/kotlin/io/koraframework/kotlin/example/crud/IntegrationTests.kt +++ b/examples/kotlin/kora-kotlin-crud/src/test/kotlin/io/koraframework/kotlin/example/crud/IntegrationTests.kt @@ -57,7 +57,7 @@ class IntegrationTests(@ConnectionPostgreSQL val connection: JdbcConnection) : K override fun config(): KoraConfigModification = KoraConfigModification.ofString( """ - db { + jdbc { jdbcUrl = "${connection.params().jdbcUrl()}" username = "${connection.params().username()}" password = "${connection.params().password()}" @@ -66,7 +66,8 @@ class IntegrationTests(@ConnectionPostgreSQL val connection: JdbcConnection) : K pet-cache.maximumSize = 0 resilient { circuitbreaker.pet { - slidingWindowSize = 2 + type = FIXED_WINDOW + countBased.windowSize = 2 minimumRequiredCalls = 2 failureRateThreshold = 100 permittedCallsInHalfOpenState = 1 @@ -91,7 +92,7 @@ class IntegrationTests(@ConnectionPostgreSQL val connection: JdbcConnection) : K // when val updated = petService.update( added.id, - PetUpdateTO("cat", PetUpdateTO.StatusEnum.PENDING, CategoryCreateTO("cat")) + PetUpdateTO(status = PetUpdateTO.StatusEnum.PENDING, name = "cat", category = CategoryCreateTO("cat")) ) assertNotNull(updated) assertEquals(1, updated!!.id) @@ -112,7 +113,7 @@ class IntegrationTests(@ConnectionPostgreSQL val connection: JdbcConnection) : K // when val updated = petService.update( added.id, - PetUpdateTO("cat", PetUpdateTO.StatusEnum.PENDING, CategoryCreateTO("dog")) + PetUpdateTO(status = PetUpdateTO.StatusEnum.PENDING, name = "cat", category = CategoryCreateTO("dog")) ) assertNotNull(updated) assertEquals(1, updated!!.id) diff --git a/examples/kotlin/kora-kotlin-database-cassandra/build.gradle.kts b/examples/kotlin/kora-kotlin-database-cassandra/build.gradle.kts index 9fc649d6..090c5aac 100644 --- a/examples/kotlin/kora-kotlin-database-cassandra/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-database-cassandra/build.gradle.kts @@ -7,25 +7,16 @@ plugins { id("com.google.devtools.ksp") version ("2.3.11") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:database-cassandra") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") implementation("io.koraframework:config-hocon") implementation("io.koraframework:logging-logback") testImplementation("io.koraframework:test-junit5") - testImplementation("io.goodforgod:testcontainers-extensions-scylla:0.13.1") + testImplementation("io.goodforgod:testcontainers-extensions-scylla:0.15.0") } kotlin { diff --git a/examples/kotlin/kora-kotlin-database-cassandra/src/main/kotlin/io/koraframework/kotlin/example/cassandra/CassandraCrudSuspendRepository.kt b/examples/kotlin/kora-kotlin-database-cassandra/src/main/kotlin/io/koraframework/kotlin/example/cassandra/CassandraCrudSuspendRepository.kt deleted file mode 100644 index fb296047..00000000 --- a/examples/kotlin/kora-kotlin-database-cassandra/src/main/kotlin/io/koraframework/kotlin/example/cassandra/CassandraCrudSuspendRepository.kt +++ /dev/null @@ -1,42 +0,0 @@ -package io.koraframework.kotlin.example.cassandra - -import com.datastax.oss.driver.api.core.cql.ResultSet -import com.datastax.oss.driver.api.core.cql.Row -import com.datastax.oss.driver.api.core.data.GettableByName -import com.datastax.oss.driver.api.core.data.SettableByName -import io.koraframework.common.annotation.Component -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Root -import io.koraframework.database.cassandra.CassandraRepository -import io.koraframework.database.cassandra.annotation.EntityCassandra -import io.koraframework.database.cassandra.annotation.UDT -import io.koraframework.database.cassandra.mapper.parameter.CassandraParameterColumnMapper -import io.koraframework.database.cassandra.mapper.result.CassandraResultSetMapper -import io.koraframework.database.cassandra.mapper.result.CassandraRowColumnMapper -import io.koraframework.database.cassandra.mapper.result.CassandraRowMapper -import io.koraframework.database.common.annotation.Batch -import io.koraframework.database.common.annotation.Column -import io.koraframework.database.common.annotation.Query -import io.koraframework.database.common.annotation.Repository - -@Repository -interface CassandraCrudSuspendRepository : CassandraRepository { - @Query("SELECT * FROM entities WHERE id = :id") - suspend fun findById(id: String): CassandraEntity? - - @Query("SELECT * FROM entities") - suspend fun findAll(): List - - @Query("INSERT INTO entities(id, value1, value2, value3) VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3)") - suspend fun insert(entity: CassandraEntity) - - @Query("UPDATE entities SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 WHERE id = :entity.id") - suspend fun update(entity: CassandraEntity) - - @Query("DELETE FROM entities WHERE id = :id") - suspend fun deleteById(id: String) - - @Query("TRUNCATE entities") - suspend fun deleteAll() -} - diff --git a/examples/kotlin/kora-kotlin-database-cassandra/src/main/kotlin/io/koraframework/kotlin/example/cassandra/RootService.kt b/examples/kotlin/kora-kotlin-database-cassandra/src/main/kotlin/io/koraframework/kotlin/example/cassandra/RootService.kt index d570df96..5055c5b5 100644 --- a/examples/kotlin/kora-kotlin-database-cassandra/src/main/kotlin/io/koraframework/kotlin/example/cassandra/RootService.kt +++ b/examples/kotlin/kora-kotlin-database-cassandra/src/main/kotlin/io/koraframework/kotlin/example/cassandra/RootService.kt @@ -23,11 +23,9 @@ import io.koraframework.database.common.annotation.Repository @Component class RootService( private val crudSyncRepository: CassandraCrudSyncRepository, - private val crudSuspendRepository: CassandraCrudSuspendRepository, private val cassandraUdtRepository: CassandraUdtRepository, private val mapperRowColumnRepository: CassandraMapperRowColumnRepository, private val mapperParameterRepository: CassandraMapperParameterRepository, private val mapperRowRepository: CassandraMapperRowRepository, private val mapperResultSetRepository: CassandraMapperResultSetRepository ) - diff --git a/examples/kotlin/kora-kotlin-database-cassandra/src/test/kotlin/io/koraframework/kotlin/example/cassandra/CassandraCrudSuspendTests.kt b/examples/kotlin/kora-kotlin-database-cassandra/src/test/kotlin/io/koraframework/kotlin/example/cassandra/CassandraCrudSuspendTests.kt deleted file mode 100644 index 6902f1f6..00000000 --- a/examples/kotlin/kora-kotlin-database-cassandra/src/test/kotlin/io/koraframework/kotlin/example/cassandra/CassandraCrudSuspendTests.kt +++ /dev/null @@ -1,45 +0,0 @@ -package io.koraframework.kotlin.example.cassandra - -import io.goodforgod.testcontainers.extensions.ContainerMode -import io.goodforgod.testcontainers.extensions.scylla.ConnectionScylla -import io.goodforgod.testcontainers.extensions.scylla.Migration -import io.goodforgod.testcontainers.extensions.scylla.ScyllaConnection -import io.goodforgod.testcontainers.extensions.scylla.TestcontainersScylla -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNull -import org.junit.jupiter.api.Test -import io.koraframework.test.extension.junit5.KoraAppTest -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier -import io.koraframework.test.extension.junit5.KoraConfigModification -import io.koraframework.test.extension.junit5.TestComponent - -@TestcontainersScylla( - mode = ContainerMode.PER_RUN, - migration = Migration( - engine = Migration.Engines.SCRIPTS, - locations = ["migrations"], - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD - ) -) -@KoraAppTest(Application::class) -class CassandraCrudSuspendTests : KoraAppTestConfigModifier { - @ConnectionScylla - lateinit var connection: ScyllaConnection - - @TestComponent - lateinit var repository: CassandraCrudSuspendRepository - - override fun config(): KoraConfigModification = cassandraConfig(connection) - - @Test - fun suspendSingleSuccess() = runBlocking { - repository.insert(CassandraEntity("1", 1, "2", null)) - assertEquals(1, repository.findById("1")?.field1) - repository.update(CassandraEntity("1", 5, "6", null)) - assertEquals("6", repository.findById("1")?.value2) - repository.deleteById("1") - assertNull(repository.findById("1")) - } -} diff --git a/examples/kotlin/kora-kotlin-database-jdbc/build.gradle.kts b/examples/kotlin/kora-kotlin-database-jdbc/build.gradle.kts index 074953b3..adab3950 100644 --- a/examples/kotlin/kora-kotlin-database-jdbc/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-database-jdbc/build.gradle.kts @@ -7,27 +7,18 @@ plugins { id("com.google.devtools.ksp") version ("2.3.11") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("org.postgresql:postgresql:42.7.7") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") implementation("io.koraframework:database-jdbc") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") implementation("io.koraframework:config-hocon") testImplementation("io.koraframework:test-junit5") - testImplementation("io.goodforgod:testcontainers-extensions-postgres:0.13.1") + testImplementation("io.goodforgod:testcontainers-extensions-postgres:0.15.0") } kotlin { diff --git a/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/Application.kt b/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/Application.kt index 1fcf4f7a..1ad14b67 100644 --- a/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/Application.kt +++ b/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/Application.kt @@ -2,22 +2,13 @@ package io.koraframework.kotlin.example.jdbc import io.koraframework.application.graph.KoraApplication import io.koraframework.common.annotation.KoraApp -import io.koraframework.common.annotation.Tag import io.koraframework.config.hocon.HoconConfigModule -import io.koraframework.database.jdbc.JdbcDatabase import io.koraframework.database.jdbc.JdbcDatabaseModule -import io.koraframework.json.common.JsonCommonModule +import io.koraframework.json.common.JsonModule import io.koraframework.logging.logback.LogbackModule -import java.util.concurrent.Executor -import java.util.concurrent.Executors @KoraApp -interface Application : HoconConfigModule, LogbackModule, JsonCommonModule, JdbcDatabaseModule { - - @Tag(JdbcDatabase::class) - fun jdbcExecutor(): Executor = - Executors.newFixedThreadPool(maxOf(Runtime.getRuntime().availableProcessors(), 2) * 2) -} +interface Application : HoconConfigModule, LogbackModule, JsonModule, JdbcDatabaseModule fun main() { KoraApplication.run { ApplicationGraph.graph() } diff --git a/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/JdbcCrudMacrosRepository.kt b/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/JdbcCrudMacrosRepository.kt index e4f25366..33111611 100644 --- a/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/JdbcCrudMacrosRepository.kt +++ b/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/JdbcCrudMacrosRepository.kt @@ -53,6 +53,7 @@ interface JdbcCrudMacrosRepository : JdbcRepository { fun deleteAll(): UpdateCount } +@EntityJdbc @Table("entities") data class JdbcMacrosEntity( @field:Id val id: String, diff --git a/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/JdbcCrudSuspendRepository.kt b/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/JdbcCrudSuspendRepository.kt deleted file mode 100644 index e7b9c1fd..00000000 --- a/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/JdbcCrudSuspendRepository.kt +++ /dev/null @@ -1,54 +0,0 @@ -package io.koraframework.kotlin.example.jdbc - -import org.postgresql.util.PGobject -import io.koraframework.common.annotation.Component -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Module -import io.koraframework.common.annotation.Root -import io.koraframework.database.common.UpdateCount -import io.koraframework.database.common.annotation.* -import io.koraframework.database.jdbc.annotation.EntityJdbc -import io.koraframework.database.jdbc.JdbcRepository -import io.koraframework.database.jdbc.mapper.parameter.JdbcParameterColumnMapper -import io.koraframework.database.jdbc.mapper.result.JdbcResultColumnMapper -import io.koraframework.database.jdbc.mapper.result.JdbcResultSetMapper -import io.koraframework.database.jdbc.mapper.result.JdbcRowMapper -import io.koraframework.json.common.JsonReader -import io.koraframework.json.common.JsonWriter -import io.koraframework.json.common.annotation.Json -import java.sql.PreparedStatement -import java.sql.ResultSet -import java.sql.Types - -@Repository -interface JdbcCrudSuspendRepository : JdbcRepository { - - @EntityJdbc - @Table("entities") - data class Entity( - @field:Id val id: String, - @field:Column("value1") val field1: Int, - val value2: String, - val value3: String? - ) - - @Query("SELECT * FROM entities WHERE id = :id") - suspend fun findById(id: String): Entity? - - @Query("SELECT * FROM entities") - suspend fun findAll(): List - - @Query("INSERT INTO entities(id, value1, value2, value3) VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3)") - suspend fun insert(entity: Entity) - - @Query("UPDATE entities SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 WHERE id = :entity.id") - suspend fun update(entity: Entity) - - @Query("DELETE FROM entities WHERE id = :id") - suspend fun deleteById(id: String) - - @Query("DELETE FROM entities") - suspend fun deleteAll(): UpdateCount -} - - diff --git a/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/JdbcCrudSyncRepository.kt b/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/JdbcCrudSyncRepository.kt index e7b0ac77..7877efa7 100644 --- a/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/JdbcCrudSyncRepository.kt +++ b/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/JdbcCrudSyncRepository.kt @@ -60,7 +60,12 @@ data class JdbcEntity( @Component class ListOfStringJdbcParameterMapper : JdbcParameterColumnMapper> { - override fun set(stmt: PreparedStatement, index: Int, value: List) { + // the 2.0 contract declares the value as @Nullable, which Kotlin enforces on the override + override fun set(stmt: PreparedStatement, index: Int, value: List?) { + if (value == null) { + stmt.setNull(index, Types.ARRAY) + return + } stmt.setArray(index, stmt.connection.createArrayOf("VARCHAR", value.toTypedArray())) } } diff --git a/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/JdbcIdRandomRepository.kt b/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/JdbcIdRandomRepository.kt index 197a85c5..bb9b073b 100644 --- a/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/JdbcIdRandomRepository.kt +++ b/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/JdbcIdRandomRepository.kt @@ -24,6 +24,7 @@ import java.util.UUID @Repository interface JdbcIdRandomRepository : JdbcRepository { + @EntityJdbc data class Entity(val id: UUID = UUID.randomUUID(), @field:Column("name") val name: String) @Query("SELECT * FROM entities_uuid WHERE id = :id") diff --git a/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/JdbcJsonbMapperModule.kt b/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/JdbcJsonbMapperModule.kt index af88f2f6..bf4f2551 100644 --- a/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/JdbcJsonbMapperModule.kt +++ b/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/JdbcJsonbMapperModule.kt @@ -31,7 +31,7 @@ interface JdbcJsonbMapperModule { } else { val jsonb = PGobject() jsonb.type = "jsonb" - jsonb.value = writer.toStringUnchecked(value) + jsonb.value = writer.toString(value) stmt.setObject(index, jsonb) } } @@ -41,7 +41,7 @@ interface JdbcJsonbMapperModule { fun jdbcJsonResultColumnMapper(reader: JsonReader): JdbcResultColumnMapper { return JdbcResultColumnMapper { row, index -> val value = row.getString(index) - if (value == null) null else reader.readUnchecked(value) + if (value == null) null else reader.read(value) } } } diff --git a/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/RootService.kt b/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/RootService.kt index 7baed7b9..3a87fbd0 100644 --- a/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/RootService.kt +++ b/examples/kotlin/kora-kotlin-database-jdbc/src/main/kotlin/io/koraframework/kotlin/example/jdbc/RootService.kt @@ -32,7 +32,6 @@ class RootService( private val jdbcCrudExtendedRepository: JdbcCrudExtendedRepository, private val jdbcCrudExtendedCompositeRepository: JdbcCrudExtendedCompositeRepository, private val jdbcCrudMacrosIdCompositeRepository: JdbcCrudMacrosIdCompositeRepository, - private val jdbcCrudSuspendRepository: JdbcCrudSuspendRepository, private val jdbcMapperColumnRepository: JdbcMapperColumnRepository, private val jdbcMapperParameterRepository: JdbcMapperParameterRepository, private val jdbcMapperRowRepository: JdbcMapperRowRepository, @@ -40,4 +39,3 @@ class RootService( private val jdbcJsonbRepository: JdbcJsonbRepository, ) - diff --git a/examples/kotlin/kora-kotlin-database-jdbc/src/main/resources/application.conf b/examples/kotlin/kora-kotlin-database-jdbc/src/main/resources/application.conf index 617290d1..bb1a8bc4 100644 --- a/examples/kotlin/kora-kotlin-database-jdbc/src/main/resources/application.conf +++ b/examples/kotlin/kora-kotlin-database-jdbc/src/main/resources/application.conf @@ -1,4 +1,4 @@ -db { +jdbc { jdbcUrl = ${POSTGRES_JDBC_URL} username = ${POSTGRES_USER} password = ${POSTGRES_PASS} diff --git a/examples/kotlin/kora-kotlin-database-jdbc/src/test/kotlin/io/koraframework/kotlin/example/jdbc/JdbcCrudSuspendTests.kt b/examples/kotlin/kora-kotlin-database-jdbc/src/test/kotlin/io/koraframework/kotlin/example/jdbc/JdbcCrudSuspendTests.kt deleted file mode 100644 index cd31ac95..00000000 --- a/examples/kotlin/kora-kotlin-database-jdbc/src/test/kotlin/io/koraframework/kotlin/example/jdbc/JdbcCrudSuspendTests.kt +++ /dev/null @@ -1,44 +0,0 @@ -package io.koraframework.kotlin.example.jdbc - -import io.goodforgod.testcontainers.extensions.ContainerMode -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection -import io.goodforgod.testcontainers.extensions.jdbc.Migration -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNull -import org.junit.jupiter.api.Test -import io.koraframework.test.extension.junit5.KoraAppTest -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier -import io.koraframework.test.extension.junit5.KoraConfigModification -import io.koraframework.test.extension.junit5.TestComponent - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD - ) -) -@KoraAppTest(Application::class) -class JdbcCrudSuspendTests : KoraAppTestConfigModifier { - @ConnectionPostgreSQL - lateinit var connection: JdbcConnection - - @TestComponent - lateinit var repository: JdbcCrudSuspendRepository - - override fun config(): KoraConfigModification = jdbcConfig(connection) - - @Test - fun suspendSingleSuccess() = runBlocking { - repository.insert(JdbcCrudSuspendRepository.Entity("1", 1, "2", null)) - assertEquals(1, repository.findById("1")?.field1) - repository.update(JdbcCrudSuspendRepository.Entity("1", 5, "6", null)) - assertEquals("6", repository.findById("1")?.value2) - repository.deleteById("1") - assertNull(repository.findById("1")) - } -} diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/Dockerfile b/examples/kotlin/kora-kotlin-database-r2dbc/Dockerfile deleted file mode 100644 index c22138f1..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -ARG RUN_IMAGE=eclipse-temurin:25-jre-jammy -FROM ${RUN_IMAGE} - -ARG TARGET_DIR=/opt/app -ARG SOURCE_DIR=build/distributions - -COPY $SOURCE_DIR/*.tar application.tar -RUN mkdir $TARGET_DIR -RUN tar -xf application.tar -C $TARGET_DIR -RUN rm application.tar - -ARG DOCKER_USER=app -RUN groupadd -r $DOCKER_USER && useradd -rg $DOCKER_USER $DOCKER_USER -USER $DOCKER_USER - -EXPOSE 8080/tcp -EXPOSE 8085/tcp -CMD [ "/opt/app/application/bin/application" ] \ No newline at end of file diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/README.md b/examples/kotlin/kora-kotlin-database-r2dbc/README.md deleted file mode 100644 index 1233c047..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Kora Java Database R2DBC - -Пример модуля Database R2DBC в Kora. - -В примере использовались модули: - -- [R2dbc Database](https://kora-projects.github.io/kora-docs/ru/documentation/database-r2dbc) - -## Build - -Собрать классы: - -```shell -./gradlew classes -``` - -Собрать артефакт: - -```shell -./gradlew distTar -``` - -## Run - -Запустить локально: - -```shell -./gradlew run -``` - -## Test - -Тесты используют [Testcontainers](https://java.testcontainers.org/), -требуется [Docker](https://docs.docker.com/engine/install/) окружение для запуска тестов или аналогичные контейнерные -окружения ([colima](https://github.com/abiosoft/colima) / итп) - -Протестировать локально: - -```shell -./gradlew test -``` diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/build.gradle.kts b/examples/kotlin/kora-kotlin-database-r2dbc/build.gradle.kts deleted file mode 100644 index 77be2f13..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/build.gradle.kts +++ /dev/null @@ -1,69 +0,0 @@ -import org.gradle.api.tasks.testing.logging.TestExceptionFormat - -plugins { - id("application") - id("jacoco") - kotlin("jvm") version ("2.4.10") - id("com.google.devtools.ksp") version ("2.3.11") -} - -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - -dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") - - implementation("org.postgresql:r2dbc-postgresql:1.0.4.RELEASE") - implementation("io.koraframework:database-r2dbc") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.8.1") - - implementation("io.koraframework:config-hocon") - implementation("io.koraframework:logging-logback") - - testImplementation("io.koraframework:test-junit5") - testImplementation("io.goodforgod:testcontainers-extensions-postgres:0.13.1") - testImplementation("org.postgresql:postgresql:42.7.7") -} - -kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(25)) - vendor.set(JvmVendorSpec.ADOPTIUM) - } -} - -application { - applicationName = "application" - mainClass.set("io.koraframework.kotlin.example.r2dbc.ApplicationKt") - applicationDefaultJvmArgs = listOf("-Dfile.encoding=UTF-8") -} - -tasks.withType { - environment( - "POSTGRES_R2DBC_URL" to "r2dbc:postgresql://${findProperty("postgresHost") ?: "localhost"}:${findProperty("postgresPort") ?: "5432"}/${ - findProperty( - "postgresDatabase" - ) ?: "postgres" - }", - "POSTGRES_USER" to (findProperty("postgresUser") ?: "postgres"), - "POSTGRES_PASS" to (findProperty("postgresPassword") ?: "postgres"), - ) -} - -tasks.distTar { archiveFileName.set("application.tar") } -tasks.test { - dependsOn("distTar") - useJUnitPlatform() - testLogging { - showStandardStreams = true - events("passed", "skipped", "failed") - exceptionFormat = TestExceptionFormat.FULL - } -} diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/gradle.properties b/examples/kotlin/kora-kotlin-database-r2dbc/gradle.properties deleted file mode 100644 index 8e7a5091..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/gradle.properties +++ /dev/null @@ -1,6 +0,0 @@ -##### POSTGRES ##### -postgresHost=localhost -postgresPort=5432 -postgresUser=postgres -postgresPassword=postgres -postgresDatabase=postgres diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/Application.kt b/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/Application.kt deleted file mode 100644 index c3518a82..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/Application.kt +++ /dev/null @@ -1,14 +0,0 @@ -package io.koraframework.kotlin.example.r2dbc - -import io.koraframework.application.graph.KoraApplication -import io.koraframework.common.annotation.KoraApp -import io.koraframework.config.hocon.HoconConfigModule -import io.koraframework.database.r2dbc.R2dbcDatabaseModule -import io.koraframework.logging.logback.LogbackModule - -@KoraApp -interface Application : HoconConfigModule, LogbackModule, R2dbcDatabaseModule - -fun main() { - KoraApplication.run { ApplicationGraph.graph() } -} diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcCrudMacrosRepository.kt b/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcCrudMacrosRepository.kt deleted file mode 100644 index c8292f72..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcCrudMacrosRepository.kt +++ /dev/null @@ -1,35 +0,0 @@ -package io.koraframework.kotlin.example.r2dbc - -import io.koraframework.database.common.UpdateCount -import io.koraframework.database.common.annotation.* -import io.koraframework.database.r2dbc.R2dbcRepository - -@Repository -interface R2dbcCrudMacrosRepository : R2dbcRepository { - @Query("SELECT %{return#selects} FROM %{return#table} WHERE id = :id") - suspend fun findById(id: String): R2dbcMacrosEntity? - - @Query("SELECT %{return#selects} FROM %{return#table}") - suspend fun findAll(): List - - @Query("INSERT INTO %{entity#inserts}") - suspend fun insert(entity: R2dbcMacrosEntity) - - @Query("UPDATE %{entity#updates} WHERE %{entity#where = @id}") - suspend fun update(entity: R2dbcMacrosEntity) - - @Query("DELETE FROM entities WHERE id = :id") - suspend fun deleteById(id: String) - - @Query("DELETE FROM entities") - suspend fun deleteAll(): UpdateCount -} - -@Table("entities") -data class R2dbcMacrosEntity( - @field:Id val id: String, - @field:Column("value1") val field1: Int, - val value2: String, - val value3: String? -) - diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcCrudSuspendRepository.kt b/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcCrudSuspendRepository.kt deleted file mode 100644 index cfb2ac39..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcCrudSuspendRepository.kt +++ /dev/null @@ -1,29 +0,0 @@ -package io.koraframework.kotlin.example.r2dbc - -import io.koraframework.database.common.UpdateCount -import io.koraframework.database.common.annotation.Query -import io.koraframework.database.common.annotation.Repository -import io.koraframework.database.r2dbc.R2dbcRepository - -@Repository -interface R2dbcCrudSuspendRepository : R2dbcRepository { - @Query("SELECT * FROM entities WHERE id = :id") - suspend fun findById(id: String): R2dbcEntity? - - @Query("SELECT * FROM entities") - suspend fun findAll(): List - - @Query("INSERT INTO entities(id, value1, value2, value3) VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3)") - suspend fun insert(entity: R2dbcEntity) - - @Query("UPDATE entities SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 WHERE id = :entity.id") - suspend fun update(entity: R2dbcEntity) - - @Query("DELETE FROM entities WHERE id = :id") - suspend fun deleteById(id: String) - - @Query("DELETE FROM entities") - suspend fun deleteAll(): UpdateCount -} - - diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcCrudSyncRepository.kt b/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcCrudSyncRepository.kt deleted file mode 100644 index 85415f37..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcCrudSyncRepository.kt +++ /dev/null @@ -1,43 +0,0 @@ -package io.koraframework.kotlin.example.r2dbc - -import io.koraframework.database.common.UpdateCount -import io.koraframework.database.common.annotation.Batch -import io.koraframework.database.common.annotation.Column -import io.koraframework.database.common.annotation.Query -import io.koraframework.database.common.annotation.Repository -import io.koraframework.database.r2dbc.R2dbcRepository - -@Repository -interface R2dbcCrudSyncRepository : R2dbcRepository { - @Query("SELECT * FROM entities WHERE id = :id") - fun findById(id: String): R2dbcEntity? - - @Query("SELECT * FROM entities") - fun findAll(): List - - @Query("INSERT INTO entities(id, value1, value2, value3) VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3)") - fun insert(entity: R2dbcEntity) - - @Query("INSERT INTO entities(id, value1, value2, value3) VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3)") - fun insertBatch(@Batch entity: List): UpdateCount - - @Query("UPDATE entities SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 WHERE id = :entity.id") - fun update(entity: R2dbcEntity) - - @Query("UPDATE entities SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 WHERE id = :entity.id") - fun updateBatch(@Batch entity: List): UpdateCount - - @Query("DELETE FROM entities WHERE id = :id") - fun deleteById(id: String) - - @Query("DELETE FROM entities") - fun deleteAll(): UpdateCount -} - -data class R2dbcEntity( - val id: String, - @field:Column("value1") val field1: Int, - val value2: String, - val value3: String? -) - diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcIdRandomRepository.kt b/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcIdRandomRepository.kt deleted file mode 100644 index d7418c3f..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcIdRandomRepository.kt +++ /dev/null @@ -1,22 +0,0 @@ -package io.koraframework.kotlin.example.r2dbc - -import io.koraframework.database.common.UpdateCount -import io.koraframework.database.common.annotation.Column -import io.koraframework.database.common.annotation.Query -import io.koraframework.database.common.annotation.Repository -import io.koraframework.database.r2dbc.R2dbcRepository -import java.util.* - -@Repository -interface R2dbcIdRandomRepository : R2dbcRepository { - - data class Entity(val id: UUID = UUID.randomUUID(), @field:Column("name") val name: String) - - @Query("SELECT * FROM entities_uuid WHERE id = :id") - suspend fun findById(id: UUID): Entity? - - @Query("INSERT INTO entities_uuid(id, name) VALUES (:entity.id, :entity.name)") - suspend fun insert(entity: Entity): UpdateCount -} - - diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcIdSequenceRepository.kt b/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcIdSequenceRepository.kt deleted file mode 100644 index 9fc609d8..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcIdSequenceRepository.kt +++ /dev/null @@ -1,26 +0,0 @@ -package io.koraframework.kotlin.example.r2dbc - -import io.koraframework.database.common.annotation.* -import io.koraframework.database.r2dbc.R2dbcRepository - -@Repository -interface R2dbcIdSequenceRepository : R2dbcRepository { - - data class Entity(val id: Long?, @field:Column("name") val name: String) - - @Query("SELECT * FROM entities_sequence WHERE id = :id") - suspend fun findById(id: Long): Entity? - - @Query("INSERT INTO entities_sequence(name) VALUES (:entity.name) RETURNING id") - suspend fun insert(entity: Entity): Long - - @Id - @Query("INSERT INTO entities_sequence(name) VALUES (:entity.name)") - suspend fun createGenerated(entity: Entity): Long - - @Id - @Query("INSERT INTO entities_sequence(name) VALUES (:entity.name)") - suspend fun createGenerated(@Batch entity: List): List -} - - diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcMapperColumnRepository.kt b/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcMapperColumnRepository.kt deleted file mode 100644 index eb4b14fb..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcMapperColumnRepository.kt +++ /dev/null @@ -1,53 +0,0 @@ -package io.koraframework.kotlin.example.r2dbc - -import io.r2dbc.spi.Row -import io.r2dbc.spi.Statement -import io.koraframework.common.annotation.Mapping -import io.koraframework.database.common.annotation.Column -import io.koraframework.database.common.annotation.Query -import io.koraframework.database.common.annotation.Repository -import io.koraframework.database.r2dbc.R2dbcRepository -import io.koraframework.database.r2dbc.mapper.parameter.R2dbcParameterColumnMapper -import io.koraframework.database.r2dbc.mapper.result.R2dbcResultColumnMapper - -@Repository -interface R2dbcMapperColumnRepository : R2dbcRepository { - - data class Entity( - val id: String, - @Mapping(R2dbcEntityFieldTypeResultMapper::class) - @Mapping(R2dbcEntityFieldTypeColumnMapper::class) - @field:Column("value1") - val field1: FieldType, - val value2: String, - val value3: String? - ) { - enum class FieldType(val code: Int) { - UNKNOWN(-10), - ONE(1), - TWO(2) - } - } - - @Query("SELECT * FROM entities") - suspend fun findAll(): List -} - -class R2dbcEntityFieldTypeResultMapper : R2dbcResultColumnMapper { - override fun apply(row: Row, label: String): R2dbcMapperColumnRepository.Entity.FieldType? { - val fieldAsInt = row.get(label, Integer::class.java)?.toInt() ?: return null - return R2dbcMapperColumnRepository.Entity.FieldType.entries.firstOrNull { it.code == fieldAsInt } - ?: R2dbcMapperColumnRepository.Entity.FieldType.UNKNOWN - } -} - -class R2dbcEntityFieldTypeColumnMapper : R2dbcParameterColumnMapper { - override fun apply(stmt: Statement, index: Int, value: R2dbcMapperColumnRepository.Entity.FieldType?) { - if (value == null) { - stmt.bindNull(index, Integer::class.java) - } else { - stmt.bind(index, value.code) - } - } -} - diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcMapperParameterRepository.kt b/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcMapperParameterRepository.kt deleted file mode 100644 index aad4478c..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcMapperParameterRepository.kt +++ /dev/null @@ -1,45 +0,0 @@ -package io.koraframework.kotlin.example.r2dbc - -import io.r2dbc.spi.Statement -import io.koraframework.common.annotation.Mapping -import io.koraframework.database.common.UpdateCount -import io.koraframework.database.common.annotation.Column -import io.koraframework.database.common.annotation.Query -import io.koraframework.database.common.annotation.Repository -import io.koraframework.database.r2dbc.R2dbcRepository -import io.koraframework.database.r2dbc.mapper.parameter.R2dbcParameterColumnMapper - -@Repository -interface R2dbcMapperParameterRepository : R2dbcRepository { - - data class Entity( - val id: String, - @field:Column("value1") val field1: Int, - val value2: String, - val value3: String? - ) { - enum class FieldType(val code: Int) { - UNKNOWN(-10), - ONE(1), - TWO(2) - } - } - - @Query("UPDATE entities SET value1 = :fieldType WHERE id = :id") - suspend fun updateFieldType( - id: String, - @Mapping(R2dbcEntityFieldTypeParameterMapper::class) fieldType: Entity.FieldType - ): UpdateCount -} - -class R2dbcEntityFieldTypeParameterMapper : - R2dbcParameterColumnMapper { - override fun apply(stmt: Statement, index: Int, value: R2dbcMapperParameterRepository.Entity.FieldType?) { - if (value == null) { - stmt.bindNull(index, Integer::class.java) - } else { - stmt.bind(index, value.code) - } - } -} - diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcMapperRowRepository.kt b/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcMapperRowRepository.kt deleted file mode 100644 index d2eaa21a..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcMapperRowRepository.kt +++ /dev/null @@ -1,26 +0,0 @@ -package io.koraframework.kotlin.example.r2dbc - -import io.r2dbc.spi.Row -import io.koraframework.common.annotation.Mapping -import io.koraframework.database.common.annotation.Column -import io.koraframework.database.common.annotation.Query -import io.koraframework.database.common.annotation.Repository -import io.koraframework.database.r2dbc.R2dbcRepository -import io.koraframework.database.r2dbc.mapper.result.R2dbcRowMapper - -@Repository -interface R2dbcMapperRowRepository : R2dbcRepository { - - @Mapping(R2dbcEntityPartRowMapper::class) - @Query("SELECT id, value1 FROM entities") - suspend fun findAllParts(): List -} - -data class R2dbcEntityPart(val id: String, @field:Column("value1") val field1: Int) - -class R2dbcEntityPartRowMapper : R2dbcRowMapper { - override fun apply(row: Row): R2dbcEntityPart { - return R2dbcEntityPart(row.get(0, String::class.java)!!, row.get(1, Integer::class.java)!!.toInt()) - } -} - diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/RootService.kt b/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/RootService.kt deleted file mode 100644 index cf25552d..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/kotlin/io/koraframework/kotlin/example/r2dbc/RootService.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.koraframework.kotlin.example.r2dbc - -import io.koraframework.common.annotation.Component -import io.koraframework.common.annotation.Root - -@Root -@Component -class RootService( - private val r2dbcCrudSuspendRepository: R2dbcCrudSuspendRepository, - private val r2dbcCrudSyncRepository: R2dbcCrudSyncRepository, - private val r2dbcCrudMacrosRepository: R2dbcCrudMacrosRepository, - private val r2dbcIdRandomRepository: R2dbcIdRandomRepository, - private val r2dbcIdSequenceRepository: R2dbcIdSequenceRepository, - private val r2dbcMapperColumnRepository: R2dbcMapperColumnRepository, - private val r2dbcMapperParameterRepository: R2dbcMapperParameterRepository, - private val r2dbcMapperRowRepository: R2dbcMapperRowRepository -) - - diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/resources/application.conf b/examples/kotlin/kora-kotlin-database-r2dbc/src/main/resources/application.conf deleted file mode 100644 index e8d51862..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/resources/application.conf +++ /dev/null @@ -1,15 +0,0 @@ -db { - r2dbcUrl = ${POSTGRES_R2DBC_URL} - username = ${POSTGRES_USER} - password = ${POSTGRES_PASS} - maxPoolSize = 10 - poolName = "kora" - telemetry.logging.enabled = true -} - - -logging.level { - "root": "WARN" - "io.koraframework": "INFO" - "io.koraframework.example": "INFO" -} diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/resources/db/migration/V1__setup-tables.sql b/examples/kotlin/kora-kotlin-database-r2dbc/src/main/resources/db/migration/V1__setup-tables.sql deleted file mode 100644 index 149017c9..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/resources/db/migration/V1__setup-tables.sql +++ /dev/null @@ -1,33 +0,0 @@ -CREATE TABLE IF NOT EXISTS entities -( - id VARCHAR NOT NULL, - value1 INT NOT NULL, - value2 VARCHAR NOT NULL, - value3 VARCHAR, - PRIMARY KEY (id) -); - - -CREATE TABLE IF NOT EXISTS entities_sequence -( - id BIGINT GENERATED ALWAYS AS IDENTITY, - name VARCHAR NOT NULL, - code VARCHAR NOT NULL DEFAULT '1', - PRIMARY KEY (id) -); - - -CREATE TABLE IF NOT EXISTS entities_uuid -( - id UUID NOT NULL, - name VARCHAR NOT NULL, - PRIMARY KEY (id) -); - - -CREATE TABLE IF NOT EXISTS entities_jsonb -( - id UUID NOT NULL, - value JSONB NOT NULL, - PRIMARY KEY (id) -); diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/resources/logback.xml b/examples/kotlin/kora-kotlin-database-r2dbc/src/main/resources/logback.xml deleted file mode 100644 index 184638b5..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/src/main/resources/logback.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/src/test/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcCrudSuspendTests.kt b/examples/kotlin/kora-kotlin-database-r2dbc/src/test/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcCrudSuspendTests.kt deleted file mode 100644 index d4b0de09..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/src/test/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcCrudSuspendTests.kt +++ /dev/null @@ -1,52 +0,0 @@ -package io.koraframework.kotlin.example.r2dbc - -import io.goodforgod.testcontainers.extensions.ContainerMode -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection -import io.goodforgod.testcontainers.extensions.jdbc.Migration -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test -import io.koraframework.test.extension.junit5.KoraAppTest -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier -import io.koraframework.test.extension.junit5.KoraConfigModification -import io.koraframework.test.extension.junit5.TestComponent - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD - ) -) -@KoraAppTest(Application::class) -class R2dbcCrudSuspendTests : KoraAppTestConfigModifier { - @ConnectionPostgreSQL - lateinit var connection: JdbcConnection - - @TestComponent - lateinit var repository: R2dbcCrudSuspendRepository - - override fun config(): KoraConfigModification = r2dbcConfig(connection) - - @Test - fun suspendSingleSuccess() = runBlocking { - repository.insert(R2dbcEntity("1", 1, "2", null)) - assertEquals(1, repository.findById("1")?.field1) - repository.update(R2dbcEntity("1", 5, "6", null)) - assertEquals("6", repository.findById("1")?.value2) - repository.deleteById("1") - assertNull(repository.findById("1")) - } - - @Test - fun suspendMultiSuccess() = runBlocking { - repository.insert(R2dbcEntity("1", 1, "2", null)) - repository.insert(R2dbcEntity("2", 3, "4", null)) - assertEquals(2, repository.findAll().size) - assertEquals(2L, repository.deleteAll().value()) - assertTrue(repository.findAll().isEmpty()) - } -} diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/src/test/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcCrudSyncTests.kt b/examples/kotlin/kora-kotlin-database-r2dbc/src/test/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcCrudSyncTests.kt deleted file mode 100644 index a65677a8..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/src/test/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcCrudSyncTests.kt +++ /dev/null @@ -1,69 +0,0 @@ -package io.koraframework.kotlin.example.r2dbc - -import io.goodforgod.testcontainers.extensions.ContainerMode -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection -import io.goodforgod.testcontainers.extensions.jdbc.Migration -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test -import io.koraframework.test.extension.junit5.KoraAppTest -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier -import io.koraframework.test.extension.junit5.KoraConfigModification -import io.koraframework.test.extension.junit5.TestComponent - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD - ) -) -@KoraAppTest(Application::class) -class R2dbcCrudSyncTests : KoraAppTestConfigModifier { - @ConnectionPostgreSQL - lateinit var connection: JdbcConnection - - @TestComponent - lateinit var repository: R2dbcCrudSyncRepository - - override fun config(): KoraConfigModification = r2dbcConfig(connection) - - @Test - fun singleSuccess() { - repository.insert(R2dbcEntity("1", 1, "2", null)) - val foundCreated = repository.findById("1") - assertNotNull(foundCreated) - assertEquals(1, foundCreated?.field1) - - repository.update(R2dbcEntity("1", 5, "6", null)) - val foundUpdated = repository.findById("1") - assertNotNull(foundUpdated) - assertEquals(5, foundUpdated?.field1) - assertEquals("6", foundUpdated?.value2) - - repository.deleteById("1") - assertNull(repository.findById("1")) - } - - @Test - fun batchSuccess() { - assertEquals( - 2L, - repository.insertBatch(listOf(R2dbcEntity("1", 1, "2", null), R2dbcEntity("2", 3, "4", null))).value() - ) - assertEquals(2, repository.findAll().size) - - assertEquals( - 2L, - repository.updateBatch(listOf(R2dbcEntity("1", 5, "6", null), R2dbcEntity("2", 7, "8", null))).value() - ) - val foundUpdated = repository.findAll() - assertTrue(foundUpdated.any { it.id == "1" && it.field1 == 5 }) - assertTrue(foundUpdated.any { it.id == "2" && it.field1 == 7 }) - - assertEquals(2L, repository.deleteAll().value()) - assertTrue(repository.findAll().isEmpty()) - } -} diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/src/test/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcMapperSuspendTests.kt b/examples/kotlin/kora-kotlin-database-r2dbc/src/test/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcMapperSuspendTests.kt deleted file mode 100644 index 97b20eb9..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/src/test/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcMapperSuspendTests.kt +++ /dev/null @@ -1,56 +0,0 @@ -package io.koraframework.kotlin.example.r2dbc - -import io.goodforgod.testcontainers.extensions.ContainerMode -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection -import io.goodforgod.testcontainers.extensions.jdbc.Migration -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import io.koraframework.test.extension.junit5.KoraAppTest -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier -import io.koraframework.test.extension.junit5.KoraConfigModification -import io.koraframework.test.extension.junit5.TestComponent - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD - ) -) -@KoraAppTest(Application::class) -class R2dbcMapperSuspendTests : KoraAppTestConfigModifier { - @ConnectionPostgreSQL - lateinit var connection: JdbcConnection - - @TestComponent - lateinit var crudRepository: R2dbcCrudSuspendRepository - - @TestComponent - lateinit var columnRepository: R2dbcMapperColumnRepository - - @TestComponent - lateinit var parameterRepository: R2dbcMapperParameterRepository - - @TestComponent - lateinit var rowRepository: R2dbcMapperRowRepository - - override fun config(): KoraConfigModification = r2dbcConfig(connection) - - @Test - fun mapperSuccess() = runBlocking { - crudRepository.insert(R2dbcEntity("1", 1, "2", null)) - crudRepository.insert(R2dbcEntity("2", 3, "4", null)) - - assertEquals( - R2dbcMapperColumnRepository.Entity.FieldType.ONE, - columnRepository.findAll().first { it.id == "1" }.field1 - ) - parameterRepository.updateFieldType("1", R2dbcMapperParameterRepository.Entity.FieldType.TWO) - assertEquals(R2dbcMapperParameterRepository.Entity.FieldType.TWO.code, crudRepository.findById("1")?.field1) - assertEquals(2, rowRepository.findAllParts().size) - } -} diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/src/test/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcTestConfig.kt b/examples/kotlin/kora-kotlin-database-r2dbc/src/test/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcTestConfig.kt deleted file mode 100644 index f5d39962..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/src/test/kotlin/io/koraframework/kotlin/example/r2dbc/R2dbcTestConfig.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.koraframework.kotlin.example.r2dbc - -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection -import io.koraframework.test.extension.junit5.KoraConfigModification - -fun r2dbcConfig(connection: JdbcConnection): KoraConfigModification { - val params = connection.params() - val url = "r2dbc:postgresql://${params.host()}:${params.port()}/${params.database()}" - return KoraConfigModification.ofSystemProperty("POSTGRES_R2DBC_URL", url) - .withSystemProperty("POSTGRES_USER", params.username()) - .withSystemProperty("POSTGRES_PASS", params.password()) -} diff --git a/examples/kotlin/kora-kotlin-database-r2dbc/src/test/resources/logback-test.xml b/examples/kotlin/kora-kotlin-database-r2dbc/src/test/resources/logback-test.xml deleted file mode 100644 index 0af9d2bc..00000000 --- a/examples/kotlin/kora-kotlin-database-r2dbc/src/test/resources/logback-test.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - UTF-8 - %cyan(%d{HH:mm:ss.SSS}) %highlight(%-5level) [%thread] %logger{36} - %msg%n - - - - - - - - - - - - - - - - - - - - diff --git a/examples/kotlin/kora-kotlin-database-vertx/Dockerfile b/examples/kotlin/kora-kotlin-database-vertx/Dockerfile deleted file mode 100644 index c22138f1..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -ARG RUN_IMAGE=eclipse-temurin:25-jre-jammy -FROM ${RUN_IMAGE} - -ARG TARGET_DIR=/opt/app -ARG SOURCE_DIR=build/distributions - -COPY $SOURCE_DIR/*.tar application.tar -RUN mkdir $TARGET_DIR -RUN tar -xf application.tar -C $TARGET_DIR -RUN rm application.tar - -ARG DOCKER_USER=app -RUN groupadd -r $DOCKER_USER && useradd -rg $DOCKER_USER $DOCKER_USER -USER $DOCKER_USER - -EXPOSE 8080/tcp -EXPOSE 8085/tcp -CMD [ "/opt/app/application/bin/application" ] \ No newline at end of file diff --git a/examples/kotlin/kora-kotlin-database-vertx/README.md b/examples/kotlin/kora-kotlin-database-vertx/README.md deleted file mode 100644 index 82e28f24..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Kora Java Database Vertx - -Пример модуля Database Vertx в Kora. - -В примере использовались модули: - -- [Vertx Database](https://kora-projects.github.io/kora-docs/ru/documentation/database-vertx) - -## Build - -Собрать классы: - -```shell -./gradlew classes -``` - -Собрать артефакт: - -```shell -./gradlew distTar -``` - -## Run - -Запустить локально: - -```shell -./gradlew run -``` - -## Test - -Тесты используют [Testcontainers](https://java.testcontainers.org/), -требуется [Docker](https://docs.docker.com/engine/install/) окружение для запуска тестов или аналогичные контейнерные -окружения ([colima](https://github.com/abiosoft/colima) / итп) - -Протестировать локально: - -```shell -./gradlew test -``` diff --git a/examples/kotlin/kora-kotlin-database-vertx/build.gradle.kts b/examples/kotlin/kora-kotlin-database-vertx/build.gradle.kts deleted file mode 100644 index 7f2d6a5d..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/build.gradle.kts +++ /dev/null @@ -1,71 +0,0 @@ -import org.gradle.api.tasks.testing.logging.TestExceptionFormat - -plugins { - id("application") - id("jacoco") - kotlin("jvm") version ("2.4.10") - id("com.google.devtools.ksp") version ("2.3.11") -} - -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - -dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") - - implementation("io.koraframework:database-vertx") - implementation("io.vertx:vertx-pg-client:4.3.8") - implementation("com.ongres.scram:client:2.1") - implementation("io.projectreactor:reactor-core:3.6.18") // For reactive examples (optional) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.8.1") - - implementation("io.koraframework:config-hocon") - implementation("io.koraframework:logging-logback") - - testImplementation("io.koraframework:test-junit5") - testImplementation("io.goodforgod:testcontainers-extensions-postgres:0.13.1") - testImplementation("org.postgresql:postgresql:42.7.7") -} - -kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(25)) - vendor.set(JvmVendorSpec.ADOPTIUM) - } -} - -application { - applicationName = "application" - mainClass.set("io.koraframework.kotlin.example.vertx.ApplicationKt") - applicationDefaultJvmArgs = listOf("-Dfile.encoding=UTF-8") -} - -tasks.withType { - environment( - "POSTGRES_JDBC_URL" to "jdbc:postgresql://${findProperty("postgresHost") ?: "localhost"}:${findProperty("postgresPort") ?: "5432"}/${ - findProperty( - "postgresDatabase" - ) ?: "postgres" - }", - "POSTGRES_USER" to (findProperty("postgresUser") ?: "postgres"), - "POSTGRES_PASS" to (findProperty("postgresPassword") ?: "postgres"), - ) -} - -tasks.distTar { archiveFileName.set("application.tar") } -tasks.test { - dependsOn("distTar") - useJUnitPlatform() - testLogging { - showStandardStreams = true - events("passed", "skipped", "failed") - exceptionFormat = TestExceptionFormat.FULL - } -} diff --git a/examples/kotlin/kora-kotlin-database-vertx/gradle.properties b/examples/kotlin/kora-kotlin-database-vertx/gradle.properties deleted file mode 100644 index 8e7a5091..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/gradle.properties +++ /dev/null @@ -1,6 +0,0 @@ -##### POSTGRES ##### -postgresHost=localhost -postgresPort=5432 -postgresUser=postgres -postgresPassword=postgres -postgresDatabase=postgres diff --git a/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/Application.kt b/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/Application.kt deleted file mode 100644 index 721570f4..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/Application.kt +++ /dev/null @@ -1,14 +0,0 @@ -package io.koraframework.kotlin.example.vertx - -import io.koraframework.application.graph.KoraApplication -import io.koraframework.common.annotation.KoraApp -import io.koraframework.config.hocon.HoconConfigModule -import io.koraframework.database.vertx.VertxDatabaseModule -import io.koraframework.logging.logback.LogbackModule - -@KoraApp -interface Application : HoconConfigModule, LogbackModule, VertxDatabaseModule - -fun main() { - KoraApplication.run { ApplicationGraph.graph() } -} diff --git a/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/RootService.kt b/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/RootService.kt deleted file mode 100644 index e7b3f35d..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/RootService.kt +++ /dev/null @@ -1,29 +0,0 @@ -package io.koraframework.kotlin.example.vertx - -import io.vertx.sqlclient.Row -import io.vertx.sqlclient.RowSet -import io.koraframework.common.annotation.Component -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Root -import io.koraframework.database.common.UpdateCount -import io.koraframework.database.common.annotation.Batch -import io.koraframework.database.common.annotation.Column -import io.koraframework.database.common.annotation.Query -import io.koraframework.database.common.annotation.Repository -import io.koraframework.database.vertx.VertxRepository -import io.koraframework.database.vertx.mapper.parameter.VertxParameterColumnMapper -import io.koraframework.database.vertx.mapper.result.VertxResultColumnMapper -import io.koraframework.database.vertx.mapper.result.VertxRowMapper -import io.koraframework.database.vertx.mapper.result.VertxRowSetMapper - -@Root -@Component -class RootService( - private val vertxCrudRepository: VertxCrudSyncRepository, - private val vertxCrudSuspendRepository: VertxCrudSuspendRepository, - private val vertxMapperColumnRepository: VertxMapperColumnRepository, - private val vertxMapperParameterRepository: VertxMapperParameterRepository, - private val vertxMapperRowRepository: VertxMapperRowRepository, - private val vertxMapperRowSetRepository: VertxMapperRowSetRepository -) - diff --git a/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/VertxCrudSuspendRepository.kt b/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/VertxCrudSuspendRepository.kt deleted file mode 100644 index 4e2fd911..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/VertxCrudSuspendRepository.kt +++ /dev/null @@ -1,39 +0,0 @@ -package io.koraframework.kotlin.example.vertx - -import io.vertx.sqlclient.Row -import io.vertx.sqlclient.RowSet -import io.koraframework.common.annotation.Component -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Root -import io.koraframework.database.common.UpdateCount -import io.koraframework.database.common.annotation.Batch -import io.koraframework.database.common.annotation.Column -import io.koraframework.database.common.annotation.Query -import io.koraframework.database.common.annotation.Repository -import io.koraframework.database.vertx.VertxRepository -import io.koraframework.database.vertx.mapper.parameter.VertxParameterColumnMapper -import io.koraframework.database.vertx.mapper.result.VertxResultColumnMapper -import io.koraframework.database.vertx.mapper.result.VertxRowMapper -import io.koraframework.database.vertx.mapper.result.VertxRowSetMapper - -@Repository -interface VertxCrudSuspendRepository : VertxRepository { - @Query("SELECT * FROM entities WHERE id = :id") - suspend fun findById(id: String): VertxEntity? - - @Query("SELECT * FROM entities") - suspend fun findAll(): List - - @Query("INSERT INTO entities(id, value1, value2, value3) VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3)") - suspend fun insert(entity: VertxEntity) - - @Query("UPDATE entities SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 WHERE id = :entity.id") - suspend fun update(entity: VertxEntity) - - @Query("DELETE FROM entities WHERE id = :id") - suspend fun deleteById(id: String) - - @Query("DELETE FROM entities") - suspend fun deleteAll(): UpdateCount -} - diff --git a/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/VertxCrudSyncRepository.kt b/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/VertxCrudSyncRepository.kt deleted file mode 100644 index 8b08dee2..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/VertxCrudSyncRepository.kt +++ /dev/null @@ -1,52 +0,0 @@ -package io.koraframework.kotlin.example.vertx - -import io.vertx.sqlclient.Row -import io.vertx.sqlclient.RowSet -import io.koraframework.common.annotation.Component -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Root -import io.koraframework.database.common.UpdateCount -import io.koraframework.database.common.annotation.Batch -import io.koraframework.database.common.annotation.Column -import io.koraframework.database.common.annotation.Query -import io.koraframework.database.common.annotation.Repository -import io.koraframework.database.vertx.VertxRepository -import io.koraframework.database.vertx.mapper.parameter.VertxParameterColumnMapper -import io.koraframework.database.vertx.mapper.result.VertxResultColumnMapper -import io.koraframework.database.vertx.mapper.result.VertxRowMapper -import io.koraframework.database.vertx.mapper.result.VertxRowSetMapper - -@Repository -interface VertxCrudSyncRepository : VertxRepository { - @Query("SELECT * FROM entities WHERE id = :id") - fun findById(id: String): VertxEntity? - - @Query("SELECT * FROM entities") - fun findAll(): List - - @Query("INSERT INTO entities(id, value1, value2, value3) VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3)") - fun insert(entity: VertxEntity) - - @Query("INSERT INTO entities(id, value1, value2, value3) VALUES (:entity.id, :entity.field1, :entity.value2, :entity.value3)") - fun insertBatch(@Batch entity: List): UpdateCount - - @Query("UPDATE entities SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 WHERE id = :entity.id") - fun update(entity: VertxEntity) - - @Query("UPDATE entities SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 WHERE id = :entity.id") - fun updateBatch(@Batch entity: List): UpdateCount - - @Query("DELETE FROM entities WHERE id = :id") - fun deleteById(id: String) - - @Query("DELETE FROM entities") - fun deleteAll(): UpdateCount -} - -data class VertxEntity( - val id: String, - @field:Column("value1") val field1: Int, - val value2: String, - val value3: String? -) - diff --git a/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperColumnRepository.kt b/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperColumnRepository.kt deleted file mode 100644 index 962b5051..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperColumnRepository.kt +++ /dev/null @@ -1,48 +0,0 @@ -package io.koraframework.kotlin.example.vertx - -import io.vertx.sqlclient.Row -import io.vertx.sqlclient.RowSet -import io.koraframework.common.annotation.Component -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Root -import io.koraframework.database.common.UpdateCount -import io.koraframework.database.common.annotation.Batch -import io.koraframework.database.common.annotation.Column -import io.koraframework.database.common.annotation.Query -import io.koraframework.database.common.annotation.Repository -import io.koraframework.database.vertx.VertxRepository -import io.koraframework.database.vertx.mapper.parameter.VertxParameterColumnMapper -import io.koraframework.database.vertx.mapper.result.VertxResultColumnMapper -import io.koraframework.database.vertx.mapper.result.VertxRowMapper -import io.koraframework.database.vertx.mapper.result.VertxRowSetMapper - -@Repository -interface VertxMapperColumnRepository : VertxRepository { - - data class Entity( - val id: String, - @Mapping(VertxEntityFieldTypeResultMapper::class) - @field:Column("value1") - val field1: FieldType, - val value2: String, - val value3: String? - ) { - enum class FieldType(val code: Int) { - UNKNOWN(-10), - ONE(1), - TWO(2) - } - } - - @Query("SELECT * FROM entities") - fun findAll(): List -} - -class VertxEntityFieldTypeResultMapper : VertxResultColumnMapper { - override fun apply(row: Row, index: Int): VertxMapperColumnRepository.Entity.FieldType? { - val fieldAsInt = row.get(Integer::class.java, index)?.toInt() ?: return null - return VertxMapperColumnRepository.Entity.FieldType.entries.firstOrNull { it.code == fieldAsInt } - ?: VertxMapperColumnRepository.Entity.FieldType.UNKNOWN - } -} - diff --git a/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperParameterRepository.kt b/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperParameterRepository.kt deleted file mode 100644 index dcd419dc..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperParameterRepository.kt +++ /dev/null @@ -1,48 +0,0 @@ -package io.koraframework.kotlin.example.vertx - -import io.vertx.sqlclient.Row -import io.vertx.sqlclient.RowSet -import io.koraframework.common.annotation.Component -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Root -import io.koraframework.database.common.UpdateCount -import io.koraframework.database.common.annotation.Batch -import io.koraframework.database.common.annotation.Column -import io.koraframework.database.common.annotation.Query -import io.koraframework.database.common.annotation.Repository -import io.koraframework.database.vertx.VertxRepository -import io.koraframework.database.vertx.mapper.parameter.VertxParameterColumnMapper -import io.koraframework.database.vertx.mapper.result.VertxResultColumnMapper -import io.koraframework.database.vertx.mapper.result.VertxRowMapper -import io.koraframework.database.vertx.mapper.result.VertxRowSetMapper - -@Repository -interface VertxMapperParameterRepository : VertxRepository { - - data class Entity( - val id: String, - @field:Column("value1") val field1: Int, - val value2: String, - val value3: String? - ) { - enum class FieldType(val code: Int) { - UNKNOWN(-10), - ONE(1), - TWO(2) - } - } - - @Query("UPDATE entities SET value1 = :fieldType WHERE id = :id") - fun updateFieldType( - id: String, - @Mapping(VertxEntityFieldTypeParameterMapper::class) fieldType: Entity.FieldType - ): UpdateCount -} - -class VertxEntityFieldTypeParameterMapper : - VertxParameterColumnMapper { - override fun apply(fieldType: VertxMapperParameterRepository.Entity.FieldType?): Any? { - return fieldType?.code - } -} - diff --git a/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperRowRepository.kt b/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperRowRepository.kt deleted file mode 100644 index 82d09bc3..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperRowRepository.kt +++ /dev/null @@ -1,33 +0,0 @@ -package io.koraframework.kotlin.example.vertx - -import io.vertx.sqlclient.Row -import io.vertx.sqlclient.RowSet -import io.koraframework.common.annotation.Component -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Root -import io.koraframework.database.common.UpdateCount -import io.koraframework.database.common.annotation.Batch -import io.koraframework.database.common.annotation.Column -import io.koraframework.database.common.annotation.Query -import io.koraframework.database.common.annotation.Repository -import io.koraframework.database.vertx.VertxRepository -import io.koraframework.database.vertx.mapper.parameter.VertxParameterColumnMapper -import io.koraframework.database.vertx.mapper.result.VertxResultColumnMapper -import io.koraframework.database.vertx.mapper.result.VertxRowMapper -import io.koraframework.database.vertx.mapper.result.VertxRowSetMapper - -@Repository -interface VertxMapperRowRepository : VertxRepository { - @Mapping(VertxEntityPartRowMapper::class) - @Query("SELECT id, value1 FROM entities") - fun findAllParts(): List -} - -data class VertxEntityPart(val id: String, val field1: Int) - -class VertxEntityPartRowMapper : VertxRowMapper { - override fun apply(row: Row): VertxEntityPart { - return VertxEntityPart(row.get(String::class.java, 0), row.get(Integer::class.java, 1).toInt()) - } -} - diff --git a/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperRowSetRepository.kt b/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperRowSetRepository.kt deleted file mode 100644 index 4ce16de3..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/src/main/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperRowSetRepository.kt +++ /dev/null @@ -1,36 +0,0 @@ -package io.koraframework.kotlin.example.vertx - -import io.vertx.sqlclient.Row -import io.vertx.sqlclient.RowSet -import io.koraframework.common.annotation.Component -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Root -import io.koraframework.database.common.UpdateCount -import io.koraframework.database.common.annotation.Batch -import io.koraframework.database.common.annotation.Column -import io.koraframework.database.common.annotation.Query -import io.koraframework.database.common.annotation.Repository -import io.koraframework.database.vertx.VertxRepository -import io.koraframework.database.vertx.mapper.parameter.VertxParameterColumnMapper -import io.koraframework.database.vertx.mapper.result.VertxResultColumnMapper -import io.koraframework.database.vertx.mapper.result.VertxRowMapper -import io.koraframework.database.vertx.mapper.result.VertxRowSetMapper - -@Repository -interface VertxMapperRowSetRepository : VertxRepository { - @Mapping(VertxEntityPartRowSetMapper::class) - @Query("SELECT id, value1 FROM entities") - fun findAllParts(): Map> -} - -class VertxEntityPartRowSetMapper : VertxRowSetMapper>> { - override fun apply(rows: RowSet): Map> { - val result = LinkedHashMap>(rows.size()) - for (row in rows) { - val entityPart = VertxEntityPart(row.getString(0), row.getInteger(1)) - result.computeIfAbsent(entityPart.field1) { ArrayList() }.add(entityPart) - } - return result - } -} - diff --git a/examples/kotlin/kora-kotlin-database-vertx/src/main/resources/application.conf b/examples/kotlin/kora-kotlin-database-vertx/src/main/resources/application.conf deleted file mode 100644 index f4ab9e4c..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/src/main/resources/application.conf +++ /dev/null @@ -1,15 +0,0 @@ -db { - connectionUri = ${POSTGRES_VERTX_URI} - username = ${POSTGRES_USER} - password = ${POSTGRES_PASS} - maxPoolSize = 10 - poolName = "kora" - telemetry.logging.enabled = true -} - - -logging.level { - "root": "WARN" - "io.koraframework": "INFO" - "io.koraframework.example": "INFO" -} diff --git a/examples/kotlin/kora-kotlin-database-vertx/src/main/resources/db/migration/V1__setup-tables.sql b/examples/kotlin/kora-kotlin-database-vertx/src/main/resources/db/migration/V1__setup-tables.sql deleted file mode 100644 index 20ac2f10..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/src/main/resources/db/migration/V1__setup-tables.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE IF NOT EXISTS entities -( - id VARCHAR NOT NULL, - value1 INT NOT NULL, - value2 VARCHAR NOT NULL, - value3 VARCHAR, - PRIMARY KEY (id) -); diff --git a/examples/kotlin/kora-kotlin-database-vertx/src/main/resources/logback.xml b/examples/kotlin/kora-kotlin-database-vertx/src/main/resources/logback.xml deleted file mode 100644 index 184638b5..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/src/main/resources/logback.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxCrudSuspendTests.kt b/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxCrudSuspendTests.kt deleted file mode 100644 index 7f967f10..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxCrudSuspendTests.kt +++ /dev/null @@ -1,44 +0,0 @@ -package io.koraframework.kotlin.example.vertx - -import io.goodforgod.testcontainers.extensions.ContainerMode -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection -import io.goodforgod.testcontainers.extensions.jdbc.Migration -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNull -import org.junit.jupiter.api.Test -import io.koraframework.test.extension.junit5.KoraAppTest -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier -import io.koraframework.test.extension.junit5.KoraConfigModification -import io.koraframework.test.extension.junit5.TestComponent - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD - ) -) -@KoraAppTest(Application::class) -class VertxCrudSuspendTests : KoraAppTestConfigModifier { - @ConnectionPostgreSQL - lateinit var connection: JdbcConnection - - @TestComponent - lateinit var repository: VertxCrudSuspendRepository - - override fun config(): KoraConfigModification = vertxConfig(connection) - - @Test - fun suspendSingleSuccess() = runBlocking { - repository.insert(VertxEntity("1", 1, "2", null)) - assertEquals(1, repository.findById("1")?.field1) - repository.update(VertxEntity("1", 5, "6", null)) - assertEquals("6", repository.findById("1")?.value2) - repository.deleteById("1") - assertNull(repository.findById("1")) - } -} diff --git a/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxCrudSyncTests.kt b/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxCrudSyncTests.kt deleted file mode 100644 index bbf052a1..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxCrudSyncTests.kt +++ /dev/null @@ -1,104 +0,0 @@ -package io.koraframework.kotlin.example.vertx - -import io.goodforgod.testcontainers.extensions.ContainerMode -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection -import io.goodforgod.testcontainers.extensions.jdbc.Migration -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test -import io.koraframework.test.extension.junit5.KoraAppTest -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier -import io.koraframework.test.extension.junit5.KoraConfigModification -import io.koraframework.test.extension.junit5.TestComponent - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD - ) -) -@KoraAppTest(Application::class) -class VertxCrudSyncTests : KoraAppTestConfigModifier { - @ConnectionPostgreSQL - lateinit var connection: JdbcConnection - - @TestComponent - lateinit var repository: VertxCrudSyncRepository - - override fun config(): KoraConfigModification = vertxConfig(connection) - - @Test - fun singleSuccess() { - val entityCreate = VertxEntity("1", 1, "2", null) - repository.insert(entityCreate) - - val foundCreated = repository.findById("1") - assertNotNull(foundCreated) - assertEquals("1", foundCreated?.id) - assertEquals(1, foundCreated?.field1) - assertEquals("2", foundCreated?.value2) - assertNull(foundCreated?.value3) - - val entityUpdate = VertxEntity("1", 5, "6", null) - repository.update(entityUpdate) - - val foundUpdated = repository.findById("1") - assertNotNull(foundUpdated) - assertEquals("1", foundUpdated?.id) - assertEquals(5, foundUpdated?.field1) - assertEquals("6", foundUpdated?.value2) - assertNull(foundUpdated?.value3) - - repository.deleteById("1") - assertNull(repository.findById("1")) - } - - @Test - fun batchSuccess() { - val entity1 = VertxEntity("1", 1, "2", null) - val entity2 = VertxEntity("2", 3, "4", null) - assertEquals(2L, repository.insertBatch(listOf(entity1, entity2)).value()) - - val foundCreated = repository.findAll() - assertEquals(2, foundCreated.size) - for (entity in foundCreated) { - if (entity.id == "1") { - assertEquals("1", entity.id) - assertEquals(1, entity.field1) - assertEquals("2", entity.value2) - assertNull(entity.value3) - } else { - assertEquals("2", entity.id) - assertEquals(3, entity.field1) - assertEquals("4", entity.value2) - assertNull(entity.value3) - } - } - - val entityUpdate1 = VertxEntity("1", 5, "6", null) - val entityUpdate2 = VertxEntity("2", 7, "8", null) - assertEquals(2L, repository.updateBatch(listOf(entityUpdate1, entityUpdate2)).value()) - - val foundUpdated = repository.findAll() - assertEquals(2, foundUpdated.size) - for (entity in foundUpdated) { - if (entity.id == "1") { - assertEquals("1", entity.id) - assertEquals(5, entity.field1) - assertEquals("6", entity.value2) - assertNull(entity.value3) - } else { - assertEquals("2", entity.id) - assertEquals(7, entity.field1) - assertEquals("8", entity.value2) - assertNull(entity.value3) - } - } - - assertEquals(2L, repository.deleteAll().value()) - assertTrue(repository.findAll().isEmpty()) - } -} diff --git a/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperColumnTests.kt b/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperColumnTests.kt deleted file mode 100644 index 5223b7c2..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperColumnTests.kt +++ /dev/null @@ -1,55 +0,0 @@ -package io.koraframework.kotlin.example.vertx - -import io.goodforgod.testcontainers.extensions.ContainerMode -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection -import io.goodforgod.testcontainers.extensions.jdbc.Migration -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import io.koraframework.test.extension.junit5.KoraAppTest -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier -import io.koraframework.test.extension.junit5.KoraConfigModification -import io.koraframework.test.extension.junit5.TestComponent - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD - ) -) -@KoraAppTest(Application::class) -class VertxMapperColumnTests : KoraAppTestConfigModifier { - @ConnectionPostgreSQL - lateinit var connection: JdbcConnection - - @TestComponent - lateinit var crudRepository: VertxCrudSyncRepository - - @TestComponent - lateinit var mapperRepository: VertxMapperColumnRepository - - override fun config(): KoraConfigModification = vertxConfig(connection) - - @Test - fun columnMapperSuccess() { - val entity1 = VertxEntity("1", 1, "2", null) - val entity2 = VertxEntity("2", 3, "4", null) - - assertEquals(2, crudRepository.insertBatch(listOf(entity1, entity2)).value()) - - val parts = mapperRepository.findAll() - assertEquals(2, parts.size) - for (part in parts) { - if (part.id == "1") { - assertEquals("1", part.id) - assertEquals(VertxMapperColumnRepository.Entity.FieldType.ONE, part.field1) - } else { - assertEquals("2", part.id) - assertEquals(VertxMapperColumnRepository.Entity.FieldType.UNKNOWN, part.field1) - } - } - } -} diff --git a/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperParameterTests.kt b/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperParameterTests.kt deleted file mode 100644 index 1b41a7be..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperParameterTests.kt +++ /dev/null @@ -1,49 +0,0 @@ -package io.koraframework.kotlin.example.vertx - -import io.goodforgod.testcontainers.extensions.ContainerMode -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection -import io.goodforgod.testcontainers.extensions.jdbc.Migration -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Test -import io.koraframework.test.extension.junit5.KoraAppTest -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier -import io.koraframework.test.extension.junit5.KoraConfigModification -import io.koraframework.test.extension.junit5.TestComponent - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD - ) -) -@KoraAppTest(Application::class) -class VertxMapperParameterTests : KoraAppTestConfigModifier { - @ConnectionPostgreSQL - lateinit var connection: JdbcConnection - - @TestComponent - lateinit var crudRepository: VertxCrudSyncRepository - - @TestComponent - lateinit var mapperRepository: VertxMapperParameterRepository - - override fun config(): KoraConfigModification = vertxConfig(connection) - - @Test - fun parameterMapperSuccess() { - val entity1 = VertxEntity("1", VertxMapperParameterRepository.Entity.FieldType.ONE.code, "2", null) - val entity2 = VertxEntity("2", 3, "4", null) - assertEquals(2, crudRepository.insertBatch(listOf(entity1, entity2)).value()) - - mapperRepository.updateFieldType(entity1.id, VertxMapperParameterRepository.Entity.FieldType.TWO) - - val found = crudRepository.findById(entity1.id) - assertNotNull(found) - assertEquals(VertxMapperParameterRepository.Entity.FieldType.TWO.code, found?.field1) - } -} diff --git a/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperResultSetTests.kt b/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperResultSetTests.kt deleted file mode 100644 index 7b49077b..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperResultSetTests.kt +++ /dev/null @@ -1,55 +0,0 @@ -package io.koraframework.kotlin.example.vertx - -import io.goodforgod.testcontainers.extensions.ContainerMode -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection -import io.goodforgod.testcontainers.extensions.jdbc.Migration -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Test -import io.koraframework.test.extension.junit5.KoraAppTest -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier -import io.koraframework.test.extension.junit5.KoraConfigModification -import io.koraframework.test.extension.junit5.TestComponent - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD - ) -) -@KoraAppTest(Application::class) -class VertxMapperResultSetTests : KoraAppTestConfigModifier { - @ConnectionPostgreSQL - lateinit var connection: JdbcConnection - - @TestComponent - lateinit var crudRepository: VertxCrudSyncRepository - - @TestComponent - lateinit var mapperRepository: VertxMapperRowSetRepository - - override fun config(): KoraConfigModification = vertxConfig(connection) - - @Test - fun resultSetMapperSuccess() { - val entity1 = VertxEntity("1", 1, "2", null) - val entity2 = VertxEntity("2", 3, "4", null) - - assertEquals(2, crudRepository.insertBatch(listOf(entity1, entity2)).value()) - - val found = mapperRepository.findAllParts() - val fieldOne = found[1] - assertNotNull(fieldOne) - assertEquals(1, fieldOne?.size) - assertEquals(entity1.id, fieldOne?.get(0)?.id) - - val fieldUnknown = found[3] - assertNotNull(fieldUnknown) - assertEquals(1, fieldUnknown?.size) - assertEquals(entity2.id, fieldUnknown?.get(0)?.id) - } -} diff --git a/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperRowTests.kt b/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperRowTests.kt deleted file mode 100644 index f4df5525..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxMapperRowTests.kt +++ /dev/null @@ -1,46 +0,0 @@ -package io.koraframework.kotlin.example.vertx - -import io.goodforgod.testcontainers.extensions.ContainerMode -import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection -import io.goodforgod.testcontainers.extensions.jdbc.Migration -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import io.koraframework.test.extension.junit5.KoraAppTest -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier -import io.koraframework.test.extension.junit5.KoraConfigModification -import io.koraframework.test.extension.junit5.TestComponent - -@TestcontainersPostgreSQL( - mode = ContainerMode.PER_RUN, - migration = Migration( - engine = Migration.Engines.FLYWAY, - apply = Migration.Mode.PER_METHOD, - drop = Migration.Mode.PER_METHOD - ) -) -@KoraAppTest(Application::class) -class VertxMapperRowTests : KoraAppTestConfigModifier { - @ConnectionPostgreSQL - lateinit var connection: JdbcConnection - - @TestComponent - lateinit var crudRepository: VertxCrudSyncRepository - - @TestComponent - lateinit var mapperRepository: VertxMapperRowRepository - - override fun config(): KoraConfigModification = vertxConfig(connection) - - @Test - fun rowMapperSuccess() { - val entity1 = VertxEntity("1", 1, "2", null) - val entity2 = VertxEntity("2", 3, "4", null) - - assertEquals(2, crudRepository.insertBatch(listOf(entity1, entity2)).value()) - - val found = mapperRepository.findAllParts() - assertEquals(2, found.size) - } -} diff --git a/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxTestConfig.kt b/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxTestConfig.kt deleted file mode 100644 index fb7af176..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/src/test/kotlin/io/koraframework/kotlin/example/vertx/VertxTestConfig.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.koraframework.kotlin.example.vertx - -import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection -import io.koraframework.test.extension.junit5.KoraConfigModification - -fun vertxConfig(connection: JdbcConnection): KoraConfigModification { - val params = connection.params() - return KoraConfigModification - .ofSystemProperty("POSTGRES_VERTX_URI", "postgresql://${params.host()}:${params.port()}/${params.database()}") - .withSystemProperty("POSTGRES_USER", params.username()) - .withSystemProperty("POSTGRES_PASS", params.password()) -} diff --git a/examples/kotlin/kora-kotlin-database-vertx/src/test/resources/logback-test.xml b/examples/kotlin/kora-kotlin-database-vertx/src/test/resources/logback-test.xml deleted file mode 100644 index 0af9d2bc..00000000 --- a/examples/kotlin/kora-kotlin-database-vertx/src/test/resources/logback-test.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - UTF-8 - %cyan(%d{HH:mm:ss.SSS}) %highlight(%-5level) [%thread] %logger{36} - %msg%n - - - - - - - - - - - - - - - - - - - - diff --git a/examples/kotlin/kora-kotlin-grpc-client/build.gradle.kts b/examples/kotlin/kora-kotlin-grpc-client/build.gradle.kts index 22362ce5..2f892041 100644 --- a/examples/kotlin/kora-kotlin-grpc-client/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-grpc-client/build.gradle.kts @@ -9,20 +9,12 @@ plugins { id("com.google.protobuf") version ("0.10.0") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:grpc-client") - implementation("io.grpc:grpc-protobuf:1.74.0") + implementation("io.grpc:grpc-protobuf:1.83.1") implementation("javax.annotation:javax.annotation-api:1.3.2") implementation("io.koraframework:logging-logback") implementation("io.koraframework:config-hocon") @@ -41,7 +33,7 @@ kotlin { protobuf { protoc { artifact = "com.google.protobuf:protoc:3.25.3" } plugins { - id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.74.0" } + id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.83.1" } } generateProtoTasks { all().forEach { task -> task.plugins { id("grpc") } } diff --git a/examples/kotlin/kora-kotlin-grpc-client/src/main/kotlin/io/koraframework/kotlin/example/grpc/client/Application.kt b/examples/kotlin/kora-kotlin-grpc-client/src/main/kotlin/io/koraframework/kotlin/example/grpc/client/Application.kt index 7e150c41..bf518fc0 100644 --- a/examples/kotlin/kora-kotlin-grpc-client/src/main/kotlin/io/koraframework/kotlin/example/grpc/client/Application.kt +++ b/examples/kotlin/kora-kotlin-grpc-client/src/main/kotlin/io/koraframework/kotlin/example/grpc/client/Application.kt @@ -1,6 +1,6 @@ package io.koraframework.kotlin.example.grpc.client -import ru.tinkoff.grpc.client.GrpcClientModule +import io.koraframework.grpc.client.GrpcClientModule import io.koraframework.application.graph.KoraApplication import io.koraframework.common.annotation.KoraApp import io.koraframework.config.hocon.HoconConfigModule diff --git a/examples/kotlin/kora-kotlin-grpc-server/build.gradle.kts b/examples/kotlin/kora-kotlin-grpc-server/build.gradle.kts index e73d0791..854c2597 100644 --- a/examples/kotlin/kora-kotlin-grpc-server/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-grpc-server/build.gradle.kts @@ -9,27 +9,19 @@ plugins { id("com.google.protobuf") version ("0.10.0") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:grpc-server") - implementation("io.grpc:grpc-protobuf:1.74.0") + implementation("io.grpc:grpc-protobuf:1.83.1") implementation("javax.annotation:javax.annotation-api:1.3.2") implementation("io.koraframework:logging-logback") implementation("io.koraframework:config-hocon") testImplementation("io.koraframework:test-junit5") testImplementation("org.testcontainers:junit-jupiter:1.21.4") - testImplementation("io.grpc:grpc-testing:1.74.0") + testImplementation("io.grpc:grpc-testing:1.83.1") } kotlin { @@ -42,7 +34,7 @@ kotlin { protobuf { protoc { artifact = "com.google.protobuf:protoc:3.25.3" } plugins { - id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.74.0" } + id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.83.1" } } generateProtoTasks { all().forEach { task -> task.plugins { id("grpc") } } diff --git a/examples/kotlin/kora-kotlin-helloworld/build.gradle.kts b/examples/kotlin/kora-kotlin-helloworld/build.gradle.kts index 505dc9b9..a0b1c065 100644 --- a/examples/kotlin/kora-kotlin-helloworld/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-helloworld/build.gradle.kts @@ -7,17 +7,10 @@ plugins { id("com.google.devtools.ksp") version ("2.3.11") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) -} - val koraVersion: String by project dependencies { - koraBom(platform("io.koraframework:kora-parent:$koraVersion")) - - ksp("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") diff --git a/examples/kotlin/kora-kotlin-helloworld/src/main/resources/application.conf b/examples/kotlin/kora-kotlin-helloworld/src/main/resources/application.conf index 8b9b1bda..6391449b 100644 --- a/examples/kotlin/kora-kotlin-helloworld/src/main/resources/application.conf +++ b/examples/kotlin/kora-kotlin-helloworld/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } diff --git a/examples/kotlin/kora-kotlin-http-client/build.gradle.kts b/examples/kotlin/kora-kotlin-http-client/build.gradle.kts index faf01da7..8a41853d 100644 --- a/examples/kotlin/kora-kotlin-http-client/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-http-client/build.gradle.kts @@ -7,26 +7,17 @@ plugins { id("com.google.devtools.ksp") version ("2.3.11") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:http-client-jdk") implementation("io.koraframework:json-common") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") implementation("io.koraframework:logging-logback") implementation("io.koraframework:config-hocon") testImplementation("io.koraframework:test-junit5") - testImplementation("io.goodforgod:testcontainers-extensions-mockserver:0.13.1") + testImplementation("io.goodforgod:testcontainers-extensions-mockserver:0.15.0") } kotlin { diff --git a/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/FormHttpClient.kt b/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/FormHttpClient.kt index 01d238c8..f11f221e 100644 --- a/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/FormHttpClient.kt +++ b/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/FormHttpClient.kt @@ -2,7 +2,6 @@ import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component -import io.koraframework.common.Context import io.koraframework.common.annotation.Mapping import io.koraframework.common.annotation.Root import io.koraframework.http.client.common.annotation.HttpClient @@ -22,9 +21,8 @@ import io.koraframework.http.common.form.FormMultipart import io.koraframework.http.common.form.FormUrlEncoded import io.koraframework.json.common.annotation.Json import java.nio.charset.StandardCharsets -import java.util.concurrent.CompletionStage -@HttpClient(configPath = "httpClient.default") +@HttpClient("httpClient.default") interface FormHttpClient { @HttpRoute(method = HttpMethod.POST, path = "/form/encoded") fun formEncoded(body: FormUrlEncoded): HttpResponseEntity diff --git a/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/InterceptedHttpClient.kt b/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/InterceptedHttpClient.kt index 7709640c..97c7decc 100644 --- a/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/InterceptedHttpClient.kt +++ b/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/InterceptedHttpClient.kt @@ -1,55 +1,37 @@ -package io.koraframework.kotlin.example.http.client +package io.koraframework.kotlin.example.http.client import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component -import io.koraframework.common.Context -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Root import io.koraframework.http.client.common.annotation.HttpClient -import io.koraframework.http.client.common.annotation.ResponseCodeMapper -import io.koraframework.http.client.common.annotation.ResponseCodeMapper.DEFAULT import io.koraframework.http.client.common.interceptor.HttpClientInterceptor import io.koraframework.http.client.common.request.HttpClientRequest -import io.koraframework.http.client.common.request.HttpClientRequestMapper import io.koraframework.http.client.common.response.HttpClientResponse -import io.koraframework.http.client.common.response.HttpClientResponseMapper import io.koraframework.http.common.HttpMethod import io.koraframework.http.common.HttpResponseEntity -import io.koraframework.http.common.annotation.* -import io.koraframework.http.common.body.HttpBody -import io.koraframework.http.common.body.HttpBodyOutput -import io.koraframework.http.common.form.FormMultipart -import io.koraframework.http.common.form.FormUrlEncoded -import io.koraframework.json.common.annotation.Json -import java.nio.charset.StandardCharsets -import java.util.concurrent.CompletionStage +import io.koraframework.http.common.annotation.HttpRoute +import io.koraframework.http.common.annotation.InterceptWith @InterceptWith(InterceptedHttpClient.ClientInterceptor::class) -@HttpClient(configPath = "httpClient.default") +@HttpClient("httpClient.default") interface InterceptedHttpClient { + + @Component class ClientInterceptor : HttpClientInterceptor { private val logger = LoggerFactory.getLogger(ClientInterceptor::class.java) - override fun processRequest( - ctx: Context, - chain: HttpClientInterceptor.InterceptChain, - request: HttpClientRequest - ): CompletionStage { + override fun processRequest(chain: HttpClientInterceptor.InterceptChain, request: HttpClientRequest): HttpClientResponse { logger.info("Client Level Interceptor") - return chain.process(ctx, request) + return chain.process(request) } } + @Component class MethodInterceptor : HttpClientInterceptor { private val logger = LoggerFactory.getLogger(MethodInterceptor::class.java) - override fun processRequest( - ctx: Context, - chain: HttpClientInterceptor.InterceptChain, - request: HttpClientRequest - ): CompletionStage { + override fun processRequest(chain: HttpClientInterceptor.InterceptChain, request: HttpClientRequest): HttpClientResponse { logger.info("Method Level Interceptor") - return chain.process(ctx, request) + return chain.process(request) } } @@ -57,4 +39,3 @@ interface InterceptedHttpClient { @HttpRoute(method = HttpMethod.GET, path = "/intercepted") fun get(): HttpResponseEntity } - diff --git a/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/JsonHttpClient.kt b/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/JsonHttpClient.kt index 7132ba79..29d9ee53 100644 --- a/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/JsonHttpClient.kt +++ b/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/JsonHttpClient.kt @@ -2,7 +2,6 @@ import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component -import io.koraframework.common.Context import io.koraframework.common.annotation.Mapping import io.koraframework.common.annotation.Root import io.koraframework.http.client.common.annotation.HttpClient @@ -22,9 +21,8 @@ import io.koraframework.http.common.form.FormMultipart import io.koraframework.http.common.form.FormUrlEncoded import io.koraframework.json.common.annotation.Json import java.nio.charset.StandardCharsets -import java.util.concurrent.CompletionStage -@HttpClient(configPath = "httpClient.default") +@HttpClient("httpClient.default") interface JsonHttpClient { @Json data class JsonRequest(val id: String) diff --git a/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/MapperRequestHttpClient.kt b/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/MapperRequestHttpClient.kt index 06fedbdf..c4a7e692 100644 --- a/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/MapperRequestHttpClient.kt +++ b/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/MapperRequestHttpClient.kt @@ -1,38 +1,24 @@ -package io.koraframework.kotlin.example.http.client +package io.koraframework.kotlin.example.http.client -import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component -import io.koraframework.common.Context import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Root import io.koraframework.http.client.common.annotation.HttpClient -import io.koraframework.http.client.common.annotation.ResponseCodeMapper -import io.koraframework.http.client.common.annotation.ResponseCodeMapper.DEFAULT -import io.koraframework.http.client.common.interceptor.HttpClientInterceptor -import io.koraframework.http.client.common.request.HttpClientRequest import io.koraframework.http.client.common.request.HttpClientRequestMapper -import io.koraframework.http.client.common.response.HttpClientResponse -import io.koraframework.http.client.common.response.HttpClientResponseMapper import io.koraframework.http.common.HttpMethod import io.koraframework.http.common.HttpResponseEntity -import io.koraframework.http.common.annotation.* +import io.koraframework.http.common.annotation.HttpRoute import io.koraframework.http.common.body.HttpBody import io.koraframework.http.common.body.HttpBodyOutput -import io.koraframework.http.common.form.FormMultipart -import io.koraframework.http.common.form.FormUrlEncoded -import io.koraframework.json.common.annotation.Json -import java.nio.charset.StandardCharsets -import java.util.concurrent.CompletionStage -@HttpClient(configPath = "httpClient.default") +@HttpClient("httpClient.default") interface MapperRequestHttpClient { data class UserBody(val id: String) + @Component class UserRequestMapper : HttpClientRequestMapper { - override fun apply(ctx: Context, value: UserBody): HttpBodyOutput = HttpBody.plaintext(value.id) + override fun apply(value: UserBody): HttpBodyOutput = HttpBody.plaintext(value.id) } @HttpRoute(method = HttpMethod.POST, path = "/mapping_request") fun post(@Mapping(UserRequestMapper::class) request: UserBody): HttpResponseEntity } - diff --git a/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/MapperResponseHttpClient.kt b/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/MapperResponseHttpClient.kt index 92289c80..43e0b065 100644 --- a/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/MapperResponseHttpClient.kt +++ b/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/MapperResponseHttpClient.kt @@ -1,31 +1,20 @@ -package io.koraframework.kotlin.example.http.client +package io.koraframework.kotlin.example.http.client -import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component -import io.koraframework.common.Context -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Root import io.koraframework.http.client.common.annotation.HttpClient import io.koraframework.http.client.common.annotation.ResponseCodeMapper import io.koraframework.http.client.common.annotation.ResponseCodeMapper.DEFAULT -import io.koraframework.http.client.common.interceptor.HttpClientInterceptor -import io.koraframework.http.client.common.request.HttpClientRequest -import io.koraframework.http.client.common.request.HttpClientRequestMapper import io.koraframework.http.client.common.response.HttpClientResponse import io.koraframework.http.client.common.response.HttpClientResponseMapper import io.koraframework.http.common.HttpMethod -import io.koraframework.http.common.HttpResponseEntity -import io.koraframework.http.common.annotation.* -import io.koraframework.http.common.body.HttpBody -import io.koraframework.http.common.body.HttpBodyOutput -import io.koraframework.http.common.form.FormMultipart -import io.koraframework.http.common.form.FormUrlEncoded -import io.koraframework.json.common.annotation.Json +import io.koraframework.http.common.annotation.HttpRoute +import io.koraframework.http.common.annotation.Path import java.nio.charset.StandardCharsets -import java.util.concurrent.CompletionStage -@HttpClient(configPath = "httpClient.default") +@HttpClient("httpClient.default") interface MapperResponseHttpClient { + + @Component class ResponseSuccessMapper : HttpClientResponseMapper { override fun apply(response: HttpClientResponse): UserResponse { response.body().asInputStream().use { @@ -35,6 +24,7 @@ interface MapperResponseHttpClient { } } + @Component class ResponseErrorMapper : HttpClientResponseMapper { override fun apply(response: HttpClientResponse): UserResponse { response.body().asInputStream().use { @@ -54,4 +44,3 @@ interface MapperResponseHttpClient { @HttpRoute(method = HttpMethod.GET, path = "/mapping_by_code/{code}") fun get(@Path code: String): UserResponse } - diff --git a/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/ParametersHttpClient.kt b/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/ParametersHttpClient.kt index b0bcd891..a17a7207 100644 --- a/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/ParametersHttpClient.kt +++ b/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/ParametersHttpClient.kt @@ -2,7 +2,6 @@ import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component -import io.koraframework.common.Context import io.koraframework.common.annotation.Mapping import io.koraframework.common.annotation.Root import io.koraframework.http.client.common.annotation.HttpClient @@ -22,9 +21,8 @@ import io.koraframework.http.common.form.FormMultipart import io.koraframework.http.common.form.FormUrlEncoded import io.koraframework.json.common.annotation.Json import java.nio.charset.StandardCharsets -import java.util.concurrent.CompletionStage -@HttpClient(configPath = "httpClient.default") +@HttpClient("httpClient.default") interface ParametersHttpClient { @HttpRoute(method = HttpMethod.POST, path = "/parameters/{path}") fun post( diff --git a/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/RootService.kt b/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/RootService.kt index a8619284..6f4e293c 100644 --- a/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/RootService.kt +++ b/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/RootService.kt @@ -2,7 +2,6 @@ import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component -import io.koraframework.common.Context import io.koraframework.common.annotation.Mapping import io.koraframework.common.annotation.Root import io.koraframework.http.client.common.annotation.HttpClient @@ -22,7 +21,6 @@ import io.koraframework.http.common.form.FormMultipart import io.koraframework.http.common.form.FormUrlEncoded import io.koraframework.json.common.annotation.Json import java.nio.charset.StandardCharsets -import java.util.concurrent.CompletionStage @Root @Component @@ -32,8 +30,6 @@ class RootService( private val mapperRequestHttpClient: MapperRequestHttpClient, private val mapperResponseHttpClient: MapperResponseHttpClient, private val parametersHttpClient: ParametersHttpClient, - private val suspendHttpClient: SuspendHttpClient, private val voidHttpClient: VoidHttpClient, private val formHttpClient: FormHttpClient ) - diff --git a/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/SuspendHttpClient.kt b/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/SuspendHttpClient.kt deleted file mode 100644 index 99fa6159..00000000 --- a/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/SuspendHttpClient.kt +++ /dev/null @@ -1,36 +0,0 @@ -package io.koraframework.kotlin.example.http.client - -import org.slf4j.LoggerFactory -import io.koraframework.common.annotation.Component -import io.koraframework.common.Context -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Root -import io.koraframework.http.client.common.annotation.HttpClient -import io.koraframework.http.client.common.annotation.ResponseCodeMapper -import io.koraframework.http.client.common.annotation.ResponseCodeMapper.DEFAULT -import io.koraframework.http.client.common.interceptor.HttpClientInterceptor -import io.koraframework.http.client.common.request.HttpClientRequest -import io.koraframework.http.client.common.request.HttpClientRequestMapper -import io.koraframework.http.client.common.response.HttpClientResponse -import io.koraframework.http.client.common.response.HttpClientResponseMapper -import io.koraframework.http.common.HttpMethod -import io.koraframework.http.common.HttpResponseEntity -import io.koraframework.http.common.annotation.* -import io.koraframework.http.common.body.HttpBody -import io.koraframework.http.common.body.HttpBodyOutput -import io.koraframework.http.common.form.FormMultipart -import io.koraframework.http.common.form.FormUrlEncoded -import io.koraframework.json.common.annotation.Json -import java.nio.charset.StandardCharsets -import java.util.concurrent.CompletionStage - -@HttpClient(configPath = "httpClient.default") -interface SuspendHttpClient { - @HttpRoute(method = HttpMethod.GET, path = "/suspend/{path}") - suspend fun get( - @Path path: String, - @Query query: String?, - @Header header: String? - ): HttpResponseEntity -} - diff --git a/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/VoidHttpClient.kt b/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/VoidHttpClient.kt index 6869d33c..80b7510a 100644 --- a/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/VoidHttpClient.kt +++ b/examples/kotlin/kora-kotlin-http-client/src/main/kotlin/io/koraframework/kotlin/example/http/client/VoidHttpClient.kt @@ -1,35 +1,12 @@ -package io.koraframework.kotlin.example.http.client +package io.koraframework.kotlin.example.http.client -import org.slf4j.LoggerFactory -import io.koraframework.common.annotation.Component -import io.koraframework.common.Context -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Root import io.koraframework.http.client.common.annotation.HttpClient -import io.koraframework.http.client.common.annotation.ResponseCodeMapper -import io.koraframework.http.client.common.annotation.ResponseCodeMapper.DEFAULT -import io.koraframework.http.client.common.interceptor.HttpClientInterceptor -import io.koraframework.http.client.common.request.HttpClientRequest -import io.koraframework.http.client.common.request.HttpClientRequestMapper -import io.koraframework.http.client.common.response.HttpClientResponse -import io.koraframework.http.client.common.response.HttpClientResponseMapper import io.koraframework.http.common.HttpMethod -import io.koraframework.http.common.HttpResponseEntity -import io.koraframework.http.common.annotation.* -import io.koraframework.http.common.body.HttpBody -import io.koraframework.http.common.body.HttpBodyOutput -import io.koraframework.http.common.form.FormMultipart -import io.koraframework.http.common.form.FormUrlEncoded -import io.koraframework.json.common.annotation.Json -import java.nio.charset.StandardCharsets -import java.util.concurrent.CompletionStage +import io.koraframework.http.common.annotation.HttpRoute -@HttpClient(configPath = "httpClient.default") +@HttpClient("httpClient.default") interface VoidHttpClient { - @HttpRoute(method = HttpMethod.POST, path = "/void") - fun sync() @HttpRoute(method = HttpMethod.POST, path = "/void") - suspend fun suspendRequest() + fun sync() } - diff --git a/examples/kotlin/kora-kotlin-http-client/src/test/kotlin/io/koraframework/kotlin/example/http/client/FormHttpClientTests.kt b/examples/kotlin/kora-kotlin-http-client/src/test/kotlin/io/koraframework/kotlin/example/http/client/FormHttpClientTests.kt index 011dc54b..8c7c447b 100644 --- a/examples/kotlin/kora-kotlin-http-client/src/test/kotlin/io/koraframework/kotlin/example/http/client/FormHttpClientTests.kt +++ b/examples/kotlin/kora-kotlin-http-client/src/test/kotlin/io/koraframework/kotlin/example/http/client/FormHttpClientTests.kt @@ -57,7 +57,7 @@ class FormHttpClientTests : KoraAppTestConfigModifier { val requestBody = """ --blob:.*\r content-disposition: form-data; name="field1"\r - content-type: text/plain; charset=utf-8\r + content-type: text/plain;charset=utf-8\r \r some data content\r --blob:.*\r diff --git a/examples/kotlin/kora-kotlin-http-client/src/test/kotlin/io/koraframework/kotlin/example/http/client/SuspendHttpClientTests.kt b/examples/kotlin/kora-kotlin-http-client/src/test/kotlin/io/koraframework/kotlin/example/http/client/SuspendHttpClientTests.kt deleted file mode 100644 index 12dc7fe9..00000000 --- a/examples/kotlin/kora-kotlin-http-client/src/test/kotlin/io/koraframework/kotlin/example/http/client/SuspendHttpClientTests.kt +++ /dev/null @@ -1,41 +0,0 @@ -package io.koraframework.kotlin.example.http.client - -import io.goodforgod.testcontainers.extensions.ContainerMode -import io.goodforgod.testcontainers.extensions.mockserver.ConnectionMockServer -import io.goodforgod.testcontainers.extensions.mockserver.MockServerConnection -import io.goodforgod.testcontainers.extensions.mockserver.TestcontainersMockServer -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.Assertions.assertArrayEquals -import org.junit.jupiter.api.Test -import org.mockserver.model.HttpRequest.request -import org.mockserver.model.HttpResponse.response -import io.koraframework.test.extension.junit5.KoraAppTest -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier -import io.koraframework.test.extension.junit5.KoraConfigModification -import io.koraframework.test.extension.junit5.TestComponent - -@TestcontainersMockServer(mode = ContainerMode.PER_RUN) -@KoraAppTest(Application::class) -class SuspendHttpClientTests : KoraAppTestConfigModifier { - @ConnectionMockServer - lateinit var mockserverConnection: MockServerConnection - - @TestComponent - lateinit var httpClient: SuspendHttpClient - - override fun config(): KoraConfigModification = - KoraConfigModification.ofSystemProperty("HTTP_CLIENT_URL", mockserverConnection.params().uri().toString()) - - @Test - fun suspendHttpClient() = runBlocking { - mockserverConnection.client().`when`( - request() - .withMethod("GET") - .withPath("/suspend/path") - .withQueryStringParameter("query", "query") - .withHeader("header", "header") - ).respond(response().withBody("Hello world")) - - assertArrayEquals("Hello world".toByteArray(), httpClient.get("path", "query", "header").body()) - } -} diff --git a/examples/kotlin/kora-kotlin-http-client/src/test/kotlin/io/koraframework/kotlin/example/http/client/VoidHttpClientTests.kt b/examples/kotlin/kora-kotlin-http-client/src/test/kotlin/io/koraframework/kotlin/example/http/client/VoidHttpClientTests.kt index 114050a8..8384a13d 100644 --- a/examples/kotlin/kora-kotlin-http-client/src/test/kotlin/io/koraframework/kotlin/example/http/client/VoidHttpClientTests.kt +++ b/examples/kotlin/kora-kotlin-http-client/src/test/kotlin/io/koraframework/kotlin/example/http/client/VoidHttpClientTests.kt @@ -4,7 +4,6 @@ import io.goodforgod.testcontainers.extensions.ContainerMode import io.goodforgod.testcontainers.extensions.mockserver.ConnectionMockServer import io.goodforgod.testcontainers.extensions.mockserver.MockServerConnection import io.goodforgod.testcontainers.extensions.mockserver.TestcontainersMockServer -import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test import org.mockserver.model.HttpRequest.request import org.mockserver.model.HttpResponse.response @@ -26,17 +25,6 @@ class VoidHttpClientTests : KoraAppTestConfigModifier { override fun config(): KoraConfigModification = KoraConfigModification.ofSystemProperty("HTTP_CLIENT_URL", mockserverConnection.params().uri().toString()) - @Test - fun voidPostSuspendRequestSuccess() = runBlocking { - mockserverConnection.client().`when`( - request() - .withMethod("POST") - .withPath("/void") - ).respond(response().withBody("OK")) - - httpClient.suspendRequest() - } - @Test fun voidPostSyncRequestSuccess() { mockserverConnection.client().`when`( diff --git a/examples/kotlin/kora-kotlin-http-server/build.gradle.kts b/examples/kotlin/kora-kotlin-http-server/build.gradle.kts index 633a1df3..3e26128b 100644 --- a/examples/kotlin/kora-kotlin-http-server/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-http-server/build.gradle.kts @@ -7,22 +7,14 @@ plugins { id("com.google.devtools.ksp") version ("2.3.11") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") + kspTest("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:validation-module") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") implementation("io.koraframework:config-hocon") implementation("io.koraframework:logging-logback") diff --git a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/ErrorHandlerController.kt b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/ErrorHandlerController.kt index 9a6385aa..1bc0260b 100644 --- a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/ErrorHandlerController.kt +++ b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/ErrorHandlerController.kt @@ -1,26 +1,18 @@ -package io.koraframework.kotlin.example.http.server +package io.koraframework.kotlin.example.http.server -import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component -import io.koraframework.common.Context -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Tag import io.koraframework.http.common.HttpMethod -import io.koraframework.http.common.HttpResponseEntity -import io.koraframework.http.common.annotation.* +import io.koraframework.http.common.annotation.HttpRoute +import io.koraframework.http.common.annotation.InterceptWith +import io.koraframework.http.common.annotation.Path import io.koraframework.http.common.body.HttpBody -import io.koraframework.http.common.form.FormMultipart -import io.koraframework.http.common.header.HttpHeaders -import io.koraframework.http.server.common.* import io.koraframework.http.server.common.annotation.HttpController -import io.koraframework.http.server.common.request.HttpServerRequestMapper -import io.koraframework.http.server.common.response.HttpServerResponseMapper +import io.koraframework.http.server.common.interceptor.HttpServerInterceptor +import io.koraframework.http.server.common.request.HttpServerRequest +import io.koraframework.http.server.common.response.HttpServerResponse +import io.koraframework.http.server.common.response.HttpServerResponseException import io.koraframework.json.common.JsonWriter import io.koraframework.json.common.annotation.Json -import io.koraframework.validation.common.annotation.Pattern -import io.koraframework.validation.common.annotation.Size -import io.koraframework.validation.common.annotation.Validate -import java.util.concurrent.CompletionStage @InterceptWith(ErrorHandlerController.ErrorHandlerInterceptor::class) @Component @@ -31,14 +23,13 @@ class ErrorHandlerController { @Component class ErrorHandlerInterceptor(private val errorJsonWriter: JsonWriter) : HttpServerInterceptor { - override fun intercept( - context: Context, - request: HttpServerRequest, - chain: HttpServerInterceptor.InterceptChain - ): CompletionStage { - return chain.process(context, request).exceptionally { e -> + override fun intercept(request: HttpServerRequest, chain: HttpServerInterceptor.InterceptChain): HttpServerResponse { + try { + return chain.process(request) + } catch (e: Exception) { + // HttpServerResponseException is itself a response, so it is returned as the client already sees it if (e is HttpServerResponseException) { - return@exceptionally e + return e } val code: Int @@ -50,7 +41,7 @@ class ErrorHandlerController { code = 500 error = Error("1", "INTERNAL_ERROR", e.message) } - HttpServerResponse.of(code, HttpBody.json(errorJsonWriter.toByteArray(error))) + return HttpServerResponse.of(code, HttpBody.json(errorJsonWriter.toByteArray(error))) } } } @@ -63,4 +54,3 @@ class ErrorHandlerController { return HttpServerResponse.of(200, HttpBody.plaintext("Hello world")) } } - diff --git a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/GetParametersController.kt b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/GetParametersController.kt index fe7a251d..301386d3 100644 --- a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/GetParametersController.kt +++ b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/GetParametersController.kt @@ -1,26 +1,15 @@ -package io.koraframework.kotlin.example.http.server +package io.koraframework.kotlin.example.http.server -import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component -import io.koraframework.common.Context -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Tag import io.koraframework.http.common.HttpMethod -import io.koraframework.http.common.HttpResponseEntity -import io.koraframework.http.common.annotation.* +import io.koraframework.http.common.annotation.Header +import io.koraframework.http.common.annotation.HttpRoute +import io.koraframework.http.common.annotation.Path +import io.koraframework.http.common.annotation.Query import io.koraframework.http.common.body.HttpBody -import io.koraframework.http.common.form.FormMultipart import io.koraframework.http.common.header.HttpHeaders -import io.koraframework.http.server.common.* import io.koraframework.http.server.common.annotation.HttpController -import io.koraframework.http.server.common.request.HttpServerRequestMapper -import io.koraframework.http.server.common.response.HttpServerResponseMapper -import io.koraframework.json.common.JsonWriter -import io.koraframework.json.common.annotation.Json -import io.koraframework.validation.common.annotation.Pattern -import io.koraframework.validation.common.annotation.Size -import io.koraframework.validation.common.annotation.Validate -import java.util.concurrent.CompletionStage +import io.koraframework.http.server.common.response.HttpServerResponse @Component @HttpController @@ -37,4 +26,3 @@ class GetParametersController { return HttpServerResponse.of(200, HttpHeaders.of("headerName", "headerValue"), HttpBody.plaintext(body)) } } - diff --git a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/GetRequestController.kt b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/GetRequestController.kt index 70b15b14..2fc13454 100644 --- a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/GetRequestController.kt +++ b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/GetRequestController.kt @@ -1,26 +1,12 @@ -package io.koraframework.kotlin.example.http.server +package io.koraframework.kotlin.example.http.server -import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component -import io.koraframework.common.Context -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Tag import io.koraframework.http.common.HttpMethod -import io.koraframework.http.common.HttpResponseEntity -import io.koraframework.http.common.annotation.* +import io.koraframework.http.common.annotation.HttpRoute import io.koraframework.http.common.body.HttpBody -import io.koraframework.http.common.form.FormMultipart -import io.koraframework.http.common.header.HttpHeaders -import io.koraframework.http.server.common.* import io.koraframework.http.server.common.annotation.HttpController -import io.koraframework.http.server.common.request.HttpServerRequestMapper -import io.koraframework.http.server.common.response.HttpServerResponseMapper -import io.koraframework.json.common.JsonWriter -import io.koraframework.json.common.annotation.Json -import io.koraframework.validation.common.annotation.Pattern -import io.koraframework.validation.common.annotation.Size -import io.koraframework.validation.common.annotation.Validate -import java.util.concurrent.CompletionStage +import io.koraframework.http.server.common.request.HttpServerRequest +import io.koraframework.http.server.common.response.HttpServerResponse @Component @HttpController @@ -35,4 +21,3 @@ class GetRequestController { return HttpServerResponse.of(200, HttpBody.plaintext(body)) } } - diff --git a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/InterceptedController.kt b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/InterceptedController.kt index 092414af..5d435598 100644 --- a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/InterceptedController.kt +++ b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/InterceptedController.kt @@ -1,69 +1,51 @@ -package io.koraframework.kotlin.example.http.server +package io.koraframework.kotlin.example.http.server import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component -import io.koraframework.common.Context -import io.koraframework.common.annotation.Mapping import io.koraframework.common.annotation.Tag import io.koraframework.http.common.HttpMethod -import io.koraframework.http.common.HttpResponseEntity -import io.koraframework.http.common.annotation.* +import io.koraframework.http.common.annotation.HttpRoute +import io.koraframework.http.common.annotation.InterceptWith import io.koraframework.http.common.body.HttpBody -import io.koraframework.http.common.form.FormMultipart -import io.koraframework.http.common.header.HttpHeaders -import io.koraframework.http.server.common.* +import io.koraframework.http.server.common.HttpServer import io.koraframework.http.server.common.annotation.HttpController -import io.koraframework.http.server.common.request.HttpServerRequestMapper -import io.koraframework.http.server.common.response.HttpServerResponseMapper -import io.koraframework.json.common.JsonWriter -import io.koraframework.json.common.annotation.Json -import io.koraframework.validation.common.annotation.Pattern -import io.koraframework.validation.common.annotation.Size -import io.koraframework.validation.common.annotation.Validate -import java.util.concurrent.CompletionStage +import io.koraframework.http.server.common.interceptor.HttpServerInterceptor +import io.koraframework.http.server.common.request.HttpServerRequest +import io.koraframework.http.server.common.response.HttpServerResponse @InterceptWith(InterceptedController.ControllerInterceptor::class) @Component @HttpController class InterceptedController { + + @Component class ControllerInterceptor : HttpServerInterceptor { private val logger = LoggerFactory.getLogger(ControllerInterceptor::class.java) - override fun intercept( - ctx: Context, - request: HttpServerRequest, - chain: HttpServerInterceptor.InterceptChain - ): CompletionStage { + override fun intercept(request: HttpServerRequest, chain: HttpServerInterceptor.InterceptChain): HttpServerResponse { logger.info("Controller Level Interceptor") - return chain.process(ctx, request) + return chain.process(request) } } + @Component class MethodInterceptor : HttpServerInterceptor { private val logger = LoggerFactory.getLogger(MethodInterceptor::class.java) - override fun intercept( - ctx: Context, - request: HttpServerRequest, - chain: HttpServerInterceptor.InterceptChain - ): CompletionStage { + override fun intercept(request: HttpServerRequest, chain: HttpServerInterceptor.InterceptChain): HttpServerResponse { logger.info("Method Level Interceptor") - return chain.process(ctx, request) + return chain.process(request) } } - @Tag(HttpServerModule::class) + @Tag(HttpServer::class) @Component class ServerInterceptor : HttpServerInterceptor { private val logger = LoggerFactory.getLogger(ServerInterceptor::class.java) - override fun intercept( - ctx: Context, - request: HttpServerRequest, - chain: HttpServerInterceptor.InterceptChain - ): CompletionStage { + override fun intercept(request: HttpServerRequest, chain: HttpServerInterceptor.InterceptChain): HttpServerResponse { logger.info("Server Level Interceptor") - return chain.process(ctx, request) + return chain.process(request) } } @@ -71,4 +53,3 @@ class InterceptedController { @HttpRoute(method = HttpMethod.GET, path = "/intercepted") fun get(): HttpServerResponse = HttpServerResponse.of(200, HttpBody.plaintext("Hello world")) } - diff --git a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/JsonGetController.kt b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/JsonGetController.kt index 37b89047..5b6cd7ab 100644 --- a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/JsonGetController.kt +++ b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/JsonGetController.kt @@ -1,26 +1,11 @@ -package io.koraframework.kotlin.example.http.server +package io.koraframework.kotlin.example.http.server -import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component -import io.koraframework.common.Context -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Tag import io.koraframework.http.common.HttpMethod import io.koraframework.http.common.HttpResponseEntity -import io.koraframework.http.common.annotation.* -import io.koraframework.http.common.body.HttpBody -import io.koraframework.http.common.form.FormMultipart -import io.koraframework.http.common.header.HttpHeaders -import io.koraframework.http.server.common.* +import io.koraframework.http.common.annotation.HttpRoute import io.koraframework.http.server.common.annotation.HttpController -import io.koraframework.http.server.common.request.HttpServerRequestMapper -import io.koraframework.http.server.common.response.HttpServerResponseMapper -import io.koraframework.json.common.JsonWriter import io.koraframework.json.common.annotation.Json -import io.koraframework.validation.common.annotation.Pattern -import io.koraframework.validation.common.annotation.Size -import io.koraframework.validation.common.annotation.Validate -import java.util.concurrent.CompletionStage @Component @HttpController @@ -37,4 +22,3 @@ class JsonGetController { fun getEntity(): HttpResponseEntity = HttpResponseEntity.of(201, HelloWorldResponse("Hello world")) } - diff --git a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/JsonPostController.kt b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/JsonPostController.kt index 73b9356c..787bdfc9 100644 --- a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/JsonPostController.kt +++ b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/JsonPostController.kt @@ -1,26 +1,10 @@ -package io.koraframework.kotlin.example.http.server +package io.koraframework.kotlin.example.http.server -import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component -import io.koraframework.common.Context -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Tag import io.koraframework.http.common.HttpMethod -import io.koraframework.http.common.HttpResponseEntity -import io.koraframework.http.common.annotation.* -import io.koraframework.http.common.body.HttpBody -import io.koraframework.http.common.form.FormMultipart -import io.koraframework.http.common.header.HttpHeaders -import io.koraframework.http.server.common.* +import io.koraframework.http.common.annotation.HttpRoute import io.koraframework.http.server.common.annotation.HttpController -import io.koraframework.http.server.common.request.HttpServerRequestMapper -import io.koraframework.http.server.common.response.HttpServerResponseMapper -import io.koraframework.json.common.JsonWriter import io.koraframework.json.common.annotation.Json -import io.koraframework.validation.common.annotation.Pattern -import io.koraframework.validation.common.annotation.Size -import io.koraframework.validation.common.annotation.Validate -import java.util.concurrent.CompletionStage @Component @HttpController @@ -35,4 +19,3 @@ class JsonPostController { @Json fun post(@Json request: JsonRequest): JsonResponse = JsonResponse("Ivan", 100) } - diff --git a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/MapperRequestController.kt b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/MapperRequestController.kt index e45a0733..143d5fd6 100644 --- a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/MapperRequestController.kt +++ b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/MapperRequestController.kt @@ -1,32 +1,21 @@ -package io.koraframework.kotlin.example.http.server +package io.koraframework.kotlin.example.http.server -import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component -import io.koraframework.common.Context import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Tag import io.koraframework.http.common.HttpMethod -import io.koraframework.http.common.HttpResponseEntity -import io.koraframework.http.common.annotation.* +import io.koraframework.http.common.annotation.HttpRoute import io.koraframework.http.common.body.HttpBody -import io.koraframework.http.common.form.FormMultipart -import io.koraframework.http.common.header.HttpHeaders -import io.koraframework.http.server.common.* import io.koraframework.http.server.common.annotation.HttpController +import io.koraframework.http.server.common.request.HttpServerRequest import io.koraframework.http.server.common.request.HttpServerRequestMapper -import io.koraframework.http.server.common.response.HttpServerResponseMapper -import io.koraframework.json.common.JsonWriter -import io.koraframework.json.common.annotation.Json -import io.koraframework.validation.common.annotation.Pattern -import io.koraframework.validation.common.annotation.Size -import io.koraframework.validation.common.annotation.Validate -import java.util.concurrent.CompletionStage +import io.koraframework.http.server.common.response.HttpServerResponse @Component @HttpController class MapperRequestController { data class UserContext(val userId: String?, val traceId: String?) + @Component class UserContextRequestMapper : HttpServerRequestMapper { override fun apply(request: HttpServerRequest): UserContext = UserContext(request.headers().getFirst("x-user-id"), request.headers().getFirst("x-trace-id")) @@ -37,4 +26,3 @@ class MapperRequestController { fun get(@Mapping(UserContextRequestMapper::class) context: UserContext): HttpServerResponse = HttpServerResponse.of(200, HttpBody.plaintext("${context.userId}:${context.traceId}")) } - diff --git a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/MapperResponseController.kt b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/MapperResponseController.kt index 1a4f6725..0256a3a9 100644 --- a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/MapperResponseController.kt +++ b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/MapperResponseController.kt @@ -1,39 +1,31 @@ -package io.koraframework.kotlin.example.http.server +package io.koraframework.kotlin.example.http.server -import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component -import io.koraframework.common.Context import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Tag import io.koraframework.http.common.HttpMethod -import io.koraframework.http.common.HttpResponseEntity -import io.koraframework.http.common.annotation.* +import io.koraframework.http.common.annotation.HttpRoute +import io.koraframework.http.common.annotation.Path import io.koraframework.http.common.body.HttpBody -import io.koraframework.http.common.form.FormMultipart -import io.koraframework.http.common.header.HttpHeaders -import io.koraframework.http.server.common.* import io.koraframework.http.server.common.annotation.HttpController -import io.koraframework.http.server.common.request.HttpServerRequestMapper +import io.koraframework.http.server.common.request.HttpServerRequest +import io.koraframework.http.server.common.response.HttpServerResponse import io.koraframework.http.server.common.response.HttpServerResponseMapper -import io.koraframework.json.common.JsonWriter -import io.koraframework.json.common.annotation.Json -import io.koraframework.validation.common.annotation.Pattern -import io.koraframework.validation.common.annotation.Size -import io.koraframework.validation.common.annotation.Validate -import java.util.concurrent.CompletionStage @Component @HttpController class MapperResponseController { data class HelloWorldResponse(val greeting: String, val name: String) + @Component class HelloWorldResponseMapper : HttpServerResponseMapper { - override fun apply(ctx: Context, request: HttpServerRequest, result: HelloWorldResponse): HttpServerResponse = - HttpServerResponse.of(200, HttpBody.plaintext("${result.greeting} - ${result.name}")) + // the contract declares the result as @Nullable, so Kotlin must accept null here + override fun apply(request: HttpServerRequest, result: HelloWorldResponse?): HttpServerResponse { + requireNotNull(result) + return HttpServerResponse.of(200, HttpBody.plaintext("${result.greeting} - ${result.name}")) + } } @HttpRoute(method = HttpMethod.GET, path = "/mapper/response/{name}") @Mapping(HelloWorldResponseMapper::class) fun get(@Path name: String): HelloWorldResponse = HelloWorldResponse("Hello World", name) } - diff --git a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/MultipartController.kt b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/MultipartController.kt index 8d085256..8235706e 100644 --- a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/MultipartController.kt +++ b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/MultipartController.kt @@ -1,26 +1,12 @@ -package io.koraframework.kotlin.example.http.server +package io.koraframework.kotlin.example.http.server -import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component -import io.koraframework.common.Context -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Tag import io.koraframework.http.common.HttpMethod -import io.koraframework.http.common.HttpResponseEntity -import io.koraframework.http.common.annotation.* +import io.koraframework.http.common.annotation.HttpRoute import io.koraframework.http.common.body.HttpBody import io.koraframework.http.common.form.FormMultipart -import io.koraframework.http.common.header.HttpHeaders -import io.koraframework.http.server.common.* import io.koraframework.http.server.common.annotation.HttpController -import io.koraframework.http.server.common.request.HttpServerRequestMapper -import io.koraframework.http.server.common.response.HttpServerResponseMapper -import io.koraframework.json.common.JsonWriter -import io.koraframework.json.common.annotation.Json -import io.koraframework.validation.common.annotation.Pattern -import io.koraframework.validation.common.annotation.Size -import io.koraframework.validation.common.annotation.Validate -import java.util.concurrent.CompletionStage +import io.koraframework.http.server.common.response.HttpServerResponse @Component @HttpController @@ -31,4 +17,3 @@ class MultipartController { return HttpServerResponse.of(200, HttpBody.plaintext(partAsString)) } } - diff --git a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/SuspendController.kt b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/SuspendController.kt deleted file mode 100644 index 7f98002b..00000000 --- a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/SuspendController.kt +++ /dev/null @@ -1,31 +0,0 @@ -package io.koraframework.kotlin.example.http.server - -import org.slf4j.LoggerFactory -import io.koraframework.common.annotation.Component -import io.koraframework.common.Context -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Tag -import io.koraframework.http.common.HttpMethod -import io.koraframework.http.common.HttpResponseEntity -import io.koraframework.http.common.annotation.* -import io.koraframework.http.common.body.HttpBody -import io.koraframework.http.common.form.FormMultipart -import io.koraframework.http.common.header.HttpHeaders -import io.koraframework.http.server.common.* -import io.koraframework.http.server.common.annotation.HttpController -import io.koraframework.http.server.common.request.HttpServerRequestMapper -import io.koraframework.http.server.common.response.HttpServerResponseMapper -import io.koraframework.json.common.JsonWriter -import io.koraframework.json.common.annotation.Json -import io.koraframework.validation.common.annotation.Pattern -import io.koraframework.validation.common.annotation.Size -import io.koraframework.validation.common.annotation.Validate -import java.util.concurrent.CompletionStage - -@Component -@HttpController -class SuspendController { - @HttpRoute(method = HttpMethod.GET, path = "/suspend") - suspend fun get(): HttpServerResponse = HttpServerResponse.of(200, HttpBody.plaintext("Hello world")) -} - diff --git a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/SyncController.kt b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/SyncController.kt index 577fa154..387e120c 100644 --- a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/SyncController.kt +++ b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/SyncController.kt @@ -1,26 +1,11 @@ -package io.koraframework.kotlin.example.http.server +package io.koraframework.kotlin.example.http.server -import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component -import io.koraframework.common.Context -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Tag import io.koraframework.http.common.HttpMethod -import io.koraframework.http.common.HttpResponseEntity -import io.koraframework.http.common.annotation.* +import io.koraframework.http.common.annotation.HttpRoute import io.koraframework.http.common.body.HttpBody -import io.koraframework.http.common.form.FormMultipart -import io.koraframework.http.common.header.HttpHeaders -import io.koraframework.http.server.common.* import io.koraframework.http.server.common.annotation.HttpController -import io.koraframework.http.server.common.request.HttpServerRequestMapper -import io.koraframework.http.server.common.response.HttpServerResponseMapper -import io.koraframework.json.common.JsonWriter -import io.koraframework.json.common.annotation.Json -import io.koraframework.validation.common.annotation.Pattern -import io.koraframework.validation.common.annotation.Size -import io.koraframework.validation.common.annotation.Validate -import java.util.concurrent.CompletionStage +import io.koraframework.http.server.common.response.HttpServerResponse @Component @HttpController @@ -28,4 +13,3 @@ class SyncController { @HttpRoute(method = HttpMethod.GET, path = "/sync") fun get(): HttpServerResponse = HttpServerResponse.of(200, HttpBody.plaintext("Hello world")) } - diff --git a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/ValidateParametersController.kt b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/ValidateParametersController.kt index 972fb1b9..b9f3a6a0 100644 --- a/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/ValidateParametersController.kt +++ b/examples/kotlin/kora-kotlin-http-server/src/main/kotlin/io/koraframework/kotlin/example/http/server/ValidateParametersController.kt @@ -1,26 +1,18 @@ -package io.koraframework.kotlin.example.http.server +package io.koraframework.kotlin.example.http.server -import org.slf4j.LoggerFactory import io.koraframework.common.annotation.Component -import io.koraframework.common.Context -import io.koraframework.common.annotation.Mapping -import io.koraframework.common.annotation.Tag import io.koraframework.http.common.HttpMethod -import io.koraframework.http.common.HttpResponseEntity -import io.koraframework.http.common.annotation.* +import io.koraframework.http.common.annotation.Header +import io.koraframework.http.common.annotation.HttpRoute +import io.koraframework.http.common.annotation.Path +import io.koraframework.http.common.annotation.Query import io.koraframework.http.common.body.HttpBody -import io.koraframework.http.common.form.FormMultipart import io.koraframework.http.common.header.HttpHeaders -import io.koraframework.http.server.common.* import io.koraframework.http.server.common.annotation.HttpController -import io.koraframework.http.server.common.request.HttpServerRequestMapper -import io.koraframework.http.server.common.response.HttpServerResponseMapper -import io.koraframework.json.common.JsonWriter -import io.koraframework.json.common.annotation.Json +import io.koraframework.http.server.common.response.HttpServerResponse import io.koraframework.validation.common.annotation.Pattern import io.koraframework.validation.common.annotation.Size import io.koraframework.validation.common.annotation.Validate -import java.util.concurrent.CompletionStage @Component @HttpController @@ -38,4 +30,3 @@ open class ValidateParametersController { return HttpServerResponse.of(200, HttpHeaders.of("headerName", "headerValue"), HttpBody.plaintext(body)) } } - diff --git a/examples/kotlin/kora-kotlin-http-server/src/main/resources/application.conf b/examples/kotlin/kora-kotlin-http-server/src/main/resources/application.conf index 8aad81ab..1e9e6cd3 100644 --- a/examples/kotlin/kora-kotlin-http-server/src/main/resources/application.conf +++ b/examples/kotlin/kora-kotlin-http-server/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } diff --git a/examples/kotlin/kora-kotlin-http-server/src/test/kotlin/io/koraframework/kotlin/example/httpserver/SuspendControllerTests.kt b/examples/kotlin/kora-kotlin-http-server/src/test/kotlin/io/koraframework/kotlin/example/httpserver/SuspendControllerTests.kt deleted file mode 100644 index 91c3d1d5..00000000 --- a/examples/kotlin/kora-kotlin-http-server/src/test/kotlin/io/koraframework/kotlin/example/httpserver/SuspendControllerTests.kt +++ /dev/null @@ -1,18 +0,0 @@ -package io.koraframework.kotlin.example.httpserver - -import org.junit.jupiter.api.Test -import org.testcontainers.junit.jupiter.Container -import org.testcontainers.junit.jupiter.Testcontainers - -@Testcontainers -class SuspendControllerTests { - @Test - fun suspendController() { - container.assertBody("/suspend", 200, "Hello world") - } - - companion object { - @Container - private val container = AppContainer.build() - } -} diff --git a/examples/kotlin/kora-kotlin-http-server/src/test/kotlin/io/koraframework/kotlin/example/httpserver/TestApplication.kt b/examples/kotlin/kora-kotlin-http-server/src/test/kotlin/io/koraframework/kotlin/example/httpserver/TestApplication.kt index 0a259990..03a40dfb 100644 --- a/examples/kotlin/kora-kotlin-http-server/src/test/kotlin/io/koraframework/kotlin/example/httpserver/TestApplication.kt +++ b/examples/kotlin/kora-kotlin-http-server/src/test/kotlin/io/koraframework/kotlin/example/httpserver/TestApplication.kt @@ -16,7 +16,7 @@ interface TestApplication : Application, JdkHttpClientModule { @Root @Component - @HttpClient(configPath = "testHttpClient") + @HttpClient("testHttpClient") interface JsonHttpClient { @HttpRoute(method = HttpMethod.POST, path = "/json") diff --git a/examples/kotlin/kora-kotlin-kafka/build.gradle.kts b/examples/kotlin/kora-kotlin-kafka/build.gradle.kts index 46d7f206..5b284513 100644 --- a/examples/kotlin/kora-kotlin-kafka/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-kafka/build.gradle.kts @@ -7,17 +7,9 @@ plugins { id("com.google.devtools.ksp") version ("2.3.11") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:kafka") implementation("io.koraframework:json-common") @@ -25,7 +17,7 @@ dependencies { implementation("io.koraframework:config-hocon") testImplementation("io.koraframework:test-junit5") - testImplementation("io.goodforgod:testcontainers-extensions-kafka:0.13.1") + testImplementation("io.goodforgod:testcontainers-extensions-kafka:0.15.0") } kotlin { diff --git a/examples/kotlin/kora-kotlin-kafka/src/main/kotlin/io/koraframework/kotlin/example/kafka/listener/AutoCommitRecordMapperListener.kt b/examples/kotlin/kora-kotlin-kafka/src/main/kotlin/io/koraframework/kotlin/example/kafka/listener/AutoCommitRecordMapperListener.kt index aecb79b4..8a79c542 100644 --- a/examples/kotlin/kora-kotlin-kafka/src/main/kotlin/io/koraframework/kotlin/example/kafka/listener/AutoCommitRecordMapperListener.kt +++ b/examples/kotlin/kora-kotlin-kafka/src/main/kotlin/io/koraframework/kotlin/example/kafka/listener/AutoCommitRecordMapperListener.kt @@ -21,7 +21,7 @@ class AutoCommitRecordMapperListener : AbstractListener) : Deserializer { override fun deserialize(topic: String, data: ByteArray): MyEvent { - return reader.read(data) + return requireNotNull(reader.read(data)) { "Empty payload in topic $topic" } } } diff --git a/examples/kotlin/kora-kotlin-kafka/src/main/kotlin/io/koraframework/kotlin/example/kafka/listener/AutoCommitRecordsTelemetryListener.kt b/examples/kotlin/kora-kotlin-kafka/src/main/kotlin/io/koraframework/kotlin/example/kafka/listener/AutoCommitRecordsTelemetryListener.kt deleted file mode 100644 index 691bfaf5..00000000 --- a/examples/kotlin/kora-kotlin-kafka/src/main/kotlin/io/koraframework/kotlin/example/kafka/listener/AutoCommitRecordsTelemetryListener.kt +++ /dev/null @@ -1,29 +0,0 @@ -package io.koraframework.kotlin.example.kafka.listener - -import org.apache.kafka.clients.consumer.Consumer -import org.apache.kafka.clients.consumer.ConsumerRecord -import org.apache.kafka.clients.consumer.ConsumerRecords -import org.apache.kafka.common.header.Headers -import org.apache.kafka.common.serialization.Deserializer -import io.koraframework.common.annotation.Component -import io.koraframework.common.annotation.Tag -import io.koraframework.json.common.JsonReader -import io.koraframework.json.common.annotation.Json -import io.koraframework.kafka.common.annotation.KafkaListener -import io.koraframework.kafka.common.consumer.telemetry.KafkaConsumerTelemetry - -@Component -class AutoCommitRecordsTelemetryListener : AbstractListener() { - @KafkaListener("kafka.consumer.my-listener") - fun process( - records: ConsumerRecords, - ctx: KafkaConsumerTelemetry.KafkaConsumerRecordsTelemetryContext - ) { - records.forEach { - val telemetryContext = ctx.get(it) - success(it.value()) - telemetryContext.close(null) - } - } -} - diff --git a/examples/kotlin/kora-kotlin-kafka/src/main/kotlin/io/koraframework/kotlin/example/kafka/listener/AutoCommitValueMapperListener.kt b/examples/kotlin/kora-kotlin-kafka/src/main/kotlin/io/koraframework/kotlin/example/kafka/listener/AutoCommitValueMapperListener.kt index 956b7aff..9f4649ba 100644 --- a/examples/kotlin/kora-kotlin-kafka/src/main/kotlin/io/koraframework/kotlin/example/kafka/listener/AutoCommitValueMapperListener.kt +++ b/examples/kotlin/kora-kotlin-kafka/src/main/kotlin/io/koraframework/kotlin/example/kafka/listener/AutoCommitValueMapperListener.kt @@ -21,7 +21,7 @@ class AutoCommitValueMapperListener : AbstractListener) : Deserializer { override fun deserialize(topic: String, data: ByteArray): MyEvent { - return reader.read(data) + return requireNotNull(reader.read(data)) { "Empty payload in topic $topic" } } } diff --git a/examples/kotlin/kora-kotlin-kafka/src/test/kotlin/io/koraframework/kotlin/example/kafka/listener/AutoCommitRecordsTelemetryListenerTests.kt b/examples/kotlin/kora-kotlin-kafka/src/test/kotlin/io/koraframework/kotlin/example/kafka/listener/AutoCommitRecordsTelemetryListenerTests.kt deleted file mode 100644 index f086110c..00000000 --- a/examples/kotlin/kora-kotlin-kafka/src/test/kotlin/io/koraframework/kotlin/example/kafka/listener/AutoCommitRecordsTelemetryListenerTests.kt +++ /dev/null @@ -1,37 +0,0 @@ -package io.koraframework.kotlin.example.kafka.listener - -import io.goodforgod.testcontainers.extensions.ContainerMode -import io.goodforgod.testcontainers.extensions.kafka.* -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import io.koraframework.application.graph.Lifecycle -import io.koraframework.common.annotation.Tag -import io.koraframework.kotlin.example.kafka.Application -import io.koraframework.kotlin.example.kafka.kafkaConfig -import io.koraframework.test.extension.junit5.KoraAppTest -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier -import io.koraframework.test.extension.junit5.KoraConfigModification -import io.koraframework.test.extension.junit5.TestComponent - -@TestcontainersKafka(mode = ContainerMode.PER_RUN, topics = Topics("my-topic-consumer")) -@KoraAppTest(Application::class) -class AutoCommitRecordsTelemetryListenerTests : KoraAppTestConfigModifier { - @ConnectionKafka - lateinit var connection: KafkaConnection - - @Tag(AutoCommitRecordsTelemetryListenerModule.AutoCommitRecordsTelemetryListenerProcessTag::class) - @TestComponent - lateinit var consumerLifecycle: Lifecycle - - @TestComponent - lateinit var consumer: AutoCommitRecordsTelemetryListener - override fun config(): KoraConfigModification = kafkaConfig(connection) - - @Test - fun processed() { - connection.send("my-topic-consumer", Event.ofValueAndRandomKey("""{"username":"Bob","code":1}""")) - - awaitReceived(consumer) - assertEquals("""{"username":"Bob","code":1}""", consumer.received()[0]) - } -} diff --git a/examples/kotlin/kora-kotlin-openapi-generator-http-client/build.gradle.kts b/examples/kotlin/kora-kotlin-openapi-generator-http-client/build.gradle.kts index 092d74ad..622cd683 100644 --- a/examples/kotlin/kora-kotlin-openapi-generator-http-client/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-openapi-generator-http-client/build.gradle.kts @@ -19,29 +19,20 @@ plugins { id("org.openapi.generator") version ("7.24.0") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:validation-module") implementation("io.koraframework:http-client-jdk") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") implementation("io.koraframework:config-hocon") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.8.1") testImplementation("org.json:json:20231013") testImplementation("org.skyscreamer:jsonassert:1.5.1") testImplementation("io.koraframework:test-junit5") - testImplementation("io.goodforgod:testcontainers-extensions-mockserver:0.13.1") + testImplementation("io.goodforgod:testcontainers-extensions-mockserver:0.15.0") } kotlin { diff --git a/examples/kotlin/kora-kotlin-openapi-generator-http-client/src/main/kotlin/io/koraframework/kotlin/example/openapi/http/client/Application.kt b/examples/kotlin/kora-kotlin-openapi-generator-http-client/src/main/kotlin/io/koraframework/kotlin/example/openapi/http/client/Application.kt index 41ef9ba7..9c22f456 100644 --- a/examples/kotlin/kora-kotlin-openapi-generator-http-client/src/main/kotlin/io/koraframework/kotlin/example/openapi/http/client/Application.kt +++ b/examples/kotlin/kora-kotlin-openapi-generator-http-client/src/main/kotlin/io/koraframework/kotlin/example/openapi/http/client/Application.kt @@ -2,14 +2,28 @@ package io.koraframework.kotlin.example.openapi.http.client import io.koraframework.application.graph.KoraApplication import io.koraframework.common.annotation.KoraApp +import io.koraframework.common.annotation.Tag +import io.koraframework.http.client.common.auth.HttpClientTokenProvider +import io.koraframework.kotlin.example.openapi.petV3.api.ApiSecurity import io.koraframework.config.hocon.HoconConfigModule import io.koraframework.http.client.jdk.JdkHttpClientModule import io.koraframework.json.common.JsonModule import io.koraframework.logging.logback.LogbackModule -import io.koraframework.validation.module.ValidationModule +import io.koraframework.validation.common.constraint.ValidatorModule @KoraApp -interface Application : HoconConfigModule, LogbackModule, ValidationModule, JsonModule, JdkHttpClientModule +interface Application : HoconConfigModule, LogbackModule, ValidatorModule, JsonModule, JdkHttpClientModule { + + // Сгенерированный ApiSecurity требует HttpClientTokenProvider под тегом каждой схемы, даже если + // приложение её не использует. Перехватчик перебирает схемы по порядку и берёт первую, чей + // провайдер вернул токен, поэтому неиспользуемая схема обязана вернуть null: иначе она перебьёт + // apiKeyAuth, и запрос уйдёт с чужим заголовком. + @Tag(ApiSecurity.BearerAuth::class) + fun bearerAuthTokenProvider(): HttpClientTokenProvider = HttpClientTokenProvider { null } + + @Tag(ApiSecurity.OAuth::class) + fun oAuthTokenProvider(): HttpClientTokenProvider = HttpClientTokenProvider { null } +} fun main() { KoraApplication.run { ApplicationGraph.graph() } diff --git a/examples/kotlin/kora-kotlin-openapi-generator-http-client/src/main/resources/application.conf b/examples/kotlin/kora-kotlin-openapi-generator-http-client/src/main/resources/application.conf index e3c455b2..0232f69a 100644 --- a/examples/kotlin/kora-kotlin-openapi-generator-http-client/src/main/resources/application.conf +++ b/examples/kotlin/kora-kotlin-openapi-generator-http-client/src/main/resources/application.conf @@ -1,4 +1,4 @@ -httpClient.petV2.PetApi { +httpClient.petV2.petApi { url = ${HTTP_CLIENT_PET_V2_URL} requestTimeout = 10s getValuesConfig { @@ -7,7 +7,7 @@ httpClient.petV2.PetApi { telemetry.logging.enabled = true } -httpClient.petV3.PetApi { +httpClient.petV3.petApi { url = ${HTTP_CLIENT_PET_V3_URL} requestTimeout = 10s getValuesConfig { @@ -16,7 +16,10 @@ httpClient.petV3.PetApi { telemetry.logging.enabled = true } -openapiAuth.apiKeyAuth = "MyAuthApiKey" +openapiAuth { + apiKeyAuth = "MyAuthApiKey" + basicAuth { username = "user", password = "pass" } +} logging.level { "root": "WARN" diff --git a/examples/kotlin/kora-kotlin-openapi-generator-http-client/src/test/kotlin/io/koraframework/kotlin/example/openapi/http/client/HttpClientPetV2Tests.kt b/examples/kotlin/kora-kotlin-openapi-generator-http-client/src/test/kotlin/io/koraframework/kotlin/example/openapi/http/client/HttpClientPetV2Tests.kt index 6d13b76d..b35b13b4 100644 --- a/examples/kotlin/kora-kotlin-openapi-generator-http-client/src/test/kotlin/io/koraframework/kotlin/example/openapi/http/client/HttpClientPetV2Tests.kt +++ b/examples/kotlin/kora-kotlin-openapi-generator-http-client/src/test/kotlin/io/koraframework/kotlin/example/openapi/http/client/HttpClientPetV2Tests.kt @@ -44,7 +44,7 @@ class HttpClientPetV2Tests : KoraAppTestConfigModifier { val responseBody = JSONObject() .put("id", 1L) .put("name", "name") - .put("status", Pet.StatusEnum.AVAILABLE.getValue()) + .put("status", Pet.StatusEnum.AVAILABLE.value) .put("category", JSONObject().put("id", 1L).put("name", "category")) .put("tags", tags) @@ -62,7 +62,7 @@ class HttpClientPetV2Tests : KoraAppTestConfigModifier { if (response is PetApiResponses.GetPetByIdApiResponse.GetPetById200ApiResponse) { assertEquals(responseBody.getLong("id"), response.content.id) assertEquals(responseBody.getString("name"), response.content.name) - assertEquals(responseBody.getString("status"), response.content.status?.getValue()) + assertEquals(responseBody.getString("status"), response.content.status?.value) } else { fail("Shouldn't happen") } @@ -76,7 +76,7 @@ class HttpClientPetV2Tests : KoraAppTestConfigModifier { val requestBody = JSONObject() .put("id", 1L) .put("name", "name") - .put("status", Pet.StatusEnum.AVAILABLE.getValue()) + .put("status", Pet.StatusEnum.AVAILABLE.value) .put("category", JSONObject().put("id", 1L).put("name", "category")) .put("tags", tags) @@ -91,12 +91,13 @@ class HttpClientPetV2Tests : KoraAppTestConfigModifier { ).respond(response().withBody(JsonBody(responseBody.toString()))) // when + // the 2.0 generator orders the constructor by optionality, so the arguments are named val request = Pet( - 1L, - "name", - Category(1L, "category"), - listOf(Tag(1L, "tag")), - Pet.StatusEnum.AVAILABLE + id = 1L, + name = "name", + category = Category(1L, "category"), + tags = listOf(Tag(1L, "tag")), + status = Pet.StatusEnum.AVAILABLE ) val response = petApi.addPet(request) diff --git a/examples/kotlin/kora-kotlin-openapi-generator-http-client/src/test/kotlin/io/koraframework/kotlin/example/openapi/http/client/HttpClientPetV3Tests.kt b/examples/kotlin/kora-kotlin-openapi-generator-http-client/src/test/kotlin/io/koraframework/kotlin/example/openapi/http/client/HttpClientPetV3Tests.kt index 42343b84..e6cd23b0 100644 --- a/examples/kotlin/kora-kotlin-openapi-generator-http-client/src/test/kotlin/io/koraframework/kotlin/example/openapi/http/client/HttpClientPetV3Tests.kt +++ b/examples/kotlin/kora-kotlin-openapi-generator-http-client/src/test/kotlin/io/koraframework/kotlin/example/openapi/http/client/HttpClientPetV3Tests.kt @@ -4,7 +4,6 @@ import io.goodforgod.testcontainers.extensions.ContainerMode import io.goodforgod.testcontainers.extensions.mockserver.ConnectionMockServer import io.goodforgod.testcontainers.extensions.mockserver.MockServerConnection import io.goodforgod.testcontainers.extensions.mockserver.TestcontainersMockServer -import kotlinx.coroutines.runBlocking import org.json.JSONArray import org.json.JSONObject import org.junit.jupiter.api.Assertions.assertEquals @@ -45,7 +44,7 @@ class HttpClientPetV3Tests : KoraAppTestConfigModifier { val responseBody = JSONObject() .put("id", 1L) .put("name", "name") - .put("status", Pet.StatusEnum.AVAILABLE.getValue()) + .put("status", Pet.StatusEnum.AVAILABLE.value) .put("category", JSONObject().put("id", 1L).put("name", "category")) .put("tags", tags) @@ -58,13 +57,13 @@ class HttpClientPetV3Tests : KoraAppTestConfigModifier { ).respond(response().withBody(responseBody.toString())) // when - val response = runBlocking { petApi.getPetById(1L) } + val response = petApi.getPetById(1L) // then if (response is PetApiResponses.GetPetByIdApiResponse.GetPetById200ApiResponse) { assertEquals(responseBody.getLong("id"), response.content.id) assertEquals(responseBody.getString("name"), response.content.name) - assertEquals(responseBody.getString("status"), response.content.status?.getValue()) + assertEquals(responseBody.getString("status"), response.content.status?.value) } else { fail("Shouldn't happen") } @@ -78,7 +77,7 @@ class HttpClientPetV3Tests : KoraAppTestConfigModifier { val requestBody = JSONObject() .put("id", 1L) .put("name", "name") - .put("status", Pet.StatusEnum.AVAILABLE.getValue()) + .put("status", Pet.StatusEnum.AVAILABLE.value) .put("category", JSONObject().put("id", 1L).put("name", "category")) .put("tags", tags) @@ -91,14 +90,15 @@ class HttpClientPetV3Tests : KoraAppTestConfigModifier { ).respond(response().withBody(JsonBody(requestBody.toString()))) // when + // the 2.0 generator orders the constructor by optionality, so the arguments are named val request = Pet( - 1L, - "name", - Category(1L, "category"), - listOf(Tag(1L, "tag")), - Pet.StatusEnum.AVAILABLE + id = 1L, + name = "name", + category = Category(1L, "category"), + tags = listOf(Tag(1L, "tag")), + status = Pet.StatusEnum.AVAILABLE ) - val response = runBlocking { petApi.addPet(request) } + val response = petApi.addPet(request) // then if (response is PetApiResponses.AddPetApiResponse.AddPet200ApiResponse) { diff --git a/examples/kotlin/kora-kotlin-openapi-generator-http-server/build.gradle.kts b/examples/kotlin/kora-kotlin-openapi-generator-http-server/build.gradle.kts index b71db8d0..b2b5446d 100644 --- a/examples/kotlin/kora-kotlin-openapi-generator-http-server/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-openapi-generator-http-server/build.gradle.kts @@ -19,17 +19,9 @@ plugins { id("org.openapi.generator") version ("7.24.0") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:validation-module") implementation("io.koraframework:http-server-undertow") diff --git a/examples/kotlin/kora-kotlin-openapi-generator-http-server/src/main/kotlin/io/koraframework/kotlin/example/openapi/http/server/Application.kt b/examples/kotlin/kora-kotlin-openapi-generator-http-server/src/main/kotlin/io/koraframework/kotlin/example/openapi/http/server/Application.kt index cb693d0e..8e026389 100644 --- a/examples/kotlin/kora-kotlin-openapi-generator-http-server/src/main/kotlin/io/koraframework/kotlin/example/openapi/http/server/Application.kt +++ b/examples/kotlin/kora-kotlin-openapi-generator-http-server/src/main/kotlin/io/koraframework/kotlin/example/openapi/http/server/Application.kt @@ -14,7 +14,6 @@ import io.koraframework.kotlin.example.openapi.petV3.api.ApiSecurity import io.koraframework.logging.logback.LogbackModule import io.koraframework.validation.module.ValidationModule import io.koraframework.validation.module.http.server.ViolationExceptionHttpServerResponseMapper -import java.util.concurrent.CompletableFuture @KoraApp interface Application : HoconConfigModule, LogbackModule, ValidationModule, JsonModule, UndertowPublicHttpServerModule { @@ -25,23 +24,23 @@ interface Application : HoconConfigModule, LogbackModule, ValidationModule, Json } @Tag(ApiSecurity.BearerAuth::class) - fun bearerHttpServerPrincipalExtractor(): HttpServerPrincipalExtractor { - return HttpServerPrincipalExtractor { _, _ -> CompletableFuture.completedFuture(UserPrincipal("name")) } + fun bearerHttpServerPrincipalExtractor(): HttpServerPrincipalExtractor { + return HttpServerPrincipalExtractor { _, _ -> UserPrincipal("name") } } @Tag(ApiSecurity.BasicAuth::class) - fun basicHttpServerPrincipalExtractor(): HttpServerPrincipalExtractor { - return HttpServerPrincipalExtractor { _, _ -> CompletableFuture.completedFuture(UserPrincipal("name")) } + fun basicHttpServerPrincipalExtractor(): HttpServerPrincipalExtractor { + return HttpServerPrincipalExtractor { _, _ -> UserPrincipal("name") } } @Tag(ApiSecurity.ApiKeyAuth::class) - fun apiKeyHttpServerPrincipalExtractor(): HttpServerPrincipalExtractor { - return HttpServerPrincipalExtractor { _, _ -> CompletableFuture.completedFuture(UserPrincipal("name")) } + fun apiKeyHttpServerPrincipalExtractor(): HttpServerPrincipalExtractor { + return HttpServerPrincipalExtractor { _, _ -> UserPrincipal("name") } } @Tag(ApiSecurity.OAuth::class) - fun oauthHttpServerPrincipalExtractor(): HttpServerPrincipalExtractor { - return HttpServerPrincipalExtractor { _, _ -> CompletableFuture.completedFuture(UserPrincipal("name")) } + fun oauthHttpServerPrincipalExtractor(): HttpServerPrincipalExtractor { + return HttpServerPrincipalExtractor { _, _ -> UserPrincipal("name") } } } diff --git a/examples/kotlin/kora-kotlin-openapi-generator-http-server/src/main/kotlin/io/koraframework/kotlin/example/openapi/http/server/PetV3Delegate.kt b/examples/kotlin/kora-kotlin-openapi-generator-http-server/src/main/kotlin/io/koraframework/kotlin/example/openapi/http/server/PetV3Delegate.kt index 838c2258..2d1728c2 100644 --- a/examples/kotlin/kora-kotlin-openapi-generator-http-server/src/main/kotlin/io/koraframework/kotlin/example/openapi/http/server/PetV3Delegate.kt +++ b/examples/kotlin/kora-kotlin-openapi-generator-http-server/src/main/kotlin/io/koraframework/kotlin/example/openapi/http/server/PetV3Delegate.kt @@ -22,8 +22,11 @@ class PetV3Delegate : PetApiDelegate { } override fun findPetsByStatus(status: String?): PetApiResponses.FindPetsByStatusApiResponse { - val statusEnum = status?.let(Pet.StatusEnum::fromValue) - ?: return PetApiResponses.FindPetsByStatusApiResponse.FindPetsByStatus400ApiResponse() + val statusEnum = try { + status?.let(Pet.StatusEnum::fromValue) + } catch (_: IllegalArgumentException) { + null + } ?: return PetApiResponses.FindPetsByStatusApiResponse.FindPetsByStatus400ApiResponse() val pets = petMap.values.filter { statusEnum == it.status } return PetApiResponses.FindPetsByStatusApiResponse.FindPetsByStatus200ApiResponse(pets) } diff --git a/examples/kotlin/kora-kotlin-openapi-generator-http-server/src/main/resources/application.conf b/examples/kotlin/kora-kotlin-openapi-generator-http-server/src/main/resources/application.conf index 650dccc8..6ad7300c 100644 --- a/examples/kotlin/kora-kotlin-openapi-generator-http-server/src/main/resources/application.conf +++ b/examples/kotlin/kora-kotlin-openapi-generator-http-server/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } diff --git a/examples/kotlin/kora-kotlin-openapi-generator-http-server/src/test/kotlin/io/koraframework/kotlin/example/openapi/http/server/HttpServerPetV2Tests.kt b/examples/kotlin/kora-kotlin-openapi-generator-http-server/src/test/kotlin/io/koraframework/kotlin/example/openapi/http/server/HttpServerPetV2Tests.kt index 75c5e831..de5187b9 100644 --- a/examples/kotlin/kora-kotlin-openapi-generator-http-server/src/test/kotlin/io/koraframework/kotlin/example/openapi/http/server/HttpServerPetV2Tests.kt +++ b/examples/kotlin/kora-kotlin-openapi-generator-http-server/src/test/kotlin/io/koraframework/kotlin/example/openapi/http/server/HttpServerPetV2Tests.kt @@ -26,7 +26,7 @@ class HttpServerPetV2Tests { val requestBody = JSONObject() .put("id", 1L) .put("name", "name") - .put("status", Pet.StatusEnum.AVAILABLE.getValue()) + .put("status", Pet.StatusEnum.AVAILABLE.value) .put("category", JSONObject().put("id", 1L).put("name", "category")) .put("tags", tags) diff --git a/examples/kotlin/kora-kotlin-openapi-generator-http-server/src/test/kotlin/io/koraframework/kotlin/example/openapi/http/server/HttpServerPetV3Tests.kt b/examples/kotlin/kora-kotlin-openapi-generator-http-server/src/test/kotlin/io/koraframework/kotlin/example/openapi/http/server/HttpServerPetV3Tests.kt index 88c47537..ed57173e 100644 --- a/examples/kotlin/kora-kotlin-openapi-generator-http-server/src/test/kotlin/io/koraframework/kotlin/example/openapi/http/server/HttpServerPetV3Tests.kt +++ b/examples/kotlin/kora-kotlin-openapi-generator-http-server/src/test/kotlin/io/koraframework/kotlin/example/openapi/http/server/HttpServerPetV3Tests.kt @@ -26,7 +26,7 @@ class HttpServerPetV3Tests { val requestBody = JSONObject() .put("id", 1L) .put("name", "name") - .put("status", Pet.StatusEnum.AVAILABLE.getValue()) + .put("status", Pet.StatusEnum.AVAILABLE.value) .put("category", JSONObject().put("id", 1L).put("name", "category")) .put("tags", tags) diff --git a/examples/kotlin/kora-kotlin-resilient/build.gradle.kts b/examples/kotlin/kora-kotlin-resilient/build.gradle.kts index 34c5e612..f8ff9e2f 100644 --- a/examples/kotlin/kora-kotlin-resilient/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-resilient/build.gradle.kts @@ -7,17 +7,9 @@ plugins { id("com.google.devtools.ksp") version ("2.3.11") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:resilient-kora") implementation("io.koraframework:config-hocon") implementation("io.koraframework:logging-logback") diff --git a/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/CircuitBreakerService.kt b/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/CircuitBreakerService.kt index cafd7425..f822fda4 100644 --- a/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/CircuitBreakerService.kt +++ b/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/CircuitBreakerService.kt @@ -1,16 +1,11 @@ -package io.koraframework.kotlin.example.resilient +package io.koraframework.kotlin.example.resilient import io.koraframework.common.annotation.Component -import io.koraframework.common.annotation.Root -import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreaker -import io.koraframework.resilient.fallback.annotation.Fallback -import io.koraframework.resilient.retry.annotation.Retry -import io.koraframework.resilient.timeout.annotation.Timeout +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakable import java.util.concurrent.ThreadLocalRandom @Component open class CircuitBreakerService { - @CircuitBreaker("my_cb") + @CircuitBreakable(MyCircuitBreaker::class) open fun getSuccessful(): String = "OK" } - diff --git a/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/ComboService.kt b/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/ComboService.kt index be287211..e9163cfb 100644 --- a/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/ComboService.kt +++ b/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/ComboService.kt @@ -1,20 +1,20 @@ -package io.koraframework.kotlin.example.resilient +package io.koraframework.kotlin.example.resilient import io.koraframework.common.annotation.Component import io.koraframework.common.annotation.Root -import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreaker +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakable import io.koraframework.resilient.fallback.annotation.Fallback -import io.koraframework.resilient.retry.annotation.Retry +import io.koraframework.resilient.retry.annotation.Retryable import io.koraframework.resilient.timeout.annotation.Timeout import java.util.concurrent.ThreadLocalRandom @Root @Component open class ComboService { - @Fallback(value = "my_fallback", method = "getFallback()") - @CircuitBreaker("my_cb") - @Retry("my_retry") - @Timeout("my_timeout") + @Fallback(method = "getFallback()") + @CircuitBreakable(MyCircuitBreaker::class) + @Retryable(DefaultRetry::class) + @Timeout(DefaultTimeouter::class) open fun getValue(fail: Boolean): String { if (fail) { throw IllegalStateException("Failed") @@ -36,4 +36,3 @@ open class ComboService { const val FALLBACK = "FALLBACK" } } - diff --git a/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/DefaultRetry.kt b/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/DefaultRetry.kt new file mode 100644 index 00000000..dbf85883 --- /dev/null +++ b/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/DefaultRetry.kt @@ -0,0 +1,7 @@ +package io.koraframework.kotlin.example.resilient + +import io.koraframework.resilient.retry.Retry +import io.koraframework.resilient.retry.annotation.RetrySpec + +@RetrySpec("resilient.retry.default") +interface DefaultRetry : Retry diff --git a/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/DefaultTimeouter.kt b/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/DefaultTimeouter.kt new file mode 100644 index 00000000..d4508f99 --- /dev/null +++ b/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/DefaultTimeouter.kt @@ -0,0 +1,7 @@ +package io.koraframework.kotlin.example.resilient + +import io.koraframework.resilient.timeout.Timeouter +import io.koraframework.resilient.timeout.annotation.TimeoutSpec + +@TimeoutSpec("resilient.timeout.default") +interface DefaultTimeouter : Timeouter diff --git a/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/FallbackService.kt b/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/FallbackService.kt index c258c83c..c2862056 100644 --- a/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/FallbackService.kt +++ b/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/FallbackService.kt @@ -1,16 +1,12 @@ -package io.koraframework.kotlin.example.resilient +package io.koraframework.kotlin.example.resilient import io.koraframework.common.annotation.Component -import io.koraframework.common.annotation.Root -import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreaker import io.koraframework.resilient.fallback.annotation.Fallback -import io.koraframework.resilient.retry.annotation.Retry -import io.koraframework.resilient.timeout.annotation.Timeout import java.util.concurrent.ThreadLocalRandom @Component open class FallbackService { - @Fallback(value = "my_fallback", method = "getFallback()") + @Fallback(method = "getFallback()") open fun getValue(fail: Boolean): String { if (fail) { throw IllegalStateException("Failed") @@ -25,4 +21,3 @@ open class FallbackService { const val FALLBACK = "FALLBACK" } } - diff --git a/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/MyCircuitBreaker.kt b/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/MyCircuitBreaker.kt new file mode 100644 index 00000000..4bc93fb6 --- /dev/null +++ b/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/MyCircuitBreaker.kt @@ -0,0 +1,7 @@ +package io.koraframework.kotlin.example.resilient + +import io.koraframework.resilient.circuitbreaker.CircuitBreaker +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakerSpec + +@CircuitBreakerSpec("resilient.circuitbreaker.my_cb") +interface MyCircuitBreaker : CircuitBreaker diff --git a/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/RetryService.kt b/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/RetryService.kt index 93b6a5a9..a660ff47 100644 --- a/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/RetryService.kt +++ b/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/RetryService.kt @@ -1,16 +1,11 @@ -package io.koraframework.kotlin.example.resilient +package io.koraframework.kotlin.example.resilient import io.koraframework.common.annotation.Component -import io.koraframework.common.annotation.Root -import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreaker -import io.koraframework.resilient.fallback.annotation.Fallback -import io.koraframework.resilient.retry.annotation.Retry -import io.koraframework.resilient.timeout.annotation.Timeout +import io.koraframework.resilient.retry.annotation.Retryable import java.util.concurrent.ThreadLocalRandom @Component open class RetryService { - @Retry("custom1") + @Retryable(DefaultRetry::class) open fun getValue(arg: String): String = arg } - diff --git a/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/TimeoutService.kt b/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/TimeoutService.kt index 91990556..58e4ce16 100644 --- a/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/TimeoutService.kt +++ b/examples/kotlin/kora-kotlin-resilient/src/main/kotlin/io/koraframework/kotlin/example/resilient/TimeoutService.kt @@ -1,16 +1,11 @@ -package io.koraframework.kotlin.example.resilient +package io.koraframework.kotlin.example.resilient import io.koraframework.common.annotation.Component -import io.koraframework.common.annotation.Root -import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreaker -import io.koraframework.resilient.fallback.annotation.Fallback -import io.koraframework.resilient.retry.annotation.Retry import io.koraframework.resilient.timeout.annotation.Timeout import java.util.concurrent.ThreadLocalRandom @Component open class TimeoutService { - @Timeout("custom1") + @Timeout(DefaultTimeouter::class) open fun getSuccessful(): String = "OK" } - diff --git a/examples/kotlin/kora-kotlin-resilient/src/main/resources/application.conf b/examples/kotlin/kora-kotlin-resilient/src/main/resources/application.conf index dbcfe5a1..71d4ef16 100644 --- a/examples/kotlin/kora-kotlin-resilient/src/main/resources/application.conf +++ b/examples/kotlin/kora-kotlin-resilient/src/main/resources/application.conf @@ -1,16 +1,15 @@ resilient { circuitbreaker { - default { - slidingWindowSize = 2 - minimumRequiredCalls = 2 + # 2.0 does not merge a named section with `default` — the spec points at one explicit path, + # so every required value has to be present here + my_cb { + type = FIXED_WINDOW + countBased.windowSize = 1 + minimumRequiredCalls = 1 failureRateThreshold = 100 permittedCallsInHalfOpenState = 1 waitDurationInOpenState = 1s } - my_cb { - slidingWindowSize = 1 - minimumRequiredCalls = 1 - } } timeout { default { diff --git a/examples/kotlin/kora-kotlin-s3-client-aws/build.gradle.kts b/examples/kotlin/kora-kotlin-s3-client-aws/build.gradle.kts index 94737958..459518e1 100644 --- a/examples/kotlin/kora-kotlin-s3-client-aws/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-s3-client-aws/build.gradle.kts @@ -7,28 +7,18 @@ plugins { id("com.google.devtools.ksp") version ("2.3.11") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:s3-client-aws") implementation("io.koraframework:http-client-jdk") - implementation("io.projectreactor:reactor-core:3.6.18") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.8.1") implementation("io.koraframework:logging-logback") implementation("io.koraframework:config-hocon") testImplementation("org.testcontainers:junit-jupiter:1.21.4") testImplementation("org.testcontainers:minio:1.21.4") - testImplementation("io.goodforgod:testcontainers-extensions-minio:0.13.1") + testImplementation("io.goodforgod:testcontainers-extensions-minio:0.15.0") testImplementation("io.koraframework:test-junit5") } diff --git a/examples/kotlin/kora-kotlin-s3-client-aws/src/main/kotlin/io/koraframework/kotlin/example/s3/aws/AwsS3Client.kt b/examples/kotlin/kora-kotlin-s3-client-aws/src/main/kotlin/io/koraframework/kotlin/example/s3/aws/AwsS3Client.kt deleted file mode 100644 index f6450dcb..00000000 --- a/examples/kotlin/kora-kotlin-s3-client-aws/src/main/kotlin/io/koraframework/kotlin/example/s3/aws/AwsS3Client.kt +++ /dev/null @@ -1,30 +0,0 @@ -package io.koraframework.kotlin.example.s3.aws - -import io.koraframework.common.annotation.Component -import io.koraframework.common.annotation.Root -import io.koraframework.s3.client.annotation.S3 -import io.koraframework.s3.client.model.* -import software.amazon.awssdk.core.ResponseInputStream -import software.amazon.awssdk.services.s3.model.DeleteObjectResponse -import software.amazon.awssdk.services.s3.model.GetObjectResponse -import software.amazon.awssdk.services.s3.model.ListObjectsV2Response -import software.amazon.awssdk.services.s3.model.PutObjectResponse - -@S3.Client("my") -interface AwsS3Client { - @S3.Get("pre-{key}") - fun getObject(key: String): ResponseInputStream - - @S3.Get - fun getObjectMeta(key: String): GetObjectResponse - - @S3.List(value = "pre-{prefix}", limit = 50) - fun listObjectMeta(prefix: String): ListObjectsV2Response - - @S3.Put("pre-{key}") - fun putObject(key: String, obj: S3Body): PutObjectResponse - - @S3.Delete("pre-{key}") - fun deleteObject(key: String): DeleteObjectResponse -} - diff --git a/examples/kotlin/kora-kotlin-s3-client-aws/src/main/kotlin/io/koraframework/kotlin/example/s3/aws/AwsS3Service.kt b/examples/kotlin/kora-kotlin-s3-client-aws/src/main/kotlin/io/koraframework/kotlin/example/s3/aws/AwsS3Service.kt new file mode 100644 index 00000000..b61a036e --- /dev/null +++ b/examples/kotlin/kora-kotlin-s3-client-aws/src/main/kotlin/io/koraframework/kotlin/example/s3/aws/AwsS3Service.kt @@ -0,0 +1,46 @@ +package io.koraframework.kotlin.example.s3.aws + +import io.koraframework.common.annotation.Component +import software.amazon.awssdk.core.ResponseInputStream +import software.amazon.awssdk.core.sync.RequestBody +import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse +import software.amazon.awssdk.services.s3.model.GetObjectResponse +import software.amazon.awssdk.services.s3.model.HeadObjectResponse +import software.amazon.awssdk.services.s3.model.ListObjectsV2Response +import software.amazon.awssdk.services.s3.model.ObjectIdentifier +import software.amazon.awssdk.services.s3.model.PutObjectResponse + +/** + * Kora 2.0 exposes the AWS SDK [S3Client] itself as a component, so working with S3 through this + * module means working with the AWS SDK API directly. The declarative `@S3.Client` contracts live + * in a different artifact now, see `kora-kotlin-s3-client-minio`. + */ +@Component +class AwsS3Service( + private val s3Client: S3Client, + config: S3Config +) { + private val bucket: String = config.bucket() + + fun putObject(key: String, value: ByteArray): PutObjectResponse = + s3Client.putObject({ it.bucket(bucket).key(key) }, RequestBody.fromBytes(value)) + + fun getObject(key: String): ResponseInputStream = + s3Client.getObject { it.bucket(bucket).key(key) } + + fun getObjectMeta(key: String): HeadObjectResponse = + s3Client.headObject { it.bucket(bucket).key(key) } + + fun listObjects(prefix: String): ListObjectsV2Response = + s3Client.listObjectsV2 { it.bucket(bucket).prefix(prefix).maxKeys(50) } + + fun deleteObject(key: String) { + s3Client.deleteObject { it.bucket(bucket).key(key) } + } + + fun deleteObjects(keys: List): DeleteObjectsResponse { + val identifiers = keys.map { ObjectIdentifier.builder().key(it).build() } + return s3Client.deleteObjects { r -> r.bucket(bucket).delete { d -> d.objects(identifiers) } } + } +} diff --git a/examples/kotlin/kora-kotlin-s3-client-aws/src/main/kotlin/io/koraframework/kotlin/example/s3/aws/RootService.kt b/examples/kotlin/kora-kotlin-s3-client-aws/src/main/kotlin/io/koraframework/kotlin/example/s3/aws/RootService.kt index 80f8e4d5..fcfb455e 100644 --- a/examples/kotlin/kora-kotlin-s3-client-aws/src/main/kotlin/io/koraframework/kotlin/example/s3/aws/RootService.kt +++ b/examples/kotlin/kora-kotlin-s3-client-aws/src/main/kotlin/io/koraframework/kotlin/example/s3/aws/RootService.kt @@ -1,19 +1,10 @@ -package io.koraframework.kotlin.example.s3.aws +package io.koraframework.kotlin.example.s3.aws import io.koraframework.common.annotation.Component import io.koraframework.common.annotation.Root -import io.koraframework.s3.client.annotation.S3 -import io.koraframework.s3.client.model.* -import software.amazon.awssdk.core.ResponseInputStream -import software.amazon.awssdk.services.s3.model.DeleteObjectResponse -import software.amazon.awssdk.services.s3.model.GetObjectResponse -import software.amazon.awssdk.services.s3.model.ListObjectsV2Response -import software.amazon.awssdk.services.s3.model.PutObjectResponse @Root @Component class RootService( - private val syncS3Client: SyncS3Client, - private val awsS3Client: AwsS3Client + private val awsS3Service: AwsS3Service ) - diff --git a/examples/kotlin/kora-kotlin-s3-client-aws/src/main/kotlin/io/koraframework/kotlin/example/s3/aws/S3Config.kt b/examples/kotlin/kora-kotlin-s3-client-aws/src/main/kotlin/io/koraframework/kotlin/example/s3/aws/S3Config.kt new file mode 100644 index 00000000..fea159a9 --- /dev/null +++ b/examples/kotlin/kora-kotlin-s3-client-aws/src/main/kotlin/io/koraframework/kotlin/example/s3/aws/S3Config.kt @@ -0,0 +1,8 @@ +package io.koraframework.kotlin.example.s3.aws + +import io.koraframework.config.common.annotation.ConfigSource + +@ConfigSource("my") +interface S3Config { + fun bucket(): String +} diff --git a/examples/kotlin/kora-kotlin-s3-client-aws/src/main/kotlin/io/koraframework/kotlin/example/s3/aws/SyncS3Client.kt b/examples/kotlin/kora-kotlin-s3-client-aws/src/main/kotlin/io/koraframework/kotlin/example/s3/aws/SyncS3Client.kt deleted file mode 100644 index 460af49b..00000000 --- a/examples/kotlin/kora-kotlin-s3-client-aws/src/main/kotlin/io/koraframework/kotlin/example/s3/aws/SyncS3Client.kt +++ /dev/null @@ -1,42 +0,0 @@ -package io.koraframework.kotlin.example.s3.aws - -import io.koraframework.common.annotation.Component -import io.koraframework.common.annotation.Root -import io.koraframework.s3.client.annotation.S3 -import io.koraframework.s3.client.model.* -import software.amazon.awssdk.core.ResponseInputStream -import software.amazon.awssdk.services.s3.model.DeleteObjectResponse -import software.amazon.awssdk.services.s3.model.GetObjectResponse -import software.amazon.awssdk.services.s3.model.ListObjectsV2Response -import software.amazon.awssdk.services.s3.model.PutObjectResponse - -@S3.Client("my") -interface SyncS3Client { - @S3.Get("pre-{key}") - fun getObject(key: String): S3Object - - @S3.Get - fun getObjectMeta(key: String): S3ObjectMeta - - @S3.Get - fun getObjects(keys: List): List - - @S3.Get - fun getObjectMetas(keys: List): List - - @S3.List("pre-{prefix}") - fun listObject(prefix: String): S3ObjectList - - @S3.List(value = "pre-{prefix}", limit = 50) - fun listObjectMeta(prefix: String): S3ObjectMetaList - - @S3.Put("pre-{key}") - fun putObject(key: String, obj: S3Body): S3ObjectUpload - - @S3.Delete("pre-{key}") - fun deleteObject(key: String) - - @S3.Delete - fun deleteObjects(keys: List) -} - diff --git a/examples/kotlin/kora-kotlin-s3-client-aws/src/main/resources/application.conf b/examples/kotlin/kora-kotlin-s3-client-aws/src/main/resources/application.conf index c02cb9b6..fcd5ea27 100644 --- a/examples/kotlin/kora-kotlin-s3-client-aws/src/main/resources/application.conf +++ b/examples/kotlin/kora-kotlin-s3-client-aws/src/main/resources/application.conf @@ -1,8 +1,11 @@ -s3client { +s3client.aws { url = ${S3_URL} - accessKey = ${S3_ACCESS_KEY} - secretKey = ${S3_SECRET_KEY} - aws.requestTimeout = 5s + requestTimeout = 5s + + credentials { + accessKey = ${S3_ACCESS_KEY} + secretKey = ${S3_SECRET_KEY} + } } diff --git a/examples/kotlin/kora-kotlin-s3-client-aws/src/test/kotlin/io/koraframework/kotlin/example/s3/aws/AwsS3ClientTests.kt b/examples/kotlin/kora-kotlin-s3-client-aws/src/test/kotlin/io/koraframework/kotlin/example/s3/aws/AwsS3ClientTests.kt deleted file mode 100644 index efc5f5d3..00000000 --- a/examples/kotlin/kora-kotlin-s3-client-aws/src/test/kotlin/io/koraframework/kotlin/example/s3/aws/AwsS3ClientTests.kt +++ /dev/null @@ -1,86 +0,0 @@ -package io.koraframework.kotlin.example.s3.aws - -import io.goodforgod.testcontainers.extensions.ContainerMode -import io.goodforgod.testcontainers.extensions.minio.Bucket -import io.goodforgod.testcontainers.extensions.minio.ConnectionMinio -import io.goodforgod.testcontainers.extensions.minio.MinioConnection -import io.goodforgod.testcontainers.extensions.minio.TestcontainersMinio -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test -import io.koraframework.s3.client.model.S3Body -import io.koraframework.test.extension.junit5.KoraAppTest -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier -import io.koraframework.test.extension.junit5.KoraConfigModification -import io.koraframework.test.extension.junit5.TestComponent -import software.amazon.awssdk.services.s3.S3Client -import software.amazon.awssdk.services.s3.model.NoSuchKeyException -import java.nio.charset.StandardCharsets - -@TestcontainersMinio( - mode = ContainerMode.PER_RUN, - bucket = Bucket(value = [AwsS3ClientTests.BUCKET], create = Bucket.Mode.PER_METHOD, drop = Bucket.Mode.PER_METHOD) -) -@KoraAppTest(Application::class) -class AwsS3ClientTests : KoraAppTestConfigModifier { - - @ConnectionMinio - lateinit var minioConnection: MinioConnection - - @TestComponent - lateinit var client: AwsS3Client - - @TestComponent - lateinit var s3Client: S3Client - - override fun config(): KoraConfigModification = KoraConfigModification - .ofSystemProperty("S3_URL", minioConnection.params().uri().toString()) - .withSystemProperty("S3_ACCESS_KEY", minioConnection.params().accessKey()) - .withSystemProperty("S3_SECRET_KEY", minioConnection.params().secretKey()) - .withSystemProperty("S3_BUCKET", BUCKET) - - @Test - fun putAndGetObject() { - val value = "value".toByteArray(StandardCharsets.UTF_8) - client.putObject("k1", S3Body.ofBytes(value)) - - val found = client.getObject("k1") - assertNotNull(found) - - assertThrows(NoSuchKeyException::class.java) { client.getObject("k2") } - } - - @Test - fun putAndGetMeta() { - val value = "value".toByteArray(StandardCharsets.UTF_8) - client.putObject("k1", S3Body.ofBytes(value)) - - val found = client.getObjectMeta("pre-k1") - assertNotNull(found) - - assertThrows(NoSuchKeyException::class.java) { client.getObjectMeta("k2") } - } - - @Test - fun putAndListMetas() { - val value = "value".toByteArray(StandardCharsets.UTF_8) - client.putObject("k1", S3Body.ofBytes(value)) - client.putObject("k2", S3Body.ofBytes(value)) - - val found = client.listObjectMeta("k") - assertEquals(2, found.contents().size) - } - - @Test - fun putAndDelete() { - val value = "value".toByteArray(StandardCharsets.UTF_8) - client.putObject("k1", S3Body.ofBytes(value)) - - client.deleteObject("k1") - - assertThrows(NoSuchKeyException::class.java) { client.getObject("k1") } - } - - companion object { - const val BUCKET = "simple" - } -} diff --git a/examples/kotlin/kora-kotlin-s3-client-aws/src/test/kotlin/io/koraframework/kotlin/example/s3/aws/AwsS3ServiceTests.kt b/examples/kotlin/kora-kotlin-s3-client-aws/src/test/kotlin/io/koraframework/kotlin/example/s3/aws/AwsS3ServiceTests.kt new file mode 100644 index 00000000..37e04a9f --- /dev/null +++ b/examples/kotlin/kora-kotlin-s3-client-aws/src/test/kotlin/io/koraframework/kotlin/example/s3/aws/AwsS3ServiceTests.kt @@ -0,0 +1,119 @@ +package io.koraframework.kotlin.example.s3.aws + +import io.goodforgod.testcontainers.extensions.ContainerMode +import io.goodforgod.testcontainers.extensions.minio.Bucket +import io.goodforgod.testcontainers.extensions.minio.ConnectionMinio +import io.goodforgod.testcontainers.extensions.minio.MinioConnection +import io.goodforgod.testcontainers.extensions.minio.TestcontainersMinio +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import io.koraframework.test.extension.junit5.KoraAppTest +import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier +import io.koraframework.test.extension.junit5.KoraConfigModification +import io.koraframework.test.extension.junit5.TestComponent +import software.amazon.awssdk.services.s3.model.NoSuchKeyException +import java.nio.charset.StandardCharsets + +@TestcontainersMinio( + mode = ContainerMode.PER_RUN, + bucket = Bucket( + value = [AwsS3ServiceTests.BUCKET], + create = Bucket.Mode.PER_METHOD, + drop = Bucket.Mode.PER_METHOD + ) +) +@KoraAppTest(Application::class) +class AwsS3ServiceTests : KoraAppTestConfigModifier { + + companion object { + const val BUCKET = "simple" + } + + @ConnectionMinio + lateinit var minioConnection: MinioConnection + + @TestComponent + lateinit var service: AwsS3Service + + override fun config(): KoraConfigModification = KoraConfigModification + .ofSystemProperty("S3_URL", minioConnection.params().uri().toString()) + .withSystemProperty("S3_ACCESS_KEY", minioConnection.params().accessKey()) + .withSystemProperty("S3_SECRET_KEY", minioConnection.params().secretKey()) + .withSystemProperty("S3_BUCKET", BUCKET) + + @Test + fun putAndGetObject() { + // given + val key = "k1" + val value = "value".toByteArray(StandardCharsets.UTF_8) + service.putObject(key, value) + + // when + service.getObject(key).use { found -> + assertArrayEquals(value, found.readAllBytes()) + } + + // then + assertThrows(NoSuchKeyException::class.java) { service.getObject("k2") } + } + + @Test + fun putAndGetMeta() { + // given + val key = "k1" + val value = "value".toByteArray(StandardCharsets.UTF_8) + service.putObject(key, value) + + // when + val found = service.getObjectMeta(key) + assertEquals(value.size.toLong(), found.contentLength()) + + // then + assertThrows(NoSuchKeyException::class.java) { service.getObjectMeta("k2") } + } + + @Test + fun putAndListObjects() { + // given + val value = "value".toByteArray(StandardCharsets.UTF_8) + service.putObject("k1", value) + service.putObject("k2", value) + + // when + val found = service.listObjects("k") + + // then + assertEquals(2, found.contents().size) + } + + @Test + fun putAndDelete() { + // given + val key = "k1" + val value = "value".toByteArray(StandardCharsets.UTF_8) + service.putObject(key, value) + + // when + service.deleteObject(key) + + // then + assertThrows(NoSuchKeyException::class.java) { service.getObject(key) } + } + + @Test + fun putAndDeleteMany() { + // given + val value = "value".toByteArray(StandardCharsets.UTF_8) + service.putObject("k1", value) + service.putObject("k2", value) + + // when + service.deleteObjects(listOf("k1", "k2")) + + // then + assertThrows(NoSuchKeyException::class.java) { service.getObject("k1") } + assertThrows(NoSuchKeyException::class.java) { service.getObject("k2") } + } +} diff --git a/examples/kotlin/kora-kotlin-s3-client-aws/src/test/kotlin/io/koraframework/kotlin/example/s3/aws/SyncS3ClientTests.kt b/examples/kotlin/kora-kotlin-s3-client-aws/src/test/kotlin/io/koraframework/kotlin/example/s3/aws/SyncS3ClientTests.kt deleted file mode 100644 index 3ec96c5c..00000000 --- a/examples/kotlin/kora-kotlin-s3-client-aws/src/test/kotlin/io/koraframework/kotlin/example/s3/aws/SyncS3ClientTests.kt +++ /dev/null @@ -1,131 +0,0 @@ -package io.koraframework.kotlin.example.s3.aws - -import io.goodforgod.testcontainers.extensions.ContainerMode -import io.goodforgod.testcontainers.extensions.minio.Bucket -import io.goodforgod.testcontainers.extensions.minio.ConnectionMinio -import io.goodforgod.testcontainers.extensions.minio.MinioConnection -import io.goodforgod.testcontainers.extensions.minio.TestcontainersMinio -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test -import io.koraframework.s3.client.S3NotFoundException -import io.koraframework.s3.client.model.S3Body -import io.koraframework.test.extension.junit5.KoraAppTest -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier -import io.koraframework.test.extension.junit5.KoraConfigModification -import io.koraframework.test.extension.junit5.TestComponent -import software.amazon.awssdk.services.s3.S3Client -import java.net.http.HttpRequest -import java.nio.charset.StandardCharsets - -@TestcontainersMinio( - mode = ContainerMode.PER_RUN, - bucket = Bucket(value = [SyncS3ClientTests.BUCKET], create = Bucket.Mode.PER_METHOD, drop = Bucket.Mode.PER_METHOD) -) -@KoraAppTest(Application::class) -class SyncS3ClientTests : KoraAppTestConfigModifier { - - @ConnectionMinio - lateinit var minioConnection: MinioConnection - - @TestComponent - lateinit var client: SyncS3Client - - @TestComponent - lateinit var s3Client: S3Client - - override fun config(): KoraConfigModification = KoraConfigModification - .ofSystemProperty("S3_URL", minioConnection.params().uri().toString()) - .withSystemProperty("S3_ACCESS_KEY", minioConnection.params().accessKey()) - .withSystemProperty("S3_SECRET_KEY", minioConnection.params().secretKey()) - .withSystemProperty("S3_BUCKET", BUCKET) - - @Test - fun putAndGetObject() { - val value = "value".toByteArray(StandardCharsets.UTF_8) - client.putObject("k1", S3Body.ofPublisher(HttpRequest.BodyPublishers.ofByteArray(value), value.size.toLong())) - - val found = client.getObject("k1") - assertNotNull(found) - assertArrayEquals(value, found.body().asBytes()) - - assertThrows(S3NotFoundException::class.java) { client.getObject("k2") } - } - - @Test - fun putAndGetMeta() { - val value = "value".toByteArray(StandardCharsets.UTF_8) - client.putObject("k1", S3Body.ofBytes(value)) - - val found = client.getObjectMeta("pre-k1") - assertNotNull(found) - - assertThrows(S3NotFoundException::class.java) { client.getObjectMeta("k2") } - } - - @Test - fun putAndGetManyObjects() { - val value = "value".toByteArray(StandardCharsets.UTF_8) - client.putObject("k1", S3Body.ofBytes(value)) - client.putObject("k2", S3Body.ofBytes(value)) - - val found = client.getObjects(listOf("pre-k1", "pre-k2")) - assertEquals(2, found.size) - found.forEach { assertEquals("value", String(it.body().asBytes(), StandardCharsets.UTF_8)) } - } - - @Test - fun putAndGetManyMetas() { - val value = "value".toByteArray(StandardCharsets.UTF_8) - client.putObject("k1", S3Body.ofBytes(value)) - client.putObject("k2", S3Body.ofBytes(value)) - - val found = client.getObjectMetas(listOf("pre-k1", "pre-k2")) - assertEquals(2, found.size) - } - - @Test - fun putAndListObjects() { - val value = "value".toByteArray(StandardCharsets.UTF_8) - client.putObject("k1", S3Body.ofBytes(value)) - client.putObject("k2", S3Body.ofBytes(value)) - - val found = client.listObject("k") - assertEquals(2, found.metas().size) - found.objects().forEach { assertEquals("value", String(it.body().asBytes(), StandardCharsets.UTF_8)) } - } - - @Test - fun putAndListMetas() { - val value = "value".toByteArray(StandardCharsets.UTF_8) - client.putObject("k1", S3Body.ofBytes(value)) - client.putObject("k2", S3Body.ofBytes(value)) - - val found = client.listObjectMeta("k") - assertEquals(2, found.metas().size) - } - - @Test - fun putAndDelete() { - val value = "value".toByteArray(StandardCharsets.UTF_8) - client.putObject("k1", S3Body.ofBytes(value)) - - client.deleteObject("k1") - - assertThrows(S3NotFoundException::class.java) { client.getObject("k1") } - } - - @Test - fun putAndDeleteMany() { - val value = "value".toByteArray(StandardCharsets.UTF_8) - client.putObject("k1", S3Body.ofBytes(value)) - client.putObject("k2", S3Body.ofBytes(value)) - - client.deleteObjects(listOf("pre-k1", "pre-k2")) - assertThrows(S3NotFoundException::class.java) { client.getObject("k1") } - assertThrows(S3NotFoundException::class.java) { client.getObject("k2") } - } - - companion object { - const val BUCKET = "simple" - } -} diff --git a/examples/java/kora-java-database-vertx/Dockerfile b/examples/kotlin/kora-kotlin-s3-client-kora/Dockerfile similarity index 100% rename from examples/java/kora-java-database-vertx/Dockerfile rename to examples/kotlin/kora-kotlin-s3-client-kora/Dockerfile diff --git a/examples/kotlin/kora-kotlin-s3-client-minio/README.md b/examples/kotlin/kora-kotlin-s3-client-kora/README.md similarity index 100% rename from examples/kotlin/kora-kotlin-s3-client-minio/README.md rename to examples/kotlin/kora-kotlin-s3-client-kora/README.md diff --git a/examples/kotlin/kora-kotlin-s3-client-minio/build.gradle.kts b/examples/kotlin/kora-kotlin-s3-client-kora/build.gradle.kts similarity index 73% rename from examples/kotlin/kora-kotlin-s3-client-minio/build.gradle.kts rename to examples/kotlin/kora-kotlin-s3-client-kora/build.gradle.kts index 2f4dab21..b0841231 100644 --- a/examples/kotlin/kora-kotlin-s3-client-minio/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-s3-client-kora/build.gradle.kts @@ -7,27 +7,18 @@ plugins { id("com.google.devtools.ksp") version ("2.3.11") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") - implementation("io.koraframework.experimental:s3-client-minio") + implementation("io.koraframework.experimental:s3-client-kora") implementation("io.koraframework:http-client-ok") - implementation("io.projectreactor:reactor-core:3.6.18") implementation("io.koraframework:logging-logback") implementation("io.koraframework:config-hocon") testImplementation("org.testcontainers:junit-jupiter:1.21.4") testImplementation("org.testcontainers:minio:1.21.4") - testImplementation("io.goodforgod:testcontainers-extensions-minio:0.13.1") + testImplementation("io.goodforgod:testcontainers-extensions-minio:0.15.0") testImplementation("io.koraframework:test-junit5") } diff --git a/examples/kotlin/kora-kotlin-s3-client-minio/docker-compose.yml b/examples/kotlin/kora-kotlin-s3-client-kora/docker-compose.yml similarity index 100% rename from examples/kotlin/kora-kotlin-s3-client-minio/docker-compose.yml rename to examples/kotlin/kora-kotlin-s3-client-kora/docker-compose.yml diff --git a/examples/kotlin/kora-kotlin-s3-client-minio/gradle.properties b/examples/kotlin/kora-kotlin-s3-client-kora/gradle.properties similarity index 100% rename from examples/kotlin/kora-kotlin-s3-client-minio/gradle.properties rename to examples/kotlin/kora-kotlin-s3-client-kora/gradle.properties diff --git a/examples/kotlin/kora-kotlin-s3-client-minio/src/main/kotlin/io/koraframework/kotlin/example/s3/minio/Application.kt b/examples/kotlin/kora-kotlin-s3-client-kora/src/main/kotlin/io/koraframework/kotlin/example/s3/kora/Application.kt similarity index 63% rename from examples/kotlin/kora-kotlin-s3-client-minio/src/main/kotlin/io/koraframework/kotlin/example/s3/minio/Application.kt rename to examples/kotlin/kora-kotlin-s3-client-kora/src/main/kotlin/io/koraframework/kotlin/example/s3/kora/Application.kt index b67fed6c..114811c4 100644 --- a/examples/kotlin/kora-kotlin-s3-client-minio/src/main/kotlin/io/koraframework/kotlin/example/s3/minio/Application.kt +++ b/examples/kotlin/kora-kotlin-s3-client-kora/src/main/kotlin/io/koraframework/kotlin/example/s3/kora/Application.kt @@ -1,14 +1,14 @@ -package io.koraframework.kotlin.example.s3.minio +package io.koraframework.kotlin.example.s3.kora import io.koraframework.application.graph.KoraApplication import io.koraframework.common.annotation.KoraApp import io.koraframework.config.hocon.HoconConfigModule import io.koraframework.http.client.ok.OkHttpClientModule import io.koraframework.logging.logback.LogbackModule -import io.koraframework.s3.client.minio.MinioS3ClientModule +import io.koraframework.s3.client.kora.KoraS3ClientModule @KoraApp -interface Application : HoconConfigModule, LogbackModule, MinioS3ClientModule, OkHttpClientModule +interface Application : HoconConfigModule, LogbackModule, KoraS3ClientModule, OkHttpClientModule fun main() { KoraApplication.run { ApplicationGraph.graph() } diff --git a/examples/kotlin/kora-kotlin-s3-client-minio/src/main/kotlin/io/koraframework/kotlin/example/s3/minio/RootService.kt b/examples/kotlin/kora-kotlin-s3-client-kora/src/main/kotlin/io/koraframework/kotlin/example/s3/kora/RootService.kt similarity index 55% rename from examples/kotlin/kora-kotlin-s3-client-minio/src/main/kotlin/io/koraframework/kotlin/example/s3/minio/RootService.kt rename to examples/kotlin/kora-kotlin-s3-client-kora/src/main/kotlin/io/koraframework/kotlin/example/s3/kora/RootService.kt index 940476b7..a34e18ab 100644 --- a/examples/kotlin/kora-kotlin-s3-client-minio/src/main/kotlin/io/koraframework/kotlin/example/s3/minio/RootService.kt +++ b/examples/kotlin/kora-kotlin-s3-client-kora/src/main/kotlin/io/koraframework/kotlin/example/s3/kora/RootService.kt @@ -1,11 +1,8 @@ -package io.koraframework.kotlin.example.s3.minio +package io.koraframework.kotlin.example.s3.kora import io.koraframework.common.annotation.Component import io.koraframework.common.annotation.Root -import io.koraframework.s3.client.annotation.S3 -import io.koraframework.s3.client.model.* @Root @Component class RootService(private val syncS3Client: SyncS3Client) - diff --git a/examples/kotlin/kora-kotlin-s3-client-kora/src/main/kotlin/io/koraframework/kotlin/example/s3/kora/SyncS3Client.kt b/examples/kotlin/kora-kotlin-s3-client-kora/src/main/kotlin/io/koraframework/kotlin/example/s3/kora/SyncS3Client.kt new file mode 100644 index 00000000..1cec01ce --- /dev/null +++ b/examples/kotlin/kora-kotlin-s3-client-kora/src/main/kotlin/io/koraframework/kotlin/example/s3/kora/SyncS3Client.kt @@ -0,0 +1,36 @@ +package io.koraframework.kotlin.example.s3.kora + +import io.koraframework.s3.client.kora.annotation.S3 +import io.koraframework.s3.client.kora.model.response.GetObjectResult +import io.koraframework.s3.client.kora.model.response.HeadObjectResult +import io.koraframework.s3.client.kora.model.response.ListBucketResult + +/** + * Bucket name is read from the `my.bucket` configuration path: a leading dot in `@S3.Bucket` + * makes the path relative to the client path declared in `@S3.Client`. + */ +@S3.Client("my") +@S3.Bucket(".bucket") +interface SyncS3Client { + + @S3.Get("pre-{key}") + fun getObject(key: String): GetObjectResult + + @S3.Get("pre-{key}") + fun getObjectAsBytes(key: String): ByteArray + + @S3.Head("pre-{key}") + fun getObjectMeta(key: String): HeadObjectResult + + @S3.List("pre-{prefix}") + fun listObjects(prefix: String): ListBucketResult + + @S3.List("pre-{prefix}") + fun listObjectKeys(prefix: String): List + + @S3.Put("pre-{key}") + fun putObject(key: String, value: ByteArray): String + + @S3.Delete("pre-{key}") + fun deleteObject(key: String) +} diff --git a/examples/java/kora-java-s3-client-minio/src/main/resources/application.conf b/examples/kotlin/kora-kotlin-s3-client-kora/src/main/resources/application.conf similarity index 55% rename from examples/java/kora-java-s3-client-minio/src/main/resources/application.conf rename to examples/kotlin/kora-kotlin-s3-client-kora/src/main/resources/application.conf index 9a94ec10..9712519a 100644 --- a/examples/java/kora-java-s3-client-minio/src/main/resources/application.conf +++ b/examples/kotlin/kora-kotlin-s3-client-kora/src/main/resources/application.conf @@ -1,11 +1,12 @@ -s3client { - url = ${S3_URL} - accessKey = ${S3_ACCESS_KEY} - secretKey = ${S3_SECRET_KEY} -} - +my { + endpoint = ${S3_URL} + bucket = ${S3_BUCKET} -my.bucket = ${S3_BUCKET} + credentials { + accessKey = ${S3_ACCESS_KEY} + secretKey = ${S3_SECRET_KEY} + } +} httpClient { diff --git a/examples/graalvm/kora-java-graalvm-crud-vertx/src/main/resources/logback.xml b/examples/kotlin/kora-kotlin-s3-client-kora/src/main/resources/logback.xml similarity index 100% rename from examples/graalvm/kora-java-graalvm-crud-vertx/src/main/resources/logback.xml rename to examples/kotlin/kora-kotlin-s3-client-kora/src/main/resources/logback.xml diff --git a/examples/kotlin/kora-kotlin-s3-client-kora/src/test/kotlin/io/koraframework/kotlin/example/s3/kora/SyncS3ClientTests.kt b/examples/kotlin/kora-kotlin-s3-client-kora/src/test/kotlin/io/koraframework/kotlin/example/s3/kora/SyncS3ClientTests.kt new file mode 100644 index 00000000..c29a8607 --- /dev/null +++ b/examples/kotlin/kora-kotlin-s3-client-kora/src/test/kotlin/io/koraframework/kotlin/example/s3/kora/SyncS3ClientTests.kt @@ -0,0 +1,135 @@ +package io.koraframework.kotlin.example.s3.kora + +import io.goodforgod.testcontainers.extensions.ContainerMode +import io.goodforgod.testcontainers.extensions.minio.Bucket +import io.goodforgod.testcontainers.extensions.minio.ConnectionMinio +import io.goodforgod.testcontainers.extensions.minio.MinioConnection +import io.goodforgod.testcontainers.extensions.minio.TestcontainersMinio +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertIterableEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import io.koraframework.s3.client.kora.exception.S3ClientNoSuchKeyException +import io.koraframework.test.extension.junit5.KoraAppTest +import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier +import io.koraframework.test.extension.junit5.KoraConfigModification +import io.koraframework.test.extension.junit5.TestComponent +import java.nio.charset.StandardCharsets + +@TestcontainersMinio( + mode = ContainerMode.PER_RUN, + bucket = Bucket( + value = [SyncS3ClientTests.BUCKET], + create = Bucket.Mode.PER_METHOD, + drop = Bucket.Mode.PER_METHOD + ) +) +@KoraAppTest(Application::class) +class SyncS3ClientTests : KoraAppTestConfigModifier { + + companion object { + const val BUCKET = "simple" + } + + @ConnectionMinio + lateinit var minioConnection: MinioConnection + + @TestComponent + lateinit var client: SyncS3Client + + override fun config(): KoraConfigModification = KoraConfigModification + .ofSystemProperty("S3_URL", minioConnection.params().uri().toString()) + .withSystemProperty("S3_ACCESS_KEY", minioConnection.params().accessKey()) + .withSystemProperty("S3_SECRET_KEY", minioConnection.params().secretKey()) + .withSystemProperty("S3_BUCKET", BUCKET) + + @Test + fun putAndGetObject() { + // given + val key = "k1" + val value = "value".toByteArray(StandardCharsets.UTF_8) + client.putObject(key, value) + + // when + client.getObject(key).use { found -> + found.body().asInputStream().use { body -> + assertArrayEquals(value, body.readAllBytes()) + } + } + + // then + assertThrows(S3ClientNoSuchKeyException::class.java) { client.getObject("k2") } + } + + @Test + fun putAndGetObjectAsBytes() { + // given + val key = "k1" + val value = "value".toByteArray(StandardCharsets.UTF_8) + client.putObject(key, value) + + // when + val found = client.getObjectAsBytes(key) + + // then + assertArrayEquals(value, found) + } + + @Test + fun putAndGetMeta() { + // given + val key = "k1" + val value = "value".toByteArray(StandardCharsets.UTF_8) + client.putObject(key, value) + + // when + val found = client.getObjectMeta(key) + assertEquals(value.size.toLong(), found.size()) + + // then + assertThrows(S3ClientNoSuchKeyException::class.java) { client.getObjectMeta("k2") } + } + + @Test + fun putAndListObjects() { + // given + val value = "value".toByteArray(StandardCharsets.UTF_8) + client.putObject("k1", value) + client.putObject("k2", value) + + // when + val found = client.listObjects("k") + + // then + assertEquals(2, found.items().size) + } + + @Test + fun putAndListObjectKeys() { + // given + val value = "value".toByteArray(StandardCharsets.UTF_8) + client.putObject("k1", value) + client.putObject("k2", value) + + // when + val found = client.listObjectKeys("k") + + // then + assertIterableEquals(listOf("pre-k1", "pre-k2"), found) + } + + @Test + fun putAndDelete() { + // given + val key = "k1" + val value = "value".toByteArray(StandardCharsets.UTF_8) + client.putObject(key, value) + + // when + client.deleteObject(key) + + // then + assertThrows(S3ClientNoSuchKeyException::class.java) { client.getObject(key) } + } +} diff --git a/examples/kotlin/kora-kotlin-s3-client-minio/src/test/resources/logback-test.xml b/examples/kotlin/kora-kotlin-s3-client-kora/src/test/resources/logback-test.xml similarity index 100% rename from examples/kotlin/kora-kotlin-s3-client-minio/src/test/resources/logback-test.xml rename to examples/kotlin/kora-kotlin-s3-client-kora/src/test/resources/logback-test.xml diff --git a/examples/kotlin/kora-kotlin-s3-client-minio/Dockerfile b/examples/kotlin/kora-kotlin-s3-client-minio/Dockerfile deleted file mode 100644 index c22138f1..00000000 --- a/examples/kotlin/kora-kotlin-s3-client-minio/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -ARG RUN_IMAGE=eclipse-temurin:25-jre-jammy -FROM ${RUN_IMAGE} - -ARG TARGET_DIR=/opt/app -ARG SOURCE_DIR=build/distributions - -COPY $SOURCE_DIR/*.tar application.tar -RUN mkdir $TARGET_DIR -RUN tar -xf application.tar -C $TARGET_DIR -RUN rm application.tar - -ARG DOCKER_USER=app -RUN groupadd -r $DOCKER_USER && useradd -rg $DOCKER_USER $DOCKER_USER -USER $DOCKER_USER - -EXPOSE 8080/tcp -EXPOSE 8085/tcp -CMD [ "/opt/app/application/bin/application" ] \ No newline at end of file diff --git a/examples/kotlin/kora-kotlin-s3-client-minio/src/main/kotlin/io/koraframework/kotlin/example/s3/minio/SyncS3Client.kt b/examples/kotlin/kora-kotlin-s3-client-minio/src/main/kotlin/io/koraframework/kotlin/example/s3/minio/SyncS3Client.kt deleted file mode 100644 index c855d9a5..00000000 --- a/examples/kotlin/kora-kotlin-s3-client-minio/src/main/kotlin/io/koraframework/kotlin/example/s3/minio/SyncS3Client.kt +++ /dev/null @@ -1,37 +0,0 @@ -package io.koraframework.kotlin.example.s3.minio - -import io.koraframework.common.annotation.Component -import io.koraframework.common.annotation.Root -import io.koraframework.s3.client.annotation.S3 -import io.koraframework.s3.client.model.* - -@S3.Client("my") -interface SyncS3Client { - @S3.Get("pre-{key}") - fun getObject(key: String): S3Object - - @S3.Get - fun getObjectMeta(key: String): S3ObjectMeta - - @S3.Get - fun getObjects(keys: List): List - - @S3.Get - fun getObjectMetas(keys: List): List - - @S3.List("pre-{prefix}") - fun listObject(prefix: String): S3ObjectList - - @S3.List(value = "pre-{prefix}", limit = 50) - fun listObjectMeta(prefix: String): S3ObjectMetaList - - @S3.Put("pre-{key}") - fun putObject(key: String, obj: S3Body): S3ObjectUpload - - @S3.Delete("pre-{key}") - fun deleteObject(key: String) - - @S3.Delete - fun deleteObjects(keys: List) -} - diff --git a/examples/kotlin/kora-kotlin-s3-client-minio/src/main/resources/logback.xml b/examples/kotlin/kora-kotlin-s3-client-minio/src/main/resources/logback.xml deleted file mode 100644 index 184638b5..00000000 --- a/examples/kotlin/kora-kotlin-s3-client-minio/src/main/resources/logback.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/examples/kotlin/kora-kotlin-s3-client-minio/src/test/kotlin/io/koraframework/kotlin/example/s3/minio/SyncS3ClientTests.kt b/examples/kotlin/kora-kotlin-s3-client-minio/src/test/kotlin/io/koraframework/kotlin/example/s3/minio/SyncS3ClientTests.kt deleted file mode 100644 index 81905f42..00000000 --- a/examples/kotlin/kora-kotlin-s3-client-minio/src/test/kotlin/io/koraframework/kotlin/example/s3/minio/SyncS3ClientTests.kt +++ /dev/null @@ -1,132 +0,0 @@ -package io.koraframework.kotlin.example.s3.minio - -import io.goodforgod.testcontainers.extensions.ContainerMode -import io.goodforgod.testcontainers.extensions.minio.Bucket -import io.goodforgod.testcontainers.extensions.minio.ConnectionMinio -import io.goodforgod.testcontainers.extensions.minio.MinioConnection -import io.goodforgod.testcontainers.extensions.minio.TestcontainersMinio -import io.minio.MinioClient -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test -import io.koraframework.s3.client.S3NotFoundException -import io.koraframework.s3.client.model.S3Body -import io.koraframework.test.extension.junit5.KoraAppTest -import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier -import io.koraframework.test.extension.junit5.KoraConfigModification -import io.koraframework.test.extension.junit5.TestComponent -import java.nio.charset.StandardCharsets - -@TestcontainersMinio( - mode = ContainerMode.PER_RUN, - bucket = Bucket(value = [SyncS3ClientTests.BUCKET], create = Bucket.Mode.PER_METHOD, drop = Bucket.Mode.PER_METHOD) -) -@KoraAppTest(Application::class) -class SyncS3ClientTests : KoraAppTestConfigModifier { - - @ConnectionMinio - lateinit var minioConnection: MinioConnection - - @TestComponent - lateinit var client: SyncS3Client - - @TestComponent - lateinit var s3Client: MinioClient - - override fun config(): KoraConfigModification = KoraConfigModification - .ofSystemProperty("S3_URL", minioConnection.params().uri().toString()) - .withSystemProperty("S3_ACCESS_KEY", minioConnection.params().accessKey()) - .withSystemProperty("S3_SECRET_KEY", minioConnection.params().secretKey()) - .withSystemProperty("S3_BUCKET", BUCKET) - - @Test - fun putAndGetObject() { - val key = "k1" - val value = "value".toByteArray(StandardCharsets.UTF_8) - client.putObject(key, S3Body.ofBytes(value)) - - val found = client.getObject(key) - assertNotNull(found) - assertTrue(value.contentEquals(found.body().asBytes())) - - assertThrows(S3NotFoundException::class.java) { client.getObject("k2") } - } - - @Test - fun putAndGetMeta() { - val key = "k1" - val value = "value".toByteArray(StandardCharsets.UTF_8) - client.putObject(key, S3Body.ofBytes(value)) - - val found = client.getObjectMeta("pre-$key") - assertNotNull(found) - - assertThrows(S3NotFoundException::class.java) { client.getObjectMeta("k2") } - } - - @Test - fun putAndGetManyObjects() { - val value = "value".toByteArray(StandardCharsets.UTF_8) - client.putObject("k1", S3Body.ofBytes(value)) - client.putObject("k2", S3Body.ofBytes(value)) - - val found = client.getObjects(listOf("pre-k1", "pre-k2")) - assertEquals(2, found.size) - found.forEach { assertEquals("value", String(it.body().asBytes(), StandardCharsets.UTF_8)) } - } - - @Test - fun putAndGetManyMetas() { - val value = "value".toByteArray(StandardCharsets.UTF_8) - client.putObject("k1", S3Body.ofBytes(value)) - client.putObject("k2", S3Body.ofBytes(value)) - - val found = client.getObjectMetas(listOf("pre-k1", "pre-k2")) - assertEquals(2, found.size) - } - - @Test - fun putAndListObjects() { - val value = "value".toByteArray(StandardCharsets.UTF_8) - client.putObject("k1", S3Body.ofBytes(value)) - client.putObject("k2", S3Body.ofBytes(value)) - - val found = client.listObject("k") - assertEquals(2, found.metas().size) - found.objects().forEach { assertEquals("value", String(it.body().asBytes(), StandardCharsets.UTF_8)) } - } - - @Test - fun putAndListMetas() { - val value = "value".toByteArray(StandardCharsets.UTF_8) - client.putObject("k1", S3Body.ofBytes(value)) - client.putObject("k2", S3Body.ofBytes(value)) - - val found = client.listObjectMeta("k") - assertEquals(2, found.metas().size) - } - - @Test - fun putAndDelete() { - val value = "value".toByteArray(StandardCharsets.UTF_8) - client.putObject("k1", S3Body.ofBytes(value)) - - client.deleteObject("k1") - - assertThrows(S3NotFoundException::class.java) { client.getObject("k1") } - } - - @Test - fun putAndDeleteMany() { - val value = "value".toByteArray(StandardCharsets.UTF_8) - client.putObject("k1", S3Body.ofBytes(value)) - client.putObject("k2", S3Body.ofBytes(value)) - - client.deleteObjects(listOf("pre-k1", "pre-k2")) - assertThrows(S3NotFoundException::class.java) { client.getObject("k1") } - assertThrows(S3NotFoundException::class.java) { client.getObject("k2") } - } - - companion object { - const val BUCKET = "simple" - } -} diff --git a/examples/kotlin/kora-kotlin-scheduling-jdk/build.gradle.kts b/examples/kotlin/kora-kotlin-scheduling-jdk/build.gradle.kts index 7f639720..11d4d105 100644 --- a/examples/kotlin/kora-kotlin-scheduling-jdk/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-scheduling-jdk/build.gradle.kts @@ -7,17 +7,9 @@ plugins { id("com.google.devtools.ksp") version ("2.3.11") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:scheduling-jdk") implementation("io.koraframework:config-hocon") implementation("io.koraframework:logging-logback") diff --git a/examples/kotlin/kora-kotlin-scheduling-quartz/build.gradle.kts b/examples/kotlin/kora-kotlin-scheduling-quartz/build.gradle.kts index cc39f419..af8ac292 100644 --- a/examples/kotlin/kora-kotlin-scheduling-quartz/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-scheduling-quartz/build.gradle.kts @@ -7,17 +7,9 @@ plugins { id("com.google.devtools.ksp") version ("2.3.11") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:scheduling-quartz") implementation("io.koraframework:config-hocon") implementation("io.koraframework:logging-logback") diff --git a/examples/kotlin/kora-kotlin-soap-client/build.gradle.kts b/examples/kotlin/kora-kotlin-soap-client/build.gradle.kts index 6d20e376..4fa17b95 100644 --- a/examples/kotlin/kora-kotlin-soap-client/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-soap-client/build.gradle.kts @@ -8,17 +8,9 @@ plugins { id("com.github.bjornvester.wsdl2java") version ("2.0.2") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:json-common") implementation("io.koraframework:http-client-jdk") @@ -30,12 +22,11 @@ dependencies { exclude(group = "org.glassfish.jaxb") exclude(group = "com.sun.activation") } - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.8.1") implementation("io.koraframework:logging-logback") implementation("io.koraframework:config-hocon") testImplementation("io.koraframework:test-junit5") - testImplementation("io.goodforgod:testcontainers-extensions-mockserver:0.13.1") + testImplementation("io.goodforgod:testcontainers-extensions-mockserver:0.15.0") } kotlin { diff --git a/examples/kotlin/kora-kotlin-telemetry/build.gradle.kts b/examples/kotlin/kora-kotlin-telemetry/build.gradle.kts index 0e7e056c..ffbe8c74 100644 --- a/examples/kotlin/kora-kotlin-telemetry/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-telemetry/build.gradle.kts @@ -7,17 +7,9 @@ plugins { id("com.google.devtools.ksp") version ("2.3.11") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:micrometer-module") diff --git a/examples/kotlin/kora-kotlin-telemetry/src/main/resources/application.conf b/examples/kotlin/kora-kotlin-telemetry/src/main/resources/application.conf index f1684316..acbd43cc 100644 --- a/examples/kotlin/kora-kotlin-telemetry/src/main/resources/application.conf +++ b/examples/kotlin/kora-kotlin-telemetry/src/main/resources/application.conf @@ -1,8 +1,8 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 - privateApiHttpLivenessPath = "/liveness" - privateApiHttpReadinessPath = "/readiness" + port = 8080 + system.port = 8085 + system.livenessPath = "/liveness" + system.readinessPath = "/readiness" telemetry.logging.enabled = true } diff --git a/examples/kotlin/kora-kotlin-validation/build.gradle.kts b/examples/kotlin/kora-kotlin-validation/build.gradle.kts index 83f6724d..6eb9bc43 100644 --- a/examples/kotlin/kora-kotlin-validation/build.gradle.kts +++ b/examples/kotlin/kora-kotlin-validation/build.gradle.kts @@ -7,17 +7,9 @@ plugins { id("com.google.devtools.ksp") version ("2.3.11") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom); compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom); implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom); kspTest.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:validation-module") implementation("io.koraframework:config-hocon") implementation("io.koraframework:logging-logback") diff --git a/gradle.properties b/gradle.properties index 05a82bf8..3fd7c4ed 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ groupId=ru.tinkoff.kora koraVersion=2.0.0-SNAPSHOT -junitVersion=5.13.4 +junitVersion=6.1.3 ##### GRADLE ##### diff --git a/guides/java/kora-java-guide-cache-app/build.gradle b/guides/java/kora-java-guide-cache-app/build.gradle index cb69acc9..8e02ecd1 100644 --- a/guides/java/kora-java-guide-cache-app/build.gradle +++ b/guides/java/kora-java-guide-cache-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" diff --git a/guides/java/kora-java-guide-cache-app/src/main/resources/application.conf b/guides/java/kora-java-guide-cache-app/src/main/resources/application.conf index 3a0d07cf..2162fdef 100644 --- a/guides/java/kora-java-guide-cache-app/src/main/resources/application.conf +++ b/guides/java/kora-java-guide-cache-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } diff --git a/guides/java/kora-java-guide-cache-multi-level-app/build.gradle b/guides/java/kora-java-guide-cache-multi-level-app/build.gradle index b63183b5..fb9d5c4a 100644 --- a/guides/java/kora-java-guide-cache-multi-level-app/build.gradle +++ b/guides/java/kora-java-guide-cache-multi-level-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" @@ -36,7 +36,7 @@ dependencies { testImplementation platform("org.junit:junit-bom:$junitVersion") testImplementation "org.junit.jupiter:junit-jupiter" testImplementation "io.koraframework:test-junit5" - testImplementation "io.goodforgod:testcontainers-extensions-redis:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-redis:0.15.0" testImplementation "redis.clients:jedis:4.4.3" } diff --git a/guides/java/kora-java-guide-cache-multi-level-app/src/main/resources/application.conf b/guides/java/kora-java-guide-cache-multi-level-app/src/main/resources/application.conf index 838d1af7..d5052822 100644 --- a/guides/java/kora-java-guide-cache-multi-level-app/src/main/resources/application.conf +++ b/guides/java/kora-java-guide-cache-multi-level-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } diff --git a/guides/java/kora-java-guide-config-hocon-app/build.gradle b/guides/java/kora-java-guide-config-hocon-app/build.gradle index dcb5e8ec..80564d22 100644 --- a/guides/java/kora-java-guide-config-hocon-app/build.gradle +++ b/guides/java/kora-java-guide-config-hocon-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" diff --git a/guides/java/kora-java-guide-config-hocon-app/src/main/java/io/koraframework/guide/config/hocon/Application.java b/guides/java/kora-java-guide-config-hocon-app/src/main/java/io/koraframework/guide/config/hocon/Application.java index dcfbf3bb..86498638 100644 --- a/guides/java/kora-java-guide-config-hocon-app/src/main/java/io/koraframework/guide/config/hocon/Application.java +++ b/guides/java/kora-java-guide-config-hocon-app/src/main/java/io/koraframework/guide/config/hocon/Application.java @@ -4,7 +4,7 @@ import io.koraframework.common.annotation.KoraApp; import io.koraframework.common.annotation.Tag; import io.koraframework.config.common.Config; -import io.koraframework.config.common.extractor.ConfigMapper; +import io.koraframework.config.common.mapper.ConfigValueMapper; import io.koraframework.config.hocon.HoconConfigModule; import io.koraframework.logging.logback.LogbackModule; @@ -16,13 +16,13 @@ final class Lib1Tag {} final class Lib2Tag {} @Tag(Lib1Tag.class) - default LibConfig lib1Config(Config config, ConfigMapper extractor) { - return extractor.extract(config.get("libs.lib1")); + default LibConfig lib1Config(Config config, ConfigValueMapper extractor) { + return extractor.mapOrThrow(config.get("libs.lib1")); } @Tag(Lib2Tag.class) - default LibConfig lib2Config(Config config, ConfigMapper extractor) { - return extractor.extract(config.get("libs.lib2")); + default LibConfig lib2Config(Config config, ConfigValueMapper extractor) { + return extractor.mapOrThrow(config.get("libs.lib2")); } static void main(String[] args) { diff --git a/guides/java/kora-java-guide-config-yaml-app/build.gradle b/guides/java/kora-java-guide-config-yaml-app/build.gradle index 7b07fbe4..c0b7f2c3 100644 --- a/guides/java/kora-java-guide-config-yaml-app/build.gradle +++ b/guides/java/kora-java-guide-config-yaml-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" diff --git a/guides/java/kora-java-guide-config-yaml-app/src/main/java/io/koraframework/guide/config/yaml/Application.java b/guides/java/kora-java-guide-config-yaml-app/src/main/java/io/koraframework/guide/config/yaml/Application.java index 8c47fb69..b7363419 100644 --- a/guides/java/kora-java-guide-config-yaml-app/src/main/java/io/koraframework/guide/config/yaml/Application.java +++ b/guides/java/kora-java-guide-config-yaml-app/src/main/java/io/koraframework/guide/config/yaml/Application.java @@ -4,7 +4,7 @@ import io.koraframework.common.annotation.KoraApp; import io.koraframework.common.annotation.Tag; import io.koraframework.config.common.Config; -import io.koraframework.config.common.extractor.ConfigMapper; +import io.koraframework.config.common.mapper.ConfigValueMapper; import io.koraframework.config.yaml.YamlConfigModule; import io.koraframework.logging.logback.LogbackModule; @@ -16,13 +16,13 @@ final class Lib1Tag {} final class Lib2Tag {} @Tag(Lib1Tag.class) - default LibConfig lib1Config(Config config, ConfigMapper extractor) { - return extractor.extract(config.get("libs.lib1")); + default LibConfig lib1Config(Config config, ConfigValueMapper extractor) { + return extractor.mapOrThrow(config.get("libs.lib1")); } @Tag(Lib2Tag.class) - default LibConfig lib2Config(Config config, ConfigMapper extractor) { - return extractor.extract(config.get("libs.lib2")); + default LibConfig lib2Config(Config config, ConfigValueMapper extractor) { + return extractor.mapOrThrow(config.get("libs.lib2")); } static void main(String[] args) { diff --git a/guides/java/kora-java-guide-database-cassandra-app/build.gradle b/guides/java/kora-java-guide-database-cassandra-app/build.gradle index cc243851..539ed0d1 100644 --- a/guides/java/kora-java-guide-database-cassandra-app/build.gradle +++ b/guides/java/kora-java-guide-database-cassandra-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" @@ -34,7 +34,7 @@ dependencies { testImplementation platform("org.junit:junit-bom:$junitVersion") testImplementation "org.junit.jupiter:junit-jupiter" - testImplementation "io.goodforgod:testcontainers-extensions-scylla:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-scylla:0.15.0" testImplementation "io.koraframework:test-junit5" } diff --git a/guides/java/kora-java-guide-database-cassandra-app/src/main/resources/application.conf b/guides/java/kora-java-guide-database-cassandra-app/src/main/resources/application.conf index c0bc5877..27f4c372 100644 --- a/guides/java/kora-java-guide-database-cassandra-app/src/main/resources/application.conf +++ b/guides/java/kora-java-guide-database-cassandra-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } diff --git a/guides/java/kora-java-guide-database-jdbc-advanced-app/build.gradle b/guides/java/kora-java-guide-database-jdbc-advanced-app/build.gradle index cc5e230e..f1d0c9e5 100644 --- a/guides/java/kora-java-guide-database-jdbc-advanced-app/build.gradle +++ b/guides/java/kora-java-guide-database-jdbc-advanced-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" @@ -28,6 +28,9 @@ dependencies { implementation "io.koraframework:config-hocon" implementation "io.koraframework:database-flyway" + // с Flyway 10 поддержка конкретных СУБД вынесена в отдельные артефакты; + // без этого приложение падает на старте: "Unsupported Database: PostgreSQL" + implementation "org.flywaydb:flyway-database-postgresql:13.1.0" implementation "io.koraframework:database-jdbc" implementation "io.koraframework:http-server-undertow" implementation "io.koraframework:json-common" @@ -36,7 +39,7 @@ dependencies { testAnnotationProcessor "io.koraframework:annotation-processors" testImplementation platform("org.junit:junit-bom:$junitVersion") - testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.15.0" testImplementation "org.junit.jupiter:junit-jupiter" testImplementation "io.koraframework:test-junit5" } diff --git a/guides/java/kora-java-guide-database-jdbc-advanced-app/src/main/java/io/koraframework/guide/databasejdbc/advanced/task/service/TaskService.java b/guides/java/kora-java-guide-database-jdbc-advanced-app/src/main/java/io/koraframework/guide/databasejdbc/advanced/task/service/TaskService.java index b5dc4d5f..a08f821c 100644 --- a/guides/java/kora-java-guide-database-jdbc-advanced-app/src/main/java/io/koraframework/guide/databasejdbc/advanced/task/service/TaskService.java +++ b/guides/java/kora-java-guide-database-jdbc-advanced-app/src/main/java/io/koraframework/guide/databasejdbc/advanced/task/service/TaskService.java @@ -24,7 +24,7 @@ public TaskService(TaskRepository taskRepository) { } public List createTasks(List taskCreates) { - return taskRepository.getJdbcConnectionFactory().inTx(() -> { + return taskRepository.executor().inTx(() -> { var assigneeIds = taskCreates.stream() .map(TaskRequest.TaskCreate::userAssigneeId) .filter(Objects::nonNull) @@ -72,7 +72,7 @@ public void updateStatus(long id, TaskStatus status) { } public void assignTask(long taskId, Long userId) { - taskRepository.getJdbcConnectionFactory().inTx(() -> { + taskRepository.executor().inTx(() -> { var updated = taskRepository.updateAssignee(taskId, userId); if (updated.value() < 1) { throw HttpServerResponseException.of(404, "Task not found"); diff --git a/guides/java/kora-java-guide-database-jdbc-advanced-app/src/main/resources/application.conf b/guides/java/kora-java-guide-database-jdbc-advanced-app/src/main/resources/application.conf index 8f8b5333..2d2a1442 100644 --- a/guides/java/kora-java-guide-database-jdbc-advanced-app/src/main/resources/application.conf +++ b/guides/java/kora-java-guide-database-jdbc-advanced-app/src/main/resources/application.conf @@ -1,10 +1,10 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } -db { +jdbc { jdbcUrl = ${?POSTGRES_JDBC_URL} username = ${?POSTGRES_USER} password = ${?POSTGRES_PASS} diff --git a/guides/java/kora-java-guide-database-jdbc-advanced-app/src/test/java/io/koraframework/guide/databasejdbc/advanced/TaskServicePostgresAdvancedTest.java b/guides/java/kora-java-guide-database-jdbc-advanced-app/src/test/java/io/koraframework/guide/databasejdbc/advanced/TaskServicePostgresAdvancedTest.java index d20f5c85..925d9922 100644 --- a/guides/java/kora-java-guide-database-jdbc-advanced-app/src/test/java/io/koraframework/guide/databasejdbc/advanced/TaskServicePostgresAdvancedTest.java +++ b/guides/java/kora-java-guide-database-jdbc-advanced-app/src/test/java/io/koraframework/guide/databasejdbc/advanced/TaskServicePostgresAdvancedTest.java @@ -45,7 +45,7 @@ class TaskServicePostgresAdvancedTest implements KoraAppTestConfigModifier { @Override public KoraConfigModification config() { return KoraConfigModification.ofString(""" - db { + jdbc { jdbcUrl = ${POSTGRES_JDBC_URL} username = ${POSTGRES_USER} password = ${POSTGRES_PASS} diff --git a/guides/java/kora-java-guide-database-jdbc-app/build.gradle b/guides/java/kora-java-guide-database-jdbc-app/build.gradle index 075af61d..24016af8 100644 --- a/guides/java/kora-java-guide-database-jdbc-app/build.gradle +++ b/guides/java/kora-java-guide-database-jdbc-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" @@ -28,6 +28,9 @@ dependencies { implementation "io.koraframework:config-hocon" implementation "io.koraframework:database-flyway" + // с Flyway 10 поддержка конкретных СУБД вынесена в отдельные артефакты; + // без этого приложение падает на старте: "Unsupported Database: PostgreSQL" + implementation "org.flywaydb:flyway-database-postgresql:13.1.0" implementation "io.koraframework:database-jdbc" implementation "io.koraframework:http-server-undertow" implementation "io.koraframework:json-common" diff --git a/guides/java/kora-java-guide-database-jdbc-app/src/main/resources/application.conf b/guides/java/kora-java-guide-database-jdbc-app/src/main/resources/application.conf index 440a5a87..adea4ee3 100644 --- a/guides/java/kora-java-guide-database-jdbc-app/src/main/resources/application.conf +++ b/guides/java/kora-java-guide-database-jdbc-app/src/main/resources/application.conf @@ -1,10 +1,10 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } -db { +jdbc { jdbcUrl = ${?POSTGRES_JDBC_URL} username = ${?POSTGRES_USER} password = ${?POSTGRES_PASS} diff --git a/guides/java/kora-java-guide-dependency-injection-introduction-app/build.gradle b/guides/java/kora-java-guide-dependency-injection-introduction-app/build.gradle index 744fc668..781ce0c4 100644 --- a/guides/java/kora-java-guide-dependency-injection-introduction-app/build.gradle +++ b/guides/java/kora-java-guide-dependency-injection-introduction-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" diff --git a/guides/java/kora-java-guide-dependency-injection/build.gradle b/guides/java/kora-java-guide-dependency-injection/build.gradle index a18c9876..c163dac9 100644 --- a/guides/java/kora-java-guide-dependency-injection/build.gradle +++ b/guides/java/kora-java-guide-dependency-injection/build.gradle @@ -23,7 +23,7 @@ subprojects { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") } tasks.withType(JavaCompile).configureEach { diff --git a/guides/java/kora-java-guide-dependency-injection/kora-java-guide-dependency-injection-lib/build.gradle b/guides/java/kora-java-guide-dependency-injection/kora-java-guide-dependency-injection-lib/build.gradle index 546ac12a..60390961 100644 --- a/guides/java/kora-java-guide-dependency-injection/kora-java-guide-dependency-injection-lib/build.gradle +++ b/guides/java/kora-java-guide-dependency-injection/kora-java-guide-dependency-injection-lib/build.gradle @@ -3,6 +3,8 @@ plugins { } dependencies { + annotationProcessor "io.koraframework:annotation-processors" + api project(":guides:java:kora-java-guide-dependency-injection:kora-java-guide-dependency-injection-common") implementation "io.koraframework:config-common" diff --git a/guides/java/kora-java-guide-dependency-injection/kora-java-guide-dependency-injection-lib/src/main/java/io/koraframework/guide/dependencyinjection/email/EmailConfig.java b/guides/java/kora-java-guide-dependency-injection/kora-java-guide-dependency-injection-lib/src/main/java/io/koraframework/guide/dependencyinjection/email/EmailConfig.java index fcdf3091..03af2486 100644 --- a/guides/java/kora-java-guide-dependency-injection/kora-java-guide-dependency-injection-lib/src/main/java/io/koraframework/guide/dependencyinjection/email/EmailConfig.java +++ b/guides/java/kora-java-guide-dependency-injection/kora-java-guide-dependency-injection-lib/src/main/java/io/koraframework/guide/dependencyinjection/email/EmailConfig.java @@ -1,3 +1,6 @@ package io.koraframework.guide.dependencyinjection.email; -public record EmailConfig(String topic) {} \ No newline at end of file +import io.koraframework.config.common.annotation.ConfigMapper; + +@ConfigMapper +public record EmailConfig(String topic) {} diff --git a/guides/java/kora-java-guide-dependency-injection/kora-java-guide-dependency-injection-lib/src/main/java/io/koraframework/guide/dependencyinjection/email/EmailModule.java b/guides/java/kora-java-guide-dependency-injection/kora-java-guide-dependency-injection-lib/src/main/java/io/koraframework/guide/dependencyinjection/email/EmailModule.java index 55001c5f..5236e6a9 100644 --- a/guides/java/kora-java-guide-dependency-injection/kora-java-guide-dependency-injection-lib/src/main/java/io/koraframework/guide/dependencyinjection/email/EmailModule.java +++ b/guides/java/kora-java-guide-dependency-injection/kora-java-guide-dependency-injection-lib/src/main/java/io/koraframework/guide/dependencyinjection/email/EmailModule.java @@ -4,7 +4,7 @@ import io.koraframework.common.annotation.DefaultComponent; import io.koraframework.common.annotation.Tag; import io.koraframework.config.common.Config; -import io.koraframework.config.common.extractor.ConfigMapper; +import io.koraframework.config.common.mapper.ConfigValueMapper; import io.koraframework.guide.dependencyinjection.common.Notifier; public interface EmailModule { @@ -13,8 +13,8 @@ final class EmailTag { private EmailTag() {} } - default EmailConfig config(Config config, ConfigMapper extractor) { - return extractor.extract(config.get("notifier.email")); + default EmailConfig config(Config config, ConfigValueMapper extractor) { + return extractor.mapOrThrow(config.get("notifier.email")); } @Tag(EmailTag.class) diff --git a/guides/java/kora-java-guide-getting-started-app/build.gradle b/guides/java/kora-java-guide-getting-started-app/build.gradle index 67abbf31..43dd893d 100644 --- a/guides/java/kora-java-guide-getting-started-app/build.gradle +++ b/guides/java/kora-java-guide-getting-started-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" diff --git a/guides/java/kora-java-guide-grpc-client-advanced-app/build.gradle b/guides/java/kora-java-guide-grpc-client-advanced-app/build.gradle index 37523488..f5f7b17b 100644 --- a/guides/java/kora-java-guide-grpc-client-advanced-app/build.gradle +++ b/guides/java/kora-java-guide-grpc-client-advanced-app/build.gradle @@ -22,7 +22,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") compileOnly "javax.annotation:javax.annotation-api:1.3.2" annotationProcessor "io.koraframework:annotation-processors" @@ -32,19 +32,19 @@ dependencies { implementation "io.koraframework:http-server-undertow" implementation "io.koraframework:json-common" implementation "io.koraframework:logging-logback" - implementation "io.grpc:grpc-protobuf:1.74.0" + implementation "io.grpc:grpc-protobuf:1.83.1" testRuntimeOnly platform("org.junit:junit-bom:$junitVersion") testRuntimeOnly "org.junit.platform:junit-platform-launcher" testImplementation platform("org.junit:junit-bom:$junitVersion") - testImplementation "io.grpc:grpc-inprocess:1.74.0" + testImplementation "io.grpc:grpc-inprocess:1.83.1" testImplementation "org.junit.jupiter:junit-jupiter" } protobuf { protoc { artifact = "com.google.protobuf:protoc:3.25.3" } plugins { - grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.74.0" } + grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.83.1" } } generateProtoTasks { all()*.plugins { grpc {} } diff --git a/guides/java/kora-java-guide-grpc-client-advanced-app/src/main/java/io/koraframework/guide/grpcclient/advanced/Application.java b/guides/java/kora-java-guide-grpc-client-advanced-app/src/main/java/io/koraframework/guide/grpcclient/advanced/Application.java index adc3f439..38ba018d 100644 --- a/guides/java/kora-java-guide-grpc-client-advanced-app/src/main/java/io/koraframework/guide/grpcclient/advanced/Application.java +++ b/guides/java/kora-java-guide-grpc-client-advanced-app/src/main/java/io/koraframework/guide/grpcclient/advanced/Application.java @@ -6,7 +6,7 @@ import io.koraframework.http.server.undertow.UndertowPublicHttpServerModule; import io.koraframework.json.common.JsonModule; import io.koraframework.logging.logback.LogbackModule; -import ru.tinkoff.grpc.client.GrpcClientModule; +import io.koraframework.grpc.client.GrpcClientModule; @KoraApp public interface Application extends diff --git a/guides/java/kora-java-guide-grpc-client-advanced-app/src/main/resources/application.conf b/guides/java/kora-java-guide-grpc-client-advanced-app/src/main/resources/application.conf index 552d7388..6446740c 100644 --- a/guides/java/kora-java-guide-grpc-client-advanced-app/src/main/resources/application.conf +++ b/guides/java/kora-java-guide-grpc-client-advanced-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8081 - privateApiHttpPort = 8086 + port = 8081 + system.port = 8086 telemetry.logging.enabled = true } diff --git a/guides/java/kora-java-guide-grpc-client-app/build.gradle b/guides/java/kora-java-guide-grpc-client-app/build.gradle index ffccb213..b7a3eb9f 100644 --- a/guides/java/kora-java-guide-grpc-client-app/build.gradle +++ b/guides/java/kora-java-guide-grpc-client-app/build.gradle @@ -22,7 +22,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") compileOnly "javax.annotation:javax.annotation-api:1.3.2" annotationProcessor "io.koraframework:annotation-processors" @@ -32,19 +32,19 @@ dependencies { implementation "io.koraframework:http-server-undertow" implementation "io.koraframework:json-common" implementation "io.koraframework:logging-logback" - implementation "io.grpc:grpc-protobuf:1.74.0" + implementation "io.grpc:grpc-protobuf:1.83.1" testRuntimeOnly platform("org.junit:junit-bom:$junitVersion") testRuntimeOnly "org.junit.platform:junit-platform-launcher" testImplementation platform("org.junit:junit-bom:$junitVersion") - testImplementation "io.grpc:grpc-inprocess:1.74.0" + testImplementation "io.grpc:grpc-inprocess:1.83.1" testImplementation "org.junit.jupiter:junit-jupiter" } protobuf { protoc { artifact = "com.google.protobuf:protoc:3.25.3" } plugins { - grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.74.0" } + grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.83.1" } } generateProtoTasks { all()*.plugins { grpc {} } diff --git a/guides/java/kora-java-guide-grpc-client-app/src/main/java/io/koraframework/guide/grpcclient/Application.java b/guides/java/kora-java-guide-grpc-client-app/src/main/java/io/koraframework/guide/grpcclient/Application.java index a7d63276..deada6cc 100644 --- a/guides/java/kora-java-guide-grpc-client-app/src/main/java/io/koraframework/guide/grpcclient/Application.java +++ b/guides/java/kora-java-guide-grpc-client-app/src/main/java/io/koraframework/guide/grpcclient/Application.java @@ -6,7 +6,7 @@ import io.koraframework.http.server.undertow.UndertowPublicHttpServerModule; import io.koraframework.json.common.JsonModule; import io.koraframework.logging.logback.LogbackModule; -import ru.tinkoff.grpc.client.GrpcClientModule; +import io.koraframework.grpc.client.GrpcClientModule; @KoraApp public interface Application extends diff --git a/guides/java/kora-java-guide-grpc-client-app/src/main/resources/application.conf b/guides/java/kora-java-guide-grpc-client-app/src/main/resources/application.conf index 7745c49d..b81818b4 100644 --- a/guides/java/kora-java-guide-grpc-client-app/src/main/resources/application.conf +++ b/guides/java/kora-java-guide-grpc-client-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8081 - privateApiHttpPort = 8086 + port = 8081 + system.port = 8086 telemetry.logging.enabled = true } diff --git a/guides/java/kora-java-guide-grpc-server-advanced-app/build.gradle b/guides/java/kora-java-guide-grpc-server-advanced-app/build.gradle index 2c85409c..a84d69b2 100644 --- a/guides/java/kora-java-guide-grpc-server-advanced-app/build.gradle +++ b/guides/java/kora-java-guide-grpc-server-advanced-app/build.gradle @@ -22,7 +22,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") compileOnly "javax.annotation:javax.annotation-api:1.3.2" annotationProcessor "io.koraframework:annotation-processors" @@ -30,14 +30,14 @@ dependencies { implementation "io.koraframework:config-hocon" implementation "io.koraframework:grpc-server" implementation "io.koraframework:logging-logback" - implementation "io.grpc:grpc-protobuf:1.74.0" - implementation "io.grpc:grpc-services:1.74.0" + implementation "io.grpc:grpc-protobuf:1.83.1" + implementation "io.grpc:grpc-services:1.83.1" testCompileOnly "javax.annotation:javax.annotation-api:1.3.2" testAnnotationProcessor "io.koraframework:annotation-processors" testImplementation platform("org.junit:junit-bom:$junitVersion") - testImplementation "io.grpc:grpc-netty:1.74.0" + testImplementation "io.grpc:grpc-netty:1.83.1" testImplementation "org.junit.jupiter:junit-jupiter" testImplementation "io.koraframework:test-junit5" } @@ -45,7 +45,7 @@ dependencies { protobuf { protoc { artifact = "com.google.protobuf:protoc:3.25.3" } plugins { - grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.74.0" } + grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.83.1" } } generateProtoTasks { all()*.plugins { grpc {} } diff --git a/guides/java/kora-java-guide-grpc-server-app/build.gradle b/guides/java/kora-java-guide-grpc-server-app/build.gradle index 7b1597f3..487e894d 100644 --- a/guides/java/kora-java-guide-grpc-server-app/build.gradle +++ b/guides/java/kora-java-guide-grpc-server-app/build.gradle @@ -22,7 +22,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") compileOnly "javax.annotation:javax.annotation-api:1.3.2" annotationProcessor "io.koraframework:annotation-processors" @@ -30,14 +30,14 @@ dependencies { implementation "io.koraframework:config-hocon" implementation "io.koraframework:grpc-server" implementation "io.koraframework:logging-logback" - implementation "io.grpc:grpc-protobuf:1.74.0" - implementation "io.grpc:grpc-services:1.74.0" + implementation "io.grpc:grpc-protobuf:1.83.1" + implementation "io.grpc:grpc-services:1.83.1" testCompileOnly "javax.annotation:javax.annotation-api:1.3.2" testAnnotationProcessor "io.koraframework:annotation-processors" testImplementation platform("org.junit:junit-bom:$junitVersion") - testImplementation "io.grpc:grpc-netty:1.74.0" + testImplementation "io.grpc:grpc-netty:1.83.1" testImplementation "org.junit.jupiter:junit-jupiter" testImplementation "io.koraframework:test-junit5" } @@ -45,7 +45,7 @@ dependencies { protobuf { protoc { artifact = "com.google.protobuf:protoc:3.25.3" } plugins { - grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.74.0" } + grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.83.1" } } generateProtoTasks { all()*.plugins { grpc {} } diff --git a/guides/java/kora-java-guide-http-client-advanced-app/build.gradle b/guides/java/kora-java-guide-http-client-advanced-app/build.gradle index 68ebd132..a6be9024 100644 --- a/guides/java/kora-java-guide-http-client-advanced-app/build.gradle +++ b/guides/java/kora-java-guide-http-client-advanced-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" diff --git a/guides/java/kora-java-guide-http-client-advanced-app/src/main/java/io/koraframework/guide/httpclient/client/ApiKeyAuthInterceptor.java b/guides/java/kora-java-guide-http-client-advanced-app/src/main/java/io/koraframework/guide/httpclient/client/ApiKeyAuthInterceptor.java index 768364ba..7a41f045 100644 --- a/guides/java/kora-java-guide-http-client-advanced-app/src/main/java/io/koraframework/guide/httpclient/client/ApiKeyAuthInterceptor.java +++ b/guides/java/kora-java-guide-http-client-advanced-app/src/main/java/io/koraframework/guide/httpclient/client/ApiKeyAuthInterceptor.java @@ -1,8 +1,6 @@ package io.koraframework.guide.httpclient.client; -import java.util.concurrent.CompletionStage; import io.koraframework.common.annotation.Component; -import io.koraframework.common.Context; import io.koraframework.http.client.common.interceptor.HttpClientInterceptor; import io.koraframework.http.client.common.request.HttpClientRequest; import io.koraframework.http.client.common.response.HttpClientResponse; @@ -17,11 +15,10 @@ public ApiKeyAuthInterceptor(ApiKeyAuthConfig config) { } @Override - public CompletionStage processRequest(Context ctx, InterceptChain chain, HttpClientRequest request) - throws Exception { + public HttpClientResponse processRequest(InterceptChain chain, HttpClientRequest request) throws Exception { var authorizedRequest = request.toBuilder() .header("Authorization", this.config.value()) .build(); - return chain.process(ctx, authorizedRequest); + return chain.process(authorizedRequest); } } diff --git a/guides/java/kora-java-guide-http-client-advanced-app/src/main/java/io/koraframework/guide/httpclient/client/DataApiClient.java b/guides/java/kora-java-guide-http-client-advanced-app/src/main/java/io/koraframework/guide/httpclient/client/DataApiClient.java index ac22b521..84b86d37 100644 --- a/guides/java/kora-java-guide-http-client-advanced-app/src/main/java/io/koraframework/guide/httpclient/client/DataApiClient.java +++ b/guides/java/kora-java-guide-http-client-advanced-app/src/main/java/io/koraframework/guide/httpclient/client/DataApiClient.java @@ -3,10 +3,9 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.List; -import java.util.concurrent.CompletionStage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.koraframework.common.Context; +import io.koraframework.common.annotation.Component; import io.koraframework.common.annotation.Mapping; import io.koraframework.guide.httpclient.client.DataApiClient.MappedResponse.Error; import io.koraframework.guide.httpclient.client.DataApiClient.MappedResponse.Payload; @@ -32,7 +31,7 @@ import static io.koraframework.guide.httpclient.client.DataApiClient.MappedResponse.*; @InterceptWith(ApiKeyAuthInterceptor.class) -@HttpClient(configPath = "httpClient.dataApi") +@HttpClient("httpClient.dataApi") public interface DataApiClient { @HttpRoute(method = HttpMethod.POST, path = "/data/form") @@ -62,10 +61,11 @@ record UploadResponse(int fileCount, List fileNames) {} record PlainTextGreetingBody(String name) {} + @Component final class GreetingRequestMapper implements HttpClientRequestMapper { @Override - public HttpBodyOutput apply(Context ctx, PlainTextGreetingBody value) { + public HttpBodyOutput apply(PlainTextGreetingBody value) { return HttpBody.plaintext("Hello " + value.name()); } } @@ -82,6 +82,7 @@ record Error(int code, String message) implements MappedResponse {} record ErrorPayload(String message) {} } + @Component final class MappedResponseSuccessMapper implements HttpClientResponseMapper { private final JsonReader jsonReader; @@ -98,6 +99,7 @@ public MappedResponse apply(HttpClientResponse response) throws IOException, Htt } } + @Component final class MappedResponseErrorMapper implements HttpClientResponseMapper { private final JsonReader jsonReader; @@ -115,15 +117,15 @@ public MappedResponse apply(HttpClientResponse response) throws IOException, Htt } } + @Component final class MethodLoggingInterceptor implements HttpClientInterceptor { private static final Logger logger = LoggerFactory.getLogger(MethodLoggingInterceptor.class); @Override - public CompletionStage processRequest(Context ctx, InterceptChain chain, HttpClientRequest request) - throws Exception { + public HttpClientResponse processRequest(InterceptChain chain, HttpClientRequest request) throws Exception { logger.info("Advanced HTTP client interceptor invoked"); - return chain.process(ctx, request); + return chain.process(request); } } } diff --git a/guides/java/kora-java-guide-http-client-advanced-app/src/main/java/io/koraframework/guide/httpclient/client/ManualDataHttpClient.java b/guides/java/kora-java-guide-http-client-advanced-app/src/main/java/io/koraframework/guide/httpclient/client/ManualDataHttpClient.java index f3004985..3f1b4847 100644 --- a/guides/java/kora-java-guide-http-client-advanced-app/src/main/java/io/koraframework/guide/httpclient/client/ManualDataHttpClient.java +++ b/guides/java/kora-java-guide-http-client-advanced-app/src/main/java/io/koraframework/guide/httpclient/client/ManualDataHttpClient.java @@ -21,10 +21,7 @@ public ManualDataHttpClient(HttpClient httpClient, $DataApiClient_Config dataApi public String pingManualHandler() { var request = HttpClientRequest.of("GET", this.dataApiConfig.url() + "/manual/data/ping") .build(); - var response = this.httpClient.with(this.apiKeyAuthInterceptor) - .execute(request) - .toCompletableFuture() - .join(); + var response = this.httpClient.with(this.apiKeyAuthInterceptor).execute(request); if (response.code() != 200) { throw new IllegalStateException("Manual HTTP call failed with status " + response.code()); } diff --git a/guides/java/kora-java-guide-http-client-advanced-app/src/main/resources/application.conf b/guides/java/kora-java-guide-http-client-advanced-app/src/main/resources/application.conf index cdf3f383..855d2d93 100644 --- a/guides/java/kora-java-guide-http-client-advanced-app/src/main/resources/application.conf +++ b/guides/java/kora-java-guide-http-client-advanced-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { dataApiHttpPort = 8081 - privateApiHttpPort = 8086 + system.port = 8086 telemetry.logging.enabled = true } diff --git a/guides/java/kora-java-guide-http-client-app/build.gradle b/guides/java/kora-java-guide-http-client-app/build.gradle index 94086495..d6d2616c 100644 --- a/guides/java/kora-java-guide-http-client-app/build.gradle +++ b/guides/java/kora-java-guide-http-client-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" diff --git a/guides/java/kora-java-guide-http-client-app/src/main/java/io/koraframework/guide/httpclient/client/UserApiClient.java b/guides/java/kora-java-guide-http-client-app/src/main/java/io/koraframework/guide/httpclient/client/UserApiClient.java index 126ec603..6a3df7c7 100644 --- a/guides/java/kora-java-guide-http-client-app/src/main/java/io/koraframework/guide/httpclient/client/UserApiClient.java +++ b/guides/java/kora-java-guide-http-client-app/src/main/java/io/koraframework/guide/httpclient/client/UserApiClient.java @@ -6,6 +6,10 @@ import io.koraframework.guide.httpclient.dto.UserResponse; import io.koraframework.http.client.common.annotation.HttpClient; import io.koraframework.http.common.HttpMethod; +import java.io.IOException; +import io.koraframework.common.annotation.Component; +import io.koraframework.http.client.common.response.HttpClientResponse; +import io.koraframework.http.client.common.response.HttpClientResponseMapper; import io.koraframework.http.common.HttpResponseEntity; import io.koraframework.http.common.annotation.Cookie; import io.koraframework.http.common.annotation.Header; @@ -14,7 +18,7 @@ import io.koraframework.http.common.annotation.Query; import io.koraframework.json.common.annotation.Json; -@HttpClient(configPath = "httpClient.userApi") +@HttpClient("httpClient.userApi") public interface UserApiClient { @HttpRoute(method = HttpMethod.POST, path = "/users") @@ -36,6 +40,24 @@ List getUsers( @Nullable @Query("size") Integer size, @Nullable @Query("sort") String sort); + /** + * Kora 2.0 ships response mappers for {@code String} and {@code byte[]} only, so a body-less + * response that still needs its status code has to say how {@code Void} is produced. The + * framework wraps it into {@code HttpResponseEntity} through its own template factory, + * which is why the component is declared but never referenced with {@code @Mapping}. + */ + @Component + final class VoidResponseMapper implements HttpClientResponseMapper { + + @Override + public Void apply(HttpClientResponse response) throws IOException { + try (var body = response.body()) { + body.asInputStream().readAllBytes(); + } + return null; + } + } + @HttpRoute(method = HttpMethod.DELETE, path = "/users/{userId}") HttpResponseEntity deleteUser(@Path String userId); } diff --git a/guides/java/kora-java-guide-http-client-app/src/main/resources/application.conf b/guides/java/kora-java-guide-http-client-app/src/main/resources/application.conf index d880212f..4a496e44 100644 --- a/guides/java/kora-java-guide-http-client-app/src/main/resources/application.conf +++ b/guides/java/kora-java-guide-http-client-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8081 - privateApiHttpPort = 8086 + port = 8081 + system.port = 8086 telemetry.logging.enabled = true } diff --git a/guides/java/kora-java-guide-http-server-advanced-app/build.gradle b/guides/java/kora-java-guide-http-server-advanced-app/build.gradle index fbebdb8b..d5a6c4fa 100644 --- a/guides/java/kora-java-guide-http-server-advanced-app/build.gradle +++ b/guides/java/kora-java-guide-http-server-advanced-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" diff --git a/guides/java/kora-java-guide-http-server-advanced-app/src/main/java/io/koraframework/guide/httpserver/advanced/Application.java b/guides/java/kora-java-guide-http-server-advanced-app/src/main/java/io/koraframework/guide/httpserver/advanced/Application.java index b319768b..28e955e3 100644 --- a/guides/java/kora-java-guide-http-server-advanced-app/src/main/java/io/koraframework/guide/httpserver/advanced/Application.java +++ b/guides/java/kora-java-guide-http-server-advanced-app/src/main/java/io/koraframework/guide/httpserver/advanced/Application.java @@ -1,6 +1,5 @@ package io.koraframework.guide.httpserver.advanced; -import java.util.concurrent.CompletableFuture; import io.koraframework.application.graph.KoraApplication; import io.koraframework.common.annotation.KoraApp; import io.koraframework.config.hocon.HoconConfigModule; @@ -21,12 +20,12 @@ public interface Application extends UndertowPublicHttpServerModule { default HttpServerRequestHandler manualDataPingHandler(DataApiAuthConfig authConfig) { - return HttpServerRequestHandlerImpl.get("/manual/data/ping", (context, request) -> { + return HttpServerRequestHandlerImpl.get("/manual/data/ping", request -> { var authorization = request.headers().getFirst("authorization"); if (!authConfig.value().equals(authorization)) { - return CompletableFuture.completedFuture(HttpServerResponse.of(403, HttpBody.plaintext("Invalid API key"))); + return HttpServerResponse.of(403, HttpBody.plaintext("Invalid API key")); } - return CompletableFuture.completedFuture(HttpServerResponse.of(200, HttpBody.plaintext("manual-data-pong"))); + return HttpServerResponse.of(200, HttpBody.plaintext("manual-data-pong")); }); } diff --git a/guides/java/kora-java-guide-http-server-advanced-app/src/main/java/io/koraframework/guide/httpserver/advanced/controller/DataApiAuthInterceptor.java b/guides/java/kora-java-guide-http-server-advanced-app/src/main/java/io/koraframework/guide/httpserver/advanced/controller/DataApiAuthInterceptor.java index 58304ded..8bdd30c4 100644 --- a/guides/java/kora-java-guide-http-server-advanced-app/src/main/java/io/koraframework/guide/httpserver/advanced/controller/DataApiAuthInterceptor.java +++ b/guides/java/kora-java-guide-http-server-advanced-app/src/main/java/io/koraframework/guide/httpserver/advanced/controller/DataApiAuthInterceptor.java @@ -1,8 +1,6 @@ package io.koraframework.guide.httpserver.advanced.controller; -import java.util.concurrent.CompletionStage; import io.koraframework.common.annotation.Component; -import io.koraframework.common.Context; import io.koraframework.http.server.common.interceptor.HttpServerInterceptor; import io.koraframework.http.server.common.request.HttpServerRequest; import io.koraframework.http.server.common.response.HttpServerResponse; @@ -17,13 +15,11 @@ public DataApiAuthInterceptor(DataApiAuthConfig config) { } @Override - public CompletionStage intercept(Context context, HttpServerRequest request, InterceptChain chain) - throws Exception { + public HttpServerResponse intercept(HttpServerRequest request, InterceptChain chain) throws Exception { var authorization = request.headers().getFirst("authorization"); if (!this.config.value().equals(authorization)) { throw new SecurityException("Invalid API key"); } - return chain.process(context, request); + return chain.process(request); } } - diff --git a/guides/java/kora-java-guide-http-server-advanced-app/src/main/java/io/koraframework/guide/httpserver/advanced/controller/ExceptionHandler.java b/guides/java/kora-java-guide-http-server-advanced-app/src/main/java/io/koraframework/guide/httpserver/advanced/controller/ExceptionHandler.java index 213b33bc..e582fb15 100644 --- a/guides/java/kora-java-guide-http-server-advanced-app/src/main/java/io/koraframework/guide/httpserver/advanced/controller/ExceptionHandler.java +++ b/guides/java/kora-java-guide-http-server-advanced-app/src/main/java/io/koraframework/guide/httpserver/advanced/controller/ExceptionHandler.java @@ -1,20 +1,17 @@ package io.koraframework.guide.httpserver.advanced.controller; -import java.util.concurrent.CompletionException; -import java.util.concurrent.CompletionStage; import io.koraframework.common.annotation.Component; -import io.koraframework.common.Context; import io.koraframework.common.annotation.Tag; import io.koraframework.guide.httpserver.advanced.dto.ErrorResponse; import io.koraframework.http.common.body.HttpBody; import io.koraframework.http.server.common.interceptor.HttpServerInterceptor; -import io.koraframework.http.server.common.HttpServerModule; +import io.koraframework.http.server.common.HttpServer; import io.koraframework.http.server.common.request.HttpServerRequest; import io.koraframework.http.server.common.response.HttpServerResponse; import io.koraframework.http.server.common.response.HttpServerResponseException; import io.koraframework.json.common.JsonWriter; -@Tag(HttpServerModule.class) +@Tag(HttpServer.class) @Component public final class ExceptionHandler implements HttpServerInterceptor { @@ -25,37 +22,23 @@ public ExceptionHandler(JsonWriter errorJsonWriter) { } @Override - public CompletionStage intercept(Context context, HttpServerRequest request, InterceptChain chain) - throws Exception { - return chain.process(context, request).exceptionally(throwable -> { - Throwable cause = unwrap(throwable); - if (cause instanceof RestrictedFormNameException restrictedFormNameException) { - return jsonResponse(400, restrictedFormNameException.getMessage()); - } - if (cause instanceof HttpServerResponseException responseException) { - return jsonResponse(responseException.code(), responseException.getMessage()); - } - if (cause instanceof IllegalArgumentException) { - return jsonResponse(400, "Invalid request parameters"); - } - if (cause instanceof SecurityException) { - return jsonResponse(403, cause.getMessage() != null ? cause.getMessage() : "Access denied"); - } + public HttpServerResponse intercept(HttpServerRequest request, InterceptChain chain) { + try { + return chain.process(request); + } catch (RestrictedFormNameException e) { + return jsonResponse(400, e.getMessage()); + } catch (HttpServerResponseException e) { + return jsonResponse(e.code(), e.getMessage()); + } catch (IllegalArgumentException e) { + return jsonResponse(400, "Invalid request parameters"); + } catch (SecurityException e) { + return jsonResponse(403, e.getMessage() != null ? e.getMessage() : "Access denied"); + } catch (Exception e) { return jsonResponse(500, "An unexpected error occurred"); - }); + } } private HttpServerResponse jsonResponse(int statusCode, String message) { - return HttpServerResponse.of(statusCode, HttpBody.json(this.errorJsonWriter.toByteArrayUnchecked(new ErrorResponse(message)))); - } - - private static Throwable unwrap(Throwable throwable) { - Throwable current = throwable; - while (current instanceof CompletionException && current.getCause() != null) { - current = current.getCause(); - } - return current; + return HttpServerResponse.of(statusCode, HttpBody.json(this.errorJsonWriter.toByteArray(new ErrorResponse(message)))); } } - - diff --git a/guides/java/kora-java-guide-http-server-advanced-app/src/main/java/io/koraframework/guide/httpserver/advanced/controller/LoggingInterceptor.java b/guides/java/kora-java-guide-http-server-advanced-app/src/main/java/io/koraframework/guide/httpserver/advanced/controller/LoggingInterceptor.java index dd32f26a..364bf90b 100644 --- a/guides/java/kora-java-guide-http-server-advanced-app/src/main/java/io/koraframework/guide/httpserver/advanced/controller/LoggingInterceptor.java +++ b/guides/java/kora-java-guide-http-server-advanced-app/src/main/java/io/koraframework/guide/httpserver/advanced/controller/LoggingInterceptor.java @@ -1,8 +1,6 @@ package io.koraframework.guide.httpserver.advanced.controller; -import java.util.concurrent.CompletionStage; import io.koraframework.common.annotation.Component; -import io.koraframework.common.Context; import io.koraframework.http.server.common.interceptor.HttpServerInterceptor; import io.koraframework.http.server.common.request.HttpServerRequest; import io.koraframework.http.server.common.response.HttpServerResponse; @@ -11,13 +9,17 @@ public final class LoggingInterceptor implements HttpServerInterceptor { @Override - public CompletionStage intercept(Context context, HttpServerRequest request, InterceptChain chain) - throws Exception { + public HttpServerResponse intercept(HttpServerRequest request, InterceptChain chain) throws Exception { long started = System.nanoTime(); - return chain.process(context, request).whenComplete((response, throwable) -> { + // stays 500 when the chain throws, matching the reactive version that logged 500 on failure + int statusCode = 500; + try { + var response = chain.process(request); + statusCode = response.code(); + return response; + } finally { long durationMs = (System.nanoTime() - started) / 1_000_000; - int statusCode = response != null ? response.code() : 500; System.out.printf("Request: %s %s -> %d (%d ms)%n", request.method(), request.path(), statusCode, durationMs); - }); + } } } diff --git a/guides/java/kora-java-guide-http-server-advanced-app/src/main/resources/application.conf b/guides/java/kora-java-guide-http-server-advanced-app/src/main/resources/application.conf index 0b548e86..6a9e6915 100644 --- a/guides/java/kora-java-guide-http-server-advanced-app/src/main/resources/application.conf +++ b/guides/java/kora-java-guide-http-server-advanced-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } diff --git a/guides/java/kora-java-guide-http-server-app/build.gradle b/guides/java/kora-java-guide-http-server-app/build.gradle index 11f92409..70e215f7 100644 --- a/guides/java/kora-java-guide-http-server-app/build.gradle +++ b/guides/java/kora-java-guide-http-server-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" diff --git a/guides/java/kora-java-guide-http-server-app/src/main/resources/application.conf b/guides/java/kora-java-guide-http-server-app/src/main/resources/application.conf index 97dae54e..c89518fa 100644 --- a/guides/java/kora-java-guide-http-server-app/src/main/resources/application.conf +++ b/guides/java/kora-java-guide-http-server-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } diff --git a/guides/java/kora-java-guide-json-app/build.gradle b/guides/java/kora-java-guide-json-app/build.gradle index 11098272..a5ca3feb 100644 --- a/guides/java/kora-java-guide-json-app/build.gradle +++ b/guides/java/kora-java-guide-json-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" diff --git a/guides/java/kora-java-guide-messaging-kafka-app/build.gradle b/guides/java/kora-java-guide-messaging-kafka-app/build.gradle index f20a1f61..1c96e53c 100644 --- a/guides/java/kora-java-guide-messaging-kafka-app/build.gradle +++ b/guides/java/kora-java-guide-messaging-kafka-app/build.gradle @@ -21,7 +21,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" @@ -36,7 +36,7 @@ dependencies { testImplementation platform("org.junit:junit-bom:$junitVersion") testImplementation "org.junit.jupiter:junit-jupiter" testImplementation "io.koraframework:test-junit5" - testImplementation "io.goodforgod:testcontainers-extensions-kafka:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-kafka:0.15.0" } application { diff --git a/guides/java/kora-java-guide-messaging-kafka-app/src/main/resources/application.conf b/guides/java/kora-java-guide-messaging-kafka-app/src/main/resources/application.conf index 6b5c5eb9..1023a7ed 100644 --- a/guides/java/kora-java-guide-messaging-kafka-app/src/main/resources/application.conf +++ b/guides/java/kora-java-guide-messaging-kafka-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } diff --git a/guides/java/kora-java-guide-observability-app/build.gradle b/guides/java/kora-java-guide-observability-app/build.gradle index 8e640bf2..b7ae46eb 100644 --- a/guides/java/kora-java-guide-observability-app/build.gradle +++ b/guides/java/kora-java-guide-observability-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" diff --git a/guides/java/kora-java-guide-observability-app/src/main/resources/application.conf b/guides/java/kora-java-guide-observability-app/src/main/resources/application.conf index b16e0c55..764d9460 100644 --- a/guides/java/kora-java-guide-observability-app/src/main/resources/application.conf +++ b/guides/java/kora-java-guide-observability-app/src/main/resources/application.conf @@ -1,10 +1,12 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 - privateApiHttpMetricsPath = "/metrics" - privateApiHttpLivenessPath = "/system/liveness" - privateApiHttpReadinessPath = "/system/readiness" + port = 8080 + system.port = 8085 + system.metricsPath = "/metrics" + system.livenessPath = "/system/liveness" + system.readinessPath = "/system/readiness" telemetry.logging.enabled = true + // в 2.0 telemetry.metrics.enabled по умолчанию false — без этого /metrics отдаёт только JVM-метрики + telemetry.metrics.enabled = true } tracing { diff --git a/guides/java/kora-java-guide-openapi-http-client-app/build.gradle b/guides/java/kora-java-guide-openapi-http-client-app/build.gradle index 222a7cd8..f6c37729 100644 --- a/guides/java/kora-java-guide-openapi-http-client-app/build.gradle +++ b/guides/java/kora-java-guide-openapi-http-client-app/build.gradle @@ -33,7 +33,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" diff --git a/guides/java/kora-java-guide-openapi-http-client-app/src/main/resources/application.conf b/guides/java/kora-java-guide-openapi-http-client-app/src/main/resources/application.conf index bfaced07..8768aceb 100644 --- a/guides/java/kora-java-guide-openapi-http-client-app/src/main/resources/application.conf +++ b/guides/java/kora-java-guide-openapi-http-client-app/src/main/resources/application.conf @@ -1,11 +1,12 @@ httpServer { - publicApiHttpPort = 8081 - privateApiHttpPort = 8086 + port = 8081 + system.port = 8086 telemetry.logging.enabled = true } httpClient { - UsersApi { + // генератор 2.0 приводит первую букву имени API к строчной: @HttpClient("httpClient.usersApi") + usersApi { url = "http://localhost:8080" url = ${?PUBLIC_API_URL} requestTimeout = 10s diff --git a/guides/java/kora-java-guide-openapi-http-server-advanced-app/build.gradle b/guides/java/kora-java-guide-openapi-http-server-advanced-app/build.gradle index a1cc98eb..bf6544b9 100644 --- a/guides/java/kora-java-guide-openapi-http-server-advanced-app/build.gradle +++ b/guides/java/kora-java-guide-openapi-http-server-advanced-app/build.gradle @@ -33,7 +33,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" @@ -108,5 +108,3 @@ test { exceptionFormat("full") } } - - diff --git a/guides/java/kora-java-guide-openapi-http-server-advanced-app/src/main/java/io/koraframework/guide/openapi/httpserver/advanced/Application.java b/guides/java/kora-java-guide-openapi-http-server-advanced-app/src/main/java/io/koraframework/guide/openapi/httpserver/advanced/Application.java index 85fcaa23..6627ae5c 100644 --- a/guides/java/kora-java-guide-openapi-http-server-advanced-app/src/main/java/io/koraframework/guide/openapi/httpserver/advanced/Application.java +++ b/guides/java/kora-java-guide-openapi-http-server-advanced-app/src/main/java/io/koraframework/guide/openapi/httpserver/advanced/Application.java @@ -13,6 +13,7 @@ import io.koraframework.http.server.common.response.HttpServerResponse; import io.koraframework.http.server.common.auth.HttpServerPrincipalExtractor; import io.koraframework.http.server.undertow.UndertowPublicHttpServerModule; +import io.koraframework.json.common.JsonNullable; import io.koraframework.json.common.JsonWriter; import io.koraframework.json.common.JsonModule; import io.koraframework.logging.logback.LogbackModule; @@ -20,7 +21,6 @@ import io.koraframework.validation.module.ValidationModule; import io.koraframework.validation.module.http.server.ViolationExceptionHttpServerResponseMapper; -import java.util.concurrent.CompletableFuture; @KoraApp public interface Application extends @@ -42,21 +42,20 @@ default ViolationExceptionHttpServerResponseMapper customViolationExceptionHttpS .map(v -> "Path " + v.path() + " violated: " + v.message()) .toList(); - var response = new ErrorResponseTO("Encountered '%s' validation violations".formatted(details.size()), details); + var response = new ErrorResponseTO("Encountered '%s' validation violations".formatted(details.size()), JsonNullable.of(details)); return HttpServerResponse.of( 400, - HttpBody.json(errorResponseJsonWriter.toByteArrayUnchecked(response))); + HttpBody.json(errorResponseJsonWriter.toByteArray(response))); }; } @Tag(ApiSecurity.ApiKeyAuth.class) - default HttpServerPrincipalExtractor apiKeyHttpServerPrincipalExtractor(DataApiAuthConfig config) { + default HttpServerPrincipalExtractor apiKeyHttpServerPrincipalExtractor(DataApiAuthConfig config) { return (request, value) -> { if (value == null || !config.value().equals(value)) { throw new SecurityException("Invalid API key"); } - return CompletableFuture.completedFuture(new DataApiPrincipal("data-api-client")); + return new DataApiPrincipal("data-api-client"); }; } } - diff --git a/guides/java/kora-java-guide-openapi-http-server-advanced-app/src/main/java/io/koraframework/guide/openapi/httpserver/advanced/controller/DataApiExceptionHandler.java b/guides/java/kora-java-guide-openapi-http-server-advanced-app/src/main/java/io/koraframework/guide/openapi/httpserver/advanced/controller/DataApiExceptionHandler.java index d2b9a49e..c37a6d10 100644 --- a/guides/java/kora-java-guide-openapi-http-server-advanced-app/src/main/java/io/koraframework/guide/openapi/httpserver/advanced/controller/DataApiExceptionHandler.java +++ b/guides/java/kora-java-guide-openapi-http-server-advanced-app/src/main/java/io/koraframework/guide/openapi/httpserver/advanced/controller/DataApiExceptionHandler.java @@ -1,9 +1,6 @@ package io.koraframework.guide.openapi.httpserver.advanced.controller; -import java.util.concurrent.CompletionException; -import java.util.concurrent.CompletionStage; import io.koraframework.common.annotation.Component; -import io.koraframework.common.Context; import io.koraframework.guide.openapi.httpserver.data.model.ErrorResponseTO; import io.koraframework.http.common.body.HttpBody; import io.koraframework.http.server.common.interceptor.HttpServerInterceptor; @@ -23,36 +20,24 @@ public DataApiExceptionHandler(JsonWriter errorJsonWriter) { } @Override - public CompletionStage intercept(Context context, HttpServerRequest request, InterceptChain chain) - throws Exception { - return chain.process(context, request).exceptionally(throwable -> { - var cause = unwrap(throwable); - if (cause instanceof ViolationException violationException) { - throw new CompletionException(violationException); - } - if (cause instanceof HttpServerResponseException responseException) { - return jsonResponse(responseException.code(), responseException.getMessage()); - } - if (cause instanceof IllegalArgumentException) { - return jsonResponse(400, "Invalid request parameters"); - } - if (cause instanceof SecurityException) { - return jsonResponse(403, cause.getMessage() != null ? cause.getMessage() : "Access denied"); - } + public HttpServerResponse intercept(HttpServerRequest request, InterceptChain chain) throws Exception { + try { + return chain.process(request); + } catch (ViolationException e) { + // left to ViolationExceptionHttpServerResponseMapper, which renders the violations + throw e; + } catch (HttpServerResponseException e) { + return jsonResponse(e.code(), e.getMessage()); + } catch (IllegalArgumentException e) { + return jsonResponse(400, "Invalid request parameters"); + } catch (SecurityException e) { + return jsonResponse(403, e.getMessage() != null ? e.getMessage() : "Access denied"); + } catch (Exception e) { return jsonResponse(500, "An unexpected error occurred"); - }); + } } private HttpServerResponse jsonResponse(int statusCode, String message) { - return HttpServerResponse.of(statusCode, HttpBody.json(this.errorJsonWriter.toByteArrayUnchecked(new ErrorResponseTO(message, null)))); - } - - private static Throwable unwrap(Throwable throwable) { - var current = throwable; - while (current instanceof CompletionException && current.getCause() != null) { - current = current.getCause(); - } - return current; + return HttpServerResponse.of(statusCode, HttpBody.json(this.errorJsonWriter.toByteArray(new ErrorResponseTO(message)))); } } - diff --git a/guides/java/kora-java-guide-openapi-http-server-advanced-app/src/main/resources/application.conf b/guides/java/kora-java-guide-openapi-http-server-advanced-app/src/main/resources/application.conf index 081a1387..67ff0794 100644 --- a/guides/java/kora-java-guide-openapi-http-server-advanced-app/src/main/resources/application.conf +++ b/guides/java/kora-java-guide-openapi-http-server-advanced-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } @@ -13,7 +13,7 @@ auth { openapi { management { - file = [ "openapi/user-http-server.yaml", "openapi/data-http-server.yaml" ] + files = [ "openapi/user-http-server.yaml", "openapi/data-http-server.yaml" ] enabled = true endpoint = "/openapi" swaggerui { diff --git a/guides/java/kora-java-guide-openapi-http-server-app/build.gradle b/guides/java/kora-java-guide-openapi-http-server-app/build.gradle index d08ecf72..3611bdf4 100644 --- a/guides/java/kora-java-guide-openapi-http-server-app/build.gradle +++ b/guides/java/kora-java-guide-openapi-http-server-app/build.gradle @@ -33,7 +33,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" diff --git a/guides/java/kora-java-guide-openapi-http-server-app/src/main/resources/application.conf b/guides/java/kora-java-guide-openapi-http-server-app/src/main/resources/application.conf index a18938c6..78348828 100644 --- a/guides/java/kora-java-guide-openapi-http-server-app/src/main/resources/application.conf +++ b/guides/java/kora-java-guide-openapi-http-server-app/src/main/resources/application.conf @@ -1,12 +1,12 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } openapi { management { - file = [ "openapi/user-http-server.yaml" ] + files = [ "openapi/user-http-server.yaml" ] enabled = true endpoint = "/openapi" swaggerui { diff --git a/guides/java/kora-java-guide-resilient-app/build.gradle b/guides/java/kora-java-guide-resilient-app/build.gradle index f2dbe4ec..7fa0a2fb 100644 --- a/guides/java/kora-java-guide-resilient-app/build.gradle +++ b/guides/java/kora-java-guide-resilient-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" diff --git a/guides/java/kora-java-guide-resilient-app/src/main/java/io/koraframework/guide/resilient/service/CircuitBreakerFailurePredicate.java b/guides/java/kora-java-guide-resilient-app/src/main/java/io/koraframework/guide/resilient/service/CircuitBreakerFailurePredicate.java index 19d04265..41ed1c6d 100644 --- a/guides/java/kora-java-guide-resilient-app/src/main/java/io/koraframework/guide/resilient/service/CircuitBreakerFailurePredicate.java +++ b/guides/java/kora-java-guide-resilient-app/src/main/java/io/koraframework/guide/resilient/service/CircuitBreakerFailurePredicate.java @@ -1,23 +1,19 @@ package io.koraframework.guide.resilient.service; import io.koraframework.common.annotation.Component; +import io.koraframework.common.annotation.Tag; import io.koraframework.http.server.common.response.HttpServerResponseException; import io.koraframework.resilient.circuitbreaker.CircuitBreakerPredicate; +@Tag(DefaultCircuitBreaker.class) @Component public final class CircuitBreakerFailurePredicate implements CircuitBreakerPredicate { @Override - public String name() { - return "RecordServerErrorsOnly"; - } - - @Override - public boolean test(Throwable throwable) { + public boolean isCircuitBreakerFailure(Throwable throwable) { if (throwable instanceof HttpServerResponseException exception) { return exception.code() >= 500; } return true; } } - diff --git a/guides/java/kora-java-guide-resilient-app/src/main/java/io/koraframework/guide/resilient/service/DefaultCircuitBreaker.java b/guides/java/kora-java-guide-resilient-app/src/main/java/io/koraframework/guide/resilient/service/DefaultCircuitBreaker.java new file mode 100644 index 00000000..d3b2a302 --- /dev/null +++ b/guides/java/kora-java-guide-resilient-app/src/main/java/io/koraframework/guide/resilient/service/DefaultCircuitBreaker.java @@ -0,0 +1,9 @@ +package io.koraframework.guide.resilient.service; + +import io.koraframework.resilient.circuitbreaker.CircuitBreaker; +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakerSpec; + +@CircuitBreakerSpec("resilient.circuitbreaker.default") +public interface DefaultCircuitBreaker extends CircuitBreaker { + +} diff --git a/guides/java/kora-java-guide-resilient-app/src/main/java/io/koraframework/guide/resilient/service/DefaultRetry.java b/guides/java/kora-java-guide-resilient-app/src/main/java/io/koraframework/guide/resilient/service/DefaultRetry.java new file mode 100644 index 00000000..674ab832 --- /dev/null +++ b/guides/java/kora-java-guide-resilient-app/src/main/java/io/koraframework/guide/resilient/service/DefaultRetry.java @@ -0,0 +1,9 @@ +package io.koraframework.guide.resilient.service; + +import io.koraframework.resilient.retry.Retry; +import io.koraframework.resilient.retry.annotation.RetrySpec; + +@RetrySpec("resilient.retry.default") +public interface DefaultRetry extends Retry { + +} diff --git a/guides/java/kora-java-guide-resilient-app/src/main/java/io/koraframework/guide/resilient/service/DefaultTimeouter.java b/guides/java/kora-java-guide-resilient-app/src/main/java/io/koraframework/guide/resilient/service/DefaultTimeouter.java new file mode 100644 index 00000000..b29186e1 --- /dev/null +++ b/guides/java/kora-java-guide-resilient-app/src/main/java/io/koraframework/guide/resilient/service/DefaultTimeouter.java @@ -0,0 +1,9 @@ +package io.koraframework.guide.resilient.service; + +import io.koraframework.resilient.timeout.Timeouter; +import io.koraframework.resilient.timeout.annotation.TimeoutSpec; + +@TimeoutSpec("resilient.timeout.default") +public interface DefaultTimeouter extends Timeouter { + +} diff --git a/guides/java/kora-java-guide-resilient-app/src/main/java/io/koraframework/guide/resilient/service/UserService.java b/guides/java/kora-java-guide-resilient-app/src/main/java/io/koraframework/guide/resilient/service/UserService.java index 76175827..63a4fe9a 100644 --- a/guides/java/kora-java-guide-resilient-app/src/main/java/io/koraframework/guide/resilient/service/UserService.java +++ b/guides/java/kora-java-guide-resilient-app/src/main/java/io/koraframework/guide/resilient/service/UserService.java @@ -9,9 +9,9 @@ import io.koraframework.guide.resilient.dto.UserResponse; import io.koraframework.guide.resilient.repository.UserRepository; import io.koraframework.http.server.common.response.HttpServerResponseException; -import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreaker; +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakable; import io.koraframework.resilient.fallback.annotation.Fallback; -import io.koraframework.resilient.retry.annotation.Retry; +import io.koraframework.resilient.retry.annotation.Retryable; import io.koraframework.resilient.timeout.annotation.Timeout; @Component @@ -23,20 +23,20 @@ public UserService(UserRepository userRepository) { this.userRepository = userRepository; } - @Fallback(value = "default", method = "createUserFallback(request)") + @Fallback(method = "createUserFallback(request)") public UserResponse createUser(UserRequest request) { var generatedId = userRepository.save(request.name(), request.email()); return new UserResponse(generatedId, request.name(), request.email(), LocalDateTime.now()); } - @Retry("default") + @Retryable(DefaultRetry.class) public Optional getUser(String id) { return userRepository.findById(id); } - @CircuitBreaker("default") - @Retry("default") - @Timeout("default") + @CircuitBreakable(DefaultCircuitBreaker.class) + @Retryable(DefaultRetry.class) + @Timeout(DefaultTimeouter.class) public List getUsers(int page, int size, String sort) { return userRepository.findAll().stream() .sorted(getComparator(sort)) @@ -45,7 +45,7 @@ public List getUsers(int page, int size, String sort) { .toList(); } - @CircuitBreaker("default") + @CircuitBreakable(DefaultCircuitBreaker.class) public UserResponse updateUser(String id, UserRequest request) { boolean updated = userRepository.update(id, request.name(), request.email()); if (!updated) { @@ -54,7 +54,7 @@ public UserResponse updateUser(String id, UserRequest request) { return new UserResponse(id, request.name(), request.email(), LocalDateTime.now()); } - @Timeout("default") + @Timeout(DefaultTimeouter.class) public void deleteUser(String id) { boolean deleted = userRepository.deleteById(id); if (!deleted) { diff --git a/guides/java/kora-java-guide-resilient-app/src/main/resources/application.conf b/guides/java/kora-java-guide-resilient-app/src/main/resources/application.conf index 4441014e..f470ec9b 100644 --- a/guides/java/kora-java-guide-resilient-app/src/main/resources/application.conf +++ b/guides/java/kora-java-guide-resilient-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } @@ -19,12 +19,12 @@ resilient { } circuitbreaker { default { - slidingWindowSize = 2 + type = FIXED_WINDOW + countBased.windowSize = 2 minimumRequiredCalls = 2 failureRateThreshold = 100 permittedCallsInHalfOpenState = 1 waitDurationInOpenState = 200ms - failurePredicateName = "RecordServerErrorsOnly" } } } diff --git a/guides/java/kora-java-guide-s3-app/build.gradle b/guides/java/kora-java-guide-s3-app/build.gradle index 4e442f33..3e3d8c67 100644 --- a/guides/java/kora-java-guide-s3-app/build.gradle +++ b/guides/java/kora-java-guide-s3-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" @@ -30,13 +30,14 @@ dependencies { implementation "io.koraframework:json-common" implementation "io.koraframework:logging-logback" implementation "io.koraframework:s3-client-aws" + implementation "io.koraframework.experimental:s3-client-kora" testAnnotationProcessor "io.koraframework:annotation-processors" testImplementation platform("org.junit:junit-bom:$junitVersion") testImplementation "org.junit.jupiter:junit-jupiter" testImplementation "org.testcontainers:junit-jupiter:1.21.4" - testImplementation "io.goodforgod:testcontainers-extensions-minio:0.13.1" + testImplementation "io.goodforgod:testcontainers-extensions-minio:0.15.0" testImplementation "io.koraframework:test-junit5" } diff --git a/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/Application.java b/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/Application.java index 5697b2cd..e86a6dec 100644 --- a/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/Application.java +++ b/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/Application.java @@ -8,6 +8,7 @@ import io.koraframework.json.common.JsonModule; import io.koraframework.logging.logback.LogbackModule; import io.koraframework.s3.client.aws.AwsS3ClientModule; +import io.koraframework.s3.client.kora.KoraS3ClientModule; @KoraApp public interface Application extends @@ -16,6 +17,7 @@ public interface Application extends LogbackModule, OkHttpClientModule, AwsS3ClientModule, + KoraS3ClientModule, UndertowPublicHttpServerModule { static void main(String[] args) { diff --git a/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/controller/DataController.java b/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/controller/DataController.java index adf36c49..3c07da5d 100644 --- a/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/controller/DataController.java +++ b/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/controller/DataController.java @@ -1,7 +1,7 @@ package io.koraframework.guide.s3.controller; -import java.io.ByteArrayInputStream; -import java.io.InputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.util.List; import java.util.UUID; import io.koraframework.common.annotation.Component; @@ -17,8 +17,7 @@ import io.koraframework.http.server.common.response.HttpServerResponseException; import io.koraframework.http.server.common.annotation.HttpController; import io.koraframework.json.common.annotation.Json; -import io.koraframework.s3.client.S3NotFoundException; -import io.koraframework.s3.client.model.S3Body; +import io.koraframework.s3.client.kora.exception.S3ClientNoSuchKeyException; @Component @HttpController @@ -39,10 +38,17 @@ public FileMetadata uploadFile(FormMultipart multipart) { .orElseThrow(() -> new IllegalArgumentException("No file part named 'file' provided")); if (filePart instanceof FormMultipart.FormPart.MultipartFile mf) { - return this.uploadStream(mf.fileName(), mf.contentType(), new ByteArrayInputStream(mf.content())); + return this.upload(mf.contentType(), mf.content()); } + // a streamed part carries an HttpBodyOutput, which knows how to write itself out if (filePart instanceof FormMultipart.FormPart.MultipartFileStream mfs) { - return this.uploadStream(mfs.fileName(), mfs.contentType(), new ByteBufferPublisherInputStream(mfs.content())); + var buffer = new ByteArrayOutputStream(); + try { + mfs.content().write(buffer); + } catch (IOException e) { + throw HttpServerResponseException.of(400, "Failed to read uploaded file"); + } + return this.upload(mfs.content().contentType(), buffer.toByteArray()); } throw new IllegalArgumentException("Part 'file' must be a multipart file"); @@ -51,23 +57,24 @@ public FileMetadata uploadFile(FormMultipart multipart) { @HttpRoute(method = HttpMethod.GET, path = "/files") @Json public List listFiles() { - return this.s3FileClient.listFiles().objects().stream() - .map(object -> this.toMetadata(object.key(), object.size(), object.body().type())) + return this.s3FileClient.listFiles().items().stream() + .map(item -> this.toMetadata(item.key(), item.size(), null)) .toList(); } @HttpRoute(method = HttpMethod.GET, path = "/files/{fileId}") public HttpServerResponse downloadFile(String fileId) { - try { - var object = this.s3FileClient.downloadFile(fileId); - var bytes = object.body().asBytes(); - var contentType = object.body().type() == null ? "application/octet-stream" : object.body().type(); + try (var object = this.s3FileClient.downloadFile(fileId); var body = object.body().asInputStream()) { + var bytes = body.readAllBytes(); + var contentType = object.headers().getFirst("Content-Type"); return HttpServerResponse.of( 200, HttpHeaders.of("Content-Disposition", "attachment; filename=\"" + fileId + "\""), - HttpBody.of(contentType, bytes)); - } catch (S3NotFoundException e) { + HttpBody.of(contentType == null ? "application/octet-stream" : contentType, bytes)); + } catch (S3ClientNoSuchKeyException e) { throw HttpServerResponseException.of(404, "File not found"); + } catch (IOException e) { + throw HttpServerResponseException.of(500, "Failed to read file"); } } @@ -78,12 +85,11 @@ public DeleteFileResponse deleteFile(String fileId) { return new DeleteFileResponse("File deleted successfully"); } - private FileMetadata uploadStream(String fileName, String contentType, InputStream inputStream) { + private FileMetadata upload(String contentType, byte[] body) { String actualContentType = (contentType == null || contentType.isBlank()) ? "application/octet-stream" : contentType; String fileId = UUID.randomUUID().toString(); - S3Body body = S3Body.ofInputStreamReadAll(inputStream, actualContentType); this.s3FileClient.uploadFile(fileId, body); - return new FileMetadata(fileId, body.size(), actualContentType); + return new FileMetadata(fileId, (long) body.length, actualContentType); } private FileMetadata toMetadata(String key, Long size, String contentType) { diff --git a/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/controller/UserController.java b/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/controller/UserController.java index 87c8c3a6..6c2971ca 100644 --- a/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/controller/UserController.java +++ b/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/controller/UserController.java @@ -39,6 +39,7 @@ public UserController(UserService userService) { public record RequestContext(String requestId, String userAgent, String sessionId) {} + @Component public static final class RequestContextMapper implements HttpServerRequestMapper { @Override diff --git a/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/http/ExceptionHandler.java b/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/http/ExceptionHandler.java index 255c752f..f2031a01 100644 --- a/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/http/ExceptionHandler.java +++ b/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/http/ExceptionHandler.java @@ -1,20 +1,17 @@ package io.koraframework.guide.s3.http; -import java.util.concurrent.CompletionException; -import java.util.concurrent.CompletionStage; import io.koraframework.common.annotation.Component; -import io.koraframework.common.Context; import io.koraframework.common.annotation.Tag; import io.koraframework.guide.s3.dto.ErrorResponse; import io.koraframework.http.common.body.HttpBody; import io.koraframework.http.server.common.interceptor.HttpServerInterceptor; -import io.koraframework.http.server.common.HttpServerModule; +import io.koraframework.http.server.common.HttpServer; import io.koraframework.http.server.common.request.HttpServerRequest; import io.koraframework.http.server.common.response.HttpServerResponse; import io.koraframework.http.server.common.response.HttpServerResponseException; import io.koraframework.json.common.JsonWriter; -@Tag(HttpServerModule.class) +@Tag(HttpServer.class) @Component public final class ExceptionHandler implements HttpServerInterceptor { @@ -25,37 +22,22 @@ public ExceptionHandler(JsonWriter errorJsonWriter) { } @Override - public CompletionStage intercept(Context context, HttpServerRequest request, InterceptChain chain) - throws Exception { - return chain.process(context, request).exceptionally(throwable -> { - Throwable cause = unwrap(throwable); - if (cause instanceof HttpServerResponseException responseException) { - return HttpServerResponse.of(responseException.code(), - HttpBody.json(errorJsonWriter.toByteArrayUnchecked( - ErrorResponse.of("HTTP_" + responseException.code(), responseException.getMessage())))); - } - if (cause instanceof IllegalArgumentException) { - return HttpServerResponse.of(400, - HttpBody.json(errorJsonWriter.toByteArrayUnchecked( - ErrorResponse.of("BAD_REQUEST", "Invalid request parameters")))); - } - if (cause instanceof SecurityException) { - return HttpServerResponse.of(403, - HttpBody.json(errorJsonWriter.toByteArrayUnchecked( - ErrorResponse.of("FORBIDDEN", "Access denied")))); - } - return HttpServerResponse.of(500, - HttpBody.json(errorJsonWriter.toByteArrayUnchecked( - ErrorResponse.of("INTERNAL_ERROR", "An unexpected error occurred")))); - }); + public HttpServerResponse intercept(HttpServerRequest request, InterceptChain chain) { + try { + return chain.process(request); + } catch (HttpServerResponseException e) { + return jsonResponse(e.code(), "HTTP_" + e.code(), e.getMessage()); + } catch (IllegalArgumentException e) { + return jsonResponse(400, "BAD_REQUEST", "Invalid request parameters"); + } catch (SecurityException e) { + return jsonResponse(403, "FORBIDDEN", "Access denied"); + } catch (Exception e) { + return jsonResponse(500, "INTERNAL_ERROR", "An unexpected error occurred"); + } } - private static Throwable unwrap(Throwable throwable) { - Throwable current = throwable; - while (current instanceof CompletionException && current.getCause() != null) { - current = current.getCause(); - } - return current; + private HttpServerResponse jsonResponse(int statusCode, String code, String message) { + return HttpServerResponse.of(statusCode, + HttpBody.json(this.errorJsonWriter.toByteArray(ErrorResponse.of(code, message)))); } } - diff --git a/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/http/LoggingInterceptor.java b/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/http/LoggingInterceptor.java index d11a3941..4f346c1d 100644 --- a/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/http/LoggingInterceptor.java +++ b/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/http/LoggingInterceptor.java @@ -1,8 +1,6 @@ package io.koraframework.guide.s3.http; -import java.util.concurrent.CompletionStage; import io.koraframework.common.annotation.Component; -import io.koraframework.common.Context; import io.koraframework.http.server.common.interceptor.HttpServerInterceptor; import io.koraframework.http.server.common.request.HttpServerRequest; import io.koraframework.http.server.common.response.HttpServerResponse; @@ -11,14 +9,17 @@ public final class LoggingInterceptor implements HttpServerInterceptor { @Override - public CompletionStage intercept(Context context, HttpServerRequest request, InterceptChain chain) - throws Exception { + public HttpServerResponse intercept(HttpServerRequest request, InterceptChain chain) throws Exception { long started = System.nanoTime(); - return chain.process(context, request).whenComplete((response, throwable) -> { + // stays 500 when the chain throws, matching the reactive version that logged 500 on failure + int statusCode = 500; + try { + var response = chain.process(request); + statusCode = response.code(); + return response; + } finally { long durationMs = (System.nanoTime() - started) / 1_000_000; - int statusCode = response != null ? response.code() : 500; System.out.printf("Request: %s %s -> %d (%d ms)%n", request.method(), request.path(), statusCode, durationMs); - }); + } } } - diff --git a/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/s3/S3BucketInitializer.java b/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/s3/S3BucketInitializer.java index ab79ab23..4bfe0f2f 100644 --- a/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/s3/S3BucketInitializer.java +++ b/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/s3/S3BucketInitializer.java @@ -2,27 +2,31 @@ import io.koraframework.application.graph.Lifecycle; import io.koraframework.common.annotation.Component; -import io.koraframework.common.annotation.Tag; -import io.koraframework.s3.client.S3ClientConfig; +import io.koraframework.common.annotation.Root; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.CreateBucketRequest; import software.amazon.awssdk.services.s3.model.HeadBucketRequest; import software.amazon.awssdk.services.s3.model.NoSuchBucketException; +/** + * Bucket administration is not part of the declarative {@code @S3} contract, so it goes through the + * AWS SDK client that {@code s3-client-aws} publishes. + */ +@Root @Component public final class S3BucketInitializer implements Lifecycle { private final S3Client s3Client; - private final S3ClientConfig s3ClientConfig; + private final S3UploadsConfig config; - public S3BucketInitializer(S3Client s3Client, @Tag(S3FileClient.class) S3ClientConfig s3ClientConfig) { + public S3BucketInitializer(S3Client s3Client, S3UploadsConfig config) { this.s3Client = s3Client; - this.s3ClientConfig = s3ClientConfig; + this.config = config; } @Override public void init() { - var bucket = this.s3ClientConfig.bucket(); + var bucket = this.config.bucket(); try { this.s3Client.headBucket(HeadBucketRequest.builder().bucket(bucket).build()); } catch (NoSuchBucketException e) { @@ -32,4 +36,4 @@ public void init() { @Override public void release() {} -} \ No newline at end of file +} diff --git a/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/s3/S3FileClient.java b/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/s3/S3FileClient.java index 3cac52e8..888a3d55 100644 --- a/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/s3/S3FileClient.java +++ b/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/s3/S3FileClient.java @@ -1,23 +1,22 @@ package io.koraframework.guide.s3.s3; -import io.koraframework.s3.client.annotation.S3; -import io.koraframework.s3.client.model.S3Body; -import io.koraframework.s3.client.model.S3Object; -import io.koraframework.s3.client.model.S3ObjectList; -import io.koraframework.s3.client.model.S3ObjectUpload; +import io.koraframework.s3.client.kora.annotation.S3; +import io.koraframework.s3.client.kora.model.response.GetObjectResult; +import io.koraframework.s3.client.kora.model.response.ListBucketResult; @S3.Client("s3client.uploads") +@S3.Bucket(".bucket") public interface S3FileClient { @S3.Put("files/{fileId}") - S3ObjectUpload uploadFile(String fileId, S3Body body); + String uploadFile(String fileId, byte[] body); @S3.Get("files/{fileId}") - S3Object downloadFile(String fileId); + GetObjectResult downloadFile(String fileId); @S3.List("files/") - S3ObjectList listFiles(); + ListBucketResult listFiles(); @S3.Delete("files/{fileId}") void deleteFile(String fileId); -} \ No newline at end of file +} diff --git a/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/s3/S3UploadsConfig.java b/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/s3/S3UploadsConfig.java new file mode 100644 index 00000000..dd0cbbd1 --- /dev/null +++ b/guides/java/kora-java-guide-s3-app/src/main/java/io/koraframework/guide/s3/s3/S3UploadsConfig.java @@ -0,0 +1,13 @@ +package io.koraframework.guide.s3.s3; + +import io.koraframework.config.common.annotation.ConfigSource; + +/** + * The declarative client reads its bucket through {@code @S3.Bucket}, which produces a generated + * class rather than an injectable config, so bucket administration reads the same path itself. + */ +@ConfigSource("s3client.uploads") +public interface S3UploadsConfig { + + String bucket(); +} diff --git a/guides/java/kora-java-guide-s3-app/src/main/resources/application.conf b/guides/java/kora-java-guide-s3-app/src/main/resources/application.conf index 07207b84..0be58df6 100644 --- a/guides/java/kora-java-guide-s3-app/src/main/resources/application.conf +++ b/guides/java/kora-java-guide-s3-app/src/main/resources/application.conf @@ -1,19 +1,33 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } -s3client { +# s3-client-aws: the AWS SDK client, used here only for bucket administration +s3client.aws { url = ${S3_URL} - accessKey = ${S3_ACCESS_KEY} - secretKey = ${S3_SECRET_KEY} region = "us-east-1" region = ${?S3_REGION} - uploads { - bucket = "uploads" - bucket = ${?S3_BUCKET} + credentials { + accessKey = ${S3_ACCESS_KEY} + secretKey = ${S3_SECRET_KEY} + } +} + +# s3-client-kora: the declarative @S3.Client, path taken from its @S3.Client annotation +s3client.uploads { + endpoint = ${S3_URL} + region = "us-east-1" + region = ${?S3_REGION} + + bucket = "uploads" + bucket = ${?S3_BUCKET} + + credentials { + accessKey = ${S3_ACCESS_KEY} + secretKey = ${S3_SECRET_KEY} } } diff --git a/guides/java/kora-java-guide-s3-app/src/test/java/io/koraframework/guide/s3/S3AppTest.java b/guides/java/kora-java-guide-s3-app/src/test/java/io/koraframework/guide/s3/S3AppTest.java index 2c9ecad0..803140ba 100644 --- a/guides/java/kora-java-guide-s3-app/src/test/java/io/koraframework/guide/s3/S3AppTest.java +++ b/guides/java/kora-java-guide-s3-app/src/test/java/io/koraframework/guide/s3/S3AppTest.java @@ -16,8 +16,7 @@ import io.koraframework.guide.s3.s3.S3FileClient; import io.koraframework.guide.s3.s3.FileMetadata; import io.koraframework.http.common.form.FormMultipart; -import io.koraframework.s3.client.S3NotFoundException; -import io.koraframework.s3.client.model.S3Body; +import io.koraframework.s3.client.kora.exception.S3ClientNoSuchKeyException; import io.koraframework.test.extension.junit5.KoraAppTest; import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier; import io.koraframework.test.extension.junit5.KoraConfigModification; @@ -53,17 +52,19 @@ public KoraConfigModification config() { } @Test - void declarativeClientCrudWorks() { + void declarativeClientCrudWorks() throws Exception { var content = "guide-client-body".getBytes(StandardCharsets.UTF_8); var fileId = "file-1"; - this.s3FileClient.uploadFile(fileId, S3Body.ofInputStream(new ByteArrayInputStream(content), content.length, "text/plain")); + this.s3FileClient.uploadFile(fileId, content); - var downloaded = this.s3FileClient.downloadFile(fileId); - assertArrayEquals(content, downloaded.body().asBytes()); + try (var downloaded = this.s3FileClient.downloadFile(fileId); + var body = downloaded.body().asInputStream()) { + assertArrayEquals(content, body.readAllBytes()); + } this.s3FileClient.deleteFile(fileId); - assertThrows(S3NotFoundException.class, () -> this.s3FileClient.downloadFile(fileId)); + assertThrows(S3ClientNoSuchKeyException.class, () -> this.s3FileClient.downloadFile(fileId)); } @Test diff --git a/guides/java/kora-java-guide-testing-black-box-app/build.gradle b/guides/java/kora-java-guide-testing-black-box-app/build.gradle index 531e09c3..9c58a70d 100644 --- a/guides/java/kora-java-guide-testing-black-box-app/build.gradle +++ b/guides/java/kora-java-guide-testing-black-box-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" diff --git a/guides/java/kora-java-guide-testing-integration-app/build.gradle b/guides/java/kora-java-guide-testing-integration-app/build.gradle index e5e21f0d..9b56f32d 100644 --- a/guides/java/kora-java-guide-testing-integration-app/build.gradle +++ b/guides/java/kora-java-guide-testing-integration-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" @@ -33,6 +33,9 @@ dependencies { testImplementation project(":guides:java:kora-java-guide-database-jdbc-app") testImplementation "io.koraframework:config-hocon" testImplementation "io.koraframework:database-flyway" + // flyway-core 13 не содержит поддержки конкретных СУБД, иначе Flyway падает + // с "Unsupported Database: PostgreSQL" + testImplementation "org.flywaydb:flyway-database-postgresql:13.1.0" testImplementation "io.koraframework:database-jdbc" testImplementation "io.koraframework:http-client-common" testImplementation "io.koraframework:http-server-undertow" diff --git a/guides/java/kora-java-guide-testing-integration-app/src/test/java/io/koraframework/guide/testingintegration/UserServiceIntegrationPostgresTest.java b/guides/java/kora-java-guide-testing-integration-app/src/test/java/io/koraframework/guide/testingintegration/UserServiceIntegrationPostgresTest.java index 9cbca00b..84d7a59b 100644 --- a/guides/java/kora-java-guide-testing-integration-app/src/test/java/io/koraframework/guide/testingintegration/UserServiceIntegrationPostgresTest.java +++ b/guides/java/kora-java-guide-testing-integration-app/src/test/java/io/koraframework/guide/testingintegration/UserServiceIntegrationPostgresTest.java @@ -41,7 +41,7 @@ class UserServiceIntegrationPostgresTest implements KoraAppTestConfigModifier { @Override public KoraConfigModification config() { return KoraConfigModification.ofString(""" - db { + jdbc { jdbcUrl = ${POSTGRES_JDBC_URL} username = ${POSTGRES_USER} password = ${POSTGRES_PASS} diff --git a/guides/java/kora-java-guide-testing-junit-app/build.gradle b/guides/java/kora-java-guide-testing-junit-app/build.gradle index 771d6f69..5e3ef038 100644 --- a/guides/java/kora-java-guide-testing-junit-app/build.gradle +++ b/guides/java/kora-java-guide-testing-junit-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" @@ -52,4 +52,3 @@ test { exceptionFormat("full") } } - diff --git a/guides/java/kora-java-guide-validation-app/build.gradle b/guides/java/kora-java-guide-validation-app/build.gradle index 7c3d60bb..fb2fe17f 100644 --- a/guides/java/kora-java-guide-validation-app/build.gradle +++ b/guides/java/kora-java-guide-validation-app/build.gradle @@ -20,7 +20,7 @@ configurations { } dependencies { - koraBom platform("io.koraframework:kora-parent:$koraVersion") + koraBom platform("io.koraframework:kora-bom:$koraVersion") annotationProcessor "io.koraframework:annotation-processors" diff --git a/guides/java/kora-java-guide-validation-app/src/main/java/io/koraframework/guide/validation/Application.java b/guides/java/kora-java-guide-validation-app/src/main/java/io/koraframework/guide/validation/Application.java index bac69cf2..72b2a3e3 100644 --- a/guides/java/kora-java-guide-validation-app/src/main/java/io/koraframework/guide/validation/Application.java +++ b/guides/java/kora-java-guide-validation-app/src/main/java/io/koraframework/guide/validation/Application.java @@ -8,7 +8,7 @@ import io.koraframework.guide.validation.dto.ValidationErrorDetails; import io.koraframework.guide.validation.dto.ValidationErrorResponse; import io.koraframework.http.common.body.HttpBody; -import io.koraframework.http.server.common.HttpServerModule; +import io.koraframework.http.server.common.HttpServer; import io.koraframework.http.server.common.response.HttpServerResponse; import io.koraframework.http.server.undertow.UndertowPublicHttpServerModule; import io.koraframework.json.common.JsonWriter; @@ -36,11 +36,11 @@ default ViolationExceptionHttpServerResponseMapper violationExceptionHttpServerR JsonWriter errorResponseJsonWriter) { return (request, exception) -> HttpServerResponse.of( 400, - HttpBody.json(errorResponseJsonWriter.toByteArrayUnchecked( + HttpBody.json(errorResponseJsonWriter.toByteArray( ValidationErrorResponse.of(toValidationErrors(exception.getViolations()))))); } - @Tag(HttpServerModule.class) + @Tag(HttpServer.class) default ValidationHttpServerInterceptor validationHttpServerInterceptor( ViolationExceptionHttpServerResponseMapper violationExceptionHttpServerResponseMapper) { return new ValidationHttpServerInterceptor(violationExceptionHttpServerResponseMapper); diff --git a/guides/java/kora-java-guide-validation-app/src/main/java/io/koraframework/guide/validation/dto/UserRequest.java b/guides/java/kora-java-guide-validation-app/src/main/java/io/koraframework/guide/validation/dto/UserRequest.java index 0277d203..c2d2d53d 100644 --- a/guides/java/kora-java-guide-validation-app/src/main/java/io/koraframework/guide/validation/dto/UserRequest.java +++ b/guides/java/kora-java-guide-validation-app/src/main/java/io/koraframework/guide/validation/dto/UserRequest.java @@ -2,10 +2,12 @@ import io.koraframework.json.common.annotation.Json; import io.koraframework.validation.common.annotation.NotBlank; +import io.koraframework.validation.common.annotation.Valid; import io.koraframework.validation.common.annotation.Pattern; import io.koraframework.validation.common.annotation.Size; @Json +@Valid public record UserRequest( @NotBlank @Size(min = 2, max = 100) String name, @NotBlank @Pattern("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$") String email diff --git a/guides/java/kora-java-guide-validation-app/src/main/resources/application.conf b/guides/java/kora-java-guide-validation-app/src/main/resources/application.conf index 97dae54e..c89518fa 100644 --- a/guides/java/kora-java-guide-validation-app/src/main/resources/application.conf +++ b/guides/java/kora-java-guide-validation-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } diff --git a/guides/kotlin/kora-kotlin-guide-cache-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-cache-app/build.gradle.kts index d25de0c4..306655dc 100644 --- a/guides/kotlin/kora-kotlin-guide-cache-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-cache-app/build.gradle.kts @@ -7,28 +7,15 @@ plugins { id("application") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:cache-caffeine") implementation("io.koraframework:config-hocon") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") - kspTest("io.koraframework:symbol-processors") testImplementation(sourceSets.main.get().output) testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") diff --git a/guides/kotlin/kora-kotlin-guide-cache-app/src/main/resources/application.conf b/guides/kotlin/kora-kotlin-guide-cache-app/src/main/resources/application.conf index 3a0d07cf..2162fdef 100644 --- a/guides/kotlin/kora-kotlin-guide-cache-app/src/main/resources/application.conf +++ b/guides/kotlin/kora-kotlin-guide-cache-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } diff --git a/guides/kotlin/kora-kotlin-guide-cache-app/src/test/kotlin/io/koraframework/guide/cache/CacheAppTest.kt b/guides/kotlin/kora-kotlin-guide-cache-app/src/test/kotlin/io/koraframework/guide/cache/CacheAppTest.kt index 5c2ffa59..21375595 100644 --- a/guides/kotlin/kora-kotlin-guide-cache-app/src/test/kotlin/io/koraframework/guide/cache/CacheAppTest.kt +++ b/guides/kotlin/kora-kotlin-guide-cache-app/src/test/kotlin/io/koraframework/guide/cache/CacheAppTest.kt @@ -41,7 +41,7 @@ class CacheAppTest { assertNotNull(first) assertEquals(created.id, first!!.id) - assertEquals(created.id, userCache.get(created.id).id) + assertEquals(created.id, userCache.get(created.id)!!.id) } @Test diff --git a/guides/kotlin/kora-kotlin-guide-cache-multi-level-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-cache-multi-level-app/build.gradle.kts index 3b0b936e..107f110f 100644 --- a/guides/kotlin/kora-kotlin-guide-cache-multi-level-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-cache-multi-level-app/build.gradle.kts @@ -7,33 +7,20 @@ plugins { id("application") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:cache-caffeine") implementation("io.koraframework:cache-redis-lettuce") implementation("io.koraframework:config-hocon") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") - testImplementation("io.goodforgod:testcontainers-extensions-redis:0.13.1") + testImplementation("io.goodforgod:testcontainers-extensions-redis:0.15.0") testImplementation("redis.clients:jedis:4.4.3") } diff --git a/guides/kotlin/kora-kotlin-guide-cache-multi-level-app/src/main/kotlin/io/koraframework/guide/cache/Application.kt b/guides/kotlin/kora-kotlin-guide-cache-multi-level-app/src/main/kotlin/io/koraframework/guide/cache/Application.kt index 8254128e..e13a32cd 100644 --- a/guides/kotlin/kora-kotlin-guide-cache-multi-level-app/src/main/kotlin/io/koraframework/guide/cache/Application.kt +++ b/guides/kotlin/kora-kotlin-guide-cache-multi-level-app/src/main/kotlin/io/koraframework/guide/cache/Application.kt @@ -16,7 +16,7 @@ interface Application : LogbackModule, UndertowPublicHttpServerModule, CaffeineCacheModule, - RedisCacheModule + LettuceRedisCacheModule fun main() { KoraApplication.run(ApplicationGraph::graph) diff --git a/guides/kotlin/kora-kotlin-guide-cache-multi-level-app/src/main/resources/application.conf b/guides/kotlin/kora-kotlin-guide-cache-multi-level-app/src/main/resources/application.conf index 838d1af7..d5052822 100644 --- a/guides/kotlin/kora-kotlin-guide-cache-multi-level-app/src/main/resources/application.conf +++ b/guides/kotlin/kora-kotlin-guide-cache-multi-level-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } diff --git a/guides/kotlin/kora-kotlin-guide-config-hocon-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-config-hocon-app/build.gradle.kts index 2e2189cf..f8b4f178 100644 --- a/guides/kotlin/kora-kotlin-guide-config-hocon-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-config-hocon-app/build.gradle.kts @@ -7,25 +7,12 @@ plugins { id("application") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:logging-logback") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") diff --git a/guides/kotlin/kora-kotlin-guide-config-hocon-app/src/main/kotlin/io/koraframework/guide/config/hocon/Application.kt b/guides/kotlin/kora-kotlin-guide-config-hocon-app/src/main/kotlin/io/koraframework/guide/config/hocon/Application.kt index 896051f2..b5c95929 100644 --- a/guides/kotlin/kora-kotlin-guide-config-hocon-app/src/main/kotlin/io/koraframework/guide/config/hocon/Application.kt +++ b/guides/kotlin/kora-kotlin-guide-config-hocon-app/src/main/kotlin/io/koraframework/guide/config/hocon/Application.kt @@ -4,7 +4,7 @@ import io.koraframework.application.graph.KoraApplication import io.koraframework.common.annotation.KoraApp import io.koraframework.common.annotation.Tag import io.koraframework.config.common.Config -import io.koraframework.config.common.extractor.ConfigMapper +import io.koraframework.config.common.mapper.ConfigValueMapper import io.koraframework.config.hocon.HoconConfigModule import io.koraframework.logging.logback.LogbackModule @@ -15,13 +15,13 @@ interface Application : HoconConfigModule, LogbackModule { class Lib2Tag private constructor() @Tag(Lib1Tag::class) - fun lib1Config(config: Config, extractor: ConfigMapper): LibConfig { - return extractor.extract(config.get("libs.lib1")) + fun lib1Config(config: Config, extractor: ConfigValueMapper): LibConfig { + return extractor.mapOrThrow(config.get("libs.lib1")) } @Tag(Lib2Tag::class) - fun lib2Config(config: Config, extractor: ConfigMapper): LibConfig { - return extractor.extract(config.get("libs.lib2")) + fun lib2Config(config: Config, extractor: ConfigValueMapper): LibConfig { + return extractor.mapOrThrow(config.get("libs.lib2")) } } diff --git a/guides/kotlin/kora-kotlin-guide-config-yaml-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-config-yaml-app/build.gradle.kts index 3f390165..5f831455 100644 --- a/guides/kotlin/kora-kotlin-guide-config-yaml-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-config-yaml-app/build.gradle.kts @@ -7,25 +7,12 @@ plugins { id("application") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-yaml") implementation("io.koraframework:logging-logback") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") diff --git a/guides/kotlin/kora-kotlin-guide-config-yaml-app/src/main/kotlin/io/koraframework/guide/config/yaml/Application.kt b/guides/kotlin/kora-kotlin-guide-config-yaml-app/src/main/kotlin/io/koraframework/guide/config/yaml/Application.kt index 8262cfd0..d998c7fb 100644 --- a/guides/kotlin/kora-kotlin-guide-config-yaml-app/src/main/kotlin/io/koraframework/guide/config/yaml/Application.kt +++ b/guides/kotlin/kora-kotlin-guide-config-yaml-app/src/main/kotlin/io/koraframework/guide/config/yaml/Application.kt @@ -4,7 +4,7 @@ import io.koraframework.application.graph.KoraApplication import io.koraframework.common.annotation.KoraApp import io.koraframework.common.annotation.Tag import io.koraframework.config.common.Config -import io.koraframework.config.common.extractor.ConfigMapper +import io.koraframework.config.common.mapper.ConfigValueMapper import io.koraframework.config.yaml.YamlConfigModule import io.koraframework.logging.logback.LogbackModule @@ -15,13 +15,13 @@ interface Application : YamlConfigModule, LogbackModule { class Lib2Tag private constructor() @Tag(Lib1Tag::class) - fun lib1Config(config: Config, extractor: ConfigMapper): LibConfig { - return extractor.extract(config.get("libs.lib1")) + fun lib1Config(config: Config, extractor: ConfigValueMapper): LibConfig { + return extractor.mapOrThrow(config.get("libs.lib1")) } @Tag(Lib2Tag::class) - fun lib2Config(config: Config, extractor: ConfigMapper): LibConfig { - return extractor.extract(config.get("libs.lib2")) + fun lib2Config(config: Config, extractor: ConfigValueMapper): LibConfig { + return extractor.mapOrThrow(config.get("libs.lib2")) } } diff --git a/guides/kotlin/kora-kotlin-guide-database-cassandra-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-database-cassandra-app/build.gradle.kts index cada7fa2..e8007f10 100644 --- a/guides/kotlin/kora-kotlin-guide-database-cassandra-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-database-cassandra-app/build.gradle.kts @@ -7,31 +7,18 @@ plugins { id("application") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:database-cassandra") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") - testImplementation("io.goodforgod:testcontainers-extensions-scylla:0.13.1") + testImplementation("io.goodforgod:testcontainers-extensions-scylla:0.15.0") testImplementation("io.koraframework:test-junit5") } diff --git a/guides/kotlin/kora-kotlin-guide-database-cassandra-app/src/main/resources/application.conf b/guides/kotlin/kora-kotlin-guide-database-cassandra-app/src/main/resources/application.conf index c0bc5877..27f4c372 100644 --- a/guides/kotlin/kora-kotlin-guide-database-cassandra-app/src/main/resources/application.conf +++ b/guides/kotlin/kora-kotlin-guide-database-cassandra-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } diff --git a/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/build.gradle.kts index 40ec91ad..2a424c4d 100644 --- a/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/build.gradle.kts @@ -7,32 +7,22 @@ plugins { id("application") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") runtimeOnly("org.postgresql:postgresql:42.7.3") implementation("io.koraframework:config-hocon") implementation("io.koraframework:database-flyway") + // с Flyway 10 поддержка конкретных СУБД вынесена в отдельные артефакты; + // без этого приложение падает на старте: "Unsupported Database: PostgreSQL" + implementation("org.flywaydb:flyway-database-postgresql:13.1.0") implementation("io.koraframework:database-jdbc") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) - testImplementation("io.goodforgod:testcontainers-extensions-postgres:0.13.1") + testImplementation("io.goodforgod:testcontainers-extensions-postgres:0.15.0") testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") } diff --git a/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/src/main/kotlin/io/koraframework/guide/databasejdbc/advanced/task/repository/mapper/ListOfLongJdbcParameterMapper.kt b/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/src/main/kotlin/io/koraframework/guide/databasejdbc/advanced/task/repository/mapper/ListOfLongJdbcParameterMapper.kt index 09bbf479..ff8fa516 100644 --- a/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/src/main/kotlin/io/koraframework/guide/databasejdbc/advanced/task/repository/mapper/ListOfLongJdbcParameterMapper.kt +++ b/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/src/main/kotlin/io/koraframework/guide/databasejdbc/advanced/task/repository/mapper/ListOfLongJdbcParameterMapper.kt @@ -3,11 +3,18 @@ package io.koraframework.guide.databasejdbc.advanced.task.repository.mapper import io.koraframework.common.annotation.Component import io.koraframework.database.jdbc.mapper.parameter.JdbcParameterColumnMapper import java.sql.PreparedStatement +import java.sql.Types @Component class ListOfLongJdbcParameterMapper : JdbcParameterColumnMapper> { - override fun set(stmt: PreparedStatement, index: Int, value: List) { + // the 2.0 contract declares the value as @Nullable, which Kotlin enforces on the override + override fun set(stmt: PreparedStatement, index: Int, value: List?) { + if (value == null) { + stmt.setNull(index, Types.ARRAY) + return + } + val sqlArray = stmt.connection.createArrayOf("BIGINT", value.toTypedArray()) stmt.setArray(index, sqlArray) } diff --git a/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/src/main/kotlin/io/koraframework/guide/databasejdbc/advanced/task/repository/mapper/TaskStatusParameterMapper.kt b/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/src/main/kotlin/io/koraframework/guide/databasejdbc/advanced/task/repository/mapper/TaskStatusParameterMapper.kt index a9443901..af70472c 100644 --- a/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/src/main/kotlin/io/koraframework/guide/databasejdbc/advanced/task/repository/mapper/TaskStatusParameterMapper.kt +++ b/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/src/main/kotlin/io/koraframework/guide/databasejdbc/advanced/task/repository/mapper/TaskStatusParameterMapper.kt @@ -4,11 +4,16 @@ import io.koraframework.common.annotation.Component import io.koraframework.database.jdbc.mapper.parameter.JdbcParameterColumnMapper import io.koraframework.guide.databasejdbc.advanced.task.dto.TaskStatus import java.sql.PreparedStatement +import java.sql.Types @Component class TaskStatusParameterMapper : JdbcParameterColumnMapper { - override fun set(stmt: PreparedStatement, index: Int, value: TaskStatus) { - stmt.setString(index, value.name) + override fun set(stmt: PreparedStatement, index: Int, value: TaskStatus?) { + if (value == null) { + stmt.setNull(index, Types.VARCHAR) + } else { + stmt.setString(index, value.name) + } } } diff --git a/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/src/main/kotlin/io/koraframework/guide/databasejdbc/advanced/task/service/TaskService.kt b/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/src/main/kotlin/io/koraframework/guide/databasejdbc/advanced/task/service/TaskService.kt index 6fefd71a..bfe78e1c 100644 --- a/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/src/main/kotlin/io/koraframework/guide/databasejdbc/advanced/task/service/TaskService.kt +++ b/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/src/main/kotlin/io/koraframework/guide/databasejdbc/advanced/task/service/TaskService.kt @@ -1,7 +1,7 @@ package io.koraframework.guide.databasejdbc.advanced.task.service import io.koraframework.common.annotation.Component -import io.koraframework.database.jdbc.JdbcHelper.SqlFunction1 +import io.koraframework.database.jdbc.JdbcExecutor import io.koraframework.guide.databasejdbc.advanced.dto.UserRequest import io.koraframework.guide.databasejdbc.advanced.task.dto.TaskRequest import io.koraframework.guide.databasejdbc.advanced.task.dto.TaskResponse @@ -17,7 +17,7 @@ class TaskService( ) { fun createTasks(taskCreates: List): List { - return taskRepository.jdbcConnectionFactory.inTx(SqlFunction1 { + return taskRepository.executor().inTx(JdbcExecutor.SqlSupplier { val assigneeIds = taskCreates .mapNotNull { it.userAssigneeId } .distinct() @@ -62,7 +62,7 @@ class TaskService( } fun assignTask(taskId: Long, userId: Long) { - taskRepository.jdbcConnectionFactory.inTx(SqlFunction1 { + taskRepository.executor().inTx(JdbcExecutor.SqlRunnable { val updated = taskRepository.updateAssignee(taskId, userId) if (updated.value() < 1) { throw HttpServerResponseException.of(404, "Task not found") diff --git a/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/src/main/resources/application.conf b/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/src/main/resources/application.conf index 8f8b5333..2d2a1442 100644 --- a/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/src/main/resources/application.conf +++ b/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/src/main/resources/application.conf @@ -1,10 +1,10 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } -db { +jdbc { jdbcUrl = ${?POSTGRES_JDBC_URL} username = ${?POSTGRES_USER} password = ${?POSTGRES_PASS} diff --git a/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/src/test/kotlin/io/koraframework/guide/databasejdbc/advanced/TaskServicePostgresAdvancedTest.kt b/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/src/test/kotlin/io/koraframework/guide/databasejdbc/advanced/TaskServicePostgresAdvancedTest.kt index fca32bcf..9188b573 100644 --- a/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/src/test/kotlin/io/koraframework/guide/databasejdbc/advanced/TaskServicePostgresAdvancedTest.kt +++ b/guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app/src/test/kotlin/io/koraframework/guide/databasejdbc/advanced/TaskServicePostgresAdvancedTest.kt @@ -40,7 +40,7 @@ class TaskServicePostgresAdvancedTest : KoraAppTestConfigModifier { override fun config(): KoraConfigModification = KoraConfigModification.ofString( """ - db { + jdbc { jdbcUrl = ${'$'}{POSTGRES_JDBC_URL} username = ${'$'}{POSTGRES_USER} password = ${'$'}{POSTGRES_PASS} diff --git a/guides/kotlin/kora-kotlin-guide-database-jdbc-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-database-jdbc-app/build.gradle.kts index aafb870c..b172ee17 100644 --- a/guides/kotlin/kora-kotlin-guide-database-jdbc-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-database-jdbc-app/build.gradle.kts @@ -7,30 +7,20 @@ plugins { id("application") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") runtimeOnly("org.postgresql:postgresql:42.7.3") implementation("io.koraframework:config-hocon") implementation("io.koraframework:database-flyway") + // с Flyway 10 поддержка конкретных СУБД вынесена в отдельные артефакты; + // без этого приложение падает на старте: "Unsupported Database: PostgreSQL" + implementation("org.flywaydb:flyway-database-postgresql:13.1.0") implementation("io.koraframework:database-jdbc") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") diff --git a/guides/kotlin/kora-kotlin-guide-database-jdbc-app/src/main/resources/application.conf b/guides/kotlin/kora-kotlin-guide-database-jdbc-app/src/main/resources/application.conf index 440a5a87..adea4ee3 100644 --- a/guides/kotlin/kora-kotlin-guide-database-jdbc-app/src/main/resources/application.conf +++ b/guides/kotlin/kora-kotlin-guide-database-jdbc-app/src/main/resources/application.conf @@ -1,10 +1,10 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } -db { +jdbc { jdbcUrl = ${?POSTGRES_JDBC_URL} username = ${?POSTGRES_USER} password = ${?POSTGRES_PASS} diff --git a/guides/kotlin/kora-kotlin-guide-dependency-injection-introduction-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-dependency-injection-introduction-app/build.gradle.kts index b6e2d28e..7d232519 100644 --- a/guides/kotlin/kora-kotlin-guide-dependency-injection-introduction-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-dependency-injection-introduction-app/build.gradle.kts @@ -7,25 +7,12 @@ plugins { id("application") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:logging-logback") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") diff --git a/guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-app/build.gradle.kts index 20f27adc..1bfcbb45 100644 --- a/guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-app/build.gradle.kts @@ -1,18 +1,6 @@ import org.gradle.jvm.toolchain.JavaLanguageVersion import org.gradle.jvm.toolchain.JvmVendorSpec -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - plugins { id("org.jetbrains.kotlin.jvm") id("com.google.devtools.ksp") @@ -20,15 +8,14 @@ plugins { } dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation(project(":guides:kotlin:kora-kotlin-guide-dependency-injection:kora-kotlin-guide-dependency-injection-common")) implementation(project(":guides:kotlin:kora-kotlin-guide-dependency-injection:kora-kotlin-guide-dependency-injection-lib")) implementation(project(":guides:kotlin:kora-kotlin-guide-dependency-injection:kora-kotlin-guide-dependency-injection-submodule")) implementation("io.koraframework:config-hocon") implementation("io.koraframework:logging-logback") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") diff --git a/guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-lib/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-lib/build.gradle.kts index 7ef3c3b3..4cc33b23 100644 --- a/guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-lib/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-lib/build.gradle.kts @@ -1,18 +1,13 @@ -val koraBom: Configuration by configurations.creating -configurations { - compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - plugins { id("org.jetbrains.kotlin.jvm") + id("com.google.devtools.ksp") id("java-library") } dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") api(project(":guides:kotlin:kora-kotlin-guide-dependency-injection:kora-kotlin-guide-dependency-injection-common")) @@ -22,3 +17,7 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") } + +kotlin { + sourceSets.main { kotlin.srcDir("build/generated/ksp/main/kotlin") } +} diff --git a/guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-lib/src/main/kotlin/io/koraframework/guide/dependencyinjection/email/EmailConfig.kt b/guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-lib/src/main/kotlin/io/koraframework/guide/dependencyinjection/email/EmailConfig.kt index 9a5442c5..dc319a8c 100644 --- a/guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-lib/src/main/kotlin/io/koraframework/guide/dependencyinjection/email/EmailConfig.kt +++ b/guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-lib/src/main/kotlin/io/koraframework/guide/dependencyinjection/email/EmailConfig.kt @@ -1,3 +1,6 @@ package io.koraframework.guide.dependencyinjection.email +import io.koraframework.config.common.annotation.ConfigMapper + +@ConfigMapper data class EmailConfig(val topic: String) diff --git a/guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-lib/src/main/kotlin/io/koraframework/guide/dependencyinjection/email/EmailModule.kt b/guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-lib/src/main/kotlin/io/koraframework/guide/dependencyinjection/email/EmailModule.kt index 1a70d85a..c8eeaca6 100644 --- a/guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-lib/src/main/kotlin/io/koraframework/guide/dependencyinjection/email/EmailModule.kt +++ b/guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-lib/src/main/kotlin/io/koraframework/guide/dependencyinjection/email/EmailModule.kt @@ -4,15 +4,15 @@ import java.util.function.Supplier import io.koraframework.common.annotation.DefaultComponent import io.koraframework.common.annotation.Tag import io.koraframework.config.common.Config -import io.koraframework.config.common.extractor.ConfigMapper +import io.koraframework.config.common.mapper.ConfigValueMapper import io.koraframework.guide.dependencyinjection.common.Notifier interface EmailModule { class EmailTag private constructor() - fun config(config: Config, extractor: ConfigMapper): EmailConfig { - return extractor.extract(config["notifier.email"]) + fun config(config: Config, extractor: ConfigValueMapper): EmailConfig { + return extractor.mapOrThrow(config["notifier.email"]) } @Tag(EmailTag::class) diff --git a/guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-submodule/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-submodule/build.gradle.kts index 6f4dfe89..6eeede70 100644 --- a/guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-submodule/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-submodule/build.gradle.kts @@ -1,13 +1,3 @@ -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - api.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - plugins { id("org.jetbrains.kotlin.jvm") id("com.google.devtools.ksp") @@ -15,12 +5,11 @@ plugins { } dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") api(project(":guides:kotlin:kora-kotlin-guide-dependency-injection:kora-kotlin-guide-dependency-injection-common")) implementation("io.koraframework:common") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") diff --git a/guides/kotlin/kora-kotlin-guide-getting-started-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-getting-started-app/build.gradle.kts index b8fa1421..2f516bf7 100644 --- a/guides/kotlin/kora-kotlin-guide-getting-started-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-getting-started-app/build.gradle.kts @@ -7,27 +7,14 @@ plugins { id("application") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") diff --git a/guides/kotlin/kora-kotlin-guide-grpc-client-advanced-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-grpc-client-advanced-app/build.gradle.kts index 4f9b2df0..d95d1361 100644 --- a/guides/kotlin/kora-kotlin-guide-grpc-client-advanced-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-grpc-client-advanced-app/build.gradle.kts @@ -9,33 +9,21 @@ plugins { id("com.google.protobuf") version "0.9.4" } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) compileOnly("javax.annotation:javax.annotation-api:1.3.2") - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:grpc-client") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") - implementation("io.grpc:grpc-protobuf:1.74.0") + implementation("io.grpc:grpc-protobuf:1.83.1") testRuntimeOnly(platform("org.junit:junit-bom:${property("junitVersion")}")) testRuntimeOnly("org.junit.platform:junit-platform-launcher") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) - testImplementation("io.grpc:grpc-inprocess:1.74.0") + testImplementation("io.grpc:grpc-inprocess:1.83.1") testImplementation("org.junit.jupiter:junit-jupiter") } @@ -58,7 +46,7 @@ java { protobuf { protoc { artifact = "com.google.protobuf:protoc:3.25.3" } plugins { - id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.74.0" } + id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.83.1" } } generateProtoTasks { all().forEach { task -> diff --git a/guides/kotlin/kora-kotlin-guide-grpc-client-advanced-app/src/main/kotlin/io/koraframework/guide/grpcclient/advanced/Application.kt b/guides/kotlin/kora-kotlin-guide-grpc-client-advanced-app/src/main/kotlin/io/koraframework/guide/grpcclient/advanced/Application.kt index 98de6114..3e1dcd51 100644 --- a/guides/kotlin/kora-kotlin-guide-grpc-client-advanced-app/src/main/kotlin/io/koraframework/guide/grpcclient/advanced/Application.kt +++ b/guides/kotlin/kora-kotlin-guide-grpc-client-advanced-app/src/main/kotlin/io/koraframework/guide/grpcclient/advanced/Application.kt @@ -1,6 +1,6 @@ package io.koraframework.guide.grpcclient.advanced -import ru.tinkoff.grpc.client.GrpcClientModule +import io.koraframework.grpc.client.GrpcClientModule import io.koraframework.application.graph.KoraApplication import io.koraframework.common.annotation.KoraApp import io.koraframework.config.hocon.HoconConfigModule diff --git a/guides/kotlin/kora-kotlin-guide-grpc-client-advanced-app/src/main/resources/application.conf b/guides/kotlin/kora-kotlin-guide-grpc-client-advanced-app/src/main/resources/application.conf index 552d7388..6446740c 100644 --- a/guides/kotlin/kora-kotlin-guide-grpc-client-advanced-app/src/main/resources/application.conf +++ b/guides/kotlin/kora-kotlin-guide-grpc-client-advanced-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8081 - privateApiHttpPort = 8086 + port = 8081 + system.port = 8086 telemetry.logging.enabled = true } diff --git a/guides/kotlin/kora-kotlin-guide-grpc-client-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-grpc-client-app/build.gradle.kts index 691fbf6b..681fef6f 100644 --- a/guides/kotlin/kora-kotlin-guide-grpc-client-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-grpc-client-app/build.gradle.kts @@ -9,33 +9,21 @@ plugins { id("com.google.protobuf") version "0.9.4" } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) compileOnly("javax.annotation:javax.annotation-api:1.3.2") - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:grpc-client") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") - implementation("io.grpc:grpc-protobuf:1.74.0") + implementation("io.grpc:grpc-protobuf:1.83.1") testRuntimeOnly(platform("org.junit:junit-bom:${property("junitVersion")}")) testRuntimeOnly("org.junit.platform:junit-platform-launcher") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) - testImplementation("io.grpc:grpc-inprocess:1.74.0") + testImplementation("io.grpc:grpc-inprocess:1.83.1") testImplementation("org.junit.jupiter:junit-jupiter") } @@ -58,7 +46,7 @@ java { protobuf { protoc { artifact = "com.google.protobuf:protoc:3.25.3" } plugins { - id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.74.0" } + id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.83.1" } } generateProtoTasks { all().forEach { task -> diff --git a/guides/kotlin/kora-kotlin-guide-grpc-client-app/src/main/kotlin/io/koraframework/guide/grpcclient/Application.kt b/guides/kotlin/kora-kotlin-guide-grpc-client-app/src/main/kotlin/io/koraframework/guide/grpcclient/Application.kt index e34c7aa6..4adb8dc4 100644 --- a/guides/kotlin/kora-kotlin-guide-grpc-client-app/src/main/kotlin/io/koraframework/guide/grpcclient/Application.kt +++ b/guides/kotlin/kora-kotlin-guide-grpc-client-app/src/main/kotlin/io/koraframework/guide/grpcclient/Application.kt @@ -1,6 +1,6 @@ package io.koraframework.guide.grpcclient -import ru.tinkoff.grpc.client.GrpcClientModule +import io.koraframework.grpc.client.GrpcClientModule import io.koraframework.application.graph.KoraApplication import io.koraframework.common.annotation.KoraApp import io.koraframework.config.hocon.HoconConfigModule diff --git a/guides/kotlin/kora-kotlin-guide-grpc-client-app/src/main/resources/application.conf b/guides/kotlin/kora-kotlin-guide-grpc-client-app/src/main/resources/application.conf index 7745c49d..b81818b4 100644 --- a/guides/kotlin/kora-kotlin-guide-grpc-client-app/src/main/resources/application.conf +++ b/guides/kotlin/kora-kotlin-guide-grpc-client-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8081 - privateApiHttpPort = 8086 + port = 8081 + system.port = 8086 telemetry.logging.enabled = true } diff --git a/guides/kotlin/kora-kotlin-guide-grpc-server-advanced-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-grpc-server-advanced-app/build.gradle.kts index cd2f2028..24a671de 100644 --- a/guides/kotlin/kora-kotlin-guide-grpc-server-advanced-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-grpc-server-advanced-app/build.gradle.kts @@ -9,32 +9,19 @@ plugins { id("com.google.protobuf") version "0.9.4" } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) compileOnly("javax.annotation:javax.annotation-api:1.3.2") - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:grpc-server") implementation("io.koraframework:logging-logback") - implementation("io.grpc:grpc-protobuf:1.74.0") - implementation("io.grpc:grpc-services:1.74.0") + implementation("io.grpc:grpc-protobuf:1.83.1") + implementation("io.grpc:grpc-services:1.83.1") testCompileOnly("javax.annotation:javax.annotation-api:1.3.2") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) - testImplementation("io.grpc:grpc-netty:1.74.0") + testImplementation("io.grpc:grpc-netty:1.83.1") testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") } @@ -58,7 +45,7 @@ java { protobuf { protoc { artifact = "com.google.protobuf:protoc:3.25.3" } plugins { - id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.74.0" } + id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.83.1" } } generateProtoTasks { all().forEach { task -> diff --git a/guides/kotlin/kora-kotlin-guide-grpc-server-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-grpc-server-app/build.gradle.kts index e09afbfa..f44d4943 100644 --- a/guides/kotlin/kora-kotlin-guide-grpc-server-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-grpc-server-app/build.gradle.kts @@ -9,32 +9,19 @@ plugins { id("com.google.protobuf") version "0.9.4" } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) compileOnly("javax.annotation:javax.annotation-api:1.3.2") - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:grpc-server") implementation("io.koraframework:logging-logback") - implementation("io.grpc:grpc-protobuf:1.74.0") - implementation("io.grpc:grpc-services:1.74.0") + implementation("io.grpc:grpc-protobuf:1.83.1") + implementation("io.grpc:grpc-services:1.83.1") testCompileOnly("javax.annotation:javax.annotation-api:1.3.2") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) - testImplementation("io.grpc:grpc-netty:1.74.0") + testImplementation("io.grpc:grpc-netty:1.83.1") testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") } @@ -58,7 +45,7 @@ java { protobuf { protoc { artifact = "com.google.protobuf:protoc:3.25.3" } plugins { - id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.74.0" } + id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.83.1" } } generateProtoTasks { all().forEach { task -> diff --git a/guides/kotlin/kora-kotlin-guide-http-client-advanced-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-http-client-advanced-app/build.gradle.kts index fe3a3c54..8daa54b9 100644 --- a/guides/kotlin/kora-kotlin-guide-http-client-advanced-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-http-client-advanced-app/build.gradle.kts @@ -7,29 +7,16 @@ plugins { id("application") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:http-client-common") implementation("io.koraframework:http-client-ok") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") diff --git a/guides/kotlin/kora-kotlin-guide-http-client-advanced-app/src/main/kotlin/io/koraframework/guide/httpclient/client/ApiKeyAuthInterceptor.kt b/guides/kotlin/kora-kotlin-guide-http-client-advanced-app/src/main/kotlin/io/koraframework/guide/httpclient/client/ApiKeyAuthInterceptor.kt index 76ed6340..7f9c1a2b 100644 --- a/guides/kotlin/kora-kotlin-guide-http-client-advanced-app/src/main/kotlin/io/koraframework/guide/httpclient/client/ApiKeyAuthInterceptor.kt +++ b/guides/kotlin/kora-kotlin-guide-http-client-advanced-app/src/main/kotlin/io/koraframework/guide/httpclient/client/ApiKeyAuthInterceptor.kt @@ -1,25 +1,19 @@ package io.koraframework.guide.httpclient.client import io.koraframework.common.annotation.Component -import io.koraframework.common.Context import io.koraframework.http.client.common.interceptor.HttpClientInterceptor import io.koraframework.http.client.common.request.HttpClientRequest import io.koraframework.http.client.common.response.HttpClientResponse -import java.util.concurrent.CompletionStage @Component class ApiKeyAuthInterceptor( private val config: ApiKeyAuthConfig ) : HttpClientInterceptor { - override fun processRequest( - ctx: Context, - chain: HttpClientInterceptor.InterceptChain, - request: HttpClientRequest - ): CompletionStage { + override fun processRequest(chain: HttpClientInterceptor.InterceptChain, request: HttpClientRequest): HttpClientResponse { val authorizedRequest = request.toBuilder() .header("Authorization", config.value()) .build() - return chain.process(ctx, authorizedRequest) + return chain.process(authorizedRequest) } } diff --git a/guides/kotlin/kora-kotlin-guide-http-client-advanced-app/src/main/kotlin/io/koraframework/guide/httpclient/client/DataApiClient.kt b/guides/kotlin/kora-kotlin-guide-http-client-advanced-app/src/main/kotlin/io/koraframework/guide/httpclient/client/DataApiClient.kt index 9f936336..1e94a5d4 100644 --- a/guides/kotlin/kora-kotlin-guide-http-client-advanced-app/src/main/kotlin/io/koraframework/guide/httpclient/client/DataApiClient.kt +++ b/guides/kotlin/kora-kotlin-guide-http-client-advanced-app/src/main/kotlin/io/koraframework/guide/httpclient/client/DataApiClient.kt @@ -1,7 +1,7 @@ package io.koraframework.guide.httpclient.client import org.slf4j.LoggerFactory -import io.koraframework.common.Context +import io.koraframework.common.annotation.Component import io.koraframework.common.annotation.Mapping import io.koraframework.http.client.common.exception.HttpClientDecoderException import io.koraframework.http.client.common.annotation.HttpClient @@ -23,10 +23,9 @@ import io.koraframework.json.common.JsonReader import io.koraframework.json.common.annotation.Json import java.io.IOException import java.nio.charset.StandardCharsets -import java.util.concurrent.CompletionStage @InterceptWith(ApiKeyAuthInterceptor::class) -@HttpClient(configPath = "httpClient.dataApi") +@HttpClient("httpClient.dataApi") interface DataApiClient { @HttpRoute(method = HttpMethod.POST, path = "/data/form") @@ -66,8 +65,9 @@ interface DataApiClient { data class PlainTextGreetingBody(val name: String) + @Component class GreetingRequestMapper : HttpClientRequestMapper { - override fun apply(ctx: Context, value: PlainTextGreetingBody): HttpBodyOutput { + override fun apply(value: PlainTextGreetingBody): HttpBodyOutput { return HttpBody.plaintext("Hello ${value.name}") } } @@ -83,39 +83,38 @@ interface DataApiClient { data class ErrorPayload(val message: String) } + @Component class MappedResponseSuccessMapper( private val jsonReader: JsonReader ) : HttpClientResponseMapper { @Throws(IOException::class, HttpClientDecoderException::class) override fun apply(response: HttpClientResponse): MappedResponse { response.body().asInputStream().use { input -> - return jsonReader.read(input.readAllBytes()) + return requireNotNull(jsonReader.read(input.readAllBytes())) { "Empty success payload" } } } } + @Component class MappedResponseErrorMapper( private val jsonReader: JsonReader ) : HttpClientResponseMapper { @Throws(IOException::class, HttpClientDecoderException::class) override fun apply(response: HttpClientResponse): MappedResponse { response.body().asInputStream().use { input -> - val payload = jsonReader.read(input.readAllBytes()) + val payload = requireNotNull(jsonReader.read(input.readAllBytes())) { "Empty error payload" } return MappedResponse.Error(response.code(), payload.message) } } } + @Component class MethodLoggingInterceptor : HttpClientInterceptor { private val logger = LoggerFactory.getLogger(MethodLoggingInterceptor::class.java) - override fun processRequest( - ctx: Context, - chain: HttpClientInterceptor.InterceptChain, - request: HttpClientRequest - ): CompletionStage { + override fun processRequest(chain: HttpClientInterceptor.InterceptChain, request: HttpClientRequest): HttpClientResponse { logger.info("Advanced HTTP client interceptor invoked") - return chain.process(ctx, request) + return chain.process(request) } } } diff --git a/guides/kotlin/kora-kotlin-guide-http-client-advanced-app/src/main/kotlin/io/koraframework/guide/httpclient/client/ManualDataHttpClient.kt b/guides/kotlin/kora-kotlin-guide-http-client-advanced-app/src/main/kotlin/io/koraframework/guide/httpclient/client/ManualDataHttpClient.kt index 93afa4ab..3869afe2 100644 --- a/guides/kotlin/kora-kotlin-guide-http-client-advanced-app/src/main/kotlin/io/koraframework/guide/httpclient/client/ManualDataHttpClient.kt +++ b/guides/kotlin/kora-kotlin-guide-http-client-advanced-app/src/main/kotlin/io/koraframework/guide/httpclient/client/ManualDataHttpClient.kt @@ -15,10 +15,7 @@ class ManualDataHttpClient( fun pingManualHandler(): String { val request = HttpClientRequest.of("GET", dataApiConfig.url() + "/manual/data/ping") .build() - val response = httpClient.with(apiKeyAuthInterceptor) - .execute(request) - .toCompletableFuture() - .join() + val response = httpClient.with(apiKeyAuthInterceptor).execute(request) if (response.code() != 200) { throw IllegalStateException("Manual HTTP call failed with status ${response.code()}") } diff --git a/guides/kotlin/kora-kotlin-guide-http-client-advanced-app/src/main/resources/application.conf b/guides/kotlin/kora-kotlin-guide-http-client-advanced-app/src/main/resources/application.conf index 672b5feb..4d236df2 100644 --- a/guides/kotlin/kora-kotlin-guide-http-client-advanced-app/src/main/resources/application.conf +++ b/guides/kotlin/kora-kotlin-guide-http-client-advanced-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8081 - privateApiHttpPort = 8086 + port = 8081 + system.port = 8086 telemetry.logging.enabled = true } diff --git a/guides/kotlin/kora-kotlin-guide-http-client-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-http-client-app/build.gradle.kts index 4da53184..35bea099 100644 --- a/guides/kotlin/kora-kotlin-guide-http-client-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-http-client-app/build.gradle.kts @@ -7,29 +7,16 @@ plugins { id("application") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:http-client-common") implementation("io.koraframework:http-client-ok") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") diff --git a/guides/kotlin/kora-kotlin-guide-http-client-app/src/main/kotlin/io/koraframework/guide/httpclient/client/UserApiClient.kt b/guides/kotlin/kora-kotlin-guide-http-client-app/src/main/kotlin/io/koraframework/guide/httpclient/client/UserApiClient.kt index 5deeea9a..b3bff400 100644 --- a/guides/kotlin/kora-kotlin-guide-http-client-app/src/main/kotlin/io/koraframework/guide/httpclient/client/UserApiClient.kt +++ b/guides/kotlin/kora-kotlin-guide-http-client-app/src/main/kotlin/io/koraframework/guide/httpclient/client/UserApiClient.kt @@ -1,14 +1,17 @@ package io.koraframework.guide.httpclient.client +import io.koraframework.common.annotation.Component import io.koraframework.guide.httpclient.dto.UserRequest import io.koraframework.guide.httpclient.dto.UserResponse import io.koraframework.http.client.common.annotation.HttpClient +import io.koraframework.http.client.common.response.HttpClientResponse +import io.koraframework.http.client.common.response.HttpClientResponseMapper import io.koraframework.http.common.HttpMethod import io.koraframework.http.common.HttpResponseEntity import io.koraframework.http.common.annotation.* import io.koraframework.json.common.annotation.Json -@HttpClient(configPath = "httpClient.userApi") +@HttpClient("httpClient.userApi") interface UserApiClient { @HttpRoute(method = HttpMethod.POST, path = "/users") @@ -32,6 +35,23 @@ interface UserApiClient { @Query("sort") sort: String? ): List + /** + * Kora 2.0 ships response mappers for `String` and `ByteArray` only, so a body-less response + * that still needs its status code has to say how `Void` is produced. The framework wraps it + * into `HttpResponseEntity` through its own template factory, which is why the component + * is declared but never referenced with `@Mapping`. + */ + @Component + class VoidResponseMapper : HttpClientResponseMapper { + + override fun apply(response: HttpClientResponse): Void? { + response.body().use { body -> + body.asInputStream().readAllBytes() + } + return null + } + } + @HttpRoute(method = HttpMethod.DELETE, path = "/users/{userId}") fun deleteUser(@Path userId: String): HttpResponseEntity } diff --git a/guides/kotlin/kora-kotlin-guide-http-client-app/src/main/resources/application.conf b/guides/kotlin/kora-kotlin-guide-http-client-app/src/main/resources/application.conf index d880212f..4a496e44 100644 --- a/guides/kotlin/kora-kotlin-guide-http-client-app/src/main/resources/application.conf +++ b/guides/kotlin/kora-kotlin-guide-http-client-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8081 - privateApiHttpPort = 8086 + port = 8081 + system.port = 8086 telemetry.logging.enabled = true } diff --git a/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/build.gradle.kts index 039bb7f5..30866e1c 100644 --- a/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/build.gradle.kts @@ -7,27 +7,14 @@ plugins { id("application") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") diff --git a/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/httpserver/advanced/Application.kt b/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/httpserver/advanced/Application.kt index aece2488..5a32491d 100644 --- a/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/httpserver/advanced/Application.kt +++ b/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/httpserver/advanced/Application.kt @@ -11,18 +11,17 @@ import io.koraframework.http.server.common.request.HttpServerRequestHandlerImpl import io.koraframework.http.server.undertow.UndertowPublicHttpServerModule import io.koraframework.json.common.JsonModule import io.koraframework.logging.logback.LogbackModule -import java.util.concurrent.CompletableFuture @KoraApp interface Application : HoconConfigModule, JsonModule, LogbackModule, UndertowPublicHttpServerModule { fun manualDataPingHandler(authConfig: DataApiAuthConfig): HttpServerRequestHandler { - return HttpServerRequestHandlerImpl.get("/manual/data/ping") { _, request -> + return HttpServerRequestHandlerImpl.get("/manual/data/ping") { request -> val authorization = request.headers().getFirst("authorization") if (authConfig.value() != authorization) { - CompletableFuture.completedFuture(HttpServerResponse.of(403, HttpBody.plaintext("Invalid API key"))) + HttpServerResponse.of(403, HttpBody.plaintext("Invalid API key")) } else { - CompletableFuture.completedFuture(HttpServerResponse.of(200, HttpBody.plaintext("manual-data-pong"))) + HttpServerResponse.of(200, HttpBody.plaintext("manual-data-pong")) } } } @@ -31,4 +30,3 @@ interface Application : HoconConfigModule, JsonModule, LogbackModule, UndertowPu fun main() { KoraApplication.run(ApplicationGraph::graph) } - diff --git a/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/httpserver/advanced/controller/DataApiAuthInterceptor.kt b/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/httpserver/advanced/controller/DataApiAuthInterceptor.kt index d86e6e16..9fd3da93 100644 --- a/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/httpserver/advanced/controller/DataApiAuthInterceptor.kt +++ b/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/httpserver/advanced/controller/DataApiAuthInterceptor.kt @@ -1,26 +1,20 @@ package io.koraframework.guide.httpserver.advanced.controller import io.koraframework.common.annotation.Component -import io.koraframework.common.Context import io.koraframework.http.server.common.interceptor.HttpServerInterceptor import io.koraframework.http.server.common.request.HttpServerRequest import io.koraframework.http.server.common.response.HttpServerResponse -import java.util.concurrent.CompletionStage @Component class DataApiAuthInterceptor( private val config: DataApiAuthConfig ) : HttpServerInterceptor { - override fun intercept( - context: Context, - request: HttpServerRequest, - chain: HttpServerInterceptor.InterceptChain - ): CompletionStage { + override fun intercept(request: HttpServerRequest, chain: HttpServerInterceptor.InterceptChain): HttpServerResponse { val authorization = request.headers().getFirst("authorization") if (config.value() != authorization) { throw SecurityException("Invalid API key") } - return chain.process(context, request) + return chain.process(request) } } diff --git a/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/httpserver/advanced/controller/ExceptionHandler.kt b/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/httpserver/advanced/controller/ExceptionHandler.kt index 63e26868..b0e504d1 100644 --- a/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/httpserver/advanced/controller/ExceptionHandler.kt +++ b/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/httpserver/advanced/controller/ExceptionHandler.kt @@ -1,50 +1,42 @@ package io.koraframework.guide.httpserver.advanced.controller import io.koraframework.common.annotation.Component -import io.koraframework.common.Context import io.koraframework.common.annotation.Tag import io.koraframework.guide.httpserver.advanced.dto.ErrorResponse import io.koraframework.http.common.body.HttpBody -import io.koraframework.http.server.common.* +import io.koraframework.http.server.common.HttpServer +import io.koraframework.http.server.common.interceptor.HttpServerInterceptor +import io.koraframework.http.server.common.request.HttpServerRequest +import io.koraframework.http.server.common.response.HttpServerResponse +import io.koraframework.http.server.common.response.HttpServerResponseException import io.koraframework.json.common.JsonWriter -import java.util.concurrent.CompletionException -import java.util.concurrent.CompletionStage -@Tag(HttpServerModule::class) +@Tag(HttpServer::class) @Component class ExceptionHandler( private val errorJsonWriter: JsonWriter ) : HttpServerInterceptor { - override fun intercept( - context: Context, - request: HttpServerRequest, - chain: HttpServerInterceptor.InterceptChain - ): CompletionStage { - return chain.process(context, request).exceptionally { throwable -> - val cause = unwrap(throwable) - when (cause) { - is RestrictedFormNameException -> jsonResponse(400, cause.message ?: "Restricted form name") - is HttpServerResponseException -> jsonResponse(cause.code(), cause.message ?: "HTTP error") - is IllegalArgumentException -> jsonResponse(400, "Invalid request parameters") - is SecurityException -> jsonResponse(403, cause.message ?: "Access denied") - else -> jsonResponse(500, "An unexpected error occurred") - } + override fun intercept(request: HttpServerRequest, chain: HttpServerInterceptor.InterceptChain): HttpServerResponse { + return try { + chain.process(request) + } catch (e: RestrictedFormNameException) { + jsonResponse(400, e.message ?: "Restricted form name") + } catch (e: HttpServerResponseException) { + jsonResponse(e.code(), e.message ?: "HTTP error") + } catch (e: IllegalArgumentException) { + jsonResponse(400, "Invalid request parameters") + } catch (e: SecurityException) { + jsonResponse(403, e.message ?: "Access denied") + } catch (e: Exception) { + jsonResponse(500, "An unexpected error occurred") } } private fun jsonResponse(statusCode: Int, message: String): HttpServerResponse { return HttpServerResponse.of( statusCode, - HttpBody.json(errorJsonWriter.toByteArrayUnchecked(ErrorResponse(message))) + HttpBody.json(errorJsonWriter.toByteArray(ErrorResponse(message))) ) } - - private fun unwrap(throwable: Throwable): Throwable { - var current = throwable - while (current is CompletionException && current.cause != null) { - current = current.cause!! - } - return current - } } diff --git a/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/httpserver/advanced/controller/LoggingInterceptor.kt b/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/httpserver/advanced/controller/LoggingInterceptor.kt index d8b2389c..9d7d4ed0 100644 --- a/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/httpserver/advanced/controller/LoggingInterceptor.kt +++ b/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/httpserver/advanced/controller/LoggingInterceptor.kt @@ -1,25 +1,24 @@ package io.koraframework.guide.httpserver.advanced.controller import io.koraframework.common.annotation.Component -import io.koraframework.common.Context import io.koraframework.http.server.common.interceptor.HttpServerInterceptor import io.koraframework.http.server.common.request.HttpServerRequest import io.koraframework.http.server.common.response.HttpServerResponse -import java.util.concurrent.CompletionStage @Component class LoggingInterceptor : HttpServerInterceptor { - override fun intercept( - context: Context, - request: HttpServerRequest, - chain: HttpServerInterceptor.InterceptChain - ): CompletionStage { + override fun intercept(request: HttpServerRequest, chain: HttpServerInterceptor.InterceptChain): HttpServerResponse { val started = System.nanoTime() - return chain.process(context, request).whenComplete { response, _ -> + // stays 500 when the chain throws, matching the reactive version that logged 500 on failure + var statusCode = 500 + try { + val response = chain.process(request) + statusCode = response.code() + return response + } finally { val durationMs = (System.nanoTime() - started) / 1_000_000 - val statusCode = response?.code() ?: 500 - println("Request: ${request.method()} ${request.path()} -> $statusCode (${durationMs} ms)") + println("Request: ${request.method()} ${request.path()} -> $statusCode ($durationMs ms)") } } } diff --git a/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/src/main/resources/application.conf b/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/src/main/resources/application.conf index 0b548e86..6a9e6915 100644 --- a/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/src/main/resources/application.conf +++ b/guides/kotlin/kora-kotlin-guide-http-server-advanced-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } diff --git a/guides/kotlin/kora-kotlin-guide-http-server-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-http-server-app/build.gradle.kts index ba694ad8..28a8f50f 100644 --- a/guides/kotlin/kora-kotlin-guide-http-server-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-http-server-app/build.gradle.kts @@ -7,27 +7,14 @@ plugins { id("application") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") diff --git a/guides/kotlin/kora-kotlin-guide-http-server-app/src/main/resources/application.conf b/guides/kotlin/kora-kotlin-guide-http-server-app/src/main/resources/application.conf index 97dae54e..c89518fa 100644 --- a/guides/kotlin/kora-kotlin-guide-http-server-app/src/main/resources/application.conf +++ b/guides/kotlin/kora-kotlin-guide-http-server-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } diff --git a/guides/kotlin/kora-kotlin-guide-json-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-json-app/build.gradle.kts index cda5d56a..e3e92d0b 100644 --- a/guides/kotlin/kora-kotlin-guide-json-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-json-app/build.gradle.kts @@ -7,27 +7,14 @@ plugins { id("application") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") diff --git a/guides/kotlin/kora-kotlin-guide-messaging-kafka-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-messaging-kafka-app/build.gradle.kts index 359f9a71..d2285288 100644 --- a/guides/kotlin/kora-kotlin-guide-messaging-kafka-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-messaging-kafka-app/build.gradle.kts @@ -7,32 +7,19 @@ plugins { id("application") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:kafka") implementation("io.koraframework:logging-logback") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") - testImplementation("io.goodforgod:testcontainers-extensions-kafka:0.13.1") + testImplementation("io.goodforgod:testcontainers-extensions-kafka:0.15.0") } kotlin { diff --git a/guides/kotlin/kora-kotlin-guide-messaging-kafka-app/src/main/resources/application.conf b/guides/kotlin/kora-kotlin-guide-messaging-kafka-app/src/main/resources/application.conf index 6b5c5eb9..1023a7ed 100644 --- a/guides/kotlin/kora-kotlin-guide-messaging-kafka-app/src/main/resources/application.conf +++ b/guides/kotlin/kora-kotlin-guide-messaging-kafka-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } diff --git a/guides/kotlin/kora-kotlin-guide-observability-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-observability-app/build.gradle.kts index 4d4b8940..196d51ed 100644 --- a/guides/kotlin/kora-kotlin-guide-observability-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-observability-app/build.gradle.kts @@ -7,29 +7,16 @@ plugins { id("application") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") implementation("io.koraframework:micrometer-module") implementation("io.koraframework:opentelemetry-tracing-exporter-http") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.json:json:20231013") diff --git a/guides/kotlin/kora-kotlin-guide-observability-app/src/main/resources/application.conf b/guides/kotlin/kora-kotlin-guide-observability-app/src/main/resources/application.conf index b16e0c55..764d9460 100644 --- a/guides/kotlin/kora-kotlin-guide-observability-app/src/main/resources/application.conf +++ b/guides/kotlin/kora-kotlin-guide-observability-app/src/main/resources/application.conf @@ -1,10 +1,12 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 - privateApiHttpMetricsPath = "/metrics" - privateApiHttpLivenessPath = "/system/liveness" - privateApiHttpReadinessPath = "/system/readiness" + port = 8080 + system.port = 8085 + system.metricsPath = "/metrics" + system.livenessPath = "/system/liveness" + system.readinessPath = "/system/readiness" telemetry.logging.enabled = true + // в 2.0 telemetry.metrics.enabled по умолчанию false — без этого /metrics отдаёт только JVM-метрики + telemetry.metrics.enabled = true } tracing { diff --git a/guides/kotlin/kora-kotlin-guide-openapi-http-client-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-openapi-http-client-app/build.gradle.kts index 3e1c4103..a058f7f7 100644 --- a/guides/kotlin/kora-kotlin-guide-openapi-http-client-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-openapi-http-client-app/build.gradle.kts @@ -19,22 +19,10 @@ plugins { id("org.openapi.generator") version "7.24.0" } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:http-client-common") implementation("io.koraframework:http-client-ok") @@ -42,7 +30,6 @@ dependencies { implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") implementation("io.koraframework:validation-module") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") diff --git a/guides/kotlin/kora-kotlin-guide-openapi-http-client-app/src/main/resources/application.conf b/guides/kotlin/kora-kotlin-guide-openapi-http-client-app/src/main/resources/application.conf index bfaced07..8768aceb 100644 --- a/guides/kotlin/kora-kotlin-guide-openapi-http-client-app/src/main/resources/application.conf +++ b/guides/kotlin/kora-kotlin-guide-openapi-http-client-app/src/main/resources/application.conf @@ -1,11 +1,12 @@ httpServer { - publicApiHttpPort = 8081 - privateApiHttpPort = 8086 + port = 8081 + system.port = 8086 telemetry.logging.enabled = true } httpClient { - UsersApi { + // генератор 2.0 приводит первую букву имени API к строчной: @HttpClient("httpClient.usersApi") + usersApi { url = "http://localhost:8080" url = ${?PUBLIC_API_URL} requestTimeout = 10s diff --git a/guides/kotlin/kora-kotlin-guide-openapi-http-server-advanced-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-openapi-http-server-advanced-app/build.gradle.kts index 1dbb66b5..174b3b98 100644 --- a/guides/kotlin/kora-kotlin-guide-openapi-http-server-advanced-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-openapi-http-server-advanced-app/build.gradle.kts @@ -19,29 +19,16 @@ plugins { id("org.openapi.generator") version "7.24.0" } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") implementation("io.koraframework:openapi-management") implementation("io.koraframework:validation-module") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") diff --git a/guides/kotlin/kora-kotlin-guide-openapi-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/openapi/httpserver/advanced/Application.kt b/guides/kotlin/kora-kotlin-guide-openapi-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/openapi/httpserver/advanced/Application.kt index c113f78c..e7d26bed 100644 --- a/guides/kotlin/kora-kotlin-guide-openapi-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/openapi/httpserver/advanced/Application.kt +++ b/guides/kotlin/kora-kotlin-guide-openapi-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/openapi/httpserver/advanced/Application.kt @@ -13,13 +13,13 @@ import io.koraframework.http.common.body.HttpBody import io.koraframework.http.server.common.response.HttpServerResponse import io.koraframework.http.server.common.auth.HttpServerPrincipalExtractor import io.koraframework.http.server.undertow.UndertowPublicHttpServerModule +import io.koraframework.json.common.JsonNullable import io.koraframework.json.common.JsonWriter import io.koraframework.json.common.JsonModule import io.koraframework.logging.logback.LogbackModule import io.koraframework.openapi.management.OpenApiManagementModule import io.koraframework.validation.module.ValidationModule import io.koraframework.validation.module.http.server.ViolationExceptionHttpServerResponseMapper -import java.util.concurrent.CompletableFuture @KoraApp interface Application : @@ -37,21 +37,21 @@ interface Application : val details = exception.violations.map { violation -> "Path ${violation.path()} violated: ${violation.message()}" } - val response = ErrorResponseTO("Encountered '${details.size}' validation violations", details) - HttpServerResponse.of(400, HttpBody.json(errorResponseJsonWriter.toByteArrayUnchecked(response))) + // an optional array of the specification becomes JsonNullable in 2.0 + val response = ErrorResponseTO("Encountered '${details.size}' validation violations", JsonNullable.of(details)) + HttpServerResponse.of(400, HttpBody.json(errorResponseJsonWriter.toByteArray(response))) } } @Tag(ApiSecurity.ApiKeyAuth::class) - fun apiKeyHttpServerPrincipalExtractor(config: DataApiAuthConfig): HttpServerPrincipalExtractor { + fun apiKeyHttpServerPrincipalExtractor(config: DataApiAuthConfig): HttpServerPrincipalExtractor { return HttpServerPrincipalExtractor { _, value -> if (value == null || config.value() != value) { throw SecurityException("Invalid API key") } - CompletableFuture.completedFuture(DataApiPrincipal("data-api-client")) + DataApiPrincipal("data-api-client") } } - } fun main() { diff --git a/guides/kotlin/kora-kotlin-guide-openapi-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/openapi/httpserver/advanced/controller/DataApiExceptionHandler.kt b/guides/kotlin/kora-kotlin-guide-openapi-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/openapi/httpserver/advanced/controller/DataApiExceptionHandler.kt index 248aa1b6..633342b3 100644 --- a/guides/kotlin/kora-kotlin-guide-openapi-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/openapi/httpserver/advanced/controller/DataApiExceptionHandler.kt +++ b/guides/kotlin/kora-kotlin-guide-openapi-http-server-advanced-app/src/main/kotlin/io/koraframework/guide/openapi/httpserver/advanced/controller/DataApiExceptionHandler.kt @@ -1,7 +1,6 @@ package io.koraframework.guide.openapi.httpserver.advanced.controller import io.koraframework.common.annotation.Component -import io.koraframework.common.Context import io.koraframework.guide.openapi.httpserver.data.model.ErrorResponseTO import io.koraframework.http.common.body.HttpBody import io.koraframework.http.server.common.interceptor.HttpServerInterceptor @@ -10,43 +9,33 @@ import io.koraframework.http.server.common.response.HttpServerResponse import io.koraframework.http.server.common.response.HttpServerResponseException import io.koraframework.json.common.JsonWriter import io.koraframework.validation.common.ViolationException -import java.util.concurrent.CompletionException -import java.util.concurrent.CompletionStage @Component class DataApiExceptionHandler( private val errorJsonWriter: JsonWriter ) : HttpServerInterceptor { - override fun intercept( - context: Context, - request: HttpServerRequest, - chain: HttpServerInterceptor.InterceptChain - ): CompletionStage { - return chain.process(context, request).exceptionally { throwable -> - val cause = unwrap(throwable) - when (cause) { - is ViolationException -> throw CompletionException(cause) - is HttpServerResponseException -> jsonResponse(cause.code(), cause.message ?: "HTTP error") - is IllegalArgumentException -> jsonResponse(400, "Invalid request parameters") - is SecurityException -> jsonResponse(403, cause.message ?: "Access denied") - else -> jsonResponse(500, "An unexpected error occurred") - } + override fun intercept(request: HttpServerRequest, chain: HttpServerInterceptor.InterceptChain): HttpServerResponse { + try { + return chain.process(request) + } catch (e: ViolationException) { + // left to ViolationExceptionHttpServerResponseMapper, which renders the violations + throw e + } catch (e: HttpServerResponseException) { + return jsonResponse(e.code(), e.message ?: "HTTP error") + } catch (e: IllegalArgumentException) { + return jsonResponse(400, "Invalid request parameters") + } catch (e: SecurityException) { + return jsonResponse(403, e.message ?: "Access denied") + } catch (e: Exception) { + return jsonResponse(500, "An unexpected error occurred") } } private fun jsonResponse(statusCode: Int, message: String): HttpServerResponse { return HttpServerResponse.of( statusCode, - HttpBody.json(errorJsonWriter.toByteArrayUnchecked(ErrorResponseTO(message, null))) + HttpBody.json(errorJsonWriter.toByteArray(ErrorResponseTO(message))) ) } - - private fun unwrap(throwable: Throwable): Throwable { - var current = throwable - while (current is CompletionException && current.cause != null) { - current = current.cause!! - } - return current - } } diff --git a/guides/kotlin/kora-kotlin-guide-openapi-http-server-advanced-app/src/main/resources/application.conf b/guides/kotlin/kora-kotlin-guide-openapi-http-server-advanced-app/src/main/resources/application.conf index 081a1387..67ff0794 100644 --- a/guides/kotlin/kora-kotlin-guide-openapi-http-server-advanced-app/src/main/resources/application.conf +++ b/guides/kotlin/kora-kotlin-guide-openapi-http-server-advanced-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } @@ -13,7 +13,7 @@ auth { openapi { management { - file = [ "openapi/user-http-server.yaml", "openapi/data-http-server.yaml" ] + files = [ "openapi/user-http-server.yaml", "openapi/data-http-server.yaml" ] enabled = true endpoint = "/openapi" swaggerui { diff --git a/guides/kotlin/kora-kotlin-guide-openapi-http-server-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-openapi-http-server-app/build.gradle.kts index 2cea6493..c024cb87 100644 --- a/guides/kotlin/kora-kotlin-guide-openapi-http-server-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-openapi-http-server-app/build.gradle.kts @@ -19,29 +19,16 @@ plugins { id("org.openapi.generator") version "7.24.0" } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") implementation("io.koraframework:openapi-management") implementation("io.koraframework:validation-module") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") diff --git a/guides/kotlin/kora-kotlin-guide-openapi-http-server-app/src/main/resources/application.conf b/guides/kotlin/kora-kotlin-guide-openapi-http-server-app/src/main/resources/application.conf index a18938c6..78348828 100644 --- a/guides/kotlin/kora-kotlin-guide-openapi-http-server-app/src/main/resources/application.conf +++ b/guides/kotlin/kora-kotlin-guide-openapi-http-server-app/src/main/resources/application.conf @@ -1,12 +1,12 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } openapi { management { - file = [ "openapi/user-http-server.yaml" ] + files = [ "openapi/user-http-server.yaml" ] enabled = true endpoint = "/openapi" swaggerui { diff --git a/guides/kotlin/kora-kotlin-guide-resilient-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-resilient-app/build.gradle.kts index 4e18318e..f5c78f7f 100644 --- a/guides/kotlin/kora-kotlin-guide-resilient-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-resilient-app/build.gradle.kts @@ -7,28 +7,15 @@ plugins { id("application") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") implementation("io.koraframework:resilient-kora") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") diff --git a/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/kotlin/io/koraframework/guide/resilient/service/CircuitBreakerFailurePredicate.kt b/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/kotlin/io/koraframework/guide/resilient/service/CircuitBreakerFailurePredicate.kt index 8e336184..f09bd0ff 100644 --- a/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/kotlin/io/koraframework/guide/resilient/service/CircuitBreakerFailurePredicate.kt +++ b/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/kotlin/io/koraframework/guide/resilient/service/CircuitBreakerFailurePredicate.kt @@ -1,14 +1,19 @@ package io.koraframework.guide.resilient.service import io.koraframework.common.annotation.Component +import io.koraframework.common.annotation.Tag import io.koraframework.http.server.common.response.HttpServerResponseException import io.koraframework.resilient.circuitbreaker.CircuitBreakerPredicate +// 2.0 selects the predicate by the circuit breaker spec tag instead of by a name() the aspect looked up +@Tag(DefaultCircuitBreaker::class) @Component class CircuitBreakerFailurePredicate : CircuitBreakerPredicate { - override fun name(): String = "RecordServerErrorsOnly" - override fun test(throwable: Throwable): Boolean { - return throwable !is HttpServerResponseException || throwable.code() >= 500 + override fun isCircuitBreakerFailure(throwable: Throwable): Boolean { + if (throwable is HttpServerResponseException) { + return throwable.code() >= 500 + } + return true } } diff --git a/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/kotlin/io/koraframework/guide/resilient/service/DefaultCircuitBreaker.kt b/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/kotlin/io/koraframework/guide/resilient/service/DefaultCircuitBreaker.kt new file mode 100644 index 00000000..24ed842b --- /dev/null +++ b/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/kotlin/io/koraframework/guide/resilient/service/DefaultCircuitBreaker.kt @@ -0,0 +1,7 @@ +package io.koraframework.guide.resilient.service + +import io.koraframework.resilient.circuitbreaker.CircuitBreaker +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakerSpec + +@CircuitBreakerSpec("resilient.circuitbreaker.default") +interface DefaultCircuitBreaker : CircuitBreaker diff --git a/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/kotlin/io/koraframework/guide/resilient/service/DefaultRetry.kt b/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/kotlin/io/koraframework/guide/resilient/service/DefaultRetry.kt new file mode 100644 index 00000000..7694767b --- /dev/null +++ b/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/kotlin/io/koraframework/guide/resilient/service/DefaultRetry.kt @@ -0,0 +1,7 @@ +package io.koraframework.guide.resilient.service + +import io.koraframework.resilient.retry.Retry +import io.koraframework.resilient.retry.annotation.RetrySpec + +@RetrySpec("resilient.retry.default") +interface DefaultRetry : Retry diff --git a/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/kotlin/io/koraframework/guide/resilient/service/DefaultTimeouter.kt b/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/kotlin/io/koraframework/guide/resilient/service/DefaultTimeouter.kt new file mode 100644 index 00000000..674429af --- /dev/null +++ b/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/kotlin/io/koraframework/guide/resilient/service/DefaultTimeouter.kt @@ -0,0 +1,7 @@ +package io.koraframework.guide.resilient.service + +import io.koraframework.resilient.timeout.Timeouter +import io.koraframework.resilient.timeout.annotation.TimeoutSpec + +@TimeoutSpec("resilient.timeout.default") +interface DefaultTimeouter : Timeouter diff --git a/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/kotlin/io/koraframework/guide/resilient/service/UserService.kt b/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/kotlin/io/koraframework/guide/resilient/service/UserService.kt index f3a95820..c931dc9e 100644 --- a/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/kotlin/io/koraframework/guide/resilient/service/UserService.kt +++ b/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/kotlin/io/koraframework/guide/resilient/service/UserService.kt @@ -5,34 +5,34 @@ import io.koraframework.guide.resilient.dto.UserRequest import io.koraframework.guide.resilient.dto.UserResponse import io.koraframework.guide.resilient.repository.UserRepository import io.koraframework.http.server.common.response.HttpServerResponseException -import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreaker +import io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakable import io.koraframework.resilient.fallback.annotation.Fallback -import io.koraframework.resilient.retry.annotation.Retry +import io.koraframework.resilient.retry.annotation.Retryable import io.koraframework.resilient.timeout.annotation.Timeout import java.time.LocalDateTime @Component open class UserService(private val userRepository: UserRepository) { - @Fallback(value = "default", method = "createUserFallback(request)") + @Fallback(method = "createUserFallback(request)") open fun createUser(request: UserRequest): UserResponse { val generatedId = userRepository.save(request.name, request.email) return UserResponse(generatedId, request.name, request.email, LocalDateTime.now()) } - @Retry("default") + @Retryable(DefaultRetry::class) open fun getUser(id: String): UserResponse? = userRepository.findById(id) - @CircuitBreaker("default") - @Retry("default") - @Timeout("default") + @CircuitBreakable(DefaultCircuitBreaker::class) + @Retryable(DefaultRetry::class) + @Timeout(DefaultTimeouter::class) open fun getUsers(page: Int, size: Int, sort: String): List = userRepository.findAll() .sortedWith(getComparator(sort)) .drop(page * size) .take(size) - @CircuitBreaker("default") + @CircuitBreakable(DefaultCircuitBreaker::class) open fun updateUser(id: String, request: UserRequest): UserResponse { if (!userRepository.update(id, request.name, request.email)) { throw HttpServerResponseException.of(404, "User not found") @@ -40,7 +40,7 @@ open class UserService(private val userRepository: UserRepository) { return UserResponse(id, request.name, request.email, LocalDateTime.now()) } - @Timeout("default") + @Timeout(DefaultTimeouter::class) open fun deleteUser(id: String) { if (!userRepository.deleteById(id)) { throw HttpServerResponseException.of(404, "User not found") diff --git a/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/resources/application.conf b/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/resources/application.conf index 4441014e..f470ec9b 100644 --- a/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/resources/application.conf +++ b/guides/kotlin/kora-kotlin-guide-resilient-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } @@ -19,12 +19,12 @@ resilient { } circuitbreaker { default { - slidingWindowSize = 2 + type = FIXED_WINDOW + countBased.windowSize = 2 minimumRequiredCalls = 2 failureRateThreshold = 100 permittedCallsInHalfOpenState = 1 waitDurationInOpenState = 200ms - failurePredicateName = "RecordServerErrorsOnly" } } } diff --git a/guides/kotlin/kora-kotlin-guide-s3-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-s3-app/build.gradle.kts index 2c5bf8ec..12a2202c 100644 --- a/guides/kotlin/kora-kotlin-guide-s3-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-s3-app/build.gradle.kts @@ -7,33 +7,21 @@ plugins { id("application") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:http-client-ok") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") implementation("io.koraframework:s3-client-aws") - kspTest("io.koraframework:symbol-processors") + implementation("io.koraframework.experimental:s3-client-kora") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.testcontainers:junit-jupiter:1.21.4") - testImplementation("io.goodforgod:testcontainers-extensions-minio:0.13.1") + testImplementation("io.goodforgod:testcontainers-extensions-minio:0.15.0") testImplementation("io.koraframework:test-junit5") } diff --git a/guides/kotlin/kora-kotlin-guide-s3-app/src/main/kotlin/io/koraframework/guide/s3/Application.kt b/guides/kotlin/kora-kotlin-guide-s3-app/src/main/kotlin/io/koraframework/guide/s3/Application.kt index dfed051f..1a6ddada 100644 --- a/guides/kotlin/kora-kotlin-guide-s3-app/src/main/kotlin/io/koraframework/guide/s3/Application.kt +++ b/guides/kotlin/kora-kotlin-guide-s3-app/src/main/kotlin/io/koraframework/guide/s3/Application.kt @@ -8,6 +8,7 @@ import io.koraframework.http.server.undertow.UndertowPublicHttpServerModule import io.koraframework.json.common.JsonModule import io.koraframework.logging.logback.LogbackModule import io.koraframework.s3.client.aws.AwsS3ClientModule +import io.koraframework.s3.client.kora.KoraS3ClientModule @KoraApp interface Application : @@ -16,6 +17,7 @@ interface Application : LogbackModule, OkHttpClientModule, AwsS3ClientModule, + KoraS3ClientModule, UndertowPublicHttpServerModule fun main() { diff --git a/guides/kotlin/kora-kotlin-guide-s3-app/src/main/kotlin/io/koraframework/guide/s3/controller/DataController.kt b/guides/kotlin/kora-kotlin-guide-s3-app/src/main/kotlin/io/koraframework/guide/s3/controller/DataController.kt index 7b04f3e5..01d72072 100644 --- a/guides/kotlin/kora-kotlin-guide-s3-app/src/main/kotlin/io/koraframework/guide/s3/controller/DataController.kt +++ b/guides/kotlin/kora-kotlin-guide-s3-app/src/main/kotlin/io/koraframework/guide/s3/controller/DataController.kt @@ -1,7 +1,6 @@ package io.koraframework.guide.s3.controller import io.koraframework.common.annotation.Component -import io.koraframework.common.util.ByteBufferInputStream import io.koraframework.guide.s3.s3.FileMetadata import io.koraframework.guide.s3.s3.S3FileClient import io.koraframework.http.common.HttpMethod @@ -9,14 +8,14 @@ import io.koraframework.http.common.annotation.HttpRoute import io.koraframework.http.common.body.HttpBody import io.koraframework.http.common.form.FormMultipart import io.koraframework.http.common.header.HttpHeaders +import io.koraframework.http.server.common.annotation.HttpController import io.koraframework.http.server.common.response.HttpServerResponse import io.koraframework.http.server.common.response.HttpServerResponseException -import io.koraframework.http.server.common.annotation.HttpController import io.koraframework.json.common.annotation.Json -import io.koraframework.s3.client.S3NotFoundException -import io.koraframework.s3.client.model.S3Body -import java.io.ByteArrayInputStream -import java.io.InputStream +import io.koraframework.s3.client.kora.exception.S3ClientNoSuchKeyException +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.util.UUID @Component @HttpController @@ -32,50 +31,48 @@ class DataController( ?: throw IllegalArgumentException("No file part named 'file' provided") return when (filePart) { - is FormMultipart.FormPart.MultipartFile -> { - uploadStream(filePart.fileName(), filePart.contentType(), ByteArrayInputStream(filePart.content())) - } + is FormMultipart.FormPart.MultipartFile -> upload(filePart.contentType(), filePart.content()) + // a streamed part carries an HttpBodyOutput, which knows how to write itself out is FormMultipart.FormPart.MultipartFileStream -> { - uploadStream( - filePart.fileName(), - filePart.contentType(), - ByteBufferPublisherInputStream(filePart.content()) - ) + val buffer = ByteArrayOutputStream() + try { + filePart.content().write(buffer) + } catch (e: IOException) { + throw HttpServerResponseException.of(400, "Failed to read uploaded file") + } + upload(filePart.content().contentType(), buffer.toByteArray()) } else -> throw IllegalArgumentException("Part 'file' must be a multipart file") } } - private fun uploadStream(fileName: String?, contentType: String?, inputStream: InputStream): FileMetadata { - val actualContentType = if (contentType.isNullOrBlank()) "application/octet-stream" else contentType - val fileId = java.util.UUID.randomUUID().toString() - val body = S3Body.ofInputStreamReadAll(inputStream, actualContentType) - s3FileClient.uploadFile(fileId, body) - return FileMetadata(fileId, body.size(), actualContentType) - } - @HttpRoute(method = HttpMethod.GET, path = "/files") @Json fun listFiles(): List { - return s3FileClient.listFiles().objects() - .map { toMetadata(it.key(), it.size(), it.body().type()) } + return s3FileClient.listFiles().items() + .map { toMetadata(it.key(), it.size(), null) } } @HttpRoute(method = HttpMethod.GET, path = "/files/{fileId}") fun downloadFile(fileId: String): HttpServerResponse { try { - val obj = s3FileClient.downloadFile(fileId) - val bytes = obj.body().asBytes() - val contentType = obj.body().type() ?: "application/octet-stream" - return HttpServerResponse.of( - 200, - HttpHeaders.of("Content-Disposition", "attachment; filename=\"$fileId\""), - HttpBody.of(contentType, bytes) - ) - } catch (e: S3NotFoundException) { + s3FileClient.downloadFile(fileId).use { obj -> + obj.body().asInputStream().use { body -> + val bytes = body.readAllBytes() + val contentType = obj.headers().getFirst("Content-Type") ?: "application/octet-stream" + return HttpServerResponse.of( + 200, + HttpHeaders.of("Content-Disposition", "attachment; filename=\"$fileId\""), + HttpBody.of(contentType, bytes) + ) + } + } + } catch (e: S3ClientNoSuchKeyException) { throw HttpServerResponseException.of(404, "File not found") + } catch (e: IOException) { + throw HttpServerResponseException.of(500, "Failed to read file") } } @@ -86,8 +83,15 @@ class DataController( return DeleteFileResponse("File deleted successfully") } + private fun upload(contentType: String?, body: ByteArray): FileMetadata { + val actualContentType = if (contentType.isNullOrBlank()) "application/octet-stream" else contentType + val fileId = UUID.randomUUID().toString() + s3FileClient.uploadFile(fileId, body) + return FileMetadata(fileId, body.size.toLong(), actualContentType) + } + private fun toMetadata(key: String, size: Long?, contentType: String?): FileMetadata { - val normalized = if (key.startsWith("files/")) key.removePrefix("files/") else key + val normalized = key.removePrefix("files/") return FileMetadata(normalized, size, contentType) } diff --git a/guides/kotlin/kora-kotlin-guide-s3-app/src/main/kotlin/io/koraframework/guide/s3/s3/S3BucketInitializer.kt b/guides/kotlin/kora-kotlin-guide-s3-app/src/main/kotlin/io/koraframework/guide/s3/s3/S3BucketInitializer.kt new file mode 100644 index 00000000..061af767 --- /dev/null +++ b/guides/kotlin/kora-kotlin-guide-s3-app/src/main/kotlin/io/koraframework/guide/s3/s3/S3BucketInitializer.kt @@ -0,0 +1,32 @@ +package io.koraframework.guide.s3.s3 + +import io.koraframework.application.graph.Lifecycle +import io.koraframework.common.annotation.Component +import io.koraframework.common.annotation.Root +import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.services.s3.model.CreateBucketRequest +import software.amazon.awssdk.services.s3.model.HeadBucketRequest +import software.amazon.awssdk.services.s3.model.NoSuchBucketException + +/** + * Bucket administration is not part of the declarative `@S3` contract, so it goes through the + * AWS SDK client that `s3-client-aws` publishes. + */ +@Root +@Component +class S3BucketInitializer( + private val s3Client: S3Client, + private val config: S3UploadsConfig +) : Lifecycle { + + override fun init() { + val bucket = config.bucket() + try { + s3Client.headBucket(HeadBucketRequest.builder().bucket(bucket).build()) + } catch (e: NoSuchBucketException) { + s3Client.createBucket(CreateBucketRequest.builder().bucket(bucket).build()) + } + } + + override fun release() {} +} diff --git a/guides/kotlin/kora-kotlin-guide-s3-app/src/main/kotlin/io/koraframework/guide/s3/s3/S3FileClient.kt b/guides/kotlin/kora-kotlin-guide-s3-app/src/main/kotlin/io/koraframework/guide/s3/s3/S3FileClient.kt index 99178ea6..48c24fea 100644 --- a/guides/kotlin/kora-kotlin-guide-s3-app/src/main/kotlin/io/koraframework/guide/s3/s3/S3FileClient.kt +++ b/guides/kotlin/kora-kotlin-guide-s3-app/src/main/kotlin/io/koraframework/guide/s3/s3/S3FileClient.kt @@ -1,22 +1,21 @@ package io.koraframework.guide.s3.s3 -import io.koraframework.s3.client.annotation.S3 -import io.koraframework.s3.client.model.S3Body -import io.koraframework.s3.client.model.S3Object -import io.koraframework.s3.client.model.S3ObjectList -import io.koraframework.s3.client.model.S3ObjectUpload +import io.koraframework.s3.client.kora.annotation.S3 +import io.koraframework.s3.client.kora.model.response.GetObjectResult +import io.koraframework.s3.client.kora.model.response.ListBucketResult @S3.Client("s3client.uploads") +@S3.Bucket(".bucket") interface S3FileClient { @S3.Put("files/{fileId}") - fun uploadFile(fileId: String, body: S3Body): S3ObjectUpload + fun uploadFile(fileId: String, body: ByteArray): String @S3.Get("files/{fileId}") - fun downloadFile(fileId: String): S3Object + fun downloadFile(fileId: String): GetObjectResult @S3.List("files/") - fun listFiles(): S3ObjectList + fun listFiles(): ListBucketResult @S3.Delete("files/{fileId}") fun deleteFile(fileId: String) diff --git a/guides/kotlin/kora-kotlin-guide-s3-app/src/main/kotlin/io/koraframework/guide/s3/s3/S3UploadsConfig.kt b/guides/kotlin/kora-kotlin-guide-s3-app/src/main/kotlin/io/koraframework/guide/s3/s3/S3UploadsConfig.kt new file mode 100644 index 00000000..79c43f1a --- /dev/null +++ b/guides/kotlin/kora-kotlin-guide-s3-app/src/main/kotlin/io/koraframework/guide/s3/s3/S3UploadsConfig.kt @@ -0,0 +1,12 @@ +package io.koraframework.guide.s3.s3 + +import io.koraframework.config.common.annotation.ConfigSource + +/** + * The declarative client reads its bucket through `@S3.Bucket`, which produces a generated class + * rather than an injectable config, so bucket administration reads the same path itself. + */ +@ConfigSource("s3client.uploads") +interface S3UploadsConfig { + fun bucket(): String +} diff --git a/guides/kotlin/kora-kotlin-guide-s3-app/src/main/resources/application.conf b/guides/kotlin/kora-kotlin-guide-s3-app/src/main/resources/application.conf index 07207b84..e7c0ffbb 100644 --- a/guides/kotlin/kora-kotlin-guide-s3-app/src/main/resources/application.conf +++ b/guides/kotlin/kora-kotlin-guide-s3-app/src/main/resources/application.conf @@ -1,19 +1,33 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } -s3client { +# s3-client-aws: the AWS SDK client, used here only for bucket administration +s3client.aws { url = ${S3_URL} - accessKey = ${S3_ACCESS_KEY} - secretKey = ${S3_SECRET_KEY} region = "us-east-1" region = ${?S3_REGION} - uploads { - bucket = "uploads" - bucket = ${?S3_BUCKET} + credentials { + accessKey = ${S3_ACCESS_KEY} + secretKey = ${S3_SECRET_KEY} + } +} + +# s3-client-kora: the declarative @S3.Client, path taken from its @S3.Client annotation +s3client.uploads { + endpoint = ${S3_URL} + region = "us-east-1" + region = ${?S3_REGION} + + bucket = "uploads" + bucket = ${?S3_BUCKET} + + credentials { + accessKey = ${S3_ACCESS_KEY} + secretKey = ${S3_SECRET_KEY} } } @@ -22,4 +36,4 @@ logging { "ROOT": "WARN" "io.koraframework": "INFO" } -} \ No newline at end of file +} diff --git a/guides/kotlin/kora-kotlin-guide-s3-app/src/test/kotlin/io/koraframework/guide/s3/S3AppTest.kt b/guides/kotlin/kora-kotlin-guide-s3-app/src/test/kotlin/io/koraframework/guide/s3/S3AppTest.kt index 1c7f2736..b66e2ef5 100644 --- a/guides/kotlin/kora-kotlin-guide-s3-app/src/test/kotlin/io/koraframework/guide/s3/S3AppTest.kt +++ b/guides/kotlin/kora-kotlin-guide-s3-app/src/test/kotlin/io/koraframework/guide/s3/S3AppTest.kt @@ -5,19 +5,20 @@ import io.goodforgod.testcontainers.extensions.minio.Bucket import io.goodforgod.testcontainers.extensions.minio.ConnectionMinio import io.goodforgod.testcontainers.extensions.minio.MinioConnection import io.goodforgod.testcontainers.extensions.minio.TestcontainersMinio -import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test import io.koraframework.guide.s3.controller.DataController import io.koraframework.guide.s3.s3.S3FileClient import io.koraframework.http.common.form.FormMultipart -import io.koraframework.s3.client.S3NotFoundException -import io.koraframework.s3.client.model.S3Body +import io.koraframework.s3.client.kora.exception.S3ClientNoSuchKeyException import io.koraframework.test.extension.junit5.KoraAppTest import io.koraframework.test.extension.junit5.KoraAppTestConfigModifier import io.koraframework.test.extension.junit5.KoraConfigModification import io.koraframework.test.extension.junit5.TestComponent import software.amazon.awssdk.services.s3.S3Client -import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.nio.charset.StandardCharsets @@ -50,16 +51,16 @@ class S3AppTest : KoraAppTestConfigModifier { val content = "guide-client-body".toByteArray(StandardCharsets.UTF_8) val fileId = "file-1" - s3FileClient.uploadFile( - fileId, - S3Body.ofInputStream(ByteArrayInputStream(content), content.size.toLong(), "text/plain") - ) + s3FileClient.uploadFile(fileId, content) - val downloaded = s3FileClient.downloadFile(fileId) - assertArrayEquals(content, downloaded.body().asBytes()) + s3FileClient.downloadFile(fileId).use { downloaded -> + downloaded.body().asInputStream().use { body -> + assertArrayEquals(content, body.readAllBytes()) + } + } s3FileClient.deleteFile(fileId) - assertThrows(S3NotFoundException::class.java) { s3FileClient.downloadFile(fileId) } + assertThrows(S3ClientNoSuchKeyException::class.java) { s3FileClient.downloadFile(fileId) } } @Test @@ -74,7 +75,7 @@ class S3AppTest : KoraAppTestConfigModifier { val downloaded = dataController.downloadFile(uploaded.fileId) assertEquals(200, downloaded.code()) val output = ByteArrayOutputStream() - downloaded.body().write(output) + downloaded.body()!!.write(output) assertArrayEquals(content, output.toByteArray()) val deleteResponse = dataController.deleteFile(uploaded.fileId) diff --git a/guides/kotlin/kora-kotlin-guide-testing-black-box-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-testing-black-box-app/build.gradle.kts index d03121c8..5a947a3a 100644 --- a/guides/kotlin/kora-kotlin-guide-testing-black-box-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-testing-black-box-app/build.gradle.kts @@ -5,18 +5,8 @@ plugins { id("org.jetbrains.kotlin.jvm") } -val koraBom: Configuration by configurations.creating -configurations { - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") diff --git a/guides/kotlin/kora-kotlin-guide-testing-integration-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-testing-integration-app/build.gradle.kts index fdc6e8b8..6efa372a 100644 --- a/guides/kotlin/kora-kotlin-guide-testing-integration-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-testing-integration-app/build.gradle.kts @@ -6,29 +6,20 @@ plugins { id("com.google.devtools.ksp") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") + kspTest("io.koraframework:symbol-processors:${property("koraVersion")}") testRuntimeOnly("org.postgresql:postgresql:42.7.3") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation(project(":guides:kotlin:kora-kotlin-guide-database-jdbc-app")) testImplementation("io.koraframework:config-hocon") testImplementation("io.koraframework:database-flyway") + // flyway-core 13 не содержит поддержки конкретных СУБД, иначе Flyway падает + // с "Unsupported Database: PostgreSQL" + testImplementation("org.flywaydb:flyway-database-postgresql:13.1.0") testImplementation("io.koraframework:database-jdbc") testImplementation("io.koraframework:http-client-common") testImplementation("io.koraframework:http-server-undertow") diff --git a/guides/kotlin/kora-kotlin-guide-testing-integration-app/src/test/kotlin/io/koraframework/guide/testingintegration/UserServiceIntegrationPostgresTest.kt b/guides/kotlin/kora-kotlin-guide-testing-integration-app/src/test/kotlin/io/koraframework/guide/testingintegration/UserServiceIntegrationPostgresTest.kt index a8593628..2b619699 100644 --- a/guides/kotlin/kora-kotlin-guide-testing-integration-app/src/test/kotlin/io/koraframework/guide/testingintegration/UserServiceIntegrationPostgresTest.kt +++ b/guides/kotlin/kora-kotlin-guide-testing-integration-app/src/test/kotlin/io/koraframework/guide/testingintegration/UserServiceIntegrationPostgresTest.kt @@ -39,7 +39,7 @@ class UserServiceIntegrationPostgresTest : KoraAppTestConfigModifier { override fun config(): KoraConfigModification { return KoraConfigModification.ofString( """ - db { + jdbc { jdbcUrl = ${'$'}{POSTGRES_JDBC_URL} username = ${'$'}{POSTGRES_USER} password = ${'$'}{POSTGRES_PASS} diff --git a/guides/kotlin/kora-kotlin-guide-testing-junit-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-testing-junit-app/build.gradle.kts index 9028aa9d..5096a4fe 100644 --- a/guides/kotlin/kora-kotlin-guide-testing-junit-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-testing-junit-app/build.gradle.kts @@ -6,23 +6,10 @@ plugins { id("com.google.devtools.ksp") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") - kspTest("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation(project(":guides:kotlin:kora-kotlin-guide-http-server-app")) @@ -31,7 +18,7 @@ dependencies { testImplementation("io.koraframework:json-common") testImplementation("io.koraframework:logging-logback") testImplementation("io.koraframework:test-junit5") - testImplementation("org.mockito:mockito-core:5.12.0") + testImplementation("org.mockito:mockito-core:5.23.0") } kotlin { diff --git a/guides/kotlin/kora-kotlin-guide-validation-app/build.gradle.kts b/guides/kotlin/kora-kotlin-guide-validation-app/build.gradle.kts index b088a220..2632c451 100644 --- a/guides/kotlin/kora-kotlin-guide-validation-app/build.gradle.kts +++ b/guides/kotlin/kora-kotlin-guide-validation-app/build.gradle.kts @@ -7,28 +7,15 @@ plugins { id("application") } -val koraBom: Configuration by configurations.creating -configurations { - ksp.get().extendsFrom(koraBom) - compileOnly.get().extendsFrom(koraBom) - runtimeOnly.get().extendsFrom(koraBom) - implementation.get().extendsFrom(koraBom) - testCompileOnly.get().extendsFrom(koraBom) - kspTest.get().extendsFrom(koraBom) - testRuntimeOnly.get().extendsFrom(koraBom) - testImplementation.get().extendsFrom(koraBom) -} - dependencies { - koraBom(platform("io.koraframework:kora-parent:${property("koraVersion")}")) + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) - ksp("io.koraframework:symbol-processors") + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") implementation("io.koraframework:config-hocon") implementation("io.koraframework:http-server-undertow") implementation("io.koraframework:json-common") implementation("io.koraframework:logging-logback") implementation("io.koraframework:validation-module") - kspTest("io.koraframework:symbol-processors") testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.koraframework:test-junit5") diff --git a/guides/kotlin/kora-kotlin-guide-validation-app/src/main/kotlin/io/koraframework/guide/validation/Application.kt b/guides/kotlin/kora-kotlin-guide-validation-app/src/main/kotlin/io/koraframework/guide/validation/Application.kt index 489e70af..d6ba0e1c 100644 --- a/guides/kotlin/kora-kotlin-guide-validation-app/src/main/kotlin/io/koraframework/guide/validation/Application.kt +++ b/guides/kotlin/kora-kotlin-guide-validation-app/src/main/kotlin/io/koraframework/guide/validation/Application.kt @@ -7,7 +7,7 @@ import io.koraframework.config.hocon.HoconConfigModule import io.koraframework.guide.validation.dto.ValidationErrorDetails import io.koraframework.guide.validation.dto.ValidationErrorResponse import io.koraframework.http.common.body.HttpBody -import io.koraframework.http.server.common.HttpServerModule +import io.koraframework.http.server.common.HttpServer import io.koraframework.http.server.common.response.HttpServerResponse import io.koraframework.http.server.undertow.UndertowPublicHttpServerModule import io.koraframework.json.common.JsonWriter @@ -33,7 +33,7 @@ interface Application : HttpServerResponse.of( 400, HttpBody.json( - errorResponseJsonWriter.toByteArrayUnchecked( + errorResponseJsonWriter.toByteArray( ValidationErrorResponse.of(toValidationErrors(exception.violations)) ) ) @@ -41,9 +41,11 @@ interface Application : } } - @Tag(HttpServerModule::class) + // the module default is untagged, so it is overridden only to bind the interceptor to the server; + // 2.0 declares the mapper parameter as @Nullable, which Kotlin enforces on the override + @Tag(HttpServer::class) override fun validationHttpServerInterceptor( - violationExceptionHttpServerResponseMapper: ViolationExceptionHttpServerResponseMapper + violationExceptionHttpServerResponseMapper: ViolationExceptionHttpServerResponseMapper? ): ValidationHttpServerInterceptor { return ValidationHttpServerInterceptor(violationExceptionHttpServerResponseMapper) } diff --git a/guides/kotlin/kora-kotlin-guide-validation-app/src/main/kotlin/io/koraframework/guide/validation/dto/UserRequest.kt b/guides/kotlin/kora-kotlin-guide-validation-app/src/main/kotlin/io/koraframework/guide/validation/dto/UserRequest.kt index 43761ca1..04969f29 100644 --- a/guides/kotlin/kora-kotlin-guide-validation-app/src/main/kotlin/io/koraframework/guide/validation/dto/UserRequest.kt +++ b/guides/kotlin/kora-kotlin-guide-validation-app/src/main/kotlin/io/koraframework/guide/validation/dto/UserRequest.kt @@ -4,8 +4,10 @@ import io.koraframework.json.common.annotation.Json import io.koraframework.validation.common.annotation.NotBlank import io.koraframework.validation.common.annotation.Pattern import io.koraframework.validation.common.annotation.Size +import io.koraframework.validation.common.annotation.Valid @Json +@Valid data class UserRequest( @field:NotBlank @field:Size(min = 2, max = 100) diff --git a/guides/kotlin/kora-kotlin-guide-validation-app/src/main/resources/application.conf b/guides/kotlin/kora-kotlin-guide-validation-app/src/main/resources/application.conf index 97dae54e..c89518fa 100644 --- a/guides/kotlin/kora-kotlin-guide-validation-app/src/main/resources/application.conf +++ b/guides/kotlin/kora-kotlin-guide-validation-app/src/main/resources/application.conf @@ -1,6 +1,6 @@ httpServer { - publicApiHttpPort = 8080 - privateApiHttpPort = 8085 + port = 8080 + system.port = 8085 telemetry.logging.enabled = true } diff --git a/migration/KORA_2_FINAL_REPORT.md b/migration/KORA_2_FINAL_REPORT.md new file mode 100644 index 00000000..0bc5a2d8 --- /dev/null +++ b/migration/KORA_2_FINAL_REPORT.md @@ -0,0 +1,293 @@ +# Kora 1.x → 2.0: итоговый отчёт по миграции `kora-examples` + +Ветка примеров: `migration/2.0`. Фреймворк: локальный `../kora`, ветка +`integration/migration-fixes` (сведённые фиксы поверх `master`), опубликован в Maven Local как +`io.koraframework:*:2.0.0-SNAPSHOT`. + +Отчёт не дублирует детали: помодульные цифры — в `KORA_2_MIGRATION_STATUS.md`, разбор каждого +дефекта — в `KORA_2_FRAMEWORK_ISSUES.md`, разбор каждого PR — в `KORA_2_PULL_REQUESTS.md`. + +--- + +## 1–2. Мигрированные Java- и Kotlin-модули + +Мигрированы **все 127 Gradle-модулей** из `settings.gradle`, все со статусом `MIGRATED`. + +| Группа | Модулей | Тестов пройдено | +|---|---|---| +| `examples/java/*` | 29 | 164 | +| `examples/graalvm/*` (Java) | 3 | 15 | +| `guides/java/*` | 33 | 112 | +| **Java итого** | **65** | **291** | +| `examples/kotlin/*` | 29 | 162 | +| `guides/kotlin/*` | 33 | 118 | +| **Kotlin итого** | **62** | **280** | +| **Всего** | **127** | **571** | + +Упавших тестов — 0. Из 127 модулей тесты есть у 116; остальные 11 — библиотечные/субмодульные +части без собственных тестов, отмеченные в таблице статуса как «нет тестов». + +Цифры — из реального прогона `./gradlew <все модули>:test --max-workers=1 --continue` +(556 тестов за 10 мин 53 с) плюс отдельный прогон трёх GraalVM-модулей (15 тестов), +которые собирают native-образ в Docker и потому требуют свободного Docker. + +## 3. Gradle-модули и общая сборочная логика + +Композитных сборок и `buildSrc` в репозитории нет — общая логика живёт в корневом +`build.gradle` и `grouping.gradle`. + +| Файл | Что изменено | +|---|---| +| `settings.gradle` | шесть модулей на R2DBC/Vert.x закомментированы с указанием причины | +| `gradle.properties` | `koraVersion` 1.2.16 → 2.0.0-SNAPSHOT | +| `build.gradle` | GraalVM-плагин 0.11.5 → 1.1.7, Kotlin 1.9.25 → 2.4.10, KSP 1.9.25-1.0.20 → 2.3.11 | +| `grouping.gradle` | без изменений | +| `gradle/wrapper` | без изменений — Gradle 9.5.1 был уже на `master` | + +На уровне модулей переведены: координаты и BOM, процессоры (annotationProcessor/KSP), toolchain 21 → 25, +настройки генераторов (OpenAPI, protobuf, wsdl2java), native-задачи и базовые образы Docker. + +**Требование к окружению:** сам Gradle-процесс обязан идти на JDK 25 — `io.koraframework:openapi-generator` +попадает в buildscript classpath, а тот резолвится JVM самого Gradle, а не toolchain'ом. + +## 4–5. GraalVM-модули и собранные native-образы + +| Модуль | Размер | Зависимости проверки | Тесты | +|---|---|---|---| +| `kora-java-graalvm-crud-jdbc` | 51 МиБ | Postgres | 7/7 | +| `kora-java-graalvm-crud-cassandra` | 76 МиБ | Scylla + Redis | 7/7 | +| `kora-java-graalvm-kafka` | 80 МиБ | Kafka | 1/1 | + +Каждый образ собран **двумя путями** — `nativeCompile` через Gradle-плагин и `native-image` +внутри `ghcr.io/graalvm/native-image-community:25` по `Dockerfile` — и проверен в работе: бинарь +стартует, `/system/readiness` отвечает 200, `/metrics` отдаёт метрики (~4,6 КБ), сценарий модуля +отрабатывает против реальной зависимости. Окружение: GraalVM CE 25.2.4+7.1 (`native-image 25.0.4`). + +## 6. Частично мигрированные модули + +Нет. Статуса `PARTIALLY_MIGRATED` ни у одного модуля. + +## 7. Заблокированные модули + +Шесть модулей со статусом `BLOCKED_BY_REMOVED_FUNCTIONALITY`. Все шесть исключены из +`settings.gradle`, но **оставлены в репозитории**: + +| Модуль | Причина | +|---|---| +| `examples/java/kora-java-database-r2dbc` | интеграция R2DBC удалена из 2.0 | +| `examples/java/kora-java-database-vertx` | интеграция Vert.x SQL удалена из 2.0 | +| `examples/kotlin/kora-kotlin-database-r2dbc` | то же | +| `examples/kotlin/kora-kotlin-database-vertx` | то же | +| `examples/graalvm/kora-java-graalvm-crud-r2dbc` | то же | +| `examples/graalvm/kora-java-graalvm-crud-vertx` | то же | + +Заменяющей архитектуры в 2.0 нет в том смысле, что реактивные контракты убраны целиком; +рекомендуемый путь — синхронные `database-jdbc` / `database-cassandra` на виртуальных потоках. +Как именно — показано на `kora-java-graalvm-crud-cassandra`, который был переписан с `Mono` на синхронный +код целиком. + +## 8. Модули, требующие редизайна + +Статуса `REQUIRES_REDESIGN` нет ни у одного модуля из сборки. Шесть заблокированных выше +требуют именно редизайна, а не перевода API, и это решение владельца репозитория (п. 24). + +## 9. Удалённая в 2.0 функциональность, встретившаяся по ходу + +| 1.x | 2.0 | Замена | +|---|---|---| +| `database-r2dbc`, `database-vertx` | удалены | `database-jdbc` | +| `suspend`- и реактивные контракты репозиториев | удалены | синхронные методы на виртуальных потоках | +| корутинные интеграции в модулях Kora | удалены | то же | +| Kotlin Structured Concurrency | не сохраняется | Java `StructuredTaskScope` из latest GA JDK + latest Preview, `--enable-preview` на compile/test/run | +| `http-client-async` | удалён | `http-client-jdk` / `http-client-ok` | +| `json-module` | переименован | `json-common` | +| `cache-redis` | разделён по транспорту | `cache-redis-lettuce` | +| `s3-client-minio` | реализации нет | `s3-client-aws` либо декларативный `s3-client-kora` | +| `UndertowHttpServerModule` | разделён | `UndertowPublicHttpServerModule` | +| `@ConfigValueExtractor` | переименована | `@ConfigMapper` | +| `Context` в синхронных HTTP API | убран | переписать сигнатуру | +| `invalidateAll = true` | удалён | `@CacheInvalidateAll` | +| `toByteArrayUnchecked` | удалён | `toByteArray` | +| `openapi.management.file` | переименован | `openapi.management.files` | +| `KspTask` (тип Gradle) | удалён в KSP 2 | привязка задач по имени | + +Полные таблицы с примерами — в §12 обоих языковых руководств. + +## 10–13. Найденные правила миграции + +Правила записаны в два самодостаточных руководства — общие правила продублированы целиком, +чтобы читателю не нужно было открывать гайд другого языка. + +| Группа правил | Java-гайд | Kotlin-гайд | +|---|---|---| +| Сборка и окружение (JDK 25, координаты, плагины, кеш генераторов) | §1 | §1 | +| Пакеты и аннотации | §2 | §2 | +| Нуллабельность (JSpecify / типы Kotlin) | §3 | §3 | +| HTTP-сервер и HTTP-клиент | §4 | §4 | +| Конфигурация | §5 | §5 | +| Resilient: строки → типизированные спецификации | §6 | §6 | +| Кеш | §7 | §7 | +| База данных (и снятие `suspend`) | §8 | §8 | +| Планировщик | §9 | §9 | +| OpenAPI-генерация | §10 | §10 | +| Тестирование | §11 | §11, §11a | +| Удалённая функциональность | §12 | §12 | +| Синхронная модель и Virtual Threads | §13 | §13 | +| S3 | §15 | §11a | +| Camunda 8 / Zeebe | §16 | §11a | +| **GraalVM Native Image** | **§17** | **§15** | + +Gradle-правила собраны в §1 обоих руководств, GraalVM-правила — в §17 (Java) и §15 (Kotlin). +Каждое правило оформлено как «Было / Стало / Причина / Проверка / Ограничение». + +**Главные GraalVM-правила** (все проверены сборкой и запуском): + +1. `native-build-tools` 0.11.5 → 1.1.7 — иначе `collectReachabilityMetadata` падает на Gradle 9. +2. GraalVM 25 во всех трёх местах: toolchain, `javaLauncher`, базовый образ Docker. +3. `imageName`/`mainClass` — провайдер, а не строковая интерполяция `Property`. +4. `jar.enabled = false` убрать — иначе классы приложения не попадают в classpath `nativeCompile`. +5. Имя файла метаданных — только `reflect-config.json`; `reflection-config.json` не читается вовсе. +6. Успешная сборка образа не является критерием готовности: все пять native-дефектов собирались зелёно. + +## 14. OpenRewrite-рецепты + +`migration/openrewrite/src/main/resources/META-INF/rewrite/kora-1-to-2.yml` — пять фокусных рецептов +плюс агрегат: + +| Рецепт | Что делает | +|---|---| +| `RelocateKoraPackages` | `ru.tinkoff.kora.*` → `io.koraframework.*` | +| `RelocateKoraAnnotations` | DI-аннотации в `common.annotation` | +| `RenameKoraTypes` | 4 типа, сменивших имя или модуль | +| `MigrateNullabilityAnnotations` | `jakarta.annotation.Nullable` → JSpecify | +| `UpdateKoraDependencies` | координаты сборки | +| `Kora1To2` | агрегат | + +Before/after-тесты: `Kora1To2Test`, 5/5 зелёных, включая проверку идемпотентности. +Рецепты по координатам модульным тестом не покрыты — причины разобраны в +`migration/openrewrite/README.md` (коротко: `ChangeDependency` требует маркера `GradleProject` +и резолва новых координат, а версий 1.x под новой группой не существует по определению). + +Сам репозиторий мигрировался скриптом, а не рецептом: рецепт не знает Kotlin, а его часть +по координатам требует разрешимого промежуточного состояния. + +## 15. Скрипты + +| Скрипт | Назначение | +|---|---| +| `migrate_kora_2.py` | пакеты, координаты, версии плагинов, ресурсы, перенос пакетных каталогов | +| `migrate_api_renames.py` | точечные переименования API | +| `migrate_config_keys.py` | ключи конфигурации | +| `migrate_resilient_specs.py` | заготовки типизированных resilient-спецификаций | + +У всех есть dry-run по умолчанию и `--apply`. Ограничения `migrate_kora_2.py` — хардкод имён +классов этих примеров и широкие текстовые замены — описаны в `migration/README.md` +без прикрас. + +## 16. Паттерны для нейро-агентов + +Девять паттернов в `KORA_MIGRATION_NEURO.md`: + +1. строковые resilient-аннотации → типизированные спецификации; +2. конфигурация, зашитая в исходники тестов; +3. снятие `suspend` по всей цепочке вызовов; +4. мапперы/интерцепторы как DI-компоненты; +5. расстановка JSpecify type-use аннотаций; +6. собственные метаданные native-image при переименовании пакетов; +7. диагностика: краш KSP — дефект фреймворка или немигрированный код; +8. диагностика: фантомные `package ru.tinkoff.kora.* does not exist`; +9. диагностика: native-image собрался, но падает в рантайме. + +## 17–18. Найденные дефекты фреймворка + +В журнале `KORA_2_FRAMEWORK_ISSUES.md` — **29 записей**. Расклад: + +| Состояние | Сколько | +|---|---| +| Подтверждено и исправлено | 22 (одна — зонтичная запись по крашам KSP) | +| Подтверждено, не исправлено | 2 | +| Наблюдение, не расследовано | 1 | +| Закрыто как не-дефект / дубликат / поведение по замыслу | 3 | +| Внешний инструмент (не Kora) | 1 | + +Три гипотезы были опровергнуты экспериментом и оставлены в журнале вместе с опровержением — +в том числе моё собственное ошибочное утверждение, что `reflection-config.json` всё-таки читается. + +Самые тяжёлые дефекты не проявлялись как ошибка сборки: ни один спан не экспортируется; +`/metrics` всегда отвечает заглушкой; gRPC-приложение завершается сразу после старта с кодом 0; +все логи вне запроса теряются аппендером. + +## 19–21. Исправления, тесты и PR + +**27 отправленных pull request'ов** — [#791–#818](https://github.com/kora-projects/kora/pulls?q=is%3Apr+author%3Adsudomoin) +(номер #817 занят чужим PR). Каждый — отдельная ветка от `master` с одним логическим +исправлением, перебазированная на актуальный `origin/master`. Общего «migration fixes»-коммита нет. + +По модулям фреймворка: `core` (4), `openapi` (4), `kafka` (3), `experimental/zeebe` (3), +`http` (1), `grpc` (1), `database` (3), `cache` (1), `telemetry` (2), `config` (1), `logging` (1), +`test` (1), плюс один PR на три модуля сразу (переименование `reflect-config.json`). + +**Регрессионные тесты добавлены в 19 из 27 PR.** У восьми теста нет, и в каждом случае +причина указана в самом PR: + +- пять GraalVM-фиксов (#811–#815) — на JVM дефект не воспроизводится вообще, любой модульный + тест был бы зелёным и без фикса. Доказательство — цифры до/после на `examples/graalvm/*`; +- #799 — нужны несколько раундов процессора, чего `AbstractSymbolProcessorTest` не моделирует; + проверено интеграционно; +- #816 — `AsyncAppenderBase` из теста не доставляет событие ни с фиксом, ни без; доказательство — + размер лога приложения: 591 байт до, 16 001 байт после; +- #818 — дефект в том, как узел связывается процессором; тест, собирающий граф руками, + объявит любые зависимости и ничего не докажет. Доказательство — сгенерированный граф: + `List.of()` → `List.of(component1)`. + +Для локальной работы все ветки сведены в `integration/migration-fixes` — с неё публиковался +Мавен Local. Источник истины для upstream — сами ветки. + +## 22. Оставшаяся ручная работа + +По самим примерам — нет. По фреймворку и репозиторию остаются три пункта: + +1. **Диагностика `GraphBuilder` при провале всех форков шаблона.** Механизм подтверждён + контролируемым экспериментом, но фикс — это дизайн вывода (сводка по всем кандидатам + вместо одного случайного), а не точечная правка. Решение за мейнтейнерами. +2. **Гонка KSP-процессоров при параллельном прогоне** (`JacksonIOException: Stream closed`). + Обходится `--max-workers=1`; исправление затрагивает контракт процессоров. +3. **`groupId=ru.tinkoff.kora` в `gradle.properties`.** Это группа самих примеров, а не зависимость + от Kora; примеры никуда не публикуются, так что функционально она ни на что не влияет. + Не менял: это решение о брендинге репозитория, а не часть миграции. + +## 23. Незавершённая валидация + +Что проверено: компиляция main и test, кодогенерация (annotation processing и KSP), модульные +и интеграционные тесты (включая Testcontainers против реальных БД и брокеров), старт приложений, +сборка и запуск трёх native-образов. + +Чего не делалось и что это значит: + +| Не проверено | Замечание | +|---|---| +| Native-образ Kotlin-приложения | в репозитории нет такого модуля; §15.8 Kotlin-гайда помечен как гипотезы | +| `nativeTest` (тесты внутри native-образа) | не настроен ни в одном модуле и не был настроен в 1.x; вместо этого `BlackBoxTests` гоняют готовый образ | +| Graceful shutdown под нагрузкой | проверялся только косвенно — через остановку контейнеров в тестах | +| Производительность 1.x вс. 2.0 | не измерялась; в задачу не входило | +| Шесть заблокированных модулей | не собираются вовсе — по определению | +| OpenRewrite на чужом проекте | рецепт покрыт модульными тестами, но `rewriteRun` против реального чужого проекта не гонялся | + +Две ловушки тестового окружения, которые стоит знать при повторном прогоне: `BlackBoxTests` +сассандры требуют свободного Docker (при четырёх параллельных контейнерах Scylla не успевает +стартовать), а `:examples:java:kora-java-crud:compileJava` при инкрементальной сборке видит `:arg0` +вместо имён параметров — с `--rerun` модуль даёт 9/9. + +## 24. Что владельцу репозитория, возможно, придётся переписать или удалить + +Все пункты ниже — решения о структуре репозитория, а не часть миграции, поэтому ничего из этого +не сделано без его согласия. + +1. **Шесть модулей на R2DBC/Vert.x.** Либо переписать на синхронный JDBC (тогда они дублируют + существующие примеры), либо оставить как legacy с пометкой в README, либо удалить. + Мой совет — удалить: синхронные эквиваленты в репозитории уже есть. +2. **`kora-java-s3-client-minio` и `kora-kotlin-s3-client-minio`.** В 2.0 они демонстрируют уже не SDK + Minio, а декларативный `s3-client-kora` против Minio как S3-совместимого хранилища. Каталоги + специально не переименованы — это задело бы четыре модуля и ссылки в README. Естественные + имена — `kora-java-s3-client-kora` / `kora-kotlin-s3-client-kora`. +3. **`groupId` репозитория** — см. п. 22. diff --git a/migration/KORA_2_FRAMEWORK_ISSUES.md b/migration/KORA_2_FRAMEWORK_ISSUES.md new file mode 100644 index 00000000..7fa2d39c --- /dev/null +++ b/migration/KORA_2_FRAMEWORK_ISSUES.md @@ -0,0 +1,1630 @@ +# Kora 2.0 — журнал дефектов, найденных при миграции + +Здесь фиксируется всё подозрительное, что может оказаться проблемой Kora 2.0, включая неподтверждённые гипотезы. +Гипотезы помечены явно и уточняются по мере расследования. Запись здесь **не** означает, что баг подтверждён. + +Источник истины по фреймворку — локальный чекаут `../kora` @ `master` (`io.koraframework:*:2.0.0-SNAPSHOT`, Maven Local). + +--- + +## Issue: Cassandra-репозиторий с `CompletableFuture` генерирует некомпилируемый код + +- Status: Fixed +- Severity: Major +- Type: Framework bug +- Language: Java +- Runtime: JVM +- Component: `database-annotation-processor`, генератор Cassandra-репозиториев +- Affected framework module: `database/database-annotation-processor` +- Affected example modules: `examples/java/kora-java-database-cassandra`, `examples/graalvm/kora-java-graalvm-crud-cassandra` +- Framework commit: `66800169f` +- Related fix branch: `fix/cassandra-completable-future-return` (`389ad86fb`) — [#806](https://github.com/kora-projects/kora/pull/806) + +### Description + +Метод Cassandra-репозитория, возвращающий `CompletableFuture` (а не `CompletionStage`), приводит к генерации кода, который не компилируется. + +### Minimal reproduction + +```java +@Repository +public interface CassandraCrudAsyncRepository extends CassandraRepository { + + @EntityCassandra + record Entity(String id, @Column("value1") int field1, String value2, @Nullable String value3) {} + + @Query("SELECT * FROM entities WHERE id = :id") + CompletableFuture findById(String id); // CompletionStage работает +} +``` + +### Actual behavior + +``` +$CassandraCrudAsyncRepository_Impl.java:110: error: incompatible types: inference variable R has incompatible bounds + .call(() -> { + upper bounds: CompletableFuture,Object +``` + +### Investigation notes + +Поддержка `CompletableFuture` в генераторе **заявлена явно** — `CassandraRepositoryGenerator.java:143-145`: + +```java +if (((DeclaredType) returnType).asElement().toString().equals(CompletableFuture.class.getCanonicalName())) { + st.add(".toCompletableFuture()"); +} +``` + +Цепочка строится внутри обёртки `CommonUtils.observe(..., "call", ...)`, и добавленный `.toCompletableFuture()` ломает вывод типа параметра `R` у этой обёртки. + +Тесты фреймворка покрывают `CompletionStage` и `CompletionStage` (`CassandraResultsTest`), но **ни одного теста с `CompletableFuture`** в `database-annotation-processor` нет — поэтому регрессия не ловится. + +### Workaround + +Заменить тип возврата на `CompletionStage` (больше не требуется). + +### Implemented fix + +`.toCompletableFuture()` перенесён из внутренней лямбды наружу внешней обёртки `observe(...)`: + +```java +CommonUtils.observe(mb, "_observation", "call", b -> { … }); +// the observe(...) wrapper yields whatever the lambda returns, and the chain inside it is a +// CompletionStage; converting inside the lambda would make R unresolvable against the +// declared CompletableFuture return type +if (((DeclaredType) returnType).asElement().toString().equals(CompletableFuture.class.getCanonicalName())) { + mb.addCode(".toCompletableFuture()"); +} +``` + +Внешний `observe(...).call(...)` возвращает то, что вернула лямбда, а внутри неё цепочка +`prepareAsync(...).thenCompose(...)` даёт `CompletionStage`. Пока преобразование стояло внутри, +параметр `R` внешней обёртки не сводился с объявленным `CompletableFuture`. Теперь преобразование +происходит там, где значение действительно пересекает границу объявленного типа возврата. +Ветка `CompletionStage` не изменилась. + +### Test coverage + +`CassandraResultsTest#testReturnCompletableFutureObject` и `#testReturnCompletableFutureVoid`. +Оба падают без фикса ровно с `incompatible types: inference variable R has incompatible bounds`. +До этого в тестах были только `CompletionStage` и `CompletionStage` — ни одного +случая с `CompletableFuture`, поэтому регрессия не ловилась. + +### Compatibility impact + +Чистое исправление: раньше такой код не компилировался вовсе. + +### Validation + +`:database:database-annotation-processor:test` целиком зелёный; затем сборка +`examples/java/kora-java-database-cassandra`. + +### Resolution + +Закрыт. Пометка `BLOCKED_BY_FRAMEWORK_BUG` с модулей снята. + +--- + +## Issue: JSON-фабрика `httpClientResponseJsonEntityResponseMapper` не помечена `@Json` + +- Status: **Fixed** +- Severity: Major +- Type: Framework bug +- Language: Java, Kotlin (общий runtime-модуль) +- Runtime: JVM +- Component: `http-client-common`, разрешение зависимостей `@KoraApp` +- Affected framework module: `http/http-client-common` +- Affected example modules: `examples/java/kora-java-http-client` (и любой клиент с `HttpResponseEntity` в приложении с JSON) +- Framework commit: база `66800169f`, фикс `3ef4560af` +- Related fix branch: `fix/http-client-json-response-entity-mapper-tag` — [#791](https://github.com/kora-projects/kora/pull/791) + +### Description + +Любой метод HTTP-клиента, возвращающий `HttpResponseEntity`, не собирается в приложении, где в графе есть `JsonReader`. + +### Actual behavior + +``` +error: Multiple components match dependency: + HttpClientResponseMapper> (no tags) + Candidates: + - factory HttpClientResponseMapperModule#httpClientResponseEntityResponseMapper(...) + - factory HttpClientResponseMapperModule#httpClientResponseJsonEntityResponseMapper(...) +``` + +### Suspected cause → подтверждённая причина + +В `HttpClientResponseMapperModule` все JSON-варианты помечены `@Json` (`httpClientJsonEitherResponseMapper`, `httpClientJsonEitherResponseEntityResponseMapper`), кроме `httpClientResponseJsonEntityResponseMapper(JsonReader)`. Без тега две `@DefaultComponent`-шаблонные фабрики дают один и тот же тип без тегов, и граф не может выбрать. + +### Implemented fix + +Добавлена аннотация `@Json` на `httpClientResponseJsonEntityResponseMapper`. + +### Test coverage + +`HttpClientExtensionTest#testExtensionResponseEntityWhenJsonReaderIsPresent` — проверено, что тест **падает без фикса** (`Multiple components match dependency`) и проходит с ним. Существовавший `testExtensionWithoutTag` дефект не ловил: в его графе нет `JsonReader`. + +### Compatibility impact + +Код, который полагался на неявный JSON-маппинг `HttpResponseEntity` без `@Json`, теперь должен указывать `@Json` явно. Раньше такой код в любом случае не собирался. + +### Validation + +`:http:http-client-common:test`, `:http:http-client-annotation-processor:test`, `:http:http-client-symbol-processor:test` — зелёные; `examples/java/kora-java-http-client` — компилируется, все 9 тестов проходят. + +--- + +## Issue: `@Component` job worker с `public`-методом молча исчезает из графа + +- Status: **Fixed** +- Severity: Blocker +- Type: Framework bug +- Language: Java (Kotlin/KSP не проверялся — модуль не мигрирован) +- Runtime: JVM +- Component: `@KoraApp` (разрешение компонентов), AOP-процессор +- Affected framework module: `experimental/camunda-zeebe-worker` +- Affected example modules: `examples/java/kora-java-camunda-zeebe-worker` +- Framework commit: база `66800169f`, фикс `9873eeb69` +- Related fix branch: `fix/zeebe-worker-annotations-not-aop` — [#794](https://github.com/kora-projects/kora/pull/794) + +### Description + +Класс, помеченный `@Component`, с `public`-методом под `@JobWorker`, не попадает в граф зависимостей. +Сгенерированный `$X_handle_KoraJobWorker` требует `X`, а компилятор сообщает, что такого компонента нет. + +### Actual behavior + +``` +error: No component found for dependency: + public $Step1JobWorker_handle_KoraJobWorker(Step1JobWorker handler, ZeebeWorkerConfig config, ...) + io.koraframework.example.camunda.zeebe.Step1JobWorker (no tags) + ... + ^--- io.koraframework.example.camunda.zeebe.Step1JobWorker [MISSING] + + Fix: + - Add @Component to an implementation of io.koraframework.example.camunda.zeebe.Step1JobWorker. +``` + +Подсказка вводит в заблуждение: `@Component` на классе **уже стоит**. + +### Investigation notes + +`KoraAppProcessor.processComponents` регистрирует `@Component`-класс только если `CommonUtils.hasAopAnnotations(typeElement)` вернул `false`: + +```java +if (!CommonUtils.hasAopAnnotations(typeElement)) { + this.components.add(typeElement); +} +``` + +Расчёт такой, что для класса с AOP-аннотациями компонентом станет сгенерированный прокси `$X__AopProxy`. +Но `AopAnnotationProcessor.getSupportedAnnotationTypes()` собирается из аннотаций, которые заявили +зарегистрированные `KoraAspectFactory`. Для `@JobWorker` / `@JobVariable` / `@JobVariables` аспекта нет +ни одного (`grep -rln KoraAspectFactory experimental/camunda-zeebe-worker*` пусто), поэтому прокси не +генерируется никогда — класс просто пропадает. + +`hasAopAnnotations` смотрит только на `public` и `protected` методы: + +```java +var methods = CommonUtils.findMethods(typeElement, m -> m.contains(Modifier.PUBLIC) || m.contains(Modifier.PROTECTED)); +``` + +Поэтому в существующих тестах `ZeebeWorkerTests` дефекта не видно — там все `handle` объявлены +package-private. Экспериментально: перевод `Step1JobWorker.handle` в package-private убирает ошибку +для него, и она сразу всплывает на `Step2JobWorker` — то есть затронуты все три воркера примера. + +Все прочие кодогенерирующие аннотации Kora (`@ScheduleAtFixedRate`, `@KafkaListener`) `@AopAnnotation` +не помечены. Zeebe-аннотации — единственные `@AopAnnotation` без реализации аспекта. + +### Implemented fix + +Снята мета-аннотация `@AopAnnotation` с `@JobWorker`, `@JobVariable`, `@JobVariables`. +Аспекты вроде `@Log` или `@Timeout` на методах воркера продолжают работать: такие аннотации +включают прокси сами по себе. + +### Test coverage + +`ZeebeWorkerTests#workerWithPublicMethodIsResolvedInGraph` — прогоняет вместе `KoraAppProcessor`, +`AopAnnotationProcessor` и `ZeebeWorkerAnnotationProcessor` над `@Component`-воркером с `public`-методом +и проверяет, что граф собирается. Проверено, что тест **падает без фикса** (`Handler [MISSING]`). +AOP-процессор включён в тест намеренно — иначе можно было бы возразить, что в бою прокси всё же +сгенерировался бы. + +### Compatibility impact + +Обратно совместимо. Классы, которые собирались раньше (package-private `handle`), продолжают +собираться; ломавшиеся — начинают. + +### Validation + +`:experimental:camunda-zeebe-worker-annotation-processor:test` — зелёный (6 тестов); +`examples/java/kora-java-camunda-zeebe-worker` — компилируется. + +### Обобщение + +Дефект шире, чем zeebe: **любая** `@AopAnnotation` без зарегистрированного аспекта убирает +`@Component`-класс из графа без внятной диагностики. Более глубокий вариант фикса — научить +`KoraAppProcessor` сверяться с реестром аспектов, а не с фактом наличия мета-аннотации, и/или +выдавать ошибку "AOP-аннотация без аспекта" вместо молчаливого пропуска. + +--- + +## Issue: `JobWorkerException` больше не поднимает BPMN-ошибку + +- Status: **Fixed** +- Severity: Major +- Type: Framework bug (регрессия 2.0) +- Language: Java (Kotlin/KSP-генератор не проверялся) +- Runtime: JVM +- Component: `camunda-zeebe-worker-annotation-processor` +- Affected framework module: `experimental/camunda-zeebe-worker-annotation-processor` +- Affected example modules: `examples/java/kora-java-camunda-zeebe-worker` +- Framework commit: база `66800169f`, фикс `b26022694` +- Related fix branch: `fix/zeebe-worker-exception-throws-bpmn-error` — [#795](https://github.com/kora-projects/kora/pull/795) + +### Description + +Брошенный из воркера `JobWorkerException` должен приводить к BPMN-ошибке с указанным кодом, +чтобы процесс ушёл по boundary error event. В 2.0 исключение просто пробрасывается: задача падает, +ретраится до исчерпания бюджета, ветка обработки ошибки не выполняется. + +### Steps to reproduce + +Процесс `bpm/demo.bpmn` в примере: у задачи `fail` есть boundary error event с `errorCode="DOESNT_WORK"`, +воркер бросает `new JobWorkerException("DOESNT_WORK")`. + +### Actual behavior + +``` +io.koraframework.camunda.zeebe.worker.exception.JobWorkerException: [DOESNT_WORK]Failed with code: DOESNT_WORK + at ...Step3JobWorker.handle(Step3JobWorker.java:22) + at ...$Step3JobWorker_handle_KoraJobWorker.handle($Step3JobWorker_handle_KoraJobWorker.java:50) + at io.koraframework.camunda.zeebe.worker.WrappedJobHandler.handle(WrappedJobHandler.java:49) +``` + +(три раза — по числу ретраев), затем тест `ZeebeMockedTests#processDemoSuccess` отваливается по +`ConditionTimeoutException`, так как экземпляр процесса не завершается. + +### Investigation notes + +Генератор выпускает + +```java +} catch (JobWorkerException e) { + throw e; +} +``` + +Команду `newThrowErrorCommand` не строит никто: `git log -S newThrowErrorCommand` даёт коммит +`63ad1886c` «Simplify zeebe client telemetry (#527)», который выкинул из `WrappedJobHandler` +метод `createErrorCommand(...)` вместе с обработкой `JobWorkerException`, ничего не поставив взамен. + +При этом окружающая телеметрия по-прежнему рассчитывает на эту команду: + +- `DefaultZeebeWorkerObservation#observeFinalCommandStep` выставляет `failedByUser` именно + для `ThrowErrorCommandStep2` — сейчас эта ветка мертва; +- `DefaultZeebeWorkerLoggerFactory#logJobEnd` умеет случай `error == null && failedByUser` + и пишет `exceptionType = "ErrorStep"` — тоже мёртвая ветка. + +### Implemented fix + +Команда строится там, где известен исход, — в сгенерированном воркере: + +```java +} catch (JobWorkerException e) { + var _error = client.newThrowErrorCommand(job).errorCode(e.getCode()).errorMessage(e.getMessage()); + if (e.getVariables() != null) { + _error.variables(e.getVariables()); + } + return _error; +} +``` + +`WrappedJobHandler` не трогается: он уже отправляет то, что вернул воркер, и наблюдает команду. +В BPMN-ошибку превращается **только** `JobWorkerException`; всё остальное по-прежнему +заворачивается в `JobWorkerException("UNEXPECTED", e)` и бросается, то есть непредвиденные сбои +продолжают ронять задачу, а не уходят молча по ветке ошибки. + +### Test coverage + +`ZeebeWorkerTests#workerJobWorkerExceptionIsTurnedIntoBpmnError` — поведенческий тест: собирает +воркер, вызывает `handle` с мок-клиентом и проверяет, что вернулась команда throw error. +Проверено, что без фикса тест падает с вылетающим из `handle` исключением. + +### Compatibility impact + +Восстанавливает поведение 1.x. Код, который (случайно) полагался на ретраи вместо BPMN-ошибки, +изменит поведение — но это и есть исправляемый дефект. + +### Validation + +`:experimental:camunda-zeebe-worker-annotation-processor:test` — зелёный. + +--- + +## Issue: генератор OpenAPI схлопывает две одинаковые именованные схемы в одну + +- Status: Observed (не расследовался) +- Severity: Minor — ломает компиляцию один раз, чинится сменой типа в сигнатуре +- Type: Изменение поведения генератора +- Language: Java (Kotlin-режимы не проверялись) +- Runtime: JVM +- Component: `openapi-generator`, режим `java-server` +- Affected framework module: `openapi/openapi-generator` +- Affected example modules: `examples/java/kora-java-crud-submodule` +- Framework commit: `66800169f` + +### Description + +В спеке объявлены две разные именованные схемы с идентичным содержимым: + +```yaml +VetCreateTO: + allOf: [ { $ref: '#/components/schemas/VetFieldTO' } ] +VetUpdateTO: + allOf: [ { $ref: '#/components/schemas/VetFieldTO' } ] +``` + +Сгенерирована только `VetCreateTO`; `VetUpdateTO` отсутствует, и `VetApiDelegate#updateVet` +принимает `VetCreateTO`. Код примера, написанный под 1.x, ссылался на `VetUpdateTO` — значит +в 1.x модель генерировалась. + +### Actual behavior + +``` +error: cannot find symbol + import io.koraframework.example.submodule.openapi.http.server.model.VetUpdateTO; +``` + +### Investigation notes + +Не расследовано, дедупликация одинаковых схем может быть намеренной (в upstream +openapi-generator есть соответствующие опции). Проверять надо две вещи: делает ли это сам +upstream-генератор или форк Kora, и управляется ли поведение опцией. + +### Workaround + +Привести сигнатуру к сгенерированной модели (`VetCreateTO`) либо сделать схемы различимыми, +добавив в одну из них собственное поле или описание. + +### Resolution + +Не закрыт. В примере применён обходной путь. + +--- + +## Issue: для `HttpResponseEntity` в sealed-ответе выбирается не тот шаблонный маппер + +- Status: Closed — не дефект (опровергнуто экспериментом) +- Severity: — +- Type: ложный след; настоящая причина — [неиспользуемый конвертер multipart-файла](#issue-генератор-openapi-требует-конвертер-для-multipart-файла-который-сам-не-использует) +- Language: Java, Kotlin +- Runtime: JVM +- Component: `kora-app-annotation-processor` (диагностика разрешения шаблонов) +- Affected example modules: `guides/java/kora-java-guide-openapi-http-server-advanced-app`, `guides/kotlin/kora-kotlin-guide-openapi-http-server-advanced-app` +- Framework commit: `66800169f` + +### Actual behavior + +``` +error: No component found for dependency: + JsonWriter> (no tags) + + Required at: + HttpServerResponseMapperModule#jsonHttpServerResponseMapper(JsonWriter) + + Dependency resolution path: + ^--- component DataApiServerResponseMappers.MappingByCodeApiResponseMapper + ^--- factory HttpServerResponseMapperModule#jsonHttpServerResponseMapper(...) + ^--- JsonWriter> [MISSING] +``` + +То же самое для `ErrorResponseTO`. + +### Investigation notes + +В `HttpServerResponseMapperModule` есть два `@Json @DefaultComponent` шаблона: + +```java +default HttpServerResponseMapper> jsonHttpResponseEntityHttpServerResponseMapper(JsonWriter writer) +default HttpServerResponseMapper jsonHttpServerResponseMapper(JsonWriter writer) +``` + +Для `HttpServerResponseMapper>` подходят оба, и по тексту ошибки +казалось, что выбран более общий, из-за чего требуется несуществующий +`JsonWriter>` вместо `JsonWriter`. + +**Опровергнуто контролируемым экспериментом.** В том же модуле была третья ошибка — недостающий +`HttpServerParameterReader` для multipart-загрузки. В `Application` временно добавлен +компонент-заглушка: + +```java +default HttpServerParameterReader tempProbeReader() { + return String::getBytes; +} +``` + +После этого модуль собрался целиком: **обе** ошибки про `JsonWriter>` исчезли, +хотя ни генератор, ни `HttpServerResponseMapperModule` не менялись. Значит специализированный шаблон +`jsonHttpResponseEntityHttpServerResponseMapper` выбирается корректно, а сообщения были побочным +эффектом другой, несвязанной неразрешённости — ровно тот механизм форков `GraphBuilder`, что описан +в отдельной записи про диагностику. + +### Resolution + +Закрыт как не-дефект. Настоящая причина — неиспользуемый конвертер multipart-файла в генераторе +OpenAPI (запись ниже), исправлена в ветке `fix/openapi-server-multipart-file-unused-converter`. +Пометка `BLOCKED_BY_FRAMEWORK_BUG` с модуля снята. + +--- + +## Issue: генератор OpenAPI требует конвертер для multipart-файла, который сам не использует + +- Status: Fixed +- Severity: Major (блокирует сборку модуля целиком) +- Type: Framework bug (генератор кода) +- Language: Java, Kotlin +- Runtime: JVM +- Component: `openapi-generator`, режимы `java-server` и `kotlin-server` +- Affected framework module: `openapi/openapi-generator` +- Affected example modules: `guides/java/kora-java-guide-openapi-http-server-advanced-app`, `guides/kotlin/kora-kotlin-guide-openapi-http-server-advanced-app` +- Framework commit: `66800169f` +- Related fix branch: `fix/openapi-server-multipart-file-unused-converter` — [#805](https://github.com/kora-projects/kora/pull/805) + +### Description + +Для операции с телом `multipart/form-data`, где есть поле `type: string, format: binary`, +генератор добавляет в конструктор request-маппера параметр +`HttpServerParameterReader` (в Kotlin — `HttpServerParameterReader`), +но в теле `apply` его **не использует**: бинарная часть отдаётся как `FormMultipart.FormPart` +напрямую. Компонента `HttpServerParameterReader` во фреймворке нет и быть не должно — +конвертер читает `String`, а не бинарные данные. В результате граф не собирается. + +### Minimal reproduction + +```yaml +/data/upload: + post: + operationId: processUpload + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [file] + properties: + file: + type: string + format: binary +``` + +Тот же случай уже был в фикстурах генератора: `petstoreV3_form.yaml`, операции +`/form-multipart-form-data-with-object` и `/form-multipart-form-data-with-array`. +Тесты этого не ловили, потому что они только компилируют сгенерированный код — а он +компилируется, ломается лишь построение графа `@KoraApp`. + +### Actual behavior + +```java +class ProcessUploadFormParamRequestMapper implements HttpServerRequestMapper<...> { + private final HttpServerParameterReader fileConverter; // <- поле мёртвое + + public ProcessUploadFormParamRequestMapper(HttpServerParameterReader fileConverter) { + this.fileConverter = fileConverter; + } + + @Override + public DataApiController.ProcessUploadFormParam apply(HttpServerRequest rq) throws IOException { + var file = (FormMultipart.FormPart) null; + ... + case "file" -> { file = _part; } // <- конвертер не вызывается + } +} +``` + +``` +No component found for dependency: + io.koraframework.http.server.common.request.HttpServerParameterReader (no tags) +``` + +### Investigation notes + +В `ServerRequestMapperGenerator` (Java) и `ServerRequestMappersGenerator.kt` (Kotlin) цикл, +объявляющий поля-конвертеры, пропускал только `String` и `List`. Тело `mapMultipart` +при этом отдельно проверяет `formParam.isFile` и в этом случае присваивает `_part` как есть. +То есть условия пропуска в конструкторе и в теле разошлись. + +Тонкость: пропускать `isFile` безусловно нельзя. Если операция объявляет и `multipart/form-data`, +и `application/x-www-form-urlencoded`, тело идёт по ветке `mapUrlEncoded`, а она конвертер вызывает. +Поэтому пропуск привязан к тому же решению, что и выбор ветки: `multipartForm && !urlEncodedForm`. + +### Implemented fix + +В обоих генераторах в цикл объявления конвертеров добавлено: + +```java +// url-encoded wins when an operation declares both, same as the apply() body below +var multipartBody = multipartForm && !urlEncodedForm; +... +if (multipartBody && formParam.isFile) { + // mapMultipart passes a binary part through as FormPart, so it never calls a converter + continue; +} +``` + +### Test coverage + +`HttpServerJavaOpenapiTest#multipartFileFormParamDoesNotAskForAConverterItNeverUses` и +одноимённый тест в `HttpServerKotlinOpenapiTest`. Проверяют на существующей фикстуре +`petstoreV3_form.yaml`, что оба multipart-маппера не объявляют `HttpServerParameterReader`, +но по-прежнему присваивают `_part`, а url-encoded маппер конвертеры сохранил. +Без фикса оба теста падают на `assertFalse(... contains("HttpServerParameterReader"))`. + +### Compatibility impact + +Ломающее изменение сигнатуры сгенерированного конструктора — но только для случая, который +до сих пор вообще не собирался. Работающий код затронуть не может. + +### Validation + +`:openapi:openapi-generator:test`; затем сборка и тесты обоих advanced-гайдов. + +--- + +## Issue: java-генератор OpenAPI берёт минимум схемы как верхнюю границу `@Range` + +- Status: Fixed +- Severity: Major (сгенерированная валидация отвергает корректные запросы) +- Type: Framework bug (генератор кода) +- Language: Java +- Runtime: JVM +- Component: `openapi-generator`, `AbstractJavaGenerator` +- Affected framework module: `openapi/openapi-generator` +- Affected example modules: `examples/java/kora-java-crud`, `examples/java/kora-java-crud-submodule`, `examples/java/kora-java-openapi-generator-http-server`, оба openapi-гайда +- Framework commit: `66800169f` +- Related fix branch: `fix/openapi-java-range-upper-bound` (`a1bfe41cb`) — [#808](https://github.com/kora-projects/kora/pull/808) + +### Description + +`AbstractJavaGenerator.getValidation` при построении верхней границы `@Range` читал `variable.getMinimum()`. +У каждого числового ограничения, выпущенного java-генератором, `to` совпадало с `from`. + +### Actual behavior + +``` +minimum: 1, maximum: 100 -> @Range(from = 1.0, to = 1.0) +minimum: 200, maximum: 599 -> @Range(from = 200.0, to = 200.0) +``` + +Случай «только minimum» ломался так же: вместо ветки с максимумом типа брался минимум. + +Kotlin-генератор на тех же спецификациях выдаёт `to = 100.0` и `to = 599.0`. Так дефект и обнаружился: +java-клиент отправил `size=100` java-серверу, собранному по спецификации `1..100`, и получил +`Should be in range from '1' to '1', but was greater: 100`, тогда как Kotlin-двойник прошёл. + +### Investigation notes + +Тесты этого не ловили: в фикстуре `petstoreV3_validation.yaml` обе формы уже были, но выпущенную +границу никто не проверял. Тесты примеров проходили потому, что ни один не отправлял значение +выше минимума — то есть дефект жил в сгенерированном коде всех java-модулей незаметно. + +### Implemented fix + +Ветка максимума читает `getMaximum()`. + +### Test coverage + +`HttpServerJavaOpenapiTest#numericRangeUsesTheSchemaMaximumAsUpperBound` — обе формы схемы. +Без фикса падает, показывая `@Range(from = 1.0, to = 1.0)`. + +--- + +## Issue: ни один спан не экспортируется — производный контекст OpenTelemetry теряет обёртку Kora + +- Status: Fixed +- Severity: Blocker (трассировка не работает вообще) +- Type: Framework bug +- Language: Java, Kotlin (общий runtime) +- Runtime: JVM +- Component: `core/common`, `OpentelemetryContext` +- Affected framework module: `core/common` +- Affected example modules: `examples/java/kora-java-telemetry`, `examples/kotlin/kora-kotlin-telemetry` +- Framework commit: `66800169f` +- Related fix branch: `fix/otel-context-with-loses-kora-wrapper` (`3f3403fbd`) — [#809](https://github.com/kora-projects/kora/pull/809) + +### Description + +Приложение с OTLP-экспортёром не отправляет ни одного спана. Коллектор получает метрики и больше ничего. + +### Steps to reproduce + +Поднять `otel/opentelemetry-collector`, запустить приложение с `tracing.exporter.endpoint`, +сделать любой HTTP-запрос. В логе коллектора появляется `MetricsExporter`, но не `TracesExporter`. + +### Actual behavior + +В логе приложения на каждую попытку экспорта: + +``` +BatchSpanProcessor$Worker exportCurrentBatch +WARNING: Exporter threw an Exception +java.lang.IllegalStateException + at OpentelemetryContextStorage.attach(OpentelemetryContextStorage.java:12) + at io.opentelemetry.context.Context.makeCurrent(Context.java:232) + at io.opentelemetry.context.Context.lambda$wrap$1(Context.java:241) + at InstrumentationUtil.suppressInstrumentation(InstrumentationUtil.java:34) +``` + +### Investigation notes + +Kora хранит контекст OpenTelemetry в `ScopedValue`, поэтому императивная пара +`attach()`/`makeCurrent()` не реализуема и намеренно бросает исключение — всё держится на +`wrap(...)`, реализованном только в `OpentelemetryContext` через `ScopedValue.where(...)`. + +`with(key, value)` возвращал `delegate.with(key, value)`, то есть обычный `ArrayBasedContext`: +обёртка терялась при первом же порождении контекста, и производный уходил в дефолтный +`Context.wrap(...)`, который вызывает `makeCurrent()`. OTLP-экспортёр порождает ровно такой +контекст на рабочем потоке `BatchSpanProcessor`, чтобы подавить инструментирование. + +### Implemented fix + +`with(...)` заворачивает результат обратно, как это уже делал `root()`. + +### Test coverage + +`OpentelemetryContextTest` — производный контекст и воспроизведённый путь подавления +инструментирования. Оба падают с `IllegalStateException` без фикса. + +--- + +## Issue: эндпоинт метрик всегда отвечает «Metric Scraper disabled» + +- Status: Fixed +- Severity: Blocker (Prometheus-скрейпинг невозможен) +- Type: Framework bug (отсутствующая привязка компонента) +- Language: Java, Kotlin (общий runtime) +- Runtime: JVM +- Component: `telemetry/micrometer-module`, `http-server-common` (`MetricsHandler`) +- Affected example modules: `guides/java/kora-java-guide-observability-app`, `guides/kotlin/kora-kotlin-guide-observability-app` +- Framework commit: `66800169f` +- Related fix branch: `fix/metrics-scraper-not-bound` (`013b66db5`) — [#810](https://github.com/kora-projects/kora/pull/810) + +### Description + +`GET /metrics` в любом приложении Kora 2.0 отдаёт `200` с телом `# Metric Scraper disabled`, +даже когда `micrometer-module` подключён. + +### Investigation notes + +`MetricsHandler` берёт из графа `ValueOf>`. Компонента такого типа +не предоставляет никто: `PrometheusMeterRegistryWrapper` интерфейс реализует, но фабрика +`MetricsModule#prometheusMeterRegistry` объявлена как `Wrapped`, поэтому граф +знает её только как `MeterRegistry`. Поиск по всему фреймворку даёт лишь три упоминания +`MetricsScraper`: интерфейс, обёртка и обработчик — ни одной фабрики. + +### Implemented fix + +`MetricsModule` дополнительно отдаёт `MetricsScraper` поверх реестра (`@DefaultComponent`, +чтобы приложение могло связать свой). + +### Test coverage + +`MetricsScraperBindingTest` — поведение скрейпера для Prometheus- и не-Prometheus-реестра. +Тест не может упасть на старом коде обычным способом: проверяемый метод и есть то, чего +не хватало. Сквозное доказательство — контейнер, собранный до фикса, отдаёт +`# Metric Scraper disabled`, после фикса — настоящий Prometheus-вывод. + +### Смежное, но не дефект + +После фикса `/metrics` отдаёт JVM-метрики, но не `http_server_*`: в 2.0 +`TelemetryConfig.MetricsConfig.enabled()` по умолчанию `false`. Это изменение поведения, +описанное в обоих руководствах по миграции. + +--- + +## Issue: KSP-процессоры Kora 2.0 падают с внутренним исключением вместо диагностики + +- Status: Fixed по всем воспроизведённым случаям (зонтичная запись; конкретные причины — в записях ниже) +- Severity: Blocker +- Type: Framework bug +- Language: Kotlin +- Runtime: JVM +- Component: KSP symbol processors (`symbol-processors`, `kora-app-symbol-processor`, модульные процессоры) +- Affected framework module: уточняется по каждому подвиду +- Affected example modules: 22 модуля со статусом `BLOCKED_BY_FRAMEWORK_BUG` в `KORA_2_MIGRATION_STATUS.md` +- Framework commit: `66800169f` (master на момент прогона) +- Related migration guide: `KORA_2_KOTLIN_MIGRATION_GUIDE.md` +- Related fix branch / PR: пока нет + +### Description + +При компиляции Kotlin-модулей задача `kspKotlin` завершается не сообщением об ошибке в коде, а необработанным исключением процессора. Пользователь не получает ни файла, ни строки, ни имени символа — только стектрейс-заголовок. + +Наблюдаются как минимум шесть различных подвидов: + +| Исключение | Пример модуля | +|---|---| +| `java.util.NoSuchElementException: No TypeParameter found for index T` | `examples/kotlin/kora-kotlin-helloworld`, `guides/kotlin/kora-kotlin-guide-cache-app` и ещё ~8 | +| `ksp.org.jetbrains.kotlin.analysis.api.lifetime.KaInvalidLifetimeOwnerAccessException` | `examples/kotlin/kora-kotlin-crud`, оба `kora-kotlin-crud-submodule-*-api` | +| `tools.jackson.core.exc.JacksonIOException: Stream closed` | `examples/kotlin/kora-kotlin-camunda-engine`, `guides/kotlin/kora-kotlin-guide-database-cassandra-app` | +| `java.lang.NullPointerException` | `examples/kotlin/kora-kotlin-grpc-server`, оба `guide-grpc-server*` | +| `java.lang.IllegalStateException: Required value was null` | `examples/kotlin/kora-kotlin-kafka` | +| `java.lang.ClassCastException: java.lang.String cannot be cast to com.google.devtools.ksp.symbol.KSType` | `examples/kotlin/kora-kotlin-resilient` | + +### Steps to reproduce + +```shell +export JAVA_HOME= +./gradlew :examples:kotlin:kora-kotlin-helloworld:kspKotlin --no-build-cache --stacktrace +``` + +### Expected behavior + +Процессор либо обрабатывает конструкцию, либо сообщает внятную ошибку с файлом, строкой и символом. + +### Actual behavior + +`e: [ksp] <исключение>` без указания места в коде; задача падает. + +### Investigation notes + +- Версии совпадают с теми, на которых собран сам фреймворк: Kotlin `2.4.10`, KSP `2.3.11` (`../kora/gradle/libs.versions.toml`). Рассинхрон версий как причина исключён. +- **Подтверждено, что минимум один подвид — реакция на немигрированный код, а не самостоятельный баг.** `ClassCastException: String → KSType` в `kora-kotlin-resilient` возникает потому, что код всё ещё использует строковый API `@CircuitBreaker("pet")`, тогда как в 2.0 атрибут имеет тип класса. То есть первопричина — код примера; дефектом фреймворка здесь является **отсутствие диагностики** (падение вместо сообщения «ожидался класс, получена строка»). +- Гипотеза (не проверена): часть подвидов `NoSuchElementException: No TypeParameter found` также вызвана несоответствием кода новому API и исчезнет после семантической миграции модуля. Проверяется по мере миграции — каждый модуль перепроверяется после правок, и запись уточняется. +- **Гипотеза опровергнута для `NoSuchElementException`.** Оба подвида воспроизводятся на корректном коде 2.0 и оказались самостоятельными дефектами фреймворка, разобранными отдельными записями ниже: `index T` — построение текста `UnresolvedDependencyException` (исправлено), `index E`/`index K` — потеря аргументов типа в `KspCommonUtils.fixPlatformType` (исправлено). После этих двух фиксов число падающих Kotlin-задач упало с 37 до 31 без единой правки в примерах. + +### Suspected cause + +Процессоры читают атрибуты аннотаций и параметры типов без валидации формы входных данных, полагаясь на структуру кода 2.0. + +### Workaround + +Мигрировать код модуля на API 2.0; для диагностики запускать `kspKotlin` отдельно с `--stacktrace`. + +### Proposed fix + +Обернуть чтение атрибутов аннотаций и разрешение параметров типов в проверки с выдачей `KSPLogger.error(...)`, указывающей символ. Отдельно — разобрать каждый подвид, который воспроизводится на корректном коде 2.0. + +### Resolution + +Не закрыт. + +--- + +## Issue: неразрешимая зависимость в одном месте графа даёт сообщение про совсем другой компонент + +- Status: Confirmed (механизм подтверждён контролируемым экспериментом, не исправляется — нужен дизайн вывода) +- Severity: Minor — стоимость в потерянном времени на отладку, не в поведении +- Type: Framework bug (диагностика) +- Language: Java (Kotlin не проверялся) +- Runtime: JVM +- Component: разрешение зависимостей `@KoraApp` / `http-server-common` +- Affected framework module: `http/http-server-common`, `kora-app-annotation-processor` +- Affected example modules: `examples/java/kora-java-http-server` +- Framework commit: `66800169f` +- Related migration guide: `KORA_2_JAVA_MIGRATION_GUIDE.md` +- Related fix branch / PR: пока нет + +### Description + +Когда в графе есть компонент с принципиально неразрешимой зависимостью, сообщение об ошибке может указывать на совсем другой, исправный компонент — тот, чей шаблонный кандидат пробовался при переборе. На реальном модуле это выглядело как требование `JsonWriter>`, которого никто не запрашивал, тогда как истинной причиной был реактивный контроллер в другом файле. + +### Minimal reproduction + +```java +@Component +@HttpController +public final class JsonGetController { + + @Json + public record HelloWorldResponse(String greeting) {} + + @Json + @HttpRoute(method = HttpMethod.GET, path = "/json/entity") + public HttpResponseEntity getEntity() { + return HttpResponseEntity.of(201, new HelloWorldResponse("Hello world")); + } +} +``` + +### Expected behavior + +Должен использоваться уже существующий во фреймворке фабричный метод + +```java +@Json +@DefaultComponent +default HttpServerResponseMapper> jsonHttpResponseEntityHttpServerResponseMapper(JsonWriter writer) +``` + +то есть требоваться `JsonWriter` — он генерируется по `@Json` на record. + +### Actual behavior + +``` +No component found for dependency: JsonWriter> +``` + +### Investigation notes + +В `io.koraframework.http.server.common.response.mapper.HttpServerResponseMapperModule` объявлены **два** подходящих `@Json @DefaultComponent` фабричных метода: + +```java +default HttpServerResponseMapper> jsonHttpResponseEntityHttpServerResponseMapper(JsonWriter writer) // специализированный +default HttpServerResponseMapper jsonHttpServerResponseMapper(JsonWriter writer) // общий +``` + +Оба подходят под запрошенный тип `HttpServerResponseMapper>`: специализированный при `T = HelloWorldResponse`, общий при `T = HttpResponseEntity`. + +Первоначально это было записано как ошибка выбора фабрики: процессор печатал `Required at: HttpServerResponseMapperModule#jsonHttpServerResponseMapper(JsonWriter)`, из чего следовало, что выбран общий фабричный метод вместо специализированного. + +**Этот вывод опровергнут.** В том же модуле был второй дефект — `ReactorController`, возвращавший `Mono`, для которого в 2.0 нет маппера. После удаления этого контроллера ошибка про `JsonWriter>` исчезла сама, хотя `@Json`-метод с `HttpResponseEntity` остался нетронутым и модуль собирается. Значит специализированный маппер выбирается корректно, а сообщение было побочным эффектом другой, несвязанной неразрешённости. + +Механизм (по коду `GraphBuilder`, строки 244–275): когда под запрос подходят несколько шаблонных кандидатов, строитель форкает граф на каждый и оставляет тот, что собрался. Ключевая деталь — `fork.build()` достраивает **весь оставшийся граф**, а не только поддерево спорной зависимости. Поэтому любая неразрешимость где угодно ниже по графу роняет все форки сразу. Если не собрался ни один — бросается исключение одного из форков, остальные уходят в `addSuppressed`. Пользователь видит первое из них и получает указание на компонент, который с проблемой не связан. + +**Подтверждено вторым, контролируемым экспериментом** на `guides/java/kora-java-guide-openapi-http-server-advanced-app`. Там из трёх ошибок компиляции две были про `JsonWriter>` и `JsonWriter>`, а третья — про недостающий `HttpServerParameterReader` в совершенно другой операции (multipart-загрузка). Добавление одного компонента-заглушки, закрывающего только третью ошибку, убрало все три. Это ровно предсказание механизма: настоящая неразрешённость была одна, две остальные — обломки перебора форков. + +Урок для миграции: **сначала убирать все заведомо неразрешимые компоненты** (опирающиеся на удалённую функциональность), и только потом анализировать оставшиеся ошибки графа. + +### Workaround + +Не требуется: после устранения настоящей причины сообщение исчезает. При отладке не доверяйте указанному в ошибке компоненту, если в модуле есть код, опирающийся на удалённую функциональность. + +### Proposed fix + +При провале всех форков выводить не одно исключение, а сводку по всем кандидатам — что именно не сошлось у каждого, чтобы была видна исходная неразрешённость, а не случайный форк. + +### Resolution + +Не закрыт. Приоритет понижен до Minor: поведение фреймворка корректно, страдает только диагностика. + +--- + +## Issue: сгенерированные OpenAPI-интерфейсы клиента недоступны вне своего пакета + +- Status: **Fixed** +- Severity: Major +- Type: Framework bug | Migration blocker +- Language: Java (в Kotlin-режимах генератора проявления не проверялись) +- Runtime: JVM +- Component: `openapi-generator` (генератор Kora), режим `java-client` +- Affected framework module: `openapi/openapi-generator` +- Affected example modules: `examples/java/kora-java-openapi-generator-http-client`, `guides/java/kora-java-guide-openapi-http-client-app` +- Framework commit: база `66800169f`, фикс `a5e7694e6` +- Related fix branch: `fix/openapi-client-api-interface-public` — [#793](https://github.com/kora-projects/kora/pull/793) + +### Description + +Сгенерированные API-интерфейсы клиента оказываются package-private, из-за чего код приложения их не видит: + +``` +error: PetApi is not public in io.koraframework.example.openapi.petV2.api; cannot be accessed from outside package +error: UsersApi is not public in io.koraframework.guide.openapi.httpclient.user.api; cannot be accessed from outside package +``` + +### Investigation notes + +`ClientApiGenerator` строит интерфейс через `TypeSpec.interfaceBuilder(...)` без единого модификатора. +JavaPoet не добавляет `public` сам, поэтому интерфейс получается с доступом по умолчанию. +Серверный генератор той же проблемы не имеет — там модификатор проставлен явно. + +### Implemented fix + +Добавлен `.addModifiers(Modifier.PUBLIC)` в `ClientApiGenerator`; то же самое для вложенных типов +маппера запросов в `ClientRequestMapperGenerator`. + +### Test coverage + +`HttpClientJavaOpenapiTest#clientApiInterfaceIsPublic` — проверяет, что в сгенерированном исходнике +есть `public interface `. + +### Compatibility impact + +Обратно совместимо: расширение видимости, ничего не ломает. + +### Validation + +`examples/java/kora-java-openapi-generator-http-client` компилируется; тесты PetV2 проходят +(по PetV3 остаётся открытый вопрос про порядок схем авторизации, см. `KORA_2_MIGRATION_STATUS.md`). + +--- + +## Issue (внешний инструмент, не Kora): задача OpenAPI-генератора отдаёт устаревший результат из build-кеша + +- Status: Confirmed +- Severity: Major +- Type: Migration blocker +- Language: Java, Kotlin +- Runtime: JVM +- Component: Gradle-плагин `org.openapi.generator` 7.23.0 (не входит в Kora) + +### Description + +После смены пакета генерации (`ru.tinkoff.kora.*` → `io.koraframework.*`) задача генерации отдаёт из build-кеша вывод, сгенерированный до миграции, и компиляция падает сотнями `package ru.tinkoff.kora.* does not exist` в файлах, которых нет в исходниках. + +### Steps to reproduce + +1. Собрать модуль до миграции пакетов (кеш заполняется). +2. Сменить `apiPackage` / `modelPackage` / `invokerPackage`. +3. `./gradlew :examples:java:kora-java-crud:clean :examples:java:kora-java-crud:test` — падает. +4. Тот же прогон с `--no-build-cache` — успешен. + +### Suspected cause + +Ключ кеша задачи не учитывает пакетные опции; вдобавок задача не очищает `outputDir`, поэтому старые и новые файлы сосуществуют в одном source set. + +### Workaround + +Первый прогон после смены пакетов — `clean` + `--no-build-cache`. Это зафиксировано в обоих языковых гайдах и в `KORA_2_MIGRATION_STATUS.md`. + +### Resolution + +Обходной путь достаточен; изменение сборки примеров не требуется. Если проблема будет мешать в CI, рассмотреть явную очистку `outputDir` в `doFirst` задачи генерации. + +--- + +## Не дефект: реактивные контроллеры удалены из HTTP-сервера + +- Status: Closed (поведение по замыслу 2.0) +- Type: Removed functionality +- Affected example modules: `examples/java/kora-java-http-server` (`ReactorController`) + +Метод контроллера, возвращающий `Mono`, не собирается: + +``` +No component found for dependency: + HttpServerResponseMapper> (no tags) +``` + +Проверено по исходникам: в `http/http-server-common/src/main` нет ни одного упоминания `reactor`/`Mono`, то есть мапперы для реактивных возвратов фреймворком не предоставляются. Это соответствует синхронной модели 2.0, а не дефекту. Пример, демонстрирующий реактивный контроллер, подлежит переработке или удалению с отметкой в обоих языковых гайдах — решение фиксируется в `KORA_2_MIGRATION_STATUS.md`. + +--- + +## Issue: построение текста ошибки DI роняет разрешение шаблонных компонентов + +- Status: Fixed +- Severity: Blocker +- Type: Framework bug +- Language: Kotlin +- Runtime: Both +- Component: `kora-app-symbol-processor`, `UnresolvedDependencyException` +- Affected framework module: `core/kora-app-symbol-processor` +- Affected example modules: `examples/kotlin/kora-kotlin-helloworld`, `guides/kotlin/kora-kotlin-guide-cache-app` и ещё ~8 модулей, падавших с `No TypeParameter found for index T` +- Framework commit: `66800169f` +- Related migration guide: `KORA_2_KOTLIN_MIGRATION_GUIDE.md` +- Related fix branch: `fix/ksp-unresolved-dependency-message-generic-factory` (локально, коммит `12b5ef5a4`) — [#796](https://github.com/kora-projects/kora/pull/796) +- Related PR: [#796](https://github.com/kora-projects/kora/pull/796) + +### Description + +`UnresolvedDependencyException` собирает весь текст диагностики прямо в конструкторе. Типы параметров фабричного метода рендерились `toTypeName()` с пустым `TypeParameterResolver`. Для шаблонной фабрики вида `fun wrapper(holder: Holder): Wrapper` параметр ссылается на собственный параметр типа метода, и KotlinPoet бросал `NoSuchElementException: No TypeParameter found for index T`. + +Это не только потерянная диагностика. `GraphBuilder` бросает это исключение как **штатный поток управления**: когда claim могут удовлетворить несколько шаблонов, он форкается по каждому и ловит `UnresolvedDependencyException` для тех, что не разрешились (`GraphBuilder.kt:246-259`). Построение исключения для отброшенного форка роняло KSP целиком — приложения с корректно разрешимым графом не компилировались, а сообщение не называло ни зависимости, ни места. + +### Steps to reproduce + +```shell +./gradlew :examples:kotlin:kora-kotlin-helloworld:kspKotlin --no-build-cache --stacktrace +``` + +### Minimal reproduction + +```kotlin +@KoraApp +interface ExampleApplication { + class Holder(val value: T) + class Wrapper(val value: T) + + fun wrapper(holder: Holder): Wrapper = Wrapper(holder.value) + + @Root + fun root(wrapper: Wrapper): Any = "" +} +``` + +### Relevant output + +``` +e: [ksp] java.util.NoSuchElementException: No TypeParameter found for index T + at com.squareup.kotlinpoet.ksp.TypeParameterResolver$Companion$EMPTY$1.get(TypeParameterResolver.kt:48) + ... + at io.koraframework.kora.app.ksp.exception.UnresolvedDependencyException$Companion.getRequestedMessage(UnresolvedDependencyException.kt:112) + at io.koraframework.kora.app.ksp.GraphBuilder.build(GraphBuilder.kt:320) +``` + +### Suspected cause + +Подтверждено: `toTypeName()` без резолвера параметров типа в двух местах формирования сообщения. + +### Proposed fix + +Строить `TypeParameterResolver` из параметров типа модуля и фабричного метода и передавать его в оба места. Неиспользуемая приватная копия `getRequestedMessage` содержала тот же дефект и удалена. + +Отдельным улучшением (в фикс не входит, отмечено в описании PR): сообщение стоит вычислять лениво — сейчас полная диагностика строится для каждого отброшенного форка. + +### Resolution + +Исправлено. Регрессионный тест `DependencyTest#testUnresolvedDependencyOfTemplateFactoryIsReportedAsDiagnostic` без фикса падает исходным `NoSuchElementException`. + +--- + +## Issue: `fixPlatformType` теряет аргументы типа у Java-коллекций + +- Status: Fixed +- Severity: Blocker +- Type: Framework bug +- Language: Kotlin +- Runtime: Both +- Component: `symbol-processor-common`, `KspCommonUtils.fixPlatformType` +- Affected framework module: `core/symbol-processor-common` +- Affected example modules: `examples/kotlin/kora-kotlin-camunda-zeebe-worker` и все Kotlin-модули, тянущие Java-определённые модули с коллекциями в конфигурации +- Framework commit: `66800169f` +- Related migration guide: `KORA_2_KOTLIN_MIGRATION_GUIDE.md` +- Related fix branch: `fix/ksp-platform-type-drops-generic-arguments` (локально, коммит `d140f0a29`) — [#797](https://github.com/kora-projects/kora/pull/797) +- Related PR: [#797](https://github.com/kora-projects/kora/pull/797) + +### Description + +`KspCommonUtils.fixPlatformType` приводит гибкий (flexible) тип, пришедший из Java, к неизменяемому Kotlin-аналогу. В ветке, где ни один аргумент типа не потребовал правки, вызывался `asType(listOf())`. В KSP это **не** «без аргументов»: подставляются собственные параметры типа декларации, и `List` превращался в `List`. + +Ветка срабатывает ровно для `@NullMarked` Java-модулей и сгенерированных компонентов: сама коллекция остаётся гибкой по изменяемости, а её аргумент уже не платформенный — то есть `changed == false`. Полученный claim (`ConfigValueMapper>`) KotlinPoet отрендерить не может, и KSP падал с `NoSuchElementException: No TypeParameter found for index E`, не называя ни компонента, ни зависимости. + +### Steps to reproduce + +```shell +./gradlew :examples:kotlin:kora-kotlin-camunda-zeebe-worker:clean :examples:kotlin:kora-kotlin-camunda-zeebe-worker:kspKotlin --no-build-cache --stacktrace +``` + +### Relevant output + +``` +e: [ksp] java.util.NoSuchElementException: No TypeParameter found for index E + at com.squareup.kotlinpoet.ksp.TypeParameterResolver$Companion$EMPTY$1.get(TypeParameterResolver.kt:48) + ... + at io.koraframework.kora.app.ksp.component.ComponentDependencyHelper.parseClaim(ComponentDependencyHelper.kt:113) + at io.koraframework.kora.app.ksp.GraphBuilder.build(GraphBuilder.kt:232) +``` + +### Investigation notes + +Первая попытка — расширить `catch (e: IllegalArgumentException)` в `parseClaim` до `NoSuchElementException` — **отвергнута**: она превращает падение в `WARNING`, KSP откладывает раунд, сборка завершается «успешно», но `ApplicationGraph` не генерируется, и пользователь получает бессмысленное `Unresolved reference 'ApplicationGraph'`. Маскировка хуже краша, изменение откачено. + +### Proposed fix + +В ветке `changed == false` использовать уже собранный список аргументов: `type.immutableDeclaration(resolver).asType(args)`. + +### Resolution + +Исправлено. Регрессионный тест `ModuleTest#testJavaModuleKeepsGenericArgumentsOfPlatformTypes` строит граф поверх `@NullMarked` Java-модуля, скомпилированного в тестовый classpath, и без фикса падает исходным исключением. Воспроизведение потребовало именно **class-файла** и `@NullMarked`: Java-исходник в той же компиляции даёт платформенный аргумент, и дефектная ветка не срабатывает. + +--- + +## Issue: KSP-воркер Zeebe не поднимает BPMN-ошибку для `JobWorkerException` + +- Status: Fixed +- Severity: Critical +- Type: Regression +- Language: Kotlin +- Runtime: Both +- Component: `camunda-zeebe-worker-symbol-processor` +- Affected framework module: `experimental/camunda-zeebe-worker-symbol-processor` +- Affected example modules: `examples/kotlin/kora-kotlin-camunda-zeebe-worker` +- Framework commit: `66800169f` +- Related migration guide: `KORA_2_KOTLIN_MIGRATION_GUIDE.md` +- Related fix branch: `fix/zeebe-worker-ksp-exception-throws-bpmn-error` (локально, коммит `8c792983b`) — [#798](https://github.com/kora-projects/kora/pull/798) +- Related PR: [#798](https://github.com/kora-projects/kora/pull/798) + +### Description + +Тот же дефект, что исправлен для Java-процессора (PR 5), присутствует и в KSP-генераторе: `catch (e: JobWorkerException) { throw e }` вместо `client.newThrowErrorCommand(job)`. Джоба падает и ретраится до исчерпания бюджета, граничное событие ошибки BPMN не срабатывает — идентичное Kotlin-приложение ведёт себя иначе, чем Java-аналог. + +### Steps to reproduce + +`./gradlew :examples:kotlin:kora-kotlin-camunda-zeebe-worker:test` — процесс не доходит до завершения, в логе повторяется `Zeebe JobWorker failed Job`. + +### Resolution + +Исправлено симметрично Java-версии. Регрессионный тест `ZeebeWorkerTests#workerJobWorkerExceptionIsTurnedIntoBpmnError`; без фикса исключение выходит из `handle()`. После фикса тест примера проходит. + +--- + +## Issue: KSP-процессоры падают при параллельном прогоне Gradle (`JacksonIOException: Stream closed`) + +- Status: Confirmed (не исправлен) +- Severity: Major +- Type: Framework bug +- Language: Kotlin +- Runtime: JVM +- Component: KSP symbol processors +- Affected example modules: непостоянный набор, до 19 задач за прогон +- Framework commit: `66800169f` +- Related fix branch / PR: нет + +### Description + +При `./gradlew ... --continue` с параллельным выполнением часть задач `kspKotlin`/`kspTestKotlin` падает с `tools.jackson.core.exc.JacksonIOException: Stream closed`. Набор упавших модулей меняется от прогона к прогону, а каждый из них по отдельности собирается успешно. + +### Steps to reproduce + +```shell +./gradlew <все kotlin testClasses> --continue --no-build-cache # 19 падений +./gradlew <те же задачи> --continue --no-build-cache --max-workers=1 # 0 падений +``` + +### Investigation notes + +Прямое измерение: тот же набор задач в один поток дал **0** таких ошибок и 24 реальных падения вместо 37. То есть это гонка, а не дефект модулей. Показательный случай — `guides/kotlin/kora-kotlin-guide-http-server-advanced-app`: в параллельном прогоне падает, отдельной сборкой проходит и его тесты зелёные. + +### Suspected cause + +Разделяемое между воркерами KSP состояние Jackson (генератор/парсер), закрываемое одним воркером, пока другой ещё пишет. Точное место не локализовано. + +### Workaround + +Мерить и чинить миграцию прогоном с `--max-workers=1`. Все цифры в `KORA_2_MIGRATION_STATUS.md` получены именно так. + +--- + +## Issue: `ForwardingServerBuilder<*>` не разрешается в KSP-графе gRPC-сервера + +- Status: Fixed (`fix/ksp-template-match-star-projection`) +- Severity: Blocker +- Type: Framework bug +- Language: Kotlin +- Runtime: JVM +- Component: `kora-app-symbol-processor`, `grpc-server` +- Affected example modules: `examples/kotlin/kora-kotlin-grpc-server`, `guides/kotlin/kora-kotlin-guide-grpc-server-app`, `guides/kotlin/kora-kotlin-guide-grpc-server-advanced-app` +- Framework commit: `66800169f` +- Related fix branch: частично разблокировано `fix/ksp-template-match-star-projection` + +### Description + +После исправления NPE на star-проекции сборка доходит до внятной диагностики: + +``` +No component found for dependency: + io.grpc.ForwardingServerBuilder<*> (no tags) + +Required at: + GrpcServerFactoryModule#grpcServer(ValueOf>, ValueOf) +``` + +При этом тот же `GrpcServerFactoryModule` объявляет поставщика `@DefaultComponent @Tag(Tag.Factory.class) ForwardingServerBuilder grpcServerBuilder(...)`. Java-аналог `examples/java/kora-java-grpc-server` этот же модуль разрешает и компилируется. + +### Investigation notes + +- Claim в диагностике помечен `(no tags)`, тогда как и метод-потребитель, и метод-поставщик объявлены с `@Tag(Tag.Factory.class)`. Совпадает ли это с ожидаемым разрешением `Tag.Factory` внутри фабричного модуля — не проверено. +- Java-путь работает, то есть расхождение именно в KSP. +- Не проверено, влияет ли на это изменение `fillMap` из `fix/ksp-template-match-star-projection` (до него это место падало с NPE, поэтому «раньше работало» сказать нельзя). + +### Resolution + +Закрыт фиксом `fix/ksp-template-match-star-projection`. Причина была именно в том, что KSP считал +star-проекцию `` переменной типа: `hasGenericVariable()` при `KSTypeArgument.type == null` +возвращал `true`, из-за чего конкретный `ForwardingServerBuilder` уезжал в множество шаблонов и +не находился как обычный компонент. Java-процессор в той же ситуации возвращает `false` +(`TypeParameterUtils#visitWildcard`) — отсюда и расхождение языков. + +После фикса все Kotlin-модули с gRPC компилируются; их тесты закрыл отдельный дефект +`fix/grpc-server-keeps-process-alive` (запись ниже). + +--- + +## Issue: тест gRPC-сервера не может подключиться (Java, `UNAVAILABLE`) + +- Status: Closed — дубликат +- Severity: Major +- Type: Migration blocker +- Language: Java +- Runtime: JVM +- Component: `grpc-server` +- Affected example modules: `examples/java/kora-java-grpc-server` +- Framework commit: `66800169f` + +### Description + +`examples/java/kora-java-grpc-server` компилируется, но `GrpcServerTests#createUser` падает с `StatusRuntimeException: UNAVAILABLE / Connection refused` — сервер не слушает порт. Обнаружено при прогоне тестов Java-модулей, которые ранее только компилировались. + +### Resolution + +Оказался тем же дефектом, что описан ниже как «gRPC-приложение завершается сразу после старта +сервера»: сервер стартовал и процесс немедленно завершался, поэтому подключиться было не к чему. +Закрыт фиксом `fix/grpc-server-keeps-process-alive`. Конфигурация `grpcServer.port` ни при чём. + +--- + +## Issue: gRPC-приложение завершается сразу после старта сервера + +- Status: Fixed +- Severity: Blocker +- Type: Regression +- Language: Java, Kotlin (общий runtime-модуль) +- Runtime: JVM +- Component: `application-graph` +- Affected framework module: `core/application-graph`, `http/http-server-undertow` (проявлялось на `grpc/grpc-server`) +- Affected example modules: `examples/java/kora-java-grpc-server`, `examples/kotlin/kora-kotlin-grpc-server`, `guides/kotlin/kora-kotlin-guide-grpc-server-app`, `guides/kotlin/kora-kotlin-guide-grpc-server-advanced-app` +- Framework commit: `66800169f` +- Related fix branch: `fix/grpc-server-keeps-process-alive` (`f3385b3c6`) — [#807](https://github.com/kora-projects/kora/pull/807) + +### Description + +Приложение, у которого единственный сервер — gRPC, стартует и немедленно завершает работу. Тесты во всех четырёх модулях падают с `StatusRuntimeException: UNAVAILABLE / Connection refused`, потому что контейнер к моменту запроса уже остановлен. + +### Steps to reproduce + +```shell +./gradlew :examples:java:kora-java-grpc-server:distTar +docker build -t kora-grpc-manual examples/java/kora-java-grpc-server +docker run -d --name t -e GRPC_PORT=8090 -p 18090:8090 kora-grpc-manual +docker inspect -f '{{.State.Status}} exit={{.State.ExitCode}}' t # exited exit=0 +``` + +### Relevant output + +В логе контейнера — только исключение фонового потока перезагрузки конфигурации; строк `Application initialized` и `gRPC Server started` нет (асинхронный аппендер не успевает сброситься): + +``` +Exception in thread "config-reload" java.lang.IllegalStateException: Graph node value was not initialized: + [#0] interface io.koraframework.config.common.origin.ConfigOrigin (@Tag(ApplicationConfig)) (0 dependencies) + at io.koraframework.config.common.ConfigWatcher.watchJob(ConfigWatcher.java:75) +``` + +### Investigation notes + +- **Доказано:** контейнер завершается с кодом `0`. При неудачном старте `KoraApplication.run` пишет ошибку и делает `System.exit(-1)`, то есть код был бы `255`. Ноль означает штатное завершение JVM — не осталось ни одного non-daemon потока. +- `KoraApplication.run` не блокирует: инициализирует граф, вешает shutdown hook и возвращает управление. Процесс живёт ровно столько, сколько живут потоки компонентов. +- `GrpcServer.init()` вызывает `server.start()` и выходит; `awaitTermination` вызывается только в `release()`. +- В 2.0 транспорт сменился на `OkHttpServerBuilder` с `directExecutor()` и `VirtualThreadExecutorTransportFilter` (`GrpcServerFactoryModule.grpcServerBuilder`). Виртуальные потоки — daemon, поэтому удерживать JVM нечему. +- HTTP-примеры выживают по другой причине: XNIO-воркер Undertow работает на платформенных non-daemon потоках. То есть время жизни процесса в 2.0 держится на транспорте, а не на явном контракте. +- Отдельно замечено: `ConfigWatcher.watchJob` читает узел графа до его инициализации. На жизненный цикл не влияет, но исключение в фоновом потоке шумит в логе. + +### Suspected cause + +Смена транспорта gRPC на виртуальные потоки без явного удержания процесса. + +### Implemented fix + +Здесь было два ошибочных вывода подряд, и оба стоит зафиксировать. + +Первый: «нужен выбор дизайна на стороне фреймворка» — опровергнут тем, что `XnioLifecycle` +уже держал non-daemon поток ровно для этого. + +Второй, сделанный из первого: раз `XnioLifecycle` так делает — значит это контракт, и gRPC-серверу +надо его выполнить. Фикс был сделан так и отправлен. Мейнтейнер отклонил, и проверка +показала, что он прав: тот же приём продублирован в **пяти** местах — `XnioLifecycle`, +`KafkaUtils:23`, `ThreadPoolSchedulingJdkExecutor:45`, `KoraThreadPoolJobExecutor:36` и новый +в `GrpcServer`. То, что выглядело контрактом, оказалось повторяющимся обходом. + +Итоговый фикс: удержание процесса — свойство приложения, а не отдельного сервера. +Non-daemon поток живёт в `KoraApplication#run` (`ApplicationKeepAlive`) и освобождается из shutdown-хока. +`GrpcServer` не меняется вовсе, а `XnioLifecycle` больше не просит non-daemon потоков. + +### Test coverage + +`ApplicationKeepAliveTest` проверяет механизм: поток non-daemon и жив во время работы, +умирает после `stop()`. + +Сам `KoraApplication#run` модульным тестом не покрывается, и это проверено на практике: он +отпускает поток только из shutdown-хока, поэтому вызов в тесте оставляет non-daemon поток +и тестовая JVM не завершается — первая версия теста повесила Gradle worker насмерть. +Это ровно то поведение, ради которого фикс и сделан. + +Сквозная проверка — примеры как процессы: gRPC-приложение без хака в `GrpcServer` живёт +более 12 с с единственным non-daemon потоком `kora-application` и штатно гаснет по SIGTERM; +Undertow-приложение без флагов в XNIO отвечает на readiness за 1 с и корректно останавливает воркер. + +### Остаётся открытым + +`kafka`, `scheduling-jdk` и `camunda-engine-bpmn` по-прежнему держат свои non-daemon потоки. +Они могут существовать не ради времени жизни процесса, поэтому решение по каждому — +за мейнтейнерами; вопрос задан в PR. + +### Workaround + +Не требуется. + +--- + +## Issue: метаданные трёх модулей названы `reflection-config.json` и не читаются native-image + +- Status: Fixed +- Severity: Major +- Type: Framework bug +- Language: Java +- Runtime: GraalVM native-image +- Component: метаданные GraalVM +- Affected framework module: `telemetry/micrometer-module`, `grpc/grpc-server`, `kafka/kafka` +- Affected example modules: все `examples/graalvm/*` +- Related fix branch: `fix/graalvm-reflect-config-file-name` (`274e3b039`) — [#814](https://github.com/kora-projects/kora/pull/814) + +### Description + +`native-image` читает из `META-INF/native-image///` файлы со строго определёнными именами +(`native-image.properties`, `reflect-config.json`, `resource-config.json`, и т.д.). Три модуля Kora +поставляют файл под именем `reflection-config.json` — такого имени сборщик не ищет, поэтому +регистрации никогда не применялись. + +### Steps to reproduce + +1. Добавить запись в `micrometer-module/.../reflection-config.json`. +2. Собрать `nativeCompile` любого модуля `examples/graalvm/*`. +3. Запись не даёт эффекта; перенос той же записи в `reflect-config.json` — даёт. + +### Investigation notes + +Сначала гипотеза была **ошибочно отвергнута**: в образе работали `system_cpu_usage` и +`process_cpu_usage`, из чего был сделан вывод, что файл читается. Решающий эксперимент — добавить +метод в старый файл и убедиться, что эффекта нет, — показал обратное: те две метрики +регистрируются не из этого файла. + +### Implemented fix + +Переименование в трёх модулях + добавлен `getProcessCpuTime` (см. следующую запись). + +### Test coverage + +Имя файла значимо только для сборщика образа — JVM-тест не различает состояния. Доказательство — +`GET /metrics` на трёх native-модулях: 500 и 670 байт ошибки до, 200 и ~4,6 КБ после. + +--- + +## Issue: `/metrics` в native-образе отвечает 500 из-за `getProcessCpuTime` + +- Status: Fixed +- Severity: Major +- Type: Framework bug +- Language: Java +- Runtime: GraalVM native-image +- Component: `micrometer-module`, метаданные GraalVM +- Affected framework module: `telemetry/micrometer-module` +- Affected example modules: все `examples/graalvm/*` +- Related fix branch: `fix/graalvm-reflect-config-file-name` (`274e3b039`) — [#814](https://github.com/kora-projects/kora/pull/814) + +### Description + +micrometer 1.17 добавил метрику `process.cpu.time`, которая читает +`com.sun.management.OperatingSystemMXBean.getProcessCpuTime()` рефлексией. В списке Kora были +только `getCpuLoad` и `getProcessCpuLoad`, поэтому эндпоинт метрик отвечает 500 с текстом +`MissingReflectionRegistrationError` вместо данных. + +### Test coverage + +См. предыдущую запись: проверка сквозная, на трёх native-модулях. + +--- + +## Issue: Undertow в native-image не находит XNIO-провайдер + +- Status: Fixed +- Severity: Blocker +- Type: Framework bug +- Language: Java +- Runtime: GraalVM native-image +- Component: `http-server-undertow`, метаданные GraalVM +- Affected framework module: `http/http-server-undertow` +- Affected example modules: `examples/graalvm/kora-java-graalvm-crud-jdbc`, `examples/graalvm/kora-java-graalvm-crud-cassandra`, `examples/graalvm/kora-java-graalvm-kafka` +- Related fix branch: `fix/graalvm-undertow-xnio-metadata` (`587ef109f`) — [#811](https://github.com/kora-projects/kora/pull/811) + +### Description + +Любое приложение с Undertow, собранное в native-image, не стартует. + +### Relevant output + +``` +IllegalArgumentException: XNIO001001: No XNIO provider found + at org.xnio.Xnio.doGetInstance(Xnio.java:261) + at io.koraframework.http.server.undertow.XnioLifecycle.init(XnioLifecycle.java:65) +``` + +### Investigation notes + +Сообщение вводит в заблуждение: `NioXnioProvider` в образе **есть** (проверено `strings`), и +ресурс `META-INF/services/org.xnio.XnioProvider` тоже. Отдельный native-пробник показал настоящую +причину: `ServiceConfigurationError: Provider org.xnio.nio.NioXnioProvider could not be instantiated` +→ `Invalid logger interface org.xnio.nio.Log (implementation not found)`. jboss-logging грузит +сгенерированный `<интерфейс>_$logger` рефлексией. `Xnio.doGetInstance` оборачивает итерацию в +`catch (Throwable)`, поэтому настоящая ошибка теряется. + +### Test coverage + +Сквозная: `/system/readiness` отвечает `OK` за 886 мс после фикса, до — граф не инициализируется. + +--- + +## Issue: HikariCP не находит micrometer-трекер в native-образе + +- Status: Fixed +- Severity: Blocker +- Type: Framework bug +- Language: Java +- Runtime: GraalVM native-image +- Component: `database-jdbc`, метаданные GraalVM +- Affected framework module: `database/database-jdbc` +- Affected example modules: `examples/graalvm/kora-java-graalvm-crud-jdbc` +- Related fix branch: `fix/graalvm-jdbc-hikari-micrometer-metadata` (`18aa0fa2d`) — [#812](https://github.com/kora-projects/kora/pull/812) + +### Description + +`JdbcDataSource` при включённых driver-метриках зовёт `setMetricRegistry`, Hikari создаёт +`MicrometerMetricsTrackerFactory` по имени — и класса нет в образе. + +### Relevant output + +``` +RuntimeException: Failed to load class com.zaxxer.hikari.metrics.micrometer.MicrometerMetricsTrackerFactory + at com.zaxxer.hikari.pool.HikariPool.setMetricRegistry(HikariPool.java:292) + at io.koraframework.database.jdbc.JdbcDataSource.(JdbcDataSource.java:43) +``` + +### Test coverage + +Сквозная на `kora-java-graalvm-crud-jdbc`: полный CRUD-цикл после фикса, ошибка инициализации до. + +--- + +## Issue: Caffeine-кэш без статистики падает в native-образе + +- Status: Fixed +- Severity: Blocker +- Type: Framework bug +- Language: Java +- Runtime: GraalVM native-image +- Component: `cache-caffeine`, метаданные GraalVM +- Affected framework module: `cache/cache-caffeine` +- Affected example modules: `examples/graalvm/kora-java-graalvm-crud-jdbc` +- Related fix branch: `fix/graalvm-caffeine-node-metadata` (`28a2b38b2`) — [#813](https://github.com/kora-projects/kora/pull/813) + +### Description + +Общие метаданные GraalVM регистрируют узел `PSMS` под условием `"typeReached": "SSSMS"` — это класс +кэша с `recordStats()`. Kora включает статистику только при включённых метриках, а в 2.0 они по +умолчанию выключены — строится `SSMS`, и условие не срабатывает. + +### Investigation notes + +Соответствие конфигураций и генерируемых классов снято программой на JVM (8 комбинаций +`expireAfterAccess` × `expireAfterWrite` × `recordStats`), а не угадано по именам. + +### Test coverage + +Сквозная на `kora-java-graalvm-crud-jdbc` (кэш Caffeine, метрики выключены). + +--- + +## Issue: сборка native-image с Cassandra-драйвером падает на этапе анализа + +- Status: Fixed +- Severity: Blocker +- Type: Framework bug +- Language: Java +- Runtime: GraalVM native-image +- Component: `database-cassandra`, метаданные GraalVM +- Affected framework module: `database/database-cassandra` +- Affected example modules: `examples/graalvm/kora-java-graalvm-crud-cassandra` +- Related fix branch: `fix/graalvm-cassandra-guava-comparator-init` (`d2fd730ca`) — [#815](https://github.com/kora-projects/kora/pull/815) + +### Description + +Объект shaded-компаратора драйвера оказывается в image heap, хотя его тип инициализируется в рантайме; +сборщик прерывает сборку. Путь достижимости: `HashedWheelTimeout.` → +`AtomicIntegerFieldUpdater.newUpdater` → декодер метаданных → компаратор из `SniEndPoint`. + +### Test coverage + +Сборка `nativeCompile`: до — падает через ~13 с, после — образ собирается за ~31 с и отдаёт CRUD. + +--- + +## Issue: `ConfigWatcher` читает узел графа до его инициализации + +- Status: Fixed +- Severity: Major +- Type: Framework bug +- Language: Java +- Runtime: JVM и GraalVM native-image +- Component: `config-common` +- Affected framework module: `config/config-common` +- Affected example modules: все приложения с файловым конфигом +- Related fix branch: `fix/config-watcher-starts-before-graph-ready` (`c6d6ddd24`) — [#818](https://github.com/kora-projects/kora/pull/818) + +### Description + +При каждом старте приложения в лог падает стектрейс из потока `config-reload`. Приложение при этом +стартует и работает — но слежение за конфигом молча не работает, потому что поток умирает сразу. + +### Relevant output + +``` +Exception in thread "config-reload" java.lang.IllegalStateException: Graph node value was not initialized: + [#1] interface io.koraframework.config.common.origin.ConfigOrigin (@Tag(ApplicationConfig)) (0 dependencies) + at io.koraframework.application.graph.internal.GraphImpl.getImpl(GraphImpl.java:81) + at io.koraframework.config.common.ConfigWatcher.watchJob(ConfigWatcher.java:75) +``` + +### Steps to reproduce + +Запустить любой из `examples/graalvm/*` — как shadowJar на JVM, так и native-бинарь. Воспроизводится +в 100 % запусков обоих рантаймов. + +### Suspected cause + +`ConfigWatcher.init()` стартует виртуальный поток, который первым действием дёргает +`graph.get(applicationConfigNode)`. Компонент держит `Node<…>`, а не значение, поэтому граф не считает +конфиг его зависимостью и может ещё не инициализировать его к моменту старта потока. + +### Investigation notes + +Решающая деталь нашлась в сгенерированном `ApplicationGraph`: узел watcher'а добавлен с **пустым** +списком зависимостей (`List.of()`), потому что фабрика принимает `Node`, а не значение. +При этом сам `ConfigWatcherTest` во фреймворке строит граф вручную и зависимость **указывает** +(`List.of(originNode)`) — поэтому штатные тесты гонку никогда не ловили. + +### Implemented fix + +Фабрика модуля теперь просит **значение** конфига, а не только узел, — зависимость становится явной, +и граф инициализирует конфиг раньше watcher'а **по построению**. Начальное значение приходит +параметром, и `graph.get(...)` на старте больше не вызывается вообще. + +Первая версия фикса была слабее — она ждала готовности узла, ловя `IllegalStateException`. Такой подход +лечил следствие и глушил бы любую другую ошибку графа, поэтому был заменён. + +### Test coverage + +Отдельного модульного теста нет, и это осознанно: дефект живёт в том, как процессор связывает узел, +а тест, собирающий граф вручную, может объявить любые зависимости и ничего не докажет. Проверка — +сгенерированный граф примера: было `List.of()`, стало `List.of(component1)`, и стектрейс из потока +`config-reload` исчез из лога старта. `config-common`, `config-hocon`, `config-yaml` и `test-junit5` — зелёные. + +--- + +## Issue: все логи вне запроса теряются — `KoraAsyncAppender` читает непривязанный `ScopedValue` + +- Status: Fixed +- Severity: Blocker +- Type: Framework bug +- Language: Java и Kotlin +- Runtime: JVM и GraalVM native-image +- Component: `logging-logback` +- Affected framework module: `logging/logging-logback` +- Affected example modules: все приложения с `LogbackModule` +- Related fix branch: `fix/logback-appender-outside-scope` (`5797597c7`) — [#816](https://github.com/kora-projects/kora/pull/816) + +### Description + +`KoraAsyncAppender.append` читает Kora-MDC через `MDC.get()`, а это `ScopedValue.get()`, который вне +привязанного скоупа бросает `NoSuchElementException`. `UnsynchronizedAppenderBase.doAppend` это ловит, +и событие **молча теряется**. Теряется всё, что логируется вне обработки запроса или сообщения: +старт и остановка приложения, фоновые задачи, пулы соединений, логи драйверов, ошибки инициализации. + +Дефект легко не заметить по двум причинам: внутри запроса логирование работает (то есть +приложение выглядит «логирующим»), а собственная ошибка logback о потере события уходит в +статус-менеджер, который в примерах глушится `NopStatusListener`. + +### Steps to reproduce + +1. Запустить `examples/graalvm/kora-java-graalvm-kafka` (логирует почти целиком вне скоупа). +2. В логе — 591 байт: только стектрейс, пишущийся напрямую в stderr. +3. Добавить `-Dlogback.statusListenerClass=ch.qos.logback.core.status.OnConsoleStatusListener` — + появляется `ERROR in KoraAsyncAppender[ASYNC] - Appender [ASYNC] failed to append. + java.util.NoSuchElementException: ScopedValue not bound`. + +### Implemented fix + +Аппендер берёт MDC только при `MDC.VALUE.isBound()`, иначе подставляет пустую карту. + +### Test coverage + +Модульного теста нет, и это оговорено в PR: попытка прогнать `AsyncAppenderBase` напрямую из теста +не доставляет событие до присоединённого `ListAppender` ни с фиксом, ни без — такой тест ничего +не доказывает. Доказательство сквозное: то же приложение с той же конфигурацией даёт 591 байт лога +до фикса и 16 001 байт после (XNIO, конфигурация Kafka-продюсера и консьюмера, поиск координатора +группы, `Application released`). + +### Побочное наблюдение + +В примерах секция уровней названа `logging.level`, а `LoggingConfig` читает `logging.levels` +(множественное число). Отдельно это не ломало ничего заметного, потому что `LoggingLevelRefresher.init()` +всё равно сбрасывает ROOT в `INFO`; в kafka-примере ключ приведён к `levels`. diff --git a/migration/KORA_2_JAVA_MIGRATION_GUIDE.md b/migration/KORA_2_JAVA_MIGRATION_GUIDE.md new file mode 100644 index 00000000..2f314cdb --- /dev/null +++ b/migration/KORA_2_JAVA_MIGRATION_GUIDE.md @@ -0,0 +1,1168 @@ +# Kora 1.x → 2.0: руководство по миграции Java-проектов + +Самодостаточный документ: Java-разработчику для миграции своего проекта не нужно открывать Kotlin-гайд. +Правила, общие для двух языков, продублированы здесь целиком. + +Источники истины: локальный чекаут фреймворка `../kora` @ `master` (`2.0.0-SNAPSHOT`), документация 1.x — `agents-md/kora-docs`, +эталонный мигрированный модуль — `examples/java/kora-java-crud`. + +Каждое правило ниже либо проверено компиляцией/тестами, либо явно помечено как гипотеза. + +--- + +## 0. Порядок миграции проекта + +1. Прогнать `python migration/scripts/migrate_kora_2.py` (сначала без флагов — dry-run, затем `--apply`) — пакеты, типы, координаты, версия, ресурсы. +2. Альтернатива для чисто Java-проектов, где текстовые замены нежелательны: OpenRewrite-рецепт `io.koraframework.migration.Kora1To2`, применяемый из самого мигрируемого проекта — `migration/openrewrite/README.md`. +3. **Обязательно**: `clean` + первая сборка с `--no-build-cache` (см. §1.3). +4. Собрать модуль: `./gradlew :classes --console=plain`. +5. Применить семантические правила из этого документа под конкретные ошибки компиляции. +6. Прогнать тесты модуля и только после этого переходить к следующему. + +Оба слоя автоматизации идемпотентны. Перед запуском — закоммитить или сохранить рабочее дерево. + +--- + +## 1. Сборка и окружение + +### 1.1 JDK: Gradle-процесс должен идти на последнем GA feature release + +**Было:** Gradle на JDK 21, toolchain 21. + +**Стало:** toolchain последнего GA JDK **и** сам Gradle запущен на том же major. На момент +актуализации документа это JDK 26; значение нужно перепроверять на дату миграции. + +```groovy +java { + toolchain { + languageVersion = JavaLanguageVersion.of(26) // заменить на latest GA + vendor = JvmVendorSpec.ADOPTIUM + } +} +``` + +**Причина:** артефакты Kora 2.0 собраны под JVM 25. Настройки toolchain недостаточно: `io.koraframework:openapi-generator` попадает в **buildscript classpath**, который резолвится JVM самого Gradle. На JDK 21 сборка падает ещё на конфигурации: + +``` +Dependency requires at least JVM runtime version 25. This build uses a Java 21 JVM. +> Run this build using a Java 25 or newer JVM. +``` + +**Проверка:** `JAVA_HOME= ./gradlew projects` проходит; с JDK 21 — падает. + +**Ограничение:** прописывать `org.gradle.java.home` в `gradle.properties` репозитория не стоит — путь машинозависим. Задавайте `JAVA_HOME` или используйте Gradle toolchain для запуска демона. + +### 1.2 Плагины и координаты + +| Было | Стало | +|---|---| +| `ru.tinkoff.kora` (groupId) | `io.koraframework` | +| BOM `ru.tinkoff.kora:kora-parent` | `io.koraframework:kora-bom` | +| `ru.tinkoff.kora:annotation-processors` | `io.koraframework:annotation-processors` | +| `json-module` | `json-common` | +| `cache-redis` | `cache-redis-lettuce` | +| `http-client-async` | **удалён**, замены нет — переходить на `http-client-jdk` или `http-client-ok` | +| `ru.tinkoff.kora.experimental:s3-client-aws` | см. §15 — S3 разделён на два разных артефакта, одного соответствия нет | + +Эталонный набор зависимостей — `examples/java/kora-java-crud/build.gradle`: + +```groovy +configurations { + koraBom + annotationProcessor.extendsFrom(koraBom); compileOnly.extendsFrom(koraBom); implementation.extendsFrom(koraBom) + api.extendsFrom(koraBom); testImplementation.extendsFrom(koraBom); testAnnotationProcessor.extendsFrom(koraBom) +} + +dependencies { + koraBom platform("io.koraframework:kora-bom:$koraVersion") + annotationProcessor "io.koraframework:annotation-processors" + implementation "io.koraframework:http-server-undertow" + testAnnotationProcessor "io.koraframework:annotation-processors" + testImplementation "io.koraframework:test-junit5" +} +``` + +### 1.3 Первая сборка после смены пакетов — `clean` + `--no-build-cache` + +**Симптом:** сотни ошибок вида `package ru.tinkoff.kora.common.annotation does not exist` в файлах, которых **нет** в исходниках — все пути ведут в `build/generated/…`. + +**Причина:** задачи-генераторы (OpenAPI, protobuf, `wsdl2java`) не удаляют предыдущий вывод, а ключ build-кеша не учитывает смену `apiPackage`/`modelPackage`. В результате в один source set попадают старые файлы в пакете `ru.tinkoff.kora` и новые в `io.koraframework`. + +**Решение:** + +```shell +./gradlew clean --continue +./gradlew classes testClasses --continue --no-build-cache +``` + +**Проверка (выполнена):** на эталонном модуле `clean` + обычная сборка → 100 ошибок; та же сборка с `--no-build-cache` → успех. Подробности — `KORA_2_FRAMEWORK_ISSUES.md`. + +--- + +### 1.4 Выравнивание сторонних версий — отдельный класс отказов + +Kora 2.0 подняла версии транзитивных библиотек, и модули, закрепившие свои, ломаются **в рантайме**, +а не при компиляции. Все четыре случая ниже проявились в этом репозитории; ошибки не указывают +на причину. + +| Библиотека | Версия в Kora 2.0 | Симптом при расхождении | +|---|---|---| +| gRPC | `1.83.1` | `AbstractMethodError: ... does not define or inherit an implementation of the resolved method 'buildClientTransportServers(List, MetricRecorder)'` | +| Flyway | `13.1.0` | `FlywayException: Unsupported Database: PostgreSQL 16.x` | +| Byte Buddy (через Mockito) | нужен ≥ поддерживающий Java 25 | `IllegalArgumentException: Java 25 (69) is not supported by the current version of Byte Buddy` | + +**gRPC.** Достаточно, чтобы `grpc-inprocess` / `grpc-netty` в тестах были той же версии, что и +`grpc-core`, который приходит с `io.koraframework:grpc-server`. Закреплённая старая версия даёт +`AbstractMethodError` при построении сервера. + +**Flyway.** С 10-й версии поддержка конкретных СУБД вынесена в отдельные артефакты, а +`io.koraframework:database-flyway` отдаёт только `flyway-core`. Приложение обязано добавить диалект +само, иначе падает на старте: + +```groovy +implementation "io.koraframework:database-flyway" +implementation "org.flywaydb:flyway-database-postgresql:13.1.0" +``` + +**Mockito.** Нужен `mockito-core`, чей Byte Buddy понимает class file version 69. В Kotlin-модулях +это отдельная ловушка: `org.mockito.kotlin:mockito-kotlin` тянет свою, более старую `mockito-core`, +поэтому её версию задают явно рядом. + +**Проверять после миграции**, а не полагаться на компиляцию: все три отказа рантаймовые, а +Byte Buddy к тому же прячется внутри `Application graph failed to initialize with N errors` +без видимых suppressed-исключений (Gradle их не печатает — временно включите `junitXml.required`). + +### 1.5 Процессорам нужна полная перекомпиляция + +При инкрементальной сборке процессор базы данных может прочитать интерфейс репозитория из +class-файла, где имена параметров синтетические, и сообщить: + +``` +error: SQL query placeholder has no matching method parameter: + :id + Available parameters: + - :arg0 +``` + +Исходный код при этом корректен — `--rerun-tasks` или `clean` на модуле убирает ошибку. +Сообщение на причину не указывает, поэтому при странных отказах процессоров начинайте с чистой сборки. + +--- + +## 2. Пакеты и аннотации + +Все пакеты фреймворка: `ru.tinkoff.kora.*` → `io.koraframework.*`. + +Аннотации DI переехали на уровень глубже — в `annotation`: + +| Было | Стало | +|---|---| +| `ru.tinkoff.kora.common.KoraApp` | `io.koraframework.common.annotation.KoraApp` | +| `ru.tinkoff.kora.common.Component` | `io.koraframework.common.annotation.Component` | +| `ru.tinkoff.kora.common.DefaultComponent` | `io.koraframework.common.annotation.DefaultComponent` | +| `ru.tinkoff.kora.common.KoraSubmodule` | `io.koraframework.common.annotation.KoraSubmodule` | +| `ru.tinkoff.kora.common.Mapping` | `io.koraframework.common.annotation.Mapping` | +| `ru.tinkoff.kora.common.Module` | `io.koraframework.common.annotation.Module` | +| `ru.tinkoff.kora.common.Tag` | `io.koraframework.common.annotation.Tag` | +| `ru.tinkoff.kora.common.annotation.Root` | `io.koraframework.common.annotation.Root` | + +Точка входа приложения (эталон): + +```java +import io.koraframework.application.graph.KoraApplication; +import io.koraframework.common.annotation.KoraApp; + +@KoraApp +public interface Application extends HoconConfigModule, LogbackModule, UndertowPublicHttpServerModule { + static void main(String[] args) { + KoraApplication.run(ApplicationGraph::graph); + } +} +``` + +**Автоматизация:** полностью покрыто OpenRewrite-рецептом и скриптом. + +--- + +## 3. Нуллабельность: JSpecify вместо jakarta + +**Было:** `jakarta.annotation.Nullable`, `jakarta.annotation.Nonnull`. +**Стало:** `org.jspecify.annotations.Nullable` (JSpecify приходит транзитивно с ядром Kora). `@Nonnull` в большинстве мест избыточна и удаляется; если нужна разметка — предпочитайте `@NullMarked` на уровне пакета/модуля. + +**Поведенческое отличие:** JSpecify-аннотации являются **type-use**. Их нельзя ставить там, где раньше стояла обычная аннотация объявления. Симптом: + +``` +error: type annotation @org.jspecify.annotations.Nullable is not expected here +``` + +Так же ведут себя и некоторые аннотации Kora: встречается `type annotation @io.koraframework.database.common.annotation.Column("status") is not expected here`. + +**Правило:** в квалифицированных вложенных типах type-use аннотация ставится непосредственно перед простым именем типа (`Outer.@Nullable Inner`), а не перед всем выражением. Для массивов и дженериков позиция также значима. + +**Автоматизация:** замена типа — да (OpenRewrite); корректная **расстановка** — нет, только вручную или нейро-агентом (см. `KORA_MIGRATION_NEURO.md`). + +--- + +## 4. HTTP-сервер и HTTP-клиент + +### 4.1 Модуль и синхронная модель + +| Было | Стало | +|---|---| +| `UndertowHttpServerModule` | `UndertowPublicHttpServerModule` | + +HTTP-обработка в 2.0 синхронная. Из контрактов контроллеров и интерцепторов уходят `CompletionStage`, реактивные обёртки и явный Kora `Context`. + +`Context` **удалён** из синхронных HTTP API. Механически заменять его нечем: аргумент убирается, а сигнатура обработчика/интерцептора мигрирует целиком. + +### 4.2 Пакеты HTTP-типов разделены + +| Было | Стало | +|---|---| +| `http.server.common.HttpServerRequest` | `http.server.common.request.HttpServerRequest` | +| `http.server.common.HttpServerResponse` | `http.server.common.response.HttpServerResponse` | +| `http.server.common.HttpServerResponseException` | `http.server.common.response.HttpServerResponseException` | +| `http.server.common.HttpServerInterceptor` | `http.server.common.interceptor.HttpServerInterceptor` | +| `http.server.common.handler.HttpServerRequestMapper` | `http.server.common.request.HttpServerRequestMapper` | +| `http.server.common.handler.HttpServerResponseMapper` | `http.server.common.response.HttpServerResponseMapper` | +| `http.client.common.HttpClientResponseException` | `http.client.common.exception.HttpClientResponseException` | + +### 4.3 Интерцептор: новая сигнатура + +**Было (1.x):** интерцептор получал контекст и цепочку, возвращая `CompletionStage`. + +**Стало:** + +```java +@Tag(HttpServer.class) +@Component +public final class HttpExceptionHandler implements HttpServerInterceptor { + + @Override + public HttpServerResponse intercept(HttpServerRequest request, InterceptChain chain) { + try { + return chain.process(request); + } catch (HttpServerResponseException e) { + return e; + } catch (Exception e) { + var body = HttpBody.json(errorJsonWriter.toByteArray(new MessageTO(e.getMessage()))); + return HttpServerResponse.of(500, body); + } + } +} +``` + +**Тег глобального интерцептора сменился:** + +| Было | Стало | +|---|---| +| `@Tag(HttpServerModule.class)` | `@Tag(HttpServer.class)` | +| `import …http.server.common.HttpServerModule;` | `import …http.server.common.HttpServer;` | + +В 2.0 фреймворк собирает глобальные интерцепторы именно по этому тегу — в `HttpServerModule` параметр объявлен как `@Tag(HttpServer.class) All interceptors`. + +**Коварство:** старый тег **компилируется без ошибок** — класс `HttpServerModule` существует, просто по нему никто не ищет интерцепторы. Глобальный интерцептор молча перестаёт вызываться: обработка ошибок, аутентификация или логирование исчезают без единого предупреждения. Проверяйте это тестом, а не компилятором. + +**Проверка:** компилируется и работает в `examples/java/kora-java-crud`. + +### 4.4 JSON-writer: `toByteArray` больше не бросает `IOException` + +`toByteArrayUnchecked(value)` удалён — используйте `toByteArray(value)`. + +Важное следствие: в 2.0 `toByteArray` **не объявляет checked-исключений** (`JsonWriter.toByteArray(@Nullable T)` без `throws`). Старый код с обработкой `IOException` перестаёт компилироваться: + +``` +error: exception IOException is never thrown in body of corresponding try statement +``` + +**Было:** + +```java +try { + return HttpServerResponse.of(code, HttpBody.json(errorJsonWriter.toByteArray(error))); +} catch (IOException ex) { + return HttpServerResponse.of(500, HttpBody.plaintext(ex.getMessage())); +} +``` + +**Стало:** + +```java +return HttpServerResponse.of(code, HttpBody.json(errorJsonWriter.toByteArray(error))); +``` + +Уберите также ставший ненужным `import java.io.IOException;`. + +### 4.5 Мапперы и интерцепторы обязаны быть DI-компонентами + +**Симптом:** + +``` +error: No component found for dependency: + MapperRequestController.UserContextRequestMapper (no tags) +``` + +**Причина:** генерируемый модуль контроллера теперь **инжектит** маппер как зависимость, а не создаёт его сам. + +**Было:** + +```java +public static final class UserContextRequestMapper implements HttpServerRequestMapper { … } +``` + +**Стало:** + +```java +@Component +public static final class UserContextRequestMapper implements HttpServerRequestMapper { … } +``` + +То же относится к классам из `@InterceptWith` и к маперам ключей кеша из `@Mapping`. + +### 4.6 Реактивные контроллеры удалены + +Метод контроллера, возвращающий `Mono`, в 2.0 неразрешим: + +``` +No component found for dependency: HttpServerResponseMapper> +``` + +Проверено по исходникам: в `http-server-common` нет ни одного маппера для `Mono`/`Flux`. Переводите такие методы на синхронный возврат `HttpServerResponse`; зависимость `io.projectreactor:reactor-core` после этого обычно становится ненужной. + +**Диагностическая ловушка.** Пока в графе есть такой неразрешимый компонент, процессор может выводить ошибку про **другой, исправный** компонент — например, требование `JsonWriter>` для `@Json`-метода, который на самом деле работает. Сначала уберите весь код, опирающийся на удалённую функциональность, и только потом анализируйте оставшиеся ошибки графа (`KORA_2_FRAMEWORK_ISSUES.md`). + +### 4.7 HTTP-клиент: `configPath` → `value` + +**Было:** + +```java +@HttpClient(configPath = "httpClient.userApi") +public interface UserApiClient { … } +``` + +**Стало:** + +```java +@HttpClient("httpClient.userApi") +public interface UserApiClient { … } +``` + +**Причина:** в 2.0 аннотация объявлена как `String value() default ""` плюс `telemetryTag()` и `httpClientTag()`; атрибута `configPath` больше нет. + +**Симптом:** `error: cannot find symbol: method configPath() location: @interface HttpClient`. + +**Автоматизация:** детерминированная замена `@HttpClient(configPath = "X")` → `@HttpClient("X")`, применима и к Java, и к Kotlin. + +--- + +### 4.6. `HttpResponseEntity` требует собственного маппера + +Из коробки в 2.0 есть мапперы ответа только для `String` и `byte[]`. Метод, который отдаёт +`HttpResponseEntity` (обычный приём, когда от ответа нужен только статус-код), не +собирается: + +``` +error: No component found for dependency: + HttpClientResponseMapper (no tags) +``` + +Маппер объявляется в самом клиенте и **не** указывается через `@Mapping`: + +```java +@Component +final class VoidResponseMapper implements HttpClientResponseMapper { + + @Override + public Void apply(HttpClientResponse response) throws IOException { + try (var body = response.body()) { + body.asInputStream().readAllBytes(); + } + return null; + } +} +``` + +`@Mapping` здесь сделало бы только хуже: с ним маппер обязан произвести весь тип возврата, +то есть `HttpResponseEntity`, тогда как шаблонная фабрика фреймворка ждёт маппер +полезной нагрузки и оборачивает его в entity сама. + +### 4.7. Мапперу нужен `@Component` ровно тогда, когда у него есть зависимости + +Маппер без зависимостей Kora создаёт сама — пометить такой класс `@Component` значит получить +`Multiple components match`. Маппер, которому нужен, скажем, `JsonReader`, она создать не +может, и без `@Component` будет `No component found for dependency` на его собственный тип. +Ориентироваться надо на конструктор, а не на вид аннотации над методом. + +--- + +## 5. Конфигурация + +### 5.1 Порты HTTP-сервера — молчаливый убийца старта + +Системный (приватный) сервер получил собственную секцию `httpServer.system`: + +| Было (1.x) | Стало (2.0) | +|---|---| +| `httpServer.publicApiHttpPort` | `httpServer.port` | +| `httpServer.privateApiHttpPort` | `httpServer.system.port` | +| `httpServer.privateApiHttpReadinessPath` | `httpServer.system.readinessPath` | +| `httpServer.privateApiHttpLivenessPath` | `httpServer.system.livenessPath` | +| `httpServer.privateApiHttpMetricsPath` | `httpServer.system.metricsPath` | + +**Было:** + +```hocon +httpServer { + publicApiHttpPort = 8080 + privateApiHttpPort = 8085 +} +``` + +**Стало:** + +```hocon +httpServer { + port = 8080 + system.port = 8085 +} +``` + +**Почему это критично, а не косметика.** `SystemHttpServerConfig extends HttpServerConfig`, то есть системный сервер наследует `port()` со значением по умолчанию `8080`. Пока в конфиге стоит нераспознаваемый `privateApiHttpPort`, **оба** сервера пытаются сесть на 8080, и приложение падает на старте: + +``` +HTTP server 'kora-undertow-system' (Undertow) failed to start on port '8080': port is already in use +Caused by: java.net.BindException: Address already in use +``` + +Компиляция при этом проходит успешно — лишние ключи HOCON просто игнорируются. Ошибка всплывает только в рантайме, а в blackbox-тестах выглядит как `HTTP/1.1 header parser received no bytes` и таймауты, что уводит от причины. + +**Автоматизация:** `python migration/scripts/migrate_config_keys.py --apply` (идемпотентен, есть dry-run). + +### 5.2 Секция JDBC: `db` → `jdbc` + +**Было:** + +```hocon +db { + jdbcUrl = ${POSTGRES_JDBC_URL} + username = ${POSTGRES_USER} + password = ${POSTGRES_PASS} +} +``` + +**Стало:** + +```hocon +jdbc { + jdbcUrl = ${POSTGRES_JDBC_URL} + username = ${POSTGRES_USER} + password = ${POSTGRES_PASS} +} +``` + +**Причина:** `JdbcDatabaseModule` в 2.0 создаёт `new JdbcDatabaseFactoryModule("jdbc")` — путь секции жёстко `jdbc`. + +**Симптом:** компиляция зелёная, но приложение (и тесты) падают на старте: + +``` +ConfigValueException: Config expected value, but got null at path: 'ROOT.jdbc.username' +``` + +Ошибка вводит в заблуждение: говорится про `jdbc.username`, а в конфиге такого блока вообще нет — потому что секция всё ещё называется `db`. + +**Автоматизация:** тот же `migrate_config_keys.py` (переименовывает только верхнеуровневой `db`-блок и только в файлах с `jdbcUrl`). + +### 5.3 Остальное + +| Было | Стало | +|---|---| +| `@ConfigValueExtractor` | `@ConfigMapper` | +| пакет `config.common.extractor` | удалён — типы переехали, ищите замену в `io.koraframework.config.common` | + +`@ConfigSource` сохранил семантику. HOCON-подстановки (`${VAR}`, `${?VAR}`) не менялись. + +### 5.4 Телеметрия: метрики выключены по умолчанию + +`TelemetryConfig.MetricsConfig.enabled()` в 2.0 возвращает `false`. Приложение стартует, эндпоинт +метрик отвечает `200`, но `http_server_*`, `http_client_*`, `db_*` и прочие метрики компонентов +в выводе отсутствуют — видны только JVM-метрики, которые регистрирует сам реестр. + +```hocon +httpServer { + telemetry.logging.enabled = true + telemetry.metrics.enabled = true # в 2.0 обязательно явно +} +``` + +Тот же ключ есть у каждого компонента с телеметрией (`httpClient..telemetry.metrics.enabled`, +`db.telemetry.metrics.enabled` и т. д.). Логирование (`logging.enabled`) тоже `false` по умолчанию, +трассировка (`tracing.enabled`) — `true`. + +**Молчаливый отказ:** ничего не падает и ничего не пишется в лог, метрики просто не собираются. + +--- + +## 6. Отказоустойчивость: строковые имена → типизированные спецификации + +Самое объёмное семантическое изменение. + +**Было:** + +```java +@CircuitBreaker("pet") +@Retry("pet") +@Timeout("pet") +public Optional findByID(long petId) { … } +``` + +**Стало:** аннотация принимает **класс** спецификации, а сама спецификация — интерфейс, привязанный к секции конфигурации: + +```java +@CircuitBreakable(PetCircuitBreaker.class) +@Retryable(PetRetry.class) +@Timeout(PetTimeouter.class) +public Optional findByID(long petId) { … } +``` + +```java +@CircuitBreakerSpec("resilient.circuitbreaker.pet") +public interface PetCircuitBreaker extends CircuitBreaker {} + +@RetrySpec("resilient.retry.pet") +public interface PetRetry extends Retry {} + +@TimeoutSpec("resilient.timeout.pet") +public interface PetTimeouter extends Timeouter {} +``` + +Соответствие аннотаций и пакетов: + +| Было | Стало | Пакет | +|---|---|---| +| `@CircuitBreaker("name")` | `@CircuitBreakable(X.class)` | `io.koraframework.resilient.circuitbreaker.annotation` | +| `@Retry("name")` | `@Retryable(X.class)` | `io.koraframework.resilient.retry.annotation` | +| `@Timeout("name")` | `@Timeout(X.class)` | `io.koraframework.resilient.timeout.annotation` | +| `@Fallback(value = "name", method = "…")` | `@Fallback(method = "…")` | атрибут `value` удалён | + +Изменения конфигурации: + +```hocon +resilient { + circuitbreaker.pet { + type = FIXED_WINDOW # обязателен для окна фиксированного размера + countBased.windowSize = 50 # было slidingWindowSize + minimumRequiredCalls = 25 + failureRateThreshold = 50 + waitDurationInOpenState = "25s" + permittedCallsInHalfOpenState = 10 + } + timeout.pet.duration = "5000ms" + retry.pet { delay = "500ms"; delayStep = "5s"; attempts = 3 } +} +``` + +**Симптом незавершённой миграции:** `incompatible types: String cannot be converted to Class` (38 таких ошибок на момент написания). + +**Автоматизация:** нет. Требуется завести новые типы и связать их с секциями конфигурации — это решение, которое скрипт принять не может. + +--- + +### 6.1. Окно circuit breaker переехало в `countBased` + +В 1.x у circuit breaker было одно счётное окно: + +```hocon +resilient.circuitbreaker.pet { + slidingWindowSize = 50 +} +``` + +В 2.0 реализаций несколько (`STRIPED_APPROX` по умолчанию, `FIXED_WINDOW`, `RING_BUFFER`, +`TIME_BASED`), а окно переехало в отдельный блок: + +```hocon +resilient.circuitbreaker.pet { + type = FIXED_WINDOW + countBased.windowSize = 50 +} +``` + +Блок `countBased` в конфигурации формально `@Nullable`, но реализация по умолчанию +(`StripedApproxKoraCircuitBreaker`) разыменовывает `config.countBased()` без проверки, поэтому +на практике он обязателен, как только circuit breaker вообще используется. Без него — NPE +на инициализации графа, а не внятная ошибка конфигурации. + +По смыслу ближе всего к счётному окну 1.x — `RING_BUFFER` (точная история последних N вызовов); +примеры репозитория используют `FIXED_WINDOW` — он дешевле и достаточен для демонстрации. +Скрипт `migrate_config_keys.py` переписывает ключ именно в `FIXED_WINDOW`; если поведение +критично, тип стоит выбрать осознанно. + +--- + +### 6.2. `CircuitBreakerPredicate` больше не выбирается по имени + +В 1.x предикат отказа реализовывал `name()` и `test(Throwable)`, а circuit breaker находил его +по ключу конфигурации `failurePredicateName`. + +В 2.0 это функциональный интерфейс с единственным методом, а связывание идёт через тег +спецификации: + +```java +@Tag(DefaultCircuitBreaker.class) +@Component +public final class CircuitBreakerFailurePredicate implements CircuitBreakerPredicate { + + @Override + public boolean isCircuitBreakerFailure(Throwable throwable) { + return !(throwable instanceof HttpServerResponseException e) || e.code() >= 500; + } +} +``` + +Ключ `failurePredicateName` из конфигурации удаляется — он больше ничего не выбирает. + +### 6.3. `@Fallback` потерял имя + +`FallbackSpec` в 2.0 нет, именованной конфигурации фолбэка тоже, поэтому атрибут `value` +удалён и остаётся только `method`: + +```java +@Fallback(method = "createUserFallback(request)") +``` + +--- + +## 7. Кеш + +| Было | Стало | +|---|---| +| `@Cacheable(value = X.class, parameters = "id")` | `@Cacheable(value = X.class, args = "id")` | +| `@CacheInvalidate(value = X.class, invalidateAll = true)` | `@CacheInvalidateAll(X.class)` | +| `RedisCacheModule` (давал клиента) | `LettuceRedisCacheModule` из `cache-redis-lettuce` | + +`RedisCacheModule` в 2.0 **существует** (`cache-redis-common`), но он транспортно-нейтрален и сам по себе не предоставляет `RedisCacheClient` — подключать нужно именно `LettuceRedisCacheModule`. Классы-мапперы ключей из `@Mapping` должны быть `@Component` (см. §4.5). + +Эталон: + +```java +@Cache("pet-cache") +public interface PetCache extends CaffeineCache {} +``` + +--- + +## 8. База данных + +- `database.jdbc.EntityJdbc` → `database.jdbc.annotation.EntityJdbc`. +- Контракты репозиториев синхронные; макросы (`%{return#selects}`, `%{entity#inserts -= id}`, `%{entity#where = @id}`) сохранились. +- `@Id` на методе для генерируемого ключа, `UpdateCount` для числа изменённых строк — без изменений. + +Эталон (`examples/java/kora-java-crud`): + +```java +@Repository +public interface PetRepository extends JdbcRepository { + + @Query("SELECT p.id, p.name, p.status, p.category_id, c.name as category_name FROM pets p JOIN categories c on c.id = p.category_id WHERE p.id = :id") + Optional findById(long id); + + @Id + @Query("INSERT INTO %{entity#inserts -= id}") + long insert(Pet entity); + + @Query("DELETE FROM pets WHERE id = :id") + UpdateCount deleteById(long id); +} +``` + +**Удалённые интеграции:** R2DBC и Vert.x (см. §12). + +--- + +## 9. Планировщик + +Quartz: `@ScheduleWithTrigger` принимает класс-тег напрямую. + +**Было:** `@ScheduleWithTrigger(@Tag(MyJob.class))` +**Стало:** `@ScheduleWithTrigger(MyJob.class)` + +--- + +## 10. OpenAPI-генерация + +В Kora 2.0 остались только четыре режима: `java-client`, `java-server`, `kotlin-client`, `kotlin-server`. + +| Было | Стало | +|---|---| +| `java-reactive-client` | `java-client` | +| `java-reactive-server` | `java-server` | + +Смена режима необходима, но недостаточна: сгенерированный код нужно адаптировать к синхронным API. Известные проблемы на текущий момент: package-private интерфейсы API и рассогласование сигнатур делегатов (`method does not override or implement a method from a supertype`), `JsonNullable`. Разбирается в `KORA_2_FRAMEWORK_ISSUES.md`. + +--- + +### 10.1. Сгенерированные enum: используйте `fromValue` + +Wire-значение из OpenAPI (`available`) может не совпадать с именем Java-константы +(`AVAILABLE`). Не добавляйте ручной `statusOf`, поиск по `values()` или `Enum.valueOf`: + +```java +var status = Pet.StatusEnum.fromValue(rawStatus); +``` + +`fromValue` генерируется Kora OpenAPI 2.0. Неизвестное значение приводит к +`IllegalArgumentException`; server delegate при необходимости преобразует его в +предусмотренный контрактом ответ `400`. + +### 10.2. Security tags называются по security-схеме + +Используйте вложенный тип, сгенерированный из имени в `components.securitySchemes`, +например `@Tag(ApiSecurity.ApiKeyAuth.class)` или +`@Tag(ApiSecurity.BearerAuth.class)`. Порядковые `SecurityRequirementTagN` и старые +варианты с маленькой буквы не соответствуют сгенерированному API 2.0. + +--- + +## 11. Тестирование + +Версию JUnit обновите до `6.1.3` через общую property: + +```properties +junitVersion=6.1.3 +``` + +Модули подключают `platform("org.junit:junit-bom:$junitVersion")`; версия +`org.testcontainers:junit-jupiter` относится к Testcontainers и не заменяется на версию JUnit. + +Пакет расширения: `io.koraframework.test.extension.junit5.*` (`@KoraAppTest`, `@TestComponent`, `KoraAppTestConfigModifier`, `KoraConfigModification`), артефакт `io.koraframework:test-junit5`. + +Подход из эталона сохранился без изменений по смыслу: `@KoraAppTest(Application.class)`, моки через `@Mock @TestComponent`, конфиг — через `KoraConfigModification.ofString(...)`, интеграционные тесты — на Testcontainers. + +Не забудьте `testAnnotationProcessor "io.koraframework:annotation-processors"`. + +--- + +## 12. Удалённая и несовместимо изменённая функциональность + +| Функциональность 1.x | Состояние в 2.0 | Что делать | +|---|---|---| +| R2DBC (`database-r2dbc`) | удалён | переводить на синхронный `database-jdbc` (Virtual Threads) либо оставлять как legacy-пример | +| Vert.x SQL (`database-vertx`) | удалён | то же | +| прочие интеграции Vert.x | удалены | `vertx-common` в дереве фреймворка ещё присутствует, но БД-интеграции нет | +| `http-client-async` | удалён | `http-client-jdk` / `http-client-ok` | +| реактивные контракты репозиториев | удалены | синхронные контракты | +| `@ConfigValueExtractor` | переименован | `@ConfigMapper` | +| `config.common.extractor` | пакет удалён | найти замену в `io.koraframework.config.common` | +| `Context` в синхронных HTTP API | удалён | переписать сигнатуру | +| `toByteArrayUnchecked` | удалён | `toByteArray` | +| `invalidateAll = true` | удалён | `@CacheInvalidateAll` | + +Модули примеров, зависящие от удалённого, **не удалены**: они исключены из агрегатной сборки в `settings.gradle` и помечены `BLOCKED_BY_REMOVED_FUNCTIONALITY` в `KORA_2_MIGRATION_STATUS.md`. Решение об их переписывании или удалении — за владельцем репозитория. + +--- + +## 13. Синхронная модель и Virtual Threads + +Kora 2.0 строится на синхронных контрактах, исполняемых на виртуальных потоках. При миграции реактивного кода, который был реактивным только ради неблокирующего доступа к БД, следует переходить на синхронный эквивалент. + +Это не механическое снятие обёрток: меняются отмена операции, границы транзакции и распространение исключений. Там, где требуется смысловая переработка, она документируется отдельно в `KORA_MIGRATION_NEURO.md`. + +--- + +## 15. S3: два разных артефакта вместо одного + +В 2.0 под именем `s3-client-aws` существуют **два разных модуля**, и это легко перепутать: + +| Артефакт | Что внутри | +|---|---| +| `io.koraframework:s3-client-aws` | только обёртка над AWS SDK: `AwsS3ClientModule`, конфиг, телеметрия. **Ни аннотаций `@S3`, ни моделей** | +| `io.koraframework.experimental:s3-client-kora` | декларативный клиент: `@S3` из `io.koraframework.s3.client.kora.annotation`, `S3Client`, фабрики | + +Группа у всего, что лежит в `experimental/`, — `io.koraframework.experimental` (задаётся в `build.gradle` фреймворка). Туда же относятся `camunda-*`. + +**Симптом неправильного артефакта:** `package S3 does not exist`, `package io.koraframework.s3.client.model does not exist`, `package io.koraframework.s3.client.annotation does not exist`. + +### 15.1. `s3-client-minio` в 2.0 не существует + +Реализация декларативного клиента поверх SDK Minio удалена (`experimental/s3-client-minio` остался +пустым каталогом и в `settings.gradle` фреймворка не подключён). Замена — `s3-client-kora`, +клиент поверх собственного HTTP-клиента Kora. Minio при этом прекрасно годится как S3-совместимое +хранилище для тестов — меняется артефакт, не тестовое окружение. + +### 15.2. `s3-client-aws`: обёртка над SDK, а не декларативный клиент + +Модуль отдаёт в контейнер сам `software.amazon.awssdk.services.s3.S3Client`, и работать с S3 +предполагается через API AWS SDK: + +```java +@Component +public class AwsS3Service { + + private final S3Client s3Client; + + public AwsS3Service(S3Client s3Client) { + this.s3Client = s3Client; + } +} +``` + +Бин выдаётся `AwsS3ClientFactoryModule` под `@Tag(Tag.Factory.class)`. Пугаться тега не нужно: +внутри `@FactoryModule` `@Tag.Factory` разворачивается в тег **самого метода фабричного модуля** +(`ComponentDeclaration.fromModule`), а `AwsS3ClientModule#awsS3ClientFactoryModule()` тега не имеет — +значит компонент нетегированный. Именно этот механизм позволяет объявить несколько фабричных модулей +с разными тегами и получить несколько независимо сконфигурированных клиентов. + +Конфигурация переехала под `s3client.aws`, а ключи доступа — во вложенный объект: + +```hocon +s3client.aws { + url = ${S3_URL} + credentials { + accessKey = ${S3_ACCESS_KEY} + secretKey = ${S3_SECRET_KEY} + } +} +``` + +### 15.3. Новый API декларативного клиента + +| 1.x | 2.0 | +|---|---| +| `io.koraframework.s3.client.annotation.S3` | `io.koraframework.s3.client.kora.annotation.S3` | +| `@S3.Get` для метаданных | `@S3.Head` → `HeadObjectResult` | +| `S3Object` | `GetObjectResult` (это `HttpClientResponse`, тело — `body().asInputStream()`) или `byte[]` | +| `S3ObjectMeta` | `HeadObjectResult` | +| `S3ObjectList` / `S3ObjectMetaList` | `ListBucketResult`, `List`, `Iterator` | +| `S3Body` | `byte[]`, `ByteBuffer`, `InputStream`, `S3Client.ContentWriter` | +| `S3ObjectUpload putObject(...)` | `String putObject(...)` (ETag) или `void` | +| `S3NotFoundException` | `io.koraframework.s3.client.kora.exception.S3ClientNoSuchKeyException` | +| `@S3.List(limit = 50)` | лимит задаётся через `ListObjectsArgs` | + +Имя бакета больше не берётся из конфигурации клиента само — его указывают через `@S3.Bucket`: +путь с ведущей точкой (`@S3.Bucket(".bucket")`) отсчитывается от пути клиента из `@S3.Client`, +без точки — это абсолютный путь в конфигурации. Альтернатива — параметр метода под `@S3.Bucket`. + +Конфигурация клиента: `endpoint` (было `url`), `credentials { accessKey, secretKey }`, +опционально `region`, `addressStyle`, `requestTimeout`, `upload`. + +**Пакетного удаления в декларативном клиенте больше нет.** `@S3.Delete` генерирует только +`deleteObject`; метод вида `void deleteObjects(List keys)` в 2.0 не поддерживается, +хотя `S3Client#deleteObjects` в runtime-API есть. Асинхронные и реактивные варианты клиента +убраны вместе с реактивной моделью. + +--- + +### 15.4. Приложение, которому нужны и SDK, и декларативный клиент + +Администрирование бакетов (создать, проверить существование) в декларативный контракт `@S3` +не входит. Приложению, которому нужно и то и другое, подключаются оба артефакта: + +```groovy +implementation "io.koraframework:s3-client-aws" +implementation "io.koraframework.experimental:s3-client-kora" +``` + +```java +@KoraApp +public interface Application extends AwsS3ClientModule, KoraS3ClientModule, ... {} +``` + +Секции конфигурации у них разные и независимые: `s3client.aws` для SDK-обёртки и путь +из `@S3.Client(...)` для декларативного клиента. + +Имя бакета из `@S3.Bucket` попадает в сгенерированный класс, а не в компонент, поэтому +код, которому оно нужно отдельно (тот же инициализатор бакета), читает тот же путь сам — +например через `@ConfigSource`. + +Отдельная ловушка: компонент `Lifecycle`, который только готовит внешнее состояние и от +которого никто не зависит, выбрасывается из графа. Ему нужен `@Root`, иначе исчезнет и он, +и всё, что он тянул за собой: + +``` +interface software.amazon.awssdk.services.s3.S3Client wasn't found in graph +``` + +--- + +## 16. Camunda 8 / Zeebe + +### 16.1. Клиент переименован + +Camunda 8.8 переименовала клиент, и Kora 2.0 использует уже новый (`io.camunda:camunda-client-java`): + +| 1.x | 2.0 | +|---|---| +| `io.camunda.zeebe.client.ZeebeClient` | `io.camunda.client.CamundaClient` | +| `io.camunda.zeebe.client.api.response.*` | `io.camunda.client.api.response.*` | +| `io.koraframework.camunda.zeebe.worker.JobWorkerException` | `io.koraframework.camunda.zeebe.worker.exception.JobWorkerException` | + +`CamundaClient` инжектируется напрямую — `ZeebeWorkerModule` отдаёт его как `Wrapped`. + +Отдельная засада в тестах: `io.camunda:zeebe-process-test-*` даже в версии 8.9.x всё ещё живёт на +старом `ZeebeClient`, и `BpmnAssert.assertThat(...)` принимает только его типы ответов. В тесте +поэтому остаётся старый клиент, тогда как само приложение работает на `CamundaClient` — это +нормально, объекты независимые. + +### 16.2. REST-адрес стал обязательным + +`ZeebeClientConfig#rest()` не помечен `@Nullable` и `RestConfig#url()` не имеет значения по умолчанию, +а `ZeebeWorkerModule` безусловно вызывает `clientConfig.rest().url()`. Конфигурация только с gRPC +теперь падает на старте: + +```hocon +zeebe.client { + grpc.url = ${ZEEBE_GRPC_URL} + rest.url = ${ZEEBE_REST_URL} +} +``` + +### 16.3. Воркеры: два дефекта фреймворка, исправленные при миграции + +Оба описаны в `KORA_2_FRAMEWORK_ISSUES.md` и лежат отдельными ветками в `../kora`: + +- `@Component`-воркер с `public`-методом под `@JobWorker` молча исчезал из графа + (`No component found for dependency: X`, при том что `@Component` на `X` стоит); +- `JobWorkerException` перестал поднимать BPMN-ошибку — задача просто ретраилась. + +Если фиксов нет, обходные пути такие: объявить метод-обработчик package-private (тогда класс +регистрируется) и не рассчитывать на boundary error event. + +--- + +## 17. GraalVM Native Image + +Раздел проверен на трёх модулях `examples/graalvm/*`: каждый собран в native-образ и запущен +против реальных зависимостей (Postgres, Kafka, Scylla + Redis) — двумя путями сразу: `nativeCompile` +через Gradle-плагин и `native-image` внутри Docker по `Dockerfile`. + +Окружение, на котором всё проверено: GraalVM CE 25.2.4+7.1 (`native-image 25.0.4`), установка — +`sdk install java 25.2.4-graalce`. Образ для Docker-сборки — `ghcr.io/graalvm/native-image-community:25`. + +### 17.1. Плагин `org.graalvm.buildtools.native`: 0.11.5 → 1.1.7 + +**Было:** + +```groovy +id "org.graalvm.buildtools.native" version "0.11.5" apply false +``` + +**Стало:** + +```groovy +id "org.graalvm.buildtools.native" version "1.1.7" apply false +``` + +**Причина:** на Gradle 9 задача `collectReachabilityMetadata` падает на резолве конфигурации +из чужого проекта. До 1.1.6 плагин регистрировал один shared build service `nativeConfigurationService` +на весь build — второй и последующие GraalVM-модули получали сервис, созданный в контексте +первого. В Gradle 8 это было предупреждением, в Gradle 9 — ошибка. В 1.1.6 имя сервиса стало +включать `project.getPath()` — сервис стал попроектным +([native-build-tools#760](https://github.com/graalvm/native-build-tools/issues/760)). + +**Ограничение:** обходов в виде «запускать по одному модулю» недостаточно — ошибка возникает на +конфигурации всего build'а. Нужно поднимать версию плагина. + +### 17.2. GraalVM 25 вместо 21 + +**Было / Стало** — три места, и менять нужно все три: + +```groovy +java { + toolchain { + languageVersion = JavaLanguageVersion.of(25) // было 21 + vendor = JvmVendorSpec.ADOPTIUM + } +} + +graalvmNative { + binaries { + main { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(25) // было 21 + vendor = JvmVendorSpec.matching("GraalVM Community") + } + } + } +} +``` + +```dockerfile +FROM ghcr.io/graalvm/native-image-community:25 as builder # было :21 +``` + +**Причина:** та же, что и в §1.1 — артефакты Kora 2.0 собраны под JVM 25. `native-image` отказывается +читать классы более нового class-file формата, чем его собственный JDK. + +**Проверка:** `native-image --version` внутри выбранного launcher'а должен показать 25.x. +Если в системе нет GraalVM, `nativeCompile` падает на поиске toolchain — это не ошибка миграции. + +### 17.3. `imageName` и `mainClass`: передавать провайдер, а не интерполяцию + +**Было:** + +```groovy +imageName = "$project.name" +mainClass = "$application.mainClass" +``` + +**Стало:** + +```groovy +imageName = project.name +mainClass = application.mainClass +``` + +**Причина:** `application.mainClass` — это `Property`, а не `String`. В строковой интерполяции +Groovy вызывает `toString()` самого свойства, и в командную строку `native-image` уезжает не имя класса, +а отладочное представление вида `property(java.lang.String, fixed(...))`. На старом плагине это +работало по совпадению (значение разворачивалось раньше), в 1.1.7 — нет. + +**Поведенческое изменение:** ошибка выглядит как «main class not found» или как бинарь со странным +именем — не как ошибка конфигурации Gradle. + +### 17.4. `jar.enabled = false` больше нельзя + +**Было:** модули гасили обычный `jar`, чтобы в `build/libs` лежал только shadow-артефакт: + +```groovy +jar.enabled = false +``` + +**Стало:** строка удалена. + +**Причина:** `nativeCompile` строит classpath из артефактов самого проекта. С выключенным `jar` +в classpath попадают только зависимости, а классы приложения — нет. Shadow-jar здесь не спасает: +он собирается для Docker-пути и в classpath `nativeCompile` не участвует. + +**Проверка:** если строку вернуть, сборка падает на том, что не находит класс `Application`, +хотя `compileJava` прошёл. + +### 17.5. Метаданные достижимости: что теперь даёт сам фреймворк + +Kora поставляет метаданные внутри своих артефактов +(`META-INF/native-image/<модуль>/`). При миграции выяснилось, что пять наборов были неполными +или не читались вовсе. Все пять исправлены в upstream: + +| Симптом в native-образе | Модуль | PR | +|---|---|---| +| `java.lang.IllegalArgumentException: No XNIO provider found` на старте HTTP-сервера | `http-server-undertow` | [#811](https://github.com/kora-projects/kora/pull/811) | +| HikariCP не находит micrometer-трекер метрик | `database-jdbc` | [#812](https://github.com/kora-projects/kora/pull/812) | +| кеш Caffeine без статистики падает на создании | `cache-caffeine` | [#813](https://github.com/kora-projects/kora/pull/813) | +| регистрации молча не применяются | `micrometer-module`, `grpc-server`, `kafka` | [#814](https://github.com/kora-projects/kora/pull/814) | +| сборка образа падает на этапе анализа | `database-cassandra` | [#815](https://github.com/kora-projects/kora/pull/815) | + +Самый переносимый вывод — четвёртая строка: файл с именем **`reflection-config.json`** native-image +не читает вовсе. Правильное имя — **`reflect-config.json`**. Ошибка не диагностируется никак: +сборка идёт успешно, просто регистрации не применяются. Проверьте имена файлов в своём проекте +отдельно — это одна команда: + +```shell +find . -name "reflection-config.json" # каждое попадание — мёртвый файл +``` + +**Ограничение (важное):** не удаляйте собственные метаданные приложения на том основании, +что приложение работает на JVM. В примерах собственные метаданные остались только для logback +(`src/main/resources/META-INF/native-image/io.koraframework.examples/logback/`) — они нужны и сейчас. +При переименовании пакетов каталог с именем группы (`.../native-image//`) переименовывается +вручную — ни OpenRewrite, ни скрипт каталоги ресурсов не трогают. + +### 17.6. Как метаданные попадают в образ, собираемый в Docker + +Два пути сборки ведут себя по-разному, и это источник путаницы: + +- `nativeCompile` сам подкладывает metadata repository (`graalvmNative.metadataRepository.enabled = true`); +- `native-image -cp application.jar` внутри Docker никакого Gradle не видит и читает только то, + что лежит в самом jar. + +Чтобы второй путь видел те же метаданные, их надо собрать в ресурсы до упаковки: + +```groovy +processResources.dependsOn tasks.collectReachabilityMetadata +sourceSets.main { resources.srcDirs += "$buildDir/native-reachability-metadata" } + +shadowJar { + mergeServiceFiles() // без этого теряются META-INF/services — в native это фатально +} +``` + +Эта связка была в примерах и в 1.x — менять её при миграции не нужно, но её надо знать: если +образ из Gradle работает, а из Docker — нет, первый подозреваемый именно она, а не код. + +### 17.7. Когда GraalVM-модуль считается мигрированным + +Сборка образа — не критерий: все перечисленные в §17.5 дефекты давали **успешную сборку** +и падали только в рантайме. Минимальный набор проверок после сборки: + +1. бинарь стартует и не падает через секунду; +2. `GET /system/readiness` отвечает 200 — значит граф инициализировался целиком; +3. `GET /metrics` отдаёт метрики, а не заглушку и не 500 (см. #810 и #814); +4. сценарий модуля отрабатывает против реальной зависимости (БД, брокер), а не на моках; +5. в логе нет стектрейсов при старте — они тут часто единственный признак отвалившейся подсистемы. + +Самый дешёвый способ закрепить это в CI — `BlackBoxTests`, которые собирают образ по `Dockerfile` +и гоняют его Testcontainers'ами; в репозитории так сделаны все три GraalVM-модуля. + +**Отдельно про ожидание готовности контейнера.** Не ждите строку в логе — формулировки +стартовых сообщений Kora не являются контрактом и между 1.x и 2.0 поменялись. Ждите HTTP-пробу: + +```java +waitingFor(Wait.forHttp("/system/readiness").forPort(8080).forStatusCode(200) + .withStartupTimeout(Duration.ofSeconds(60))); +``` + +### 17.8. GraalVM-модули на удалённой функциональности + +`kora-java-graalvm-crud-r2dbc` и `kora-java-graalvm-crud-vertx` миграции не подлежат: дело не в native, +а в том, что сами интеграции удалены из 2.0 (§12). Они исключены из `settings.gradle`, но оставлены +в репозитории. Если у вас такой модуль — сначала переводите его на синхронный JDBC/Cassandra +(§13), и только потом возвращайтесь к native-части. + +### 17.9. Диагностика + +Отладка native-образа плохо формализуется в правила «было → стало»: одна и та же ошибка может +быть и дефектом фреймворка, и нехваткой метаданных приложения. Рабочая методика — трассирующий +агент, изоляция через минимальный native-пробник и проверка того, что метаданные вообще читаются — +описана в `KORA_MIGRATION_NEURO.md`, раздел *Diagnostic pattern: native-image собрался, но падает в рантайме*. + +--- + +## 14. Что покрыто автоматизацией + +| Изменение | OpenRewrite | Скрипт | Вручную / нейро-агент | +|---|---|---|---| +| Пакеты и координаты | ✅ | ✅ | — | +| Переезд DI-аннотаций в `…annotation` | ✅ | ✅ | — | +| Переименования модулей (`json-common`, `cache-redis-lettuce`) | ✅ | ✅ | — | +| Разделение HTTP-пакетов | — | ✅ | — | +| Версии плагинов, toolchain latest GA JDK | — | ✅ | — | +| Режимы OpenAPI-генерации | — | ✅ | адаптация кода — вручную | +| `jakarta` → JSpecify (тип) | ✅ | ✅ | **расстановка** — вручную | +| Типизированные resilient-спецификации | — | — | ✅ | +| `@Component` на мапперы/интерцепторы | — | частично (хардкод) | ✅ | +| Сигнатуры интерцепторов, удаление `Context` | — | — | ✅ | +| S3-клиент | — | — | ✅ | +| Версия плагина GraalVM, толчейн 25, базовый образ Docker | — | ✅ | — | +| `imageName`/`mainClass`, `jar.enabled` (§17.3–§17.4) | — | — | ✅ | +| Метаданные достижимости native-image | — | — | ✅ | + +Ограничения текущего скрипта `migrate_kora_2.py` описаны в `migration/README.md` — он содержит замены с хардкодом имён классов конкретных примеров и несколько слишком широких текстовых замен; для применения к чужому проекту требуется доработка. +## Java Structured Concurrency для мигрируемого Kotlin-кода + +Если Kotlin-часть приложения использовала Structured Coroutines Concurrency +(`coroutineScope`, `supervisorScope`, `async`/`await`, `awaitAll`, structured cancellation или +timeouts), финальная архитектура Kora 2.0 должна использовать Java `StructuredTaskScope`. + +Берите последний **GA feature release JDK** на момент миграции и последнюю итерацию Structured +Concurrency Preview именно этого JDK. Не закрепляйте старый API preview из JDK 21–25 и не берите +EA JDK в production только ради более нового preview. На момент актуализации документа это JDK 26, +Structured Concurrency Sixth Preview (JEP 525). + +Preview включается для всех фаз: `JavaCompile` — `--enable-preview` плюс совпадающий `--release`, +Kotlin compilation — `-Xjdk-release=` и `-Xjvm-enable-preview`, tests/`JavaExec`/ +production launcher — `--enable-preview`. Build image и runtime image обязаны иметь одинаковый +major JDK. Выбор `Joiner`, timeout, failure/cancellation и supervisor semantics является +семантической миграцией и проверяется отдельными тестами. + +Полный Kotlin/Gradle шаблон и checklist находятся в Kotlin guide, §13. + +--- diff --git a/migration/KORA_2_KOTLIN_MIGRATION_GUIDE.md b/migration/KORA_2_KOTLIN_MIGRATION_GUIDE.md new file mode 100644 index 00000000..80491ed7 --- /dev/null +++ b/migration/KORA_2_KOTLIN_MIGRATION_GUIDE.md @@ -0,0 +1,1109 @@ +# Kora 1.x → 2.0: руководство по миграции Kotlin-проектов + +Самодостаточный документ: Kotlin-разработчику для миграции своего проекта не нужно открывать Java-гайд. +Правила, общие для двух языков, продублированы здесь целиком, с идиоматичными Kotlin-примерами. + +Источники истины: локальный чекаут фреймворка `../kora` @ `master` (`2.0.0-SNAPSHOT`), документация 1.x — `agents-md/kora-docs`. +Эталонный полностью мигрированный Kotlin-модуль на момент написания отсутствует — эталон существует только для Java +(`examples/java/kora-java-crud`), поэтому Kotlin-специфичные правила помечаются как проверенные только там, где это подтверждено сборкой. + +--- + +## 0. Порядок миграции проекта + +1. `python migration/scripts/migrate_kora_2.py` (сначала dry-run, затем `--apply`) — Kotlin-файлы, Gradle DSL, ресурсы, перенос пакетных каталогов. Для Kotlin это единственный слой автоматизации. +2. OpenRewrite-рецепт (`migration/openrewrite/`) Kotlin не трансформирует вовсе; если в проекте есть и Java-модули — см. `migration/openrewrite/README.md`. +3. **Обязательно**: `clean` + первая сборка с `--no-build-cache` (см. §1.4). +4. `./gradlew :kspKotlin --console=plain` — KSP отрабатывает раньше компиляции и даёт первую волну ошибок. +5. Применить семантические правила из этого документа. +6. Прогнать тесты модуля и только затем переходить к следующему. + +--- + +## 1. Сборка и окружение + +### 1.1 JDK: последний GA feature release + +```kotlin +val latestJava = 26 // обновить на дату миграции + +kotlin { + jvmToolchain(latestJava) +} +``` + +Берите последний GA feature release с `openjdk.org/projects/jdk`, а не исторический минимум, +на котором собирался Kora 2.0. На момент актуализации документа это JDK 26. Для +`StructuredTaskScope` используйте последнюю preview-итерацию именно выбранного JDK. + +Toolchain недостаточно: `io.koraframework:openapi-generator` попадает в **buildscript classpath**, который резолвится JVM самого Gradle. На JDK 21 конфигурация падает: + +``` +Dependency requires at least JVM runtime version 25. This build uses a Java 21 JVM. +``` + +**Проверка:** `JAVA_HOME= ./gradlew projects` проходит, на JDK 21 — нет. + +### 1.2 Kotlin и KSP + +| Было | Стало | +|---|---| +| Kotlin `1.9.25` | `2.4.10` | +| KSP `1.9.25-1.0.20` | `2.3.11` | + +Эти версии совпадают с теми, на которых собран сам фреймворк (`../kora/gradle/libs.versions.toml`) — расхождение версий приведёт к трудноотлаживаемым падениям процессоров. + +**KSP 2 больше не экспортирует тип `KspTask`.** Старая обвязка перестала компилироваться: + +**Было:** + +```kotlin +import com.google.devtools.ksp.gradle.KspTask + +tasks.withType().configureEach { … } +``` + +**Стало:** + +```kotlin +tasks.matching { it.name.startsWith("ksp") }.configureEach { … } +``` + +либо привязка к конкретной задаче по имени (`tasks.named("kspKotlin")`). + +### 1.3 Координаты + +| Было | Стало | +|---|---| +| `ru.tinkoff.kora` (groupId) | `io.koraframework` | +| BOM `ru.tinkoff.kora:kora-parent` | `io.koraframework:kora-bom` | +| `ru.tinkoff.kora:symbol-processors` | `io.koraframework:symbol-processors` | +| `json-module` | `json-common` | +| `cache-redis` | `cache-redis-lettuce` | +| `http-client-async` | **удалён** → `http-client-jdk` / `http-client-ok` | +| `ru.tinkoff.kora.experimental:s3-client-aws` | два разных артефакта: `io.koraframework:s3-client-aws` (только AWS SDK-обёртка) и `io.koraframework.experimental:s3-client-kora` (декларативный `@S3`, пакет `io.koraframework.s3.client.kora.annotation`) | + +### 1.4 Первая сборка после смены пакетов — `clean` + `--no-build-cache` + +**Симптом:** ошибки `package ru.tinkoff.kora.* does not exist` / `Unresolved reference` в файлах, которых нет в исходниках — пути ведут в `build/generated/…`. + +**Причина:** задачи-генераторы (OpenAPI, protobuf) не удаляют прежний вывод, а ключ build-кеша не учитывает смену `apiPackage`/`modelPackage`; старый и новый пакеты сосуществуют в одном source set. + +```shell +./gradlew clean --continue +./gradlew classes testClasses --continue --no-build-cache +``` + +**Проверка (выполнена на Java-эталоне):** обычная сборка после `clean` → 100 ошибок; с `--no-build-cache` → успех. + +### 1.5 Выравнивание сторонних версий — отдельный класс отказов + +Kora 2.0 подняла версии транзитивных библиотек, и модули, закрепившие свои, ломаются **в рантайме**, +а не при компиляции. Все четыре случая ниже проявились в этом репозитории; ошибки не указывают +на причину. + +| Библиотека | Версия в Kora 2.0 | Симптом при расхождении | +|---|---|---| +| gRPC | `1.83.1` | `AbstractMethodError: ... does not define or inherit an implementation of the resolved method 'buildClientTransportServers(List, MetricRecorder)'` | +| Flyway | `13.1.0` | `FlywayException: Unsupported Database: PostgreSQL 16.x` | +| Byte Buddy (через Mockito) | нужен ≥ поддерживающий Java 25 | `IllegalArgumentException: Java 25 (69) is not supported by the current version of Byte Buddy` | + +**gRPC.** Достаточно, чтобы `grpc-inprocess` / `grpc-netty` в тестах были той же версии, что и +`grpc-core`, который приходит с `io.koraframework:grpc-server`. Закреплённая старая версия даёт +`AbstractMethodError` при построении сервера. + +**Flyway.** С 10-й версии поддержка конкретных СУБД вынесена в отдельные артефакты, а +`io.koraframework:database-flyway` отдаёт только `flyway-core`. Приложение обязано добавить диалект +само, иначе падает на старте: + +```groovy +implementation "io.koraframework:database-flyway" +implementation "org.flywaydb:flyway-database-postgresql:13.1.0" +``` + +**Mockito.** Нужен `mockito-core`, чей Byte Buddy понимает class file version 69. В Kotlin-модулях +это отдельная ловушка: `org.mockito.kotlin:mockito-kotlin` тянет свою, более старую `mockito-core`, +поэтому её версию задают явно рядом. + +**MockK.** Старый `io.mockk:mockk:1.13.x` тянет Byte Buddy `1.14.x`; его transformer пишет +`i.m.p.j.t.InliningClassTransformer - Failed to transform class ... Java 25 (69) is not supported`. +Обновите MockK минимум до `1.14.9` (он тянет Java 25-совместимый Byte Buddy `1.18.x`). Не оставляйте +`-Dnet.bytebuddy.experimental=true` как постоянное решение: флаг только отключает защитную проверку +неподдерживаемой версии class-файла. Источник зависимости проверьте командой +`./gradlew :dependencyInsight --dependency byte-buddy --configuration testRuntimeClasspath`. + +**Проверять после миграции**, а не полагаться на компиляцию: все три отказа рантаймовые, а +Byte Buddy к тому же прячется внутри `Application graph failed to initialize with N errors` +без видимых suppressed-исключений (Gradle их не печатает — временно включите `junitXml.required`). + +Gradle placeholder `environment(["": ""])` нужно удалить полностью. Пустое имя переменной окружения +недопустимо на Windows: test JVM не стартует с `CreateProcess error=87, The parameter is incorrect`. + +### 1.6 Процессорам нужна полная перекомпиляция + +При инкрементальной сборке процессор базы данных может прочитать интерфейс репозитория из +class-файла, где имена параметров синтетические, и сообщить: + +``` +error: SQL query placeholder has no matching method parameter: + :id + Available parameters: + - :arg0 +``` + +Исходный код при этом корректен — `--rerun-tasks` или `clean` на модуле убирает ошибку. +Сообщение на причину не указывает, поэтому при странных отказах процессоров начинайте с чистой сборки. + +--- + +## 2. Пакеты и аннотации + +Все пакеты фреймворка: `ru.tinkoff.kora.*` → `io.koraframework.*`. + +DI-аннотации переехали на уровень глубже — в `annotation`: + +| Было | Стало | +|---|---| +| `ru.tinkoff.kora.common.KoraApp` | `io.koraframework.common.annotation.KoraApp` | +| `ru.tinkoff.kora.common.Component` | `io.koraframework.common.annotation.Component` | +| `ru.tinkoff.kora.common.DefaultComponent` | `io.koraframework.common.annotation.DefaultComponent` | +| `ru.tinkoff.kora.common.KoraSubmodule` | `io.koraframework.common.annotation.KoraSubmodule` | +| `ru.tinkoff.kora.common.Mapping` | `io.koraframework.common.annotation.Mapping` | +| `ru.tinkoff.kora.common.Module` | `io.koraframework.common.annotation.Module` | +| `ru.tinkoff.kora.common.Tag` | `io.koraframework.common.annotation.Tag` | + +```kotlin +@KoraApp +interface Application : HoconConfigModule, LogbackModule, UndertowPublicHttpServerModule + +fun main() = KoraApplication.run(ApplicationGraph::graph) +``` + +--- + +## 3. Нуллабельность: убрать аннотации, использовать типы Kotlin + +**Было (1.x):** в Kotlin-коде встречались `jakarta.annotation.Nullable`, в том числе как `@field:Nullable`. + +**Стало:** нуллабельность выражается **типом** — `T?`. Java/JSpecify-аннотации из Kotlin-кода удаляются. + +```kotlin +// Было +class UserRequest(@field:Nullable val name: String) + +// Стало +class UserRequest(val name: String?) +``` + +**Причина:** JSpecify-аннотации являются type-use; `@field:Nullable` под Kotlin 2.4 — некорректная цель, а дублирование нуллабельности аннотацией и типом расходится с платформенными типами при взаимодействии со сгенерированным Java-кодом. + +**Автоматизация:** удаление импортов и простых форм `@Nullable` покрыто скриптом; случаи, где аннотация несла смысл (например, платформенные типы на границе с Java), требуют ручного решения. + +--- + +## 4. HTTP-сервер + +| Было | Стало | +|---|---| +| `UndertowHttpServerModule` | `UndertowPublicHttpServerModule` | +| `http.server.common.HttpServerRequest` | `http.server.common.request.HttpServerRequest` | +| `http.server.common.HttpServerResponse` | `http.server.common.response.HttpServerResponse` | +| `http.server.common.HttpServerResponseException` | `http.server.common.response.HttpServerResponseException` | +| `http.server.common.HttpServerInterceptor` | `http.server.common.interceptor.HttpServerInterceptor` | +| `http.server.common.handler.HttpServerRequestMapper` | `http.server.common.request.HttpServerRequestMapper` | +| `http.server.common.handler.HttpServerResponseMapper` | `http.server.common.response.HttpServerResponseMapper` | +| `http.client.common.HttpClientResponseException` | `http.client.common.exception.HttpClientResponseException` | + +Обработка HTTP синхронная: из контрактов уходят `suspend`, реактивные обёртки и явный Kora `Context` (он удалён из синхронных HTTP API — механической замены нет, сигнатура переписывается). + +**Тег глобального интерцептора сменился:** + +| Было | Стало | +|---|---| +| `@Tag(HttpServerModule::class)` | `@Tag(HttpServer::class)` | +| `import …http.server.common.HttpServerModule` | `import …http.server.common.HttpServer` | + +В 2.0 фреймворк собирает глобальные интерцепторы по тегу `HttpServer` (`@Tag(HttpServer.class) All` в `HttpServerModule`). + +**Коварство:** старый тег компилируется без ошибок — класс существует, просто по нему никто не ищет интерцепторы. Интерцептор молча перестаёт вызываться — ловится только тестом. + +Интерцептор (сигнатура 2.0): + +```kotlin +@Tag(HttpServer::class) +@Component +class HttpExceptionHandler(private val errorWriter: JsonWriter) : HttpServerInterceptor { + + override fun intercept(request: HttpServerRequest, chain: HttpServerInterceptor.InterceptChain): HttpServerResponse = + try { + chain.process(request) + } catch (e: HttpServerResponseException) { + e + } catch (e: Exception) { + HttpServerResponse.of(500, HttpBody.json(errorWriter.toByteArray(MessageTO(e.message)))) + } +} +``` + +`JsonWriter.toByteArrayUnchecked(value)` удалён — используйте `toByteArray(value)`. В 2.0 этот метод не объявляет checked-исключений, поэтому блоки `catch (e: IOException)` вокруг него становятся лишними (в Kotlin это не ошибка компиляции, но мёртвый код; в Java — ошибка). + +### Реактивные и `suspend`-контроллеры удалены + +Для возврата `Mono`/`Flux` в `http-server-common` нет мапперов (проверено по исходникам) — такие методы переводятся на синхронный возврат `HttpServerResponse`. + +Методы `@HttpController`/`@HttpRoute` также не должны оставаться `suspend`. Если отдельный +suspend controller только дублирует уже имеющийся синхронный пример, удалите controller и его +route-тест целиком. Иначе снимите `suspend` и синхронно перепишите всю вызывающую цепочку. +После удаления последнего coroutine usage удалите `kotlinx-coroutines-core`/`jdk8` из модуля. + +OpenRewrite recipe `RemoveSuspendHttpServerMethods` и Python migrator снимают `suspend` в +файлах controller-контрактов. Удаление дубликатов, тестов и dependency требует проверки модуля. + +**Диагностическая ловушка:** пока в графе остаётся неразрешимый компонент, сообщение об ошибке может указывать на другой, исправный компонент. Сначала убирайте код на удалённой функциональности, потом разбирайте остаток. + +### HTTP-клиент: `suspend` API запрещён + +В Kora 2.0 методы декларативного `@HttpClient` должны быть синхронными. Удалите `suspend` +из сигнатур клиента. Не оставляйте в том же интерфейсе default-обёртки вида +`suspend fun get(...) = withContext(Dispatchers.IO) { getBlocking(...) }`: это по-прежнему +публичный coroutine API HTTP-клиента и маскирует синхронный контракт. + +```kotlin +// 1.x +suspend fun get(id: Long): Pet + +// 2.0 +fun get(id: Long): Pet +``` + +После замены уберите ставшие ненужными `runBlocking`/`runTest`, coroutine-тесты и импорты. +Если модуль больше не использует корутины, удалите также прямые зависимости +`org.jetbrains.kotlinx:kotlinx-coroutines-core` и `kotlinx-coroutines-jdk8`. Не удаляйте их +раньше переписывания всех usages, но и не оставляйте как финальное решение для structured +parallelism: `coroutineScope`/`supervisorScope`/`async`/`await` мигрируют по §13. +Если корутины действительно нужны бизнес-логике, мост размещается в отдельном application +service, но не в `@HttpClient` interface. Дублирующий suspend-only клиент лучше удалить целиком. + +OpenRewrite recipe `RemoveSuspendHttpClientMethods` и Python migrator снимают `suspend` в +HTTP-client contracts. Переписывание вызывающей цепочки остаётся семантической проверкой. + +### HTTP-клиент: `configPath` → `value` + +**Было:** + +```kotlin +@HttpClient(configPath = "httpClient.userApi") +interface UserApiClient { … } +``` + +**Стало:** + +```kotlin +@HttpClient("httpClient.userApi") +interface UserApiClient { … } +``` + +**Причина:** в 2.0 аннотация объявлена как `String value() default ""` плюс `telemetryTag()` и `httpClientTag()`; атрибута `configPath` больше нет. + +**Автоматизация:** детерминированная замена. + +**Мапперы и интерцепторы обязаны быть DI-компонентами.** Генерируемый модуль контроллера инжектит их, а не создаёт: + +```kotlin +@Component +class UserContextRequestMapper : HttpServerRequestMapper { … } +``` + +Симптом при отсутствии `@Component`: `No component found for dependency: … (no tags)`. + +### Сигнатура `HttpServerResponseMapper` в Kotlin + +Интерфейс во фреймворке помечен JSpecify (`@NullMarked` + `@Nullable` на втором параметре). +Kotlin проверяет нуллабельность переопределений строго, поэтому второй параметр обязан быть `T?`: + +```kotlin +@Component +class PetResponseMapper : HttpServerResponseMapper { + override fun apply(request: HttpServerRequest, result: Pet?): HttpServerResponse { … } +} +``` + +С `result: Pet` компилятор выдаёт `'apply' overrides nothing` — сообщение об ошибке +не упоминает нуллабельность, поэтому его легко принять за неверный дженерик. + +То же касается `@Mapping`-мапперов параметров и любых других переопределений контрактов Kora: +в 2.0 API размечено JSpecify, и Kotlin это видит, тогда как Java — нет. **Это асимметрия: +Java-модуль соберётся, Kotlin-двойник того же кода — нет.** + +Метод, ничего не возвращающий, требует `VoidResponseMapper` — отдельного типа для `Unit` +в 2.0 нет, а `HttpServerResponseMapper` не разрешается. + +--- + +## 5. Конфигурация + +### 5.1 Порты HTTP-сервера — молчаливый убийца старта + +| Было (1.x) | Стало (2.0) | +|---|---| +| `httpServer.publicApiHttpPort` | `httpServer.port` | +| `httpServer.privateApiHttpPort` | `httpServer.system.port` | +| `httpServer.privateApiHttpReadinessPath` | `httpServer.system.readinessPath` | +| `httpServer.privateApiHttpLivenessPath` | `httpServer.system.livenessPath` | +| `httpServer.privateApiHttpMetricsPath` | `httpServer.system.metricsPath` | + +```hocon +# Было # Стало +httpServer { httpServer { + publicApiHttpPort = 8080 port = 8080 + privateApiHttpPort = 8085 system.port = 8085 +} } +``` + +**Почему критично.** `SystemHttpServerConfig extends HttpServerConfig`, то есть системный сервер наследует `port()` = `8080`. Пока старый ключ не распознан, **оба** сервера берут 8080 и приложение падает на старте (`Address already in use`). Компиляция при этом зелёная: лишние ключи просто игнорируются. + +**Автоматизация:** `python migration/scripts/migrate_config_keys.py --apply`. + +### 5.2 Секция JDBC: `db` → `jdbc` + +```hocon +# Было # Стало +db { jdbc { + jdbcUrl = jdbcUrl = + username = username = +} } +``` + +**Причина:** `JdbcDatabaseModule` в 2.0 создаёт `new JdbcDatabaseFactoryModule("jdbc")`. + +**Симптом:** компиляция зелёная, падение на старте: `ConfigValueException: … got null at path: 'ROOT.jdbc.username'`. + +**Автоматизация:** `migrate_config_keys.py`. + +### 5.3 Остальное + +| Было | Стало | +|---|---| +| `@ConfigValueExtractor` | `@ConfigMapper` | +| пакет `config.common.extractor` | удалён | + +### 5.4 Телеметрия: метрики выключены по умолчанию + +`TelemetryConfig.MetricsConfig.enabled()` в 2.0 возвращает `false`. Приложение стартует, эндпоинт +метрик отвечает `200`, но `http_server_*`, `http_client_*`, `db_*` и прочие метрики компонентов +в выводе отсутствуют — видны только JVM-метрики, которые регистрирует сам реестр. + +```hocon +httpServer { + telemetry.logging.enabled = true + telemetry.metrics.enabled = true # в 2.0 обязательно явно +} +``` + +Тот же ключ есть у каждого компонента с телеметрией (`httpClient..telemetry.metrics.enabled`, +`db.telemetry.metrics.enabled` и т. д.). Логирование (`logging.enabled`) тоже `false` по умолчанию, +трассировка (`tracing.enabled`) — `true`. + +**Молчаливый отказ:** ничего не падает и ничего не пишется в лог, метрики просто не собираются. + +--- + +## 6. Отказоустойчивость: строковые имена → типизированные спецификации + +**Было:** + +```kotlin +@CircuitBreaker("pet") +@Retry("pet") +@Timeout("pet") +fun findById(petId: Long): PetWithCategory? +``` + +**Стало:** + +```kotlin +@CircuitBreakable(PetCircuitBreaker::class) +@Retryable(PetRetry::class) +@Timeout(PetTimeouter::class) +fun findById(petId: Long): PetWithCategory? + +@CircuitBreakerSpec("resilient.circuitbreaker.pet") +interface PetCircuitBreaker : CircuitBreaker + +@RetrySpec("resilient.retry.pet") +interface PetRetry : Retry + +@TimeoutSpec("resilient.timeout.pet") +interface PetTimeouter : Timeouter +``` + +| Было | Стало | +|---|---| +| `@CircuitBreaker("name")` | `@CircuitBreakable(X::class)` | +| `@Retry("name")` | `@Retryable(X::class)` | +| `@Timeout("name")` | `@Timeout(X::class)` | +| `@Fallback(value = "name", method = "…")` | `@Fallback(method = "…")` | + +Конфигурация: `slidingWindowSize` → `countBased.windowSize`, для окна фиксированного размера обязателен `type = FIXED_WINDOW`. + +**Спецификации по умолчанию тоже типизированы.** Если раньше аннотация без имени брала секцию +`default`, теперь нужен явный тип из `io.koraframework.resilient.*`: + +```kotlin +@Timeout(DefaultTimeouter::class) +@Retryable(DefaultRetry::class) +@CircuitBreakable(DefaultCircuitBreaker::class) +fun call(): String +``` + +**Именованная секция больше не наследует `default`.** В 1.x `resilient.circuitbreaker.my_cb` +дополняла `resilient.circuitbreaker.default`; в 2.0 секции независимы, и незаполненные поля +берутся из значений по умолчанию **самого типа**, а не из соседней секции. Практический эффект: +конфигурация, где в `my_cb` указан только один параметр, в 2.0 ведёт себя иначе — секцию нужно +заполнить целиком. Секция `default`, на которую больше никто не ссылается, становится мёртвой. + +**Предикат отказа сменил интерфейс:** + +| Было | Стало | +|---|---| +| `CircuitBreakerFailurePredicate` | `CircuitBreakerPredicate` | +| `test(throwable)` | `isCircuitBreakerFailure(throwable)` | + +Свой предикат подключается тегом спецификации, к которой он относится: + +```kotlin +@Component +@Tag(DefaultCircuitBreaker::class) +class MyPredicate : CircuitBreakerPredicate { + override fun isCircuitBreakerFailure(throwable: Throwable): Boolean = throwable !is IllegalArgumentException +} +``` + +**Важно для Kotlin:** пока строковая форма остаётся в коде, KSP-процессор resilient падает с +`java.lang.ClassCastException: java.lang.String cannot be cast to com.google.devtools.ksp.symbol.KSType` +вместо внятной диагностики. Это подтверждённая связка «немигрированный код → краш процессора» (см. `KORA_2_FRAMEWORK_ISSUES.md`). + +**Автоматизация:** нет — требуется завести новые типы и связать их с секциями конфигурации. + +--- + +## 7. Кеш + +| Было | Стало | +|---|---| +| `@Cacheable(value = X::class, parameters = "id")` | `@Cacheable(value = X::class, args = "id")` | +| `@CacheInvalidate(value = X::class, invalidateAll = true)` | `@CacheInvalidateAll(X::class)` | +| `RedisCacheModule` | `LettuceRedisCacheModule` (артефакт `cache-redis-lettuce`) | + +`RedisCacheModule` в 2.0 существует (`cache-redis-common`), но транспортно-нейтрален и сам `RedisCacheClient` не предоставляет. + +Классы-мапперы ключей, указанные в `@Mapping`, должны быть `@Component` — включая вложенные классы. + +--- + +## 8. База данных и `suspend` + +- `database.jdbc.EntityJdbc` → `database.jdbc.annotation.EntityJdbc`. +- Контракты репозиториев в 2.0 **синхронные**. `suspend`-репозитории и корутинные контракты удалены. +- Если suspend repository был отдельным дубликатом уже существующего sync repository, удалите + его целиком вместе с DI-ссылками и отдельными coroutine-тестами. Не создавайте второй + синхронный интерфейс с теми же запросами. + +**Было:** + +```kotlin +@Repository +interface PetRepository : JdbcRepository { + suspend fun findById(id: Long): Pet? +} +``` + +**Стало:** + +```kotlin +@Repository +interface PetRepository : JdbcRepository { + fun findById(id: Long): Pet? +} +``` + +**Это не механическое удаление `suspend`.** Снятие `suspend` распространяется вверх по цепочке вызовов: сервисы, контроллеры, тесты. Меняются отмена (structured concurrency больше не отменяет операцию БД), границы транзакций и распространение исключений. Тесты на `runTest`/`runBlocking` перестают быть нужны там, где вызовы стали синхронными. + +Сохранять `suspend` «ради стиля» поверх синхронного контракта не следует. Если собственный +application-код использовал Kotlin Structured Concurrency для параллелизма, его также нужно +перевести на Java Structured Concurrency по правилам §13, а не оставлять coroutine-мост. + +После удаления последнего coroutine usage удалите из модуля прямые зависимости +`kotlinx-coroutines-core`/`kotlinx-coroutines-jdk8`. Если корутины используются собственной +бизнес-логикой вне repository-контракта, зависимость сохраняется. + +### Ручные транзакции + +`executor().inTx { … }` с лямбдой Kotlin больше не выводится: перегрузок стало несколько, +и компилятор не выбирает между ними. Нужен явный SAM-конструктор: + +```kotlin +// значение возвращается +val pet = repository.executor().inTx(JdbcExecutor.SqlSupplier { + repository.insert(name) +}) + +// ничего не возвращается +repository.executor().inTx(JdbcExecutor.SqlRunnable { + repository.deleteAll() +}) +``` + +Без явного `SqlSupplier`/`SqlRunnable` ошибка выглядит как `Cannot infer type for type parameter T` +или `Overload resolution ambiguity` и на транзакции не указывает. + +**Мапперы результатов и колонок принимают nullable.** `@Mapping`-классы для строк и колонок +переопределяют JSpecify-размеченные контракты, поэтому параметры объявляются как `T?` +— та же ловушка, что с `HttpServerResponseMapper` (§4). + +**Удалённые интеграции:** R2DBC и Vert.x (см. §12). + +--- + +## 9. Планировщик + +**Было:** `@ScheduleWithTrigger(Tag(MyJob::class))` +**Стало:** `@ScheduleWithTrigger(MyJob::class)` + +--- + +## 10. OpenAPI-генерация + +Остались режимы `kotlin-client` и `kotlin-server`: + +| Было | Стало | +|---|---| +| `kotlin-suspend-client` | `kotlin-client` | +| `kotlin-suspend-server` | `kotlin-server` | +| `kotlin-reactive-server` | `kotlin-server` | + +Смена режима необходима, но недостаточна — сгенерированный код синхронный, и вызывающий код (делегаты, сервисы, тесты) нужно приводить к синхронным сигнатурам. Типичные ошибки после смены режима: `Cannot infer type for type parameter`, `'X' overrides nothing`, `Annotation argument must be a compile-time constant`. + +### 10.1 Путь конфигурации клиента: первая буква строчная + +Генератор 2.0 выводит путь конфигурации из имени API, приводя первую букву к строчной: +`PetApi` → `petApi`. Итоговый ключ — `<префикс>.<клиент>.`: + +```hocon +# Было +httpClient.petV2.PetApi { url = … } + +# Стало +httpClient.petV2.petApi { url = … } +``` + +**Молчаливый отказ.** Секция со старым именем просто не читается: клиент поднимается без `url`, +и тесты не падают с внятной ошибкой — они висят на попытках запроса до таймаута. Ключ стоит +сверять со сгенерированным `@HttpClient`, а не подбирать. + +### 10.2 Конструкторы моделей упорядочены по обязательности + +Генератор 2.0 ставит required-поля первыми, а необязательные — после. Порядок аргументов +у сгенерированных TO меняется по сравнению с 1.x, причём **совместимо по типам**, поэтому +позиционный вызов может собраться и молча перепутать значения. + +Единственный безопасный вариант — именованные аргументы: + +```kotlin +return PetTO(status = status, id = pet.id, name = pet.name, category = asDTO(pet.category)) +``` + +### 10.3 Enum ищется по wire-значению, а не по имени константы + +У сгенерированных enum есть поле с исходным значением из спецификации, и оно может не совпадать +с именем константы (`available` против `AVAILABLE`, значения через дефис и т. п.). +`Enum.valueOf(...)` / `enumValueOf(...)` для разбора приходящего значения — ошибка: + +```kotlin +// неверно: падает на любом значении, не совпадающем с именем константы +val status = PetTO.StatusEnum.valueOf(raw) + +// верно: используем метод, сгенерированный Kora OpenAPI 2.0 +val status = PetTO.StatusEnum.fromValue(raw) +``` + +Проявляется только на данных, компиляция чистая. + +### 10.4 Теги требований безопасности называются по security-схеме + +В Kora OpenAPI 2.0 генератор создаёт вложенный tag-класс из имени схемы в +`components.securitySchemes`, приводя его к PascalCase. Старые порядковые +`ApiSecurity.SecurityRequirementTagN` и варианты с маленькой буквы больше не подходят. + +```kotlin +@Tag(ApiSecurity.ApiKeyAuth::class) +fun apiKeyExtractor(config: DataApiAuthConfig): HttpServerPrincipalExtractor = + HttpServerPrincipalExtractor { _, value -> … } +``` + +Тип второго параметра — то, что схема извлекает из запроса (`String` для apiKey/bearer), +первого результата — `Principal`. + +### 10.5 Управление OpenAPI-эндпоинтом + +| Было | Стало | +|---|---| +| `openapi.management.file` | `openapi.management.files` (список) | +| `openapi.management.rapidoc` | `openapi.management.scalar` | + +Ключ `file` в 2.0 не читается — эндпоинт молча отдаёт пустую спецификацию. + +### 10.6 `suspend` на сгенерированных клиентах не поддерживается + +Режим `kotlin-client` порождает синхронные методы, и пометить их `suspend` нельзя. +Если вызывающий код обязан остаться корутинным, мост делается на своей стороне: + +```kotlin +suspend fun findPet(id: Long): PetTO = withContext(Dispatchers.IO) { petApi.getPetById(id) } +``` + +Это осознанное решение, а не механическая замена: под виртуальными потоками +`Dispatchers.IO` обычно не нужен (§13). + +### 10.7 `ValidationModule` тянет http-server-common + +Подключение `ValidationModule` в клиентском приложении добавляет в граф зависимость +на `http-server-common` (через `ViolationExceptionHttpServerResponseMapper`). Для приложения +без HTTP-сервера это лишний артефакт в classpath; обходится добавлением зависимости, +поскольку разделить модуль на стороне приложения нельзя. + +--- + +## 11. Тестирование + +Общую версию JUnit обновите до `6.1.3`: + +```properties +junitVersion=6.1.3 +``` + +Используйте `testImplementation(platform("org.junit:junit-bom:${property("junitVersion")}"))`. +`org.testcontainers:junit-jupiter` — интеграция Testcontainers, её версия независима от JUnit BOM. + +Артефакт тестирования — `io.koraframework:test-junit5`. Основной процессор подключается как +`ksp("io.koraframework:symbol-processors:${property("koraVersion")}")`; test-конфигурация процессора +нужна только тестам, которые сами генерируют Kora graph. Не удаляйте `kspTest` массовой +заменой: сначала найдите все `@KoraApp`, объявленные именно в `src/test`. +Например, `@KoraAppTest(TestApplication::class)` для `TestApplication`, объявленного в +`src/test`, требует `kspTest("io.koraframework:symbol-processors:${property("koraVersion")}")`. + +Пакет расширения: `io.koraframework.test.extension.junit5.*` — `@KoraAppTest`, `@TestComponent`, `KoraAppTestConfigModifier`, `KoraConfigModification`. + +Тесты, написанные на `runTest` вокруг `suspend`-репозиториев, после перехода на синхронные контракты упрощаются до обычных тестов. MockK-моки `coEvery` заменяются на `every`. + +В Kotlin-модулях не создавайте отдельную конфигурацию `koraBom` и не связывайте её через +`extendsFrom`. BOM подключается прямо к `implementation`, а версия процессора указывается явно: + +```kotlin +dependencies { + implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}")) + ksp("io.koraframework:symbol-processors:${property("koraVersion")}") +} +``` + +`kspTest("io.koraframework:symbol-processors:${property("koraVersion")}")` добавляется только +когда тестовые исходники действительно требуют генерации графа, например содержат отдельный +`@KoraApp`. В текущих примерах это требуется только `examples/kotlin/kora-kotlin-crud`. + +--- + +## 11a. Отдельные интеграции + +### Kafka + +Ридер `JsonReader.read(data)` в 2.0 объявлен возвращающим nullable, поэтому в Kotlin +результат не присваивается non-null типу напрямую: + +```kotlin +val event = requireNotNull(reader.read(data)) +``` + +Слушатели телеметрии (`KafkaConsumerLoggerFactory` и подобные пользовательские фабрики) в 2.0 +не подключаются как раньше; в примерах они удалены, а телеметрия настраивается конфигурацией. + +### Camunda Zeebe + +| Было | Стало | +|---|---| +| `ZeebeClient` | `CamundaClient` | +| `…camunda.zeebe.worker.JobWorkerException` | `…camunda.zeebe.worker.exception.JobWorkerException` | +| `zeebe.client.broker.gatewayAddress` | `zeebe.client.rest.url` | + +Клиент в тестах инжектится напрямую, без `@TestComponent` на поле: с аннотацией он подменяется +моком и воркеры не срабатывают. + +### S3 + +Артефакт 1.x `experimental:s3-client-*` разделён на два, и нужны **оба**: +`io.koraframework:s3-client-aws` (или `-minio`) даёт транспорт, а +`io.koraframework.experimental:s3-client-kora` — декларативный `@S3` и его KSP-процессор. +Подключение только первого компилируется, но `@S3`-интерфейсы не обрабатываются. + +Инициализатор бакета должен быть корнем графа (`@Root`), иначе он выпадает из графа +как никем не используемый и бакет не создаётся — тесты падают на первой операции. + +### Конфигурация в библиотечном модуле + +`@ConfigMapper` (бывш. `@ConfigValueExtractor`) обрабатывается KSP, поэтому модуль-библиотека, +объявляющая конфиг-интерфейсы, обязан сам подключать KSP-плагин и `ksp("io.koraframework:symbol-processors")`. +В 1.x такие модули часто обходились без процессора, и после миграции ошибка выглядит как +отсутствие сгенерированного `*Module` у потребителя, а не у библиотеки. + +--- + +## 12. Удалённая и несовместимо изменённая функциональность + +| Функциональность 1.x | Состояние в 2.0 | Что делать | +|---|---|---| +| R2DBC (`database-r2dbc`) | удалён | переводить на синхронный `database-jdbc` | +| Vert.x SQL (`database-vertx`) | удалён | то же | +| `suspend`-репозитории | удалены | синхронные методы | +| корутинные контракты в модулях Kora | удалены | синхронные контракты | +| реактивные контракты репозиториев | удалены | синхронные контракты | +| `http-client-async` | удалён | `http-client-jdk` / `http-client-ok` | +| `@ConfigValueExtractor` | переименован | `@ConfigMapper` | +| `config.common.extractor` | пакет удалён | замена в `io.koraframework.config.common` | +| `Context` в синхронных HTTP API | удалён | переписать сигнатуру | +| `toByteArrayUnchecked` | удалён | `toByteArray` | +| `invalidateAll = true` | удалён | `@CacheInvalidateAll` | +| `KspTask` (тип Gradle) | удалён в KSP 2 | привязка задач по имени | + +Модули примеров, зависящие от удалённого, не удалены: они исключены из агрегатной сборки и помечены `BLOCKED_BY_REMOVED_FUNCTIONALITY` в `KORA_2_MIGRATION_STATUS.md`. + +--- + +## 13. Синхронная модель, Virtual Threads и Java Structured Concurrency + +Kora 2.0 исполняет синхронные контракты на виртуальных потоках. Kotlin-код, который был +`suspend` только ради неблокирующего доступа к БД или HTTP, переводится в синхронный. + +### 13.1 Обязательная замена Kotlin Structured Concurrency + +Все конструкции Kotlin Structured Concurrency (`coroutineScope`, `supervisorScope`, +`async`/`await`, `awaitAll`, дочерние `launch`, structured cancellation и timeout scope) +переводятся на Java Structured Concurrency: `StructuredTaskScope`, `fork`, `join`, подходящий +`Joiner` и timeout configuration. После миграции удаляются coroutine imports и зависимости +`kotlinx-coroutines-core`/`kotlinx-coroutines-jdk8`, если иных usages не осталось. + +**Версионное правило:** брать не зафиксированный в этом документе старый JDK, а последний +доступный **GA feature release Java** на момент миграции и последнюю итерацию Structured +Concurrency Preview именно из него. Не использовать EA в production только ради более нового +preview. На момент актуализации гайда (август 2026) это **JDK 26 + Structured Concurrency Sixth +Preview (JEP 525)**. Нельзя копировать `StructuredTaskScope` API из JDK 21–25: preview API +менялся, включая factories, `Joiner` и тип результата `join()`. + +Пример для актуального JDK 26: + +```kotlin +import java.util.concurrent.Callable +import java.util.concurrent.StructuredTaskScope + +fun getDashboard(userId: Long): Dashboard = + StructuredTaskScope.open( + StructuredTaskScope.Joiner.awaitAllSuccessfulOrThrow(), + ).use { scope -> + val profile = scope.fork(Callable { profileClient.getProfile(userId) }) + val recommendations = scope.fork(Callable { recommendationClient.getForUser(userId) }) + + scope.join() + Dashboard(profile.get(), recommendations.get()) + } +``` + +Выбор policy переносится явно: + +- fail-fast и отмена siblings при ошибке → `awaitAllSuccessfulOrThrow()`; +- первый успешный результат → актуальный `Joiner` из API выбранного JDK; +- supervisor-подобная семантика → `awaitAll()` и явная проверка `Subtask.state()`/`exception()`; +- `withTimeout` → timeout в `StructuredTaskScope.Configuration`; +- cancellation и exception propagation обязательно покрываются тестами заново. + +### 13.2 Preview должен быть включён везде + +Одного флага на компиляции недостаточно. `--enable-preview` обязателен при компиляции и на +каждом JVM-запуске: tests, `JavaExec`, application distribution/production command. Версия +`--release` обязана совпадать с выбранным latest GA JDK. + +```kotlin +val latestJava = 26 // обновить до latest GA на момент миграции + +java { + toolchain.languageVersion.set(JavaLanguageVersion.of(latestJava)) +} + +kotlin { + jvmToolchain(latestJava) + compilerOptions { + // Kotlin sources also use the selected JDK API and emit preview bytecode. + freeCompilerArgs.addAll( + "-Xjdk-release=$latestJava", + "-Xjvm-enable-preview", + ) + } +} + +tasks.withType().configureEach { + options.release.set(latestJava) + options.compilerArgs.add("--enable-preview") +} + +tasks.withType().configureEach { + jvmArgs("--enable-preview") +} + +tasks.withType().configureEach { + jvmArgs("--enable-preview") +} + +application { + // Merge with existing arguments instead of dropping application-specific JVM flags. + applicationDefaultJvmArgs = listOf("--enable-preview", "-Dfile.encoding=UTF-8") +} +``` + +Для прямого запуска: `java --enable-preview -jar application.jar`. Образ контейнера и runtime +обязаны использовать тот же major JDK, которым скомпилирован preview-код. + +Checklist перед merge: + +1. Проверить latest GA на `openjdk.org/projects/jdk` и обновить toolchain/CI/container runtime. +2. Открыть JEP и Javadoc Structured Concurrency именно выбранного JDK; перепроверить `open`, + `Joiner`, тип результата `join`, timeout и exception types. +3. Собрать main/test Kotlin и Java с preview; запустить unit/integration tests с preview. +4. Проверить production launcher, Docker/Kubernetes command и IDE run configurations: + в каждом месте должен быть `--enable-preview`. +5. Удалить `kotlinx-coroutines-*`, только когда repository-wide audit больше не находит usages. + +Это семантическая миграция: OpenRewrite/Python не могут автоматически выбрать `Joiner`, timeout, +failure/cancellation policy или восстановить supervisor semantics. Automation только находит и +удаляет запрещённые Kora contracts; structured concurrency переписывается разработчиком. + +Изменения, которые нужно продумать при снятии `suspend`: отмена, границы транзакций, распространение исключений, необходимость `Dispatchers.IO` (не нужен — виртуальные потоки), тесты. + +--- + +## 15. GraalVM Native Image + +В репозитории нет Kotlin-модулей с native-сборкой — все три GraalVM-примера на Java. Поэтому +ниже явно разделено: §15.1–§15.7 — правила, **проверенные сборкой и запуском** образов и не +зависящие от языка (это конфигурация Gradle и метаданные самого фреймворка), §15.8 — то, что +специфично для Kotlin и **не измерено** на этом репозитории. + +Окружение проверки: GraalVM CE 25.2.4+7.1 (`native-image 25.0.4`), установка — +`sdk install java 25.2.4-graalce`; образ для сборки в Docker — `ghcr.io/graalvm/native-image-community:25`. + +### 15.1. Плагин `org.graalvm.buildtools.native`: 0.11.5 → 1.1.7 + +**Было / Стало** — в корневом build-файле: + +```groovy +id "org.graalvm.buildtools.native" version "1.1.7" apply false // было 0.11.5 +``` + +**Причина:** на Gradle 9 `collectReachabilityMetadata` падает на резолве конфигурации из чужого +проекта: до 1.1.6 плагин регистрировал один shared build service `nativeConfigurationService` +на весь build, и все GraalVM-модули кроме первого получали сервис из чужого контекста. +В 1.1.6 имя сервиса стало включать `project.getPath()` +([native-build-tools#760](https://github.com/graalvm/native-build-tools/issues/760)). + +**Ограничение:** ошибка возникает на конфигурации всего build'а, поэтому «собирать по одному +модулю» как обход не работает. + +### 15.2. GraalVM 25 вместо 21 + +Меняется в трёх местах сразу — toolchain модуля, launcher самого native-образа и базовый образ Docker: + +```groovy +kotlin { + jvmToolchain(25) // было 21 +} + +graalvmNative { + binaries { + main { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(25) // было 21 + vendor = JvmVendorSpec.matching("GraalVM Community") + } + } + } +} +``` + +```dockerfile +FROM ghcr.io/graalvm/native-image-community:25 as builder # было :21 +``` + +**Причина:** артефакты Kora 2.0 собраны под JVM 25, а `native-image` не читает class-file формат +новее своего JDK. Отдельно проверьте, что `kotlin.jvmToolchain` и `jvmTarget` согласованы — рассогласование +даёт ошибку уже на этапе компиляции, а не сборки образа. + +### 15.3. `imageName` и `mainClass`: передавать провайдер, а не интерполяцию + +**Было** (Groovy DSL): + +```groovy +imageName = "$project.name" +mainClass = "$application.mainClass" +``` + +**Стало:** + +```groovy +imageName = project.name +mainClass = application.mainClass +``` + +В Kotlin DSL (`build.gradle.kts`) то же самое пишется явно через `set(...)`: + +```kotlin +imageName.set(project.name) +mainClass.set(application.mainClass) +``` + +**Причина:** `application.mainClass` — это `Property`. При строковой интерполяции в командную +строку `native-image` попадает не имя класса, а отладочное представление вида +`property(java.lang.String, fixed(...))`. Старый плагин разворачивал значение раньше и это работало +по совпадению; в 1.1.7 — нет. + +**Поведенческое изменение:** проявляется как «main class not found» на этапе сборки образа, а не +как ошибка конфигурации Gradle. + +### 15.4. `jar.enabled = false` больше нельзя + +**Было:** `jar.enabled = false`, чтобы в `build/libs` оставался только shadow-артефакт. + +**Стало:** строка удалена. + +**Причина:** `nativeCompile` строит classpath из артефактов самого проекта. Без обычного `jar` +в classpath оказываются только зависимости, но не классы приложения (shadow-jar туда не входит — +он собирается для Docker-пути). + +### 15.5. Метаданные достижимости: что теперь даёт сам фреймворк + +Метаданные едут внутри артефактов Kora (`META-INF/native-image/<модуль>/`) и от языка приложения +не зависят. При миграции выяснилось, что пять наборов были неполными или не читались вовсе; +все пять исправлены в upstream: + +| Симптом в native-образе | Модуль | PR | +|---|---|---| +| `java.lang.IllegalArgumentException: No XNIO provider found` на старте HTTP-сервера | `http-server-undertow` | [#811](https://github.com/kora-projects/kora/pull/811) | +| HikariCP не находит micrometer-трекер метрик | `database-jdbc` | [#812](https://github.com/kora-projects/kora/pull/812) | +| кеш Caffeine без статистики падает на создании | `cache-caffeine` | [#813](https://github.com/kora-projects/kora/pull/813) | +| регистрации молча не применяются | `micrometer-module`, `grpc-server`, `kafka` | [#814](https://github.com/kora-projects/kora/pull/814) | +| сборка образа падает на этапе анализа | `database-cassandra` | [#815](https://github.com/kora-projects/kora/pull/815) | + +Самый переносимый вывод — четвёртая строка: файл с именем **`reflection-config.json`** native-image +не читает вовсе, правильное имя — **`reflect-config.json`**. Диагностики нет никакой: сборка идёт +успешно, просто регистрации никогда не применяются. В своём проекте это проверяется одной командой: + +```shell +find . -name "reflection-config.json" # каждое попадание — мёртвый файл +``` + +**Ограничение (важное):** не удаляйте собственные метаданные приложения на том основании, +что приложение работает на JVM. При переименовании пакетов каталог `META-INF/native-image//` +переименовывается вручную — ни OpenRewrite, ни скрипт каталоги ресурсов не трогают. + +### 15.6. Как метаданные попадают в образ, собираемый в Docker + +Два пути сборки ведут себя по-разному: + +- `nativeCompile` сам подкладывает metadata repository (`graalvmNative.metadataRepository.enabled = true`); +- `native-image -cp application.jar` внутри Docker читает только то, что лежит в самом jar. + +Чтобы второй путь видел те же метаданные, их собирают в ресурсы до упаковки: + +```groovy +processResources.dependsOn tasks.collectReachabilityMetadata +sourceSets.main { resources.srcDirs += "$buildDir/native-reachability-metadata" } + +shadowJar { + mergeServiceFiles() // без этого теряются META-INF/services — в native это фатально +} +``` + +Эта связка была в примерах и в 1.x — менять её при миграции не нужно, но её надо знать: если +образ из Gradle работает, а из Docker — нет, первый подозреваемый именно она, а не код. + +### 15.7. Когда GraalVM-модуль считается мигрированным + +Успешная сборка образа критерием не является: все пять дефектов из §15.5 давали зелёную сборку +и падали только в рантайме. Минимальный набор проверок: + +1. бинарь стартует и не падает через секунду; +2. `GET /system/readiness` отвечает 200 — граф инициализировался целиком; +3. `GET /metrics` отдаёт метрики, а не заглушку и не 500; +4. сценарий модуля отрабатывает против реальной зависимости, а не на моках; +5. в логе нет стектрейсов при старте. + +Ожидание готовности контейнера стройте на HTTP-пробе, а не на строке в логе: формулировки +стартовых сообщений Kora не являются контрактом и между 1.x и 2.0 поменялись: + +```kotlin +waitingFor( + Wait.forHttp("/system/readiness").forPort(8080).forStatusCode(200) + .withStartupTimeout(Duration.ofSeconds(60)) +) +``` + +### 15.8. Что специфично для Kotlin (гипотезы, не измерено) + +Native-образ Kotlin-приложения на Kora 2.0 в этом репозитории не собирался. Ниже — рассуждения, +которые нужно проверить на своём проекте, а не принимать на веру: + +- **Вывод KSP дополнительных метаданных требовать не должен.** Сгенерированный `ApplicationGraph` + — обычный JVM-байткод со статическими вызовами конструкторов; DI в Kora не рефлексивен + ни в Java-, ни в Kotlin-варианте. Это главная причина ожидать паритета с Java. +- **`kotlin-reflect` — главный риск.** Он притягивается транзитивно (например, через Jackson + `jackson-module-kotlin`) и требует своих регистраций. JSON в Kora 2.0 генерируется и в рефлексии + не нуждается — перед native-сборкой стоит проверить `./gradlew :dependencies` на предмет + случайного `kotlin-reflect` и убрать его. +- **`data class` с аргументами по умолчанию** компилируется в дополнительный синтетический + конструктор `(..., int, DefaultConstructorMarker)`. Если какой-то библиотеке нужно создавать + такой класс рефлексивно, регистрировать надо именно его, а не «видимый» конструктор из исходника. + +Методика проверки любой из этих гипотез — трассирующий агент и минимальный native-пробник; +она описана в `KORA_MIGRATION_NEURO.md`, раздел +*Diagnostic pattern: native-image собрался, но падает в рантайме*. + +### 15.9. GraalVM-модули на удалённой функциональности + +Модуль на R2DBC или Vert.x не мигрируется в native не потому, что он native: сами интеграции удалены +из 2.0 (§12). Сначала переводите его на синхронный JDBC/Cassandra (§13), и только потом возвращайтесь +к native-части. + +--- + +## 14. Что покрыто автоматизацией + +| Изменение | OpenRewrite | Скрипт | Вручную / нейро-агент | +|---|---|---|---| +| Пакеты и координаты | только Java | ✅ | — | +| Переезд DI-аннотаций | только Java | ✅ | — | +| Переименования модулей | только Java | ✅ | — | +| Разделение HTTP-пакетов | — | ✅ | — | +| Версии Kotlin/KSP, toolchain latest GA JDK | — | ✅ | — | +| `KspTask` → привязка по имени | — | ✅ | проверить каждое место | +| Режимы OpenAPI-генерации | — | ✅ | адаптация кода — вручную | +| Удаление `@Nullable` в Kotlin | — | ✅ | спорные случаи — вручную | +| Типизированные resilient-спецификации | — | — | ✅ | +| Снятие `suspend` по цепочке вызовов | — | — | ✅ | +| `@Component` на мапперы/интерцепторы | — | частично (хардкод) | ✅ | +| S3-клиент | — | — | ✅ | +| Ключи конфигурации (`openapi.management.files`, `rapidoc`→`scalar`) | — | ✅ | — | +| Путь конфигурации openapi-клиента (`PetApi`→`petApi`) | — | — | ✅ | +| Нуллабельность в переопределениях (`result: T?`) | — | — | ✅ | +| `JdbcExecutor.SqlSupplier`/`SqlRunnable` в `inTx` | — | — | ✅ | +| `kspTest` в модулях с `@KoraApp` в тестах | — | — | ✅ | +| Именованные аргументы для сгенерированных TO | — | — | ✅ | +| Версия плагина GraalVM, толчейн 25, базовый образ Docker | — | ✅ | — | +| `imageName`/`mainClass`, `jar.enabled` (§15.3–§15.4) | — | — | ✅ | +| Метаданные достижимости native-image | — | — | ✅ | + +OpenRewrite в текущем виде **не трансформирует Kotlin** — для Kotlin-модулей рабочей автоматикой остаётся скрипт, а всё семантическое делается вручную или нейро-агентом по `KORA_MIGRATION_NEURO.md`. diff --git a/migration/KORA_2_MIGRATION_GUIDE.md b/migration/KORA_2_MIGRATION_GUIDE.md deleted file mode 100644 index c3ff88dc..00000000 --- a/migration/KORA_2_MIGRATION_GUIDE.md +++ /dev/null @@ -1,113 +0,0 @@ -# Kora 1.x -> 2.0 migration guide - -This file is the living migration record for this repository. It is updated after every migrated Gradle module and verified against `agents-md/kora-2`. - -## Scope and sources - -- 1.x baseline: `agents-md/kora-docs` and Git revision before `0ae3b7f`. -- 2.0 source of truth: `agents-md/kora-2`. -- Verified reference application: `examples/java/kora-java-crud`. -- One service equals one Gradle leaf module. -- Removed modules are not deleted automatically. They are excluded from the aggregate build and listed below for manual deletion. - -## Migration order - -1. Run the OpenRewrite recipe for Java AST-safe package/type/dependency changes. -2. Run `python migration/scripts/migrate_kora_2.py --apply` for Gradle/Kotlin/OpenAPI/resource changes and package-directory moves that OpenRewrite cannot safely cover. -3. Compile one module with `./gradlew :classes --console plain`. -4. Apply module-specific semantic changes from this guide. -5. Run that module's tests before moving to the next module. - -Both automation layers are idempotent. Always commit or back up the target repository first and inspect the dry-run output. - -## Repository-wide mandatory changes - -### Runtime and build toolchain - -- Java toolchain: `21` -> `25`. Kora 2.0 artifacts are compiled for JVM 25 and Gradle rejects them on a Java 21 compile classpath. -- Kotlin Gradle plugin: `1.9.25` -> `2.4.10`. -- KSP plugin: `1.9.25-1.0.20` -> `2.3.11`. -- OpenAPI Generator Gradle plugin: use the version aligned with Kora 2 source (`7.24.0`). -- KSP 2 no longer exposes the old `com.google.devtools.ksp.gradle.KspTask` type used by these build scripts. Replace `tasks.withType()` wiring with name-based task wiring such as `tasks.matching { it.name.startsWith("ksp") }`, or wire the exact generated KSP task by name. -- Maven group for Kora artifacts: `ru.tinkoff.kora` -> `io.koraframework`. -- BOM stays named `kora-parent`: `io.koraframework:kora-parent:2.0.0-SNAPSHOT` for the local snapshot used by this repository. - -### Namespace - -Framework packages moved from `ru.tinkoff.kora.*` to `io.koraframework.*`. This repository also renames its example/guide packages from `ru.tinkoff.kora.*` to `io.koraframework.*`, matching the already migrated CRUD example. For application repositories whose own package does not start with `ru.tinkoff.kora`, only framework imports change. - -Core DI annotations moved one level deeper: - -- `ru.tinkoff.kora.common.KoraApp` -> `io.koraframework.common.annotation.KoraApp` -- `Component`, `DefaultComponent`, `KoraSubmodule`, `Mapping`, `Module`, and `Tag` follow the same `io.koraframework.common.annotation.*` pattern. -- `Context` was removed from synchronous HTTP APIs; do not mechanically replace it. Remove the argument and migrate the affected callback/interceptor signature. - -### Dependency/module renames - -- `json-module` -> `json-common`; `JsonModule` is now `io.koraframework.json.common.JsonModule`. -- `cache-redis` -> `cache-redis-lettuce`. -- Applications using Redis cache must include `LettuceRedisCacheModule` instead of the transport-neutral `RedisCacheModule`; the latter no longer provides `RedisCacheClient` by itself. -- `http-client-async` has no Kora 2 module and must be migrated to a supported client implementation such as `http-client-jdk` or `http-client-ok`. -- `jakarta.annotation.Nullable` -> JSpecify `org.jspecify.annotations.Nullable`; JSpecify is exposed transitively by Kora core. Redundant `@Nonnull` annotations can be removed; if null-marking is required, prefer package/module-level JSpecify `@NullMarked` and place type-use annotations correctly on qualified nested types. -- In Kotlin, remove Java/JSpecify `@Nullable` use-site annotations and express nullability with `T?`; `@field:Nullable` is not a valid JSpecify target under Kotlin 2.4. -- `@ConfigValueExtractor` -> `@ConfigMapper`. - -### HTTP server - -- `UndertowHttpServerModule` -> `UndertowPublicHttpServerModule`. -- HTTP server execution is synchronous in 2.0. Remove `CompletionStage`, Reactor wrappers, and explicit Kora `Context` from controller/interceptor contracts where the new API returns `HttpServerResponse` directly. -- `HttpServerInterceptor.intercept(...)` now accepts `(HttpServerRequest, InterceptChain)` and calls `chain.process(request)`. -- HTTP types were split into subpackages: requests and request mappers/handlers moved to `http.server.common.request`, responses and response mappers/exceptions to `http.server.common.response`, interceptors to `http.server.common.interceptor`. -- JSON writers use `toByteArray(value)`; old `toByteArrayUnchecked(value)` is gone. -- Custom interceptors and response/request mappers referenced by `@InterceptWith` / `@Mapping` must be DI components (`@Component`) unless a module method provides them. - -### OpenAPI generation - -Only these modes exist in Kora 2: `java-client`, `java-server`, `kotlin-client`, `kotlin-server`. - -- `java-reactive-client` -> `java-client` -- `java-reactive-server` -> `java-server` -- `kotlin-suspend-client` -> `kotlin-client` -- `kotlin-suspend-server` / `kotlin-reactive-server` -> `kotlin-server` - -Generated code must then be adapted to synchronous Kora 2 APIs; changing the mode alone is necessary but not always sufficient. - -### Resilience - -String-based annotations were replaced by typed specs/components: - -- `@CircuitBreaker("pet")` -> `@CircuitBreakable(PetCircuitBreaker.class)` plus an interface extending `CircuitBreaker` and annotated with `@CircuitBreakerSpec("resilient.circuitbreaker.pet")`. -- `@Retry("pet")` -> `@Retryable(PetRetry.class)` plus `Retry`/`@RetrySpec` interface. -- `@Timeout("pet")` -> `@Timeout(PetTimeouter.class)` plus `Timeouter`/`@TimeoutSpec` interface. -- Fixed-window circuit breaker config requires `type = FIXED_WINDOW`. -- `@Fallback` no longer has a config-name `value`; keep only `method = "fallback(...)"`. -- Circuit breaker `slidingWindowSize` -> `countBased.windowSize`. - -### Cache - -- Cache key selector attribute: `parameters` -> `args`. -- Whole-cache invalidation moved from `@CacheInvalidate(..., invalidateAll = true)` to dedicated `@CacheInvalidateAll(CacheType.class)` / Kotlin `CacheType::class`. -- Custom cache key mapper classes referenced by `@Mapping` must be DI components (for example, annotate nested `UserContextMapping` with `@Component`) because generated AOP proxies now inject them. - -### Scheduling - -- Quartz `@ScheduleWithTrigger` now accepts the tag class directly: `@ScheduleWithTrigger(@Tag(MyJob.class))` -> `@ScheduleWithTrigger(MyJob.class)`; Kotlin `Tag(MyJob::class)` -> `MyJob::class`. - -## Removed or currently non-migratable modules - -These Kora 1.x integrations do not exist in `agents-md/kora-2` and must not remain in the aggregate 2.0 build: - -- Java/Kotlin `database-r2dbc` examples. -- Java/Kotlin `database-vertx` examples. -- GraalVM CRUD R2DBC and Vert.x examples. - -The source directories are retained for manual deletion. Final status and any additional removed integrations will be appended after full inventory. - -## Module status - -- `examples:java:kora-java-crud` — migrated by user; compiles and tests pass; reference module. -- All other modules — in progress. - -## Automation limits - -OpenRewrite handles Java syntax, type/package changes, and Gradle dependency coordinates. It cannot reliably migrate arbitrary Groovy/Kotlin DSL maps, generated-code mode strings, source directory moves, HOCON semantic changes, or design new typed resilience specs. `migration/scripts/migrate_kora_2.py` handles deterministic text/filesystem changes. Semantic API redesign remains module-specific and is verified by compilation/tests. diff --git a/migration/KORA_2_MIGRATION_NEURO.md b/migration/KORA_2_MIGRATION_NEURO.md new file mode 100644 index 00000000..34febca1 --- /dev/null +++ b/migration/KORA_2_MIGRATION_NEURO.md @@ -0,0 +1,569 @@ +# Kora 1.x → 2.0: инструкции для нейро-агентов + +Документ адресован ИИ-агенту, мигрирующему **чужой** проект, и покрывает только те трансформации, +которые нельзя выполнить безопасно через OpenRewrite или детерминированный скрипт. + +Разделение ответственности: + +| Слой | Что делает | Где описан | +|---|---|---| +| OpenRewrite | Java AST: пакеты, типы, координаты зависимостей | `migration/openrewrite/` | +| Скрипт `migrate_kora_2.py` | текстовые/файловые замены: Gradle, Kotlin, ресурсы, перенос каталогов пакетов | `migration/scripts/` | +| **Нейро-агент (этот файл)** | смысловые изменения, требующие анализа окружающего кода | ниже | +| Человек | архитектурные решения по модулям с удалённой функциональностью | `KORA_2_MIGRATION_STATUS.md` | + +Прежде чем применять паттерн — прочитайте соответствующий раздел языкового гайда +(`KORA_2_JAVA_MIGRATION_GUIDE.md` / `KORA_2_KOTLIN_MIGRATION_GUIDE.md`). + +--- + +## Migration pattern: строковые resilient-аннотации → типизированные спецификации + +### Applies when + +В коде есть `@CircuitBreaker("name")`, `@Retry("name")`, `@Timeout("name")`, `@Fallback(value = "name", …)`. + +### Search for + +- Java: `@CircuitBreaker("`, `@Retry("`, `@Timeout("`, `@Fallback(value` +- Kotlin: то же +- Конфигурация: секции `resilient.circuitbreaker.*`, `resilient.retry.*`, `resilient.timeout.*` в `application.conf` / `application.yaml` + +### Required analysis + +1. Собрать все строковые имена и сопоставить каждому путь в конфигурации. Имя в аннотации 1.x — это **последний сегмент** пути конфигурации (`@CircuitBreaker("pet")` → `resilient.circuitbreaker.pet`). +2. Проверить, используется ли одно имя в нескольких классах: тогда spec-интерфейс должен быть один и общий, а не по копии на класс. +3. Найти секцию конфигурации: если её нет, значение бралось по умолчанию — секцию всё равно нужно создать или убедиться, что дефолты 2.0 подходят. +4. Проверить тип окна circuit breaker: в 2.0 для окна фиксированного размера обязателен `type = FIXED_WINDOW`. + +### Target Kora 2.0 design + +Отдельный интерфейс-спецификация на каждое логическое имя, привязанный к пути конфигурации, и аннотация, принимающая класс этого интерфейса. + +### Java migration + +```java +// Было +@CircuitBreaker("pet") +@Retry("pet") +@Timeout("pet") +public Optional findById(long id) { … } + +// Стало +@CircuitBreakable(PetCircuitBreaker.class) +@Retryable(PetRetry.class) +@Timeout(PetTimeouter.class) +public Optional findById(long id) { … } +``` + +Новые типы (обычно рядом с сервисом, который их использует): + +```java +@CircuitBreakerSpec("resilient.circuitbreaker.pet") +public interface PetCircuitBreaker extends CircuitBreaker {} + +@RetrySpec("resilient.retry.pet") +public interface PetRetry extends Retry {} + +@TimeoutSpec("resilient.timeout.pet") +public interface PetTimeouter extends Timeouter {} +``` + +Импорты: `io.koraframework.resilient.circuitbreaker.annotation.{CircuitBreakable, CircuitBreakerSpec}`, +`io.koraframework.resilient.retry.annotation.{Retryable, RetrySpec}`, +`io.koraframework.resilient.timeout.annotation.{Timeout, TimeoutSpec}`, +базовые типы — `io.koraframework.resilient.circuitbreaker.CircuitBreaker`, `…retry.Retry`, `…timeout.Timeouter`. + +### Kotlin migration + +```kotlin +@CircuitBreakable(PetCircuitBreaker::class) +@Retryable(PetRetry::class) +@Timeout(PetTimeouter::class) +fun findById(id: Long): Pet? + +@CircuitBreakerSpec("resilient.circuitbreaker.pet") +interface PetCircuitBreaker : CircuitBreaker + +@RetrySpec("resilient.retry.pet") +interface PetRetry : Retry + +@TimeoutSpec("resilient.timeout.pet") +interface PetTimeouter : Timeouter +``` + +### Gradle changes + +Никаких, кроме общей смены координат: артефакт остаётся `io.koraframework:resilient-kora`. + +### Configuration changes + +```hocon +resilient { + circuitbreaker.pet { + type = FIXED_WINDOW + countBased.windowSize = 50 # было slidingWindowSize + minimumRequiredCalls = 25 + failureRateThreshold = 50 + waitDurationInOpenState = "25s" + permittedCallsInHalfOpenState = 10 + } + timeout.pet.duration = "5000ms" + retry.pet { delay = "500ms"; delayStep = "5s"; attempts = 3 } +} +``` + +### Test migration + +Тесты, переопределяющие конфигурацию отказоустойчивости (`KoraConfigModification.ofString`), должны использовать тот же путь и содержать `type = FIXED_WINDOW`. + +### Common mistakes + +- Создавать по spec-интерфейсу на каждый метод — нужен один на логическое имя. +- Забыть блок `countBased` и получить NPE на инициализации графа: в `CircuitBreakerConfig` он объявлен `@Nullable`, но реализация по умолчанию (`StripedApproxKoraCircuitBreaker`) разыменовывает `config.countBased()` без проверки. Сам `type` при этом опционален — по умолчанию `STRIPED_APPROX`. +- Оставить `@Fallback(value = "…")` — атрибут `value` удалён, остаётся только `method`. +- Заменить `@Timeout("pet")` на `@Timeoutable(...)` — такой аннотации нет, имя `@Timeout` сохранилось, изменился только тип атрибута. + +### Validation + +`./gradlew :classes` — исчезает `incompatible types: String cannot be converted to Class`. +Для Kotlin дополнительно исчезает краш `kspKotlin` с `ClassCastException: String → KSType`. + +### Escalate when + +Одно строковое имя используется с разными наборами параметров в разных местах — это архитектурное решение (одна спецификация или несколько), его принимает человек. + +--- + +## Migration pattern: конфигурация, зашитая в исходники тестов + +### Applies when + +Тест переопределяет конфигурацию графа через `KoraConfigModification.ofString("""…""")`, +и внутри текстового блока лежит HOCON с ключами 1.x. + +### Search for + +``` +rg -n 'ofString\(' --type java --type kotlin +rg -n 'slidingWindowSize|publicApiHttpPort|privateApiHttpPort|^\s*db\s*\{' -g '*.java' -g '*.kt' +``` + +### Required analysis + +Это самый неприятный класс расхождений: ресурсные `.conf` уже мигрированы, код компилируется, +а тест падает на инициализации графа — иногда с внятным `ConfigValueException`, иногда +(как с circuit breaker) с NPE в реализации. Скрипты, которые сканируют только `.conf` и `.yaml`, +такие места не видят по определению. + +Проверять нужно те же три вещи, что и в ресурсах: + +- секция датасорса `db` → `jdbc`; +- порты `publicApiHttpPort` / `privateApiHttpPort` → `port` / `system.port`; +- окно circuit breaker `slidingWindowSize` → `countBased.windowSize`. + +### Target Kora 2.0 design + +Внутри текстового блока — ровно тот же HOCON, что и в ресурсах модуля. Расхождение между +`src/main/resources/application.conf` и тестовым блоком само по себе подозрительно: если тест +переопределяет секцию целиком, он обязан использовать актуальные имена ключей. + +### Java migration + +```java +return KoraConfigModification.ofString(""" + jdbc { + jdbcUrl = ${POSTGRES_JDBC_URL} + username = ${POSTGRES_USER} + password = ${POSTGRES_PASS} + } + resilient.circuitbreaker.pet { + type = FIXED_WINDOW + countBased.windowSize = 2 + } + """); +``` + +### Kotlin migration + +Идентично: в Kotlin тот же разделитель `"""`, отличается только синтаксис вокруг. + +### Gradle changes + +Нет. + +### Test migration + +Это и есть миграция теста. Отдельно стоит проверить `withSystemProperty(...)`: имена +переменных окружения меняться не обязаны, но подставляются они уже в новые ключи. + +### Common mistakes + +- Прогнать скрипт миграции конфигурации и решить, что конфигурация мигрирована целиком. +- Применить правила конфигурации ко всему `.java`/`.kt` файлу: `int slidingWindowSize = 2;` + внешне похож на строку HOCON. Переписывать можно только содержимое текстового блока. +- Починить `application.conf` и забыть, что у теста своя копия секции. + +### Validation + +`./gradlew :test` — граф инициализируется. Компиляция дефект не ловит вообще. + +### Escalate when + +Тестовый блок содержит ключи, которых нет ни в ресурсах модуля, ни в конфигурации 2.0 — +значит тест полагается на что-то удалённое, и нужно решение о смысле теста. + +--- + +## Migration pattern: снятие `suspend` по всей цепочке вызовов (Kotlin) + +### Applies when + +Проект использует `suspend`-репозитории Kora, корутинные контракты Kora или реактивные типы исключительно ради неблокирующего доступа к БД. + +### Search for + +- `suspend fun` в интерфейсах с `@Repository` +- `runBlocking`, `runTest`, `coEvery`, `Dispatchers.IO` вокруг вызовов Kora +- `Mono`/`Flux` в сигнатурах, приходящих из репозиториев Kora + +### Required analysis + +1. Построить полный граф вызовов от `suspend`-метода репозитория вверх — до контроллера/слушателя/теста. Снятие `suspend` обязано пройти по всей цепочке; частичная правка оставит несобираемый код. +2. Отдельно выделить места, где корутины использовались ради параллелизма (`coroutineScope`, + `supervisorScope`, `async`/`await`, `awaitAll`, structured cancellation/timeouts). Они не + остаются в application service: их семантика переводится на Java `StructuredTaskScope`. + Использовать последний GA JDK и последнюю preview-итерацию этого JDK; включить + `--enable-preview` для compile, test и каждого runtime launch. В `@HttpClient` interface + нельзя оставлять даже default `suspend`-обёртки над blocking-методом. + Для Kotlin compilation дополнительно задать `-Xjdk-release=` и + `-Xjvm-enable-preview`; одного `JavaCompile.compilerArgs += --enable-preview` недостаточно + для описания Kotlin-задач. Не переносить названия API из предыдущего preview JDK. +3. Проверить транзакции: код, полагавшийся на корутинный контекст транзакции, требует пересборки границ. +4. Проверить отмену: `withTimeout`/отмена скоупа больше не прерывает операцию БД. + +### Target Kora 2.0 design + +Синхронные контракты, исполняемые на виртуальных потоках. Никаких `Dispatchers.IO` вокруг вызовов Kora — виртуальные потоки уже решают задачу блокирующего I/O. + +### Kotlin migration + +```kotlin +// Было +@Repository +interface PetRepository : JdbcRepository { + suspend fun findById(id: Long): Pet? +} + +class PetService(private val repository: PetRepository) { + suspend fun get(id: Long): Pet? = withContext(Dispatchers.IO) { repository.findById(id) } +} + +// Стало +@Repository +interface PetRepository : JdbcRepository { + fun findById(id: Long): Pet? +} + +class PetService(private val repository: PetRepository) { + fun get(id: Long): Pet? = repository.findById(id) +} +``` + +### Java migration + +Неприменимо (паттерн Kotlin-специфичный). Java-аналог — снятие `CompletionStage`/реактивных обёрток с тем же анализом транзакций и отмены. + +### Test migration + +`runTest { … }` вокруг ставших синхронными вызовов убирается; `coEvery` → `every`, `coVerify` → `verify`. Тест, который проверял отмену корутины, теряет смысл — его нужно переписать или удалить с обоснованием, а не «ослабить». + +### Common mistakes + +- Механически убрать `suspend`, оставив `runBlocking` в вызывающем коде. +- Оставить `withContext(Dispatchers.IO)` вокруг синхронного вызова Kora — лишний переброс между пулами. +- Снять `suspend` с метода, который использовал корутины ради параллелизма, и потерять параллельность. + +### Validation + +`./gradlew :compileKotlin :compileTestKotlin` и прогон тестов модуля. + +### Escalate when + +В цепочке есть параллелизм, отмена по таймауту или supervisor semantics — разработчик обязан +выбрать эквивалентный `StructuredTaskScope.Joiner`/timeout policy и покрыть failure/cancellation +тестами. Каналы/Flow не имеют механического аналога в `StructuredTaskScope` и требуют отдельного +редизайна; сохранять Kotlin Structured Concurrency как обход миграции нельзя. + +--- + +## Migration pattern: мапперы, интерцепторы и мапперы ключей кеша должны быть DI-компонентами + +### Applies when + +Класс упоминается в `@Mapping(X.class)` / `@InterceptWith(X.class)`, но сам не является компонентом графа. + +### Search for + +`@Mapping(`, `@InterceptWith(` — и для каждого аргумента проверить, есть ли на классе `@Component` либо фабричный метод в `@Module`. + +### Required analysis + +Проверить, не предоставляется ли класс уже методом модуля — тогда `@Component` добавлять не нужно (получите неоднозначность). Учесть вложенные классы: они тоже должны быть компонентами. + +### Target Kora 2.0 design + +Генерируемый модуль контроллера/аспекта **инжектит** маппер как зависимость, а не создаёт его сам, поэтому маппер обязан присутствовать в графе. + +### Java migration + +```java +@Component // ← добавлено +public static final class UserContextRequestMapper implements HttpServerRequestMapper { … } +``` + +### Kotlin migration + +```kotlin +@Component +class UserContextMapping : CacheKeyMapper<…> { … } +``` + +### Common mistakes + +- Пометить `@Component` класс, который уже отдаётся методом модуля → неоднозначность в графе. +- Пропустить вложенные классы: сообщение об ошибке называет внешний класс через точку (`Controller.Mapper`), что легко принять за внешний тип. +- Разметить `@Component` **все** мапперы подряд. Признак — конструктор: маппер без зависимостей + Kora создаёт сама, и такой класс, помеченный `@Component`, даёт `Multiple components match`. + Маппер с зависимостями (типичный случай — `JsonReader` внутри `@ResponseCodeMapper`) + создать самостоятельно она не может, и без `@Component` получается + `No component found for dependency` на его собственный тип. +- Сослаться на маппер через `@Mapping`, когда метод возвращает `HttpResponseEntity`: + `@Mapping` заставляет маппер производить **весь** тип возврата, а шаблонная фабрика + фреймворка ожидает маппер полезной нагрузки и оборачивает его в entity сама. + Симптом — `incompatible types: T cannot be converted to HttpResponseEntity`. + +### Validation + +Исчезает `No component found for dependency: … (no tags)` при сборке. + +### Escalate when + +Маппер требует конструктора с параметрами, которых нет в графе — нужно решение о том, как их предоставить. + +--- + +## Migration pattern: расстановка JSpecify type-use аннотаций + +### Applies when + +После автозамены `jakarta.annotation.Nullable` → `org.jspecify.annotations.Nullable` компилятор выдаёт +`type annotation @org.jspecify.annotations.Nullable is not expected here`. + +### Required analysis + +JSpecify-аннотации — **type-use**. Позиция в объявлении значима, особенно для: + +- квалифицированных вложенных типов: `Outer.@Nullable Inner`, а не `@Nullable Outer.Inner`; +- массивов: `String @Nullable []` (nullable массив) против `@Nullable String []` (массив nullable-строк); +- дженериков: `List<@Nullable String>`. + +Такое же поведение у некоторых аннотаций Kora (встречалось `type annotation @Column("status") is not expected here`). + +### Kotlin migration + +В Kotlin аннотации нуллабельности **удаляются**, нуллабельность выражается типом `T?`. `@field:Nullable` под Kotlin 2.4 — некорректная цель. + +### Common mistakes + +- Просто удалить аннотацию, чтобы «прошла компиляция», потеряв контракт нуллабельности. Если тип действительно nullable — аннотацию нужно поставить правильно, а не убрать. + +### Validation + +Компиляция модуля; для API — сверка сгенерированных сигнатур с ожидаемыми. + +### Escalate when + +Требуется решение о переходе на `@NullMarked` на уровне пакета/модуля — это меняет контракт всего кода. + +--- + +## Diagnostic pattern: краш KSP-процессора — дефект фреймворка или немигрированный код? + +### Applies when + +`kspKotlin` падает с `e: [ksp] <исключение>` без указания файла и строки. + +### Required analysis + +Не считайте краш дефектом фреймворка сразу. Порядок: + +1. Проверить, не остался ли в модуле код 1.x, который процессор не ожидает. Подтверждённый пример: строковый `@CircuitBreaker("pet")` вызывает `ClassCastException: String → KSType` — первопричина в коде, а не в процессоре. +2. Смигрировать модуль по языковому гайду и перезапустить `kspKotlin`. +3. Если краш воспроизводится на корректном коде 2.0 — минимизировать репродукцию (один класс, одна аннотация) и завести запись в `KORA_2_FRAMEWORK_ISSUES.md`. +4. Только после минимизации предлагать фикс во фреймворке. + +### Common mistakes + +- Записать краш как баг фреймворка, не смигрировав код (ложное срабатывание). +- Считать краш «своей виной» и переписывать корректный код 2.0 в обход (маскирует реальный дефект). + +### Escalate when + +Краш воспроизводится на минимальном корректном примере — это работа по фреймворку, а не по приложению. + +--- + +## Diagnostic pattern: фантомные ошибки `package ru.tinkoff.kora.* does not exist` + +### Applies when + +Компилятор ругается на пакеты 1.x в файлах, которых **нет** в исходниках; пути ведут в `build/generated/…`. + +### Required analysis + +Это не миграционная ошибка. Задачи-генераторы (OpenAPI, protobuf, `wsdl2java`) не удаляют предыдущий вывод, а ключ build-кеша не учитывает смену `apiPackage`/`modelPackage` — в одном source set оказываются старый и новый пакеты. + +### Fix + +```shell +./gradlew clean --continue +./gradlew classes testClasses --continue --no-build-cache +``` + +### Common mistakes + +- Начать «чинить» сгенерированные файлы руками. Править сгенерированный код как постоянное решение нельзя. +- Добавлять зависимости, чтобы «найти» несуществующий пакет 1.x. + +### Validation + +После `clean` + `--no-build-cache` все упоминания `ru.tinkoff.kora` из вывода компилятора исчезают. Если остаются — значит, ссылка действительно есть в исходниках или в конфигурации генерации (например, `ru.tinkoff.grpc.client` в настройках protobuf). + +--- + +## Migration pattern: собственные метаданные native-image при переименовании пакетов + +### Applies when + +В модуле есть `src/main/resources/META-INF/native-image/<что-то>/` и проект меняет группу/пакеты. + +### Search for + +```shell +find . -path "*/META-INF/native-image/*" -type d +find . -name "reflection-config.json" # такого имени native-image не знает вовсе +``` + +### Required analysis + +Два независимых вопроса, которые легко спутать: + +1. **Имя каталога** после `native-image/` — это просто пространство имён, обычно `/`. + На работоспособность оно не влияет, но оставлять старую группу после миграции нельзя — это единственное, + что разводит метаданные разных библиотек по разным каталогам в одном jar. +2. **Имена файлов внутри** — вот здесь ошибка смертельная и тихая. Читаются только + `reflect-config.json`, `resource-config.json`, `proxy-config.json`, `serialization-config.json`, + `jni-config.json`, `native-image.properties`, `reachability-metadata.json`. Файл с любым другим именем + (классика — `reflection-config.json`) просто игнорируется: ошибки нет, предупреждения нет, + регистраций тоже нет. В самом Kora 2.0 три модуля жили с таким файлом + ([#814](https://github.com/kora-projects/kora/pull/814)). + +### Target design + +Каталог переименован под новую группу, имена файлов каноничные, содержимое **не трогается**. + +``` +META-INF/native-image/ru.tinkoff.kora.examples/logback/reflect-config.json +→ META-INF/native-image/io.koraframework.examples/logback/reflect-config.json +``` + +### Gradle changes + +Нет. + +### Common mistakes + +- Удалить метаданные «за ненадобностью», потому что приложение работает на JVM. JVM их не читает вообще — + такая проверка ничего не доказывает. +- Править содержимое (имена классов внутри) вместе с переименованием каталога: в метаданных + перечислены классы **библиотек** (logback, XNIO), а они не переименовывались. + +### Validation + +`./gradlew :nativeCompile` и запуск бинаря. Для logback признак потерянных метаданных — +приложение стартует, но логи пусты или идут мимо конфигурации. + +--- + +## Diagnostic pattern: native-image собрался, но падает в рантайме + +### Applies when + +`nativeCompile` завершился успешно, а бинарь падает на старте или ведёт себя иначе, чем на JVM. +Типичные формулировки: `No XNIO provider found`, `ClassNotFoundException` на класс, который точно +в classpath, `/metrics` отвечает 500, кеш падает на создании. + +### Required analysis + +Главное правило: **текст исключения здесь часто врёт**. Библиотеки ловят `Throwable` при поиске +провайдеров и репортят вторичную ошибку. Канонический пример: `No XNIO provider found` означал +не отсутствие провайдера, а невозможность создать его логгер — jboss-logging грузит реализацию +`<интерфейс>_$logger` рефлексивно ([#811](https://github.com/kora-projects/kora/pull/811)). + +Порядок работы — три шага, каждый даёт факт, а не гипотезу. + +**Шаг 1. Трассирующий агент** — что реально грузится рефлексивно. Запускать на GraalVM-JDK +тот же shadow-jar, что идёт в образ, и прогнать сценарий приложения целиком: + +```shell +java -agentlib:native-image-agent=config-output-dir=/tmp/na-config \ + -jar build/libs/-all.jar +# прогнать все сценарии, остановить штатно (SIGTERM), иначе конфиг не запишется +diff -r /tmp/na-config src/main/resources/META-INF/native-image// +``` + +Агент видит только те ветки, которые были выполнены — это ограничение, а не баг. Его вывод — +гипотеза для шага 3, а не готовый патч: вкладывать его целиком в ресурсы приложения нельзя — +там будут сотни записей про чужие библиотеки, место которым — в этих библиотеках. + +**Шаг 2. Минимальный native-пробник** — развести «дефект фреймворка» и «ошибка приложения». +Отдельный `main`, который дёргает только подозреваемую библиотеку без Kora, собранный тем же +`native-image`. Если пробник падает — причина в библиотеке или в метаданных модуля Kora, +и искать её в коде приложения бессмысленно. + +**Шаг 3. Проверить, что метаданные вообще читаются.** Этот шаг пропускают чаще всего, +а он один отличает «запись неполная» от «файл не читается». Схема эксперимента: + +1. добавить в проверяемый файл заведомо наблюдаемую регистрацию (метод, который точно вызовется); +2. пересобрать образ и посмотреть, изменилось ли поведение; +3. если нет — положить ту же запись в файл с каноническим именем и повторить. + +Именно так выяснилось, что `reflection-config.json` не читается: добавление метода в старый файл +не дало ничего, та же запись в `reflect-config.json` — сработала. + +### Common mistakes + +- **Поверить тексту исключения** и искать то, чего в ошибке написано (провайдер, который на самом + деле на месте). Сначала убедитесь, что исключение не вторичное после `catch (Throwable)`. +- **Считать успешную сборку признаком готовности.** Все пять найденных при миграции + native-дефектов давали зелёную сборку. +- **Ждать строку в логе как признак старта** (в тестах или вручную). Формулировки меняются между + версиями, а событие может быть вообще потеряно аппендером + ([#816](https://github.com/kora-projects/kora/pull/816)). Ждите `/system/readiness`. +- **Записать вывод агента в ресурсы приложения целиком.** Метаданные чужой библиотеки — её ответственность; + в приложении они застынут и разойдутся с обновлённой библиотекой. +- **Проверять фикс только через `nativeCompile`.** Если в проекте есть ещё и сборка в Docker, + пути получения метаданных у них разные — проверять надо оба. + +### Validation + +Бинарь стартует; `/system/readiness` отвечает 200; `/metrics` отдаёт метрики; сценарий приложения +отрабатывает против реальной зависимости; в логе нет стектрейсов при старте. + +### Escalate when + +Минимальный пробник (шаг 2) воспроизводит падение без кода приложения — это работа по фреймворку +или по библиотеке, а не по миграции. Заводите запись в `KORA_2_FRAMEWORK_ISSUES.md` +с указанием цифр до/после — модульный тест на такой дефект написать нельзя: на JVM он +не воспроизводится вообще. diff --git a/migration/KORA_2_MIGRATION_STATUS.md b/migration/KORA_2_MIGRATION_STATUS.md new file mode 100644 index 00000000..ec7ecb78 --- /dev/null +++ b/migration/KORA_2_MIGRATION_STATUS.md @@ -0,0 +1,299 @@ +# Kora 1.x → 2.0 — статус миграции модулей + +Живой документ. Обновляется после каждого мигрированного или заблокированного модуля. + +## Как получены данные + +Статусы ниже — не оценка, а результат реального прогона тестов каждого модуля: + +```shell +export JAVA_HOME= +./gradlew <все модули>:test --max-workers=1 --continue +``` + +- Ветка примеров: `migration/2.0`, фреймворк: локальный `../kora` @ `integration/migration-fixes`, + опубликован как `io.koraframework:*:2.0.0-SNAPSHOT` в Maven Local. +- **Gradle-процесс обязан идти на JDK 25.** На JDK 21 сборка падает ещё на конфигурации: + buildscript-зависимость `io.koraframework:openapi-generator` требует JVM 25. Toolchain-настройки + модулей этого не покрывают — нужен именно JDK, которым запущен Gradle. +- **Первый прогон после смены пакетов делать с `--no-build-cache` и после `clean`.** Задачи-генераторы + (OpenAPI, protobuf, wsdl2java) не удаляют предыдущий вывод, а ключ кеша не учитывает смену + `apiPackage`/`modelPackage`. Без этого компиляция валится сотнями фантомных + `package ru.tinkoff.kora.* does not exist` из устаревших сгенерированных файлов. +- **`--max-workers=1` обязателен для достоверных цифр.** При параллельной сборке KSP-процессоры + дают гонку (`JacksonIOException: Stream closed`) — на одном воркере таких падений ноль. +- Колонка «Тесты» — фактические `PASSED`/`FAILED` из лога прогона, а не XML-отчёты: часть модулей + их отключает (`junitXml.required = false`). + +## Сводка + +| Показатель | Значение | +|---|---| +| Gradle-модулей в `settings.gradle` | 127 | +| Модулей с тестами | 116 | +| Тестов пройдено | **556** | +| Тестов упало | **0** | +| Модулей со статусом `MIGRATED` | 127 | +| Модулей со статусом `MIGRATION_IN_PROGRESS` | 0 | +| GraalVM native-образов собрано и проверено в работе | 3 | +| Дефектов фреймворка исправлено | 27 | + +Прогон: `./gradlew <116 задач>:test --max-workers=1 --continue`, 10 мин 53 с. + +Единственное падение в этом прогоне — `:examples:java:kora-java-crud:compileJava` — воспроизводится +только при инкрементальной сборке: процессор базы данных читает интерфейс репозитория из +class-файла и видит `:arg0` вместо имён параметров. С `--rerun` модуль собирается и даёт 9/9. +Это описано в обоих руководствах (§1.5/§1.6) как ловушка инкрементальной компиляции, а не как +дефект примера. + +## Что сделано + +Мигрированы все модули репозитория — Java, Kotlin и GraalVM. Для трёх модулей `examples/graalvm/*` +native-образ собран на GraalVM CE 25.2.4 (`native-image 25.0.4`) и проверен в работе: бинарь запущен против реальных +зависимостей (Postgres, Kafka, Scylla + Redis), отвечает на `/system/readiness`, отдаёт метрики и +обслуживает свой сценарий (CRUD либо обработку сообщения). + +По ходу миграции найдено и исправлено **двадцать семь дефектов самого фреймворка** — каждый +отдельной веткой от `master`. Все двадцать семь отправлены в upstream как отдельные pull request'ы +[#791–#818](https://github.com/kora-projects/kora/pulls?q=is%3Apr+author%3Adsudomoin); +разбор каждого — в `KORA_2_PULL_REQUESTS.md`, причины и опровергнутые гипотезы — +в `KORA_2_FRAMEWORK_ISSUES.md`. + +Самые тяжёлые из них не проявлялись как ошибка компиляции: + +| Дефект | Почему был незаметен | +|---|---| +| ни один спан не экспортируется (`OpentelemetryContext.with` теряет обёртку) | приложение работает, метрики идут, трассировка молча пуста | +| эндпоинт метрик всегда «Metric Scraper disabled» | HTTP 200, тело-заглушка | +| java-генератор OpenAPI ставит `to = minimum` в `@Range` | тесты не отправляли значений выше минимума | +| gRPC-приложение завершается сразу после старта | контейнер выходит с кодом 0, как при штатном завершении | +| `TestGraph` теряет permit семафора при упавшей инициализации | прогон висит вместо падения, видна только первая ошибка | +| метаданные трёх модулей названы `reflection-config.json` | файл с таким именем native-image не читает вовсе — регистрации никогда не применялись | +| Undertow в native падает с «No XNIO provider found» | провайдер в образе есть; настоящую ошибку глотает `catch (Throwable)` в XNIO | +| все логи вне запроса теряются `KoraAsyncAppender`'ом | приложение работает, в логе просто нет части событий — заметно только когда по ним чего-то ждёшь | +| `ConfigWatcher` стартует до инициализации узла конфигурации | стектрейс при каждом старте и молча неработающее слежение за конфигом | + +## Закрытые ранее открытые вопросы + +**Порядок схем авторизации в `openapi-generator-http-client`.** Записывался как вопрос к дизайну +фреймворка — оказался ошибкой примера: заглушки bearer/oAuth возвращали константы и перебивали +apiKey. Провайдер без учётных данных обязан возвращать `null`. Заодно выяснилось, что секцию +безопасности генератор читает по `clientConfigPrefix`, а не по `securityConfigPrefix`. + +**`BlackBoxTests` в `kora-java-crud-submodule`.** Контейнер выходил с кодом 255, не дождавшись +`/system/readiness`. Причина — ключ `openapi.management.file`, который в 2.0 называется `files` +и молча не читался. 7/7. + +**«Для `HttpResponseEntity` выбирается не тот шаблонный маппер».** Опровергнуто экспериментом: +это каскад от дефекта multipart-конвертера. `GraphBuilder` форкает весь оставшийся граф на каждого +кандидата шаблона, поэтому одна нерешаемая зависимость роняет все форки и печатает ошибку про +несвязанный компонент. + +**«gRPC требует решения по дизайну фреймворка».** Тоже опровергнуто: `XnioLifecycle` уже держит +non-daemon-поток ровно для этого и прямо это комментирует — контракт установлен, gRPC-сервер просто +перестал его выполнять после перехода на виртуальные потоки. + +## Замечание по именованию S3-модулей + +`kora-java-s3-client-minio` и `kora-kotlin-s3-client-minio` в 2.0 демонстрируют уже не SDK Minio +(реализации `s3-client-minio` в 2.0 нет), а декларативный клиент `s3-client-kora`, который гоняется +против Minio как S3-совместимого хранилища. Каталоги специально **не переименованы**: это решение +о структуре репозитория, а не часть миграции, и оно задело бы четыре модуля примеров плюс ссылки +в README. Расхождение объяснено в README самих модулей. Если решение будет принято, естественные +имена — `kora-java-s3-client-kora` / `kora-kotlin-s3-client-kora`. + +## Что осталось + +Три записи журнала дефектов остаются незакрытыми — все три требуют решения со стороны мейнтейнеров, +а не патча: + +- **Диагностика `GraphBuilder` при провале всех форков шаблона.** Дефект подтверждён контролируемым + экспериментом и описан, но не исправлен: нужен дизайн вывода (сводка по всем кандидатам вместо + одного случайного), а не точечная правка. +- **KSP-процессоры падают при параллельном прогоне Gradle** (`JacksonIOException: Stream closed`). + Воспроизводится стабильно, обходится `--max-workers=1`. Причина — общий разделяемый поток вывода + между воркерами; исправление затрагивает контракт процессоров. +- **Генератор OpenAPI схлопывает две одинаковые именованные схемы в одну.** Записано как + наблюдение, до корневой причины не доводилось: на примерах репозитория не блокирует. + +`ConfigWatcher` из этого списка ушёл — исправлен в +[#818](https://github.com/kora-projects/kora/pull/818): узел конфигурации теперь приходит +в `ConfigWatcher` ещё и значением, поэтому граф обязан инициализировать его раньше и ожидание +с `catch (IllegalStateException)` стало не нужно. + +## Модули + +Колонки: язык, рантайм, основные интеграции Kora, статус, результат компиляции, результат +кодогенерации, результат тестов, результат native-сборки. + +Про колонку «Native» у `examples/graalvm/*`: образ собран `nativeCompile` на GraalVM CE 25.2.4 +и запущен против реальных зависимостей — `/system/readiness` отвечает `OK`, `/metrics` отдаёт +метрики, сценарий модуля отрабатывает (CRUD либо обработка сообщения). + +Сверх того, у всех трёх модулей тот же образ собирается **внутри Docker** по `Dockerfile` +(`ghcr.io/graalvm/native-image-community:25`) и проверяется `BlackBoxTests` против реальных +зависимостей — Postgres, Kafka, Scylla + Redis. + +**Две ловушки тестового окружения, которые стоит знать.** `BlackBoxTests` сассандры требуют +свободного Docker: при параллельно поднятых четырёх контейнерах Scylla не успевает стартовать +и расширение отваливается по таймауту. А kafka-модуль ждал готовности контейнера по строке в логе — +а строка не появлялась, потому что логи терялись из-за дефекта `KoraAsyncAppender` +([#816](https://github.com/kora-projects/kora/pull/816)). После его фикса тест проходит практически +в исходном виде: расширение по-прежнему управляет брокером и топиками, а изменилось два места — +готовность контейнера ждётся по `/system/readiness` (формулировки логов Kora — не контракт), и убраны +отладочные `System.out.println`, оставшиеся от миграции. Подмена порта на `:9093` — не костыль: +брокер Testcontainers объявляет хосту `:9092`, а внутрисетевой listener — на `:9093`. + +| Модуль | Язык | Рантайм | Интеграции Kora | Статус | Компиляция | Кодоген | Тесты | Native | +|---|---|---|---|---|---|---|---|---| +| `examples/graalvm/kora-java-graalvm-crud-cassandra` | Java | JVM + GraalVM | openapi-gen, http-server, cassandra, metrics, json, validation, cache-redis, resilient, config-hocon, openapi-mgmt, logback | `MIGRATED` | OK | OK | 7/7 (вкл. `BlackBoxTests` на native-образе) | OK, 76 МиБ | +| `examples/graalvm/kora-java-graalvm-crud-jdbc` | Java | JVM + GraalVM | openapi-gen, http-server, jdbc, metrics, json, validation, cache-caffeine, resilient, config-hocon, openapi-mgmt, logback | `MIGRATED` | OK | OK | 7/7 (вкл. `BlackBoxTests` на native-образе) | OK, 51 МиБ | +| `examples/graalvm/kora-java-graalvm-kafka` | Java | JVM + GraalVM | http-server, kafka, json, config-yaml, metrics, logback | `MIGRATED` | OK | — | 1/1 (`BlackBoxTests` на native-образе) | OK, 80 МиБ | +| `examples/java/kora-java-cache-caffeine` | Java | JVM | cache-caffeine, logback, config-hocon | `MIGRATED` | OK | OK | 10/10 | — | +| `examples/java/kora-java-cache-redis` | Java | JVM | cache-redis, logback, config-hocon | `MIGRATED` | OK | OK | 9/9 | — | +| `examples/java/kora-java-camunda-engine` | Java | JVM | http-server, camunda-engine, json, jdbc, logback, config-hocon | `MIGRATED` | OK | OK | 3/3 | — | +| `examples/java/kora-java-camunda-zeebe-worker` | Java | JVM | zeebe, scheduling, logback, config-hocon | `MIGRATED` | OK | OK | 1/1 | — | +| `examples/java/kora-java-config-hocon` | Java | JVM | logback, config-hocon | `MIGRATED` | OK | OK | 1/1 | — | +| `examples/java/kora-java-config-yaml` | Java | JVM | logback, config-yaml | `MIGRATED` | OK | OK | 1/1 | — | +| `examples/java/kora-java-crud` | Java | JVM | openapi-gen, http-server, jdbc, metrics, json, validation, cache-caffeine, resilient, config-hocon, openapi-mgmt, logback | `MIGRATED` | OK | OK | 9/9 | — | +| `examples/java/kora-java-crud-submodule/kora-java-crud-submodule-app` | Java | JVM | openapi-gen, http-server, config-hocon, logback, json, metrics, validation, openapi-mgmt | `MIGRATED` | OK | OK | 7/7 | — | +| `examples/java/kora-java-crud-submodule/kora-java-crud-submodule-common` | Java | JVM | — | `MIGRATED` | OK | OK | нет тестов | — | +| `examples/java/kora-java-crud-submodule/kora-java-crud-submodule-pet-api` | Java | JVM | jdbc, cache-caffeine, resilient, config-hocon | `MIGRATED` | OK | OK | 2/2 | — | +| `examples/java/kora-java-crud-submodule/kora-java-crud-submodule-vet-api` | Java | JVM | jdbc, cache-caffeine, resilient, config-hocon | `MIGRATED` | OK | OK | 2/2 | — | +| `examples/java/kora-java-database-cassandra` | Java | JVM | cassandra, logback, config-hocon | `MIGRATED` | OK | OK | 9/9 | — | +| `examples/java/kora-java-database-jdbc` | Java | JVM | jdbc, logback, config-hocon | `MIGRATED` | OK | OK | 22/22 | — | +| `examples/java/kora-java-grpc-client` | Java | JVM | grpc-client, logback, config-hocon | `MIGRATED` | OK | OK | 1/1 | — | +| `examples/java/kora-java-grpc-server` | Java | JVM | grpc-server, logback, config-hocon | `MIGRATED` | OK | OK | 1/1 | — | +| `examples/java/kora-java-helloworld` | Java | JVM | http-server, json, config-hocon, logback | `MIGRATED` | OK | OK | 2/2 | — | +| `examples/java/kora-java-http-client` | Java | JVM | http-client, json, logback, config-hocon | `MIGRATED` | OK | OK | 9/9 | — | +| `examples/java/kora-java-http-server` | Java | JVM | http-server, json, validation, logback, config-hocon, http-client | `MIGRATED` | OK | OK | 16/16 | — | +| `examples/java/kora-java-kafka` | Java | JVM | kafka, json, logback, config-hocon | `MIGRATED` | OK | OK | 24/24 | — | +| `examples/java/kora-java-openapi-generator-http-client` | Java | JVM | openapi-gen, validation, http-client, json, logback, config-hocon | `MIGRATED` | OK | OK | 4/4 | — | +| `examples/java/kora-java-openapi-generator-http-server` | Java | JVM | openapi-gen, validation, http-server, json, logback, config-hocon | `MIGRATED` | OK | OK | 2/2 | — | +| `examples/java/kora-java-resilient` | Java | JVM | resilient, logback, config-hocon | `MIGRATED` | OK | OK | 1/1 | — | +| `examples/java/kora-java-s3-client-aws` | Java | JVM | s3-aws, http-client, logback, config-hocon | `MIGRATED` | OK | OK | 5/5 | — | +| `examples/java/kora-java-s3-client-minio` | Java | JVM | s3-kora, http-client, logback, config-hocon | `MIGRATED` | OK | OK | 6/6 | — | +| `examples/java/kora-java-scheduling-jdk` | Java | JVM | scheduling, logback, config-hocon | `MIGRATED` | OK | OK | 4/4 | — | +| `examples/java/kora-java-scheduling-quartz` | Java | JVM | scheduling-quartz, logback, config-hocon | `MIGRATED` | OK | OK | 3/3 | — | +| `examples/java/kora-java-soap-client` | Java | JVM | json, http-client, soap, logback, config-hocon | `MIGRATED` | OK | OK | 1/1 | — | +| `examples/java/kora-java-telemetry` | Java | JVM | metrics, tracing, http-server, logback, config-hocon | `MIGRATED` | OK | OK | 4/4 | — | +| `examples/java/kora-java-validation` | Java | JVM | validation, logback, config-hocon | `MIGRATED` | OK | OK | 5/5 | — | +| `examples/kotlin/kora-kotlin-cache-caffeine` | Kotlin | JVM | cache-caffeine, config-hocon, logback | `MIGRATED` | OK | OK | 10/10 | — | +| `examples/kotlin/kora-kotlin-cache-redis` | Kotlin | JVM | cache-redis, config-hocon, logback | `MIGRATED` | OK | OK | 9/9 | — | +| `examples/kotlin/kora-kotlin-camunda-engine` | Kotlin | JVM | http-server, camunda-engine, json, jdbc, logback, config-hocon | `MIGRATED` | OK | OK | 3/3 | — | +| `examples/kotlin/kora-kotlin-camunda-zeebe-worker` | Kotlin | JVM | zeebe, scheduling, logback, config-hocon, json | `MIGRATED` | OK | OK | 1/1 | — | +| `examples/kotlin/kora-kotlin-config-hocon` | Kotlin | JVM | config-hocon, logback | `MIGRATED` | OK | OK | 1/1 | — | +| `examples/kotlin/kora-kotlin-config-yaml` | Kotlin | JVM | config-yaml, logback | `MIGRATED` | OK | OK | 1/1 | — | +| `examples/kotlin/kora-kotlin-crud` | Kotlin | JVM | openapi-gen, http-server, http-client, jdbc, metrics, json, validation, cache-caffeine, resilient, config-hocon, openapi-mgmt, logback | `MIGRATED` | OK | OK | 9/9 | — | +| `examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-app` | Kotlin | JVM | openapi-gen, http-server, config-hocon, logback, json, metrics, validation, openapi-mgmt | `MIGRATED` | OK | OK | 7/7 | — | +| `examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-common` | Kotlin | JVM | — | `MIGRATED` | OK | OK | нет тестов | — | +| `examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-pet-api` | Kotlin | JVM | jdbc, cache-caffeine, resilient, config-hocon | `MIGRATED` | OK | OK | 2/2 | — | +| `examples/kotlin/kora-kotlin-crud-submodule/kora-kotlin-crud-submodule-vet-api` | Kotlin | JVM | jdbc, cache-caffeine, resilient, config-hocon | `MIGRATED` | OK | OK | 2/2 | — | +| `examples/kotlin/kora-kotlin-database-cassandra` | Kotlin | JVM | cassandra, config-hocon, logback | `MIGRATED` | OK | OK | 8/8 | — | +| `examples/kotlin/kora-kotlin-database-jdbc` | Kotlin | JVM | jdbc, json, logback, config-hocon | `MIGRATED` | OK | OK | 18/18 | — | +| `examples/kotlin/kora-kotlin-grpc-client` | Kotlin | JVM | grpc-client, logback, config-hocon | `MIGRATED` | OK | OK | 1/1 | — | +| `examples/kotlin/kora-kotlin-grpc-server` | Kotlin | JVM | grpc-server, logback, config-hocon | `MIGRATED` | OK | OK | 1/1 | — | +| `examples/kotlin/kora-kotlin-helloworld` | Kotlin | JVM | http-server, json, config-hocon, logback | `MIGRATED` | OK | OK | 2/2 | — | +| `examples/kotlin/kora-kotlin-http-client` | Kotlin | JVM | http-client, json, logback, config-hocon | `MIGRATED` | OK | OK | 11/11 | — | +| `examples/kotlin/kora-kotlin-http-server` | Kotlin | JVM | http-server, json, validation, config-hocon, logback, http-client | `MIGRATED` | OK | OK | 17/17 | — | +| `examples/kotlin/kora-kotlin-kafka` | Kotlin | JVM | kafka, json, logback, config-hocon | `MIGRATED` | OK | OK | 24/24 | — | +| `examples/kotlin/kora-kotlin-openapi-generator-http-client` | Kotlin | JVM | openapi-gen, validation, http-client, json, logback, config-hocon | `MIGRATED` | OK | OK | 4/4 | — | +| `examples/kotlin/kora-kotlin-openapi-generator-http-server` | Kotlin | JVM | openapi-gen, validation, http-server, json, logback, config-hocon | `MIGRATED` | OK | OK | 2/2 | — | +| `examples/kotlin/kora-kotlin-resilient` | Kotlin | JVM | resilient, config-hocon, logback | `MIGRATED` | OK | OK | 1/1 | — | +| `examples/kotlin/kora-kotlin-s3-client-aws` | Kotlin | JVM | s3-aws, http-client, logback, config-hocon | `MIGRATED` | OK | OK | 5/5 | — | +| `examples/kotlin/kora-kotlin-s3-client-minio` | Kotlin | JVM | s3-kora, http-client, logback, config-hocon | `MIGRATED` | OK | OK | 6/6 | — | +| `examples/kotlin/kora-kotlin-scheduling-jdk` | Kotlin | JVM | scheduling, config-hocon, logback | `MIGRATED` | OK | OK | 4/4 | — | +| `examples/kotlin/kora-kotlin-scheduling-quartz` | Kotlin | JVM | scheduling-quartz, config-hocon, logback | `MIGRATED` | OK | OK | 3/3 | — | +| `examples/kotlin/kora-kotlin-soap-client` | Kotlin | JVM | json, http-client, soap, logback, config-hocon | `MIGRATED` | OK | OK | 1/1 | — | +| `examples/kotlin/kora-kotlin-telemetry` | Kotlin | JVM | http-server, json, metrics, tracing, config-hocon, logback | `MIGRATED` | OK | OK | 4/4 | — | +| `examples/kotlin/kora-kotlin-validation` | Kotlin | JVM | validation, config-hocon, logback | `MIGRATED` | OK | OK | 5/5 | — | +| `guides/java/kora-java-guide-cache-app` | Java | JVM | cache-caffeine, config-hocon, http-server, json, logback | `MIGRATED` | OK | OK | 5/5 | — | +| `guides/java/kora-java-guide-cache-multi-level-app` | Java | JVM | cache-caffeine, cache-redis, config-hocon, http-server, json, logback | `MIGRATED` | OK | OK | 4/4 | — | +| `guides/java/kora-java-guide-config-hocon-app` | Java | JVM | config-hocon, logback | `MIGRATED` | OK | OK | 3/3 | — | +| `guides/java/kora-java-guide-config-yaml-app` | Java | JVM | config-yaml, logback | `MIGRATED` | OK | OK | 3/3 | — | +| `guides/java/kora-java-guide-database-cassandra-app` | Java | JVM | config-hocon, cassandra, http-server, json, logback | `MIGRATED` | OK | OK | 3/3 | — | +| `guides/java/kora-java-guide-database-jdbc-advanced-app` | Java | JVM | config-hocon, flyway, jdbc, http-server, json, logback | `MIGRATED` | OK | OK | 4/4 | — | +| `guides/java/kora-java-guide-database-jdbc-app` | Java | JVM | config-hocon, flyway, jdbc, http-server, json, logback | `MIGRATED` | OK | OK | нет тестов | — | +| `guides/java/kora-java-guide-dependency-injection-introduction-app` | Java | JVM | config-hocon, logback | `MIGRATED` | OK | OK | 3/3 | — | +| `guides/java/kora-java-guide-dependency-injection/kora-java-guide-dependency-injection-app` | Java | JVM | config-hocon, logback | `MIGRATED` | OK | OK | 1/1 | — | +| `guides/java/kora-java-guide-dependency-injection/kora-java-guide-dependency-injection-common` | Java | JVM | — | `MIGRATED` | OK | OK | нет тестов | — | +| `guides/java/kora-java-guide-dependency-injection/kora-java-guide-dependency-injection-lib` | Java | JVM | config-common | `MIGRATED` | OK | OK | нет тестов | — | +| `guides/java/kora-java-guide-dependency-injection/kora-java-guide-dependency-injection-submodule` | Java | JVM | — | `MIGRATED` | OK | OK | нет тестов | — | +| `guides/java/kora-java-guide-getting-started-app` | Java | JVM | config-hocon, http-server, json, logback | `MIGRATED` | OK | OK | 1/1 | — | +| `guides/java/kora-java-guide-grpc-client-advanced-app` | Java | JVM | config-hocon, grpc-client, http-server, json, logback | `MIGRATED` | OK | OK | 4/4 | — | +| `guides/java/kora-java-guide-grpc-client-app` | Java | JVM | config-hocon, grpc-client, http-server, json, logback | `MIGRATED` | OK | OK | 6/6 | — | +| `guides/java/kora-java-guide-grpc-server-advanced-app` | Java | JVM | config-hocon, grpc-server, logback | `MIGRATED` | OK | OK | 7/7 | — | +| `guides/java/kora-java-guide-grpc-server-app` | Java | JVM | config-hocon, grpc-server, logback | `MIGRATED` | OK | OK | 6/6 | — | +| `guides/java/kora-java-guide-http-client-advanced-app` | Java | JVM | config-hocon, http-client, http-server, json, logback | `MIGRATED` | OK | OK | 1/1 | — | +| `guides/java/kora-java-guide-http-client-app` | Java | JVM | config-hocon, http-client, http-server, json, logback | `MIGRATED` | OK | OK | 5/5 | — | +| `guides/java/kora-java-guide-http-server-advanced-app` | Java | JVM | config-hocon, http-server, json, logback | `MIGRATED` | OK | OK | нет тестов | — | +| `guides/java/kora-java-guide-http-server-app` | Java | JVM | config-hocon, http-server, json, logback | `MIGRATED` | OK | OK | 1/1 | — | +| `guides/java/kora-java-guide-json-app` | Java | JVM | config-hocon, http-server, json, logback | `MIGRATED` | OK | OK | 1/1 | — | +| `guides/java/kora-java-guide-messaging-kafka-app` | Java | JVM | config-hocon, http-server, json, kafka, logback | `MIGRATED` | OK | OK | 2/2 | — | +| `guides/java/kora-java-guide-observability-app` | Java | JVM | config-hocon, http-server, json, logback, metrics, tracing | `MIGRATED` | OK | OK | 5/5 | — | +| `guides/java/kora-java-guide-openapi-http-client-app` | Java | JVM | openapi-gen, config-hocon, http-client, http-server, json, logback, validation | `MIGRATED` | OK | OK | 5/5 | — | +| `guides/java/kora-java-guide-openapi-http-server-advanced-app` | Java | JVM | openapi-gen, config-hocon, http-server, json, logback, openapi-mgmt, validation | `MIGRATED` | OK | OK | 2/2 | — | +| `guides/java/kora-java-guide-openapi-http-server-app` | Java | JVM | openapi-gen, config-hocon, http-server, json, logback, openapi-mgmt, validation | `MIGRATED` | OK | OK | 1/1 | — | +| `guides/java/kora-java-guide-resilient-app` | Java | JVM | config-hocon, http-server, json, logback, resilient | `MIGRATED` | OK | OK | 8/8 | — | +| `guides/java/kora-java-guide-s3-app` | Java | JVM | config-hocon, http-client, http-server, json, logback, s3-aws, s3-kora | `MIGRATED` | OK | OK | 2/2 | — | +| `guides/java/kora-java-guide-testing-black-box-app` | Java | JVM | — | `MIGRATED` | OK | OK | 6/6 | — | +| `guides/java/kora-java-guide-testing-integration-app` | Java | JVM | config-hocon, flyway, jdbc, http-client, http-server, json, logback | `MIGRATED` | OK | OK | 4/4 | — | +| `guides/java/kora-java-guide-testing-junit-app` | Java | JVM | http-server | `MIGRATED` | OK | OK | 6/6 | — | +| `guides/java/kora-java-guide-validation-app` | Java | JVM | config-hocon, http-server, json, logback, validation | `MIGRATED` | OK | OK | 13/13 | — | +| `guides/kotlin/kora-kotlin-guide-cache-app` | Kotlin | JVM | cache-caffeine, config-hocon, http-server, json, logback | `MIGRATED` | OK | OK | 5/5 | — | +| `guides/kotlin/kora-kotlin-guide-cache-multi-level-app` | Kotlin | JVM | cache-caffeine, cache-redis, config-hocon, http-server, json, logback | `MIGRATED` | OK | OK | 4/4 | — | +| `guides/kotlin/kora-kotlin-guide-config-hocon-app` | Kotlin | JVM | config-hocon, logback | `MIGRATED` | OK | OK | 3/3 | — | +| `guides/kotlin/kora-kotlin-guide-config-yaml-app` | Kotlin | JVM | config-yaml, logback | `MIGRATED` | OK | OK | 3/3 | — | +| `guides/kotlin/kora-kotlin-guide-database-cassandra-app` | Kotlin | JVM | config-hocon, cassandra, http-server, json, logback | `MIGRATED` | OK | OK | 3/3 | — | +| `guides/kotlin/kora-kotlin-guide-database-jdbc-advanced-app` | Kotlin | JVM | config-hocon, flyway, jdbc, http-server, json, logback | `MIGRATED` | OK | OK | 4/4 | — | +| `guides/kotlin/kora-kotlin-guide-database-jdbc-app` | Kotlin | JVM | config-hocon, flyway, jdbc, http-server, json, logback | `MIGRATED` | OK | OK | нет тестов | — | +| `guides/kotlin/kora-kotlin-guide-dependency-injection-introduction-app` | Kotlin | JVM | config-hocon, logback | `MIGRATED` | OK | OK | 3/3 | — | +| `guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-app` | Kotlin | JVM | config-hocon, logback | `MIGRATED` | OK | OK | 1/1 | — | +| `guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-common` | Kotlin | JVM | — | `MIGRATED` | OK | — | нет тестов | — | +| `guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-lib` | Kotlin | JVM | config-common | `MIGRATED` | OK | OK | нет тестов | — | +| `guides/kotlin/kora-kotlin-guide-dependency-injection/kora-kotlin-guide-dependency-injection-submodule` | Kotlin | JVM | — | `MIGRATED` | OK | OK | нет тестов | — | +| `guides/kotlin/kora-kotlin-guide-getting-started-app` | Kotlin | JVM | config-hocon, http-server, json, logback | `MIGRATED` | OK | OK | 2/2 | — | +| `guides/kotlin/kora-kotlin-guide-grpc-client-advanced-app` | Kotlin | JVM | config-hocon, grpc-client, http-server, json, logback | `MIGRATED` | OK | OK | 4/4 | — | +| `guides/kotlin/kora-kotlin-guide-grpc-client-app` | Kotlin | JVM | config-hocon, grpc-client, http-server, json, logback | `MIGRATED` | OK | OK | 6/6 | — | +| `guides/kotlin/kora-kotlin-guide-grpc-server-advanced-app` | Kotlin | JVM | config-hocon, grpc-server, logback | `MIGRATED` | OK | OK | 7/7 | — | +| `guides/kotlin/kora-kotlin-guide-grpc-server-app` | Kotlin | JVM | config-hocon, grpc-server, logback | `MIGRATED` | OK | OK | 6/6 | — | +| `guides/kotlin/kora-kotlin-guide-http-client-advanced-app` | Kotlin | JVM | config-hocon, http-client, http-server, json, logback | `MIGRATED` | OK | OK | 1/1 | — | +| `guides/kotlin/kora-kotlin-guide-http-client-app` | Kotlin | JVM | config-hocon, http-client, http-server, json, logback | `MIGRATED` | OK | OK | 5/5 | — | +| `guides/kotlin/kora-kotlin-guide-http-server-advanced-app` | Kotlin | JVM | config-hocon, http-server, json, logback | `MIGRATED` | OK | OK | 3/3 | — | +| `guides/kotlin/kora-kotlin-guide-http-server-app` | Kotlin | JVM | config-hocon, http-server, json, logback | `MIGRATED` | OK | OK | 2/2 | — | +| `guides/kotlin/kora-kotlin-guide-json-app` | Kotlin | JVM | config-hocon, http-server, json, logback | `MIGRATED` | OK | OK | 2/2 | — | +| `guides/kotlin/kora-kotlin-guide-messaging-kafka-app` | Kotlin | JVM | config-hocon, http-server, json, kafka, logback | `MIGRATED` | OK | OK | 2/2 | — | +| `guides/kotlin/kora-kotlin-guide-observability-app` | Kotlin | JVM | config-hocon, http-server, json, logback, metrics, tracing | `MIGRATED` | OK | OK | 5/5 | — | +| `guides/kotlin/kora-kotlin-guide-openapi-http-client-app` | Kotlin | JVM | openapi-gen, config-hocon, http-client, http-server, json, logback, validation | `MIGRATED` | OK | OK | 5/5 | — | +| `guides/kotlin/kora-kotlin-guide-openapi-http-server-advanced-app` | Kotlin | JVM | openapi-gen, config-hocon, http-server, json, logback, openapi-mgmt, validation | `MIGRATED` | OK | OK | 2/2 | — | +| `guides/kotlin/kora-kotlin-guide-openapi-http-server-app` | Kotlin | JVM | openapi-gen, config-hocon, http-server, json, logback, openapi-mgmt, validation | `MIGRATED` | OK | OK | 1/1 | — | +| `guides/kotlin/kora-kotlin-guide-resilient-app` | Kotlin | JVM | config-hocon, http-server, json, logback, resilient | `MIGRATED` | OK | OK | 8/8 | — | +| `guides/kotlin/kora-kotlin-guide-s3-app` | Kotlin | JVM | config-hocon, http-client, http-server, json, logback, s3-aws, s3-kora | `MIGRATED` | OK | OK | 2/2 | — | +| `guides/kotlin/kora-kotlin-guide-testing-black-box-app` | Kotlin | JVM | — | `MIGRATED` | OK | — | 6/6 | — | +| `guides/kotlin/kora-kotlin-guide-testing-integration-app` | Kotlin | JVM | config-hocon, flyway, jdbc, http-client, http-server, json, logback | `MIGRATED` | OK | OK | 4/4 | — | +| `guides/kotlin/kora-kotlin-guide-testing-junit-app` | Kotlin | JVM | config-hocon, http-server, json, logback | `MIGRATED` | OK | OK | 6/6 | — | +| `guides/kotlin/kora-kotlin-guide-validation-app` | Kotlin | JVM | config-hocon, http-server, json, logback, validation | `MIGRATED` | OK | OK | 13/13 | — | + +## Легенда статусов + +Используются статусы из регламента миграции: `ALREADY_MIGRATED`, `READY_FOR_MIGRATION`, +`MIGRATION_IN_PROGRESS`, `MIGRATED`, `PARTIALLY_MIGRATED`, `BLOCKED_BY_REMOVED_FUNCTIONALITY`, +`BLOCKED_BY_FRAMEWORK_BUG`, `BLOCKED_BY_GRAALVM_BUILD`, `REQUIRES_REDESIGN`, +`REQUIRES_INVESTIGATION`, `REQUIRES_GRAALVM_INVESTIGATION`, `NOT_APPLICABLE`. + +Статусов `MIGRATION_IN_PROGRESS`, `BLOCKED_BY_GRAALVM_BUILD` и `REQUIRES_GRAALVM_INVESTIGATION` в таблице +больше нет: все три модуля `examples/graalvm/*` собираются в native-образ и проверены в работе +— как через `nativeCompile` (Gradle-плагин), так и через `Dockerfile` (сборка внутри +`ghcr.io/graalvm/native-image-community:25`), который гоняется тестами `BlackBoxTests`. + +Модули на удалённой в 2.0 функциональности (`database-r2dbc`, `database-vertx`) исключены из +`settings.gradle`, но **не удалены** из репозитория и помечены `BLOCKED_BY_REMOVED_FUNCTIONALITY`: + +| Модуль | Статус | Причина | +|---|---|---| +| `examples/java/kora-java-database-r2dbc` | `BLOCKED_BY_REMOVED_FUNCTIONALITY` | интеграция R2DBC удалена в 2.0 | +| `examples/java/kora-java-database-vertx` | `BLOCKED_BY_REMOVED_FUNCTIONALITY` | интеграция Vert.x SQL удалена в 2.0 | +| `examples/kotlin/kora-kotlin-database-r2dbc` | `BLOCKED_BY_REMOVED_FUNCTIONALITY` | то же | +| `examples/kotlin/kora-kotlin-database-vertx` | `BLOCKED_BY_REMOVED_FUNCTIONALITY` | то же | +| `examples/graalvm/kora-java-graalvm-crud-r2dbc` | `BLOCKED_BY_REMOVED_FUNCTIONALITY` | то же | +| `examples/graalvm/kora-java-graalvm-crud-vertx` | `BLOCKED_BY_REMOVED_FUNCTIONALITY` | то же | diff --git a/migration/KORA_2_PULL_REQUESTS.md b/migration/KORA_2_PULL_REQUESTS.md new file mode 100644 index 00000000..94ee811d --- /dev/null +++ b/migration/KORA_2_PULL_REQUESTS.md @@ -0,0 +1,1088 @@ +# Изменения во фреймворк, отправленные в upstream + +Все двадцать семь исправлений отправлены как отдельные pull request'ы в +[`kora-projects/kora`](https://github.com/kora-projects/kora) из форка `dsudomoin/kora`. +Каждая ветка отведена от `master`, содержит один логический фикс с регрессионным тестом и +перебазирована на актуальный `origin/master` перед отправкой. + +| PR | Ветка | Коммит | Модуль | +|---|---|---|---| +| [#791](https://github.com/kora-projects/kora/pull/791) | `fix/http-client-json-response-entity-mapper-tag` | `93254226e` | `http/http-client-common` | +| [#792](https://github.com/kora-projects/kora/pull/792) | `fix/kafka-listener-exception-param-type-use` | `d5948efbd` | `kafka/kafka-annotation-processor` | +| [#793](https://github.com/kora-projects/kora/pull/793) | `fix/openapi-client-api-interface-public` | `307a1ec80` | `openapi/openapi-generator` | +| [#794](https://github.com/kora-projects/kora/pull/794) | `fix/zeebe-worker-annotations-not-aop` | `8da8bd4f6` | `experimental/camunda-zeebe-worker-annotation-processor` | +| [#795](https://github.com/kora-projects/kora/pull/795) | `fix/zeebe-worker-exception-throws-bpmn-error` | `006297793` | `experimental/camunda-zeebe-worker-annotation-processor` | +| [#796](https://github.com/kora-projects/kora/pull/796) | `fix/ksp-unresolved-dependency-message-generic-factory` | `8c336c912` | `core/kora-app-symbol-processor` | +| [#797](https://github.com/kora-projects/kora/pull/797) | `fix/ksp-platform-type-drops-generic-arguments` | `4d49f7c92` | `core/symbol-processor-common` | +| [#798](https://github.com/kora-projects/kora/pull/798) | `fix/zeebe-worker-ksp-exception-throws-bpmn-error` | `aa45fb674` | `experimental/camunda-zeebe-worker-symbol-processor` | +| [#799](https://github.com/kora-projects/kora/pull/799) | `fix/ksp-submodule-processor-stale-symbols` | `39b5fe87d` | `core/kora-app-symbol-processor` | +| [#800](https://github.com/kora-projects/kora/pull/800) | `fix/kafka-listener-parameter-unresolved-type` | `5ca20e17a` | `kafka/kafka-symbol-processor` | +| [#801](https://github.com/kora-projects/kora/pull/801) | `fix/kafka-publisher-observation-unbound-mdc` | `8d1863ed0` | `kafka/kafka` | +| [#802](https://github.com/kora-projects/kora/pull/802) | `fix/ksp-template-match-star-projection` | `a24c4e97a` | `core/kora-app-symbol-processor` | +| [#803](https://github.com/kora-projects/kora/pull/803) | `fix/openapi-kotlin-security-config-data-class` | `5359de430` | `openapi/openapi-generator` | +| [#804](https://github.com/kora-projects/kora/pull/804) | `fix/test-junit5-graph-init-lock-leak` | `561e4d6b4` | `test/test-junit5` | +| [#805](https://github.com/kora-projects/kora/pull/805) | `fix/openapi-server-multipart-file-unused-converter` | `2bb823858` | `openapi/openapi-generator` | +| [#806](https://github.com/kora-projects/kora/pull/806) | `fix/cassandra-completable-future-return` | `5f3ba7c17` | `database/database-annotation-processor` | +| [#807](https://github.com/kora-projects/kora/pull/807) | `fix/grpc-server-keeps-process-alive` | `f3385b3c6` | `grpc/grpc-server` | +| [#808](https://github.com/kora-projects/kora/pull/808) | `fix/openapi-java-range-upper-bound` | `13089ab8f` | `openapi/openapi-generator` | +| [#809](https://github.com/kora-projects/kora/pull/809) | `fix/otel-context-with-loses-kora-wrapper` | `52e95a17f` | `core/common` | +| [#810](https://github.com/kora-projects/kora/pull/810) | `fix/metrics-scraper-not-bound` | `500975770` | `telemetry/micrometer-module` | +| [#811](https://github.com/kora-projects/kora/pull/811) | `fix/graalvm-undertow-xnio-metadata` | `587ef109f` | `http/http-server-undertow` | +| [#812](https://github.com/kora-projects/kora/pull/812) | `fix/graalvm-jdbc-hikari-micrometer-metadata` | `18aa0fa2d` | `database/database-jdbc` | +| [#813](https://github.com/kora-projects/kora/pull/813) | `fix/graalvm-caffeine-node-metadata` | `28a2b38b2` | `cache/cache-caffeine` | +| [#814](https://github.com/kora-projects/kora/pull/814) | `fix/graalvm-reflect-config-file-name` | `274e3b039` | `telemetry/micrometer-module`, `grpc/grpc-server`, `kafka/kafka` | +| [#815](https://github.com/kora-projects/kora/pull/815) | `fix/graalvm-cassandra-guava-comparator-init` | `d2fd730ca` | `database/database-cassandra` | +| [#816](https://github.com/kora-projects/kora/pull/816) | `fix/logback-appender-outside-scope` | `5797597c7` | `logging/logging-logback` | +| [#818](https://github.com/kora-projects/kora/pull/818) | `fix/config-watcher-starts-before-graph-ready` | `c6d6ddd24` | `config/config-common` | + +> [#811](https://github.com/kora-projects/kora/pull/811)–[#815](https://github.com/kora-projects/kora/pull/815) — дефекты, найденные только при сборке GraalVM +> native-image. Ни один из них не воспроизводится на JVM, поэтому ни к одному невозможно написать +> модульный тест, который краснеет без фикса; это оговорено в каждом PR, а доказательством служит +> воспроизведение на модулях `examples/graalvm/*` с конкретными цифрами до/после. + +> [#805](https://github.com/kora-projects/kora/pull/805) и +> [#808](https://github.com/kora-projects/kora/pull/808) обе добавляют тест в +> `HttpServerJavaOpenapiTest`. По отдельности каждая сливается чисто; вторая по порядку слияния +> потребует тривиального ребейза с сохранением обоих тестов. + +> Для локальной работы те же ветки сведены в `integration/migration-fixes` — с неё публиковался +> Maven Local, чтобы фиксы не затирали друг друга при проверке примеров. Источник истины для +> upstream — сами ветки, а не интеграционная. + +Ниже — разбор каждого исправления на русском: постановка задачи, причина, что сделано, покрытие +тестами и влияние на совместимость. Текст самих PR — на английском, по языку проекта. + +--- + +## PR 1 — `fix(http-client): tag JSON response entity mapper with @Json` + +**Ветка:** `fix/http-client-json-response-entity-mapper-tag` — [#791](https://github.com/kora-projects/kora/pull/791) +**Затронутый модуль:** `http/http-client-common` + +### Проблема + +Любой метод HTTP-клиента, возвращающий `HttpResponseEntity`, не компилируется в приложении, где в графе есть `JsonReader`: + +``` +error: Multiple components match dependency: + HttpClientResponseMapper> (no tags) + Candidates: + - factory HttpClientResponseMapperModule#httpClientResponseEntityResponseMapper(...) + - factory HttpClientResponseMapperModule#httpClientResponseJsonEntityResponseMapper(...) +``` + +### Воспроизведение + +```java +@HttpClient("httpClient.default") +public interface SomeClient { + @HttpRoute(method = HttpMethod.GET, path = "/intercepted") + HttpResponseEntity get(); +} +``` + +в приложении, где есть любой `JsonReader` (то есть практически в любом сервисе с JSON). + +### Корневая причина + +В `HttpClientResponseMapperModule` каждая JSON-фабрика помечена `@Json` — `httpClientJsonEitherResponseMapper`, `httpClientJsonEitherResponseEntityResponseMapper`, — **кроме** `httpClientResponseJsonEntityResponseMapper(JsonReader)`. Без тега две `@DefaultComponent`-шаблонные фабрики дают один и тот же нетегированный тип `HttpClientResponseMapper>`, и граф не может выбрать между ними. JSON-вариант по замыслу доступен только под тегом `@Json`. + +### Реализованный фикс + +Одна строка — аннотация `@Json` на `httpClientResponseJsonEntityResponseMapper`. + +### Покрытие тестами + +`HttpClientExtensionTest#testExtensionResponseEntityWhenJsonReaderIsPresent`. + +Проверено обеими сторонами: без фикса тест падает с `Multiple components match dependency`, с фиксом проходит. Существовавший `testExtensionWithoutTag` дефект не ловил — в его графе нет `JsonReader`. + +Прогнаны: `:http:http-client-common:test`, `:http:http-client-annotation-processor:test`, `:http:http-client-symbol-processor:test` — зелёные. + +### Влияние на совместимость + +Код, полагавшийся на неявный JSON-маппинг `HttpResponseEntity` без `@Json`, должен теперь указывать `@Json` явно. Практического слома нет: раньше такой код не собирался вовсе. + +### Связанные модули kora-examples + +`examples/java/kora-java-http-client` — после фикса компилируется, 9/9 тестов проходят. + +--- + +## PR 2 — `fix(kafka): detect Exception listener parameter by type element, not by printed type` + +**Ветка:** `fix/kafka-listener-exception-param-type-use` — [#792](https://github.com/kora-projects/kora/pull/792) +**Затронутый модуль:** `kafka/kafka-annotation-processor` + +### Проблема + +Листенер с параметром-исключением отвергается процессором, если параметр помечен JSpecify-аннотацией: + +``` +error: Kafka record listener method has unsupported parameter: + java.lang.@org.jspecify.annotations.Nullable Exception +``` + +Тот же листенер без `@Nullable` компилируется нормально. + +### Воспроизведение + +```java +@KafkaListener("kafka.consumer.my-listener") +public void process(@Nullable ConsumerRecord record, + @Nullable Exception exception) { +} +``` + +### Корневая причина + +`KafkaUtils.isAnyException` сравнивал `tm.toString()` со строкой `"java.lang.Exception"`. JSpecify-аннотации являются type-use, поэтому `toString()` печатает их внутри имени типа: `java.lang.@org.jspecify.annotations.Nullable Exception`. Совпадения нет, параметр классифицируется как `ConsumerParameter.Unknown` и отвергается. + +Это единственная проверка в `KafkaUtils`, сделанная по строке: `isConsumer`, `isHeaders`, `isKeyDeserializationException`, `isValueDeserializationException` разрешают тип через `dt.asElement()` и потому к аннотациям устойчивы. + +Актуальность повышена тем, что в Kora 2.0 JSpecify — штатный способ разметки нуллабельности, то есть под удар попадает обычный прикладной код. + +### Реализованный фикс + +`isAnyException` резолвит элемент типа и сравнивает квалифицированное имя — так же, как соседние проверки в этом же классе. + +### Покрытие тестами + +`KafkaListenerRecordTest#testProcessRecordAndParseExceptionWithTypeUseAnnotation`. + +Проверено обеими сторонами: без фикса тест падает с исходной ошибкой `Kafka record listener method has unsupported parameter`, с фиксом проходит. + +### Влияние на совместимость + +Строго расширяющее: ранее отвергавшиеся сигнатуры теперь принимаются. Поведение листенеров без аннотаций не меняется. + +### Связанные модули kora-examples + +`examples/java/kora-java-kafka` — после фикса компилируется. + +--- + +## PR 3 — `fix(openapi-generator): generate public client API interfaces` + +- Ветка: `fix/openapi-client-api-interface-public` — [#793](https://github.com/kora-projects/kora/pull/793) +- База: `master` @ `66800169f` +- Коммит: `1371e7fb5` +- Затронутый модуль: `openapi/openapi-generator` + +### Проблема + +Интерфейсы, которые генератор Kora выпускает в режиме `java-client`, получаются package-private, +поэтому приложение не может ими пользоваться: + +``` +error: PetApi is not public in io.koraframework.example.openapi.petV2.api; cannot be accessed from outside package +``` + +### Воспроизведение + +Сгенерировать любой клиент в режиме `java-client` и обратиться к интерфейсу из другого пакета — +именно так устроен каждый реальный проект, где спека лежит в своём пакете. + +### Корневая причина + +`ClientApiGenerator` создаёт тип через `TypeSpec.interfaceBuilder(...)`, не задавая модификаторы. +JavaPoet ничего не подставляет по умолчанию, и интерфейс выходит с пакетной видимостью. +Серверный генератор проставляет `PUBLIC` явно, поэтому там проблемы нет. + +### Реализованный фикс + +`.addModifiers(Modifier.PUBLIC)` в `ClientApiGenerator` и для вложенных типов +в `ClientRequestMapperGenerator`. + +### Покрытие тестами + +`HttpClientJavaOpenapiTest#clientApiInterfaceIsPublic`. + +### Влияние на совместимость + +Обратно совместимо — только расширение видимости. + +### Связанные модули kora-examples + +`examples/java/kora-java-openapi-generator-http-client`, `guides/java/kora-java-guide-openapi-http-client-app`. + +--- + +## PR 4 — `fix(camunda-zeebe-worker): stop marking job worker annotations as AOP annotations` + +- Ветка: `fix/zeebe-worker-annotations-not-aop` — [#794](https://github.com/kora-projects/kora/pull/794) +- База: `master` @ `66800169f` +- Коммит: `9873eeb69` +- Затронутые модули: `experimental/camunda-zeebe-worker`, `experimental/camunda-zeebe-worker-annotation-processor` + +### Проблема + +`@Component`-класс с `public`-методом под `@JobWorker` не попадает в граф. Компилятор советует +добавить `@Component`, который уже стоит: + +``` +error: No component found for dependency: + io.koraframework.example.camunda.zeebe.Step1JobWorker (no tags) + ^--- io.koraframework.example.camunda.zeebe.Step1JobWorker [MISSING] + Fix: + - Add @Component to an implementation of io.koraframework.example.camunda.zeebe.Step1JobWorker. +``` + +### Воспроизведение + +```java +@Component +public final class Handler { + + @JobWorker("worker") + public void handle(@JobVariable String var1) {} +} +``` + +в приложении, где есть корень, зависящий от `All`. С package-private `handle` всё +собирается — именно поэтому дефект не виден в текущих тестах процессора. + +### Корневая причина + +`KoraAppProcessor.processComponents` пропускает `@Component`-класс, если `hasAopAnnotations` вернул +`true`, рассчитывая, что компонентом станет сгенерированный AOP-прокси. Но `AopAnnotationProcessor` +обрабатывает только те аннотации, которые заявили зарегистрированные `KoraAspectFactory`, а для +`@JobWorker` / `@JobVariable` / `@JobVariables` аспекта нет ни одного. Прокси не появляется, класс +исчезает. `hasAopAnnotations` при этом смотрит только `public`/`protected` методы, поэтому дефект +зависит от модификатора метода-обработчика. + +### Реализованный фикс + +Снята мета-аннотация `@AopAnnotation` с трёх zeebe-аннотаций. Все прочие кодогенерирующие аннотации +Kora (`@ScheduleAtFixedRate`, `@KafkaListener`) её тоже не несут. Аспекты вроде `@Log` или `@Timeout` +на методах воркера продолжают работать — они включают прокси сами. + +### Покрытие тестами + +`ZeebeWorkerTests#workerWithPublicMethodIsResolvedInGraph` — прогоняет `KoraAppProcessor`, +`AopAnnotationProcessor` и `ZeebeWorkerAnnotationProcessor` вместе. Проверено, что без фикса тест +падает с `Handler [MISSING]`, с фиксом проходит. + +### Влияние на совместимость + +Обратно совместимо: то, что собиралось, продолжает собираться; то, что ломалось, начинает работать. + +### Связанные модули kora-examples + +`examples/java/kora-java-camunda-zeebe-worker`. + +### Замечание для ревью + +Дефект шире zeebe: любая `@AopAnnotation` без зарегистрированного аспекта молча убирает +`@Component`-класс из графа. Этот PR закрывает конкретный случай; отдельно стоит обсудить, +не должен ли `KoraAppProcessor` сверяться с реестром аспектов или явно ругаться на +AOP-аннотацию без аспекта. + +--- + +## PR 5 — `fix(camunda-zeebe-worker): raise a BPMN error for JobWorkerException again` + +- Ветка: `fix/zeebe-worker-exception-throws-bpmn-error` — [#795](https://github.com/kora-projects/kora/pull/795) +- База: `master` @ `66800169f` +- Коммит: `b26022694` +- Затронутый модуль: `experimental/camunda-zeebe-worker-annotation-processor` + +### Проблема + +`JobWorkerException`, брошенный из воркера, должен поднимать BPMN-ошибку с указанным кодом, +чтобы процесс ушёл по boundary error event. Сейчас исключение просто пробрасывается: задача +падает, ретраится до исчерпания бюджета, ветка обработки ошибки не выполняется. + +### Воспроизведение + +Процесс с boundary error event `errorCode="DOESNT_WORK"` и воркер: + +```java +@JobWorker("fail") +public void handle(@JobVariable int someNumber) { + throw new JobWorkerException("DOESNT_WORK"); +} +``` + +Экземпляр процесса не завершается. + +### Корневая причина + +Коммит `63ad1886c` («Simplify zeebe client telemetry», #527) убрал из `WrappedJobHandler` +метод `createErrorCommand(...)`, который строил +`client.newThrowErrorCommand(job.getKey()).errorCode(...).errorMessage(...)`, +и не поставил ничего взамен: `git log -S newThrowErrorCommand` показывает только этот коммит +и исходный «Camunda 8». Генератор при этом до сих пор пишет `catch (JobWorkerException e) { throw e; }`. + +Окружающая телеметрия эту команду по-прежнему ждёт — обе ветки сейчас мертвы: + +- `DefaultZeebeWorkerObservation#observeFinalCommandStep` помечает `ThrowErrorCommandStep2` как `failedByUser`; +- `DefaultZeebeWorkerLoggerFactory#logJobEnd` отдельно обрабатывает `error == null && failedByUser`. + +### Реализованный фикс + +Команда строится там, где известен исход, — в сгенерированном воркере; `WrappedJobHandler` +не меняется, он уже отправляет возвращённую команду и наблюдает её. В BPMN-ошибку превращается +только `JobWorkerException`; остальное по-прежнему заворачивается в +`JobWorkerException("UNEXPECTED", e)` и бросается. + +### Покрытие тестами + +`ZeebeWorkerTests#workerJobWorkerExceptionIsTurnedIntoBpmnError` — поведенческий тест на +возвращаемую команду. Проверено, что без фикса он падает с вылетающим из `handle` исключением. + +### Влияние на совместимость + +Восстанавливает поведение 1.x. + +### Связанные модули kora-examples + +`examples/java/kora-java-camunda-zeebe-worker` — тест `ZeebeMockedTests#processDemoSuccess`. + +### Порядок применения + +PR 4 и PR 5 независимы, но затрагивают один тестовый файл `ZeebeWorkerTests.java` — при +последовательном применении будет тривиальный конфликт по импортам и хвосту файла. + +--- + +## Обнаружено, но не исправлено + +`Cassandra-репозиторий с CompletableFuture генерирует некомпилируемый код` — подробности, репродукция и предлагаемый фикс в `KORA_2_FRAMEWORK_ISSUES.md`. Фикс требует изменения генерации внутри обёртки `observe(...).call(...)` и отдельного теста в `CassandraResultsTest`; в рамках текущего прохода не делался, модуль помечен `BLOCKED_BY_FRAMEWORK_BUG`. + +--- + +## PR 6 — `fix(kora-app-symbol-processor): stop template resolution from dying on message rendering` + +- **Ветка:** `fix/ksp-unresolved-dependency-message-generic-factory` — [#796](https://github.com/kora-projects/kora/pull/796) +- **Коммит:** `8c336c912` +- **Модуль фреймворка:** `core/kora-app-symbol-processor` + +### Постановка задачи + +`UnresolvedDependencyException` строит всю диагностику в конструкторе и рендерит объявленные типы параметров фабричного метода через `toTypeName()` с пустым `TypeParameterResolver`. Для шаблонной фабрики параметры ссылаются на собственные параметры типа метода, и KotlinPoet бросает `NoSuchElementException: No TypeParameter found for index T`. + +Это не только потерянная диагностика: `GraphBuilder` бросает это исключение как штатный поток управления при переборе шаблонов-кандидатов (`GraphBuilder.kt:246-259`). Построение исключения для отброшенного форка роняло KSP, и приложение с корректно разрешимым графом не собиралось. + +### Воспроизведение + +```kotlin +@KoraApp +interface ExampleApplication { + class Holder(val value: T) + class Wrapper(val value: T) + + fun wrapper(holder: Holder): Wrapper = Wrapper(holder.value) + + @Root + fun root(wrapper: Wrapper): Any = "" +} +``` + +### Причина + +`toTypeName()` без резолвера параметров типа в `getRequestedMessage` и в рендере параметра-источника claim. + +### Что сделано + +Оба места используют резолвер, построенный из параметров типа модуля и фабричного метода. Неиспользуемая приватная копия `getRequestedMessage` с тем же дефектом удалена. + +### Покрытие тестами + +`DependencyTest#testUnresolvedDependencyOfTemplateFactoryIsReportedAsDiagnostic` — без фикса падает исходным `NoSuchElementException`. + +### Совместимость + +Поведение меняется только на пути ошибки: вместо краша выдаётся штатная диагностика, а корректные графы собираются. + +### Предложение вне рамок фикса + +Сообщение стоит вычислять лениво: сейчас полная диагностика строится для каждого отброшенного форка шаблона. + +### Затронутые модули `kora-examples` + +`examples/kotlin/kora-kotlin-helloworld`, `guides/kotlin/kora-kotlin-guide-cache-app` и ещё ~8 модулей. + +--- + +## PR 7 — `fix(symbol-processor-common): keep generic arguments when unwrapping a Java platform type` + +- **Ветка:** `fix/ksp-platform-type-drops-generic-arguments` — [#797](https://github.com/kora-projects/kora/pull/797) +- **Коммит:** `4d49f7c92` +- **Модуль фреймворка:** `core/symbol-processor-common` + +### Постановка задачи + +`KspCommonUtils.fixPlatformType` приводит гибкий тип из Java к неизменяемому Kotlin-аналогу. В ветке, где ни один аргумент не требовал правки, вызывался `asType(listOf())` — в KSP это подставляет собственные параметры типа декларации, превращая `List` в `List`. + +Ветка срабатывает для `@NullMarked` Java-модулей и сгенерированных компонентов. Claim `ConfigValueMapper>` нерендерим, KSP падал с `NoSuchElementException: No TypeParameter found for index E` без указания компонента и зависимости. Kotlin-приложения на `camunda-zeebe-worker` и других Java-определённых модулях не собирались вовсе. + +### Причина + +`asType(listOf())` вместо `asType(args)` в ветке `changed == false`. + +### Что сделано + +Ветка переиспользует уже собранный список аргументов. + +### Покрытие тестами + +`ModuleTest#testJavaModuleKeepsGenericArgumentsOfPlatformTypes` строит граф поверх `@NullMarked` Java-модуля, скомпилированного в тестовый classpath. `AbstractSymbolProcessorTest` получил перегрузку `compile0`, принимающую Java-исходники — она нужна другим случаям, но конкретно этот дефект требует именно class-файла. + +### Совместимость + +Исправляет типы, которые раньше были заведомо некорректны; корректные типы не затрагивает. + +### Затронутые модули `kora-examples` + +`examples/kotlin/kora-kotlin-camunda-zeebe-worker` и все Kotlin-модули с Java-определёнными модулями, содержащими коллекции в конфигурации. + +--- + +## PR 8 — `fix(camunda-zeebe-worker): raise a BPMN error for JobWorkerException in the KSP worker too` + +- **Ветка:** `fix/zeebe-worker-ksp-exception-throws-bpmn-error` — [#798](https://github.com/kora-projects/kora/pull/798) +- **Коммит:** `aa45fb674` +- **Модуль фреймворка:** `experimental/camunda-zeebe-worker-symbol-processor` + +### Постановка задачи + +Kotlin-генератор воркера содержит тот же дефект, что исправлен для Java-процессора в PR 5: `JobWorkerException` пробрасывается вместо `client.newThrowErrorCommand(...)`. Джоба падает и ретраится, граничное событие ошибки BPMN не срабатывает — идентичное Kotlin-приложение ведёт себя иначе, чем Java. + +### Что сделано + +Симметрично PR 5. Только `JobWorkerException` отображается в BPMN-ошибку; остальные исключения по-прежнему оборачиваются в `JobWorkerException("UNEXPECTED", e)`. + +### Покрытие тестами + +`ZeebeWorkerTests#workerJobWorkerExceptionIsTurnedIntoBpmnError`; без фикса исключение выходит из `handle()`. Модуль получил тестовую зависимость `mockito-core`. + +### Связь с PR 5 + +Логически один дефект в двух реализациях генератора. PR 5 и PR 8 трогают разные модули и не конфликтуют; порядок слияния не важен. + +### Затронутые модули `kora-examples` + +`examples/kotlin/kora-kotlin-camunda-zeebe-worker` — после фикса тест проходит. + +--- + +## PR 9 — `fix(kora-app-symbol-processor): resolve submodule symbols again instead of holding them` + +- **Ветка:** `fix/ksp-submodule-processor-stale-symbols` — [#799](https://github.com/kora-projects/kora/pull/799) +- **Коммит:** `39b5fe87d` +- **Модуль фреймворка:** `core/kora-app-symbol-processor` + +### Постановка задачи + +`KoraSubmoduleProcessor` накапливал `KSClassDeclaration` между раундами KSP и использовал их в `finish()`. В KSP2 символ, полученный в раунде N, после закрытия раунда недействителен: его резолвер уничтожен. Сборка модуля с `@KoraSubmodule` падала или молча выдавала пустой субмодуль в зависимости от того, в каком раунде объявление попало в обработку. + +### Что сделано + +Между раундами хранятся только **квалифицированные имена** (`MutableSet` / `MutableList`), плюс ссылка на последний живой `Resolver`. В `finish()` каждое имя резолвится заново через `resolver.classDeclaration(name)`. Добавлены `collectSubmodule(...)` и `generateSubmodule(resolver, submodule)`, чтобы сбор и генерация не смешивались. + +### Покрытие тестами + +Регрессия воспроизводится на реальном многомодульном примере: `examples/kotlin/kora-kotlin-crud-submodule`. Изолированного unit-теста нет — процессор нужно прогнать через несколько раундов, а тестовая инфраструктура `AbstractSymbolProcessorTest` этого не моделирует. Это ограничение указано честно: фикс проверен интеграционно, не юнит-тестом. + +### Влияние на совместимость + +Поведение не меняется, только момент резолва. Java-процессор этой проблемы не имел. + +### Затронутые модули `kora-examples` + +`examples/kotlin/kora-kotlin-crud-submodule` (4 подмодуля) — после фикса собирается, 7/7 тестов проходят. + +--- + +## PR 10 — `fix(kafka-symbol-processor): report an unresolvable listener parameter instead of dying` + +- **Ветка:** `fix/kafka-listener-parameter-unresolved-type` — [#800](https://github.com/kora-projects/kora/pull/800) +- **Коммит:** `5ca20e17a` +- **Модуль фреймворка:** `kafka/kafka-symbol-processor` + +### Постановка задачи + +Kotlin-аналог PR 2. `KafkaUtils` определял тип параметра слушателя сравнением **печатного представления** типа. Когда тип не резолвится (опечатка в импорте, ещё не сгенерированный класс, ошибка в другом файле), печатное представление — ``, ни с чем не совпадает, и процессор падал с внутренним исключением вместо диагностики. Пользователь видел стектрейс KSP, а не «не могу разрешить тип параметра». + +### Что сделано + +- Добавлен `private fun KSType.isClass(className: ClassName)`, сравнивающий квалифицированные имена, а не строки. +- `parseParameters` бросает `ProcessingErrorException` при `type.isError`, указывая на конкретный параметр. + +### Покрытие тестами + +Регрессионный тест в `kafka-symbol-processor`: слушатель с неразрешимым типом параметра. Без фикса — внутреннее исключение, с фиксом — сообщение с указанием параметра. + +### Влияние на совместимость + +Только диагностика. Корректный код собирается как прежде. + +--- + +## PR 11 — `fix(kafka): publish a record when no MDC scope is bound` + +- **Ветка:** `fix/kafka-publisher-observation-unbound-mdc` — [#801](https://github.com/kora-projects/kora/pull/801) +- **Коммит:** `8d1863ed0` +- **Модуль фреймворка:** `kafka/kafka` + +### Постановка задачи + +`DefaultKafkaPublisherRecordObservation` безусловно вызывал `MDC.get().fork()`. В синхронной модели Kora 2.0 `MDC.VALUE` — скоуп-переменная, и вне HTTP-запроса или другого установленного скоупа она не привязана: публикация из `main`, из планировщика или из теста падала. + +### Что сделано + +```java +this.mdc = MDC.VALUE.isBound() ? MDC.get().fork() : new MDC(); +``` + +Ровно то же, что делает остальной код при отсутствии скоупа: пустой контекст вместо исключения. + +### Покрытие тестами + +Тест публикации без установленного скоупа MDC. Без фикса падает на `MDC.get()`. + +### Влияние на совместимость + +Расширение области применимости: то, что раньше падало, теперь работает. + +### Затронутые модули `kora-examples` + +`examples/java/kora-java-kafka`, `examples/kotlin/kora-kotlin-kafka` — 48/48 тестов на два языка. + +--- + +## PR 12 — `fix(kora-app-symbol-processor): treat a star projection as a concrete type, not a template` + +- **Ветка:** `fix/ksp-template-match-star-projection` — [#802](https://github.com/kora-projects/kora/pull/802) +- **Коммит:** `a24c4e97a` +- **Модуль фреймворка:** `core/kora-app-symbol-processor` + +### Постановка задачи + +KSP видит Java-подстановку `` как star projection, у которой `KSTypeArgument.type == null`. Обе реализации `hasGenericVariable()` трактовали `null` как «переменная типа есть», из-за чего конкретный тип вроде `ForwardingServerBuilder` уезжал в множество шаблонов. Java-процессор в аналогичной ситуации возвращает `false` (`TypeParameterUtils#visitWildcard`) — то есть Kotlin и Java расходились в поведении графа. + +Дополнительно `ComponentTemplateHelper.initMap`/`fillMap` и `ComponentDependencyHelper.parseClaim` разыменовывали `KSTypeArgument.type` без проверки на `null` и падали с NPE на том же входе. + +### Что сделано + +- Обе `hasGenericVariable()` при `type == null` возвращают `false` — как Java-процессор. +- `initMap`/`fillMap` пропускают неразрешимый аргумент вместо NPE. +- `parseClaim` при отсутствии первого аргумента типа падает обратно на claim по целому типу (новый хелпер `claimOfWholeType`). + +### Покрытие тестами + +Синтетические тесты для star projection и raw-супертипа **не добавлены сознательно**: написанные варианты проходили и с фиксом, и без него — воспроизвести путь через `AbstractSymbolProcessorTest` не удалось, потому что для этого нужен именно Java-класс в classpath, а не Kotlin-исходник. Тест, который зелёный в обоих случаях, даёт ложную уверенность, поэтому он удалён, и это оговорено в сообщении коммита. Фикс проверен интеграционно на трёх gRPC-модулях. + +### Затронутые модули `kora-examples` + +`examples/kotlin/kora-kotlin-grpc-server`, `kora-kotlin-grpc-client`, `guides/kotlin/kora-kotlin-guide-grpc-server-app` — после фикса компилируются. + +--- + +## PR 13 — `fix(openapi-generator): generate the Kotlin basic auth config as a data class` + +- **Ветка:** `fix/openapi-kotlin-security-config-data-class` — [#803](https://github.com/kora-projects/kora/pull/803) +- **Коммит:** `5359de430` +- **Модуль фреймворка:** `openapi/openapi-generator` + +### Постановка задачи + +`ClientSecuritySchemaGenerator.basicAuthConfig` генерировал обычный `class`, а `@ConfigSource` в Kotlin требует `data class` — конфигурационному процессору нужны `componentN`. Kotlin-клиент с basic-авторизацией не собирался, Java-аналог собирался (в Java `record` генерировался корректно). + +### Что сделано + +`.addModifiers(KModifier.DATA)` в `basicAuthConfig`. + +### Покрытие тестами + +Тест генератора на спецификации с `basicAuth`: сгенерированный конфиг объявлен как `data class`. Без фикса падает. + +### Затронутые модули `kora-examples` + +`examples/kotlin/kora-kotlin-openapi-generator-http-client`. + +--- + +## PR 14 — `fix(test-junit5): return the init lock permits when graph initialization fails` + +- **Ветка:** `fix/test-junit5-graph-init-lock-leak` — [#804](https://github.com/kora-projects/kora/pull/804) +- **Коммит:** `561e4d6b4` +- **Модуль фреймворка:** `test/test-junit5` + +### Постановка задачи + +`TestGraph.initialize()` захватывал разрешения общего семафора, вызывал `initGraph()` и освобождал их **следующей строкой**, без `finally`. Любая упавшая инициализация графа удерживала их навсегда, и каждый последующий `@KoraAppTest` в той же JVM вставал в `acquireUninterruptibly()`. Прогон не завершался и не падал — он висел до таймаута CI, показывая только самую первую ошибку. + +Найдено при миграции `kora-examples`: один неправильно сконфигурированный модуль превращал 30-секундный прогон в 22-минутный, и настоящие причины остальных падений были не видны. + +### Что сделано + +Обе точки захвата освобождают разрешения в `finally`. + +### Покрытие тестами + +`TestGraphLockLeakTest` дёргает `TestGraph` напрямую с `Config`, у которого `setup()` бросает `IOException`, и проверяет, что число доступных разрешений не изменилось. Без фикса — 63 из 64 для ветки без системных свойств и 0 из 64 для ветки с ними. + +Попытка инициализации выполняется в daemon-потоке с `join(30_000)`: тестируемый режим отказа — это **блокировка**, а не исключение, поэтому прямой вызов повесил бы сборку вместо того, чтобы её уронить. В `finally` утёкшие разрешения возвращаются, иначе первый тест отравил бы второй. + +### Влияние на совместимость + +Чистое исправление. Ранее работавшие прогоны не затрагиваются. + +--- + +## PR 15 — `fix(openapi-generator): stop requesting a converter for a multipart file part` + +- **Ветка:** `fix/openapi-server-multipart-file-unused-converter` — [#805](https://github.com/kora-projects/kora/pull/805) +- **Коммит:** `2bb823858` +- **Модуль фреймворка:** `openapi/openapi-generator` (режимы `java-server` и `kotlin-server`) + +### Постановка задачи + +Для операции с `multipart/form-data` и полем `type: string, format: binary` генератор добавлял в конструктор request-маппера `HttpServerParameterReader` (Kotlin — ``), но в теле `apply` его не использовал: бинарная часть отдаётся как `FormMultipart.FormPart` напрямую. Такого компонента во фреймворке нет и быть не должно — конвертер читает `String`. Приложение не собиралось. + +Дефект существовал в фикстурах генератора (`petstoreV3_form.yaml`), но тесты его не ловили: они только компилируют сгенерированный код, а он компилируется — ломается лишь построение графа `@KoraApp`. + +### Что сделано + +В обоих генераторах цикл объявления конвертеров пропускает `formParam.isFile`, но только когда тело действительно пойдёт по multipart-ветке: + +```java +// url-encoded wins when an operation declares both, same as the apply() body below +var multipartBody = multipartForm && !urlEncodedForm; +``` + +Привязка к тому же решению, что и выбор ветки в `apply`, а не к одному лишь `multipartForm`: если операция объявляет оба content-type, тело идёт по `mapUrlEncoded`, а она конвертер вызывает. + +### Покрытие тестами + +`HttpServerJavaOpenapiTest#multipartFileFormParamDoesNotAskForAConverterItNeverUses` и одноимённый в `HttpServerKotlinOpenapiTest`. На существующей фикстуре проверяют, что оба multipart-маппера не объявляют `HttpServerParameterReader`, по-прежнему присваивают `_part`, а url-encoded маппер конвертеры сохранил. Без фикса оба падают. + +### Влияние на совместимость + +Меняется сигнатура сгенерированного конструктора — но только для случая, который до сих пор не собирался вовсе. + +### Затронутые модули `kora-examples` + +`guides/java/kora-java-guide-openapi-http-server-advanced-app`, `guides/kotlin/kora-kotlin-guide-openapi-http-server-advanced-app`. + +--- + +## PR 16 — `fix(database-annotation-processor): support CompletableFuture from a Cassandra repository` + +- **Ветка:** `fix/cassandra-completable-future-return` — [#806](https://github.com/kora-projects/kora/pull/806) +- **Коммит:** `5f3ba7c17` +- **Модуль фреймворка:** `database/database-annotation-processor` + +### Постановка задачи + +Метод Cassandra-репозитория, возвращающий `CompletableFuture`, порождал код, который не компилируется: + +``` +error: incompatible types: inference variable R has incompatible bounds + upper bounds: CompletableFuture,Object +``` + +`CompletableFuture` поддерживается явно — генератор дописывал `.toCompletableFuture()`, — но делал это **внутри** внутренней лямбды `observe(...)`. Внешняя обёртка `observe(...).call(...)` при этом всё равно возвращала результат `prepareAsync(...).thenCompose(...)`, то есть `CompletionStage`, и параметр `R` не сводился с объявленным типом возврата. + +### Что сделано + +Преобразование перенесено наружу внешней обёртки — туда, где значение действительно пересекает границу объявленного типа возврата. Ветка `CompletionStage` не изменилась. + +### Покрытие тестами + +`CassandraResultsTest#testReturnCompletableFutureObject` и `#testReturnCompletableFutureVoid`. В тестах были только `CompletionStage` и `CompletionStage` — ни одного случая с `CompletableFuture`, поэтому дефект не ловился. Оба новых теста падают без фикса с приведённой выше ошибкой. + +### Влияние на совместимость + +Чистое исправление: раньше такой код не компилировался вовсе. + +### Затронутые модули `kora-examples` + +`examples/java/kora-java-database-cassandra`, `examples/graalvm/kora-java-graalvm-crud-cassandra` — с обоих снята пометка `BLOCKED_BY_FRAMEWORK_BUG`. + +--- + +## PR 17 — `fix(application-graph): keep the process alive for the whole application` + +- **Ветка:** `fix/grpc-server-keeps-process-alive` — [#807](https://github.com/kora-projects/kora/pull/807) +- **Коммит:** `f3385b3c6` +- **Модули фреймворка:** `core/application-graph`, `http/http-server-undertow` + +### Постановка задачи + +Приложение, у которого единственный сервер — gRPC, стартовало и немедленно завершалось: контейнер останавливался с кодом `0` (штатный выход JVM, а не ошибка старта — при ошибке `KoraApplication.run` делает `System.exit(-1)`), и все тесты падали с `UNAVAILABLE / Connection refused`. Затронуты четыре модуля примеров на обоих языках. + +`KoraApplication.run` не блокирует: инициализирует граф, вешает shutdown hook и возвращает управление. Процесс живёт ровно столько, сколько какой-нибудь компонент удерживает non-daemon поток. В 2.0 транспорт gRPC — `OkHttpServerBuilder` с `directExecutor()` и `VirtualThreadExecutorTransportFilter`, а виртуальные потоки всегда daemon, поэтому удерживать JVM стало нечему: `GrpcServer.init()` вызывал `server.start()` и выходил, а `awaitTermination()` достигался только из `release()`. + +### Первая версия и почему от неё отказались + +Сначала фикс был в `GrpcServer`: свой non-daemon поток на `server.awaitTermination()`. Обоснование +было такое: `XnioLifecycle` уже держит такой поток, значит контракт «серверный компонент удерживает +процесс сам» установлен, а gRPC его нарушает. + +Мейнтейнер ([комментарий в #807](https://github.com/kora-projects/kora/pull/807)) это отклонил, и справедливо. +Тот же приём оказался продублирован в пяти модулях — `XnioLifecycle`, `KafkaUtils:23`, +`ThreadPoolSchedulingJdkExecutor:45`, `KoraThreadPoolJobExecutor:36` и мой новый в `GrpcServer`. То есть +я принял за контракт то, что на самом деле повторяющийся обход, и чинил симптом в одном из пяти мест. + +### Что сделано + +Удержание процесса — свойство не отдельного сервера, а приложения. Non-daemon поток переехал +в `KoraApplication#run` (класс `ApplicationKeepAlive`) и освобождается из shutdown-хока. Следствия: + +- `GrpcServer` не меняется вообще — мой хак оттуда убран; +- `XnioLifecycle` больше не просит non-daemon потоков; его init-поток остался платформенным только + потому, что XNIO при построении воркера делает блокирующую работу, а `init` идёт на виртуальном; +- `kafka`, `scheduling-jdk` и `camunda-engine-bpmn` не трогал: их non-daemon потоки могут существовать не ради + времени жизни процесса, и решение по каждому — отдельный вопрос, заданный в PR. + +### Покрытие тестами + +`ApplicationKeepAliveTest` проверяет сам механизм: поток non-daemon и жив во время работы, умирает после `stop()`. + +Гонять сам `KoraApplication#run` из теста нельзя, и это проверено на практике: `run` отпускает поток +только из shutdown-хока, поэтому вызов в процессе теста оставляет non-daemon поток и тестовая JVM +не завершается — первая версия теста повесила Gradle worker. Это ровно то поведение, ради которого +фикс и сделан. + +Сквозная проверка — примеры, запущенные как процессы: + +| Пример | Результат | +|---|---| +| `kora-java-grpc-server` (без хака в `GrpcServer`) | жив через 12 с; единственный non-daemon поток — `kora-application`; по SIGTERM штатный shutdown | +| `kora-java-http-server` (без флагов в XNIO) | readiness за 1 с, тот же единственный non-daemon поток, `XnioWorker stopped` по SIGTERM | + +До фикса тот же gRPC-бинарь выходил с кодом 0 в первую секунду. + +Зелёные: `:core:application-graph:test`, `:http:http-server-undertow:test`, `:grpc:grpc-server:test`, +а в примерах — gRPC-модули на Java и Kotlin плюс guide. + +### Влияние на совместимость + +Приложение, у которого non-daemon поток уже был, продолжает работать — просто теперь их два. +Приложение, встраивающее `KoraApplication#run` в процесс, который должен завершиться сам, +теперь не завершится — впрочем, для любого приложения с Undertow или Kafka это было верно и раньше. + +### Затронутые модули `kora-examples` + +`examples/java/kora-java-grpc-server`, `examples/kotlin/kora-kotlin-grpc-server`, `guides/kotlin/kora-kotlin-guide-grpc-server-app`, `guides/kotlin/kora-kotlin-guide-grpc-server-advanced-app`, плюс guides с gRPC-клиентами, которые поднимают сервер. + +--- + +## PR 18 — `fix(openapi-generator): use the schema maximum as the Range upper bound` + +- **Ветка:** `fix/openapi-java-range-upper-bound` — [#808](https://github.com/kora-projects/kora/pull/808) +- **Коммит:** `13089ab8f` +- **Модуль фреймворка:** `openapi/openapi-generator` (java-генератор) + +### Постановка задачи + +`AbstractJavaGenerator.getValidation` при построении верхней границы `@Range` читал `variable.getMinimum()`, поэтому у **каждого** числового ограничения, которое выпускал java-генератор, `to` совпадало с `from`: + +``` +minimum: 1, maximum: 100 -> @Range(from = 1.0, to = 1.0) +minimum: 200, maximum: 599 -> @Range(from = 200.0, to = 200.0) +``` + +Любое значение больше минимума сгенерированная валидация отвергала с 400. Случай «только minimum» тоже ломался: вместо ветки с максимумом типа брался минимум, и `minimum: 1` на int64-параметре пути тоже давал `to = 1.0`. + +Kotlin-генератор считает это правильно, то есть два языка расходились на одинаковых спецификациях — так дефект и всплыл: java-клиент отправил `size=100` java-серверу, собранному по спецификации `1..100`, и получил `Should be in range from '1' to '1', but was greater: 100`, тогда как Kotlin-двойник прошёл. + +### Что сделано + +Ветка максимума читает `getMaximum()`. + +### Покрытие тестами + +`HttpServerJavaOpenapiTest#numericRangeUsesTheSchemaMaximumAsUpperBound` на существующей фикстуре `petstoreV3_validation.yaml`, где уже были обе формы (`minimum`+`maximum` и только `minimum`) — не хватало лишь проверки выпущенной границы. Без фикса падает. + +### Затронутые модули `kora-examples` + +`examples/java/kora-java-crud`, `examples/java/kora-java-crud-submodule`, `examples/java/kora-java-openapi-generator-http-server`, оба openapi-гайда. Их тесты проходили только потому, что ни один не отправлял значение выше минимума. + +--- + +## PR 19 — `fix(common): keep the Kora wrapper when deriving an OpenTelemetry context` + +- **Ветка:** `fix/otel-context-with-loses-kora-wrapper` — [#809](https://github.com/kora-projects/kora/pull/809) +- **Коммит:** `52e95a17f` +- **Модуль фреймворка:** `core/common` + +### Постановка задачи + +Ни один спан не экспортировался ни одним приложением Kora 2.0 с OTLP-экспортёром. Коллектор получал метрики и больше ничего, а в логе приложения на каждую попытку экспорта: + +``` +BatchSpanProcessor$Worker exportCurrentBatch +WARNING: Exporter threw an Exception +java.lang.IllegalStateException + at OpentelemetryContextStorage.attach(OpentelemetryContextStorage.java:12) + at io.opentelemetry.context.Context.makeCurrent(Context.java:232) + at io.opentelemetry.context.Context.lambda$wrap$1(Context.java:241) + at InstrumentationUtil.suppressInstrumentation(InstrumentationUtil.java:34) +``` + +Kora хранит контекст OpenTelemetry в `ScopedValue`, поэтому императивная пара `attach()`/`makeCurrent()` не реализуема и намеренно бросает исключение. Всё держится на `wrap(...)`, который реализован только в `OpentelemetryContext` — через `ScopedValue.where(...)`. + +Но `with(key, value)` возвращал `delegate.with(key, value)`, то есть обычный `ArrayBasedContext`. Обёртка терялась при первом же порождении контекста, и порождённый уходил в дефолтный `Context.wrap(...)`, который вызывает `makeCurrent()`. OTLP-экспортёр порождает ровно такой контекст на своём рабочем потоке, чтобы подавить инструментирование, — и каждый экспорт упирался в throw. + +### Что сделано + +`with(...)` заворачивает результат обратно, как это уже делал `root()`. + +### Покрытие тестами + +`OpentelemetryContextTest` — порождённый контекст и воспроизведённый путь подавления инструментирования (`InstrumentationUtil` внутренний, поэтому вызов выписан явно). Оба падают с `IllegalStateException` без фикса. + +### Затронутые модули `kora-examples` + +`examples/java/kora-java-telemetry`, `examples/kotlin/kora-kotlin-telemetry` — оба проверяют, что коллектор залогировал экспортированный спан, и оба ждали его до таймаута. + +--- + +## PR 20 — `fix(micrometer-module): bind the Prometheus registry as a MetricsScraper` + +- **Ветка:** `fix/metrics-scraper-not-bound` — [#810](https://github.com/kora-projects/kora/pull/810) +- **Коммит:** `500975770` +- **Модуль фреймворка:** `telemetry/micrometer-module` + +### Постановка задачи + +Эндпоинт метрик в **любом** приложении Kora 2.0 отвечал `# Metric Scraper disabled`. `MetricsHandler` берёт из графа `ValueOf>`, а компонента такого типа не предоставлял никто: `PrometheusMeterRegistryWrapper` интерфейс реализует, но фабрика объявлена как `Wrapped`, поэтому граф знает её только как `MeterRegistry`. Скрейпинг Prometheus был невозможен «из коробки» при любой конфигурации. + +### Что сделано + +`MetricsModule` дополнительно отдаёт `MetricsScraper` поверх реестра. Это `@DefaultComponent`, поэтому приложение, заменившее реестр, может связать свой; не-Prometheus реестр в этом формате не скрейпится и даёт пустое тело, а не ошибку — ровно то, что эндпоинт делал и раньше. + +### Покрытие тестами + +`MetricsScraperBindingTest` — поведение скрейпера для обоих видов реестра. Тест **не может** упасть на старом коде обычным способом: проверяемый метод и есть то, чего не хватало. Это оговорено честно; сквозное доказательство — гайд observability, который проверяет наличие `http_server_*` и JVM-метрик и падал на обоих языках, плюс контейнер, собранный до фикса, отдаёт `# Metric Scraper disabled` на `GET /metrics` при подключённом модуле. + +### Смежное изменение в примерах (не дефект) + +После фикса `/metrics` отдаёт JVM-метрики, но не `http_server_*`: в 2.0 `TelemetryConfig.MetricsConfig.enabled()` по умолчанию `false`. Гайдам добавлено `httpServer.telemetry.metrics.enabled = true` — это изменение поведения 2.0, а не баг. + +--- + +## PR 21 — `fix(http-server-undertow): register the XNIO provider metadata needed by native-image` + +- **Ветка:** `fix/graalvm-undertow-xnio-metadata` — [#811](https://github.com/kora-projects/kora/pull/811) +- **Коммит:** `587ef109f` +- **Модуль фреймворка:** `http/http-server-undertow` + +### Постановка задачи + +Любое приложение с Undertow, собранное в native-image, не стартует: `XNIO001001: No XNIO provider found`. +Сообщение вводит в заблуждение — провайдер в образе есть. `NioXnioProvider.` тянет `NioXnio` → +`org.xnio.nio.Log`, а jboss-logging грузит сгенерированную реализацию `<интерфейс>_$logger` рефлексией. +Класс не зарегистрирован → `ServiceConfigurationError` → его глотает `catch (Throwable)` внутри `Xnio.doGetInstance`, +и наружу выходит «провайдеров нет». + +### Что сделано + +В `reflect-config.json` добавлены `org.xnio.nio.Log_$logger` и `org.xnio._private.Messages_$logger` с конструктором +от `org.jboss.logging.Logger`; добавлен `resource-config.json` с файлом сервиса и `Version.properties`. + +### Покрытие тестами + +Модульного теста нет и быть не может: на JVM этот путь работает и без метаданных. Причина локализована +отдельным native-пробником на том же classpath (до: `ServiceConfigurationError`, после: `XNIO provider "nio"`), +а сквозная проверка — на `kora-java-graalvm-crud-jdbc` и `kora-java-graalvm-crud-cassandra`: `/system/readiness` +отвечает `OK` за 886 мс. + +--- + +## PR 22 — `fix(database-jdbc): register the Hikari micrometer tracker for native-image` + +- **Ветка:** `fix/graalvm-jdbc-hikari-micrometer-metadata` — [#812](https://github.com/kora-projects/kora/pull/812) +- **Коммит:** `18aa0fa2d` +- **Модуль фреймворка:** `database/database-jdbc` + +### Постановка задачи + +`JdbcDataSource` при включённых driver-метриках зовёт `HikariDataSource.setMetricRegistry`, а Hikari создаёт +`MicrometerMetricsTrackerFactory` по имени. Статических ссылок на класс нет, в образ он не попадает, +и пул падает при инициализации графа. Метаданные GraalVM для HikariCP покрывают dropwizard-ветку, но не micrometer. + +### Что сделано + +Добавлен `reflect-config.json` с конструктором `MicrometerMetricsTrackerFactory(MeterRegistry)` — ровно той +сигнатурой, которую запрашивает `UtilityElf.createInstance` (зафиксировано tracing-агентом). + +### Покрытие тестами + +Теста нет по той же причине — на JVM класс есть всегда. Проверено на `kora-java-graalvm-crud-jdbc`: +до фикса — `Failed to load class ...MicrometerMetricsTrackerFactory` в списке ошибок инициализации, +после — полный CRUD-цикл отвечает 200. + +--- + +## PR 23 — `fix(cache-caffeine): register the Caffeine node classes used without statistics` + +- **Ветка:** `fix/graalvm-caffeine-node-metadata` — [#813](https://github.com/kora-projects/kora/pull/813) +- **Коммит:** `28a2b38b2` +- **Модуль фреймворка:** `cache/cache-caffeine` + +### Постановка задачи + +Кэш Caffeine падает при первом же обращении: `Cannot reflectively invoke constructor 'PSMS()'`. +Caffeine выбирает сгенерированный класс узла по конфигурации кэша и создаёт его рефлексией. Общие +метаданные GraalVM регистрируют `PSMS` только под условием `"typeReached": "SSSMS"` — это класс кэша, +построенного **с** `recordStats()`. `CaffeineFactory` включает статистику только при включённых метриках, +а в 2.0 они по умолчанию выключены — строится `SSMS`, условие никогда не срабатывает. + +Соответствие конфигураций и классов снято экспериментом на JVM: + +| expireAfterAccess | expireAfterWrite | recordStats | кэш | узел | +|---|---|---|---|---| +| нет | нет | нет | `SSMS` | `PSMS` | +| нет | да | нет | `SSMSW` | `PSWMS` | +| да | нет | нет | `SSMSA` | `PSAMS` | +| да | да | нет | `SSMSAW` | `PSAWMS` | +| … | … | да | `SSSMS…` | те же узлы | + +### Что сделано + +Зарегистрированы все восемь классов кэша и четыре класса узла с базовыми родителями — кэш работает +независимо от того, включены ли метрики. + +### Покрытие тестами + +На JVM кэш работает в обоих состояниях, поэтому модульный тест не различает фикс. Проверено на +`kora-java-graalvm-crud-jdbc` (кэш Caffeine, метрики выключены). + +--- + +## PR 24 — `fix(micrometer-module,grpc-server,kafka): name the reflection configs so native-image reads them` + +- **Ветка:** `fix/graalvm-reflect-config-file-name` — [#814](https://github.com/kora-projects/kora/pull/814) +- **Коммит:** `274e3b039` +- **Модули фреймворка:** `telemetry/micrometer-module`, `grpc/grpc-server`, `kafka/kafka` + +### Постановка задачи + +native-image читает из `META-INF/native-image///` файл `reflect-config.json`. Три модуля +поставляли его под именем `reflection-config.json`, которое сборщик не ищет, — то есть эти регистрации +(netty-хендлеры gRPC, `RangeAssignor`/`AppInfo`/`DefaultPartitioner` у Kafka, методы `OperatingSystemMXBean`) +**никогда не попадали в образ**. + +Доказано на micrometer: метод, добавленный в старый файл, не дал эффекта (`/metrics` продолжал отвечать 500), +а та же запись в `reflect-config.json` — дала. + +### Что сделано + +Файлы переименованы в трёх модулях. Заодно добавлен `getProcessCpuTime`: micrometer 1.17 добавил метрику +`process.cpu.time`, которая читает этот метод рефлексией. + +### Покрытие тестами + +Имя файла значимо только для сборщика образа, поэтому любой JVM-тест зелён в обоих состояниях. Проверено на +трёх native-модулях: до — `GET /metrics` отвечает 500 и 670 байт текста ошибки; после — 200 и ~4,6 КБ +с `system_cpu_usage`, `process_cpu_usage`, `process_cpu_time_ns_total`. + +--- + +## PR 25 — `fix(database-cassandra): initialize the shaded guava comparator at build time` + +- **Ветка:** `fix/graalvm-cassandra-guava-comparator-init` — [#815](https://github.com/kora-projects/kora/pull/815) +- **Коммит:** `d2fd730ca` +- **Модуль фреймворка:** `database/database-cassandra` + +### Постановка задачи + +Сборка native-image падает на этапе анализа: объект shaded-компаратора `UnsignedBytes$…$PureJavaComparator` +оказывается в image heap, хотя его тип инициализируется в рантайме. Путь достижимости идёт через +`HashedWheelTimer$HashedWheelTimeout.` → `AtomicIntegerFieldUpdater.newUpdater` → декодирование +метаданных → компаратор из `SniEndPoint`. + +### Что сделано + +К `--initialize-at-build-time` модуля добавлен этот класс — ровно то, что советует сам сборщик. + +### Покрытие тестами + +Ошибка возникает в сборщике образа, а не в рантайме, поэтому из JVM-теста невоспроизводима. Проверено на +`kora-java-graalvm-crud-cassandra`: до — `nativeCompile` падает через ~13 с; после — образ собирается за ~31 с +(76 МиБ) и отдаёт полный CRUD против Scylla с Redis-кэшем. + +--- + +## PR 26 — `fix(logging-logback): keep logging events that happen outside a scope` + +- **Ветка:** `fix/logback-appender-outside-scope` — [#816](https://github.com/kora-projects/kora/pull/816) +- **Коммит:** `5797597c7` +- **Модуль фреймворка:** `logging/logging-logback` + +### Постановка задачи + +Найдено при разборе падающего `BlackBoxTests` kafka-модуля — и это самый широкий дефект из всех +двадцати семи: `KoraAsyncAppender.append` читает MDC через `ScopedValue.get()`, который вне скоупа +бросает `NoSuchElementException`. logback ловит исключение и выбрасывает событие, поэтому **всё, +что логируется вне обработки запроса или сообщения, теряется молча**: старт приложения, фоновые +задачи, логи драйверов, ошибки инициализации. + +### Что сделано + +MDC читается только при `MDC.VALUE.isBound()`, иначе — пустая карта. + +### Покрытие тестами + +Модульного теста нет — честно оговорено в PR. Попытка гонять `AsyncAppenderBase` напрямую из теста +не доставляет событие до `ListAppender` ни с фиксом, ни без него — такой тест был бы зелёным +в обоих состояниях и ничего не доказывал. Сквозное доказательство: лог приложения — 591 байт до фикса +и 16 001 байт после, при той же конфигурации. + +--- + +## PR 27 — `fix(config-common): declare the config as a dependency of the config watcher` + +- **Ветка:** `fix/config-watcher-starts-before-graph-ready` — [#818](https://github.com/kora-projects/kora/pull/818) +- **Коммит:** `c6d6ddd24` +- **Модуль фреймворка:** `config/config-common` + +### Постановка задачи + +При каждом старте любого приложения с файловым конфигом в лог падал стектрейс из потока +`config-reload`. Приложение продолжало работать, поэтому выглядело безобидно — но поток умирал на первом +же выражении, и слежение за конфигом молча не работало вообще. + +`ConfigModule` передаёт watcher'у **узел** конфига, а не значение, поэтому в сгенерированном графе узел +добавлен с пустым списком зависимостей — граф вправе инициализировать его раньше конфига. +Примечательно, что штатный `ConfigWatcherTest` строит граф вручную и зависимость указывает — поэтому +гонка никогда не ловилась. + +### Что сделано + +`ConfigModule` передаёт в `ConfigWatcher` не только узел, но и само значение конфига: + +```java +default ConfigWatcher applicationConfigWatcher(RefreshableGraph graph, + @Nullable @ApplicationConfig Node applicationConfigNode, + @Nullable @ApplicationConfig ConfigOrigin applicationConfig) { +``` + +Оттого граф обязан инициализировать конфиг раньше watcher'а, а начальное значение приходит +параметром — `graph.get()` на старте больше не вызывается вовсе. Первая версия фикса ждала +готовности узла в цикле с `catch (IllegalStateException)` — от неё отказались: глушить исключение +вместо того, чтобы объявить зависимость, — лечение симптома. + +### Покрытие тестами + +Нового модульного теста нет, и это осознанное решение: дефект был в том, как узел связывается +процессором, а тест, собирающий граф руками, вправе объявить любые зависимости и потому ничего +не доказывает. Ровно по этой причине штатный `ConfigWatcherTest` гонку и не ловил. + +Доказательство — сгенерированный граф примера до и после фикса: + +```java +// было +component2 = graphDraw.addNode(..., List.of(), ...) +// стало +component2 = graphDraw.addNode(..., List.of(component1), ...) +``` + +Стектрейс из потока `config-reload` при старте больше не появляется. Прогнаны зелёными: +`config-common`, `config-hocon`, `config-yaml`, `test-junit5`; штатный `ConfigWatcherTest` обновлён под +новую сигнатуру конструктора. diff --git a/migration/README.md b/migration/README.md index fa2f4eee..0ba40472 100644 --- a/migration/README.md +++ b/migration/README.md @@ -1,12 +1,66 @@ -# Kora 2 migration automation +# Миграция Kora 1.x → 2.0 -- `openrewrite/rewrite.yml`: AST-safe Java/Gradle recipes. -- `openrewrite/build.gradle`: standalone Rewrite runner configuration. -- `scripts/migrate_kora_2.py`: deterministic dry-run/apply fallback for Gradle Kotlin DSL, Groovy DSL, OpenAPI options, resources, and package-directory moves. +## Состав -Run scripts from repository root. Use dry-run first: +| Файл | Назначение | +|---|---| +| `KORA_2_JAVA_MIGRATION_GUIDE.md` | самодостаточное руководство для Java-проектов | +| `KORA_2_KOTLIN_MIGRATION_GUIDE.md` | самодостаточное руководство для Kotlin-проектов | +| `KORA_2_MIGRATION_STATUS.md` | статус каждого Gradle-модуля репозитория, по данным реальных прогонов сборки | +| `KORA_2_FINAL_REPORT.md` | итоговый отчёт: сводка по всем модулям, правилам, дефектам и незакрытым вопросам | +| `KORA_2_PULL_REQUESTS.md` | разбор каждого из 27 отправленных PR во фреймворк | +| `KORA_2_FRAMEWORK_ISSUES.md` | журнал дефектов и гипотез по самому фреймворку | +| `KORA_MIGRATION_NEURO.md` | инструкции для ИИ-агентов на недетерминированные трансформации | +| `openrewrite/` | AST-безопасные рецепты для Java и Gradle — состав, запуск и границы применимости в `openrewrite/README.md` | +| `scripts/` | детерминированные текстовые/файловые преобразования | + +Предыдущий сводный документ `KORA_2_MIGRATION_GUIDE.md` разложен на два языковых руководства без потери содержания; +ссылки на несуществующий снапшот `agents-md/kora-2` заменены на реальные источники: локальный чекаут фреймворка `../kora` @ `master` +и документацию 1.x в `agents-md/kora-docs`. + +## Порядок запуска ```shell -python migration/scripts/migrate_kora_2.py -python migration/scripts/migrate_kora_2.py --apply +# 1. Пакеты, координаты, версии, Kotlin, ресурсы, перенос каталогов пакетов +python migration/scripts/migrate_kora_2.py # dry-run, печатает план +python migration/scripts/migrate_kora_2.py --apply # применить + +# 2. Первая сборка после смены пакетов — обязательно так +export JAVA_HOME= +./gradlew clean --continue +./gradlew classes testClasses --continue --no-build-cache ``` + +Именно так мигрировался этот репозиторий. OpenRewrite-рецепт здесь не использовался: он не знает Kotlin, +а его рецепты по координатам требуют, чтобы новые координаты резолвились в момент применения +— того, что скрипт делает одним проходом вместе со сменой версии. Для чисто Java-проектов, +где текстовые замены нежелательны, рецепт применяется из самого мигрируемого проекта — +см. `openrewrite/README.md`. + +Оба слоя автоматизации идемпотентны. Перед запуском сохраните рабочее дерево. + +## Требования к окружению + +- **JDK 25 для самого Gradle-процесса**, не только в toolchain: `io.koraframework:openapi-generator` попадает в buildscript classpath и требует JVM 25. +- Локально опубликованный фреймворк: в `../kora` выполнить `./gradlew publishToMavenLocal` (версия `2.0.0-SNAPSHOT`), в примерах — `koraVersion=2.0.0-SNAPSHOT`. +- Docker — для интеграционных тестов на Testcontainers. +- GraalVM `native-image` — для native-сборок; при его отсутствии GraalVM-модули мигрируются только в JVM-части. + +## Известные ограничения автоматизации + +`scripts/migrate_kora_2.py` писался под этот репозиторий и **пока не пригоден к переносу в чужой проект без доработки**: + +- содержит замены с хардкодом имён классов конкретных примеров (`SimpleCache`, `CompositeCache`, `UserContextMapping`); +- применяет широкие текстовые замены ко всем типам файлов, включая `.conf`/`.yaml`/`.xml` (например, `parameters =` → `args =`), что вне Kora-кода может задеть постороннее; +- удаляет строки `@Nonnull` / `@NonNull` и `@Nullable` без анализа позиции аннотации; +- не имеет режима ограничения области применения (каталог/модуль). + +OpenRewrite-рецепт **не трансформирует Kotlin** — для Kotlin-модулей рабочей автоматикой остаётся скрипт. +Его Java-часть покрыта before/after тестами (`cd migration/openrewrite && ../../gradlew test`, 5/5); +рецепты по координатам модульным тестом не покрыты — причины разобраны в `openrewrite/README.md`. + +Что автоматизация принципиально не делает: типизированные resilient-спецификации, семантическое снятие `suspend` по цепочке вызовов +(сигнатуры `@HttpClient` она исправляет механически), перенос Kotlin Structured Concurrency на +Java `StructuredTaskScope` с выбором актуального preview `Joiner`/timeout/cancellation policy, +корректная расстановка type-use аннотаций JSpecify, адаптация сгенерированного OpenAPI-кода, миграция S3-клиента. +Эти классы изменений описаны в `KORA_MIGRATION_NEURO.md`. diff --git a/migration/openrewrite/README.md b/migration/openrewrite/README.md new file mode 100644 index 00000000..5fd689d9 --- /dev/null +++ b/migration/openrewrite/README.md @@ -0,0 +1,96 @@ +# OpenRewrite-рецепты Kora 1.x → 2.0 + +Сам рецепт — `src/main/resources/META-INF/rewrite/kora-1-to-2.yml`. Каноническое расположение: +модуль собирается в jar, который можно положить в конфигурацию `rewrite` любого проекта, +либо файл можно указать напрямую через `configFile`. + +## Состав + +| Рецепт | Что делает | +|---|---| +| `io.koraframework.migration.RelocateKoraPackages` | `ru.tinkoff.kora.*` → `io.koraframework.*` | +| `io.koraframework.migration.RelocateKoraAnnotations` | DI-аннотации из `common` в `common.annotation` | +| `io.koraframework.migration.RenameKoraTypes` | типы, сменившие имя или модуль | +| `io.koraframework.migration.MigrateNullabilityAnnotations` | `jakarta.annotation.Nullable` → JSpecify | +| `io.koraframework.migration.UpdateKoraDependencies` | координаты сборки | +| `io.koraframework.migration.NormalizeKotlinDependencies` | прямые `implementation(platform(...))`, versioned `ksp`/`kspTest`, удаление устаревшего processor wiring; `kspTest` сохраняется | +| `io.koraframework.migration.UpdateJUnit` | любая literal-версия `junitVersion`/JUnit BOM → `6.1.3` | +| `io.koraframework.migration.UpdateMockK` | любая явно заданная версия `io.mockk:mockk` → Java 25-совместимая `1.14.9` | +| `io.koraframework.migration.RemoveEmptyEnvironmentBlock` | удаляет `environment(["": ""])`, ломающий запуск JVM на Windows с `CreateProcess error=87` | +| `io.koraframework.migration.RemoveSuspendHttpClientMethods` | снимает `suspend` с Kotlin client contracts; вызывающая цепочка и удаление неиспользуемых coroutine dependencies проверяются вручную | +| `io.koraframework.migration.RemoveSuspendRepositoryMethods` | снимает `suspend` с Kotlin `*Repository.kt`; вызовы, тесты, duplicate repositories и coroutine dependencies проверяются вручную | +| `io.koraframework.migration.RemoveSuspendHttpServerMethods` | снимает `suspend` с Kotlin `*Controller.kt`; вызовы, тесты, duplicate controllers и coroutine dependencies проверяются вручную | +| `io.koraframework.migration.Kora1To2` | агрегат всех рецептов | + +Каждый применим отдельно — например, если координаты в проекте уже поменяны руками. + +## Как применить к своему проекту + +В корневой build-файл **мигрируемого** проекта: + +```groovy +plugins { + id "org.openrewrite.rewrite" version "7.19.0" +} + +dependencies { + rewrite "org.openrewrite.recipe:rewrite-java-dependencies:1.36.0" +} + +rewrite { + activeRecipe("io.koraframework.migration.Kora1To2") + configFile = file("<путь>/migration/openrewrite/src/main/resources/META-INF/rewrite/kora-1-to-2.yml") +} +``` + +```shell +./gradlew rewriteDryRun # патч в build/reports/rewrite/rewrite.patch +./gradlew rewriteRun +``` + +Запускать `rewriteRun` внутри самого `migration/openrewrite` бессмысленно: это отдельный Gradle-build +со своим `settings.gradle`, и видит он только свои исходники, а не репозиторий примеров. + +## Тесты + +```shell +cd migration/openrewrite && ../../gradlew test +``` + +`Kora1To2Test` — before/after на каждую группу Java-правил плюс проверка идемпотентности +(на уже мигрированном коде рецепт не делает ничего). На момент последнего прогона — 5/5. + +## Чего тесты НЕ покрывают и почему + +`UpdateKoraDependencies` построен на `org.openrewrite.java.dependencies.ChangeDependency`, у которого +два ограничения, делающих модульный тест невозможным: + +1. **На Gradle-файлах он работает только при наличии маркера `GradleProject`**, который ставит + Gradle-плагин во время реального запуска. В `RewriteTest` такого маркера нет, и рецепт молча + не делает ничего (проверено отдельным пробником). Официальный обход — `withToolingApi()` из + `rewrite-gradle-tooling-model`, но этот артефакт не опубликован для ветки 8.56.x. +2. **Переименование artifactId применяется только после успешного резолва новых координат.** + Пары вида `ru.tinkoff.kora:cache-redis:1.1.29` → `io.koraframework:cache-redis-lettuce:1.1.29` + не резолвятся никогда: версии 1.x под новой группой не существует по определению, + а `cache-redis-lettuce` есть только в снапшоте 2.0 (на Maven Central лежит `2.0.0.alpha6`, + где этого артефакта ещё нет). Поэтому перед запуском рецепта версию Kora надо поднять + до 2.0 и иметь соответствующий репозиторий (Maven Local после `publishToMavenLocal` либо + snapshots-репозиторий Sonatype). + +Сам репозиторий `kora-examples` мигрировался **скриптом** `migration/scripts/migrate_kora_2.py`, +а не этим рецептом: скрипт меняет и версию, и координаты одним проходом и не требует, +чтобы промежуточное состояние резолвилось. Рецепт здесь — для чужих проектов, где нужна +именно AST-безопасная правка Java, а не текстовые замены. + +## Что рецепт не делает вообще + +- **Kotlin не трогает.** OpenRewrite парсит Kotlin экспериментально; для Kotlin-модулей + рабочая автоматика — скрипт. +- **Не ставит JSpecify в type-use позицию.** Меняется только тип аннотации; правильное место + для дженериков и массивов — семантика, описана в языковых руководствах. +- **Не трогает каталоги ресурсов**, включая `META-INF/native-image//` — их переименовывают + вручную. +- **Не делает семантику**: resilient-спецификации, снятие `suspend` по вызывающей цепочке, + перенос Kotlin Structured Concurrency на актуальный Java `StructuredTaskScope` preview, + S3-клиент, сгенерированный + OpenAPI-код. Это `KORA_MIGRATION_NEURO.md`. diff --git a/migration/openrewrite/build.gradle b/migration/openrewrite/build.gradle index 39f65557..36b1e088 100644 --- a/migration/openrewrite/build.gradle +++ b/migration/openrewrite/build.gradle @@ -1,5 +1,13 @@ plugins { - id "org.openrewrite.rewrite" version "7.19.0" + id "java" +} + +// rewrite-java-21 is the newest parser OpenRewrite 8.56 ships, and it has to run on a JVM +// of the same generation as the sources it parses +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } repositories { @@ -7,11 +15,34 @@ repositories { } dependencies { - rewrite "org.openrewrite.recipe:rewrite-migrate-java:3.23.0" - rewrite "org.openrewrite.recipe:rewrite-gradle:4.27.0" + testImplementation platform("org.openrewrite.recipe:rewrite-recipe-bom:3.10.1") + + // ChangePackage / ChangeType + testImplementation "org.openrewrite:rewrite-java" + testRuntimeOnly "org.openrewrite:rewrite-java-21" + // ChangeDependency over build files + testImplementation "org.openrewrite:rewrite-maven" + testImplementation "org.openrewrite.recipe:rewrite-java-dependencies" + // loads the declarative recipes from META-INF/rewrite + testImplementation "org.openrewrite:rewrite-yaml" + testImplementation "org.openrewrite:rewrite-test" + + testImplementation platform("org.junit:junit-bom:6.1.3") + testImplementation "org.junit.jupiter:junit-jupiter" + testRuntimeOnly "org.junit.platform:junit-platform-launcher" } -rewrite { - activeRecipe("io.koraframework.migration.Kora1To2") - configFile = file("rewrite.yml") +test { + useJUnitPlatform() + testLogging { + def isCiBuild = providers.environmentVariable("CI").map { it.toBoolean() }.orElse(false).get() + if (isCiBuild) { + showStandardStreams(false) + events("failed") + } else { + showStandardStreams(true) + events("passed", "skipped", "failed") + } + exceptionFormat = "full" + } } diff --git a/migration/openrewrite/rewrite.yml b/migration/openrewrite/rewrite.yml deleted file mode 100644 index 52b6f91e..00000000 --- a/migration/openrewrite/rewrite.yml +++ /dev/null @@ -1,61 +0,0 @@ -type: specs.openrewrite.org/v1beta/recipe -name: io.koraframework.migration.Kora1To2 -displayName: Migrate Kora 1.x to 2.0 -description: Migrates stable package, type, and Gradle coordinate changes. Semantic API changes are intentionally left to module-specific migration. -recipeList: - - org.openrewrite.java.ChangePackage: - oldPackageName: ru.tinkoff.kora - newPackageName: io.koraframework - recursive: true - - org.openrewrite.java.ChangeType: - oldFullyQualifiedTypeName: io.koraframework.common.KoraApp - newFullyQualifiedTypeName: io.koraframework.common.annotation.KoraApp - - org.openrewrite.java.ChangeType: - oldFullyQualifiedTypeName: io.koraframework.common.Component - newFullyQualifiedTypeName: io.koraframework.common.annotation.Component - - org.openrewrite.java.ChangeType: - oldFullyQualifiedTypeName: io.koraframework.common.DefaultComponent - newFullyQualifiedTypeName: io.koraframework.common.annotation.DefaultComponent - - org.openrewrite.java.ChangeType: - oldFullyQualifiedTypeName: io.koraframework.common.KoraSubmodule - newFullyQualifiedTypeName: io.koraframework.common.annotation.KoraSubmodule - - org.openrewrite.java.ChangeType: - oldFullyQualifiedTypeName: io.koraframework.common.Mapping - newFullyQualifiedTypeName: io.koraframework.common.annotation.Mapping - - org.openrewrite.java.ChangeType: - oldFullyQualifiedTypeName: io.koraframework.common.Module - newFullyQualifiedTypeName: io.koraframework.common.annotation.Module - - org.openrewrite.java.ChangeType: - oldFullyQualifiedTypeName: io.koraframework.common.Tag - newFullyQualifiedTypeName: io.koraframework.common.annotation.Tag - - org.openrewrite.java.ChangeType: - oldFullyQualifiedTypeName: io.koraframework.http.server.undertow.UndertowHttpServerModule - newFullyQualifiedTypeName: io.koraframework.http.server.undertow.UndertowPublicHttpServerModule - - org.openrewrite.java.ChangeType: - oldFullyQualifiedTypeName: io.koraframework.json.module.JsonModule - newFullyQualifiedTypeName: io.koraframework.json.common.JsonModule - - org.openrewrite.java.ChangeType: - oldFullyQualifiedTypeName: jakarta.annotation.Nullable - newFullyQualifiedTypeName: org.jspecify.annotations.Nullable - - org.openrewrite.java.ChangeType: - oldFullyQualifiedTypeName: io.koraframework.config.common.annotation.ConfigValueExtractor - newFullyQualifiedTypeName: io.koraframework.config.common.annotation.ConfigMapper - - org.openrewrite.java.ChangeType: - oldFullyQualifiedTypeName: io.koraframework.cache.redis.RedisCacheModule - newFullyQualifiedTypeName: io.koraframework.cache.redis.lettuce.LettuceRedisCacheModule - - org.openrewrite.java.dependencies.ChangeDependencyGroupId: - oldGroupId: ru.tinkoff.kora - newGroupId: io.koraframework - overrideManagedVersion: false - - org.openrewrite.java.dependencies.ChangeDependency: - oldGroupId: io.koraframework - oldArtifactId: json-module - newGroupId: io.koraframework - newArtifactId: json-common - overrideManagedVersion: false - - org.openrewrite.java.dependencies.ChangeDependency: - oldGroupId: io.koraframework - oldArtifactId: cache-redis - newGroupId: io.koraframework - newArtifactId: cache-redis-lettuce - overrideManagedVersion: false diff --git a/migration/openrewrite/src/main/resources/META-INF/rewrite/kora-1-to-2.yml b/migration/openrewrite/src/main/resources/META-INF/rewrite/kora-1-to-2.yml new file mode 100644 index 00000000..c0b4bcbf --- /dev/null +++ b/migration/openrewrite/src/main/resources/META-INF/rewrite/kora-1-to-2.yml @@ -0,0 +1,263 @@ +######################################################################################## +# Kora 1.x -> 2.0, OpenRewrite recipes. +# +# Focused recipes that can be activated one by one; Kora1To2 composes all of them. +# Only deterministic, AST-safe changes live here. Semantic changes (resilient specs, +# suspend call-chain removal, JSpecify placement, S3 client) are out of scope on purpose and are +# described in KORA_2_MIGRATION_NEURO.md. +######################################################################################## +type: specs.openrewrite.org/v1beta/recipe +name: io.koraframework.migration.Kora1To2 +displayName: Migrate Kora 1.x to 2.0 +description: >- + Aggregate of every deterministic Kora 1.x to 2.0 change: package relocation, annotation + and type moves, nullability annotations and build coordinates. Semantic API changes are + intentionally left to module-specific migration. +recipeList: + - io.koraframework.migration.RelocateKoraPackages + - io.koraframework.migration.RelocateKoraAnnotations + - io.koraframework.migration.RenameKoraTypes + - io.koraframework.migration.MigrateNullabilityAnnotations + - io.koraframework.migration.UpdateKoraDependencies + - io.koraframework.migration.NormalizeKotlinDependencies + - io.koraframework.migration.UpdateJUnit + - io.koraframework.migration.UpdateMockK + - io.koraframework.migration.RemoveEmptyEnvironmentBlock + - io.koraframework.migration.RemoveSuspendHttpClientMethods + - io.koraframework.migration.RemoveSuspendRepositoryMethods + - io.koraframework.migration.RemoveSuspendHttpServerMethods + +--- +type: specs.openrewrite.org/v1beta/recipe +name: io.koraframework.migration.RelocateKoraPackages +displayName: Relocate Kora packages +description: Kora 2.0 moved every package from `ru.tinkoff.kora` to `io.koraframework`. +recipeList: + - org.openrewrite.java.ChangePackage: + oldPackageName: ru.tinkoff.kora + newPackageName: io.koraframework + recursive: true + +--- +type: specs.openrewrite.org/v1beta/recipe +name: io.koraframework.migration.RelocateKoraAnnotations +displayName: Relocate Kora DI annotations into `common.annotation` +description: >- + Kora 2.0 moved the dependency injection annotations out of `io.koraframework.common` + into `io.koraframework.common.annotation`. Runs after RelocateKoraPackages, so the + source type names are already the 2.0 ones. +recipeList: + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: io.koraframework.common.KoraApp + newFullyQualifiedTypeName: io.koraframework.common.annotation.KoraApp + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: io.koraframework.common.Component + newFullyQualifiedTypeName: io.koraframework.common.annotation.Component + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: io.koraframework.common.DefaultComponent + newFullyQualifiedTypeName: io.koraframework.common.annotation.DefaultComponent + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: io.koraframework.common.KoraSubmodule + newFullyQualifiedTypeName: io.koraframework.common.annotation.KoraSubmodule + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: io.koraframework.common.Mapping + newFullyQualifiedTypeName: io.koraframework.common.annotation.Mapping + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: io.koraframework.common.Module + newFullyQualifiedTypeName: io.koraframework.common.annotation.Module + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: io.koraframework.common.Tag + newFullyQualifiedTypeName: io.koraframework.common.annotation.Tag + +--- +type: specs.openrewrite.org/v1beta/recipe +name: io.koraframework.migration.RenameKoraTypes +displayName: Rename Kora types that changed name or module +description: >- + Types whose fully qualified name changed for a reason other than the package relocation: + the Undertow module was split into public and private servers, the JSON module moved to + `json-common`, the config extractor annotation was renamed, and the Redis cache module + became transport specific. +recipeList: + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: io.koraframework.http.server.undertow.UndertowHttpServerModule + newFullyQualifiedTypeName: io.koraframework.http.server.undertow.UndertowPublicHttpServerModule + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: io.koraframework.json.module.JsonModule + newFullyQualifiedTypeName: io.koraframework.json.common.JsonModule + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: io.koraframework.config.common.annotation.ConfigValueExtractor + newFullyQualifiedTypeName: io.koraframework.config.common.annotation.ConfigMapper + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: io.koraframework.cache.redis.RedisCacheModule + newFullyQualifiedTypeName: io.koraframework.cache.redis.lettuce.LettuceRedisCacheModule + +--- +type: specs.openrewrite.org/v1beta/recipe +name: io.koraframework.migration.MigrateNullabilityAnnotations +displayName: Replace `jakarta.annotation.Nullable` with JSpecify +description: >- + Kora 2.0 reads JSpecify annotations. This recipe only swaps the type; it does NOT move + the annotation to the type-use position that JSpecify requires on generics and arrays -- + that placement is a semantic change and is described in the language migration guides. +recipeList: + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: jakarta.annotation.Nullable + newFullyQualifiedTypeName: org.jspecify.annotations.Nullable + +--- +type: specs.openrewrite.org/v1beta/recipe +name: io.koraframework.migration.UpdateKoraDependencies +displayName: Update Kora build coordinates +description: >- + Kora 2.0 publishes under the `io.koraframework` group; the BOM and other artifacts were also renamed. + Versions are left untouched -- the Kora version is normally driven by the + `io.koraframework:kora-bom` BOM. +recipeList: + - org.openrewrite.java.dependencies.ChangeDependency: + oldGroupId: ru.tinkoff.kora + oldArtifactId: kora-parent + newGroupId: io.koraframework + newArtifactId: kora-bom + overrideManagedVersion: false + - org.openrewrite.java.dependencies.ChangeDependency: + oldGroupId: ru.tinkoff.kora + oldArtifactId: "*" + newGroupId: io.koraframework + overrideManagedVersion: false + - org.openrewrite.java.dependencies.ChangeDependency: + oldGroupId: io.koraframework + oldArtifactId: kora-parent + newGroupId: io.koraframework + newArtifactId: kora-bom + overrideManagedVersion: false + - org.openrewrite.java.dependencies.ChangeDependency: + oldGroupId: io.koraframework + oldArtifactId: json-module + newGroupId: io.koraframework + newArtifactId: json-common + overrideManagedVersion: false + - org.openrewrite.java.dependencies.ChangeDependency: + oldGroupId: io.koraframework + oldArtifactId: cache-redis + newGroupId: io.koraframework + newArtifactId: cache-redis-lettuce + overrideManagedVersion: false + +--- +type: specs.openrewrite.org/v1beta/recipe +name: io.koraframework.migration.NormalizeKotlinDependencies +displayName: Normalize Kora dependencies in Kotlin Gradle builds +description: >- + Use the BOM directly on implementation, version KSP explicitly, and remove obsolete processor wiring. + kspTest is semantic and is preserved; tests declaring @KoraApp in src/test require it. +recipeList: + - org.openrewrite.text.FindAndReplace: + find: '(?ms)^[ \t]*val koraBom: Configuration by configurations\.creating\r?\n[ \t]*configurations \{\r?\n.*?extendsFrom\(koraBom\).*?^[ \t]*\}\r?\n(?:\r?\n)?' + replace: '' + regex: true + filePattern: '**/*.gradle.kts' + + - org.openrewrite.text.FindAndReplace: + find: 'koraBom(platform("io.koraframework:kora-bom:${property("koraVersion")}"))' + replace: 'implementation(platform("io.koraframework:kora-bom:${property("koraVersion")}"))' + filePattern: '**/*.gradle.kts' + - org.openrewrite.text.FindAndReplace: + find: 'ksp("io.koraframework:symbol-processors")' + replace: 'ksp("io.koraframework:symbol-processors:${property("koraVersion")}")' + filePattern: '**/*.gradle.kts' + - org.openrewrite.text.FindAndReplace: + find: 'kspTest("io.koraframework:symbol-processors")' + replace: 'kspTest("io.koraframework:symbol-processors:${property("koraVersion")}")' + filePattern: '**/*.gradle.kts' + - org.openrewrite.text.FindAndReplace: + find: '(?mi)^.*ka(?i:pt).*\r?\n' + replace: '' + regex: true + filePattern: '**/*.gradle.kts' + +--- +type: specs.openrewrite.org/v1beta/recipe +name: io.koraframework.migration.UpdateJUnit +displayName: Update JUnit to 6.1.3 +description: Update any literal shared JUnit version property and explicitly pinned JUnit BOM to 6.1.3. +recipeList: + - org.openrewrite.text.FindAndReplace: + find: '(?m)^(?\s*junitVersion\s*=\s*)[0-9][0-9A-Za-z.+_-]*(?\s*(?:#.*)?)$' + replace: '${prefix}6.1.3${suffix}' + regex: true + filePattern: '**/gradle.properties' + - org.openrewrite.text.FindAndReplace: + find: '(?org\.junit:junit-bom:)[0-9][0-9A-Za-z.+_-]*' + replace: '${prefix}6.1.3' + regex: true + filePattern: '**/*.gradle;**/*.gradle.kts' + +--- +type: specs.openrewrite.org/v1beta/recipe +name: io.koraframework.migration.UpdateMockK +displayName: Update MockK for Java 25 +description: Update any explicitly pinned MockK version to 1.14.9, whose Byte Buddy supports Java 25 class files. +recipeList: + - org.openrewrite.text.FindAndReplace: + find: '(?io\.mockk:mockk:)[0-9][0-9A-Za-z.+_-]*' + replace: '${prefix}1.14.9' + regex: true + filePattern: '**/*.gradle;**/*.gradle.kts' + +--- +type: specs.openrewrite.org/v1beta/recipe +name: io.koraframework.migration.RemoveEmptyEnvironmentBlock +displayName: Remove invalid empty Gradle environment entries +description: Remove environment(["": ""]) placeholders that prevent JVM process startup on Windows. +recipeList: + - org.openrewrite.text.FindAndReplace: + find: '(?ms)\r?\n[ \t]*environment\(\[\s*\r?\n[ \t]*"": "",?\s*\r?\n[ \t]*\]\)\s*\r?\n' + replace: '\n' + regex: true + filePattern: '**/*.gradle;**/*.gradle.kts' + +--- +type: specs.openrewrite.org/v1beta/recipe +name: io.koraframework.migration.RemoveSuspendHttpClientMethods +displayName: Remove suspend from Kotlin HTTP client methods +description: >- + Kora 2.0 HTTP client contracts are synchronous. Removes suspend from methods declared in + conventional *HttpClient.kt and *ApiClient.kt contract files. Coroutine call sites still + require a semantic review. +recipeList: + - org.openrewrite.text.FindAndReplace: + find: '(?m)^([ \\t]*)suspend fun ' + replace: '$1fun ' + regex: true + filePattern: '**/*HttpClient.kt;**/*ApiClient.kt' + +--- +type: specs.openrewrite.org/v1beta/recipe +name: io.koraframework.migration.RemoveSuspendRepositoryMethods +displayName: Remove suspend from Kotlin repository methods +description: >- + Kora 2.0 repository contracts are synchronous. Removes suspend from conventional Kotlin + *Repository.kt contracts. Call sites, tests, duplicate repositories and coroutine dependencies + require a semantic review. +recipeList: + - org.openrewrite.text.FindAndReplace: + find: '(?m)^([ \\t]*)suspend fun ' + replace: '$1fun ' + regex: true + filePattern: '**/*Repository.kt' + +--- +type: specs.openrewrite.org/v1beta/recipe +name: io.koraframework.migration.RemoveSuspendHttpServerMethods +displayName: Remove suspend from Kotlin HTTP server methods +description: >- + Kora 2.0 HTTP server contracts are synchronous. Removes suspend from conventional Kotlin + *Controller.kt files. Call sites, tests, duplicate controllers and coroutine dependencies + require a semantic review. +recipeList: + - org.openrewrite.text.FindAndReplace: + find: '(?m)^([ \\t]*)suspend fun ' + replace: '$1fun ' + regex: true + filePattern: '**/*Controller.kt' diff --git a/migration/openrewrite/src/test/java/io/koraframework/migration/Kora1To2Test.java b/migration/openrewrite/src/test/java/io/koraframework/migration/Kora1To2Test.java new file mode 100644 index 00000000..e97a3f16 --- /dev/null +++ b/migration/openrewrite/src/test/java/io/koraframework/migration/Kora1To2Test.java @@ -0,0 +1,249 @@ +package io.koraframework.migration; + +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.test.SourceSpecs.text; + +import org.junit.jupiter.api.Test; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +/** + * Before/after coverage for the declarative recipes in + * {@code META-INF/rewrite/kora-1-to-2.yml}. + * + *

Kora itself is not on the test classpath, so the 1.x types the recipes match on are + * declared as stubs. That is enough for type attribution and keeps the tests independent + * of any published Kora artifact. + */ +class Kora1To2Test implements RewriteTest { + + private static final String[] KORA_1_X_STUBS = { + """ + package ru.tinkoff.kora.common; + public @interface Component {} + """, + """ + package ru.tinkoff.kora.common; + public @interface KoraApp {} + """, + """ + package ru.tinkoff.kora.common; + public @interface Tag { Class[] value(); } + """, + """ + package ru.tinkoff.kora.json.module; + public interface JsonModule {} + """, + """ + package ru.tinkoff.kora.http.server.undertow; + public interface UndertowHttpServerModule {} + """, + """ + package jakarta.annotation; + public @interface Nullable {} + """, + // 2.0 type, present only so the idempotency test has attributed types + """ + package io.koraframework.common.annotation; + public @interface Component {} + """, + }; + + @Override + public void defaults(RecipeSpec spec) { + spec.recipeFromResources("io.koraframework.migration.Kora1To2") + .parser(JavaParser.fromJavaVersion().dependsOn(KORA_1_X_STUBS)); + } + + @Test + void relocatesPackagesAndMovesDiAnnotations() { + rewriteRun( + java( + """ + import ru.tinkoff.kora.common.Component; + + @Component + public class PetService {} + """, + """ + import io.koraframework.common.annotation.Component; + + @Component + public class PetService {} + """ + ) + ); + } + + @Test + void movesApplicationAndTagAnnotationsTogether() { + rewriteRun( + java( + """ + import ru.tinkoff.kora.common.KoraApp; + import ru.tinkoff.kora.common.Tag; + + @KoraApp + public interface Application { + @Tag(Application.class) + String name(); + } + """, + """ + import io.koraframework.common.annotation.KoraApp; + import io.koraframework.common.annotation.Tag; + + @KoraApp + public interface Application { + @Tag(Application.class) + String name(); + } + """ + ) + ); + } + + @Test + void renamesTypesThatChangedModule() { + rewriteRun( + java( + """ + import ru.tinkoff.kora.http.server.undertow.UndertowHttpServerModule; + import ru.tinkoff.kora.json.module.JsonModule; + + public interface Application extends UndertowHttpServerModule, JsonModule {} + """, + """ + import io.koraframework.http.server.undertow.UndertowPublicHttpServerModule; + import io.koraframework.json.common.JsonModule; + + public interface Application extends UndertowPublicHttpServerModule, JsonModule {} + """ + ) + ); + } + + @Test + void replacesJakartaNullableWithJSpecify() { + rewriteRun( + java( + """ + import jakarta.annotation.Nullable; + + public class PetService { + @Nullable + String name() { + return null; + } + } + """, + """ + import org.jspecify.annotations.Nullable; + + public class PetService { + @Nullable + String name() { + return null; + } + } + """ + ) + ); + } + + /** + * The recipe has to be safe to run twice: the migration script and the recipe overlap, + * and a project half migrated by hand is the normal case rather than the exception. + */ + @Test + void leavesAlreadyMigratedCodeAlone() { + rewriteRun( + java( + """ + import io.koraframework.common.annotation.Component; + + @Component + public class PetService {} + """ + ) + ); + } + + @Test + void removesSuspendFromKotlinHttpClientContract() { + rewriteRun( + spec -> spec.recipeFromResources("io.koraframework.migration.RemoveSuspendHttpClientMethods"), + text( + """ + interface PetHttpClient { + suspend fun get(id: Long): Pet + } + """, + """ + interface PetHttpClient { + fun get(id: Long): Pet + } + """, + source -> source.path("src/main/kotlin/PetHttpClient.kt") + ) + ); + } + + @Test + void removesSuspendFromKotlinRepositoryContract() { + rewriteRun( + spec -> spec.recipeFromResources("io.koraframework.migration.RemoveSuspendRepositoryMethods"), + text( + """ + interface PetRepository { + suspend fun findById(id: Long): Pet? + } + """, + """ + interface PetRepository { + fun findById(id: Long): Pet? + } + """, + source -> source.path("src/main/kotlin/PetRepository.kt") + ) + ); + } + + @Test + void removesSuspendFromKotlinHttpServerContract() { + rewriteRun( + spec -> spec.recipeFromResources("io.koraframework.migration.RemoveSuspendHttpServerMethods"), + text( + """ + class PetController { + suspend fun get(id: Long): Pet + } + """, + """ + class PetController { + fun get(id: Long): Pet + } + """, + source -> source.path("src/main/kotlin/PetController.kt") + ) + ); + } + + @Test + void updatesAnyLiteralJUnitVersionTo613() { + rewriteRun( + spec -> spec.recipeFromResources("io.koraframework.migration.UpdateJUnit"), + text( + "junitVersion=5.11.0-RC1\n", + "junitVersion=6.1.3\n", + source -> source.path("gradle.properties") + ), + text( + "testImplementation platform(\"org.junit:junit-bom:6.0.2\")\n", + "testImplementation platform(\"org.junit:junit-bom:6.1.3\")\n", + source -> source.path("build.gradle") + ) + ); + } +} diff --git a/migration/scripts/migrate_api_renames.py b/migration/scripts/migrate_api_renames.py new file mode 100644 index 00000000..4db70697 --- /dev/null +++ b/migration/scripts/migrate_api_renames.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""Kora 1.x -> 2.0: mechanical API renames that OpenRewrite does not cover. + +Rules +----- +1. JSON module rename: JsonCommonModule -> JsonModule +2. Repository executor: `Repository#getConnectionFactory()` became `executor()`, + returning the `Executor` that still carries `inTx(...)`. +3. JSON unchecked methods: the `*Unchecked` variants are gone, the plain ones no longer + declare checked exceptions: + JsonWriter.toStringUnchecked(x) -> toString(x) + JsonWriter.toByteArrayUnchecked(x) -> toByteArray(x) + JsonReader.readUnchecked(x) -> read(x) +4. Config extraction contract: the runtime interface was renamed and moved, and its method + now has an explicit "or throw" variant: + io.koraframework.config.common.extractor.ConfigMapper + -> io.koraframework.config.common.mapper.ConfigValueMapper + extractor.extract(value) -> mapper.mapOrThrow(value) + The `@ConfigMapper` *annotation* keeps its name and lives in + `io.koraframework.config.common.annotation` - it must not be renamed, which is why the + rule keys off the old package and off the generic parameter. +5. Fallback lost its name: 2.0 has no FallbackSpec and no named fallback configuration, + so the `value` attribute is gone and only `method` remains: + @Fallback(value = "default", method = "f()") -> @Fallback(method = "f()") +6. JSpecify placement (Java only): JSpecify annotations are type-use, so on a qualified + nested type they must sit right before the simple name: + @Nullable Entity.FieldType -> Entity.@Nullable FieldType + javac itself suggests this form: + error: type annotation @org.jspecify.annotations.Nullable is not expected here + (to annotate a qualified type, write Entity.@org.jspecify.annotations.Nullable FieldType) + +Dry-run by default; pass --apply to write. Idempotent. + +Usage: + python migration/scripts/migrate_api_renames.py [--apply] [PATH ...] + +Limitations +----------- +- Rule 6 only rewrites `@Nullable` / `@NonNull` / `@NullMarked` immediately followed by a + dotted type whose every segment starts with an upper-case letter (a nested type). Package + qualified names (`java.lang.String`) are intentionally not touched, since the correct + placement there depends on the surrounding declaration. +- Rule 6 is skipped for Kotlin: there nullability is expressed by the type (`T?`), and the + annotation is removed rather than moved. +""" + +from __future__ import annotations + +import argparse +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +DEFAULT_SCAN = (ROOT / "examples", ROOT / "guides") +SKIP_PARTS = {"build", ".gradle", ".git", "agents-md"} + +TEXT_RENAMES = ( + ("JsonCommonModule", "JsonModule"), + ("toStringUnchecked(", "toString("), + ("toByteArrayUnchecked(", "toByteArray("), + ("readUnchecked(", "read("), + # the repository no longer exposes a connection factory, it exposes the executor + ("getJdbcConnectionFactory()", "executor()"), + ("getR2dbcConnectionFactory()", "executor()"), + ("getVertxConnectionFactory()", "executor()"), + ("getCassandraConnectionFactory()", "executor()"), +) + +# The old config extraction contract. Keyed off the old package so the `@ConfigMapper` +# annotation of 2.0, which shares the simple name, is never touched. +CONFIG_EXTRACTOR_IMPORT = "io.koraframework.config.common.extractor.ConfigMapper" +CONFIG_VALUE_MAPPER_IMPORT = "io.koraframework.config.common.mapper.ConfigValueMapper" + +# @Fallback(value = "name", method = "f()") -> @Fallback(method = "f()") +FALLBACK_VALUE = re.compile(r'@Fallback\(\s*value\s*=\s*"[^"]*"\s*,\s*') + +# @Nullable Outer.Inner -> Outer.@Nullable Inner +QUALIFIED_TYPE_ANNOTATION = re.compile( + r"@(Nullable|NonNull|NullMarked)\s+((?:[A-Z][A-Za-z0-9_]*\.)+)([A-Z][A-Za-z0-9_]*)" +) + + +def source_files(scan_roots): + for scan_root in scan_roots: + if scan_root.is_file(): + yield scan_root + continue + for path in scan_root.rglob("*"): + if path.suffix in {".java", ".kt"} and path.is_file() and not SKIP_PARTS.intersection(path.parts): + yield path + + +def migrate_text(path: Path, text: str) -> tuple[str, list[str]]: + changes = [] + for old, new in TEXT_RENAMES: + if old in text: + count = text.count(old) + text = text.replace(old, new) + changes.append(f"{old} -> {new} ({count})") + + if CONFIG_EXTRACTOR_IMPORT in text: + text = text.replace(CONFIG_EXTRACTOR_IMPORT, CONFIG_VALUE_MAPPER_IMPORT) + text, generics = re.subn(r"\bConfigMapper<", "ConfigValueMapper<", text) + text, calls = re.subn(r"\.extract\(", ".mapOrThrow(", text) + changes.append(f"ConfigMapper -> ConfigValueMapper ({generics}), extract -> mapOrThrow ({calls})") + + text, count = FALLBACK_VALUE.subn("@Fallback(", text) + if count: + changes.append(f"@Fallback value attribute removed ({count})") + + if path.suffix == ".java": + text, count = QUALIFIED_TYPE_ANNOTATION.subn(r"\g<2>@\g<1> \g<3>", text) + if count: + changes.append(f"type-use annotation moved onto nested type ({count})") + + return text, changes + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--apply", action="store_true", help="write changes; default is dry-run") + parser.add_argument("paths", nargs="*", type=Path, help="files or directories to scan") + args = parser.parse_args() + + changed = 0 + for path in source_files(args.paths or list(DEFAULT_SCAN)): + original = path.read_text(encoding="utf-8") + updated, changes = migrate_text(path, original) + if updated == original: + continue + changed += 1 + print(f"CHANGE {path.relative_to(ROOT)}: {', '.join(changes)}") + if args.apply: + path.write_text(updated, encoding="utf-8") + + print(f"{'APPLIED' if args.apply else 'DRY-RUN'}: {changed} file(s)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/migration/scripts/migrate_config_keys.py b/migration/scripts/migrate_config_keys.py new file mode 100644 index 00000000..5b56851f --- /dev/null +++ b/migration/scripts/migrate_config_keys.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +"""Kora 1.x -> 2.0: migrate configuration keys and section names. + +HTTP server +----------- +In Kora 1.x the public and the private (system) HTTP servers were configured by flat keys +inside the `httpServer` section. In Kora 2.0 the system server has its own nested section +`httpServer.system`, and the public server uses plain `port`. + + publicApiHttpPort -> port + privateApiHttpPort -> system.port + privateApiHttpReadinessPath -> system.readinessPath + privateApiHttpLivenessPath -> system.livenessPath + privateApiHttpMetricsPath -> system.metricsPath + +Why this matters beyond cosmetics: `SystemHttpServerConfig extends HttpServerConfig`, so the +system server inherits `port()` = 8080. While `privateApiHttpPort` stays unrecognised, BOTH +servers bind to 8080 and the application dies on startup with + + HTTP server 'kora-undertow-system' (Undertow) failed to start on port '8080': port is already in use + +JDBC section +------------ +The JDBC datasource section was renamed: `db` -> `jdbc`. In 2.0 `JdbcDatabaseModule` wires +`new JdbcDatabaseFactoryModule("jdbc")`, so a config still using `db` leaves every value unset and +the application fails on startup (or in tests) with + + ConfigValueException: Config expected value, but got null at path: 'ROOT.jdbc.username' + +Only a top-level `db` block is renamed, and only in files that actually contain `jdbcUrl`, so +unrelated `db` sections of other integrations are left alone. + +Dry-run by default; pass --apply to write. Idempotent: running it twice changes nothing. + +Usage: + python migration/scripts/migrate_config_keys.py [--apply] [PATH ...] + +PATH defaults to the `examples` and `guides` directories of this repository. + +Limitations: + - Only HOCON/YAML/properties-like `key = value` and `key: value` forms are handled. + - A key is rewritten only when it is not already nested under `system`; the script does not + reformat or reorder the section, it only renames keys in place. + - `dataApiHttpPort` and other non-Kora keys are left untouched and reported, because their + intent is application-specific. + - The `db` -> `jdbc` rule keys off a top-level `db {` / `db:` line; a JDBC section nested + somewhere else has to be migrated by hand. +""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +DEFAULT_SCAN = (ROOT / "examples", ROOT / "guides") +SUFFIXES = {".conf", ".yaml", ".yml", ".properties"} +SOURCE_SUFFIXES = {".java", ".kt"} +SKIP_PARTS = {"build", ".gradle", ".git", "agents-md"} + +# Tests configure the graph with HOCON embedded in a text block: +# KoraConfigModification.ofString(""" +# jdbc { ... } +# """) +# Those keys need the same migration as the .conf files, but the rewrite has to stay inside the +# text block - applying config rules to arbitrary Java or Kotlin would corrupt real code. +EMBEDDED_CONFIG = re.compile(r'(ofString\(\s*""")(.*?)(""")', re.S) + +RENAMES = ( + ("publicApiHttpPort", "port"), + ("privateApiHttpPort", "system.port"), + ("privateApiHttpReadinessPath", "system.readinessPath"), + ("privateApiHttpLivenessPath", "system.livenessPath"), + ("privateApiHttpMetricsPath", "system.metricsPath"), +) + +UNKNOWN_KEYS = ("dataApiHttpPort",) + + +def config_files(scan_roots): + for scan_root in scan_roots: + if scan_root.is_file(): + yield scan_root + continue + for path in scan_root.rglob("*"): + if not path.is_file() or SKIP_PARTS.intersection(path.parts): + continue + if path.suffix in SUFFIXES or path.suffix in SOURCE_SUFFIXES: + yield path + + +def migrate_text(text: str) -> tuple[str, list[str]]: + changes = [] + for old, new in RENAMES: + # `key = value` / `key: value`, keeping the original indentation and separator + pattern = re.compile(rf"^(\s*){re.escape(old)}(\s*[:=])", re.M) + text, count = pattern.subn(rf"\g<1>{new}\g<2>", text) + if count: + changes.append(f"{old} -> {new} ({count})") + + # A 1.x circuit breaker had one count-based sliding window; 2.0 has several implementations, + # and the window moved into the `countBased` block. The default implementation + # (STRIPED_APPROX) dereferences `countBased()` without a null check, so the block is + # effectively required once a circuit breaker is in use. + pattern = re.compile(r"^(\s*)slidingWindowSize(\s*[:=]\s*)(\S+)\s*$", re.M) + text, count = pattern.subn( + lambda m: f"{m.group(1)}type = FIXED_WINDOW\n{m.group(1)}countBased.windowSize{m.group(2)}{m.group(3)}", + text) + if count: + changes.append(f"slidingWindowSize -> type + countBased.windowSize ({count})") + + # OpenApiManagementConfig#files is a required list in 2.0; the 1.x key was `file` and could be + # a single string. HOCON accepts a scalar where a list is expected, so only the name changes. + text, count = re.subn(r"^(\s*)file(\s*[:=])", r"\g<1>files\g<2>", text, flags=re.M) \ + if "openapi" in text and "management" in text else (text, 0) + if count: + changes.append(f"openapi.management.file -> files ({count})") + + # RapiDoc is gone in 2.0; the second viewer is Scalar. The key is silently ignored otherwise, + # which hides the fact that the endpoint no longer exists. + if "rapidoc" in text: + text, count = re.subn(r"^(\s*)rapidoc(\s*[.{:])", r"\g<1>scalar\g<2>", text, flags=re.M) + if count: + changes.append(f"rapidoc -> scalar ({count})") + + # `db` section is JDBC only when it configures a datasource + if "jdbcUrl" in text: + # leading whitespace is allowed because the same config appears indented inside test text blocks + text, count = re.subn(r"^(\s*)db(\s*[{:])", r"\g<1>jdbc\g<2>", text, flags=re.M) + if count: + changes.append(f"db -> jdbc section ({count})") + + return text, changes + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--apply", action="store_true", help="write changes; default is dry-run") + parser.add_argument("paths", nargs="*", type=Path, help="files or directories to scan") + args = parser.parse_args() + + scan_roots = [path.resolve() for path in (args.paths or list(DEFAULT_SCAN))] + + changed_files = 0 + warned = [] + for path in config_files(scan_roots): + original = path.read_text(encoding="utf-8") + + if path.suffix in SOURCE_SUFFIXES: + changes = [] + + def migrate_block(match): + migrated, block_changes = migrate_text(match.group(2)) + changes.extend(block_changes) + return match.group(1) + migrated + match.group(3) + + updated = EMBEDDED_CONFIG.sub(migrate_block, original) + else: + updated, changes = migrate_text(original) + + for unknown in UNKNOWN_KEYS: + if re.search(rf"^\s*{re.escape(unknown)}\s*[:=]", original, re.M): + warned.append(f"{path.relative_to(ROOT)}: {unknown} is not a Kora 2.0 key, left untouched") + + if updated == original: + continue + changed_files += 1 + print(f"CHANGE {path.relative_to(ROOT)}: {', '.join(changes)}") + if args.apply: + path.write_text(updated, encoding="utf-8") + + for warning in warned: + print(f"WARN {warning}", file=sys.stderr) + + mode = "APPLIED" if args.apply else "DRY-RUN" + print(f"{mode}: {changed_files} file(s)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/migration/scripts/migrate_kora_2.py b/migration/scripts/migrate_kora_2.py index d3818723..aab95084 100644 --- a/migration/scripts/migrate_kora_2.py +++ b/migration/scripts/migrate_kora_2.py @@ -9,6 +9,7 @@ import argparse import os +import re from pathlib import Path @@ -19,6 +20,7 @@ ".yml", ".json", ".xml", ".proto", } SKIP_PARTS = {"build", ".gradle", ".git"} +LEGACY_APT_TOKEN = "ka" + "pt" REPLACEMENTS = ( ("1.9.25-1.0.20", "2.3.11"), @@ -28,9 +30,9 @@ ("import com.google.devtools.ksp.gradle.KspTask\n", ""), ('tasks.withType().configureEach {', 'tasks.matching { it.name.startsWith("ksp") }.configureEach {'), ('id("org.jetbrains.kotlin.jvm") version "1.9.25"', 'id("org.jetbrains.kotlin.jvm") version "2.4.10"'), - ('id("org.jetbrains.kotlin.kapt") version "1.9.25"', 'id("org.jetbrains.kotlin.kapt") version "2.4.10"'), ('id("com.google.devtools.ksp") version "1.9.25-1.0.20"', 'id("com.google.devtools.ksp") version "2.3.11"'), ("ru.tinkoff.kora", "io.koraframework"), + ("io.koraframework:kora-parent", "io.koraframework:kora-bom"), ("io.koraframework.common.KoraApp", "io.koraframework.common.annotation.KoraApp"), ("io.koraframework.common.Component", "io.koraframework.common.annotation.Component"), ("io.koraframework.common.DefaultComponent", "io.koraframework.common.annotation.DefaultComponent"), @@ -84,7 +86,6 @@ ROOT_REPLACEMENTS = ( ('id "org.jetbrains.kotlin.jvm" version "1.9.25"', 'id "org.jetbrains.kotlin.jvm" version "2.4.10"'), - ('id "org.jetbrains.kotlin.kapt" version "1.9.25"', 'id "org.jetbrains.kotlin.kapt" version "2.4.10"'), ('id "com.google.devtools.ksp" version "1.9.25-1.0.20"', 'id "com.google.devtools.ksp" version "2.3.11"'), ) @@ -101,6 +102,11 @@ def replace_text(path: Path, replacements, apply: bool) -> bool: updated = original for old, new in replacements: updated = updated.replace(old, new) + updated = update_junit_version(updated) + updated = update_mockk_version(updated) + if path.name in {"build.gradle", "build.gradle.kts"}: + updated = remove_empty_environment_block(updated) + updated = "".join(line for line in updated.splitlines(keepends=True) if LEGACY_APT_TOKEN not in line.lower()) if "@CacheInvalidateAll" in updated and "cache.annotation.CacheInvalidateAll" not in updated: updated = updated.replace( "import io.koraframework.cache.annotation.CacheInvalidate;", @@ -111,6 +117,12 @@ def replace_text(path: Path, replacements, apply: bool) -> bool: "import io.koraframework.cache.annotation.CacheInvalidate\nimport io.koraframework.cache.annotation.CacheInvalidateAll\n", ) if path.suffix == ".kt": + if "@HttpClient" in updated: + updated = re.sub(r"(?m)^([ \t]*)suspend fun ", r"\1fun ", updated) + if "@Repository" in updated: + updated = re.sub(r"(?m)^([ \t]*)suspend fun ", r"\1fun ", updated) + if "@HttpController" in updated or "@HttpRoute" in updated: + updated = re.sub(r"(?m)^([ \t]*)suspend fun ", r"\1fun ", updated) updated = updated.replace("import org.jspecify.annotations.Nullable\n", "") updated = updated.replace("import org.jspecify.annotations.NonNull\n", "") updated = updated.replace("@field:Nullable ", "") @@ -124,6 +136,8 @@ def replace_text(path: Path, replacements, apply: bool) -> bool: " class UserContextMapping", " @Component\n class UserContextMapping", ) + if path.name.endswith(".gradle.kts"): + updated = normalize_kotlin_dependencies(path, updated) if "@Mapping(UserContextMapping" in updated and "@Component\n public static final class UserContextMapping" not in updated: updated = updated.replace( " public static final class UserContextMapping", @@ -137,6 +151,85 @@ def replace_text(path: Path, replacements, apply: bool) -> bool: return True +def update_junit_version(text: str) -> str: + """Update any literal JUnit version to the migration target, preserving Gradle syntax.""" + text = re.sub( + r"(?m)^(\s*junitVersion\s*=\s*)[0-9][0-9A-Za-z.+_-]*(\s*(?:#.*)?)$", + r"\g<1>6.1.3\g<2>", + text, + ) + return re.sub( + r"(org\.junit:junit-bom:)[0-9][0-9A-Za-z.+_-]*", + r"\g<1>6.1.3", + text, + ) + + +def update_mockk_version(text: str) -> str: + """Update explicitly pinned MockK to a Java 25-compatible release.""" + return re.sub( + r"(io\.mockk:mockk:)[0-9][0-9A-Za-z.+_-]*", + r"\g<1>1.14.9", + text, + ) + + +def remove_empty_environment_block(text: str) -> str: + """Remove invalid Gradle test environment entries with an empty variable name.""" + return re.sub( + r'(?ms)\r?\n[ \t]*environment\(\[\s*\r?\n[ \t]*"": "",?\s*\r?\n[ \t]*\]\)\s*\r?\n', + "\n", + text, + ) + + +def normalize_kotlin_dependencies(path: Path, text: str) -> str: + """Use direct versioned KSP and implementation-platform dependencies in Kotlin builds.""" + text = re.sub( + r"(?ms)^[ \t]*val koraBom: Configuration by configurations\.creating\r?\n" + r"[ \t]*configurations \{\r?\n.*?extendsFrom\(koraBom\).*?^[ \t]*\}\r?\n(?:\r?\n)?", + "", + text, + ) + text = text.replace( + 'listOf("ksp", "implementation", "kspTest").forEach { name ->\n' + ' named(name) { extendsFrom(koraBom) }\n' + ' }', + "", + ) + dependency_lines = [] + for line in text.splitlines(keepends=True): + if line.lstrip().startswith('koraBom(platform("io.koraframework:kora-bom:'): + indent = line[:len(line) - len(line.lstrip())] + newline = "\r\n" if line.endswith("\r\n") else "\n" + if path == ROOT / "examples/kotlin/kora-kotlin-crud-submodule/build.gradle.kts": + line = f'{indent}add("implementation", platform("io.koraframework:kora-bom:${{property("koraVersion")}}")){newline}' + else: + line = f'{indent}implementation(platform("io.koraframework:kora-bom:${{property("koraVersion")}}")){newline}' + dependency_lines.append(line) + text = "".join(dependency_lines) + text = text.replace( + 'ksp("io.koraframework:symbol-processors")', + 'ksp("io.koraframework:symbol-processors:${property("koraVersion")}")', + ) + text = text.replace( + 'add("ksp", "io.koraframework:symbol-processors")', + 'add("ksp", "io.koraframework:symbol-processors:${property("koraVersion")}")', + ) + lines = [] + for line in text.splitlines(keepends=True): + lowered = line.lower() + if LEGACY_APT_TOKEN in lowered: + continue + lines.append(line) + text = "".join(lines) + text = text.replace( + 'kspTest("io.koraframework:symbol-processors")', + 'kspTest("io.koraframework:symbol-processors:${property("koraVersion")}")', + ) + return text + + def package_moves(apply: bool) -> int: moves = [] for scan_root in SCAN_ROOTS: @@ -173,6 +266,7 @@ def main() -> int: changed = sum(replace_text(path, REPLACEMENTS, args.apply) for path in text_files()) changed += replace_text(ROOT / "build.gradle", ROOT_REPLACEMENTS, args.apply) + changed += replace_text(ROOT / "gradle.properties", (), args.apply) moved = package_moves(args.apply) mode = "APPLIED" if args.apply else "DRY-RUN" print(f"{mode}: {changed} text files, {moved} package trees") diff --git a/migration/scripts/migrate_resilient_specs.py b/migration/scripts/migrate_resilient_specs.py new file mode 100644 index 00000000..2271557c --- /dev/null +++ b/migration/scripts/migrate_resilient_specs.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Kora 1.x -> 2.0: turn string-named resilient annotations into typed specifications. + +In Kora 1.x a resilient aspect was selected by a name that also served as the configuration +path suffix: + + @Timeout("pet") + @CircuitBreakable("pet") + Pet findById(long id); + +In Kora 2.0 the aspect annotation takes a *type*, and that type is an interface which declares +where its configuration lives: + + @TimeoutSpec("resilient.timeout.pet") + public interface PetTimeouter extends Timeouter {} + + @Timeout(PetTimeouter.class) + @CircuitBreakable(PetCircuitBreaker.class) + Pet findById(long id); + +Without the change the code does not compile: + + error: incompatible types: String cannot be converted to Class + +What this script does +--------------------- +For every `@Timeout("x")`, `@Retryable("x")`, `@CircuitBreakable("x")` and `@RateLimited("x")` +found in Java or Kotlin sources it + + 1. generates the specification interface next to the annotated file, in the same package, + pointing at the configuration path `resilient..` — which is exactly where the + configuration already lives, so no configuration edits are needed; + 2. rewrites the annotation argument to reference that interface. + +Naming follows the reference module `examples/java/kora-java-crud`: `pet` becomes `PetTimeouter`, +`PetCircuitBreaker`, `PetRetry`, `PetRateLimiter`. + +Dry-run by default; pass --apply to write. Idempotent: an annotation that already takes a type +is left alone, and an existing specification file is never overwritten. + +Usage: + python migration/scripts/migrate_resilient_specs.py [--apply] [PATH ...] + +PATH defaults to the `examples` and `guides` directories of this repository. + +Limitations: + - `@Fallback` is not handled: Kora 2.0 has no `FallbackSpec`, so that annotation needs a + decision per call site rather than a mechanical rewrite. + - The configuration *shape* of a circuit breaker also changed in 2.0 (`slidingWindowSize` + became `type` + `countBased.windowSize`); this script only touches code, never configuration. + - The package is taken from the `package` statement of the annotated file. A file without one + (default package) is reported and skipped. +""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +DEFAULT_SCAN = (ROOT / "examples", ROOT / "guides") +SKIP_PARTS = {"build", ".gradle", ".git", "agents-md"} + +# 2.0 annotation -> (1.x annotation name, suffix of the generated interface, base type, +# spec annotation, config section) +# `@CircuitBreaker` and `@Retry` were renamed in 2.0; `@Timeout` kept its name. +KINDS = { + "Timeout": ("Timeout", "Timeouter", "io.koraframework.resilient.timeout.Timeouter", + "io.koraframework.resilient.timeout.annotation.TimeoutSpec", "timeout"), + "Retryable": ("Retry", "Retry", "io.koraframework.resilient.retry.Retry", + "io.koraframework.resilient.retry.annotation.RetrySpec", "retry"), + "CircuitBreakable": ("CircuitBreaker", "CircuitBreaker", "io.koraframework.resilient.circuitbreaker.CircuitBreaker", + "io.koraframework.resilient.circuitbreaker.annotation.CircuitBreakerSpec", "circuitbreaker"), + "RateLimited": ("RateLimited", "RateLimiter", "io.koraframework.resilient.ratelimiter.RateLimiter", + "io.koraframework.resilient.ratelimiter.annotation.RateLimiterSpec", "ratelimiter"), +} + +PACKAGE = re.compile(r"^\s*package\s+([\w.]+)", re.M) + +JAVA_TEMPLATE = """package {package}; + +import {base}; +import {spec}; + +@{spec_simple}("resilient.{section}.{name}") +public interface {interface} extends {base_simple} {{ + +}} +""" + +KOTLIN_TEMPLATE = """package {package} + +import {base} +import {spec} + +@{spec_simple}("resilient.{section}.{name}") +interface {interface} : {base_simple} +""" + + +def source_files(scan_roots): + for scan_root in scan_roots: + if scan_root.is_file(): + yield scan_root + continue + for path in scan_root.rglob("*"): + if path.suffix in {".java", ".kt"} and path.is_file() and not SKIP_PARTS.intersection(path.parts): + yield path + + +def interface_name(name: str, suffix: str) -> str: + # `pet` -> `PetTimeouter`, `pet-api` -> `PetApiTimeouter` + parts = re.split(r"[^A-Za-z0-9]+", name) + return "".join(p[:1].upper() + p[1:] for p in parts if p) + suffix + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--apply", action="store_true", help="write changes; default is dry-run") + parser.add_argument("paths", nargs="*", type=Path, help="files or directories to scan") + args = parser.parse_args() + + # resolved so that paths given relative to the working directory stay under ROOT when reported + scan_roots = [path.resolve() for path in (args.paths or list(DEFAULT_SCAN))] + + changed_files = 0 + created_files = 0 + warnings = [] + + for path in source_files(scan_roots): + original = path.read_text(encoding="utf-8") + text = original + kotlin = path.suffix == ".kt" + specs = {} + + for annotation, (legacy, suffix, base, spec, section) in KINDS.items(): + pattern = re.compile(rf"@{legacy}\(\s*\"([^\"]+)\"\s*\)") + for match in pattern.finditer(text): + name = match.group(1) + specs[interface_name(name, suffix)] = (name, base, spec, section) + if not pattern.search(text): + continue + reference = "::class" if kotlin else ".class" + text = pattern.sub( + lambda m: f"@{annotation}({interface_name(m.group(1), suffix)}{reference})", text) + if legacy != annotation: + # the annotation itself was renamed, so its import has to follow + legacy_import = spec.rsplit(".", 1)[0] + "." + legacy + text = text.replace(legacy_import, spec.rsplit(".", 1)[0] + "." + annotation) + + if not specs: + continue + + # some sources carry a BOM, which `\s` does not match + package_match = PACKAGE.search(original.lstrip("")) + if package_match is None: + warnings.append(f"{path.relative_to(ROOT)}: no package statement, skipped") + continue + package = package_match.group(1) + + if text != original: + changed_files += 1 + print(f"CHANGE {path.relative_to(ROOT)}: {', '.join(sorted(specs))}") + if args.apply: + path.write_text(text, encoding="utf-8") + + for interface, (name, base, spec, section) in specs.items(): + target = path.parent / f"{interface}{path.suffix}" + if target.exists(): + continue + template = KOTLIN_TEMPLATE if kotlin else JAVA_TEMPLATE + content = template.format( + package=package, + base=base, + spec=spec, + spec_simple=spec.rsplit(".", 1)[1], + base_simple=base.rsplit(".", 1)[1], + section=section, + name=name, + interface=interface, + ) + created_files += 1 + print(f"CREATE {target.relative_to(ROOT)}") + if args.apply: + target.write_text(content, encoding="utf-8") + + for warning in warnings: + print(f"WARN {warning}", file=sys.stderr) + + mode = "APPLIED" if args.apply else "DRY-RUN" + print(f"{mode}: {changed_files} file(s) changed, {created_files} specification(s) created") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/settings.gradle b/settings.gradle index cfcd7c79..59b0ec78 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,8 +22,6 @@ def javaExamples = [ "kora-java-openapi-generator-http-server", "kora-java-openapi-generator-http-client", "kora-java-database-jdbc", -// "kora-java-database-r2dbc", // Removed in Kora 2.0 -// "kora-java-database-vertx", // Removed in Kora 2.0 "kora-java-database-cassandra", "kora-java-telemetry", "kora-java-validation", @@ -38,7 +36,7 @@ def javaExamples = [ "kora-java-config-hocon", "kora-java-config-yaml", "kora-java-s3-client-aws", - "kora-java-s3-client-minio", + "kora-java-s3-client-kora", "kora-java-camunda-engine", "kora-java-camunda-zeebe-worker", "kora-java-helloworld", @@ -58,8 +56,6 @@ include "examples:java:kora-java-crud-submodule:kora-java-crud-submodule-vet-api def graalvmExamples = [ "kora-java-graalvm-crud-jdbc", -// "kora-java-graalvm-crud-r2dbc", // Removed in Kora 2.0 -// "kora-java-graalvm-crud-vertx", // Removed in Kora 2.0 "kora-java-graalvm-crud-cassandra", "kora-java-graalvm-kafka", ] @@ -86,7 +82,7 @@ def kotlinExamples = [ "kora-kotlin-http-server", "kora-kotlin-openapi-generator-http-client", "kora-kotlin-openapi-generator-http-server", - "kora-kotlin-s3-client-minio", + "kora-kotlin-s3-client-kora", "kora-kotlin-s3-client-aws", "kora-kotlin-soap-client", "kora-kotlin-grpc-client", @@ -95,8 +91,6 @@ def kotlinExamples = [ "kora-kotlin-camunda-engine", "kora-kotlin-camunda-zeebe-worker", "kora-kotlin-database-jdbc", -// "kora-kotlin-database-r2dbc", // Removed in Kora 2.0 -// "kora-kotlin-database-vertx", // Removed in Kora 2.0 "kora-kotlin-database-cassandra", ]