From b7d8b121f56af5706f50c9aa5b6de082329a0e3b Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Sat, 20 Jun 2026 16:26:23 +0300 Subject: [PATCH 1/6] feat: add copy-on-write derivation to Configuration Add Configuration.newBuilder() and Configuration.withOptions(Consumer) so a caller can derive a reconfigured copy of an existing Configuration without mutating the original. The override map is copied defensively while the env/property lookup seams are shared by reference, so a derived instance never aliases the receiver's mutable state. withOptions covers the common derive-in-one-call case; newBuilder exposes the prefilled builder for code that threads it through other builder-folding steps before build(). ConfigurationBuilder now implements Builder and gains a prefilled constructor, bringing it in line with the newBuilder() pattern the other SDK builders (Request, Response, Headers, RequestConditions, ...) already follow. --- sdk-core/api/sdk-core.api | 8 +- .../dexpace/sdk/core/config/Configuration.kt | 51 ++++++++++++ .../sdk/core/config/ConfigurationBuilder.kt | 23 +++++- .../sdk/core/config/ConfigurationTest.kt | 81 +++++++++++++++++++ 4 files changed, 159 insertions(+), 4 deletions(-) diff --git a/sdk-core/api/sdk-core.api b/sdk-core/api/sdk-core.api index 7ebcc312..a0bb0647 100644 --- a/sdk-core/api/sdk-core.api +++ b/sdk-core/api/sdk-core.api @@ -36,7 +36,9 @@ public final class org/dexpace/sdk/core/config/Configuration { public static final fun getGlobalConfiguration ()Lorg/dexpace/sdk/core/config/Configuration; public final fun getInt (Ljava/lang/String;I)I public final fun getProperty (Ljava/lang/String;)Ljava/lang/String; + public final fun newBuilder ()Lorg/dexpace/sdk/core/config/ConfigurationBuilder; public static final fun setGlobalConfiguration (Lorg/dexpace/sdk/core/config/Configuration;)V + public final fun withOptions (Ljava/util/function/Consumer;)Lorg/dexpace/sdk/core/config/Configuration; } public final class org/dexpace/sdk/core/config/Configuration$Companion { @@ -44,9 +46,11 @@ public final class org/dexpace/sdk/core/config/Configuration$Companion { public final fun setGlobalConfiguration (Lorg/dexpace/sdk/core/config/Configuration;)V } -public final class org/dexpace/sdk/core/config/ConfigurationBuilder { +public final class org/dexpace/sdk/core/config/ConfigurationBuilder : org/dexpace/sdk/core/generics/Builder { public fun ()V - public final fun build ()Lorg/dexpace/sdk/core/config/Configuration; + public fun (Lorg/dexpace/sdk/core/config/Configuration;)V + public synthetic fun build ()Ljava/lang/Object; + public fun build ()Lorg/dexpace/sdk/core/config/Configuration; public final fun envSource (Ljava/util/function/Function;)Lorg/dexpace/sdk/core/config/ConfigurationBuilder; public final fun propsSource (Ljava/util/function/Function;)Lorg/dexpace/sdk/core/config/ConfigurationBuilder; public final fun put (Ljava/lang/String;Ljava/lang/String;)Lorg/dexpace/sdk/core/config/ConfigurationBuilder; diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt index 321de182..9c52b2c8 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt @@ -9,6 +9,7 @@ package org.dexpace.sdk.core.config import java.time.Duration import java.util.Locale +import java.util.function.Consumer import java.util.function.Function /** @@ -23,6 +24,23 @@ import java.util.function.Function * * Constructed via [ConfigurationBuilder]. * + * ## Deriving a reconfigured copy (copy-on-write) + * [withOptions] returns a **new** immutable [Configuration] with a mutator applied on top of this + * one, leaving the receiver untouched: + * + * ```java + * Configuration base = new ConfigurationBuilder().put("MAX_RETRY_ATTEMPTS", "3").build(); + * Configuration derived = base.withOptions(b -> b.put("LOG_LEVEL", "DEBUG")); + * // base is unchanged; derived carries both overrides. + * ``` + * + * The derivation is copy-on-write in the value sense: the override map is copied so the original and + * the derived instance never alias the same mutable state, while the [envSource]/[propsSource] + * lookup functions are shared by reference (they are pure read seams and are never mutated). A + * mutator that touches no override and replaces no source produces an independent instance equal in + * behaviour to the original. [newBuilder] exposes the same prefilled builder for callers that prefer + * to thread it through other builder-folding code before [ConfigurationBuilder.build]. + * * ## Thread-safety * Instances are immutable once built (the override map is copied) and safe to share across threads. * The process-wide global slot is published via `@Volatile`; readers observe the most-recently-set @@ -33,6 +51,39 @@ public class Configuration internal constructor( private val envSource: Function = Function { name -> System.getenv(name) }, private val propsSource: Function = Function { name -> System.getProperty(name) }, ) { + /** A defensive copy of the explicit overrides, for prefilling a derived [ConfigurationBuilder]. */ + internal fun overridesSnapshot(): Map = overrides.toMap() + + /** The environment-variable lookup seam, shared by reference into derived builders. */ + internal fun envSource(): Function = envSource + + /** The system-property lookup seam, shared by reference into derived builders. */ + internal fun propsSource(): Function = propsSource + + /** + * Returns a fresh [ConfigurationBuilder] preloaded with this instance's overrides and lookup + * sources. Mutating the returned builder never affects this [Configuration]; the override map is + * copied up front. Use this when you want to thread the builder through other configuration code + * before calling [ConfigurationBuilder.build]; prefer [withOptions] for the common + * derive-in-one-call case. + */ + public fun newBuilder(): ConfigurationBuilder = ConfigurationBuilder(this) + + /** + * Derive a new immutable [Configuration] by applying [mutator] to a builder prefilled from this + * instance, then building it. This [Configuration] is left unchanged (copy-on-write): the + * override map is copied before [mutator] runs, so overrides added or replaced inside [mutator] + * never leak back into the receiver. The env/property lookup seams are inherited by reference. + * + * Kotlin's compiler-generated non-null parameter check raises `NullPointerException` when a Java + * caller passes `null` for [mutator], so no explicit guard is needed here. + */ + public fun withOptions(mutator: Consumer): Configuration { + val builder = newBuilder() + mutator.accept(builder) + return builder.build() + } + /** * Look up a configuration value by [name]. * diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/ConfigurationBuilder.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/ConfigurationBuilder.kt index 7050ba18..569f735e 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/ConfigurationBuilder.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/ConfigurationBuilder.kt @@ -7,6 +7,7 @@ package org.dexpace.sdk.core.config +import org.dexpace.sdk.core.generics.Builder import java.util.function.Function /** @@ -14,15 +15,33 @@ import java.util.function.Function * system properties), and [envSource] / [propsSource] as test seams to substitute the env / property * lookups with hermetic functions. * + * Implements the generic [Builder] contract so it can be folded by builder-style configuration code. + * Construct empty for a configuration built from scratch, or via [Configuration.newBuilder] / + * [Configuration.withOptions] to derive a reconfigured copy of an existing [Configuration]. + * * ## Thread-safety * Builders are *not* thread-safe — construct, configure, and [build] from a single thread. The * resulting [Configuration] is immutable and safe to share. */ -public class ConfigurationBuilder { +public class ConfigurationBuilder : Builder { private val overrides = mutableMapOf() private var envSource: Function = Function { name -> System.getenv(name) } private var propsSource: Function = Function { name -> System.getProperty(name) } + /** Creates an empty builder with the default env / system-property lookup sources. */ + public constructor() + + /** + * Creates a builder preloaded with [source]'s overrides and lookup sources. The override map is + * copied, so mutating this builder never affects [source]. Used by [Configuration.newBuilder] and + * [Configuration.withOptions]. + */ + public constructor(source: Configuration) { + overrides.putAll(source.overridesSnapshot()) + envSource = source.envSource() + propsSource = source.propsSource() + } + /** * Register an explicit override. Overrides win over every other layer. Kotlin's * compiler-generated non-null parameter check raises `NullPointerException` when a Java @@ -46,5 +65,5 @@ public class ConfigurationBuilder { * Materialize the immutable [Configuration]. The current override map is defensively copied so * subsequent [put] calls on this builder do not mutate the returned configuration. */ - public fun build(): Configuration = Configuration(overrides.toMap(), envSource, propsSource) + override fun build(): Configuration = Configuration(overrides.toMap(), envSource, propsSource) } diff --git a/sdk-core/src/test/kotlin/org/dexpace/sdk/core/config/ConfigurationTest.kt b/sdk-core/src/test/kotlin/org/dexpace/sdk/core/config/ConfigurationTest.kt index a86efd5f..9b1684d0 100644 --- a/sdk-core/src/test/kotlin/org/dexpace/sdk/core/config/ConfigurationTest.kt +++ b/sdk-core/src/test/kotlin/org/dexpace/sdk/core/config/ConfigurationTest.kt @@ -529,4 +529,85 @@ class ConfigurationTest { method.invoke(null, custom) assertEquals("hit", Configuration.getGlobalConfiguration().get("BRIDGE")) } + + // ----- withOptions / newBuilder (copy-on-write derivation) ----- + + @Test + fun `withOptions adds a new override on the derived configuration`() { + val base = ConfigurationBuilder().put("MAX_RETRY_ATTEMPTS", "3").build() + val derived = base.withOptions { it.put("LOG_LEVEL", "DEBUG") } + assertEquals("3", derived.get("MAX_RETRY_ATTEMPTS")) + assertEquals("DEBUG", derived.get("LOG_LEVEL")) + } + + @Test + fun `withOptions leaves the original configuration unchanged`() { + val base = ConfigurationBuilder().put("MAX_RETRY_ATTEMPTS", "3").build() + val derived = base.withOptions { it.put("LOG_LEVEL", "DEBUG") } + // The override added to the derived copy must not leak back into the receiver. + assertNull(base.get("LOG_LEVEL")) + assertEquals("3", base.get("MAX_RETRY_ATTEMPTS")) + // The two instances are distinct objects. + assertFalse(base === derived) + } + + @Test + fun `withOptions can override an existing key without mutating the original`() { + val base = ConfigurationBuilder().put("MAX_RETRY_ATTEMPTS", "3").build() + val derived = base.withOptions { it.put("MAX_RETRY_ATTEMPTS", "9") } + assertEquals("9", derived.get("MAX_RETRY_ATTEMPTS")) + assertEquals("3", base.get("MAX_RETRY_ATTEMPTS")) + } + + @Test + fun `withOptions inherits the env and property lookup seams`() { + val base = + ConfigurationBuilder() + .envSource { name -> if (name == "MAX_RETRY_ATTEMPTS") "5" else null } + .propsSource { name -> if (name == "log.level") "INFO" else null } + .build() + val derived = base.withOptions { it.put("LOG_LEVEL", "DEBUG") } + // Inherited env seam still resolves on the derived copy. + assertEquals("5", derived.get("MAX_RETRY_ATTEMPTS")) + // Explicit override on the derived copy wins over the inherited property seam. + assertEquals("DEBUG", derived.get("LOG_LEVEL")) + // The base, queried for the same key, still falls through to the property seam. + assertEquals("INFO", base.get("LOG_LEVEL")) + } + + @Test + fun `withOptions with an empty mutator yields an equivalent independent configuration`() { + val base = ConfigurationBuilder().put("MAX_RETRY_ATTEMPTS", "3").build() + val derived = base.withOptions { /* no-op */ } + assertFalse(base === derived) + assertEquals("3", derived.get("MAX_RETRY_ATTEMPTS")) + } + + @Test + fun `newBuilder prefills overrides and sources and is independent of the source`() { + val base = ConfigurationBuilder().put("MAX_RETRY_ATTEMPTS", "3").build() + val builder = base.newBuilder() + builder.put("LOG_LEVEL", "DEBUG") + val derived = builder.build() + assertEquals("3", derived.get("MAX_RETRY_ATTEMPTS")) + assertEquals("DEBUG", derived.get("LOG_LEVEL")) + // Mutating the builder after the fact never affects the already-derived configuration + // nor the original. + builder.put("EXTRA", "x") + assertNull(derived.get("EXTRA")) + assertNull(base.get("EXTRA")) + assertNull(base.get("LOG_LEVEL")) + } + + @Test + fun `prefilled builder constructor copies the override map defensively`() { + val base = ConfigurationBuilder().put("MAX_RETRY_ATTEMPTS", "3").build() + val first = base.newBuilder().put("A", "1").build() + val second = base.newBuilder().put("B", "2").build() + // Two independent derivations from the same base do not see each other's overrides. + assertEquals("1", first.get("A")) + assertNull(first.get("B")) + assertEquals("2", second.get("B")) + assertNull(second.get("A")) + } } From 7f8ff1fd86dc31fe040b87dc186544fd60abd435 Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Sat, 20 Jun 2026 16:40:25 +0300 Subject: [PATCH 2/6] refactor: simplify Configuration derivation seams to internal vals Replace the three internal accessor methods (overridesSnapshot/envSource/ propsSource) with internal-val backing fields read directly by the prefilled ConfigurationBuilder constructor. This removes the property/method name collision and drops a redundant defensive copy of the override map (the builder's putAll already copies into its own mutable map). Add a test pinning the documented contract that the env/property read seams are inherited by reference, not copied. --- .../org/dexpace/sdk/core/config/Configuration.kt | 15 +++------------ .../sdk/core/config/ConfigurationBuilder.kt | 6 +++--- .../dexpace/sdk/core/config/ConfigurationTest.kt | 12 ++++++++++++ 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt index 9c52b2c8..0dcf47d4 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt @@ -47,19 +47,10 @@ import java.util.function.Function * configuration under last-write-wins semantics. */ public class Configuration internal constructor( - private val overrides: Map, - private val envSource: Function = Function { name -> System.getenv(name) }, - private val propsSource: Function = Function { name -> System.getProperty(name) }, + internal val overrides: Map, + internal val envSource: Function = Function { name -> System.getenv(name) }, + internal val propsSource: Function = Function { name -> System.getProperty(name) }, ) { - /** A defensive copy of the explicit overrides, for prefilling a derived [ConfigurationBuilder]. */ - internal fun overridesSnapshot(): Map = overrides.toMap() - - /** The environment-variable lookup seam, shared by reference into derived builders. */ - internal fun envSource(): Function = envSource - - /** The system-property lookup seam, shared by reference into derived builders. */ - internal fun propsSource(): Function = propsSource - /** * Returns a fresh [ConfigurationBuilder] preloaded with this instance's overrides and lookup * sources. Mutating the returned builder never affects this [Configuration]; the override map is diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/ConfigurationBuilder.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/ConfigurationBuilder.kt index 569f735e..21f491f8 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/ConfigurationBuilder.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/ConfigurationBuilder.kt @@ -37,9 +37,9 @@ public class ConfigurationBuilder : Builder { * [Configuration.withOptions]. */ public constructor(source: Configuration) { - overrides.putAll(source.overridesSnapshot()) - envSource = source.envSource() - propsSource = source.propsSource() + overrides.putAll(source.overrides) + envSource = source.envSource + propsSource = source.propsSource } /** diff --git a/sdk-core/src/test/kotlin/org/dexpace/sdk/core/config/ConfigurationTest.kt b/sdk-core/src/test/kotlin/org/dexpace/sdk/core/config/ConfigurationTest.kt index 9b1684d0..9af6c02e 100644 --- a/sdk-core/src/test/kotlin/org/dexpace/sdk/core/config/ConfigurationTest.kt +++ b/sdk-core/src/test/kotlin/org/dexpace/sdk/core/config/ConfigurationTest.kt @@ -8,6 +8,7 @@ package org.dexpace.sdk.core.config import java.time.Duration +import java.util.function.Function import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals @@ -575,6 +576,17 @@ class ConfigurationTest { assertEquals("INFO", base.get("LOG_LEVEL")) } + @Test + fun `withOptions inherits the env and property seams by reference, not by copy`() { + val env = Function { null } + val props = Function { null } + val base = ConfigurationBuilder().envSource(env).propsSource(props).build() + val derived = base.withOptions { it.put("LOG_LEVEL", "DEBUG") } + // The pure read seams are inherited by reference; the override map is the only copied state. + assertSame(base.envSource, derived.envSource) + assertSame(base.propsSource, derived.propsSource) + } + @Test fun `withOptions with an empty mutator yields an equivalent independent configuration`() { val base = ConfigurationBuilder().put("MAX_RETRY_ATTEMPTS", "3").build() From 4c723714d5252f9510bb57f2262f715f4429f6db Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Sat, 20 Jun 2026 17:22:45 +0300 Subject: [PATCH 3/6] refactor: rename Configuration.withOptions to derive and broaden derivation coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the copy-on-write derivation entry point from withOptions to derive. The name states the operation — deriving a reconfigured copy — instead of introducing an "options" noun that appears nowhere else in the configuration API, and it pairs cleanly with newBuilder. Constrain the derivation read seams (overrides/envSource/propsSource) with @get:JvmSynthetic so their internal accessors stay off the Java-callable surface, matching the SDK's internal-seam convention. Surface derive/newBuilder in the class-level KDoc summary, and add tests for chained derivation and intermediate independence, the null-mutator NPE contract, an override beating an inherited env seam, source replacement detaching only the derived copy, empty-env skip-to-sysprop inheritance, and derivation from an override-less base. --- sdk-core/api/sdk-core.api | 2 +- .../dexpace/sdk/core/config/Configuration.kt | 14 +- .../sdk/core/config/ConfigurationBuilder.kt | 4 +- .../sdk/core/config/ConfigurationTest.kt | 125 ++++++++++++++++-- 4 files changed, 124 insertions(+), 21 deletions(-) diff --git a/sdk-core/api/sdk-core.api b/sdk-core/api/sdk-core.api index a0bb0647..ff47b93b 100644 --- a/sdk-core/api/sdk-core.api +++ b/sdk-core/api/sdk-core.api @@ -28,6 +28,7 @@ public final class org/dexpace/sdk/core/config/Configuration { public static final field LOG_LEVEL Ljava/lang/String; public static final field MAX_RETRY_ATTEMPTS Ljava/lang/String; public static final field NO_PROXY Ljava/lang/String; + public final fun derive (Ljava/util/function/Consumer;)Lorg/dexpace/sdk/core/config/Configuration; public final fun get (Ljava/lang/String;)Ljava/lang/String; public final fun get (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; public static synthetic fun get$default (Lorg/dexpace/sdk/core/config/Configuration;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/String; @@ -38,7 +39,6 @@ public final class org/dexpace/sdk/core/config/Configuration { public final fun getProperty (Ljava/lang/String;)Ljava/lang/String; public final fun newBuilder ()Lorg/dexpace/sdk/core/config/ConfigurationBuilder; public static final fun setGlobalConfiguration (Lorg/dexpace/sdk/core/config/Configuration;)V - public final fun withOptions (Ljava/util/function/Consumer;)Lorg/dexpace/sdk/core/config/Configuration; } public final class org/dexpace/sdk/core/config/Configuration$Companion { diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt index 0dcf47d4..519f7300 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt @@ -22,15 +22,16 @@ import java.util.function.Function * Typed accessors (`getInt`, `getBoolean`, `getDuration`) return the provided default on parse failures — * configuration issues never throw at the lookup site. * - * Constructed via [ConfigurationBuilder]. + * Constructed via [ConfigurationBuilder]; derive a reconfigured copy of an existing instance with + * [derive] or [newBuilder]. * * ## Deriving a reconfigured copy (copy-on-write) - * [withOptions] returns a **new** immutable [Configuration] with a mutator applied on top of this + * [derive] returns a **new** immutable [Configuration] with a mutator applied on top of this * one, leaving the receiver untouched: * * ```java * Configuration base = new ConfigurationBuilder().put("MAX_RETRY_ATTEMPTS", "3").build(); - * Configuration derived = base.withOptions(b -> b.put("LOG_LEVEL", "DEBUG")); + * Configuration derived = base.derive(b -> b.put("LOG_LEVEL", "DEBUG")); * // base is unchanged; derived carries both overrides. * ``` * @@ -47,15 +48,18 @@ import java.util.function.Function * configuration under last-write-wins semantics. */ public class Configuration internal constructor( + @get:JvmSynthetic internal val overrides: Map, + @get:JvmSynthetic internal val envSource: Function = Function { name -> System.getenv(name) }, + @get:JvmSynthetic internal val propsSource: Function = Function { name -> System.getProperty(name) }, ) { /** * Returns a fresh [ConfigurationBuilder] preloaded with this instance's overrides and lookup * sources. Mutating the returned builder never affects this [Configuration]; the override map is * copied up front. Use this when you want to thread the builder through other configuration code - * before calling [ConfigurationBuilder.build]; prefer [withOptions] for the common + * before calling [ConfigurationBuilder.build]; prefer [derive] for the common * derive-in-one-call case. */ public fun newBuilder(): ConfigurationBuilder = ConfigurationBuilder(this) @@ -69,7 +73,7 @@ public class Configuration internal constructor( * Kotlin's compiler-generated non-null parameter check raises `NullPointerException` when a Java * caller passes `null` for [mutator], so no explicit guard is needed here. */ - public fun withOptions(mutator: Consumer): Configuration { + public fun derive(mutator: Consumer): Configuration { val builder = newBuilder() mutator.accept(builder) return builder.build() diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/ConfigurationBuilder.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/ConfigurationBuilder.kt index 21f491f8..f1a367cc 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/ConfigurationBuilder.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/ConfigurationBuilder.kt @@ -17,7 +17,7 @@ import java.util.function.Function * * Implements the generic [Builder] contract so it can be folded by builder-style configuration code. * Construct empty for a configuration built from scratch, or via [Configuration.newBuilder] / - * [Configuration.withOptions] to derive a reconfigured copy of an existing [Configuration]. + * [Configuration.derive] to derive a reconfigured copy of an existing [Configuration]. * * ## Thread-safety * Builders are *not* thread-safe — construct, configure, and [build] from a single thread. The @@ -34,7 +34,7 @@ public class ConfigurationBuilder : Builder { /** * Creates a builder preloaded with [source]'s overrides and lookup sources. The override map is * copied, so mutating this builder never affects [source]. Used by [Configuration.newBuilder] and - * [Configuration.withOptions]. + * [Configuration.derive]. */ public constructor(source: Configuration) { overrides.putAll(source.overrides) diff --git a/sdk-core/src/test/kotlin/org/dexpace/sdk/core/config/ConfigurationTest.kt b/sdk-core/src/test/kotlin/org/dexpace/sdk/core/config/ConfigurationTest.kt index 9af6c02e..9d5dfe8e 100644 --- a/sdk-core/src/test/kotlin/org/dexpace/sdk/core/config/ConfigurationTest.kt +++ b/sdk-core/src/test/kotlin/org/dexpace/sdk/core/config/ConfigurationTest.kt @@ -531,20 +531,20 @@ class ConfigurationTest { assertEquals("hit", Configuration.getGlobalConfiguration().get("BRIDGE")) } - // ----- withOptions / newBuilder (copy-on-write derivation) ----- + // ----- derive / newBuilder (copy-on-write derivation) ----- @Test - fun `withOptions adds a new override on the derived configuration`() { + fun `derive adds a new override on the derived configuration`() { val base = ConfigurationBuilder().put("MAX_RETRY_ATTEMPTS", "3").build() - val derived = base.withOptions { it.put("LOG_LEVEL", "DEBUG") } + val derived = base.derive { it.put("LOG_LEVEL", "DEBUG") } assertEquals("3", derived.get("MAX_RETRY_ATTEMPTS")) assertEquals("DEBUG", derived.get("LOG_LEVEL")) } @Test - fun `withOptions leaves the original configuration unchanged`() { + fun `derive leaves the original configuration unchanged`() { val base = ConfigurationBuilder().put("MAX_RETRY_ATTEMPTS", "3").build() - val derived = base.withOptions { it.put("LOG_LEVEL", "DEBUG") } + val derived = base.derive { it.put("LOG_LEVEL", "DEBUG") } // The override added to the derived copy must not leak back into the receiver. assertNull(base.get("LOG_LEVEL")) assertEquals("3", base.get("MAX_RETRY_ATTEMPTS")) @@ -553,21 +553,21 @@ class ConfigurationTest { } @Test - fun `withOptions can override an existing key without mutating the original`() { + fun `derive can override an existing key without mutating the original`() { val base = ConfigurationBuilder().put("MAX_RETRY_ATTEMPTS", "3").build() - val derived = base.withOptions { it.put("MAX_RETRY_ATTEMPTS", "9") } + val derived = base.derive { it.put("MAX_RETRY_ATTEMPTS", "9") } assertEquals("9", derived.get("MAX_RETRY_ATTEMPTS")) assertEquals("3", base.get("MAX_RETRY_ATTEMPTS")) } @Test - fun `withOptions inherits the env and property lookup seams`() { + fun `derive inherits the env and property lookup seams`() { val base = ConfigurationBuilder() .envSource { name -> if (name == "MAX_RETRY_ATTEMPTS") "5" else null } .propsSource { name -> if (name == "log.level") "INFO" else null } .build() - val derived = base.withOptions { it.put("LOG_LEVEL", "DEBUG") } + val derived = base.derive { it.put("LOG_LEVEL", "DEBUG") } // Inherited env seam still resolves on the derived copy. assertEquals("5", derived.get("MAX_RETRY_ATTEMPTS")) // Explicit override on the derived copy wins over the inherited property seam. @@ -577,24 +577,123 @@ class ConfigurationTest { } @Test - fun `withOptions inherits the env and property seams by reference, not by copy`() { + fun `derive override wins over the inherited env seam on the derived copy`() { + val base = + ConfigurationBuilder() + .envSource { name -> if (name == "MAX_RETRY_ATTEMPTS") "5" else null } + .propsSource { null } + .build() + // The inherited env seam supplies "5"; the derived copy adds an explicit override for the + // same key. Override -> env precedence must hold across derivation. + val derived = base.derive { it.put("MAX_RETRY_ATTEMPTS", "9") } + assertEquals("9", derived.get("MAX_RETRY_ATTEMPTS")) + // The base, queried for the same key, still falls through to the inherited env seam. + assertEquals("5", base.get("MAX_RETRY_ATTEMPTS")) + } + + @Test + fun `derive inherits the env and property seams by reference, not by copy`() { val env = Function { null } val props = Function { null } val base = ConfigurationBuilder().envSource(env).propsSource(props).build() - val derived = base.withOptions { it.put("LOG_LEVEL", "DEBUG") } + val derived = base.derive { it.put("LOG_LEVEL", "DEBUG") } // The pure read seams are inherited by reference; the override map is the only copied state. assertSame(base.envSource, derived.envSource) assertSame(base.propsSource, derived.propsSource) } @Test - fun `withOptions with an empty mutator yields an equivalent independent configuration`() { + fun `derive replacing a source detaches only the derived copy from the shared seam`() { + val baseEnv = Function { null } + val newEnv = Function { name -> if (name == "MAX_RETRY_ATTEMPTS") "9" else null } + val base = ConfigurationBuilder().envSource(baseEnv).propsSource { null }.build() + val derived = base.derive { it.envSource(newEnv) } + // The derived copy swaps in the new seam... + assertSame(newEnv, derived.envSource) + // ...while the base keeps its original reference (copy-on-write, not aliased mutation). + assertSame(baseEnv, base.envSource) + // Behaviour follows the rebinding: derived resolves via newEnv, base does not. + assertEquals("9", derived.get("MAX_RETRY_ATTEMPTS")) + assertNull(base.get("MAX_RETRY_ATTEMPTS")) + } + + @Test + fun `derive preserves the empty-env skip-to-sysprop semantics on the derived copy`() { + val base = + ConfigurationBuilder() + .envSource { "" } // present-but-empty: must be treated as absent + .propsSource { name -> if (name == "max.retry.attempts") "7" else null } + .build() + val derived = base.derive { it.put("LOG_LEVEL", "DEBUG") } + // The empty-env skip is inherited: the derived copy still falls through to the sysprop seam. + assertEquals("7", derived.get("MAX_RETRY_ATTEMPTS")) + assertEquals("DEBUG", derived.get("LOG_LEVEL")) + } + + @Test + fun `derive on a configuration with no overrides yields an override-only copy`() { + // Empty override map + pinned-absent seams: exercises the prefilled constructor's + // putAll(emptyMap) path hermetically, without touching the real process environment. + val base = ConfigurationBuilder().envSource { null }.propsSource { null }.build() + val derived = base.derive { it.put("LOG_LEVEL", "DEBUG") } + assertEquals("DEBUG", derived.get("LOG_LEVEL")) + assertNull(base.get("LOG_LEVEL")) + assertFalse(base === derived) + } + + @Test + fun `derive chained twice accumulates overrides and leaves each level independent`() { + val base = + ConfigurationBuilder() + .put("A", "1") + .envSource { null } + .propsSource { null } + .build() + val d1 = base.derive { it.put("B", "2") } + val d2 = + d1.derive { + it.put("C", "3") + it.put("A", "9") + } + // d2 accumulates across both hops, with its own override shadowing the inherited A. + assertEquals("9", d2.get("A")) + assertEquals("2", d2.get("B")) + assertEquals("3", d2.get("C")) + // The intermediate d1 is unaffected by d2's later additions/overrides. + assertEquals("1", d1.get("A")) + assertEquals("2", d1.get("B")) + assertNull(d1.get("C")) + // The base never sees anything added downstream. + assertEquals("1", base.get("A")) + assertNull(base.get("B")) + assertNull(base.get("C")) + } + + @Test + fun `derive with an empty mutator yields an equivalent independent configuration`() { val base = ConfigurationBuilder().put("MAX_RETRY_ATTEMPTS", "3").build() - val derived = base.withOptions { /* no-op */ } + val derived = base.derive { /* no-op */ } assertFalse(base === derived) assertEquals("3", derived.get("MAX_RETRY_ATTEMPTS")) } + @Test + fun `derive null mutator throws NullPointerException`() { + // Force `null` past the Kotlin non-null param via reflection — mirrors a Java caller + // who hands in null. The compiler-generated non-null check must trigger, as the KDoc claims. + val method = + Configuration::class.java.getMethod( + "derive", + java.util.function.Consumer::class.java, + ) + val base = ConfigurationBuilder().put("MAX_RETRY_ATTEMPTS", "3").build() + val ex = + assertFailsWith { + method.invoke(base, null as java.util.function.Consumer?) + } + assertTrue(ex.targetException is NullPointerException, "Expected NPE, got ${ex.targetException}") + } + @Test fun `newBuilder prefills overrides and sources and is independent of the source`() { val base = ConfigurationBuilder().put("MAX_RETRY_ATTEMPTS", "3").build() From d2de23aa4dc4e30ed98acca7388ac12371f39c1e Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Sat, 20 Jun 2026 17:32:39 +0300 Subject: [PATCH 4/6] feat: add Configuration.builder() factory to match peer models Every other SDK value model exposes a @JvmStatic builder() entry point (Request, Response, Headers, RequestConditions, RetrySettings, ClientIdentityStep, IdempotencyKeyStep). Configuration was the lone exception, forcing Java callers to write `new ConfigurationBuilder()`. Add `Configuration.builder()` returning a fresh empty ConfigurationBuilder, completing the construction surface: builder() for from-scratch, newBuilder()/derive() for copy-on-write derivation from an existing instance. --- sdk-core/api/sdk-core.api | 2 ++ .../org/dexpace/sdk/core/config/Configuration.kt | 8 ++++++++ .../org/dexpace/sdk/core/config/ConfigurationTest.kt | 11 +++++++++++ 3 files changed, 21 insertions(+) diff --git a/sdk-core/api/sdk-core.api b/sdk-core/api/sdk-core.api index ff47b93b..0a37bd0f 100644 --- a/sdk-core/api/sdk-core.api +++ b/sdk-core/api/sdk-core.api @@ -28,6 +28,7 @@ public final class org/dexpace/sdk/core/config/Configuration { public static final field LOG_LEVEL Ljava/lang/String; public static final field MAX_RETRY_ATTEMPTS Ljava/lang/String; public static final field NO_PROXY Ljava/lang/String; + public static final fun builder ()Lorg/dexpace/sdk/core/config/ConfigurationBuilder; public final fun derive (Ljava/util/function/Consumer;)Lorg/dexpace/sdk/core/config/Configuration; public final fun get (Ljava/lang/String;)Ljava/lang/String; public final fun get (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; @@ -42,6 +43,7 @@ public final class org/dexpace/sdk/core/config/Configuration { } public final class org/dexpace/sdk/core/config/Configuration$Companion { + public final fun builder ()Lorg/dexpace/sdk/core/config/ConfigurationBuilder; public final fun getGlobalConfiguration ()Lorg/dexpace/sdk/core/config/Configuration; public final fun setGlobalConfiguration (Lorg/dexpace/sdk/core/config/Configuration;)V } diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt index 519f7300..54bf2937 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt @@ -141,6 +141,14 @@ public class Configuration internal constructor( ): Duration = get(name)?.let { parseDuration(it) } ?: default public companion object { + /** + * Returns a fresh empty [ConfigurationBuilder]. Java-friendly entry point matching the + * `Configuration.builder()` idiom every other SDK model exposes; build from scratch with this, + * or derive a reconfigured copy of an existing instance with [newBuilder] / [derive]. + */ + @JvmStatic + public fun builder(): ConfigurationBuilder = ConfigurationBuilder() + // Well-known keys. `const val` so callers reference them as `Configuration.MAX_RETRY_ATTEMPTS` // from both Kotlin and Java without going through `Companion`. diff --git a/sdk-core/src/test/kotlin/org/dexpace/sdk/core/config/ConfigurationTest.kt b/sdk-core/src/test/kotlin/org/dexpace/sdk/core/config/ConfigurationTest.kt index 9d5dfe8e..b9062cca 100644 --- a/sdk-core/src/test/kotlin/org/dexpace/sdk/core/config/ConfigurationTest.kt +++ b/sdk-core/src/test/kotlin/org/dexpace/sdk/core/config/ConfigurationTest.kt @@ -331,6 +331,17 @@ class ConfigurationTest { // ----- ConfigurationBuilder behaviors ----- + @Test + fun `builder factory returns a fresh empty builder each call`() { + // Java-friendly factory: equivalent to `new ConfigurationBuilder()`, distinct each call. + val first = Configuration.builder() + val second = Configuration.builder() + assertFalse(first === second) + // It starts empty: with the seams pinned absent, an unset key resolves to null. + val cfg = Configuration.builder().envSource { null }.propsSource { null }.build() + assertNull(cfg.get("MAX_RETRY_ATTEMPTS")) + } + @Test fun `builder put null name throws NullPointerException`() { val method = From feb2b540eb052cc9953cac661410495163e82600 Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Sat, 20 Jun 2026 18:38:40 +0300 Subject: [PATCH 5/6] feat: add ConfigurationBuilder.remove to drop overrides during derivation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Configuration derivation could previously only add or shadow overrides — there was no supported way to drop an inherited explicit override and let a key fall back to the environment / system-property layers again. Code holding a derived Configuration that wanted "the same config, but stop forcing LOG_LEVEL" had to rebuild from scratch and re-thread the lookup seams. Add ConfigurationBuilder.remove(name), the inverse of put(), following the existing Headers.Builder.remove convention (returns the builder for chaining, no-op when the key is absent). It drops only the override layer: a later get(name) falls through to the environment variable, then the normalized system property, then the caller's default. It does not force the key to null and does not suppress an inherited env/property value for that key. This lets Configuration.derive { it.remove(k) } un-pin an override inherited from the source while leaving the source unchanged. Tests cover builder-level removal (existing key, absent-key no-op, fall-through to the env and property seams, chaining, null-name NPE) and derivation-level removal (un-pinning without mutating the base, fall-through to the inherited env and property seams). Also tidies two KDoc lines on the surrounding construction surface. --- sdk-core/api/sdk-core.api | 1 + .../dexpace/sdk/core/config/Configuration.kt | 11 +- .../sdk/core/config/ConfigurationBuilder.kt | 16 +++ .../sdk/core/config/ConfigurationTest.kt | 134 ++++++++++++++++++ 4 files changed, 157 insertions(+), 5 deletions(-) diff --git a/sdk-core/api/sdk-core.api b/sdk-core/api/sdk-core.api index 0a37bd0f..056d235b 100644 --- a/sdk-core/api/sdk-core.api +++ b/sdk-core/api/sdk-core.api @@ -56,6 +56,7 @@ public final class org/dexpace/sdk/core/config/ConfigurationBuilder : org/dexpac public final fun envSource (Ljava/util/function/Function;)Lorg/dexpace/sdk/core/config/ConfigurationBuilder; public final fun propsSource (Ljava/util/function/Function;)Lorg/dexpace/sdk/core/config/ConfigurationBuilder; public final fun put (Ljava/lang/String;Ljava/lang/String;)Lorg/dexpace/sdk/core/config/ConfigurationBuilder; + public final fun remove (Ljava/lang/String;)Lorg/dexpace/sdk/core/config/ConfigurationBuilder; } public abstract interface class org/dexpace/sdk/core/generics/Builder { diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt index 54bf2937..7b6598bc 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt @@ -30,7 +30,7 @@ import java.util.function.Function * one, leaving the receiver untouched: * * ```java - * Configuration base = new ConfigurationBuilder().put("MAX_RETRY_ATTEMPTS", "3").build(); + * Configuration base = Configuration.builder().put("MAX_RETRY_ATTEMPTS", "3").build(); * Configuration derived = base.derive(b -> b.put("LOG_LEVEL", "DEBUG")); * // base is unchanged; derived carries both overrides. * ``` @@ -67,8 +67,9 @@ public class Configuration internal constructor( /** * Derive a new immutable [Configuration] by applying [mutator] to a builder prefilled from this * instance, then building it. This [Configuration] is left unchanged (copy-on-write): the - * override map is copied before [mutator] runs, so overrides added or replaced inside [mutator] - * never leak back into the receiver. The env/property lookup seams are inherited by reference. + * override map is copied before [mutator] runs, so overrides added, replaced, or removed inside + * [mutator] never leak back into the receiver. The env/property lookup seams are inherited by + * reference. * * Kotlin's compiler-generated non-null parameter check raises `NullPointerException` when a Java * caller passes `null` for [mutator], so no explicit guard is needed here. @@ -143,8 +144,8 @@ public class Configuration internal constructor( public companion object { /** * Returns a fresh empty [ConfigurationBuilder]. Java-friendly entry point matching the - * `Configuration.builder()` idiom every other SDK model exposes; build from scratch with this, - * or derive a reconfigured copy of an existing instance with [newBuilder] / [derive]. + * `builder()` idiom every other SDK model exposes; build from scratch with this, or derive a + * reconfigured copy of an existing instance with [newBuilder] / [derive]. */ @JvmStatic public fun builder(): ConfigurationBuilder = ConfigurationBuilder() diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/ConfigurationBuilder.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/ConfigurationBuilder.kt index f1a367cc..32952df1 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/ConfigurationBuilder.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/ConfigurationBuilder.kt @@ -55,6 +55,22 @@ public class ConfigurationBuilder : Builder { overrides[name] = value } + /** + * Remove the explicit override for [name], if one is set. This drops only the override layer: + * a later [Configuration.get] for [name] falls through to the environment-variable and + * system-property seams (and finally the caller's default) exactly as if the override had never + * been registered — it does **not** force the key to resolve to `null`. Removing a key that + * carries no override is a no-op. As the inverse of [put], this is what lets + * [Configuration.derive] un-pin an override inherited from the source instance. + * + * Kotlin's compiler-generated non-null parameter check raises `NullPointerException` when a + * Java caller passes `null` for [name], so no explicit guard is needed here. + */ + public fun remove(name: String): ConfigurationBuilder = + apply { + overrides.remove(name) + } + /** Test seam: override the environment-variable source. */ public fun envSource(source: Function): ConfigurationBuilder = apply { envSource = source } diff --git a/sdk-core/src/test/kotlin/org/dexpace/sdk/core/config/ConfigurationTest.kt b/sdk-core/src/test/kotlin/org/dexpace/sdk/core/config/ConfigurationTest.kt index b9062cca..ccf0c0b6 100644 --- a/sdk-core/src/test/kotlin/org/dexpace/sdk/core/config/ConfigurationTest.kt +++ b/sdk-core/src/test/kotlin/org/dexpace/sdk/core/config/ConfigurationTest.kt @@ -372,6 +372,82 @@ class ConfigurationTest { assertTrue(ex.targetException is NullPointerException, "Expected NPE, got ${ex.targetException}") } + @Test + fun `builder remove deletes an existing override`() { + // Seams pinned absent, so a removed override leaves nothing to fall back to: get -> null. + val cfg = + ConfigurationBuilder() + .put("MAX_RETRY_ATTEMPTS", "3") + .remove("MAX_RETRY_ATTEMPTS") + .envSource { null } + .propsSource { null } + .build() + assertNull(cfg.get("MAX_RETRY_ATTEMPTS")) + } + + @Test + fun `builder remove of an absent key is a no-op`() { + // Removing a key that was never overridden must not throw and must not disturb other keys. + val cfg = + ConfigurationBuilder() + .put("A", "1") + .remove("NEVER_SET") + .envSource { null } + .propsSource { null } + .build() + assertEquals("1", cfg.get("A")) + assertNull(cfg.get("NEVER_SET")) + } + + @Test + fun `builder remove drops only the override layer - env seam still resolves`() { + // remove un-pins the explicit override; it does NOT suppress the inherited env seam for + // that key. The key must still resolve via env after the override is gone. + val cfg = + ConfigurationBuilder() + .put("MAX_RETRY_ATTEMPTS", "3") + .envSource { name -> if (name == "MAX_RETRY_ATTEMPTS") "5" else null } + .propsSource { null } + .remove("MAX_RETRY_ATTEMPTS") + .build() + assertEquals("5", cfg.get("MAX_RETRY_ATTEMPTS")) + } + + @Test + fun `builder remove drops only the override layer - property seam still resolves`() { + // Mirror of the env fall-through case, exercising the distinct system-property branch of + // get(): env is pinned absent, so the removed override falls through to the envToProp- + // normalized property lookup (MAX_RETRY_ATTEMPTS -> max.retry.attempts). + val cfg = + ConfigurationBuilder() + .put("MAX_RETRY_ATTEMPTS", "3") + .envSource { null } + .propsSource { name -> if (name == "max.retry.attempts") "9" else null } + .remove("MAX_RETRY_ATTEMPTS") + .build() + assertEquals("9", cfg.get("MAX_RETRY_ATTEMPTS")) + } + + @Test + fun `builder remove returns same instance for chaining`() { + val b = ConfigurationBuilder() + assertSame(b, b.remove("anything")) + } + + @Test + fun `builder remove null name throws NullPointerException`() { + val method = + ConfigurationBuilder::class.java.getMethod( + "remove", + String::class.java, + ) + val ex = + assertFailsWith { + method.invoke(ConfigurationBuilder(), null as String?) + } + assertTrue(ex.targetException is NullPointerException, "Expected NPE, got ${ex.targetException}") + } + @Test fun `builder test seams isolate from process env and sysprops`() { // Hermetic: neither the real env nor real sysprops should be touched. @@ -732,4 +808,62 @@ class ConfigurationTest { assertEquals("2", second.get("B")) assertNull(second.get("A")) } + + @Test + fun `derive removing an inherited override un-pins it without mutating the original`() { + val base = + ConfigurationBuilder() + .put("LOG_LEVEL", "DEBUG") + .envSource { null } + .propsSource { null } + .build() + val derived = base.derive { it.remove("LOG_LEVEL") } + // The derived copy no longer carries the override and, with seams pinned absent, resolves null. + assertNull(derived.get("LOG_LEVEL")) + // The base keeps its override: removal on the derived copy must not leak back. + assertEquals("DEBUG", base.get("LOG_LEVEL")) + } + + @Test + fun `derive remove drops only the override and falls back to the inherited env seam`() { + // The crux of remove's contract across derivation: removing an inherited override un-pins + // the explicit value but does NOT suppress the inherited env seam for that key. The base + // forces "3"; the derived copy removes the override and so falls back to the env seam's "5". + val base = + ConfigurationBuilder() + .put("MAX_RETRY_ATTEMPTS", "3") + .envSource { name -> if (name == "MAX_RETRY_ATTEMPTS") "5" else null } + .propsSource { null } + .build() + val derived = base.derive { it.remove("MAX_RETRY_ATTEMPTS") } + assertEquals("5", derived.get("MAX_RETRY_ATTEMPTS")) + // The base still resolves the explicit override it was built with. + assertEquals("3", base.get("MAX_RETRY_ATTEMPTS")) + } + + @Test + fun `derive remove drops only the override and falls back to the inherited property seam`() { + // Property-seam mirror of the env fall-through case: env pinned absent, so removing the + // inherited override on the derived copy lets the key resolve via the inherited, + // envToProp-normalized system-property seam (MAX_RETRY_ATTEMPTS -> max.retry.attempts). + val base = + ConfigurationBuilder() + .put("MAX_RETRY_ATTEMPTS", "3") + .envSource { null } + .propsSource { name -> if (name == "max.retry.attempts") "9" else null } + .build() + val derived = base.derive { it.remove("MAX_RETRY_ATTEMPTS") } + assertEquals("9", derived.get("MAX_RETRY_ATTEMPTS")) + // The base still resolves the explicit override it was built with. + assertEquals("3", base.get("MAX_RETRY_ATTEMPTS")) + } + + @Test + fun `derive remove of an absent override is a harmless no-op`() { + val base = ConfigurationBuilder().put("A", "1").envSource { null }.propsSource { null }.build() + val derived = base.derive { it.remove("NEVER_SET") } + assertEquals("1", derived.get("A")) + assertNull(derived.get("NEVER_SET")) + assertFalse(base === derived) + } } From 427b18727b6ad702228c3b0f0bfffa026f1da28b Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Sat, 20 Jun 2026 18:38:46 +0300 Subject: [PATCH 6/6] refactor: use anonymous binding for ignored catch parameter in AsyncResponseBridge closeQuietly intentionally discards any exception from response.close(). Rename the bound-but-unused catch parameter to the anonymous `_` to state that intent directly and match idiomatic Kotlin for ignored bindings. --- .../sdk/transport/jdkhttp/internal/AsyncResponseBridge.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk-transport-jdkhttp/src/main/kotlin/org/dexpace/sdk/transport/jdkhttp/internal/AsyncResponseBridge.kt b/sdk-transport-jdkhttp/src/main/kotlin/org/dexpace/sdk/transport/jdkhttp/internal/AsyncResponseBridge.kt index cb71bb37..4b7e40c2 100644 --- a/sdk-transport-jdkhttp/src/main/kotlin/org/dexpace/sdk/transport/jdkhttp/internal/AsyncResponseBridge.kt +++ b/sdk-transport-jdkhttp/src/main/kotlin/org/dexpace/sdk/transport/jdkhttp/internal/AsyncResponseBridge.kt @@ -66,7 +66,7 @@ internal fun bridgeAsyncResponse( private fun closeQuietly(response: SdkResponse) { try { response.close() - } catch (ignored: Exception) { + } catch (_: Exception) { // Intentionally ignored: the response is already being discarded. } }