Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions sdk-core/api/sdk-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,20 @@ public final class org/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 static final fun setGlobalConfiguration (Lorg/dexpace/sdk/core/config/Configuration;)V
public final fun toBuilder ()Lorg/dexpace/sdk/core/config/ConfigurationBuilder;
public final fun withOptions (Ljava/util/function/Consumer;)Lorg/dexpace/sdk/core/config/Configuration;
}

public final class org/dexpace/sdk/core/config/Configuration$Companion {
public final fun getGlobalConfiguration ()Lorg/dexpace/sdk/core/config/Configuration;
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 <init> ()V
public final fun build ()Lorg/dexpace/sdk/core/config/Configuration;
public fun <init> (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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -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. [toBuilder] 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
Expand All @@ -33,6 +51,39 @@ public class Configuration internal constructor(
private val envSource: Function<String, String?> = Function { name -> System.getenv(name) },
private val propsSource: Function<String, String?> = Function { name -> System.getProperty(name) },
) {
/** A defensive copy of the explicit overrides, for prefilling a derived [ConfigurationBuilder]. */
internal fun overridesSnapshot(): Map<String, String> = overrides.toMap()

/** The environment-variable lookup seam, shared by reference into derived builders. */
internal fun envSource(): Function<String, String?> = envSource

/** The system-property lookup seam, shared by reference into derived builders. */
internal fun propsSource(): Function<String, String?> = 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 toBuilder(): 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<ConfigurationBuilder>): Configuration {
val builder = toBuilder()
mutator.accept(builder)
return builder.build()
}

/**
* Look up a configuration value by [name].
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,41 @@

package org.dexpace.sdk.core.config

import org.dexpace.sdk.core.generics.Builder
import java.util.function.Function

/**
* Builder for [Configuration]. Use [put] to add explicit overrides (which win over env vars and
* 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.toBuilder] /
* [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<Configuration> {
private val overrides = mutableMapOf<String, String>()
private var envSource: Function<String, String?> = Function { name -> System.getenv(name) }
private var propsSource: Function<String, String?> = 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.toBuilder] 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
Expand All @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -529,4 +529,85 @@ class ConfigurationTest {
method.invoke(null, custom)
assertEquals("hit", Configuration.getGlobalConfiguration().get("BRIDGE"))
}

// ----- withOptions / toBuilder (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 `toBuilder prefills overrides and sources and is independent of the source`() {
val base = ConfigurationBuilder().put("MAX_RETRY_ATTEMPTS", "3").build()
val builder = base.toBuilder()
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.toBuilder().put("A", "1").build()
val second = base.toBuilder().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"))
}
}
Loading