Skip to content

Commit 7fb4faf

Browse files
Add meta annotation support in Kotlin annotation processor (#431)
1 parent 74d9e5a commit 7fb4faf

File tree

5 files changed

+136
-54
lines changed

5 files changed

+136
-54
lines changed

sdk-api-kotlin-gen/src/main/kotlin/dev/restate/sdk/kotlin/gen/KElementConverter.kt

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -66,47 +66,17 @@ class KElementConverter(
6666
logger.error("The annotated class is private", classDeclaration)
6767
}
6868

69-
// Figure out service type annotations
70-
val serviceAnnotation =
71-
classDeclaration
72-
.getAnnotationsByType(dev.restate.sdk.annotation.Service::class)
73-
.firstOrNull()
74-
val virtualObjectAnnotation =
75-
classDeclaration
76-
.getAnnotationsByType(dev.restate.sdk.annotation.VirtualObject::class)
77-
.firstOrNull()
78-
val workflowAnnotation =
79-
classDeclaration
80-
.getAnnotationsByType(dev.restate.sdk.annotation.Workflow::class)
81-
.firstOrNull()
82-
val isAnnotatedWithService = serviceAnnotation != null
83-
val isAnnotatedWithVirtualObject = virtualObjectAnnotation != null
84-
val isAnnotatedWithWorkflow = workflowAnnotation != null
85-
86-
// Check there's exactly one annotation
87-
if (!(isAnnotatedWithService xor isAnnotatedWithVirtualObject xor isAnnotatedWithWorkflow)) {
88-
logger.error(
89-
"The type can be annotated only with one annotation between @VirtualObject and @Service and @Workflow",
90-
classDeclaration)
91-
}
92-
93-
data.withServiceType(
94-
if (isAnnotatedWithService) ServiceType.SERVICE
95-
else if (isAnnotatedWithWorkflow) ServiceType.WORKFLOW else ServiceType.VIRTUAL_OBJECT)
96-
9769
// Infer names
9870
val targetPkg = classDeclaration.packageName.asString()
9971
val targetFqcn = classDeclaration.qualifiedName!!.asString()
100-
var serviceName =
101-
if (isAnnotatedWithService) serviceAnnotation!!.name
102-
else if (isAnnotatedWithWorkflow) workflowAnnotation!!.name
103-
else virtualObjectAnnotation!!.name
104-
if (serviceName.isEmpty()) {
72+
data.withTargetPkg(targetPkg).withTargetFqcn(targetFqcn)
73+
74+
if (data.serviceName.isNullOrEmpty()) {
10575
// Use Simple class name
10676
// With this logic we make sure we flatten subclasses names
107-
serviceName = targetFqcn.substring(targetPkg.length).replace(Pattern.quote(".").toRegex(), "")
77+
data.withServiceName(
78+
targetFqcn.substring(targetPkg.length).replace(Pattern.quote(".").toRegex(), ""))
10879
}
109-
data.withTargetPkg(targetPkg).withTargetFqcn(targetFqcn).withServiceName(serviceName)
11080

11181
// Compute handlers
11282
classDeclaration
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
2+
//
3+
// This file is part of the Restate Java SDK,
4+
// which is released under the MIT license.
5+
//
6+
// You can find a copy of the license in file LICENSE in the root
7+
// directory of this repository or package, or at
8+
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
9+
package dev.restate.sdk.kotlin.gen
10+
11+
import com.google.devtools.ksp.symbol.KSAnnotated
12+
import com.google.devtools.ksp.symbol.KSName
13+
import dev.restate.sdk.common.ServiceType
14+
15+
internal data class MetaRestateAnnotation(
16+
val annotationName: KSName,
17+
val serviceType: ServiceType
18+
) {
19+
fun resolveName(annotated: KSAnnotated): String? =
20+
annotated.annotations
21+
.find { it.annotationType.resolve().declaration.qualifiedName == annotationName }
22+
?.arguments
23+
?.firstOrNull { it -> it.name?.getShortName() == "name" }
24+
?.value as String?
25+
}

sdk-api-kotlin-gen/src/main/kotlin/dev/restate/sdk/kotlin/gen/ServiceProcessor.kt

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ package dev.restate.sdk.kotlin.gen
1111
import com.github.jknack.handlebars.io.ClassPathTemplateLoader
1212
import com.google.devtools.ksp.KspExperimental
1313
import com.google.devtools.ksp.containingFile
14+
import com.google.devtools.ksp.getClassDeclarationByName
1415
import com.google.devtools.ksp.getKotlinClassByName
1516
import com.google.devtools.ksp.processing.*
17+
import com.google.devtools.ksp.symbol.ClassKind
1618
import com.google.devtools.ksp.symbol.KSAnnotated
1719
import com.google.devtools.ksp.symbol.KSClassDeclaration
1820
import com.google.devtools.ksp.symbol.Origin
@@ -68,35 +70,24 @@ class ServiceProcessor(private val logger: KSPLogger, private val codeGenerator:
6870
resolver.builtIns,
6971
resolver.getKotlinClassByName(ByteArray::class.qualifiedName!!)!!.asType(listOf()))
7072

71-
val resolved =
72-
resolver
73-
.getSymbolsWithAnnotation(dev.restate.sdk.annotation.Service::class.qualifiedName!!)
74-
.toSet() +
75-
resolver
76-
.getSymbolsWithAnnotation(
77-
dev.restate.sdk.annotation.VirtualObject::class.qualifiedName!!)
78-
.toSet() +
79-
resolver
80-
.getSymbolsWithAnnotation(
81-
dev.restate.sdk.annotation.Workflow::class.qualifiedName!!)
82-
// Workflow annotation can be set on functions too
83-
.filter { ksAnnotated -> ksAnnotated is KSClassDeclaration }
84-
.toSet()
73+
val discovered = discoverRestateAnnotatedOrMetaAnnotatedServices(resolver)
8574

8675
val services =
87-
resolved
88-
.filter { it.containingFile!!.origin == Origin.KOTLIN }
76+
discovered
8977
.map {
9078
val serviceBuilder = Service.builder()
91-
converter.visitAnnotated(it, serviceBuilder)
79+
serviceBuilder.withServiceType(it.first.serviceType)
80+
serviceBuilder.withServiceName(it.first.resolveName(it.second))
81+
82+
converter.visitAnnotated(it.second, serviceBuilder)
9283

9384
var serviceModel: Service? = null
9485
try {
9586
serviceModel = serviceBuilder.validateAndBuild()
9687
} catch (e: Exception) {
97-
logger.error("Unable to build service: $e", it)
88+
logger.error("Unable to build service: $e", it.second)
9889
}
99-
(it to serviceModel!!)
90+
(it.second to serviceModel!!)
10091
}
10192
.toList()
10293

@@ -127,6 +118,80 @@ class ServiceProcessor(private val logger: KSPLogger, private val codeGenerator:
127118
return emptyList()
128119
}
129120

121+
private fun discoverRestateAnnotatedOrMetaAnnotatedServices(
122+
resolver: Resolver
123+
): Set<Pair<MetaRestateAnnotation, KSAnnotated>> {
124+
val discoveredAnnotatedElements = mutableSetOf<Pair<MetaRestateAnnotation, KSAnnotated>>()
125+
126+
val metaAnnotationsToProcess =
127+
mutableListOf(
128+
MetaRestateAnnotation(
129+
resolver
130+
.getClassDeclarationByName<dev.restate.sdk.annotation.Service>()!!
131+
.qualifiedName!!,
132+
ServiceType.SERVICE),
133+
MetaRestateAnnotation(
134+
resolver
135+
.getClassDeclarationByName<dev.restate.sdk.annotation.VirtualObject>()!!
136+
.qualifiedName!!,
137+
ServiceType.VIRTUAL_OBJECT),
138+
MetaRestateAnnotation(
139+
resolver
140+
.getClassDeclarationByName<dev.restate.sdk.annotation.Workflow>()!!
141+
.qualifiedName!!,
142+
ServiceType.WORKFLOW))
143+
val discoveredAnnotations = mutableSetOf<String>()
144+
145+
var metaAnnotation = metaAnnotationsToProcess.removeFirstOrNull()
146+
while (metaAnnotation != null) {
147+
if (!discoveredAnnotations.add(metaAnnotation.annotationName.asString())) {
148+
// We alredy discovered it, skip
149+
continue
150+
}
151+
for (annotatedElement in
152+
resolver.getSymbolsWithAnnotation(metaAnnotation.annotationName.asString())) {
153+
if (annotatedElement !is KSClassDeclaration) {
154+
continue
155+
}
156+
when (annotatedElement.classKind) {
157+
ClassKind.INTERFACE,
158+
ClassKind.CLASS -> {
159+
if (annotatedElement.containingFile!!.origin != Origin.KOTLIN) {
160+
// Skip if it's not kotlin
161+
continue
162+
}
163+
discoveredAnnotatedElements.add(metaAnnotation to annotatedElement)
164+
}
165+
ClassKind.ANNOTATION_CLASS -> {
166+
metaAnnotationsToProcess.add(
167+
MetaRestateAnnotation(annotatedElement.qualifiedName!!, metaAnnotation.serviceType))
168+
}
169+
else ->
170+
logger.error(
171+
"The ServiceProcessor supports only interfaces or classes declarations",
172+
annotatedElement)
173+
}
174+
}
175+
metaAnnotation = metaAnnotationsToProcess.removeFirstOrNull()
176+
}
177+
178+
val knownAnnotations = discoveredAnnotations.toSet()
179+
180+
// Check annotated elements are annotated with only one of the given annotations.
181+
discoveredAnnotatedElements.forEach { it ->
182+
val forbiddenAnnotations = knownAnnotations - setOf(it.first.annotationName.asString())
183+
val elementAnnotations =
184+
it.second.annotations
185+
.mapNotNull { it.annotationType.resolve().declaration.qualifiedName?.asString() }
186+
.toSet()
187+
if (forbiddenAnnotations.intersect(elementAnnotations).isNotEmpty()) {
188+
logger.error("The type is annotated with more than one Restate annotation", it.second)
189+
}
190+
}
191+
192+
return discoveredAnnotatedElements.toSet()
193+
}
194+
130195
private fun generateMetaINF(services: List<Pair<KSAnnotated, Service>>) {
131196
val resourceFile = "META-INF/services/${ServiceDefinitionFactory::class.java.canonicalName}"
132197
val dependencies =

sdk-api-kotlin-gen/src/test/kotlin/dev/restate/sdk/kotlin/CodegenTest.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,15 @@ class CodegenTest : TestDefinitions.TestSuite {
202202
}
203203
}
204204

205+
// Just needs to compile
206+
@MyMetaServiceAnnotation(name = "MetaAnnotatedGreeter")
207+
class MetaAnnotatedGreeter {
208+
@Handler
209+
suspend fun greet(context: Context, request: String): String {
210+
return MetaAnnotatedGreeterClient.fromContext(context).greet(request).await()
211+
}
212+
}
213+
205214
override fun definitions(): Stream<TestDefinition> {
206215
return Stream.of(
207216
testInvocation({ ServiceGreeter() }, "greet")
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
2+
//
3+
// This file is part of the Restate Java SDK,
4+
// which is released under the MIT license.
5+
//
6+
// You can find a copy of the license in file LICENSE in the root
7+
// directory of this repository or package, or at
8+
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
9+
package dev.restate.sdk.kotlin
10+
11+
import dev.restate.sdk.annotation.Service
12+
13+
@Service annotation class MyMetaServiceAnnotation(val name: String = "")

0 commit comments

Comments
 (0)