Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create new annotation for classes that allows you to have multiple channels within #76

Merged
merged 18 commits into from
Jan 6, 2025
Merged
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
2 changes: 1 addition & 1 deletion CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Default owners
* @lorenzsimon @sueskind @gimlet2
* @lorenzsimon @gimlet2 @asyncapi-bot-eve
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.asyncapi.kotlinasyncapi.annotation

@Target(AnnotationTarget.CLASS)
@AsyncApiAnnotation
annotation class AsyncApiComponent
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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>()
Expand Down Expand Up @@ -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) ->
Expand All @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
)
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -52,6 +53,8 @@ class AsyncApiModule(

private val channelProcessor = ChannelProcessor()

private val asyncApiComponentProcessor = AsyncApiComponentProcessor()

private val annotationScanner = DefaultAnnotationScanner()

private val annotationProvider = with(configuration) {
Expand All @@ -62,6 +65,7 @@ class AsyncApiModule(
messageProcessor = messageProcessor,
schemaProcessor = schemaProcessor,
channelProcessor = channelProcessor,
asyncApiComponentProcessor = asyncApiComponentProcessor
)
}

Expand Down
2 changes: 1 addition & 1 deletion kotlin-asyncapi-spring-web/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -102,6 +104,10 @@ internal open class AsyncApiAnnotationAutoConfiguration {
open fun channelProcessor() =
ChannelProcessor()

@Bean
open fun asyncApiComponentProcessor() =
AsyncApiComponentProcessor()

@Bean
open fun annotationScanner() =
DefaultAnnotationScanner()
Expand All @@ -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,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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?
)
}
Original file line number Diff line number Diff line change
@@ -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": {
Copy link
Member

Choose a reason for hiding this comment

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

Should be TestChannel

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No - in places where the channel is inside a class, it should be the value of the annotation not the name of the class. https://github.com/azizabah/kotlin-asyncapi/blob/bc83ab4f5825d56f7741f126fce481bad57e8c6b/kotlin-asyncapi-spring-web/src/test/kotlin/org/openfolder/kotlinasyncapi/springweb/controller/AsyncApiControllerIntegrationTest.kt#L251

If you do it as the value of the class, you will run into conflicts when you have multiple inside the same class which is the exact functionality this entire PR is about.

"publish": {
"description": "testDescription",
"message": {
"$ref": "#/components/messages/TestMessage"
}
}
}
},
"messages": {
"TestMessage": {
"payload": {
"$ref": "#/components/schemas/TestMessage"
},
"schemaFormat": "application/schema+json;version=draft-07"
}
}
}
}
Loading
Loading