Skip to content

Commit b9e4402

Browse files
authored
feat: add a checkRequired builder helper for uniform required-field validation (#91)
PR: #91
1 parent 6ce5377 commit b9e4402

6 files changed

Lines changed: 99 additions & 7 deletions

File tree

sdk-core/api/sdk-core.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ public abstract interface class org/dexpace/sdk/core/generics/Builder {
5656
public abstract fun build ()Ljava/lang/Object;
5757
}
5858

59+
public final class org/dexpace/sdk/core/generics/BuilderChecksKt {
60+
public static final fun checkRequired (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;
61+
}
62+
5963
public final class org/dexpace/sdk/core/http/auth/AuthChallengeParser {
6064
public static final field INSTANCE Lorg/dexpace/sdk/core/http/auth/AuthChallengeParser;
6165
public static final fun parse (Ljava/lang/String;)Ljava/util/List;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright (c) 2026 dexpace and Omar Aljarrah
3+
*
4+
* Licensed under the MIT License. See LICENSE in the project root.
5+
* SPDX-License-Identifier: MIT
6+
*/
7+
8+
package org.dexpace.sdk.core.generics
9+
10+
/**
11+
* Asserts that a required builder field [value] has been set, returning it non-null.
12+
*
13+
* Builders that materialize an immutable value in [Builder.build] use this to validate
14+
* mandatory inputs in one uniform place. When [value] is `null` it throws
15+
* [IllegalStateException] with the message `"<name> is required"`, where [name] is the
16+
* caller-supplied field name (typically the builder property, e.g. `"method"`); when
17+
* [value] is present it is returned as a non-null reference so the result can be assigned
18+
* directly:
19+
*
20+
* ```
21+
* override fun build(): Request =
22+
* Request(
23+
* method = checkRequired("method", method),
24+
* url = checkRequired("url", url),
25+
* // ...
26+
* )
27+
* ```
28+
*
29+
* Replacing the ad-hoc `checkNotNull(field) { "Field is required." }` calls scattered
30+
* across builders with this helper keeps the missing-field diagnostics identical, which
31+
* matters most once builders are emitted by codegen.
32+
*
33+
* @param name The field name to report in the failure message.
34+
* @param value The field value to validate.
35+
* @return [value], guaranteed non-null.
36+
* @throws IllegalStateException If [value] is `null`.
37+
*/
38+
public fun <T : Any> checkRequired(
39+
name: String,
40+
value: T?,
41+
): T = checkNotNull(value) { "$name is required" }

sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/request/Request.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package org.dexpace.sdk.core.http.request
99

1010
import org.dexpace.sdk.core.generics.Builder
11+
import org.dexpace.sdk.core.generics.checkRequired
1112
import org.dexpace.sdk.core.http.common.Headers
1213
import org.dexpace.sdk.core.http.common.HttpHeaderName
1314
import java.net.MalformedURLException
@@ -251,8 +252,8 @@ public data class Request private constructor(
251252
*/
252253
override fun build(): Request =
253254
Request(
254-
method = checkNotNull(method) { "Method is required." },
255-
url = checkNotNull(url) { "URL is required." },
255+
method = checkRequired("method", method),
256+
url = checkRequired("url", url),
256257
headers = headersBuilder.build(),
257258
body = body,
258259
)

sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/response/Response.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package org.dexpace.sdk.core.http.response
99

1010
import org.dexpace.sdk.core.generics.Builder
11+
import org.dexpace.sdk.core.generics.checkRequired
1112
import org.dexpace.sdk.core.http.common.Headers
1213
import org.dexpace.sdk.core.http.common.HttpHeaderName
1314
import org.dexpace.sdk.core.http.common.Protocol
@@ -252,9 +253,9 @@ public data class Response private constructor(
252253
*/
253254
override fun build(): Response =
254255
Response(
255-
request = checkNotNull(request) { "request is required" },
256-
protocol = checkNotNull(protocol) { "protocol is required" },
257-
status = checkNotNull(status) { "status is required" },
256+
request = checkRequired("request", request),
257+
protocol = checkRequired("protocol", protocol),
258+
status = checkRequired("status", status),
258259
message = message,
259260
headers = headersBuilder.build(),
260261
body = body,
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright (c) 2026 dexpace and Omar Aljarrah
3+
*
4+
* Licensed under the MIT License. See LICENSE in the project root.
5+
* SPDX-License-Identifier: MIT
6+
*/
7+
8+
package org.dexpace.sdk.core.generics
9+
10+
import kotlin.test.Test
11+
import kotlin.test.assertEquals
12+
import kotlin.test.assertFailsWith
13+
import kotlin.test.assertSame
14+
15+
class BuilderChecksTest {
16+
@Test
17+
fun `returns the value when present`() {
18+
val value = "GET"
19+
assertEquals(value, checkRequired("method", value))
20+
}
21+
22+
@Test
23+
fun `returns the same instance when present`() {
24+
val value = listOf(1, 2, 3)
25+
assertSame(value, checkRequired("items", value))
26+
}
27+
28+
@Test
29+
fun `throws IllegalStateException naming the field when value is null`() {
30+
val ex =
31+
assertFailsWith<IllegalStateException> {
32+
checkRequired<String>("method", null)
33+
}
34+
assertEquals("method is required", ex.message)
35+
}
36+
37+
@Test
38+
fun `message uses the supplied field name verbatim`() {
39+
val ex =
40+
assertFailsWith<IllegalStateException> {
41+
checkRequired<Int>("status", null)
42+
}
43+
assertEquals("status is required", ex.message)
44+
}
45+
}

sdk-core/src/test/kotlin/org/dexpace/sdk/core/http/request/RequestTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ class RequestTest {
207207
assertFailsWith<IllegalStateException> {
208208
Request.builder().url("https://example.test").build()
209209
}
210-
assertEquals("Method is required.", ex.message)
210+
assertEquals("method is required", ex.message)
211211
}
212212

213213
@Test
@@ -216,7 +216,7 @@ class RequestTest {
216216
assertFailsWith<IllegalStateException> {
217217
Request.builder().method(Method.GET).build()
218218
}
219-
assertEquals("URL is required.", ex.message)
219+
assertEquals("url is required", ex.message)
220220
}
221221

222222
// ---------------------------------------------------------------------

0 commit comments

Comments
 (0)