Skip to content

Commit

Permalink
feat: introduce AsyncApiComponent annotation (#78)
Browse files Browse the repository at this point in the history
* feat: Implement message model annotations (#43)

* feat - Implement message model annotations

* chore - Remove unused dependencies

* feat: Process message and schema annotations (#44)

* feat - Implement Message annotation processing

* feat - Merge annotation components

* feat - Add schema annotation processor

* refactor - Make annotation processor context dynamic
test - Add annotation provider integration test

* chore - ktlint format

* refactor - Refactor dependency management

---------

Co-authored-by: lorenzsimon <[email protected]>

* feat: Channel annotation processing (#45)

* feat - Add Channel and Operation annotations

* feat - Add Channel processing

* refactor - Annotation keys to values

* chore - Fix confusing test values

* refactor: Annotation mapping (#47)

* refactor - Annotation mapping improvements

* refactor - Add option for inline messages and schemas

* refactor - Use classname for channel component keys if autogenerated

* fix - Typo

* test - Fix Schemas test

* feat: Add Kotlin module to model resolver (#48)

feat - Add Kotlin module to model resolver

* feat: Bind channels to annotation components (#49)

* refactor - Context providers

* feat - Bind channels to annotation components

* refactor - Annotation components binding

* chore - Format

* chore: Add Spring Boot example application (#63)

* chore: Add Spring Boot example application

* fix: Java version

* fix: Test

* chore: Bump dependencies (#65)

* chore: Bump dependencies

* chore: Bump dependencies

* chore: Set Java version in GH actions

* fix: Autoconfig migration

* fix: Migrate Jakarta

* chore: Refactor data objects (#67)

* chore: Revert

* release: 3.0.3

* pre-release: 3.0.4

* feat: Add Ktor integration (#72)

* docs: Update Spring support

* pre-release: 3.0.4 (#70)

* docs: Link Spring Boot example

* Add Ktor integration

* Fix content type

* Fix test

* Fix test

* Add Script extension

* Add Script extension

* Refactor

* Add plugin integration test

* Add docs for ktor integration

* Move to new domain namespace

* Fix kts example usage

* Move domain

* Bump dependencies

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

* allow annotation to target function also.

* add support for channel on functions instead of just classes.

* split into a separate processor with new annotation to handle finding the correct classes.

* match previous white space

* name updates per PR comments.

* fix annotation location

* add AsyncApiComponent population of channel map behavior to match Channel behavior. added value attribute to AsyncApiComponent for parity.

* chore: move domain (#77)

* feat: Implement message model annotations (#43)

* feat - Implement message model annotations

* chore - Remove unused dependencies

* feat: Process message and schema annotations (#44)

* feat - Implement Message annotation processing

* feat - Merge annotation components

* feat - Add schema annotation processor

* refactor - Make annotation processor context dynamic
test - Add annotation provider integration test

* chore - ktlint format

* refactor - Refactor dependency management

---------

Co-authored-by: lorenzsimon <[email protected]>

* feat: Channel annotation processing (#45)

* feat - Add Channel and Operation annotations

* feat - Add Channel processing

* refactor - Annotation keys to values

* chore - Fix confusing test values

* refactor: Annotation mapping (#47)

* refactor - Annotation mapping improvements

* refactor - Add option for inline messages and schemas

* refactor - Use classname for channel component keys if autogenerated

* fix - Typo

* test - Fix Schemas test

* feat: Add Kotlin module to model resolver (#48)

feat - Add Kotlin module to model resolver

* feat: Bind channels to annotation components (#49)

* refactor - Context providers

* feat - Bind channels to annotation components

* refactor - Annotation components binding

* chore - Format

* chore: Add Spring Boot example application (#63)

* chore: Add Spring Boot example application

* fix: Java version

* fix: Test

* chore: Bump dependencies (#65)

* chore: Bump dependencies

* chore: Bump dependencies

* chore: Set Java version in GH actions

* fix: Autoconfig migration

* fix: Migrate Jakarta

* chore: Refactor data objects (#67)

* chore: Revert

* release: 3.0.3

* pre-release: 3.0.4

* feat: Add Ktor integration (#72)

* docs: Update Spring support

* pre-release: 3.0.4 (#70)

* docs: Link Spring Boot example

* Add Ktor integration

* Fix content type

* Fix test

* Fix test

* Add Script extension

* Add Script extension

* Refactor

* Add plugin integration test

* Add docs for ktor integration

* Move to new domain namespace

* Fix kts example usage

* Move domain

* Bump dependencies

---------

Co-authored-by: Lorenz Simon <[email protected]>

* chore: Update repositories

* change how the map is populated for channels found via AsyncApiComponent and for Channel annotations to handle the fact that not all of them will be named based on their class and that will only happen if they target a class instead of a function.

* Update CODEOWNERS

* chore: Update pom.xml

* Update CODEOWNERS

* changes per PR requests.

* updates to handle merge conflicts.

---------

Co-authored-by: Charles Bazeley <[email protected]>
Co-authored-by: Lorenz Simon <[email protected]>
Co-authored-by: Lorenz Simon <[email protected]>

* Revert version constraint

---------

Co-authored-by: Lorenz Simon <[email protected]>
Co-authored-by: CharlesB <[email protected]>
Co-authored-by: Charles Bazeley <[email protected]>
  • Loading branch information
4 people authored Jan 6, 2025
1 parent 123efad commit baecdbe
Show file tree
Hide file tree
Showing 10 changed files with 254 additions and 5 deletions.
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
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": {
"publish": {
"description": "testDescription",
"message": {
"$ref": "#/components/messages/TestMessage"
}
}
}
},
"messages": {
"TestMessage": {
"payload": {
"$ref": "#/components/schemas/TestMessage"
},
"schemaFormat": "application/schema+json;version=draft-07"
}
}
}
}

0 comments on commit baecdbe

Please sign in to comment.