feat: add copy-on-write derivation to Configuration#161
Merged
Conversation
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<Configuration> and gains a prefilled constructor, bringing it in line with the newBuilder() pattern the other SDK builders (Request, Response, Headers, RequestConditions, ...) already follow.
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.
OmarAlJarrah
added a commit
that referenced
this pull request
Jun 20, 2026
…vation coverage 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.
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.
…tion
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.
…esponseBridge 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Configurationis immutable and built once viaConfigurationBuilder, but there was no supported way to take an existing instance and produce a slightly-different one. Callers wanting "the same configuration, but withLOG_LEVEL=DEBUG" had to reconstruct it from scratch and manually re-thread the env/property lookup seams — easy to get subtly wrong, and impossible for code that only holds a builtConfiguration(it can't see the original overrides or sources).Change
Add copy-on-write derivation to
Configuration, and round out its construction surface:Configuration.derive(Consumer<ConfigurationBuilder>)— derive a newConfigurationby applying a mutator to a builder prefilled from the receiver, then building. The receiver is left untouched. This is the common derive-in-one-call path.Configuration.newBuilder()— returns the same prefilled builder, for callers that prefer to thread it through other builder-folding code beforebuild().Configuration.builder()—@JvmStaticfactory returning a fresh empty builder, matching thebuilder()idiom every other SDK model exposes (Request,Response,Headers,RequestConditions,RetrySettings,ClientIdentityStep,IdempotencyKeyStep) so Java callers no longer have to reach fornew ConfigurationBuilder().ConfigurationBuildernow implementsBuilder<Configuration>and gains a prefilledconstructor(Configuration), bringing it in line with the rest of the SDK's builders.Together these complete the construction surface:
builder()for from-scratch,newBuilder()/derive()for copy-on-write derivation from an existing instance.Semantics are copy-on-write in the value sense: the override map is copied defensively so the original and derived instances never alias the same mutable state, while the
envSource/propsSourcelookup functions are shared by reference (they are pure read seams and are never mutated). A mutator that changes nothing yields an independent instance equal in behaviour to the original.The method is named
derive(rather thanwithOptions) because it names the operation directly — deriving a reconfigured copy — instead of introducing an "options" noun that appears nowhere else in the configuration API.newBuilder()(rather thantoBuilder()) matches the prefilled-builder accessor every other model already exposes.The three derivation read seams (
overrides/envSource/propsSource) are exposed to the builder as@get:JvmSynthetic internal, so the prefilled constructor can read them without leaking a Java-callable accessor onto the public surface.Tests
Cases covering: the
builder()factory returning a fresh empty builder; adding a new override on the derived copy; the original staying unchanged; overriding an existing key without mutating the original; inherited env/property seams, with an explicit override winning over both an inherited property and an inherited env seam; seams shared by reference; source replacement inside the mutator detaching only the derived copy; the empty-mutator independent-but-equivalent case; empty-env skip-to-sysprop semantics surviving derivation; derivation from an override-less base; chained derivation accumulating overrides while each level stays independent; the null-mutatorNullPointerExceptioncontract; and two builder-isolation cases (post-build mutation, and two independent derivations from one base not seeing each other's overrides).API
Public-API additions;
sdk-core.apisnapshot regenerated viaapiDump.Gated build
Result: BUILD SUCCESSFUL.
Closes #60.