Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.asyncapi.kotlinasyncapi.context.service

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import com.asyncapi.kotlinasyncapi.model.AsyncApi
import kotlin.script.experimental.host.toScriptSource
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

internal class AsyncApiExtensionTest {

Expand Down Expand Up @@ -34,6 +34,7 @@ internal class AsyncApiExtensionTest {
title("titleValue")
version("versionValue")
}
channels { }
}
)

Expand Down Expand Up @@ -69,6 +70,10 @@ internal class AsyncApiExtensionTest {
}
val actual = extension.extend(
AsyncApi.asyncApi {
info {
title("titleValue")
version("versionValue")
}
Comment on lines +73 to +76
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is actually expected that we build a partial asyncapi spec here. the context module is a core feature of the library which lets users build asnycapi documentation from multiple sources. for example you can define the info in code and extend it with the channels from annotation processors.

My initial idea does not seem to work since we need the ability to build partial asyncapi in order to extend it with context providers.

So our only option is to introduce a separate validation step before we serialise it.

channels {
channel("oldChannelKey") {
description("oldDescriptionValue")
Expand Down Expand Up @@ -109,6 +114,7 @@ internal class AsyncApiExtensionTest {
title("titleValue")
version("versionValue")
}
channels { }
}
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.asyncapi.kotlinasyncapi.model.channel.ReferencableChannelsMap
import com.asyncapi.kotlinasyncapi.model.component.Components
import com.asyncapi.kotlinasyncapi.model.info.Info
import com.asyncapi.kotlinasyncapi.model.server.ReferencableServersMap
import com.asyncapi.kotlinasyncapi.util.checkInitialized

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

inline fun info(build: Info.() -> Unit): Info =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you remove the inline modifier?

fun info(build: Info.() -> Unit): Info =
Info().apply(build).also { info = it }

inline fun servers(build: ReferencableServersMap.() -> Unit): ReferencableServersMap =
fun servers(build: ReferencableServersMap.() -> Unit): ReferencableServersMap =
ReferencableServersMap().apply(build).also { servers = it }

inline fun channels(build: ReferencableChannelsMap.() -> Unit): ReferencableChannelsMap =
fun channels(build: ReferencableChannelsMap.() -> Unit): ReferencableChannelsMap =
ReferencableChannelsMap().apply(build).also { channels = it }

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

inline fun components(build: Components.() -> Unit): Components =
fun components(build: Components.() -> Unit): Components =
Components().apply(build).also { components = it }

inline fun tags(build: TagsList.() -> Unit): TagsList =
fun tags(build: TagsList.() -> Unit): TagsList =
TagsList().apply(build).also { tags = it }

inline fun externalDocs(build: ExternalDocumentation.() -> Unit): ExternalDocumentation =
fun externalDocs(build: ExternalDocumentation.() -> Unit): ExternalDocumentation =
ExternalDocumentation().apply(build).also { externalDocs = it }

internal fun validateForSerialization() {
checkInitialized(
"info" to { info },
"channels" to { channels }
)
}
Comment on lines +49 to +54
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n 'validateForSerialization' --type=kotlin -C3

Repository: asyncapi/kotlin-asyncapi

Length of output: 1824


🏁 Script executed:

# Look at the full test context
sed -n '45,70p' kotlin-asyncapi-core/src/test/kotlin/com/asyncapi/kotlinasyncapi/model/AsyncApiTest.kt

# Search for any serialization implementations or hooks
rg -n 'KSerializer|@Serializable|toJson|serialize' --type=kotlin kotlin-asyncapi-core/src/main/kotlin/com/asyncapi/kotlinasyncapi/model/AsyncApi.kt

# Check if validateForSerialization is called elsewhere outside tests
rg -n 'validateForSerialization' --type=kotlin -C2 | grep -v "Test.kt"

Repository: asyncapi/kotlin-asyncapi

Length of output: 1458


🏁 Script executed:

# Look for TestUtils.json implementation to see if it calls validateForSerialization
fd -t f -name 'TestUtils.kt' kotlin-asyncapi-core/src/test

# Search for any toJson, json, or serialization methods in AsyncApi
rg -n 'toJson|json|serialize' --type=kotlin kotlin-asyncapi-core/src/main/kotlin/com/asyncapi/kotlinasyncapi/model/AsyncApi.kt

# Check if there's a custom serializer defined
rg -n 'KSerializer|@Serializable' --type=kotlin kotlin-asyncapi-core/src/main/kotlin/com/asyncapi/kotlinasyncapi/model/

Repository: asyncapi/kotlin-asyncapi

Length of output: 298


🏁 Script executed:

# Find TestUtils.kt file
fd -t f -name 'TestUtils.kt' kotlin-asyncapi-core/src/test

# Search for json method definition and validateForSerialization calls
rg 'fun json|TestUtils.json' --type=kotlin -A5 -B2

Repository: asyncapi/kotlin-asyncapi

Length of output: 48147


🏁 Script executed:

# Look for any custom Jackson serializer for AsyncApi
rg 'AsyncApi' --type=kotlin | grep -i 'serializer\|json\|deserializer\|custom' | head -20

# Check if there's a `@JsonSerialize` annotation on AsyncApi class
sed -n '1,70p' kotlin-asyncapi-core/src/main/kotlin/com/asyncapi/kotlinasyncapi/model/AsyncApi.kt

Repository: asyncapi/kotlin-asyncapi

Length of output: 5463


🏁 Script executed:

# Find and examine the DefaultAsyncApiSerializer implementation
fd -type f -name 'DefaultAsyncApiSerializer.kt'

# Also check the AsyncApiSerializer interface
rg 'interface AsyncApiSerializer' --type=kotlin -A10

Repository: asyncapi/kotlin-asyncapi

Length of output: 1640


🏁 Script executed:

# Read the full DefaultAsyncApiSerializer implementation
cat kotlin-asyncapi-context/src/main/kotlin/com/asyncapi/kotlinasyncapi/context/service/AsyncApiSerializer.kt

Repository: asyncapi/kotlin-asyncapi

Length of output: 748


validateForSerialization() is never invoked during actual serialization.

The method is defined but DefaultAsyncApiSerializer.serialize() only calls objectMapper.writeValueAsString(this) without invoking validateForSerialization(). The DSL entry point asyncApi() also does not call it. As a result, users can create and serialize invalid AsyncApi instances (missing required info or channels fields) without any error, making the validation dead code.

🤖 Prompt for AI Agents
In
`@kotlin-asyncapi-core/src/main/kotlin/com/asyncapi/kotlinasyncapi/model/AsyncApi.kt`
around lines 49 - 54, validateForSerialization() is never called, so invalid
AsyncApi instances can be serialized; call validateForSerialization() before
actual serialization to enforce required fields. Update
DefaultAsyncApiSerializer.serialize() to invoke
AsyncApi.validateForSerialization() (or a public wrapper) immediately before
objectMapper.writeValueAsString(this), and/or ensure the asyncApi() DSL
entrypoint triggers the same validation at build/return time so any missing
"info" or "channels" causes an error rather than producing invalid JSON. Ensure
you reference the existing validateForSerialization() method on AsyncApi when
adding the call.


companion object {
const val VERSION = "2.4.0"

inline fun asyncApi(build: AsyncApi.() -> Unit): AsyncApi =
fun asyncApi(build: AsyncApi.() -> Unit): AsyncApi =
AsyncApi().apply(build)
Comment on lines +59 to 60
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

DSL entry point does not enforce validation.

The asyncApi() function builds and returns an AsyncApi without calling validateForSerialization(). Per the PR objectives, the intent is to "fail fast" so invalid documents can't be created. Consider calling the validation here:

Proposed fix
         fun asyncApi(build: AsyncApi.() -> Unit): AsyncApi =
-            AsyncApi().apply(build)
+            AsyncApi().apply(build).also { it.validateForSerialization() }
🤖 Prompt for AI Agents
In
`@kotlin-asyncapi-core/src/main/kotlin/com/asyncapi/kotlinasyncapi/model/AsyncApi.kt`
around lines 59 - 60, The DSL entry function asyncApi(build: AsyncApi.() ->
Unit): AsyncApi should enforce validation before returning; after constructing
and applying the build to AsyncApi() (the AsyncApi() instance returned by
AsyncApi().apply(build)), call the instance's validateForSerialization() (or
equivalent validation method on AsyncApi) and let any validation exceptions
propagate (or throw a descriptive exception) so invalid documents fail fast;
update the asyncApi function to perform this validation step on the built
AsyncApi object prior to returning it.

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.asyncapi.kotlinasyncapi.util

internal fun <T> T.checkInitialized(
vararg checks: Pair<String, T.() -> Unit>
): T = also {
val missing = checks.mapNotNull { (name, check) ->
try {
it.check()
null
} catch (_: UninitializedPropertyAccessException) {
name
}
}

check(missing.isEmpty()) {
"Missing required properties: ${missing.joinToString()}"
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.asyncapi.kotlinasyncapi.model

import com.asyncapi.kotlinasyncapi.model.info.Info
import io.mockk.clearConstructorMockk
import io.mockk.every
import io.mockk.mockkConstructor
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import com.asyncapi.kotlinasyncapi.model.info.Info
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertThrows

internal class AsyncApiTest {

Expand Down Expand Up @@ -43,4 +46,31 @@ internal class AsyncApiTest {

TestUtils.assertJsonEquals(expected, actual)
}

@Test
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After moving the T.checkInitialized to a separate file. You could write a unit test for it, not in the model test.

fun `should fail fast when mandatory fields are missing`() {
val api = AsyncApi.asyncApi {
}

val exception = assertThrows(IllegalStateException::class.java) {
api.validateForSerialization()
}

assertTrue(exception.message!!.contains("Missing required properties"))
assertTrue(exception.message!!.contains("info"))
assertTrue(exception.message!!.contains("channels"))
}

@Test
fun `should succeed when mandatory fields are initialized`() {
val api = AsyncApi.asyncApi {
info {
title = "Test API"
version = "1.0.0"
}
channels { }
}

assertNotNull(api)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.asyncapi.kotlinasyncapi.util

import org.junit.jupiter.api.Assertions.assertDoesNotThrow
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
import java.lang.IllegalStateException

internal class ValidationUtilsTest {

class TestClass {
lateinit var requiredProperty: String
var optionalProperty: String? = null
}

@Test
fun `checkInitialized should pass when required property is initialized`() {
val instance = TestClass().apply {
requiredProperty = "value"
}

assertDoesNotThrow {
instance.checkInitialized(
"requiredProperty" to { requiredProperty }
)
}
}

@Test
fun `checkInitialized should fail when required property is not initialized`() {
val instance = TestClass()

val exception = assertThrows(IllegalStateException::class.java) {
instance.checkInitialized(
"requiredProperty" to { requiredProperty }
)
}

assert(exception.message!!.contains("Missing required properties: requiredProperty"))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

assert() may silently pass when JVM assertions are disabled.

Kotlin's assert() is a no-op unless the JVM is started with -ea. If the test runner doesn't enable assertions, lines 38, 52, and 53 will never actually check anything. Use JUnit's assertTrue instead for reliable test behavior.

Proposed fix
-        assert(exception.message!!.contains("Missing required properties: requiredProperty"))
+        assertTrue(exception.message!!.contains("Missing required properties: requiredProperty"))

And similarly for lines 52–53:

-        assert(exception.message!!.contains("requiredProperty"))
-        assert(exception.message!!.contains("anotherMissing"))
+        assertTrue(exception.message!!.contains("requiredProperty"))
+        assertTrue(exception.message!!.contains("anotherMissing"))

Add the import at the top:

 import org.junit.jupiter.api.Assertions.assertDoesNotThrow
 import org.junit.jupiter.api.Assertions.assertThrows
+import org.junit.jupiter.api.Assertions.assertTrue
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert(exception.message!!.contains("Missing required properties: requiredProperty"))
assertTrue(exception.message!!.contains("Missing required properties: requiredProperty"))
🤖 Prompt for AI Agents
In
`@kotlin-asyncapi-core/src/test/kotlin/com/asyncapi/kotlinasyncapi/util/ValidationUtilsTest.kt`
at line 38, Replace the JVM-dependent Kotlin assert() calls in
ValidationUtilsTest (specifically the assertions at the locations checking
exception.message!!.contains(...), currently at lines referenced around the
checks) with JUnit assertions so the tests run reliably; change each assert(...)
to Assertions.assertTrue(...) (or import assertTrue from
org.junit.jupiter.api.Assertions and call assertTrue(...)) for the checks in the
test methods in ValidationUtilsTest.kt (the exception message contains
assertions around requiredProperty) so they execute regardless of JVM assertion
flags.

}

@Test
fun `checkInitialized should handle multiple checks`() {
val instance = TestClass()

val exception = assertThrows(IllegalStateException::class.java) {
instance.checkInitialized(
"requiredProperty" to { requiredProperty },
"anotherMissing" to { throw UninitializedPropertyAccessException() }
)
}

assert(exception.message!!.contains("requiredProperty"))
assert(exception.message!!.contains("anotherMissing"))
}
}