Skip to content

Commit 833247f

Browse files
committed
feat: bump base version to 0.1.17 and enhance service provider processing
1 parent 45cef86 commit 833247f

File tree

4 files changed

+91
-55
lines changed

4 files changed

+91
-55
lines changed

.github/workflows/release-and-publish.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ jobs:
3939
- name: Build Artifacts
4040
run: ./gradlew clean copyJar --no-daemon --stacktrace
4141

42+
- name: Publish to Maven Central and GitHub Packages
43+
run: ./gradlew publishToMavenCentral publishAllPublicationsToGitHubPackagesRepository --stacktrace
44+
4245
- name: Create GitHub Release
4346
uses: softprops/action-gh-release@v2
4447
with:
@@ -47,7 +50,4 @@ jobs:
4750
generate_release_notes: true
4851
files: build/libs/*
4952
env:
50-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51-
52-
- name: Publish to Maven Central and GitHub Packages
53-
run: ./gradlew publishToMavenCentral publishAllPublicationsToGitHubPackagesRepository --stacktrace
53+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
kotlin.code.style=official
22
group=io.github.eventhorizonlab
3-
baseVersion=0.1.16
3+
baseVersion=0.1.17
44
org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8

modules/processor/src/main/kotlin/com/github/eventhorizonlab/spi/ServiceSchemeProcessor.kt

Lines changed: 48 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.github.eventhorizonlab.spi
22

33
import com.google.auto.service.AutoService
4-
import java.io.Writer
54
import javax.annotation.processing.*
65
import javax.lang.model.SourceVersion
76
import javax.lang.model.element.ElementKind
@@ -11,6 +10,12 @@ import javax.lang.model.type.MirroredTypeException
1110
import javax.tools.Diagnostic
1211
import javax.tools.StandardLocation
1312

13+
private data class ProviderInfo(
14+
val contractCanonical: String,
15+
val contractBinary: String,
16+
val providerBinary: String
17+
)
18+
1419
@SupportedOptions("org.gradle.annotation.processing.aggregating")
1520
@AutoService(Processor::class)
1621
@SupportedSourceVersion(SourceVersion.RELEASE_24)
@@ -21,36 +26,35 @@ class ServiceSchemeProcessor : AbstractProcessor() {
2126
ServiceContract::class.java.canonicalName
2227
)
2328

24-
private val contracts = mutableSetOf<String>()
25-
private val providers = mutableMapOf<String, MutableList<String>>()
29+
private val contracts = mutableSetOf<String>() // canonical names
30+
private val providers = mutableListOf<ProviderInfo>()
2631

2732
override fun process(
2833
annotations: MutableSet<out TypeElement>,
2934
roundEnv: RoundEnvironment
3035
): Boolean {
31-
processingEnv.messager.printMessage(Diagnostic.Kind.NOTE, "ServiceSchemeProcessor running")
32-
3336
// 1) Collect contracts defined in this compilation
3437
roundEnv.getElementsAnnotatedWith(ServiceContract::class.java)
3538
.filter { it.kind == ElementKind.INTERFACE }
3639
.map { (it as TypeElement).qualifiedName.toString() }
3740
.forEach { contracts += it }
3841

39-
// 2) Collect providers (KClass-safe)
40-
roundEnv.getElementsAnnotatedWith(ServiceProvider::class.java)
41-
.forEach { element ->
42-
val ann = element.getAnnotation(ServiceProvider::class.java)
43-
val contractName = try {
44-
ann.value.qualifiedName ?: error("No qualified name for ${ann.value}")
45-
} catch (mte: MirroredTypeException) {
46-
val typeMirror = mte.typeMirror
47-
val typeElement = (typeMirror as DeclaredType).asElement() as TypeElement
48-
typeElement.qualifiedName.toString()
49-
}
50-
providers.computeIfAbsent(contractName) { mutableListOf() }
51-
.add((element as TypeElement).qualifiedName.toString())
42+
// 2) Collect providers
43+
roundEnv.getElementsAnnotatedWith(ServiceProvider::class.java).forEach { element ->
44+
val contractElement = try {
45+
(element.getAnnotation(ServiceProvider::class.java).value as? TypeElement)
46+
?: throw IllegalStateException()
47+
} catch (mte: MirroredTypeException) {
48+
(mte.typeMirror as DeclaredType).asElement() as TypeElement
5249
}
5350

51+
val contractCanonical = contractElement.qualifiedName.toString()
52+
val contractBinary = processingEnv.elementUtils.getBinaryName(contractElement).toString()
53+
val providerBinary = processingEnv.elementUtils.getBinaryName(element as TypeElement).toString()
54+
55+
providers += ProviderInfo(contractCanonical, contractBinary, providerBinary)
56+
}
57+
5458
// 3) On final round, generate files + validate
5559
if (roundEnv.processingOver()) {
5660
generateServiceFiles()
@@ -61,26 +65,25 @@ class ServiceSchemeProcessor : AbstractProcessor() {
6165
}
6266

6367
private fun generateServiceFiles() {
64-
// Write service files for all contracts that actually have providers (cross-module safe)
65-
providers.forEach { (contract, impls) ->
66-
if (impls.isNotEmpty()) {
67-
writeServiceFile(contract, impls.distinct().sorted())
68+
providers.groupBy { it.contractBinary }
69+
.forEach { (contractBinary, infos) ->
70+
writeServiceFile(contractBinary, infos.map { it.providerBinary }.distinct().sorted())
6871
}
69-
}
7072

71-
// Also flag any contracts declared here that have no providers
72-
contracts.filter { providers[it].isNullOrEmpty() }
73-
.forEach { contract ->
74-
processingEnv.messager.printMessage(
75-
Diagnostic.Kind.ERROR,
76-
missingServiceProviderErrorMessage(contract)
77-
)
78-
}
73+
// Flag contracts declared here with no providers
74+
contracts.filter { canonical ->
75+
providers.none { it.contractCanonical == canonical }
76+
}.forEach { contract ->
77+
processingEnv.messager.printMessage(
78+
Diagnostic.Kind.ERROR,
79+
missingServiceProviderErrorMessage(contract)
80+
)
81+
}
7982
}
8083

8184
private fun validateProviders() {
82-
providers.keys.forEach { contractName ->
83-
val contractElement = processingEnv.elementUtils.getTypeElement(contractName)
85+
providers.map { it.contractCanonical }.distinct().forEach { canonicalName ->
86+
val contractElement = processingEnv.elementUtils.getTypeElement(canonicalName)
8487
val hasAnnotation = contractElement?.annotationMirrors?.any {
8588
val annType = (it.annotationType.asElement() as TypeElement)
8689
.qualifiedName.toString()
@@ -95,27 +98,23 @@ class ServiceSchemeProcessor : AbstractProcessor() {
9598
}
9699
processingEnv.messager.printMessage(
97100
Diagnostic.Kind.ERROR,
98-
"@ServiceProvider target $contractName is not annotated with @ServiceContract. $hint"
101+
"@ServiceProvider target $canonicalName is not annotated with @ServiceContract. $hint"
99102
)
100103
}
101104
}
102105
}
103106

104-
private fun writeServiceFile(contract: String, impls: List<String>) {
105-
try {
106-
processingEnv.messager.printMessage(
107-
Diagnostic.Kind.NOTE,
108-
"Writing META-INF/services/$contract with ${impls.size} implementation(s): ${impls.joinToString()}"
109-
)
110-
processingEnv.filer
111-
.createResource(StandardLocation.CLASS_OUTPUT, "", "META-INF/services/$contract")
112-
.openWriter()
113-
.use { writer: Writer ->
114-
impls.forEach { writer.write("$it\n") }
115-
}
116-
} catch (e: Exception) {
117-
throw RuntimeException(e)
118-
}
107+
private fun writeServiceFile(contractBinary: String, impls: List<String>) {
108+
processingEnv.messager.printMessage(
109+
Diagnostic.Kind.NOTE,
110+
"Writing META-INF/services/$contractBinary with ${impls.size} implementation(s): ${impls.joinToString()}"
111+
)
112+
processingEnv.filer
113+
.createResource(StandardLocation.CLASS_OUTPUT, "", "META-INF/services/$contractBinary")
114+
.openWriter()
115+
.use { writer ->
116+
impls.forEach { writer.write("$it\n") }
117+
}
119118
}
120119
}
121120

modules/processor/src/test/kotlin/com/github/eventhorizonlab/spi/ServiceSchemeProcessorSpec.kt

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,44 @@ class ServiceSchemeProcessorSpec : FunSpec({
2323
this.classpaths = classpaths
2424
}.compile()
2525

26+
test("generates correct META-INF/services for nested provider") {
27+
val api = SourceFile.kotlin(
28+
"Api.kt",
29+
"""
30+
package my.api
31+
import com.github.eventhorizonlab.spi.ServiceContract
32+
interface Outer {
33+
@ServiceContract
34+
interface Inner
35+
}
36+
""".trimIndent()
37+
)
38+
39+
val impl = SourceFile.kotlin(
40+
"Impl.kt",
41+
"""
42+
package my.impl
43+
import my.api.Outer
44+
import my.api.Outer.Inner
45+
import com.github.eventhorizonlab.spi.ServiceProvider
46+
class Impl {
47+
@ServiceProvider(Inner::class)
48+
class ImplInner : Inner
49+
}
50+
""".trimIndent()
51+
)
52+
53+
val apiResult = compile(listOf(api), runProcessor = false)
54+
apiResult.exitCode shouldBe KotlinCompilation.ExitCode.OK
55+
56+
val implResult = compile(listOf(impl), listOf(apiResult.outputDirectory))
57+
implResult.exitCode shouldBe KotlinCompilation.ExitCode.OK
58+
59+
implResult.classLoader.readServiceFile("my.api.Outer\$Inner")?.trim() shouldBe "my.impl.Impl\$ImplInner"
60+
}
61+
2662
test("generates META-INF/services for cross-module provider") {
27-
var api = SourceFile.kotlin(
63+
val api = SourceFile.kotlin(
2864
"Api.kt",
2965
"""
3066
package my.api
@@ -95,6 +131,7 @@ class ServiceSchemeProcessorSpec : FunSpec({
95131
result.messages shouldContain "@ServiceProvider target my.api.NotAContract is not annotated with @ServiceContract"
96132
}
97133

134+
98135
test("writes all providers for a contract into META-INF/services") {
99136
val api = SourceFile.kotlin(
100137
"Api.kt",

0 commit comments

Comments
 (0)