Skip to content

Commit

Permalink
feat: Add Ktor integration (#73)
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

---------

Co-authored-by: Lorenz Simon <[email protected]>
  • Loading branch information
lorenzsimon and lorenz-scalable authored Aug 27, 2024
1 parent a3dc058 commit 703c7f5
Show file tree
Hide file tree
Showing 59 changed files with 1,150 additions and 233 deletions.
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

0 comments on commit 703c7f5

Please sign in to comment.