Skip to content
11 changes: 9 additions & 2 deletions sdk-core/api/sdk-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ 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;
public static synthetic fun get$default (Lorg/dexpace/sdk/core/config/Configuration;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/String;
Expand All @@ -36,20 +38,25 @@ 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 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
}

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;
public final fun remove (Ljava/lang/String;)Lorg/dexpace/sdk/core/config/ConfigurationBuilder;
}

public abstract interface class org/dexpace/sdk/core/generics/Builder {
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 @@ -21,18 +22,64 @@ 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)
* [derive] returns a **new** immutable [Configuration] with a mutator applied on top of this
* one, leaving the receiver untouched:
*
* ```java
* 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.
* ```
*
* 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
* configuration under last-write-wins semantics.
*/
public class Configuration internal constructor(
private val overrides: Map<String, String>,
private val envSource: Function<String, String?> = Function { name -> System.getenv(name) },
private val propsSource: Function<String, String?> = Function { name -> System.getProperty(name) },
@get:JvmSynthetic
internal val overrides: Map<String, String>,
@get:JvmSynthetic
internal val envSource: Function<String, String?> = Function { name -> System.getenv(name) },
@get:JvmSynthetic
internal val propsSource: Function<String, String?> = 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 [derive] 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, 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.
*/
public fun derive(mutator: Consumer<ConfigurationBuilder>): Configuration {
val builder = newBuilder()
mutator.accept(builder)
return builder.build()
}

/**
* Look up a configuration value by [name].
*
Expand Down Expand Up @@ -95,6 +142,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
* `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`.

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.newBuilder] /
* [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
* 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.newBuilder] and
* [Configuration.derive].
*/
public constructor(source: Configuration) {
overrides.putAll(source.overrides)
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 @@ -36,6 +55,22 @@ public class ConfigurationBuilder {
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<String, String?>): ConfigurationBuilder = apply { envSource = source }

Expand All @@ -46,5 +81,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)
}
Loading
Loading