diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5984599 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,63 @@ +name: Build & Kotest +on: + push: + branches: + - master + pull_request: + types: + - opened + - synchronize + +permissions: + packages: read + +jobs: + build: + runs-on: ubuntu-latest + env: + ORG_GRADLE_PROJECT_gprUse: 'true' + ORG_GRADLE_PROJECT_gprUser: ${{ github.actor }} + ORG_GRADLE_PROJECT_gprToken: ${{ secrets.GITHUB_TOKEN }} + permissions: + checks: write + pull-requests: write + steps: + - name: Checkout Repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '24' + cache: 'gradle' + + - name: Executable Gradle + run: chmod +x gradlew + + - name: Check Code Quality + run: ./gradlew spotlessCheck + + - name: Build with Gradle + run: ./gradlew build + + - name: Run Kotest tests + run: ./gradlew kotest + + - name: Publish Test Report + uses: mikepenz/action-junit-report@v5 + with: + check_name: 'Test Report' + report_paths: '**/jvmKotest/TEST-*.xml' + fail_on_failure: 'true' + require_tests: 'true' + detailed_summary: 'true' + flaky_summary: 'true' + include_time_in_summary: 'true' + comment: 'true' + updateComment: 'true' + include_passed: 'true' + + diff --git a/build.gradle b/build.gradle index 62dec83..dbeba17 100644 --- a/build.gradle +++ b/build.gradle @@ -6,8 +6,8 @@ plugins { id 'idea' } -group = 'org.eventhorizonlab.kyoriadventuredsl' -version = '1.0-SNAPSHOT' +group = 'com.github.eventhorizonlab' +version = '0.0.1-PRE-ALPHA' subprojects { apply plugin: 'org.jetbrains.kotlin.jvm' @@ -16,11 +16,7 @@ subprojects { apply plugin: 'org.jlleitschuh.gradle.ktlint' kotlin { - jvmToolchain(23) - } - - test { - useJUnitPlatform() + jvmToolchain(24) } apply from: rootProject.file('gradle/repositories.gradle') @@ -33,6 +29,14 @@ subprojects { destinationDirectory.set(rootProject.layout.buildDirectory.dir("libs")) } + tasks.named("check") { + dependsOn("spotlessApply") + } + + tasks.named("test") { + enabled = false + } + apply from: rootProject.file('gradle/spotless.gradle') } diff --git a/e2e/build.gradle b/e2e/build.gradle new file mode 100644 index 0000000..5a5d44e --- /dev/null +++ b/e2e/build.gradle @@ -0,0 +1,8 @@ +dependencies { + testImplementation(project(":core-api")) + testImplementation(files(project(":core").tasks.named("jar").map { it.archiveFile })) +} + +tasks.named("kotest") { + dependsOn(":core:jar") +} diff --git a/e2e/src/test/kotlin/com/github/eventhorizonlab/kyoriadventuredsl/api/ServiceLoaderE2ETest.kt b/e2e/src/test/kotlin/com/github/eventhorizonlab/kyoriadventuredsl/api/ServiceLoaderE2ETest.kt new file mode 100644 index 0000000..1717be9 --- /dev/null +++ b/e2e/src/test/kotlin/com/github/eventhorizonlab/kyoriadventuredsl/api/ServiceLoaderE2ETest.kt @@ -0,0 +1,47 @@ +package com.github.eventhorizonlab.kyoriadventuredsl.api + +import com.github.eventhorizonlab.core.api.TextComponentScope +import com.github.eventhorizonlab.core.api.textComponent +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.instanceOf +import net.kyori.adventure.text.format.NamedTextColor +import java.util.* + +class ServiceLoaderE2ETest : + StringSpec({ + "ServiceLoader should discover and use TextComponentScope.Factory" { + val loader = + ServiceLoader.load( + TextComponentScope.Factory::class.java, + Thread.currentThread().contextClassLoader + ) + val factories = loader.toList() + + factories.size shouldBe 1 + + val factory = factories.first() + + factory shouldBe instanceOf() + factory.javaClass.classLoader shouldBe TextComponentScope::class.java.classLoader + factory.javaClass.isInterface shouldBe false + + val factoryComponent = + factory.create { + content("Hello E2E") + color(NamedTextColor.RED) + } + + factoryComponent.content() shouldBe "Hello E2E" + factoryComponent.color() shouldBe NamedTextColor.RED + + val component = + textComponent { + content("Hello E2E") + color(NamedTextColor.RED) + } + + component.content() shouldBe "Hello E2E" + component.color() shouldBe NamedTextColor.RED + } + }) \ No newline at end of file diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 0bd1c2c..3c294af 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -7,12 +7,14 @@ configurations { } def dep = [ - "adventure-api": "net.kyori:adventure-api:${versions.'adventure-api'}", - "kotest" : "io.kotest:kotest-framework-engine:${versions.kotest}" + "adventure-api" : "net.kyori:adventure-api:${versions.'adventure-api'}", + "kotest" : "io.kotest:kotest-framework-engine:${versions.kotest}", + "kotest-assertions" : "io.kotest:kotest-assertions-core:${versions.kotest}" ] dependencies { implementation dep."adventure-api" testImplementation dep.kotest + testImplementation dep."kotest-assertions" } \ No newline at end of file diff --git a/gradle/repositories.gradle b/gradle/repositories.gradle index dd36c19..715e5b3 100644 --- a/gradle/repositories.gradle +++ b/gradle/repositories.gradle @@ -1,4 +1,17 @@ repositories { mavenLocal() mavenCentral() + maven { + url = uri("https://central.sonatype.com/repository/maven-snapshots/") + } + if (project.findProperty("gprUse") != null) { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/EventHorizonLab/SPI-Tooling") + credentials { + username = project.findProperty("gprUser") as String + password = project.findProperty("gprToken") as String + } + } + } } \ No newline at end of file diff --git a/gradle/spotless.gradle b/gradle/spotless.gradle index 02ae737..15ddc6c 100644 --- a/gradle/spotless.gradle +++ b/gradle/spotless.gradle @@ -5,7 +5,7 @@ spotless { target '*.gradle', '.gitattributes', '.gitignore' trimTrailingWhitespace() - indentWithSpaces(4) + leadingTabsToSpaces() endWithNewline() } diff --git a/gradle/versions.json b/gradle/versions.json index 5cc9dd1..7ebdcbf 100644 --- a/gradle/versions.json +++ b/gradle/versions.json @@ -1,4 +1,5 @@ { "kotest": "6.0.1", - "adventure-api": "4.24.0" + "adventure-api": "4.24.0", + "spi": "0.1.17" } \ No newline at end of file diff --git a/modules/core-api/build.gradle b/modules/core-api/build.gradle new file mode 100644 index 0000000..e27e05e --- /dev/null +++ b/modules/core-api/build.gradle @@ -0,0 +1,3 @@ +dependencies { + compileOnly "io.github.eventhorizonlab:spi-tooling-annotations:${versions.spi}" +} diff --git a/modules/core-api/src/main/kotlin/com/github/eventhorizonlab/core/api/TextComponentScope.kt b/modules/core-api/src/main/kotlin/com/github/eventhorizonlab/core/api/TextComponentScope.kt new file mode 100644 index 0000000..5b7d397 --- /dev/null +++ b/modules/core-api/src/main/kotlin/com/github/eventhorizonlab/core/api/TextComponentScope.kt @@ -0,0 +1,39 @@ +package com.github.eventhorizonlab.core.api + +import com.github.eventhorizonlab.spi.ServiceContract +import net.kyori.adventure.text.TextComponent +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.format.TextDecoration +import java.util.* + +@DslMarker +internal annotation class AdventureDsl + +@AdventureDsl +interface TextComponentScope { + fun content(content: String) + + fun color(color: NamedTextColor) + + fun decorate(vararg decorations: TextDecoration) + + fun text(init: TextComponentScope.() -> Unit) + + @ServiceContract + interface Factory { + fun create(init: TextComponentScope.() -> Unit): TextComponent + } +} + +fun textComponent(init: TextComponentScope.() -> Unit): TextComponent { + val loader = ServiceLoader.load(TextComponentScope.Factory::class.java) + val apiClassLoader = TextComponentScope::class.java.classLoader + + val factory = + loader.firstOrNull { + it.javaClass.classLoader != apiClassLoader + } ?: loader.firstOrNull() + ?: error("No TextComponentScope.Factory implementation found") + + return factory.create(init) +} \ No newline at end of file diff --git a/modules/core/build.gradle b/modules/core/build.gradle new file mode 100644 index 0000000..f7c8cf1 --- /dev/null +++ b/modules/core/build.gradle @@ -0,0 +1,10 @@ +plugins { + id 'org.jetbrains.kotlin.kapt' +} + +dependencies { + implementation(project(":core-api")) + + compileOnly "io.github.eventhorizonlab:spi-tooling-annotations:${versions.spi}" + kapt "io.github.eventhorizonlab:spi-tooling-processor:${versions.spi}" +} diff --git a/modules/core/src/main/kotlin/com/github/eventhorizonlab/kyoriadventuredsl/core/DefaultTextComponentScope.kt b/modules/core/src/main/kotlin/com/github/eventhorizonlab/kyoriadventuredsl/core/DefaultTextComponentScope.kt new file mode 100644 index 0000000..be73eb6 --- /dev/null +++ b/modules/core/src/main/kotlin/com/github/eventhorizonlab/kyoriadventuredsl/core/DefaultTextComponentScope.kt @@ -0,0 +1,40 @@ +package com.github.eventhorizonlab.kyoriadventuredsl.core + +import com.github.eventhorizonlab.core.api.TextComponentScope +import com.github.eventhorizonlab.spi.ServiceProvider +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.TextComponent +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.format.TextDecoration + +internal class DefaultTextComponentScope( + private val builder: TextComponent.Builder +) : TextComponentScope { + override fun content(content: String) { + builder.content(content) + } + + override fun color(color: NamedTextColor) { + builder.color(color) + } + + override fun decorate(vararg decorations: TextDecoration) { + decorations.forEach { builder.decoration(it, true) } + } + + override fun text(init: TextComponentScope.() -> Unit) { + val childBuilder = Component.text() + DefaultTextComponentScope(childBuilder).init() + builder.append(childBuilder.build()) + } + + fun build() = builder.build() + + @ServiceProvider(TextComponentScope.Factory::class) + class Factory : TextComponentScope.Factory { + override fun create(init: TextComponentScope.() -> Unit): TextComponent { + val builder = Component.text() + return DefaultTextComponentScope(builder).apply(init).build() + } + } +} \ No newline at end of file diff --git a/modules/core/src/test/kotlin/com/github/eventhorizonlab/kyoriadventuredsl/core/TextComponentScopeTest.kt b/modules/core/src/test/kotlin/com/github/eventhorizonlab/kyoriadventuredsl/core/TextComponentScopeTest.kt new file mode 100644 index 0000000..4eb1694 --- /dev/null +++ b/modules/core/src/test/kotlin/com/github/eventhorizonlab/kyoriadventuredsl/core/TextComponentScopeTest.kt @@ -0,0 +1,70 @@ +package com.github.eventhorizonlab.kyoriadventuredsl.core + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.instanceOf +import net.kyori.adventure.text.TextComponent +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.format.TextDecoration + +class TextComponentScopeTest : + StringSpec({ + + // Use the concrete Factory to create components + val factory = DefaultTextComponentScope.Factory() + + "builds a simple text component with content" { + val content = "Hello World!" + val component = + factory.create { + content(content) + } + + component.content() shouldBe content + component.color() shouldBe null + component.decorations().forEach { (_, state) -> + state shouldBe TextDecoration.State.NOT_SET + } + } + + "applies a color to the text" { + val color = NamedTextColor.RED + val component = + factory.create { + color(color) + } + + component.color() shouldBe color + } + + "applies multiple decorations" { + val decorations = arrayOf(TextDecoration.BOLD, TextDecoration.ITALIC) + val component = + factory.create { + decorate(*decorations) + } + + decorations.forEach { + component.hasDecoration(it) shouldBe true + component.decoration(it) shouldBe TextDecoration.State.TRUE + } + } + + "supports nested text components" { + val component = + factory.create { + content("Parent") + text { + content(" Child") + color(NamedTextColor.GREEN) + } + } + + component.children().size shouldBe 1 + val child = component.children().first() + child shouldBe instanceOf() + val textComponent = child as TextComponent + textComponent.content() shouldBe " Child" + textComponent.color() shouldBe NamedTextColor.GREEN + } + }) \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 6005e81..8aa27e6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,5 +11,9 @@ plugins { rootProject.name = 'KyoriAdventureDSL' -include 'core' -project(':core').projectDir = file('modules/core') +['core', 'core-api'].forEach { + include it + project(":$it").projectDir = file("modules/$it") +} + +include 'e2e' \ No newline at end of file