Skip to content

Commit 1acbd05

Browse files
committed
[wip] integration plugin to reduce boilerplate
The plugin currently supports auto-generating `NodeHandlerContributor` for `@RegisterNode` annotated `NodeHandler`s
1 parent 2418daf commit 1acbd05

File tree

31 files changed

+176
-111
lines changed

31 files changed

+176
-111
lines changed

integration-plugin/build.gradle.kts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
plugins {
2+
alias(libs.plugins.kotlin.jvm)
3+
}
4+
5+
dependencies {
6+
implementation(integrationPlugin.ksp.api)
7+
implementation(projects.integrations.api)
8+
val kotlinpoet = "1.18.1"
9+
implementation("com.squareup:kotlinpoet-jvm:$kotlinpoet")
10+
implementation("com.squareup:kotlinpoet-ksp:$kotlinpoet")
11+
implementation("com.squareup:kotlinpoet-metadata:$kotlinpoet")
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package me.snoty.integration.plugin.processor
2+
3+
import com.google.devtools.ksp.KspExperimental
4+
import com.google.devtools.ksp.getAnnotationsByType
5+
import com.google.devtools.ksp.processing.*
6+
import com.google.devtools.ksp.symbol.KSAnnotated
7+
import com.google.devtools.ksp.symbol.KSClassDeclaration
8+
import com.squareup.kotlinpoet.*
9+
import com.squareup.kotlinpoet.ksp.toClassName
10+
import com.squareup.kotlinpoet.ksp.writeTo
11+
import com.squareup.kotlinpoet.metadata.specs.toTypeSpec
12+
import me.snoty.integration.common.annotation.RegisterNode
13+
import me.snoty.integration.common.wiring.node.NodeDescriptor
14+
import me.snoty.integration.common.wiring.node.NodeHandlerContributor
15+
16+
class NodeHandlerContributorProcessor(val logger: KSPLogger, private val codeGenerator: CodeGenerator) : SymbolProcessor {
17+
override fun process(resolver: Resolver): List<KSAnnotated> {
18+
resolver.getSymbolsWithAnnotation(RegisterNode::class.qualifiedName!!)
19+
.filterIsInstance<KSClassDeclaration>()
20+
.forEach { processClass(it) }
21+
22+
return emptyList()
23+
}
24+
25+
@OptIn(KspExperimental::class)
26+
private fun processClass(clazz: KSClassDeclaration) {
27+
val node = clazz.getAnnotationsByType(RegisterNode::class).first()
28+
29+
val contributorClassName = ClassName(clazz.packageName.asString(), "${clazz.simpleName.asString()}Contributor")
30+
val classBuilder = TypeSpec.classBuilder(contributorClassName)
31+
val fileSpec = FileSpec.builder(contributorClassName)
32+
33+
classBuilder
34+
.addSuperinterface(NodeHandlerContributor::class)
35+
.addFunction(createNodeHandlerContributorFun(clazz, node))
36+
37+
// write SPI file
38+
codeGenerator.createNewFileByPath(
39+
dependencies = Dependencies(aggregating = false, clazz.containingFile!!),
40+
path = "META-INF/services/${NodeHandlerContributor::class.qualifiedName}",
41+
extensionName = ""
42+
).writer().use {
43+
it.appendLine(contributorClassName.canonicalName)
44+
}
45+
46+
// write contributor file
47+
fileSpec
48+
.addType(classBuilder.build())
49+
.build()
50+
.writeTo(
51+
codeGenerator = codeGenerator,
52+
aggregating = false,
53+
originatingKSFiles = listOf(clazz.containingFile!!)
54+
)
55+
}
56+
57+
private fun createNodeHandlerContributorFun(handler: KSClassDeclaration, node: RegisterNode): FunSpec {
58+
val abstractFun = NodeHandlerContributor::class.toTypeSpec(lenient = true)
59+
.funSpecs
60+
.first()
61+
val funSpec = abstractFun
62+
.toBuilder()
63+
.apply {
64+
modifiers -= KModifier.ABSTRACT
65+
modifiers += KModifier.OVERRIDE
66+
}
67+
.addStatement("val descriptor = %T(%L, %L)", NodeDescriptor::class.asTypeName(), "\"${node.subsystem}\"", "\"${node.type}\"")
68+
.addStatement("val nodeContext = nodeContextBuilder(descriptor)")
69+
.addStatement("val handler = %T(nodeContext)", handler.toClassName())
70+
.addStatement("registry.registerHandler(descriptor, handler)")
71+
72+
return funSpec.build()
73+
}
74+
75+
class Provider : SymbolProcessorProvider {
76+
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
77+
return NodeHandlerContributorProcessor(environment.logger, environment.codeGenerator)
78+
}
79+
}
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
me.snoty.integration.plugin.processor.NodeHandlerContributorProcessor$Provider

integrations/api/build.gradle.kts

+1-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ plugins {
22
alias(libs.plugins.kotlin.jvm)
33
alias(libs.plugins.kotlin.serialization)
44
alias(libs.plugins.kotlin.kover)
5+
alias(integrationPlugin.plugins.ksp)
56
}
67

78
dependencies {
@@ -23,9 +24,6 @@ dependencies {
2324
api(libraries.jackson.kotlin)
2425
api(libraries.bson.kotlinx)
2526

26-
// liquid for java
27-
implementation("nl.big-o:liqp:0.9.0.3")
28-
2927
testImplementation(kotlin("test"))
3028
testImplementation(tests.mockk)
3129
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package me.snoty.integration.common.annotation
2+
3+
import me.snoty.integration.common.wiring.node.Subsystem
4+
5+
annotation class RegisterNode(
6+
val displayName: String,
7+
/**
8+
* Unique node type / id, in snake_case.
9+
*/
10+
val type: String,
11+
val subsystem: String = Subsystem.INTEGRATION
12+
)

integrations/api/src/main/kotlin/me/snoty/integration/common/config/NodeService.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import me.snoty.backend.errors.ServiceResult
66
import me.snoty.backend.integration.config.flow.NodeId
77
import me.snoty.integration.common.wiring.StandaloneNode
88
import me.snoty.integration.common.wiring.node.NodeDescriptor
9-
import me.snoty.integration.common.wiring.node.NodePosition
109
import me.snoty.integration.common.wiring.node.NodeSettings
10+
import me.snoty.integration.common.model.NodePosition
1111
import java.util.*
1212

1313
interface NodeService {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package me.snoty.integration.common.model
2+
3+
enum class NodePosition {
4+
START,
5+
MIDDLE,
6+
END
7+
}

integrations/api/src/main/kotlin/me/snoty/integration/common/wiring/node/NodeHandler.kt

-8
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,5 @@ interface NodeHandler {
2525
context(NodeHandlerContext, EmitNodeOutputContext)
2626
suspend fun process(logger: Logger, node: Node, input: IntermediateData)
2727

28-
/**
29-
* Where the node is placed.
30-
* Start nodes cannot have incoming edges.
31-
* End nodes cannot have outgoing edges.
32-
*
33-
* Mostly useful to build a database query for all start nodes.
34-
*/
35-
val position: NodePosition
3628
val settingsClass: KClass<out NodeSettings>
3729
}
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
package me.snoty.integration.common.wiring.node
22

3-
import me.snoty.integration.common.wiring.NodeContextBuilder
4-
import me.snoty.integration.common.wiring.NodeHandlerContext
5-
6-
enum class NodePosition {
7-
START,
8-
MIDDLE,
9-
END
10-
}
3+
import me.snoty.integration.common.model.NodePosition
114

125
interface NodeRegistry {
136
fun lookupHandler(descriptor: NodeDescriptor): NodeHandler?
@@ -22,22 +15,3 @@ interface NodeRegistry {
2215

2316
fun getHandlers(): Map<NodeDescriptor, NodeHandler>
2417
}
25-
26-
fun NodeRegistry.registerHandler(
27-
subsystem: String,
28-
type: String,
29-
nodeContextBuilder: NodeContextBuilder,
30-
handlerBuilder: (NodeHandlerContext) -> NodeHandler
31-
): NodeDescriptor {
32-
val descriptor = NodeDescriptor(subsystem, type)
33-
val nodeContext = nodeContextBuilder(descriptor)
34-
registerHandler(descriptor, handlerBuilder(nodeContext))
35-
return descriptor
36-
}
37-
38-
fun NodeRegistry.registerIntegrationHandler(
39-
type: String,
40-
nodeContextBuilder: NodeContextBuilder,
41-
handlerBuilder: (NodeHandlerContext) -> NodeHandler
42-
): NodeDescriptor =
43-
registerHandler(Subsystem.INTEGRATION, type, nodeContextBuilder, handlerBuilder)

integrations/api/src/main/resources/META-INF/services/me.snoty.integration.common.wiring.node.NodeHandlerContributor

-1
This file was deleted.

integrations/builtin/build.gradle.kts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
plugins {
2+
alias(libs.plugins.kotlin.jvm)
3+
alias(libs.plugins.kotlin.serialization)
4+
alias(libs.plugins.kotlin.kover)
5+
alias(integrationPlugin.plugins.ksp)
6+
}
7+
8+
dependencies {
9+
compileOnly(projects.integrations.api)
10+
ksp(projects.integrationPlugin)
11+
12+
// liquid for java
13+
implementation("nl.big-o:liqp:0.9.0.3")
14+
}

integrations/api/src/main/kotlin/me/snoty/integration/common/wiring/node/impl/mapper/MapperEngines.kt integrations/builtin/src/main/kotlin/me/snoty/integration/builtin/mapper/MapperEngines.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package me.snoty.integration.common.wiring.node.impl.mapper
1+
package me.snoty.integration.builtin.mapper
22

33
import liqp.TemplateParser
44
import org.bson.Document

integrations/api/src/main/kotlin/me/snoty/integration/common/wiring/node/impl/mapper/MapperNode.kt integrations/builtin/src/main/kotlin/me/snoty/integration/builtin/mapper/MapperNode.kt

+6-11
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
package me.snoty.integration.common.wiring.node.impl.mapper
1+
package me.snoty.integration.builtin.mapper
22

33
import kotlinx.serialization.Serializable
4+
import me.snoty.integration.common.annotation.RegisterNode
45
import me.snoty.integration.common.wiring.*
56
import me.snoty.integration.common.wiring.data.EmitNodeOutputContext
67
import me.snoty.integration.common.wiring.data.IntermediateData
7-
import me.snoty.integration.common.wiring.node.*
8+
import me.snoty.integration.common.wiring.node.NodeHandler
9+
import me.snoty.integration.common.wiring.node.NodeSettings
10+
import me.snoty.integration.common.wiring.node.Subsystem
811
import org.bson.Document
912
import org.slf4j.Logger
1013
import kotlin.reflect.KClass
@@ -16,9 +19,9 @@ data class MapperSettings(
1619
val fields: Map<String, String>
1720
) : NodeSettings
1821

22+
@RegisterNode(displayName = "Mapper", type = "mapper", subsystem = Subsystem.PROCESSOR)
1923
class MapperNodeHandler(override val nodeHandlerContext: NodeHandlerContext) : NodeHandler {
2024
override val settingsClass: KClass<out NodeSettings> = MapperSettings::class
21-
override val position: NodePosition = NodePosition.MIDDLE
2225

2326
context(NodeHandlerContext, EmitNodeOutputContext)
2427
override suspend fun process(logger: Logger, node: Node, input: IntermediateData) {
@@ -34,11 +37,3 @@ class MapperNodeHandler(override val nodeHandlerContext: NodeHandlerContext) : N
3437
}
3538
}
3639
}
37-
38-
class MapperNodeHandlerContributor : NodeHandlerContributor {
39-
override fun contributeHandlers(registry: NodeRegistry, nodeContextBuilder: NodeContextBuilder) {
40-
registry.registerHandler(Subsystem.PROCESSOR, "mapper", nodeContextBuilder) {
41-
MapperNodeHandler(it)
42-
}
43-
}
44-
}

integrations/api/src/main/kotlin/me/snoty/integration/common/wiring/node/impl/mapper/Templater.kt integrations/builtin/src/main/kotlin/me/snoty/integration/builtin/mapper/Templater.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package me.snoty.integration.common.wiring.node.impl.mapper
1+
package me.snoty.integration.builtin.mapper
22

33
import org.bson.Document
44

integrations/discord/build.gradle.kts

+3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ plugins {
22
alias(libs.plugins.kotlin.jvm)
33
alias(libs.plugins.kotlin.serialization)
44
alias(libs.plugins.kotlin.kover)
5+
alias(integrationPlugin.plugins.ksp)
56
}
67

78
dependencies {
89
compileOnly(projects.integrations.api)
910
implementation(projects.integrations.utils.calendar)
11+
12+
ksp(projects.integrationPlugin)
1013
}

integrations/discord/src/main/kotlin/me/snoty/integration/discord/DiscordIntegration.kt

+3-10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import io.ktor.client.*
44
import io.ktor.client.request.*
55
import io.ktor.http.*
66
import kotlinx.serialization.Serializable
7+
import me.snoty.integration.common.annotation.RegisterNode
78
import me.snoty.integration.common.utils.RedactInJobName
89
import me.snoty.integration.common.wiring.*
910
import me.snoty.integration.common.wiring.data.EmitNodeOutputContext
@@ -20,11 +21,11 @@ data class DiscordSettings(
2021
val emptyIsError: Boolean = true
2122
) : NodeSettings
2223

24+
@RegisterNode(displayName = "Discord", type = "discord")
2325
class DiscordNodeHandler(
2426
override val nodeHandlerContext: NodeHandlerContext,
25-
private val client: HttpClient
27+
private val client: HttpClient = nodeHandlerContext.httpClient()
2628
) : NodeHandler {
27-
override val position: NodePosition = NodePosition.END
2829
override val settingsClass: KClass<out NodeSettings> = DiscordSettings::class
2930

3031
context(NodeHandlerContext, EmitNodeOutputContext)
@@ -49,11 +50,3 @@ class DiscordNodeHandler(
4950
}
5051
}
5152
}
52-
53-
class DiscordNodeHandlerContributor : NodeHandlerContributor {
54-
override fun contributeHandlers(registry: NodeRegistry, nodeContextBuilder: NodeContextBuilder) {
55-
registry.registerIntegrationHandler("discord", nodeContextBuilder) { context ->
56-
DiscordNodeHandler(context, context.httpClient())
57-
}
58-
}
59-
}

integrations/discord/src/main/resources/META-INF/services/me.snoty.integration.common.wiring.node.NodeHandlerContributor

-1
This file was deleted.

integrations/moodle/build.gradle.kts

+3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ plugins {
22
alias(libs.plugins.kotlin.jvm)
33
alias(libs.plugins.kotlin.serialization)
44
alias(libs.plugins.kotlin.kover)
5+
alias(integrationPlugin.plugins.ksp)
56
}
67

78
dependencies {
89
compileOnly(projects.integrations.api)
910
implementation(projects.integrations.utils.calendar)
11+
12+
ksp(projects.integrationPlugin)
1013
}

integrations/moodle/src/main/kotlin/me/snoty/integration/moodle/MoodleFetcher.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
package me.snoty.integration.moodle
22

3+
import me.snoty.integration.common.annotation.RegisterNode
34
import me.snoty.integration.common.fetch.AbstractIntegrationFetcher
45
import me.snoty.integration.common.fetch.FetchContext
56
import me.snoty.integration.common.wiring.*
67
import me.snoty.integration.common.wiring.data.EmitNodeOutputContext
78
import me.snoty.integration.common.wiring.data.IntermediateData
8-
import me.snoty.integration.common.wiring.node.NodePosition
99
import me.snoty.integration.common.wiring.node.NodeSettings
1010
import me.snoty.integration.moodle.model.MoodleAssignment
1111
import me.snoty.integration.moodle.request.getCalendarUpcoming
1212
import org.jobrunr.jobs.context.JobContext
1313
import org.slf4j.Logger
1414
import kotlin.reflect.KClass
1515

16+
@RegisterNode(displayName = "Moodle", type = "moodle")
1617
open class MoodleFetcher(
1718
override val nodeHandlerContext: NodeHandlerContext,
1819
private val moodleAPI: MoodleAPI = MoodleAPIImpl(nodeHandlerContext.httpClient())
1920
) : AbstractIntegrationFetcher() {
20-
override val position = NodePosition.START
2121
override val settingsClass: KClass<out NodeSettings> = MoodleSettings::class
2222

2323
context(NodeHandlerContext, FetchContext)
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
package me.snoty.integration.moodle
22

33
import kotlinx.serialization.Serializable
4-
import me.snoty.integration.common.wiring.NodeContextBuilder
54
import me.snoty.integration.common.utils.RedactInJobName
6-
import me.snoty.integration.common.wiring.node.NodeHandlerContributor
7-
import me.snoty.integration.common.wiring.node.NodeRegistry
85
import me.snoty.integration.common.wiring.node.NodeSettings
9-
import me.snoty.integration.common.wiring.node.registerIntegrationHandler
106

117
@Serializable
128
data class MoodleSettings(
@@ -16,11 +12,3 @@ data class MoodleSettings(
1612
@RedactInJobName
1713
val appSecret: String,
1814
) : NodeSettings
19-
20-
class MoodleNodeHandlerContributor : NodeHandlerContributor {
21-
override fun contributeHandlers(registry: NodeRegistry, nodeContextBuilder: NodeContextBuilder) {
22-
registry.registerIntegrationHandler("moodle", nodeContextBuilder) { context ->
23-
MoodleFetcher(context)
24-
}
25-
}
26-
}

integrations/moodle/src/main/resources/META-INF/services/me.snoty.integration.common.wiring.node.NodeHandlerContributor

-1
This file was deleted.

integrations/webuntis/build.gradle.kts

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ plugins {
22
alias(libs.plugins.kotlin.jvm)
33
alias(libs.plugins.kotlin.serialization)
44
alias(libs.plugins.kotlin.kover)
5+
alias(integrationPlugin.plugins.ksp)
56
}
67

78
dependencies {
@@ -13,6 +14,8 @@ dependencies {
1314
testImplementation(ktor.serialization.kotlinx.json)
1415
testImplementation(projects.integrations.api)
1516
testImplementation(tests.mockk)
17+
18+
ksp(projects.integrationPlugin)
1619
}
1720

1821
sourceSets.test.configure {

0 commit comments

Comments
 (0)