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: Process message and schema annotations #44

Merged
merged 6 commits into from
Mar 20, 2023
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.openfolder.kotlinasyncapi.annotation

@Target(AnnotationTarget.ANNOTATION_CLASS)
annotation class AsyncApiAnnotation
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,13 @@ package org.openfolder.kotlinasyncapi.annotation

import kotlin.reflect.KClass

@Target(
AnnotationTarget.CLASS,
AnnotationTarget.ANNOTATION_CLASS
)
@Repeatable
@AsyncApiAnnotation
annotation class Schema(
val default: Boolean = false,
val reference: String = "",
val title: String = "",
val description: String = "",
val implementation: KClass<*> = Void::class,
val readOnly: Boolean = false,
val writeOnly: Boolean = false,
val examples: Array<String> = [],
val type: String = "",
val multipleOf: Int = 0,
val maximum: Int = 0,
val exclusiveMaximum: Int = 0,
val minimum: Int = 0,
val exclusiveMinimum: Int = 0,
val maxLength: Int = 0,
val minLength: Int = 0,
val pattern: String = "",
val maxProperties: Int = 0,
val minProperties: Int = 0,
val required: Array<String> = [],
val additionalProperties: String = "",
val allOf: Array<KClass<*>> = [],
val anyOf: Array<KClass<*>> = [],
val oneOf: Array<KClass<*>> = [],
val not: KClass<*> = Void::class,
val format: String = "",
val discriminator: String = "",
val externalDocs: ExternalDocumentation = ExternalDocumentation(default = true, url = ""),
val deprecated: Boolean = false
val implementation: KClass<*> = Void::class
drazengrabovac marked this conversation as resolved.
Show resolved Hide resolved
)
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package org.openfolder.kotlinasyncapi.annotation.channel

import org.openfolder.kotlinasyncapi.annotation.AsyncApiAnnotation
import org.openfolder.kotlinasyncapi.annotation.CorrelationID
import org.openfolder.kotlinasyncapi.annotation.ExternalDocumentation
import org.openfolder.kotlinasyncapi.annotation.Schema
import org.openfolder.kotlinasyncapi.annotation.Tag

@Target(
AnnotationTarget.CLASS,
AnnotationTarget.FUNCTION,
AnnotationTarget.VALUE_PARAMETER,
AnnotationTarget.ANNOTATION_CLASS
)
@Repeatable
@AsyncApiAnnotation
annotation class Message(
val default: Boolean = false,
val reference: String = "",
Expand All @@ -22,8 +22,8 @@ annotation class Message(
val title: String = "",
val summary: String = "",
val description: String = "",
val headers: Array<Schema> = [],
val payload: Schema = Schema(),
val headers: Schema = Schema(default = true),
val payload: Schema = Schema(default = true),
val correlationId: CorrelationID = CorrelationID(default = true, location = ""),
val tags: Array<Tag> = [],
val externalDocs: ExternalDocumentation = ExternalDocumentation(default = true, url = ""),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package org.openfolder.kotlinasyncapi.annotation.channel

import org.openfolder.kotlinasyncapi.annotation.Schema

annotation class MessageExample(
val default: Boolean = false,
val headers: Schema = Schema(default = true),
val headers: String = "",
drazengrabovac marked this conversation as resolved.
Show resolved Hide resolved
val payload: String = "",
val name: String = "",
val summary: String = ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ annotation class MessageTrait(
val title: String = "",
val summary: String = "",
val description: String = "",
val headers: Array<Schema> = [],
val headers: Schema = Schema(default = true),
val correlationId: CorrelationID = CorrelationID(default = true, location = ""),
val tags: Array<Tag> = [],
val externalDocs: ExternalDocumentation = ExternalDocumentation(default = true, url = ""),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class Message {
var description: String? = null
var tags: TagsList? = null
var externalDocs: ExternalDocumentation? = null
var bindings: ReferencableMessageBindingsMap? = null
var bindings: Any? = null
var examples: MessageExamplesList? = null
var traits: MessageTraitsList? = null

Expand Down Expand Up @@ -96,8 +96,11 @@ class Message {
inline fun externalDocs(build: ExternalDocumentation.() -> Unit): ExternalDocumentation =
ExternalDocumentation().apply(build).also { externalDocs = it }

inline fun bindings(build: ReferencableMessageBindingsMap.() -> Unit): ReferencableMessageBindingsMap =
ReferencableMessageBindingsMap().apply(build).also { bindings = it }
inline fun bindings(build: MessageBindings.() -> Unit): MessageBindings =
MessageBindings().apply(build).also { bindings = it }

inline fun bindingsRef(build: Reference.() -> Unit): Reference =
Reference().apply(build).also { bindings = it }

inline fun examples(build: MessageExamplesList.() -> Unit): MessageExamplesList =
MessageExamplesList().apply(build).also { examples = it }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,8 @@ internal class AsyncApiIntegrationTest {
}
}
}
bindings {
reference("http") {
ref("#/components/messageBindings/streamingHeaders")
}
bindingsRef {
ref("#/components/messageBindings/streamingHeaders")
lorenz-scalable marked this conversation as resolved.
Show resolved Hide resolved
}
}
message("heartbeat") {
Expand All @@ -92,10 +90,8 @@ internal class AsyncApiIntegrationTest {
type("string")
enum("\r\n")
}
bindings {
reference("http") {
ref("#/components/messageBindings/streamingHeaders")
}
bindingsRef {
ref("#/components/messageBindings/streamingHeaders")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,7 @@
"schemaFormat" : "application/schema+yaml;version=draft-07",
"summary" : "A message represents an individual chat message sent to a room.",
"bindings" : {
"http" : {
"$ref" : "#/components/messageBindings/streamingHeaders"
}
"$ref" : "#/components/messageBindings/streamingHeaders"
}
},
"heartbeat" : {
Expand All @@ -74,9 +72,7 @@
"schemaFormat" : "application/schema+yaml;version=draft-07",
"summary" : "Its purpose is to keep the connection alive.",
"bindings" : {
"http" : {
"$ref" : "#/components/messageBindings/streamingHeaders"
}
"$ref" : "#/components/messageBindings/streamingHeaders"
}
}
},
Expand Down
8 changes: 8 additions & 0 deletions kotlin-asyncapi-spring-web/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,18 @@
<groupId>org.openfolder</groupId>
<artifactId>kotlin-asyncapi-script</artifactId>
</dependency>
<dependency>
<groupId>org.openfolder</groupId>
<artifactId>kotlin-asyncapi-annotation</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
package org.openfolder.kotlinasyncapi.springweb

import org.openfolder.kotlinasyncapi.annotation.Schema
import org.openfolder.kotlinasyncapi.annotation.channel.Message
import org.openfolder.kotlinasyncapi.model.AsyncApi
import org.openfolder.kotlinasyncapi.springweb.context.DefaultAnnotationProvider
import org.openfolder.kotlinasyncapi.springweb.context.DefaultInfoProvider
import org.openfolder.kotlinasyncapi.springweb.context.DefaultResourceProvider
import org.openfolder.kotlinasyncapi.springweb.context.ResourceProvider
import org.openfolder.kotlinasyncapi.springweb.context.annotation.AnnotationScanner
import org.openfolder.kotlinasyncapi.springweb.context.annotation.DefaultAnnotationScanner
import org.openfolder.kotlinasyncapi.springweb.context.annotation.processor.AnnotationProcessor
import org.openfolder.kotlinasyncapi.springweb.context.annotation.processor.MessageProcessor
import org.openfolder.kotlinasyncapi.springweb.context.annotation.processor.SchemaProcessor
import org.openfolder.kotlinasyncapi.springweb.controller.AsyncApiController
import org.openfolder.kotlinasyncapi.springweb.service.AsyncApiExtension
import org.openfolder.kotlinasyncapi.springweb.service.AsyncApiSerializer
Expand All @@ -19,12 +27,13 @@ import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import kotlin.reflect.KClass
import kotlin.script.experimental.host.toScriptSource
import kotlin.script.experimental.jvmhost.BasicJvmScriptingHost

@Configuration
@ConditionalOnBean(AsyncApiMarkerConfiguration.Marker::class)
@Import(AsyncApiScriptAutoConfiguration::class)
@Import(AsyncApiScriptAutoConfiguration::class, AsyncApiAnnotationAutoConfiguration::class)
internal open class AsyncApiAutoConfiguration {

@Bean
Expand Down Expand Up @@ -81,6 +90,31 @@ internal open class AsyncApiScriptAutoConfiguration {
}
}

@Configuration
@ConditionalOnProperty(name = ["asyncapi.annotation.enabled"], havingValue = "true", matchIfMissing = true)
internal open class AsyncApiAnnotationAutoConfiguration {

@Bean
open fun messageProcessor() =
MessageProcessor()

@Bean
open fun schemaProcessor() =
SchemaProcessor()

@Bean
open fun annotationScanner(context: ApplicationContext) =
DefaultAnnotationScanner(context)

@Bean
open fun annotationProvider(
context: ApplicationContext,
scanner: AnnotationScanner,
messageProcessor: AnnotationProcessor<Message, KClass<*>>,
schemaProcessor: AnnotationProcessor<Schema, KClass<*>>
) = DefaultAnnotationProvider(context, scanner, messageProcessor, schemaProcessor)
}

@Configuration
@ConditionalOnClass(BasicJvmScriptingHost::class)
internal open class AsyncApiEmbeddedScriptAutoConfiguration {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package org.openfolder.kotlinasyncapi.springweb.context

import org.openfolder.kotlinasyncapi.annotation.AsyncApiAnnotation
import org.openfolder.kotlinasyncapi.annotation.Schema
import org.openfolder.kotlinasyncapi.annotation.channel.Message
import org.openfolder.kotlinasyncapi.model.ReferencableCorrelationIDsMap
import org.openfolder.kotlinasyncapi.model.ReferencableSchemasMap
import org.openfolder.kotlinasyncapi.model.channel.ReferencableChannelBindingsMap
import org.openfolder.kotlinasyncapi.model.channel.ReferencableChannelsMap
import org.openfolder.kotlinasyncapi.model.channel.ReferencableMessageBindingsMap
import org.openfolder.kotlinasyncapi.model.channel.ReferencableMessageTraitsMap
import org.openfolder.kotlinasyncapi.model.channel.ReferencableMessagesMap
import org.openfolder.kotlinasyncapi.model.channel.ReferencableOperationBindingsMap
import org.openfolder.kotlinasyncapi.model.channel.ReferencableOperationTraitsMap
import org.openfolder.kotlinasyncapi.model.channel.ReferencableParametersMap
import org.openfolder.kotlinasyncapi.model.component.Components
import org.openfolder.kotlinasyncapi.model.component.ReferencableSecuritySchemasMap
import org.openfolder.kotlinasyncapi.model.server.ReferencableServerBindingsMap
import org.openfolder.kotlinasyncapi.model.server.ReferencableServerVariablesMap
import org.openfolder.kotlinasyncapi.model.server.ReferencableServersMap
import org.openfolder.kotlinasyncapi.springweb.EnableAsyncApi
import org.openfolder.kotlinasyncapi.springweb.context.annotation.AnnotationScanner
import org.openfolder.kotlinasyncapi.springweb.context.annotation.processor.AnnotationProcessor
import org.springframework.context.ApplicationContext
import org.springframework.stereotype.Component
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation

internal interface AnnotationProvider {

val components: Components?
}

@Component
internal class DefaultAnnotationProvider(
context: ApplicationContext,
scanner: AnnotationScanner,
messageProcessor: AnnotationProcessor<Message, KClass<*>>,
schemaProcessor: AnnotationProcessor<Schema, KClass<*>>
) : AnnotationProvider {

override val components: Components? by lazy {
val scanPackage = context.getBeansWithAnnotation(EnableAsyncApi::class.java).values
.firstOrNull()
?.let { it::class.java.`package`.name }
?.takeIf { it.isNotEmpty() }

val annotatedClasses = scanPackage?.let {
scanner.scan(scanPackage = it, annotation = AsyncApiAnnotation::class)
} ?: emptyList()

Components().apply {
annotatedClasses
.flatMap { clazz ->
listOfNotNull(
clazz.findAnnotation<Message>()?.let { clazz to it },
clazz.findAnnotation<Schema>()?.let { clazz to it }
)
}
.mapNotNull { (clazz, annotation) ->
when (annotation) {
is Message -> messageProcessor.process(annotation, clazz)
is Schema -> schemaProcessor.process(annotation, clazz)
else -> null
}
}
.forEach { this + it }
}
}

private operator fun Components.plus(components: Components) {
components.schemas?.also {
schemas = (schemas ?: ReferencableSchemasMap()).apply { putAll(it) }
}
components.servers?.also {
servers = (servers ?: ReferencableServersMap()).apply { putAll(it) }
}
components.serverVariables?.also {
serverVariables = (serverVariables ?: ReferencableServerVariablesMap()).apply { putAll(it) }
}
components.channels?.also {
channels = (channels ?: ReferencableChannelsMap()).apply { putAll(it) }
}
components.messages?.also {
messages = (messages ?: ReferencableMessagesMap()).apply { putAll(it) }
}
components.securitySchemes?.also {
securitySchemes = (securitySchemes ?: ReferencableSecuritySchemasMap()).apply { putAll(it) }
}
components.parameters?.also {
parameters = (parameters ?: ReferencableParametersMap()).apply { putAll(it) }
}
components.correlationIds?.also {
correlationIds = (correlationIds ?: ReferencableCorrelationIDsMap()).apply { putAll(it) }
}
components.operationTraits?.also {
operationTraits = (operationTraits ?: ReferencableOperationTraitsMap()).apply { putAll(it) }
}
components.messageTraits?.also {
messageTraits = (messageTraits ?: ReferencableMessageTraitsMap()).apply { putAll(it) }
}
components.serverBindings?.also {
serverBindings = (serverBindings ?: ReferencableServerBindingsMap()).apply { putAll(it) }
}
components.channelBindings?.also {
channelBindings = (channelBindings ?: ReferencableChannelBindingsMap()).apply { putAll(it) }
}
components.operationBindings?.also {
operationBindings = (operationBindings ?: ReferencableOperationBindingsMap()).apply { putAll(it) }
}
components.messageBindings?.also {
messageBindings = (messageBindings ?: ReferencableMessageBindingsMap()).apply { putAll(it) }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.openfolder.kotlinasyncapi.springweb.context.annotation

import org.openfolder.kotlinasyncapi.annotation.AsyncApiAnnotation
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider
import org.springframework.core.type.filter.AnnotationTypeFilter
import org.springframework.stereotype.Component
import kotlin.reflect.KClass

internal interface AnnotationScanner {
fun scan(scanPackage: String, annotation: KClass<out Annotation>): List<KClass<*>>
}

@Component
internal class DefaultAnnotationScanner(
private val context: ApplicationContext
) : AnnotationScanner {
override fun scan(scanPackage: String, annotation: KClass<out Annotation>): List<KClass<*>> {
val classPathScanner = ClassPathScanningCandidateComponentProvider(false).also {
it.addIncludeFilter(AnnotationTypeFilter(AsyncApiAnnotation::class.java))
}

return classPathScanner.findCandidateComponents(scanPackage).map {
Class.forName(it.beanClassName).kotlin
}.toList()
}
}
Loading