@@ -11,8 +11,10 @@ package dev.restate.sdk.kotlin.gen
1111import com.github.jknack.handlebars.io.ClassPathTemplateLoader
1212import com.google.devtools.ksp.KspExperimental
1313import com.google.devtools.ksp.containingFile
14+ import com.google.devtools.ksp.getClassDeclarationByName
1415import com.google.devtools.ksp.getKotlinClassByName
1516import com.google.devtools.ksp.processing.*
17+ import com.google.devtools.ksp.symbol.ClassKind
1618import com.google.devtools.ksp.symbol.KSAnnotated
1719import com.google.devtools.ksp.symbol.KSClassDeclaration
1820import 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 =
0 commit comments