diff --git a/kotlin-asyncapi-context/src/main/kotlin/org/openfolder/kotlinasyncapi/context/annotation/AnnotationProvider.kt b/kotlin-asyncapi-context/src/main/kotlin/org/openfolder/kotlinasyncapi/context/annotation/AnnotationProvider.kt index c6f1c2f..42d4511 100644 --- a/kotlin-asyncapi-context/src/main/kotlin/org/openfolder/kotlinasyncapi/context/annotation/AnnotationProvider.kt +++ b/kotlin-asyncapi-context/src/main/kotlin/org/openfolder/kotlinasyncapi/context/annotation/AnnotationProvider.kt @@ -24,6 +24,7 @@ import org.openfolder.kotlinasyncapi.model.server.ReferencableServerVariablesMap import org.openfolder.kotlinasyncapi.model.server.ReferencableServersMap import kotlin.reflect.KClass import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.functions class AnnotationProvider( private val applicationPackage: Package? = null, @@ -31,7 +32,7 @@ class AnnotationProvider( private val scanner: AnnotationScanner, private val messageProcessor: AnnotationProcessor>, private val schemaProcessor: AnnotationProcessor>, - private val channelProcessor: AnnotationProcessor> + private val channelProcessor: AnnotationProcessor ) : AsyncApiContextProvider { private val componentToChannelMapping = mutableMapOf() @@ -67,19 +68,31 @@ class AnnotationProvider( annotatedClasses .flatMap { clazz -> - listOfNotNull( + val classAnnotations = listOfNotNull( clazz.findAnnotation()?.let { clazz to it }, clazz.findAnnotation()?.let { clazz to it }, clazz.findAnnotation()?.let { clazz to it } ) + + val functionAnnotations = clazz.functions.flatMap { function -> + listOfNotNull( + function.findAnnotation()?.let { function to it }, + function.findAnnotation()?.let { function to it }, + function.findAnnotation()?.let { function to it } + ) + } + + classAnnotations + functionAnnotations } - .mapNotNull { (clazz, annotation) -> + .mapNotNull { (element, annotation) -> when (annotation) { - is Message -> messageProcessor.process(annotation, clazz) - is Schema -> schemaProcessor.process(annotation, clazz) - is Channel -> channelProcessor.process(annotation, clazz).also { - componentToChannelMapping[clazz.java.simpleName] = - annotation.value.takeIf { it.isNotEmpty() } ?: clazz.java.simpleName + is Message -> if (element is KClass<*>) messageProcessor.process(annotation, element) else null + is Schema -> if (element is KClass<*>) schemaProcessor.process(annotation, element) else null + is Channel -> channelProcessor.process(annotation, element).also { + if (element is KClass<*>) { + componentToChannelMapping[element.java.simpleName] = + annotation.value.takeIf { it.isNotEmpty() } ?: element.java.simpleName + } } else -> null } diff --git a/kotlin-asyncapi-context/src/main/kotlin/org/openfolder/kotlinasyncapi/context/annotation/processor/ChannelProcessor.kt b/kotlin-asyncapi-context/src/main/kotlin/org/openfolder/kotlinasyncapi/context/annotation/processor/ChannelProcessor.kt index 5ba68e5..2eaa1b9 100644 --- a/kotlin-asyncapi-context/src/main/kotlin/org/openfolder/kotlinasyncapi/context/annotation/processor/ChannelProcessor.kt +++ b/kotlin-asyncapi-context/src/main/kotlin/org/openfolder/kotlinasyncapi/context/annotation/processor/ChannelProcessor.kt @@ -5,22 +5,38 @@ import org.openfolder.kotlinasyncapi.annotation.channel.Publish import org.openfolder.kotlinasyncapi.annotation.channel.Subscribe import org.openfolder.kotlinasyncapi.model.component.Components import kotlin.reflect.KClass +import kotlin.reflect.KFunction import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.functions import kotlin.reflect.full.hasAnnotation -class ChannelProcessor : AnnotationProcessor> { - override fun process(annotation: Channel, context: KClass<*>): Components { +class ChannelProcessor : AnnotationProcessor { + override fun process(annotation: Channel, context: Any): Components { return Components().apply { channels { - annotation.toChannel() - .apply { - subscribe = subscribe ?: context.findSubscribeOperation()?.toOperation() - publish = publish ?: context.findPublishOperation()?.toOperation() + when (context) { + is KClass<*> -> { + annotation.toChannel() + .apply { + subscribe = subscribe ?: context.findSubscribeOperation()?.toOperation() + publish = publish ?: context.findPublishOperation()?.toOperation() + } + .also { + put(context.java.simpleName, it) + } } - .also { - put(context.java.simpleName, it) + is KFunction<*> -> { + annotation.toChannel() + .apply { + subscribe = subscribe ?: context.findAnnotation()?.toOperation() + publish = publish ?: context.findAnnotation()?.toOperation() + } + .also { + put(context.name, it) + } } + else -> throw IllegalArgumentException("Unsupported context type: ${context::class}") + } } } } @@ -30,4 +46,4 @@ class ChannelProcessor : AnnotationProcessor> { private fun KClass<*>.findPublishOperation(): Publish? = functions.firstOrNull { it.hasAnnotation() }?.findAnnotation() -} +} \ No newline at end of file diff --git a/kotlin-asyncapi-context/src/test/kotlin/org/openfolder/kotlinasyncapi/context/annotation/processor/ChannelProcessorTest.kt b/kotlin-asyncapi-context/src/test/kotlin/org/openfolder/kotlinasyncapi/context/annotation/processor/ChannelProcessorTest.kt index 34bc22b..b5ed641 100644 --- a/kotlin-asyncapi-context/src/test/kotlin/org/openfolder/kotlinasyncapi/context/annotation/processor/ChannelProcessorTest.kt +++ b/kotlin-asyncapi-context/src/test/kotlin/org/openfolder/kotlinasyncapi/context/annotation/processor/ChannelProcessorTest.kt @@ -11,13 +11,14 @@ import org.openfolder.kotlinasyncapi.annotation.channel.Subscribe import org.openfolder.kotlinasyncapi.context.TestUtils.assertJsonEquals import org.openfolder.kotlinasyncapi.context.TestUtils.json import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.functions internal class ChannelProcessorTest { private val processor = ChannelProcessor() @Test - fun `should process channel annotation`() { + fun `should process channel annotation on class`() { val payload = TestChannel::class val annotation = payload.findAnnotation()!! @@ -65,6 +66,43 @@ internal class ChannelProcessorTest { fun testPublish() {} } + @Test + fun `should process channel annotation on function`() { + val payload = TestChannelFunction::class + val annotation = payload.functions.flatMap { it.annotations }.filterIsInstance().firstOrNull()!! + + val expected = json("annotation/channel_component_function.json") + val actual = json(processor.process(annotation, payload)) + + assertJsonEquals(expected, actual) + } + + + class TestChannelFunction { + @Channel( + value = "some/{parameter}/channel", + description = "testDescription", + servers = ["dev"], + parameters = [ + Parameter( + value = "parameter", + description = "testDescription" + ) + ] + ) + @Subscribe( + operationId = "testOperationId", + security = [ + SecurityRequirement( + key = "petstore_auth", + values = ["write:pets", "read:pets"] + ) + ], + message = Message(TestSubscribeMessage::class) + ) + fun testSubscribe() {} + } + @Message data class TestSubscribeMessage( val id: Int = 0, diff --git a/kotlin-asyncapi-context/src/test/resources/annotation/channel_component_function.json b/kotlin-asyncapi-context/src/test/resources/annotation/channel_component_function.json new file mode 100644 index 0000000..1170bf8 --- /dev/null +++ b/kotlin-asyncapi-context/src/test/resources/annotation/channel_component_function.json @@ -0,0 +1,22 @@ +{ + "channels" : { + "TestChannelFunction" : { + "description" : "testDescription", + "servers" : [ "dev" ], + "subscribe" : { + "operationId" : "testOperationId", + "security" : [ { + "petstore_auth" : [ "write:pets", "read:pets" ] + } ], + "message" : { + "$ref" : "#/components/messages/TestSubscribeMessage" + } + }, + "parameters" : { + "parameter" : { + "description" : "testDescription" + } + } + } + } +} diff --git a/kotlin-asyncapi-spring-web/pom.xml b/kotlin-asyncapi-spring-web/pom.xml index 0926cc3..dc0bb83 100644 --- a/kotlin-asyncapi-spring-web/pom.xml +++ b/kotlin-asyncapi-spring-web/pom.xml @@ -32,7 +32,7 @@ org.springframework.boot spring-boot-autoconfigure - [2.6.4,2.7.17], [3.2.0,) + [2.6.4,2.7.17], [3.2.0,3.3.5] org.jetbrains.kotlin diff --git a/kotlin-asyncapi-spring-web/src/main/kotlin/org/openfolder/kotlinasyncapi/springweb/AsyncApiAutoConfiguration.kt b/kotlin-asyncapi-spring-web/src/main/kotlin/org/openfolder/kotlinasyncapi/springweb/AsyncApiAutoConfiguration.kt index eaf07a0..bd8ffd4 100644 --- a/kotlin-asyncapi-spring-web/src/main/kotlin/org/openfolder/kotlinasyncapi/springweb/AsyncApiAutoConfiguration.kt +++ b/kotlin-asyncapi-spring-web/src/main/kotlin/org/openfolder/kotlinasyncapi/springweb/AsyncApiAutoConfiguration.kt @@ -13,9 +13,9 @@ import org.openfolder.kotlinasyncapi.context.annotation.AnnotationProvider import org.openfolder.kotlinasyncapi.context.annotation.AnnotationScanner import org.openfolder.kotlinasyncapi.context.annotation.DefaultAnnotationScanner import org.openfolder.kotlinasyncapi.context.annotation.processor.AnnotationProcessor -import org.openfolder.kotlinasyncapi.context.annotation.processor.ChannelProcessor import org.openfolder.kotlinasyncapi.context.annotation.processor.MessageProcessor import org.openfolder.kotlinasyncapi.context.annotation.processor.SchemaProcessor +import org.openfolder.kotlinasyncapi.context.annotation.processor.ChannelProcessor import org.openfolder.kotlinasyncapi.context.service.AsyncApiExtension import org.openfolder.kotlinasyncapi.context.service.AsyncApiSerializer import org.openfolder.kotlinasyncapi.context.service.AsyncApiService @@ -112,7 +112,7 @@ internal open class AsyncApiAnnotationAutoConfiguration { scanner: AnnotationScanner, messageProcessor: AnnotationProcessor>, schemaProcessor: AnnotationProcessor>, - channelProcessor: AnnotationProcessor> + channelProcessor: AnnotationProcessor ) = packageFromContext(context)?.let { AnnotationProvider( applicationPackage = it,