diff --git a/CODEOWNERS b/CODEOWNERS index e690364..951eacc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,2 +1,2 @@ # Default owners -* @lorenzsimon @sueskind @gimlet2 +* @lorenzsimon @gimlet2 @asyncapi-bot-eve diff --git a/kotlin-asyncapi-annotation/src/main/kotlin/com/asyncapi/kotlinasyncapi/annotation/AsyncApiComponent.kt b/kotlin-asyncapi-annotation/src/main/kotlin/com/asyncapi/kotlinasyncapi/annotation/AsyncApiComponent.kt new file mode 100644 index 0000000..c7c3c0f --- /dev/null +++ b/kotlin-asyncapi-annotation/src/main/kotlin/com/asyncapi/kotlinasyncapi/annotation/AsyncApiComponent.kt @@ -0,0 +1,5 @@ +package com.asyncapi.kotlinasyncapi.annotation + +@Target(AnnotationTarget.CLASS) +@AsyncApiAnnotation +annotation class AsyncApiComponent \ No newline at end of file diff --git a/kotlin-asyncapi-annotation/src/main/kotlin/com/asyncapi/kotlinasyncapi/annotation/channel/Channel.kt b/kotlin-asyncapi-annotation/src/main/kotlin/com/asyncapi/kotlinasyncapi/annotation/channel/Channel.kt index 2e2e487..20b49b6 100644 --- a/kotlin-asyncapi-annotation/src/main/kotlin/com/asyncapi/kotlinasyncapi/annotation/channel/Channel.kt +++ b/kotlin-asyncapi-annotation/src/main/kotlin/com/asyncapi/kotlinasyncapi/annotation/channel/Channel.kt @@ -4,7 +4,8 @@ import com.asyncapi.kotlinasyncapi.annotation.AsyncApiAnnotation @Target( AnnotationTarget.CLASS, - AnnotationTarget.ANNOTATION_CLASS + AnnotationTarget.ANNOTATION_CLASS, + AnnotationTarget.FUNCTION ) @AsyncApiAnnotation annotation class Channel( diff --git a/kotlin-asyncapi-context/src/main/kotlin/com/asyncapi/kotlinasyncapi/context/annotation/AnnotationProvider.kt b/kotlin-asyncapi-context/src/main/kotlin/com/asyncapi/kotlinasyncapi/context/annotation/AnnotationProvider.kt index b435d09..66b4bab 100644 --- a/kotlin-asyncapi-context/src/main/kotlin/com/asyncapi/kotlinasyncapi/context/annotation/AnnotationProvider.kt +++ b/kotlin-asyncapi-context/src/main/kotlin/com/asyncapi/kotlinasyncapi/context/annotation/AnnotationProvider.kt @@ -1,6 +1,7 @@ package com.asyncapi.kotlinasyncapi.context.annotation import com.asyncapi.kotlinasyncapi.annotation.AsyncApiAnnotation +import com.asyncapi.kotlinasyncapi.annotation.AsyncApiComponent import com.asyncapi.kotlinasyncapi.annotation.Schema import com.asyncapi.kotlinasyncapi.annotation.channel.Channel import com.asyncapi.kotlinasyncapi.annotation.channel.Message @@ -31,7 +32,9 @@ class AnnotationProvider( private val scanner: AnnotationScanner, private val messageProcessor: AnnotationProcessor<Message, KClass<*>>, private val schemaProcessor: AnnotationProcessor<Schema, KClass<*>>, - private val channelProcessor: AnnotationProcessor<Channel, KClass<*>> + private val channelProcessor: AnnotationProcessor<Channel, KClass<*>>, + private val asyncApiComponentProcessor: AnnotationProcessor<AsyncApiComponent, KClass<*>> + ) : AsyncApiContextProvider { private val componentToChannelMapping = mutableMapOf<String, String>() @@ -70,7 +73,8 @@ class AnnotationProvider( listOfNotNull( clazz.findAnnotation<Message>()?.let { clazz to it }, clazz.findAnnotation<Schema>()?.let { clazz to it }, - clazz.findAnnotation<Channel>()?.let { clazz to it } + clazz.findAnnotation<Channel>()?.let { clazz to it }, + clazz.findAnnotation<AsyncApiComponent>()?.let { clazz to it} ) } .mapNotNull { (clazz, annotation) -> @@ -81,6 +85,11 @@ class AnnotationProvider( componentToChannelMapping[clazz.java.simpleName] = annotation.value.takeIf { it.isNotEmpty() } ?: clazz.java.simpleName } + is AsyncApiComponent -> asyncApiComponentProcessor.process(annotation, clazz).also { processedComponents -> + processedComponents.channels?.forEach { (channelName, _) -> + componentToChannelMapping[channelName] = channelName + } + } else -> null } } diff --git a/kotlin-asyncapi-context/src/main/kotlin/com/asyncapi/kotlinasyncapi/context/annotation/processor/AsyncApiComponentProcessor.kt b/kotlin-asyncapi-context/src/main/kotlin/com/asyncapi/kotlinasyncapi/context/annotation/processor/AsyncApiComponentProcessor.kt new file mode 100644 index 0000000..18ce768 --- /dev/null +++ b/kotlin-asyncapi-context/src/main/kotlin/com/asyncapi/kotlinasyncapi/context/annotation/processor/AsyncApiComponentProcessor.kt @@ -0,0 +1,31 @@ +package com.asyncapi.kotlinasyncapi.context.annotation.processor + +import com.asyncapi.kotlinasyncapi.annotation.AsyncApiComponent +import com.asyncapi.kotlinasyncapi.annotation.channel.Channel +import com.asyncapi.kotlinasyncapi.annotation.channel.Publish +import com.asyncapi.kotlinasyncapi.annotation.channel.Subscribe +import com.asyncapi.kotlinasyncapi.model.component.Components +import kotlin.reflect.KClass +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.functions +import kotlin.reflect.full.hasAnnotation + +class AsyncApiComponentProcessor : AnnotationProcessor<AsyncApiComponent, KClass<*>> { + override fun process(annotation: AsyncApiComponent, context: KClass<*>): Components { + return Components().apply { + channels { + context.functions.filter { it.hasAnnotation<Channel>() }.forEach { currentFunction -> + var currentAnnotation = currentFunction.findAnnotation<Channel>()!! + currentAnnotation.toChannel() + .apply { + subscribe = subscribe ?: currentFunction.findAnnotation<Subscribe>()?.toOperation() + publish = publish ?: currentFunction.findAnnotation<Publish>()?.toOperation() + } + .also { + put(currentAnnotation.value, it) + } + } + } + } + } +} \ No newline at end of file diff --git a/kotlin-asyncapi-context/src/test/kotlin/com/asyncapi/kotlinasyncapi/context/annotation/processor/AsyncApiComponentProcessorTest.kt b/kotlin-asyncapi-context/src/test/kotlin/com/asyncapi/kotlinasyncapi/context/annotation/processor/AsyncApiComponentProcessorTest.kt new file mode 100644 index 0000000..b4acdd8 --- /dev/null +++ b/kotlin-asyncapi-context/src/test/kotlin/com/asyncapi/kotlinasyncapi/context/annotation/processor/AsyncApiComponentProcessorTest.kt @@ -0,0 +1,62 @@ +package com.asyncapi.kotlinasyncapi.context.annotation.processor + +import org.junit.jupiter.api.Test +import com.asyncapi.kotlinasyncapi.annotation.AsyncApiComponent +import com.asyncapi.kotlinasyncapi.annotation.channel.Channel +import com.asyncapi.kotlinasyncapi.annotation.channel.Message +import com.asyncapi.kotlinasyncapi.annotation.channel.Parameter +import com.asyncapi.kotlinasyncapi.annotation.channel.SecurityRequirement +import com.asyncapi.kotlinasyncapi.annotation.channel.Subscribe +import com.asyncapi.kotlinasyncapi.context.TestUtils.assertJsonEquals +import com.asyncapi.kotlinasyncapi.context.TestUtils.json +import kotlin.reflect.full.findAnnotation + +internal class AsyncApiComponentProcessorTest { + + private val processor = AsyncApiComponentProcessor() + + @Test + fun `should process async api component annotation on class`() { + val payload = TestChannelFunction::class + val annotation = payload.findAnnotation<AsyncApiComponent>()!! + + val expected = json("annotation/async_api_component.json") + val actual = json(processor.process(annotation, payload)) + + assertJsonEquals(expected, actual) + } + + + @AsyncApiComponent + 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, + val name: String, + val isTest: Boolean + ) +} \ No newline at end of file diff --git a/kotlin-asyncapi-context/src/test/resources/annotation/async_api_component.json b/kotlin-asyncapi-context/src/test/resources/annotation/async_api_component.json new file mode 100644 index 0000000..a9edb0d --- /dev/null +++ b/kotlin-asyncapi-context/src/test/resources/annotation/async_api_component.json @@ -0,0 +1,22 @@ +{ + "channels" : { + "some/{parameter}/channel" : { + "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-ktor/src/main/kotlin/com/asyncapi/kotlinasyncapi/ktor/AsyncApiModule.kt b/kotlin-asyncapi-ktor/src/main/kotlin/com/asyncapi/kotlinasyncapi/ktor/AsyncApiModule.kt index 56d7680..2ddd5e2 100644 --- a/kotlin-asyncapi-ktor/src/main/kotlin/com/asyncapi/kotlinasyncapi/ktor/AsyncApiModule.kt +++ b/kotlin-asyncapi-ktor/src/main/kotlin/com/asyncapi/kotlinasyncapi/ktor/AsyncApiModule.kt @@ -14,6 +14,7 @@ import com.asyncapi.kotlinasyncapi.context.PackageInfoProvider import com.asyncapi.kotlinasyncapi.context.ResourceProvider import com.asyncapi.kotlinasyncapi.context.annotation.AnnotationProvider import com.asyncapi.kotlinasyncapi.context.annotation.DefaultAnnotationScanner +import com.asyncapi.kotlinasyncapi.context.annotation.processor.AsyncApiComponentProcessor import com.asyncapi.kotlinasyncapi.context.annotation.processor.ChannelProcessor import com.asyncapi.kotlinasyncapi.context.annotation.processor.MessageProcessor import com.asyncapi.kotlinasyncapi.context.annotation.processor.SchemaProcessor @@ -52,6 +53,8 @@ class AsyncApiModule( private val channelProcessor = ChannelProcessor() + private val asyncApiComponentProcessor = AsyncApiComponentProcessor() + private val annotationScanner = DefaultAnnotationScanner() private val annotationProvider = with(configuration) { @@ -62,6 +65,7 @@ class AsyncApiModule( messageProcessor = messageProcessor, schemaProcessor = schemaProcessor, channelProcessor = channelProcessor, + asyncApiComponentProcessor = asyncApiComponentProcessor ) } diff --git a/kotlin-asyncapi-spring-web/pom.xml b/kotlin-asyncapi-spring-web/pom.xml index 1d8791d..719a33d 100644 --- a/kotlin-asyncapi-spring-web/pom.xml +++ b/kotlin-asyncapi-spring-web/pom.xml @@ -32,7 +32,7 @@ <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> - <version>[2.6.4,2.7.17], [3.2.0,)</version> + <version>[2.6.4,2.7.17], [3.2.0,3.3.5]</version> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> diff --git a/kotlin-asyncapi-spring-web/src/main/kotlin/com/asyncapi/kotlinasyncapi/springweb/AsyncApiAutoConfiguration.kt b/kotlin-asyncapi-spring-web/src/main/kotlin/com/asyncapi/kotlinasyncapi/springweb/AsyncApiAutoConfiguration.kt index 2b31f90..6e3150f 100644 --- a/kotlin-asyncapi-spring-web/src/main/kotlin/com/asyncapi/kotlinasyncapi/springweb/AsyncApiAutoConfiguration.kt +++ b/kotlin-asyncapi-spring-web/src/main/kotlin/com/asyncapi/kotlinasyncapi/springweb/AsyncApiAutoConfiguration.kt @@ -1,5 +1,6 @@ package com.asyncapi.kotlinasyncapi.springweb +import com.asyncapi.kotlinasyncapi.annotation.AsyncApiComponent import kotlin.reflect.KClass import kotlin.script.experimental.host.toScriptSource import kotlin.script.experimental.jvmhost.BasicJvmScriptingHost @@ -13,6 +14,7 @@ import com.asyncapi.kotlinasyncapi.context.annotation.AnnotationProvider import com.asyncapi.kotlinasyncapi.context.annotation.AnnotationScanner import com.asyncapi.kotlinasyncapi.context.annotation.DefaultAnnotationScanner import com.asyncapi.kotlinasyncapi.context.annotation.processor.AnnotationProcessor +import com.asyncapi.kotlinasyncapi.context.annotation.processor.AsyncApiComponentProcessor import com.asyncapi.kotlinasyncapi.context.annotation.processor.ChannelProcessor import com.asyncapi.kotlinasyncapi.context.annotation.processor.MessageProcessor import com.asyncapi.kotlinasyncapi.context.annotation.processor.SchemaProcessor @@ -102,6 +104,10 @@ internal open class AsyncApiAnnotationAutoConfiguration { open fun channelProcessor() = ChannelProcessor() + @Bean + open fun asyncApiComponentProcessor() = + AsyncApiComponentProcessor() + @Bean open fun annotationScanner() = DefaultAnnotationScanner() @@ -112,14 +118,16 @@ internal open class AsyncApiAnnotationAutoConfiguration { scanner: AnnotationScanner, messageProcessor: AnnotationProcessor<Message, KClass<*>>, schemaProcessor: AnnotationProcessor<Schema, KClass<*>>, - channelProcessor: AnnotationProcessor<Channel, KClass<*>> + channelClassProcessor: AnnotationProcessor<Channel, KClass<*>>, + asyncApiComponentProcessor: AnnotationProcessor<AsyncApiComponent, KClass<*>> ) = packageFromContext(context)?.let { AnnotationProvider( applicationPackage = it, scanner = scanner, messageProcessor = messageProcessor, schemaProcessor = schemaProcessor, - channelProcessor = channelProcessor, + channelProcessor = channelClassProcessor, + asyncApiComponentProcessor = asyncApiComponentProcessor, ) } diff --git a/kotlin-asyncapi-spring-web/src/test/kotlin/com/asyncapi/kotlinasyncapi/springweb/controller/AsyncApiControllerIntegrationTest.kt b/kotlin-asyncapi-spring-web/src/test/kotlin/com/asyncapi/kotlinasyncapi/springweb/controller/AsyncApiControllerIntegrationTest.kt index 467946c..28080f5 100644 --- a/kotlin-asyncapi-spring-web/src/test/kotlin/com/asyncapi/kotlinasyncapi/springweb/controller/AsyncApiControllerIntegrationTest.kt +++ b/kotlin-asyncapi-spring-web/src/test/kotlin/com/asyncapi/kotlinasyncapi/springweb/controller/AsyncApiControllerIntegrationTest.kt @@ -1,5 +1,6 @@ package com.asyncapi.kotlinasyncapi.springweb.controller +import com.asyncapi.kotlinasyncapi.annotation.AsyncApiComponent import org.junit.jupiter.api.Test import com.asyncapi.kotlinasyncapi.annotation.channel.Channel import com.asyncapi.kotlinasyncapi.annotation.channel.Message @@ -212,3 +213,52 @@ internal class AsyncApiControllerAnnotationIntegrationTest { val optionalValue: Boolean? ) } + +@SpringBootTest +@AutoConfigureMockMvc +internal class AsyncApiComponentAnnotationControllerIntegrationTest { + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `should return AsyncApi document`() { + val expected = TestUtils.json("async_api_component_annotation_integration.json") + + mockMvc.perform(get("/docs/asyncapi")) + .andExpect(MockMvcResultMatchers.status().is2xxSuccessful) + .andExpect(content().json(expected)) + } + + @SpringBootConfiguration + @EnableAutoConfiguration + @EnableAsyncApi + open class TestConfig { + + @Bean + open fun asyncApiExtension() = + AsyncApiExtension.builder { + info { + title("testTitle") + version("testVersion") + } + } + } + + @AsyncApiComponent + class TestChannel { + + @Channel("my/channel") + @Publish( + description = "testDescription", + message = Message(TestMessage::class) + ) + fun testOperation() {} + } + + @Message + data class TestMessage( + val value: String, + val optionalValue: Boolean? + ) +} diff --git a/kotlin-asyncapi-spring-web/src/test/resources/async_api_component_annotation_integration.json b/kotlin-asyncapi-spring-web/src/test/resources/async_api_component_annotation_integration.json new file mode 100644 index 0000000..519b246 --- /dev/null +++ b/kotlin-asyncapi-spring-web/src/test/resources/async_api_component_annotation_integration.json @@ -0,0 +1,57 @@ +{ + "asyncapi": "2.4.0", + "info": { + "title": "testTitle", + "version": "testVersion" + }, + "channels": { + "my/channel": { + "$ref": "#/components/channels/TestChannel" + } + }, + "components": { + "schemas": { + "TestMessage": { + "required": [ + "value" + ], + "type": "object", + "properties": { + "value": { + "type": "string", + "exampleSetFlag": false, + "types": [ + "string" + ] + }, + "optionalValue": { + "type": "boolean", + "exampleSetFlag": false, + "types": [ + "boolean" + ] + } + }, + "exampleSetFlag": false + } + }, + "channels": { + "my/channel": { + "publish": { + "description": "testDescription", + "message": { + "$ref": "#/components/messages/TestMessage" + } + } + } + }, + "messages": { + "TestMessage": { + "payload": { + "$ref": "#/components/schemas/TestMessage" + }, + "schemaFormat": "application/schema+json;version=draft-07" + } + } + } +} diff --git a/pom.xml b/pom.xml index 7dfb8bc..6f638c6 100644 --- a/pom.xml +++ b/pom.xml @@ -154,7 +154,7 @@ <extensions>true</extensions> <configuration> <serverId>ossrh</serverId> - <nexusUrl>https://s01.oss.sonatype.org/</nexusUrl> + <nexusUrl>https://oss.sonatype.org/</nexusUrl> <autoReleaseAfterClose>true</autoReleaseAfterClose> </configuration> </plugin> @@ -200,12 +200,12 @@ <snapshotRepository> <id>ossrh</id> <name>Sonatype Nexus Snapshots</name> - <url>https://s01.oss.sonatype.org/content/repositories/snapshots</url> + <url>https://oss.sonatype.org/content/repositories/snapshots</url> </snapshotRepository> <repository> <id>ossrh</id> <name>Nexus Release Repository</name> - <url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url> + <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url> </repository> </distributionManagement>