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: Add Ktor integration #73

Merged
merged 21 commits into from
Aug 27, 2024
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
93 changes: 85 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
* [Usage](#usage)
* [Kotlin DSL](#kotlin-dsl-usage)
* [Spring Web](#spring-web-usage)
* [Ktor](#ktor-usage)
* [Annotation](#annotation-usage)
* [Kotlin Script](#kotlin-script-usage)
* [Examples](#examples)
* [Configuration](#configuration)
* [Spring Web](#spring-web-configuration)
* [Ktor](#ktor-configuration)
* [Maven Plugin](#maven-plugin-configuration)
* [License](#license)

Expand Down Expand Up @@ -169,6 +171,66 @@ data class ChatMessage(
</dependency>
```

### <a name="ktor-usage"></a>Ktor
To serve your AsyncAPI specification via Ktor:
- add the `kotlin-asyncapi-ktor` dependency
- install the `AsyncApiPlugin` in you application
- document your API with `AsyncApiExtension` and/or Kotlin scripting (see [Kotlin script usage](#kotlin-script-usage))
- add annotations to auto-generate components (see [annotation usage](#annotation-usage))

You can register multiple extensions to extend and override AsyncAPI components. Extensions with a higher order override extensions with a lower order. Please note that you can only extend top-level components for now (`info`, `channels`, `servers`...). Subcomponents will always be overwritten.

**Example** (simplified version of [Gitter example](https://github.com/asyncapi/spec/blob/22c6f2c7a61846338bfbd43d81024cb12cf4ed5f/examples/gitter-streaming.yml))
```kotlin
fun main() {
embeddedServer(Netty, port = 8000) {
install(AsyncApiPlugin) {
extension = AsyncApiExtension.builder(order = 10) {
info {
title("Gitter Streaming API")
version("1.0.0")
}
servers {
// ...
}
// ...
}
}
}.start(wait = true)
}

@Channel(
value = "/rooms/{roomId}",
parameters = [
Parameter(
value = "roomId",
schema = Schema(
type = "string",
examples = ["53307860c3599d1de448e19d"]
)
)
]
)
class RoomsChannel {

@Subscribe(message = Message(ChatMessage::class))
fun publish(/*...*/) { /*...*/ }
}

@Message
data class ChatMessage(
val id: String,
val text: String
)
```
```xml
<dependency>
<groupId>org.openfolder</groupId>
<artifactId>kotlin-asyncapi-ktor</artifactId>
<version>${kotlin-asyncapi.version}</version>
</dependency>
```

### <a name="annotation-usage"></a>Annotation
The `kotlin-asyncapi-annotation` module defines technology-agnostic annotations that can be used to document event-driven microservice APIs.

Expand All @@ -191,6 +253,7 @@ You have two options to use Kotlin scripting in your project:

### <a name="examples"></a>Examples
- [Spring Boot Application](kotlin-asyncapi-examples/kotlin-asyncapi-spring-boot-example)
- [Ktor Application](kotlin-asyncapi-examples/kotlin-asyncapi-ktor-example)

#### Maven Plugin
The Maven plugin evaluates your `asyncapi.kts` script, generates a valid AsyncAPI JSON file and adds it to the project resources. The `kotlin-asyncapi-spring-web` module picks the generated resource up and converts it to an `AsyncApiExtension`.
Expand Down Expand Up @@ -261,14 +324,28 @@ In order to enable embedded scripting, you need to make some additional configur
### <a name="spring-web-configuration"></a>Spring Web
You can configure the Spring Web integration in the application properties:

| Property | Description | Default |
|---------------------------------|---------------------------------------------------------------|----------------------------------------------|
| `asyncapi.enabled` | Enables the autoconfiguration | `true` |
| `asyncapi.path` | The resource path for serving the generated AsyncAPI document | `/docs/asyncapi` |
| `asyncapi.annotation.enabled` | Enables the annotation scanning and processing | `true` |
| `asyncapi.script.enabled` | Enables the Kotlin script support | `true` |
| `asyncapi.script.resource-path` | Path to the generated script resource file | `classpath:asyncapi/generated/asyncapi.json` |
| `asyncapi.script.source-path` | Path to the AsyncAPI Kotlin script file | `classpath:build.asyncapi.kts` |
| Property | Description | Default |
|---------------------------------|---------------------------------------------------------------|------------------------------------|
| `asyncapi.enabled` | Enables the autoconfiguration | `true` |
| `asyncapi.path` | The resource path for serving the generated AsyncAPI document | `/docs/asyncapi` |
| `asyncapi.annotation.enabled` | Enables the annotation scanning and processing | `true` |
| `asyncapi.script.enabled` | Enables the Kotlin script support | `true` |
| `asyncapi.script.resource-path` | Path to the generated script resource file | `asyncapi/generated/asyncapi.json` |
| `asyncapi.script.source-path` | Path to the AsyncAPI Kotlin script file | `build.asyncapi.kts` |

### <a name="ktor-configuration"></a>Ktor
You can configure the Ktor integration in the plugin configuration:

| Property | Description | Default |
|-------------------|---------------------------------------------------------------|------------------------------------|
| `path` | The resource path for serving the generated AsyncAPI document | `/docs/asyncapi` |
| `baseClass` | The base class to filter code scanning packages | `null` |
| `scanAnnotations` | Enables class path scanning for annotations | `true` |
| `extension` | AsyncApiExtension hook | `AsyncApiExtension.empty()` |
| `extensions` | For registering multiple AsyncApiExtension hooks | `emptyList()` |
| `resourcePath` | Path to the generated script resource file | `asyncapi/generated/asyncapi.json` |
| `sourcePath` | Path to the AsyncAPI Kotlin script file | `build.asyncapi.kts` |


### <a name="maven-plugin-configuration"></a>Maven Plugin
You can configure the plugin in the plugin configuration:
Expand Down
62 changes: 62 additions & 0 deletions kotlin-asyncapi-context/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.openfolder</groupId>
<artifactId>kotlin-asyncapi-parent</artifactId>
<version>3.0.4-SNAPSHOT</version>
</parent>

<artifactId>kotlin-asyncapi-context</artifactId>
<packaging>jar</packaging>

<name>Kotlin AsyncAPI Context</name>
<description>Context module for framework integrations</description>

<dependencies>
<dependency>
<groupId>org.openfolder</groupId>
<artifactId>kotlin-asyncapi-core</artifactId>
</dependency>
<dependency>
<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>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-core-jakarta</artifactId>
</dependency>
<dependency>
<groupId>io.github.classgraph</groupId>
<artifactId>classgraph</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-scripting-jvm-host</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.skyscreamer</groupId>
<artifactId>jsonassert</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.openfolder.kotlinasyncapi.context

import org.openfolder.kotlinasyncapi.model.AsyncApi

interface AsyncApiContextProvider {

val asyncApi: AsyncApi?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.openfolder.kotlinasyncapi.context

import org.openfolder.kotlinasyncapi.model.AsyncApi

class PackageInfoProvider(
private val applicationPackage: Package?
) : AsyncApiContextProvider {

override val asyncApi: AsyncApi? by lazy {
AsyncApi().apply {
info {
title(applicationPackage?.implementationTitle ?: "AsyncAPI Definition")
version(applicationPackage?.implementationVersion ?: "SNAPSHOT")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.openfolder.kotlinasyncapi.context

import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.ObjectMapper
import org.openfolder.kotlinasyncapi.model.AsyncApi

class ResourceProvider(path: String) : AsyncApiContextProvider {

private val objectMapper = ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL)

override val asyncApi: AsyncApi? by lazy {
resource?.let { objectMapper.readValue(it, AsyncApi::class.java) }
}

val resource: String? = javaClass.classLoader.getResource(path)?.readText()
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package org.openfolder.kotlinasyncapi.springweb.context
package org.openfolder.kotlinasyncapi.context.annotation

import org.openfolder.kotlinasyncapi.annotation.AsyncApiAnnotation
import org.openfolder.kotlinasyncapi.annotation.Schema
import org.openfolder.kotlinasyncapi.annotation.channel.Channel
import org.openfolder.kotlinasyncapi.annotation.channel.Message
import org.openfolder.kotlinasyncapi.context.AsyncApiContextProvider
import org.openfolder.kotlinasyncapi.context.annotation.processor.AnnotationProcessor
import org.openfolder.kotlinasyncapi.model.AsyncApi
import org.openfolder.kotlinasyncapi.model.ReferencableCorrelationIDsMap
import org.openfolder.kotlinasyncapi.model.ReferencableSchemasMap
Expand All @@ -20,17 +22,12 @@ import org.openfolder.kotlinasyncapi.model.component.ReferencableSecuritySchemas
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

@Component
internal class AnnotationProvider(
private val context: ApplicationContext,
class AnnotationProvider(
private val applicationPackage: Package? = null,
private val classLoader: ClassLoader? = null,
private val scanner: AnnotationScanner,
private val messageProcessor: AnnotationProcessor<Message, KClass<*>>,
private val schemaProcessor: AnnotationProcessor<Schema, KClass<*>>,
Expand Down Expand Up @@ -59,14 +56,14 @@ internal class AnnotationProvider(
}

private fun bind(components: Components) {
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()
val packageName = applicationPackage?.name
val annotatedClasses = packageName.let {
scanner.scan(
scanPackage = it,
classLoader = classLoader,
annotation = AsyncApiAnnotation::class
)
}

annotatedClasses
.flatMap { clazz ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.openfolder.kotlinasyncapi.context.annotation

import io.github.classgraph.ClassGraph
import kotlin.reflect.KClass

interface AnnotationScanner {
fun scan(classLoader: ClassLoader? = null, scanPackage: String? = null, annotation: KClass<out Annotation>): List<KClass<*>>
}

class DefaultAnnotationScanner : AnnotationScanner {
override fun scan(classLoader: ClassLoader?, scanPackage: String?, annotation: KClass<out Annotation>): List<KClass<*>> {
val packageClasses = ClassGraph()
.enableAllInfo()
.apply {
if (classLoader != null) {
addClassLoader(classLoader)
}
if (scanPackage != null) {
acceptPackages(scanPackage)
}
}
.scan()

return packageClasses.getClassesWithAnnotation(annotation.java).standardClasses.map {
it.loadClass().kotlin
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.openfolder.kotlinasyncapi.context.annotation.processor

import org.openfolder.kotlinasyncapi.model.component.Components

interface AnnotationProcessor<T, U> {
fun process(annotation: T, context: U): Components
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
package org.openfolder.kotlinasyncapi.springweb.context.annotation.processor
package org.openfolder.kotlinasyncapi.context.annotation.processor

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 org.springframework.stereotype.Component
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.functions
import kotlin.reflect.full.hasAnnotation

@Component
internal class ChannelProcessor : AnnotationProcessor<Channel, KClass<*>> {
class ChannelProcessor : AnnotationProcessor<Channel, KClass<*>> {
override fun process(annotation: Channel, context: KClass<*>): Components {
return Components().apply {
channels {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.openfolder.kotlinasyncapi.springweb.context.annotation.processor
package org.openfolder.kotlinasyncapi.context.annotation.processor

import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import io.swagger.v3.core.converter.ModelConverters
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.openfolder.kotlinasyncapi.springweb.context.annotation.processor
package org.openfolder.kotlinasyncapi.context.annotation.processor

import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package org.openfolder.kotlinasyncapi.springweb.context.annotation.processor
package org.openfolder.kotlinasyncapi.context.annotation.processor

import org.openfolder.kotlinasyncapi.annotation.channel.Message
import org.openfolder.kotlinasyncapi.model.Reference
import org.openfolder.kotlinasyncapi.model.component.Components
import org.springframework.stereotype.Component
import kotlin.reflect.KClass

@Component
internal class MessageProcessor : AnnotationProcessor<Message, KClass<*>> {
class MessageProcessor : AnnotationProcessor<Message, KClass<*>> {
override fun process(annotation: Message, context: KClass<*>): Components {
val jsonSchema = MODEL_RESOLVER.readAll(context.java)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package org.openfolder.kotlinasyncapi.springweb.context.annotation.processor
package org.openfolder.kotlinasyncapi.context.annotation.processor

import org.openfolder.kotlinasyncapi.annotation.Schema
import org.openfolder.kotlinasyncapi.model.component.Components
import org.springframework.stereotype.Component
import kotlin.reflect.KClass

@Component
internal class SchemaProcessor : AnnotationProcessor<Schema, KClass<*>> {
class SchemaProcessor : AnnotationProcessor<Schema, KClass<*>> {
override fun process(annotation: Schema, context: KClass<*>): Components {
val jsonSchema = MODEL_RESOLVER.readAll(context.java)

Expand Down
Loading
Loading