Skip to content

Commit 9ca7fc0

Browse files
committed
refactor: move mandatory field validation to serialization lifecycle
- Remove validation from DSL entry point - Move checkInitialized to internal utility file - Add dedicated utility tests - Introduce validateForSerialization() in AsyncApi
1 parent 24c9bd9 commit 9ca7fc0

File tree

5 files changed

+128
-11
lines changed

5 files changed

+128
-11
lines changed

kotlin-asyncapi-context/src/test/kotlin/com/asyncapi/kotlinasyncapi/context/service/AsyncApiExtensionTest.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package com.asyncapi.kotlinasyncapi.context.service
22

3-
import org.assertj.core.api.Assertions.assertThat
4-
import org.junit.jupiter.api.Test
53
import com.asyncapi.kotlinasyncapi.model.AsyncApi
64
import kotlin.script.experimental.host.toScriptSource
5+
import org.assertj.core.api.Assertions.assertThat
6+
import org.junit.jupiter.api.Test
77

88
internal class AsyncApiExtensionTest {
99

@@ -34,6 +34,7 @@ internal class AsyncApiExtensionTest {
3434
title("titleValue")
3535
version("versionValue")
3636
}
37+
channels { }
3738
}
3839
)
3940

@@ -69,6 +70,10 @@ internal class AsyncApiExtensionTest {
6970
}
7071
val actual = extension.extend(
7172
AsyncApi.asyncApi {
73+
info {
74+
title("titleValue")
75+
version("versionValue")
76+
}
7277
channels {
7378
channel("oldChannelKey") {
7479
description("oldDescriptionValue")
@@ -109,6 +114,7 @@ internal class AsyncApiExtensionTest {
109114
title("titleValue")
110115
version("versionValue")
111116
}
117+
channels { }
112118
}
113119
)
114120

kotlin-asyncapi-core/src/main/kotlin/com/asyncapi/kotlinasyncapi/model/AsyncApi.kt

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.asyncapi.kotlinasyncapi.model.channel.ReferencableChannelsMap
44
import com.asyncapi.kotlinasyncapi.model.component.Components
55
import com.asyncapi.kotlinasyncapi.model.info.Info
66
import com.asyncapi.kotlinasyncapi.model.server.ReferencableServersMap
7+
import com.asyncapi.kotlinasyncapi.util.checkInitialized
78

89
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPEALIAS)
910
@DslMarker
@@ -24,31 +25,38 @@ class AsyncApi {
2425
fun id(value: String): String =
2526
value.also { id = it }
2627

27-
inline fun info(build: Info.() -> Unit): Info =
28+
fun info(build: Info.() -> Unit): Info =
2829
Info().apply(build).also { info = it }
2930

30-
inline fun servers(build: ReferencableServersMap.() -> Unit): ReferencableServersMap =
31+
fun servers(build: ReferencableServersMap.() -> Unit): ReferencableServersMap =
3132
ReferencableServersMap().apply(build).also { servers = it }
3233

33-
inline fun channels(build: ReferencableChannelsMap.() -> Unit): ReferencableChannelsMap =
34+
fun channels(build: ReferencableChannelsMap.() -> Unit): ReferencableChannelsMap =
3435
ReferencableChannelsMap().apply(build).also { channels = it }
3536

3637
fun defaultContentType(value: String): String =
3738
value.also { defaultContentType = it }
3839

39-
inline fun components(build: Components.() -> Unit): Components =
40+
fun components(build: Components.() -> Unit): Components =
4041
Components().apply(build).also { components = it }
4142

42-
inline fun tags(build: TagsList.() -> Unit): TagsList =
43+
fun tags(build: TagsList.() -> Unit): TagsList =
4344
TagsList().apply(build).also { tags = it }
4445

45-
inline fun externalDocs(build: ExternalDocumentation.() -> Unit): ExternalDocumentation =
46+
fun externalDocs(build: ExternalDocumentation.() -> Unit): ExternalDocumentation =
4647
ExternalDocumentation().apply(build).also { externalDocs = it }
4748

49+
internal fun validateForSerialization() {
50+
checkInitialized(
51+
"info" to { info },
52+
"channels" to { channels }
53+
)
54+
}
55+
4856
companion object {
4957
const val VERSION = "2.4.0"
5058

51-
inline fun asyncApi(build: AsyncApi.() -> Unit): AsyncApi =
59+
fun asyncApi(build: AsyncApi.() -> Unit): AsyncApi =
5260
AsyncApi().apply(build)
5361
}
54-
}
62+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.asyncapi.kotlinasyncapi.util
2+
3+
internal fun <T> T.checkInitialized(
4+
vararg checks: Pair<String, T.() -> Unit>
5+
): T = also {
6+
val missing = checks.mapNotNull { (name, check) ->
7+
try {
8+
it.check()
9+
null
10+
} catch (_: UninitializedPropertyAccessException) {
11+
name
12+
}
13+
}
14+
15+
check(missing.isEmpty()) {
16+
"Missing required properties: ${missing.joinToString()}"
17+
}
18+
}

kotlin-asyncapi-core/src/test/kotlin/com/asyncapi/kotlinasyncapi/model/AsyncApiTest.kt

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package com.asyncapi.kotlinasyncapi.model
22

3+
import com.asyncapi.kotlinasyncapi.model.info.Info
34
import io.mockk.clearConstructorMockk
45
import io.mockk.every
56
import io.mockk.mockkConstructor
67
import org.junit.jupiter.api.AfterEach
78
import org.junit.jupiter.api.Test
8-
import com.asyncapi.kotlinasyncapi.model.info.Info
9+
import org.junit.jupiter.api.Assertions.assertTrue
10+
import org.junit.jupiter.api.Assertions.assertNotNull
11+
import org.junit.jupiter.api.Assertions.assertThrows
912

1013
internal class AsyncApiTest {
1114

@@ -43,4 +46,31 @@ internal class AsyncApiTest {
4346

4447
TestUtils.assertJsonEquals(expected, actual)
4548
}
49+
50+
@Test
51+
fun `should fail fast when mandatory fields are missing`() {
52+
val api = AsyncApi.asyncApi {
53+
}
54+
55+
val exception = assertThrows(IllegalStateException::class.java) {
56+
api.validateForSerialization()
57+
}
58+
59+
assertTrue(exception.message!!.contains("Missing required properties"))
60+
assertTrue(exception.message!!.contains("info"))
61+
assertTrue(exception.message!!.contains("channels"))
62+
}
63+
64+
@Test
65+
fun `should succeed when mandatory fields are initialized`() {
66+
val api = AsyncApi.asyncApi {
67+
info {
68+
title = "Test API"
69+
version = "1.0.0"
70+
}
71+
channels { }
72+
}
73+
74+
assertNotNull(api)
75+
}
4676
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.asyncapi.kotlinasyncapi.util
2+
3+
import org.junit.jupiter.api.Assertions.assertDoesNotThrow
4+
import org.junit.jupiter.api.Assertions.assertThrows
5+
import org.junit.jupiter.api.Test
6+
import java.lang.IllegalStateException
7+
8+
internal class ValidationUtilsTest {
9+
10+
class TestClass {
11+
lateinit var requiredProperty: String
12+
var optionalProperty: String? = null
13+
}
14+
15+
@Test
16+
fun `checkInitialized should pass when required property is initialized`() {
17+
val instance = TestClass().apply {
18+
requiredProperty = "value"
19+
}
20+
21+
assertDoesNotThrow {
22+
instance.checkInitialized(
23+
"requiredProperty" to { requiredProperty }
24+
)
25+
}
26+
}
27+
28+
@Test
29+
fun `checkInitialized should fail when required property is not initialized`() {
30+
val instance = TestClass()
31+
32+
val exception = assertThrows(IllegalStateException::class.java) {
33+
instance.checkInitialized(
34+
"requiredProperty" to { requiredProperty }
35+
)
36+
}
37+
38+
assert(exception.message!!.contains("Missing required properties: requiredProperty"))
39+
}
40+
41+
@Test
42+
fun `checkInitialized should handle multiple checks`() {
43+
val instance = TestClass()
44+
45+
val exception = assertThrows(IllegalStateException::class.java) {
46+
instance.checkInitialized(
47+
"requiredProperty" to { requiredProperty },
48+
"anotherMissing" to { throw UninitializedPropertyAccessException() }
49+
)
50+
}
51+
52+
assert(exception.message!!.contains("requiredProperty"))
53+
assert(exception.message!!.contains("anotherMissing"))
54+
}
55+
}

0 commit comments

Comments
 (0)