From 29c169f5dede2aa8eac6e4450e11004395caa13a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Kobyli=C5=84ski?= Date: Fri, 17 Jul 2026 11:00:52 +0200 Subject: [PATCH 1/3] fix: add @JvmOverloads to OutboxProcessor constructor (#74) The defaulted listener and clock parameters were invisible to Java, so Java callers could no longer use the 2-arg form new OutboxProcessor(store, entryProcessor). Restore the Java-visible overloads and guard them with a reflection-based test (okapi-core had no Java interop tests, which is why this slipped). --- .../okapi/core/OutboxProcessor.kt | 2 +- .../core/OutboxProcessorJavaInteropTest.kt | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 okapi-core/src/test/kotlin/com/softwaremill/okapi/core/OutboxProcessorJavaInteropTest.kt diff --git a/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxProcessor.kt b/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxProcessor.kt index 84cc82d..6006e3c 100644 --- a/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxProcessor.kt +++ b/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxProcessor.kt @@ -18,7 +18,7 @@ import java.time.Duration * fire-flush-await — making per-entry timing meaningless). Use * [OutboxProcessorListener.onBatchProcessed] when you need batch-level timing. */ -class OutboxProcessor( +class OutboxProcessor @JvmOverloads constructor( private val store: OutboxStore, private val entryProcessor: OutboxEntryProcessor, private val listener: OutboxProcessorListener? = null, diff --git a/okapi-core/src/test/kotlin/com/softwaremill/okapi/core/OutboxProcessorJavaInteropTest.kt b/okapi-core/src/test/kotlin/com/softwaremill/okapi/core/OutboxProcessorJavaInteropTest.kt new file mode 100644 index 0000000..17ea3a5 --- /dev/null +++ b/okapi-core/src/test/kotlin/com/softwaremill/okapi/core/OutboxProcessorJavaInteropTest.kt @@ -0,0 +1,33 @@ +package com.softwaremill.okapi.core + +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.core.spec.style.FunSpec + +/** + * Guards the `@JvmOverloads` on [OutboxProcessor]'s constructor. Kotlin default + * parameters are invisible to Java, so without `@JvmOverloads` a Java caller is + * forced to pass `listener` and `clock` explicitly. These reflection checks fail + * if the annotation is dropped — Kotlin call sites would keep compiling and hide + * the regression otherwise. + */ +class OutboxProcessorJavaInteropTest : + FunSpec({ + test("exposes a (store, entryProcessor) constructor to Java") { + shouldNotThrowAny { + OutboxProcessor::class.java.getConstructor( + OutboxStore::class.java, + OutboxEntryProcessor::class.java, + ) + } + } + + test("exposes a (store, entryProcessor, listener) constructor to Java") { + shouldNotThrowAny { + OutboxProcessor::class.java.getConstructor( + OutboxStore::class.java, + OutboxEntryProcessor::class.java, + OutboxProcessorListener::class.java, + ) + } + } + }) From bdcaf5ceb67ff6796dd4a3eb06b84d89d2a10a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Kobyli=C5=84ski?= Date: Fri, 17 Jul 2026 11:42:33 +0200 Subject: [PATCH 2/3] test: guard OutboxProcessor Java interop with a real Java source (#74) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the reflection-based check with a Java source (JavaOutboxProcessor- Construction) that calls the shorter constructors, driven by the Kotest test. This tests the actual contract — Java source compiling — instead of a runtime proxy: dropping @JvmOverloads now fails compileTestJava (verified: 'constructor OutboxProcessor cannot be applied ... found: (store, entryProcessor)'). It's the idiomatic way to guard @Jvm* interop and doubles as Java usage documentation. --- .../core/JavaOutboxProcessorConstruction.java | 26 +++++++++ .../core/OutboxProcessorJavaInteropTest.kt | 55 ++++++++++++------- 2 files changed, 60 insertions(+), 21 deletions(-) create mode 100644 okapi-core/src/test/java/com/softwaremill/okapi/core/JavaOutboxProcessorConstruction.java diff --git a/okapi-core/src/test/java/com/softwaremill/okapi/core/JavaOutboxProcessorConstruction.java b/okapi-core/src/test/java/com/softwaremill/okapi/core/JavaOutboxProcessorConstruction.java new file mode 100644 index 0000000..17234de --- /dev/null +++ b/okapi-core/src/test/java/com/softwaremill/okapi/core/JavaOutboxProcessorConstruction.java @@ -0,0 +1,26 @@ +package com.softwaremill.okapi.core; + +/** + * Java-side guard for {@code @JvmOverloads} on {@link OutboxProcessor}'s constructor. + * + *

These call sites compile only if {@code OutboxProcessor} exposes the shorter, + * default-omitting constructors to Java. Drop {@code @JvmOverloads} and okapi-core's test + * compilation fails right here — which is the point: a Kotlin-only test keeps compiling + * (Kotlin fills defaults at the call site) and would hide the regression. {@code + * OutboxProcessorJavaInteropTest} calls these so the guard also runs. + */ +public final class JavaOutboxProcessorConstruction { + + private JavaOutboxProcessorConstruction() {} + + /** {@code OutboxProcessor(store, entryProcessor)} — listener and clock default. */ + public static OutboxProcessor withStoreAndProcessor(OutboxStore store, OutboxEntryProcessor entryProcessor) { + return new OutboxProcessor(store, entryProcessor); + } + + /** {@code OutboxProcessor(store, entryProcessor, listener)} — clock defaults. */ + public static OutboxProcessor withListener( + OutboxStore store, OutboxEntryProcessor entryProcessor, OutboxProcessorListener listener) { + return new OutboxProcessor(store, entryProcessor, listener); + } +} diff --git a/okapi-core/src/test/kotlin/com/softwaremill/okapi/core/OutboxProcessorJavaInteropTest.kt b/okapi-core/src/test/kotlin/com/softwaremill/okapi/core/OutboxProcessorJavaInteropTest.kt index 17ea3a5..e2851df 100644 --- a/okapi-core/src/test/kotlin/com/softwaremill/okapi/core/OutboxProcessorJavaInteropTest.kt +++ b/okapi-core/src/test/kotlin/com/softwaremill/okapi/core/OutboxProcessorJavaInteropTest.kt @@ -1,33 +1,46 @@ package com.softwaremill.okapi.core -import io.kotest.assertions.throwables.shouldNotThrowAny import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.nulls.shouldNotBeNull +import java.time.Clock +import java.time.Instant /** - * Guards the `@JvmOverloads` on [OutboxProcessor]'s constructor. Kotlin default - * parameters are invisible to Java, so without `@JvmOverloads` a Java caller is - * forced to pass `listener` and `clock` explicitly. These reflection checks fail - * if the annotation is dropped — Kotlin call sites would keep compiling and hide - * the regression otherwise. + * Runs the Java call sites in [JavaOutboxProcessorConstruction], which compile only when + * [OutboxProcessor]'s constructor carries `@JvmOverloads`. The real guard is that Java source + * compiling — okapi-core has no other Java interop test, which is why the missing annotation + * regressed unnoticed. The stubs are inert: the constructor only stores its arguments. */ class OutboxProcessorJavaInteropTest : FunSpec({ - test("exposes a (store, entryProcessor) constructor to Java") { - shouldNotThrowAny { - OutboxProcessor::class.java.getConstructor( - OutboxStore::class.java, - OutboxEntryProcessor::class.java, - ) - } - } + val store = + object : OutboxStore { + override fun persist(entry: OutboxEntry) = entry + + override fun claimPending(limit: Int) = emptyList() + + override fun updateAfterProcessing(entry: OutboxEntry) = entry - test("exposes a (store, entryProcessor, listener) constructor to Java") { - shouldNotThrowAny { - OutboxProcessor::class.java.getConstructor( - OutboxStore::class.java, - OutboxEntryProcessor::class.java, - OutboxProcessorListener::class.java, - ) + override fun removeDeliveredBefore(time: Instant, limit: Int) = 0 + + override fun findOldestCreatedAt(statuses: Set) = emptyMap() + + override fun countByStatuses() = emptyMap() } + val entryProcessor = + OutboxEntryProcessor( + deliverer = + object : MessageDeliverer { + override val type = "stub" + + override fun deliver(entry: OutboxEntry) = DeliveryResult.Success + }, + retryPolicy = RetryPolicy(3), + clock = Clock.systemUTC(), + ) + + test("Java can construct OutboxProcessor with trailing defaults omitted (@JvmOverloads)") { + JavaOutboxProcessorConstruction.withStoreAndProcessor(store, entryProcessor).shouldNotBeNull() + JavaOutboxProcessorConstruction.withListener(store, entryProcessor, null).shouldNotBeNull() } }) From c719de6761d8b23dfb1fe02d6b674b087e5d5d97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Wokacz?= Date: Fri, 17 Jul 2026 13:11:44 +0200 Subject: [PATCH 3/3] align with code format convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Rafał Wokacz --- .../softwaremill/okapi/core/OutboxProcessorJavaInteropTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/okapi-core/src/test/kotlin/com/softwaremill/okapi/core/OutboxProcessorJavaInteropTest.kt b/okapi-core/src/test/kotlin/com/softwaremill/okapi/core/OutboxProcessorJavaInteropTest.kt index e2851df..64649b7 100644 --- a/okapi-core/src/test/kotlin/com/softwaremill/okapi/core/OutboxProcessorJavaInteropTest.kt +++ b/okapi-core/src/test/kotlin/com/softwaremill/okapi/core/OutboxProcessorJavaInteropTest.kt @@ -35,7 +35,7 @@ class OutboxProcessorJavaInteropTest : override fun deliver(entry: OutboxEntry) = DeliveryResult.Success }, - retryPolicy = RetryPolicy(3), + retryPolicy = RetryPolicy(maxRetries = 3), clock = Clock.systemUTC(), )