Skip to content

Commit d7c7027

Browse files
committed
Merge branch 'main' into feat/async-pipeline-steps
2 parents b4c9dcc + 7982393 commit d7c7027

54 files changed

Lines changed: 2920 additions & 260 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ allprojects {
142142

143143
dependencies {
144144
add("compileOnly", libs.slf4j.api)
145+
// Modern nullability annotations on the compile classpath. kotlin-stdlib pins the ancient
146+
// org.jetbrains:annotations:13.0 transitively, which lacks @UnknownNullability; Kotlin 2.4.0
147+
// tooling materialises inferred platform types with that annotation and reports it as
148+
// inaccessible against 13.0. compileOnly keeps it out of the published POM and ABI.
149+
add("compileOnly", libs.jetbrains.annotations)
145150
}
146151
}
147152

gradle/libs.versions.toml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
[versions]
2-
kotlin = "2.3.21"
2+
kotlin = "2.4.0"
33
kotlinx-coroutines = "1.11.0"
44
slf4j = "2.0.18"
5+
# Compile-only nullability annotations (@NotNull/@Nullable/@UnknownNullability). kotlin-stdlib drags
6+
# in the ancient 13.0 transitively, which predates UnknownNullability; Kotlin 2.4.0 tooling materialises
7+
# inferred platform types with that annotation, so a modern version must be on the compile classpath.
8+
jetbrains-annotations = "26.0.2"
59
okio = "3.17.0"
610
okhttp = "5.0.0"
711
mockwebserver = "5.0.0"
@@ -10,15 +14,17 @@ netty = "4.2.13.Final"
1014
jackson = "2.18.2"
1115
junit-jupiter = "5.10.2"
1216
# R8 is used only by the test-only sdk-shrink-test module to verify the SDK survives consumer-side
13-
# shrinking. It is fetched from Google's Maven repo and never enters a published artifact.
14-
r8 = "8.9.35"
17+
# shrinking. It is fetched from Google's Maven repo and never enters a published artifact. Kept current
18+
# with the Kotlin bump so R8 can parse the metadata version emitted by the Kotlin compiler.
19+
r8 = "9.1.31"
1520
kover = "0.9.8"
1621
binary-compatibility-validator = "0.16.3"
1722
ktlint-plugin = "12.1.1"
1823
detekt = "1.23.6"
1924

2025
[libraries]
2126
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
27+
jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" }
2228
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
2329
slf4j-nop = { module = "org.slf4j:slf4j-nop", version.ref = "slf4j" }
2430
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }

sdk-async-virtualthreads/src/main/kotlin/org/dexpace/sdk/async/virtualthreads/MdcAwareExecutor.kt

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,32 +44,36 @@ import java.util.concurrent.TimeUnit
4444
* [VirtualThreadAsyncHttpClient] for the `close()` path so shutdown semantics are unchanged.
4545
*/
4646
internal class MdcAwareExecutor(private val delegate: ExecutorService) : ExecutorService by delegate {
47+
private fun MdcSnapshot.wrap(command: Runnable): Runnable = Runnable { withMdc { command.run() } }
48+
49+
private fun <T> MdcSnapshot.wrap(task: Callable<T>): Callable<T> = Callable { withMdc { task.call() } }
50+
4751
override fun execute(command: Runnable) {
4852
val snapshot = MdcSnapshot.capture()
49-
delegate.execute { snapshot.withMdc { command.run() } }
53+
delegate.execute(snapshot.wrap(command))
5054
}
5155

5256
override fun <T : Any?> submit(task: Callable<T>): Future<T> {
5357
val snapshot = MdcSnapshot.capture()
54-
return delegate.submit(Callable { snapshot.withMdc { task.call() } })
58+
return delegate.submit(snapshot.wrap(task))
5559
}
5660

5761
override fun submit(task: Runnable): Future<*> {
5862
val snapshot = MdcSnapshot.capture()
59-
return delegate.submit { snapshot.withMdc { task.run() } }
63+
return delegate.submit(snapshot.wrap(task))
6064
}
6165

6266
override fun <T : Any?> submit(
6367
task: Runnable,
6468
result: T,
6569
): Future<T> {
6670
val snapshot = MdcSnapshot.capture()
67-
return delegate.submit({ snapshot.withMdc { task.run() } }, result)
71+
return delegate.submit(snapshot.wrap(task), result)
6872
}
6973

7074
override fun <T : Any?> invokeAll(tasks: MutableCollection<out Callable<T>>): MutableList<Future<T>> {
7175
val snapshot = MdcSnapshot.capture()
72-
return delegate.invokeAll(tasks.map { task -> Callable { snapshot.withMdc { task.call() } } })
76+
return delegate.invokeAll(tasks.map { task -> snapshot.wrap(task) })
7377
}
7478

7579
override fun <T : Any?> invokeAll(
@@ -78,12 +82,12 @@ internal class MdcAwareExecutor(private val delegate: ExecutorService) : Executo
7882
unit: TimeUnit,
7983
): MutableList<Future<T>> {
8084
val snapshot = MdcSnapshot.capture()
81-
return delegate.invokeAll(tasks.map { task -> Callable { snapshot.withMdc { task.call() } } }, timeout, unit)
85+
return delegate.invokeAll(tasks.map { task -> snapshot.wrap(task) }, timeout, unit)
8286
}
8387

8488
override fun <T : Any?> invokeAny(tasks: MutableCollection<out Callable<T>>): T {
8589
val snapshot = MdcSnapshot.capture()
86-
return delegate.invokeAny(tasks.map { task -> Callable { snapshot.withMdc { task.call() } } })
90+
return delegate.invokeAny(tasks.map { task -> snapshot.wrap(task) })
8791
}
8892

8993
override fun <T : Any?> invokeAny(
@@ -92,6 +96,6 @@ internal class MdcAwareExecutor(private val delegate: ExecutorService) : Executo
9296
unit: TimeUnit,
9397
): T {
9498
val snapshot = MdcSnapshot.capture()
95-
return delegate.invokeAny(tasks.map { task -> Callable { snapshot.withMdc { task.call() } } }, timeout, unit)
99+
return delegate.invokeAny(tasks.map { task -> snapshot.wrap(task) }, timeout, unit)
96100
}
97101
}

sdk-core/api/sdk-core.api

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public final class org/dexpace/sdk/core/config/Configuration {
2828
public static final field LOG_LEVEL Ljava/lang/String;
2929
public static final field MAX_RETRY_ATTEMPTS Ljava/lang/String;
3030
public static final field NO_PROXY Ljava/lang/String;
31+
public static final fun builder ()Lorg/dexpace/sdk/core/config/ConfigurationBuilder;
32+
public final fun derive (Ljava/util/function/Consumer;)Lorg/dexpace/sdk/core/config/Configuration;
3133
public final fun get (Ljava/lang/String;)Ljava/lang/String;
3234
public final fun get (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
3335
public static synthetic fun get$default (Lorg/dexpace/sdk/core/config/Configuration;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/String;
@@ -36,20 +38,25 @@ public final class org/dexpace/sdk/core/config/Configuration {
3638
public static final fun getGlobalConfiguration ()Lorg/dexpace/sdk/core/config/Configuration;
3739
public final fun getInt (Ljava/lang/String;I)I
3840
public final fun getProperty (Ljava/lang/String;)Ljava/lang/String;
41+
public final fun newBuilder ()Lorg/dexpace/sdk/core/config/ConfigurationBuilder;
3942
public static final fun setGlobalConfiguration (Lorg/dexpace/sdk/core/config/Configuration;)V
4043
}
4144

4245
public final class org/dexpace/sdk/core/config/Configuration$Companion {
46+
public final fun builder ()Lorg/dexpace/sdk/core/config/ConfigurationBuilder;
4347
public final fun getGlobalConfiguration ()Lorg/dexpace/sdk/core/config/Configuration;
4448
public final fun setGlobalConfiguration (Lorg/dexpace/sdk/core/config/Configuration;)V
4549
}
4650

47-
public final class org/dexpace/sdk/core/config/ConfigurationBuilder {
51+
public final class org/dexpace/sdk/core/config/ConfigurationBuilder : org/dexpace/sdk/core/generics/Builder {
4852
public fun <init> ()V
49-
public final fun build ()Lorg/dexpace/sdk/core/config/Configuration;
53+
public fun <init> (Lorg/dexpace/sdk/core/config/Configuration;)V
54+
public synthetic fun build ()Ljava/lang/Object;
55+
public fun build ()Lorg/dexpace/sdk/core/config/Configuration;
5056
public final fun envSource (Ljava/util/function/Function;)Lorg/dexpace/sdk/core/config/ConfigurationBuilder;
5157
public final fun propsSource (Ljava/util/function/Function;)Lorg/dexpace/sdk/core/config/ConfigurationBuilder;
5258
public final fun put (Ljava/lang/String;Ljava/lang/String;)Lorg/dexpace/sdk/core/config/ConfigurationBuilder;
59+
public final fun remove (Ljava/lang/String;)Lorg/dexpace/sdk/core/config/ConfigurationBuilder;
5360
}
5461

5562
public abstract interface class org/dexpace/sdk/core/generics/Builder {
@@ -1067,6 +1074,7 @@ public final class org/dexpace/sdk/core/http/request/Method : java/lang/Enum {
10671074
public static final field TRACE Lorg/dexpace/sdk/core/http/request/Method;
10681075
public static fun getEntries ()Lkotlin/enums/EnumEntries;
10691076
public final fun getMethod ()Ljava/lang/String;
1077+
public final fun getPermitsRequestBody ()Z
10701078
public fun toString ()Ljava/lang/String;
10711079
public static fun valueOf (Ljava/lang/String;)Lorg/dexpace/sdk/core/http/request/Method;
10721080
public static fun values ()[Lorg/dexpace/sdk/core/http/request/Method;
@@ -2018,6 +2026,16 @@ public abstract interface class org/dexpace/sdk/core/io/Source : java/io/Closeab
20182026
public abstract fun read (Lorg/dexpace/sdk/core/io/Buffer;J)J
20192027
}
20202028

2029+
public final class org/dexpace/sdk/core/pagination/AsyncPaginator {
2030+
public fun <init> (Lorg/dexpace/sdk/core/client/AsyncHttpClient;Lorg/dexpace/sdk/core/http/request/Request;Lorg/dexpace/sdk/core/pagination/PaginationStrategy;)V
2031+
public fun <init> (Lorg/dexpace/sdk/core/client/AsyncHttpClient;Lorg/dexpace/sdk/core/http/request/Request;Lorg/dexpace/sdk/core/pagination/PaginationStrategy;J)V
2032+
public synthetic fun <init> (Lorg/dexpace/sdk/core/client/AsyncHttpClient;Lorg/dexpace/sdk/core/http/request/Request;Lorg/dexpace/sdk/core/pagination/PaginationStrategy;JILkotlin/jvm/internal/DefaultConstructorMarker;)V
2033+
public final fun collectAllAsync ()Ljava/util/concurrent/CompletableFuture;
2034+
public final fun collectAllAsync (Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;
2035+
public final fun forEachAsync (Ljava/util/function/Consumer;)Ljava/util/concurrent/CompletableFuture;
2036+
public final fun forEachAsync (Ljava/util/function/Consumer;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;
2037+
}
2038+
20212039
public final class org/dexpace/sdk/core/pagination/CursorPaginationStrategy : org/dexpace/sdk/core/pagination/PaginationStrategy {
20222040
public fun <init> (Lkotlin/jvm/functions/Function1;)V
20232041
public fun <init> (Lkotlin/jvm/functions/Function1;Ljava/lang/String;)V
@@ -2459,3 +2477,9 @@ public final class org/dexpace/sdk/core/util/SdkInfo {
24592477
public final fun getSdkVersion ()Ljava/lang/String;
24602478
}
24612479

2480+
public final class org/dexpace/sdk/core/util/ValueEquality {
2481+
public static final field INSTANCE Lorg/dexpace/sdk/core/util/ValueEquality;
2482+
public static final fun contentEquals (Ljava/lang/Object;Ljava/lang/Object;)Z
2483+
public static final fun contentHashCode (Ljava/lang/Object;)I
2484+
}
2485+

sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package org.dexpace.sdk.core.config
99

1010
import java.time.Duration
1111
import java.util.Locale
12+
import java.util.function.Consumer
1213
import java.util.function.Function
1314

1415
/**
@@ -21,18 +22,64 @@ import java.util.function.Function
2122
* Typed accessors (`getInt`, `getBoolean`, `getDuration`) return the provided default on parse failures —
2223
* configuration issues never throw at the lookup site.
2324
*
24-
* Constructed via [ConfigurationBuilder].
25+
* Constructed via [ConfigurationBuilder]; derive a reconfigured copy of an existing instance with
26+
* [derive] or [newBuilder].
27+
*
28+
* ## Deriving a reconfigured copy (copy-on-write)
29+
* [derive] returns a **new** immutable [Configuration] with a mutator applied on top of this
30+
* one, leaving the receiver untouched:
31+
*
32+
* ```java
33+
* Configuration base = Configuration.builder().put("MAX_RETRY_ATTEMPTS", "3").build();
34+
* Configuration derived = base.derive(b -> b.put("LOG_LEVEL", "DEBUG"));
35+
* // base is unchanged; derived carries both overrides.
36+
* ```
37+
*
38+
* The derivation is copy-on-write in the value sense: the override map is copied so the original and
39+
* the derived instance never alias the same mutable state, while the [envSource]/[propsSource]
40+
* lookup functions are shared by reference (they are pure read seams and are never mutated). A
41+
* mutator that touches no override and replaces no source produces an independent instance equal in
42+
* behaviour to the original. [newBuilder] exposes the same prefilled builder for callers that prefer
43+
* to thread it through other builder-folding code before [ConfigurationBuilder.build].
2544
*
2645
* ## Thread-safety
2746
* Instances are immutable once built (the override map is copied) and safe to share across threads.
2847
* The process-wide global slot is published via `@Volatile`; readers observe the most-recently-set
2948
* configuration under last-write-wins semantics.
3049
*/
3150
public class Configuration internal constructor(
32-
private val overrides: Map<String, String>,
33-
private val envSource: Function<String, String?> = Function { name -> System.getenv(name) },
34-
private val propsSource: Function<String, String?> = Function { name -> System.getProperty(name) },
51+
@get:JvmSynthetic
52+
internal val overrides: Map<String, String>,
53+
@get:JvmSynthetic
54+
internal val envSource: Function<String, String?> = Function { name -> System.getenv(name) },
55+
@get:JvmSynthetic
56+
internal val propsSource: Function<String, String?> = Function { name -> System.getProperty(name) },
3557
) {
58+
/**
59+
* Returns a fresh [ConfigurationBuilder] preloaded with this instance's overrides and lookup
60+
* sources. Mutating the returned builder never affects this [Configuration]; the override map is
61+
* copied up front. Use this when you want to thread the builder through other configuration code
62+
* before calling [ConfigurationBuilder.build]; prefer [derive] for the common
63+
* derive-in-one-call case.
64+
*/
65+
public fun newBuilder(): ConfigurationBuilder = ConfigurationBuilder(this)
66+
67+
/**
68+
* Derive a new immutable [Configuration] by applying [mutator] to a builder prefilled from this
69+
* instance, then building it. This [Configuration] is left unchanged (copy-on-write): the
70+
* override map is copied before [mutator] runs, so overrides added, replaced, or removed inside
71+
* [mutator] never leak back into the receiver. The env/property lookup seams are inherited by
72+
* reference.
73+
*
74+
* Kotlin's compiler-generated non-null parameter check raises `NullPointerException` when a Java
75+
* caller passes `null` for [mutator], so no explicit guard is needed here.
76+
*/
77+
public fun derive(mutator: Consumer<ConfigurationBuilder>): Configuration {
78+
val builder = newBuilder()
79+
mutator.accept(builder)
80+
return builder.build()
81+
}
82+
3683
/**
3784
* Look up a configuration value by [name].
3885
*
@@ -95,6 +142,14 @@ public class Configuration internal constructor(
95142
): Duration = get(name)?.let { parseDuration(it) } ?: default
96143

97144
public companion object {
145+
/**
146+
* Returns a fresh empty [ConfigurationBuilder]. Java-friendly entry point matching the
147+
* `builder()` idiom every other SDK model exposes; build from scratch with this, or derive a
148+
* reconfigured copy of an existing instance with [newBuilder] / [derive].
149+
*/
150+
@JvmStatic
151+
public fun builder(): ConfigurationBuilder = ConfigurationBuilder()
152+
98153
// Well-known keys. `const val` so callers reference them as `Configuration.MAX_RETRY_ATTEMPTS`
99154
// from both Kotlin and Java without going through `Companion`.
100155

@@ -147,7 +202,7 @@ public class Configuration internal constructor(
147202
// ISO-8601 path: `PT5S`, `P1D`, etc. Reject negative durations (e.g. `PT-5S`) for the
148203
// same reason the shorthand path does below — downstream consumers (Clock.sleep,
149204
// Futures.delay) assume a non-negative duration and throw on a negative one.
150-
if (Character.toUpperCase(raw[0]) == 'P') {
205+
if (raw[0].uppercaseChar() == 'P') {
151206
return try {
152207
val d = Duration.parse(raw)
153208
if (d.isNegative) null else d

sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/ConfigurationBuilder.kt

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,41 @@
77

88
package org.dexpace.sdk.core.config
99

10+
import org.dexpace.sdk.core.generics.Builder
1011
import java.util.function.Function
1112

1213
/**
1314
* Builder for [Configuration]. Use [put] to add explicit overrides (which win over env vars and
1415
* system properties), and [envSource] / [propsSource] as test seams to substitute the env / property
1516
* lookups with hermetic functions.
1617
*
18+
* Implements the generic [Builder] contract so it can be folded by builder-style configuration code.
19+
* Construct empty for a configuration built from scratch, or via [Configuration.newBuilder] /
20+
* [Configuration.derive] to derive a reconfigured copy of an existing [Configuration].
21+
*
1722
* ## Thread-safety
1823
* Builders are *not* thread-safe — construct, configure, and [build] from a single thread. The
1924
* resulting [Configuration] is immutable and safe to share.
2025
*/
21-
public class ConfigurationBuilder {
26+
public class ConfigurationBuilder : Builder<Configuration> {
2227
private val overrides = mutableMapOf<String, String>()
2328
private var envSource: Function<String, String?> = Function { name -> System.getenv(name) }
2429
private var propsSource: Function<String, String?> = Function { name -> System.getProperty(name) }
2530

31+
/** Creates an empty builder with the default env / system-property lookup sources. */
32+
public constructor()
33+
34+
/**
35+
* Creates a builder preloaded with [source]'s overrides and lookup sources. The override map is
36+
* copied, so mutating this builder never affects [source]. Used by [Configuration.newBuilder] and
37+
* [Configuration.derive].
38+
*/
39+
public constructor(source: Configuration) {
40+
overrides.putAll(source.overrides)
41+
envSource = source.envSource
42+
propsSource = source.propsSource
43+
}
44+
2645
/**
2746
* Register an explicit override. Overrides win over every other layer. Kotlin's
2847
* compiler-generated non-null parameter check raises `NullPointerException` when a Java
@@ -36,6 +55,22 @@ public class ConfigurationBuilder {
3655
overrides[name] = value
3756
}
3857

58+
/**
59+
* Remove the explicit override for [name], if one is set. This drops only the override layer:
60+
* a later [Configuration.get] for [name] falls through to the environment-variable and
61+
* system-property seams (and finally the caller's default) exactly as if the override had never
62+
* been registered — it does **not** force the key to resolve to `null`. Removing a key that
63+
* carries no override is a no-op. As the inverse of [put], this is what lets
64+
* [Configuration.derive] un-pin an override inherited from the source instance.
65+
*
66+
* Kotlin's compiler-generated non-null parameter check raises `NullPointerException` when a
67+
* Java caller passes `null` for [name], so no explicit guard is needed here.
68+
*/
69+
public fun remove(name: String): ConfigurationBuilder =
70+
apply {
71+
overrides.remove(name)
72+
}
73+
3974
/** Test seam: override the environment-variable source. */
4075
public fun envSource(source: Function<String, String?>): ConfigurationBuilder = apply { envSource = source }
4176

@@ -46,5 +81,5 @@ public class ConfigurationBuilder {
4681
* Materialize the immutable [Configuration]. The current override map is defensively copied so
4782
* subsequent [put] calls on this builder do not mutate the returned configuration.
4883
*/
49-
public fun build(): Configuration = Configuration(overrides.toMap(), envSource, propsSource)
84+
override fun build(): Configuration = Configuration(overrides.toMap(), envSource, propsSource)
5085
}

sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/auth/AuthChallengeParser.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -290,15 +290,15 @@ public object AuthChallengeParser {
290290
}
291291
}
292292

293+
private val TOKEN_PUNCTUATION: Set<Char> = "!#$%&'*+-.^_`|~".toSet()
294+
295+
private val TOKEN68_PUNCTUATION: Set<Char> = "-._~+/".toSet()
296+
293297
/** RFC 7230 token char: ALPHA / DIGIT / one of the punctuation set. */
294298
private fun isTokenChar(c: Char): Boolean =
295-
(c in 'a'..'z') || (c in 'A'..'Z') || (c in '0'..'9') ||
296-
c == '!' || c == '#' || c == '$' || c == '%' || c == '&' ||
297-
c == '\'' || c == '*' || c == '+' || c == '-' || c == '.' ||
298-
c == '^' || c == '_' || c == '`' || c == '|' || c == '~'
299+
(c in 'a'..'z') || (c in 'A'..'Z') || (c in '0'..'9') || c in TOKEN_PUNCTUATION
299300

300301
/** RFC 7235 token68 char (excluding the trailing "=" pad, handled separately). */
301302
private fun isToken68Char(c: Char): Boolean =
302-
(c in 'a'..'z') || (c in 'A'..'Z') || (c in '0'..'9') ||
303-
c == '-' || c == '.' || c == '_' || c == '~' || c == '+' || c == '/'
303+
(c in 'a'..'z') || (c in 'A'..'Z') || (c in '0'..'9') || c in TOKEN68_PUNCTUATION
304304
}

0 commit comments

Comments
 (0)