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 7 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
@@ -0,0 +1,5 @@
package org.openfolder.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 org.openfolder.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 org.openfolder.kotlinasyncapi.context.annotation

import org.openfolder.kotlinasyncapi.annotation.AsyncApiAnnotation
import org.openfolder.kotlinasyncapi.annotation.AsyncApiComponent
import org.openfolder.kotlinasyncapi.annotation.Schema
import org.openfolder.kotlinasyncapi.annotation.channel.Channel
import org.openfolder.kotlinasyncapi.annotation.channel.Message
Expand Down Expand Up @@ -31,7 +32,8 @@ 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 @@ -81,6 +83,7 @@ class AnnotationProvider(
componentToChannelMapping[clazz.java.simpleName] =
annotation.value.takeIf { it.isNotEmpty() } ?: clazz.java.simpleName
}
is AsyncApiComponent -> asyncApiComponentProcessor.process(annotation, clazz)
Copy link
Member

Choose a reason for hiding this comment

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

sorry, there is actually still something missing: you see that componentToChannelMapping above? you have to add the the channels you find in the class to this map. because it is used to bind the channel components to the actual channel top level object. not sure if that makes sense. but currently you would only add the channels under components.channels but you also want them in asyncapi.channels.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@lorenzsimon - Ok. I wasn't sure what the purpose of that map was. I will add it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated.

Copy link
Member

Choose a reason for hiding this comment

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

AsyncApiComponent annotation does not need the value property. You need to map the value of the channel annotation itself. In your example, some/{parameter}/channel should be the name of the channel. And this is what the bind method is doing. It checks what the value property is, uses this as the name of the channel and then references the channel component based on the class name (or the function name in your case).

{
  "channels": {
    "some/{parameter}/channel": ...
  }
}

I think we should add it to the spring integration test:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@lorenzsimon - AsyncApiComponent is the class though not the function, we changed the behavior at your request. There can be multiple channels inside an AsyncApiComponent so you want me to replicate that logic to populate this map?

Something like this (which doesn't work):

is AsyncApiComponent -> asyncApiComponentProcessor.process(annotation, clazz).also { processedComponents ->
                        processedComponents.channels?.forEach { (channelName, _) ->
                            componentToChannelMapping[channelName] = channelName
                        }
                    }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok. Looking at the integration test was the clue I needed to find the issues with my implementation. This should be resolved now.

else -> null
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.openfolder.kotlinasyncapi.context.annotation.processor

import org.openfolder.kotlinasyncapi.annotation.AsyncApiComponent
import org.openfolder.kotlinasyncapi.annotation.channel.Channel
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.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 ->
currentFunction.findAnnotation<Channel>()!!.toChannel()
.apply {
subscribe = subscribe ?: currentFunction.findAnnotation<Subscribe>()?.toOperation()
publish = publish ?: currentFunction.findAnnotation<Publish>()?.toOperation()
}
.also {
put(currentFunction.name, it)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.openfolder.kotlinasyncapi.context.annotation.processor

import org.junit.jupiter.api.Test
import org.openfolder.kotlinasyncapi.annotation.AsyncApiComponent
import org.openfolder.kotlinasyncapi.annotation.channel.Channel
import org.openfolder.kotlinasyncapi.annotation.channel.Message
import org.openfolder.kotlinasyncapi.annotation.channel.Parameter
import org.openfolder.kotlinasyncapi.annotation.channel.SecurityRequirement
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

internal class AsyncApiComponentProcessorTest {

private val processor = AsyncApiComponentProcessor()

@Test
fun `should process async api documentation annotation on class`() {
val payload = TestChannelFunction::class
val annotation = payload.findAnnotation<AsyncApiComponent>()!!

val expected = json("annotation/async_api_documentation_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" : {
"testSubscribe" : {
"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 org.openfolder.kotlinasyncapi.context.PackageInfoProvider
import org.openfolder.kotlinasyncapi.context.ResourceProvider
import org.openfolder.kotlinasyncapi.context.annotation.AnnotationProvider
import org.openfolder.kotlinasyncapi.context.annotation.DefaultAnnotationScanner
import org.openfolder.kotlinasyncapi.context.annotation.processor.AsyncApiComponentProcessor
import org.openfolder.kotlinasyncapi.context.annotation.processor.ChannelProcessor
import org.openfolder.kotlinasyncapi.context.annotation.processor.MessageProcessor
import org.openfolder.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 org.openfolder.kotlinasyncapi.springweb

import org.openfolder.kotlinasyncapi.annotation.AsyncApiComponent
import kotlin.reflect.KClass
import kotlin.script.experimental.host.toScriptSource
import kotlin.script.experimental.jvmhost.BasicJvmScriptingHost
Expand All @@ -13,9 +14,10 @@ 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.AsyncApiComponentProcessor
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
Expand Down Expand Up @@ -102,6 +104,10 @@ internal open class AsyncApiAnnotationAutoConfiguration {
open fun channelProcessor() =
ChannelProcessor()

@Bean
open fun asyncApiDocumentationProcessor() =
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
Loading