From 306e6f2a655b7be5a2587b7ecfb3f7ccf0d79691 Mon Sep 17 00:00:00 2001 From: y9vad9 Date: Sat, 3 Oct 2026 15:24:50 +0200 Subject: [PATCH 1/7] build: modernize build system to gradle 9.8.0 and adopt version catalog --- build-logic/configuration/build.gradle.kts | 38 --- .../KotlinAndroidApplicationConfiguration.kt | 10 - .../KotlinAndroidLibraryConfiguration.kt | 10 - .../src/main/kotlin/KotlinJsConfiguration.kt | 9 - .../src/main/kotlin/KotlinJvmConfiguration.kt | 9 - .../KotlinMultiplatformConfiguration.kt | 9 - build-logic/dependencies/README.md | 3 - build-logic/dependencies/build.gradle.kts | 17 -- .../dependencies/src/main/kotlin/AppInfo.kt | 4 - .../dependencies/src/main/kotlin/Deps.kt | 256 ------------------ .../src/main/kotlin/unused/GradlePlugin.kt | 9 - build-logic/library-deploy/build.gradle.kts | 25 -- .../src/main/kotlin/DeployExtension.kt | 17 -- .../src/main/kotlin/DeployPlugin.kt | 67 ----- .../src/main/kotlin/SshSessionExtension.kt | 17 -- build.gradle.kts | 47 ++-- buildSrc/build.gradle.kts | 12 - gradle.properties | 27 +- gradle/libs.versions.toml | 18 ++ gradle/wrapper/gradle-wrapper.properties | 2 +- ksp/build.gradle.kts | 53 ++-- settings.gradle.kts | 23 +- test/build.gradle.kts | 18 +- 23 files changed, 104 insertions(+), 596 deletions(-) delete mode 100644 build-logic/configuration/build.gradle.kts delete mode 100644 build-logic/configuration/src/main/kotlin/KotlinAndroidApplicationConfiguration.kt delete mode 100644 build-logic/configuration/src/main/kotlin/KotlinAndroidLibraryConfiguration.kt delete mode 100644 build-logic/configuration/src/main/kotlin/KotlinJsConfiguration.kt delete mode 100644 build-logic/configuration/src/main/kotlin/KotlinJvmConfiguration.kt delete mode 100644 build-logic/configuration/src/main/kotlin/KotlinMultiplatformConfiguration.kt delete mode 100644 build-logic/dependencies/README.md delete mode 100644 build-logic/dependencies/build.gradle.kts delete mode 100755 build-logic/dependencies/src/main/kotlin/AppInfo.kt delete mode 100755 build-logic/dependencies/src/main/kotlin/Deps.kt delete mode 100644 build-logic/dependencies/src/main/kotlin/unused/GradlePlugin.kt delete mode 100644 build-logic/library-deploy/build.gradle.kts delete mode 100644 build-logic/library-deploy/src/main/kotlin/DeployExtension.kt delete mode 100644 build-logic/library-deploy/src/main/kotlin/DeployPlugin.kt delete mode 100644 build-logic/library-deploy/src/main/kotlin/SshSessionExtension.kt delete mode 100644 buildSrc/build.gradle.kts create mode 100644 gradle/libs.versions.toml diff --git a/build-logic/configuration/build.gradle.kts b/build-logic/configuration/build.gradle.kts deleted file mode 100644 index f41fe83..0000000 --- a/build-logic/configuration/build.gradle.kts +++ /dev/null @@ -1,38 +0,0 @@ -plugins { - `kotlin-dsl` - id("dependencies") -} - -repositories { - mavenCentral() - google() -} - -dependencies { - implementation(Deps.Plugins.Dependencies.Classpath) - implementation(Deps.Plugins.Android.Classpath) - implementation(Deps.Plugins.Kotlin.Classpath) -} - -gradlePlugin { - plugins.register("k-mpp") { - id = "k-mpp" - implementationClass = "KotlinMultiplatformConfiguration" - } - plugins.register("k-jvm") { - id = "k-jvm" - implementationClass = "KotlinJvmConfiguration" - } - plugins.register("k-js") { - id = "k-js" - implementationClass = "KotlinJsConfiguration" - } - plugins.register("k-android-app") { - id = "k-android-app" - implementationClass = "KotlinAndroidApplicationConfiguration" - } - plugins.register("k-android-library") { - id = "k-android-library" - implementationClass = "KotlinAndroidLibraryConfiguration" - } -} diff --git a/build-logic/configuration/src/main/kotlin/KotlinAndroidApplicationConfiguration.kt b/build-logic/configuration/src/main/kotlin/KotlinAndroidApplicationConfiguration.kt deleted file mode 100644 index 28e74cd..0000000 --- a/build-logic/configuration/src/main/kotlin/KotlinAndroidApplicationConfiguration.kt +++ /dev/null @@ -1,10 +0,0 @@ -import org.gradle.api.Plugin -import org.gradle.api.Project - - -class KotlinAndroidApplicationConfiguration : Plugin { - override fun apply(target: Project) { - target.plugins.apply(Deps.Plugins.Android.Application) - target.plugins.apply(Deps.Plugins.Kotlin.Android) - } -} diff --git a/build-logic/configuration/src/main/kotlin/KotlinAndroidLibraryConfiguration.kt b/build-logic/configuration/src/main/kotlin/KotlinAndroidLibraryConfiguration.kt deleted file mode 100644 index 73e2008..0000000 --- a/build-logic/configuration/src/main/kotlin/KotlinAndroidLibraryConfiguration.kt +++ /dev/null @@ -1,10 +0,0 @@ -import org.gradle.api.Plugin -import org.gradle.api.Project - - -class KotlinAndroidLibraryConfiguration : Plugin { - override fun apply(target: Project) { - target.plugins.apply(Deps.Plugins.Android.Library) - target.plugins.apply(Deps.Plugins.Kotlin.Android) - } -} diff --git a/build-logic/configuration/src/main/kotlin/KotlinJsConfiguration.kt b/build-logic/configuration/src/main/kotlin/KotlinJsConfiguration.kt deleted file mode 100644 index ca1bf6f..0000000 --- a/build-logic/configuration/src/main/kotlin/KotlinJsConfiguration.kt +++ /dev/null @@ -1,9 +0,0 @@ -import org.gradle.api.Plugin -import org.gradle.api.Project - - -class KotlinJsConfiguration : Plugin { - override fun apply(target: Project) { - target.plugins.apply(Deps.Plugins.Kotlin.Js) - } -} diff --git a/build-logic/configuration/src/main/kotlin/KotlinJvmConfiguration.kt b/build-logic/configuration/src/main/kotlin/KotlinJvmConfiguration.kt deleted file mode 100644 index 5464fd8..0000000 --- a/build-logic/configuration/src/main/kotlin/KotlinJvmConfiguration.kt +++ /dev/null @@ -1,9 +0,0 @@ -import org.gradle.api.Plugin -import org.gradle.api.Project - - -class KotlinJvmConfiguration : Plugin { - override fun apply(target: Project) { - target.plugins.apply(Deps.Plugins.Kotlin.Jvm) - } -} \ No newline at end of file diff --git a/build-logic/configuration/src/main/kotlin/KotlinMultiplatformConfiguration.kt b/build-logic/configuration/src/main/kotlin/KotlinMultiplatformConfiguration.kt deleted file mode 100644 index eb0f12c..0000000 --- a/build-logic/configuration/src/main/kotlin/KotlinMultiplatformConfiguration.kt +++ /dev/null @@ -1,9 +0,0 @@ -import org.gradle.api.Plugin -import org.gradle.api.Project - - -class KotlinMultiplatformConfiguration : Plugin { - override fun apply(target: Project) { - target.plugins.apply(Deps.Plugins.Kotlin.Multiplatform) - } -} diff --git a/build-logic/dependencies/README.md b/build-logic/dependencies/README.md deleted file mode 100644 index df381ae..0000000 --- a/build-logic/dependencies/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Dependencies - -Replacement for `buildSrc` to store dependencies diff --git a/build-logic/dependencies/build.gradle.kts b/build-logic/dependencies/build.gradle.kts deleted file mode 100644 index 4209e85..0000000 --- a/build-logic/dependencies/build.gradle.kts +++ /dev/null @@ -1,17 +0,0 @@ -plugins { - `kotlin-dsl` -} - -group = "dependencies" -version = "SNAPSHOT" - -gradlePlugin { - plugins.register("dependencies") { - id = "dependencies" - implementationClass = "unused.GradlePlugin" - } -} - -repositories { - mavenCentral() -} diff --git a/build-logic/dependencies/src/main/kotlin/AppInfo.kt b/build-logic/dependencies/src/main/kotlin/AppInfo.kt deleted file mode 100755 index bbf5536..0000000 --- a/build-logic/dependencies/src/main/kotlin/AppInfo.kt +++ /dev/null @@ -1,4 +0,0 @@ -object AppInfo { - const val PACKAGE = "com.y9vad9.implier" - const val VERSION = "1.0.4" -} \ No newline at end of file diff --git a/build-logic/dependencies/src/main/kotlin/Deps.kt b/build-logic/dependencies/src/main/kotlin/Deps.kt deleted file mode 100755 index d8406fb..0000000 --- a/build-logic/dependencies/src/main/kotlin/Deps.kt +++ /dev/null @@ -1,256 +0,0 @@ -@file:Suppress("FunctionName", "unused") - -object Deps { - const val compileSdkVersion = 31 - const val minSdkVersion = 21 - - private const val kotlinVersion = "1.7.20-Beta" - private const val coroutinesVersion = "1.6.0-RC" - private const val serializationVersion = "1.3.1" - private const val nodejsExternalsVersion = "0.0.7" - private const val ktorVersion = "1.6.5" - private const val ktorOpenapiVersion = "0.2-beta.20" - private const val exposedVersion = "0.35.1" - private const val datetimeVersion = "0.3.0" - - private const val postgresqlVersion = "42.2.24" - private const val slf4jJVersion = "1.7.32" - private const val logbackVersion = "1.2.6" - private const val sshVersion = "2.10.1" - - private const val materialVersion = "1.5.0-alpha03" - private const val recyclerViewVersion = "1.3.0-alpha01" - private const val swipeRefreshLayoutVersion = "1.2.0" - private const val constraintLayoutVersion = "2.1.0" - private const val lifecycleVersion = "2.3.1" - private const val glideVersion = "4.12.0" - private const val androidAppCompatVersion = "1.3.1" - private const val androidComposeVersion = "1.1.0-alpha04" - private const val androidGradlePluginVersion = "4.2.2" - - private const val kvisionVersion = "5.1.1" - private const val shadowVer = "7.0.0" - - private const val kdsVer = "1.1.0" - private const val scriptKtVer = "0.0.2" - private const val kspVersion = "1.7.20-Beta-1.0.6" - - object Libs { - - object KSP { - const val Api = "com.google.devtools.ksp:symbol-processing-api:$kspVersion" - } - - object Kotlin { - const val Reflection = "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" - const val JUnit = "org.jetbrains.kotlin:kotlin-test-junit5:$kotlinVersion" - } - - object KotlinPoet { - const val KotlinPoet = "com.squareup:kotlinpoet:1.12.0" - const val KSP = "com.squareup:kotlinpoet-ksp:1.12.0" - } - - object KotlinGang { - object KDS { - const val FileDataStorage = "fun.kotlingang.kds:json-files:$kdsVer" - const val LocalDataStorage = "fun.kotlingang.kds:json-files:$kdsVer" - const val BundleDataStorage = "fun.kotlingang.kds:json-bundle:$kdsVer" - - object Integrations { - const val Androidx = "fun.kotlingang.kds:extensions-androidx:$kdsVer" - const val Coroutines = "fun.kotlingang.kds:extensions-coroutines:$kdsVer" - const val KVision = "fun.kotlingang.kds:extensions-kvision:$kdsVer" - } - - const val Core = "fun.kotlingang.kds:core:$kdsVer" - const val Json = "fun.kotlingang.kds:json:$kdsVer" - } - - object ScriptKt { - const val Wrapper = "fun.kotlingang.scriptkt:scriptkt:$scriptKtVer" - const val ImportScript = "fun.kotlingang.scriptkt:import-script:$scriptKtVer" - const val MavenResolver = "fun.kotlingang.scriptkt:maven-resolver:$scriptKtVer" - } - } - - object KVision { - const val Core = "io.kvision:kvision:$kvisionVersion" - - object Bootstrap { - const val Core = "io.kvision:kvision-bootstrap:$kvisionVersion" - const val Css = "io.kvision:kvision-bootstrap-css:$kvisionVersion" - const val DateTime = "io.kvision:kvision-bootstrap-datetime:$kvisionVersion" - const val Select = "io.kvision:kvision-bootstrap-select:$kvisionVersion" - const val Spinner = "io.kvision:kvision-bootstrap-spinner:$kvisionVersion" - const val Upload = "io.kvision:kvision-bootstrap-upload:$kvisionVersion" - const val Dialog = "io.kvision:kvision-bootstrap-dialog:$kvisionVersion" - const val Typeahead = "io.kvision:kvision-bootstrap-typeahead:$kvisionVersion" - } - - const val FontAwesome = "io.kvision:kvision-fontawesome:$kvisionVersion" - const val i18n = "io.kvision:kvision-i18n:$kvisionVersion" - const val RichText = "io.kvision:kvision-richtext:$kvisionVersion" - const val Handlebars = "io.kvision:kvision-handlebars:$kvisionVersion" - const val DataContainer = "io.kvision:kvision-datacontainer:$kvisionVersion" - const val Chart = "io.kvision:kvision-chart:$kvisionVersion" - const val Tabulator = "io.kvision:kvision-tabulator:$kvisionVersion" - const val Pace = "io.kvision:kvision-pace:$kvisionVersion" - const val Toast = "io.kvision:kvision-toast:$kvisionVersion" - const val React = "io.kvision:kvision-react:$kvisionVersion" - const val Navigo = "io.kvision:kvision-routing-navigo:$kvisionVersion" - const val State = "io.kvision:kvision-state:$kvisionVersion" - const val Rest = "io.kvision:kvision-rest:$kvisionVersion" - } - - object Kotlinx { - const val Coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" - const val Serialization = "org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion" - const val Datetime = "org.jetbrains.kotlinx:kotlinx-datetime:$datetimeVersion" - const val Nodejs = "org.jetbrains.kotlinx:kotlinx-nodejs:$nodejsExternalsVersion" - } - - object Ktor { - object Client { - const val Core = "io.ktor:ktor-client-core:$ktorVersion" - const val Cio = "io.ktor:ktor-client-cio:$ktorVersion" - const val Serialization = "io.ktor:ktor-client-serialization:$ktorVersion" - } - - object Server { - const val Core = "io.ktor:ktor-server-core:$ktorVersion" - const val Cio = "io.ktor:ktor-server-cio:$ktorVersion" - const val Serialization = "io.ktor:ktor-serialization:$ktorVersion" - const val Openapi = "com.github.papsign:Ktor-OpenAPI-Generator:$ktorOpenapiVersion" - } - } - - object Exposed { - const val Core = "org.jetbrains.exposed:exposed-core:$exposedVersion" - const val Jdbc = "org.jetbrains.exposed:exposed-jdbc:$exposedVersion" - const val Time = "org.jetbrains.exposed:exposed-java-time:$exposedVersion" - } - - object Postgres { - const val Jdbc = "org.postgresql:postgresql:$postgresqlVersion" - } - - object Logback { - const val Classic = "ch.qos.logback:logback-classic:$logbackVersion" - } - - object Slf4j { - const val Simple = "org.slf4j:slf4j-simple:$slf4jJVersion" - } - - object Androidx { - const val AppCompat = - "androidx.appcompat:appcompat:$androidAppCompatVersion" - const val Material = - "com.google.android.material:material:$materialVersion" - const val RecyclerView = - "androidx.recyclerview:recyclerview:$recyclerViewVersion" - const val SwipeRefreshLayout = - "androidx.swiperefreshlayout:swiperefreshlayout:$swipeRefreshLayoutVersion" - const val ConstraintLayout = - "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion" - const val Lifecycle = - "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion" - const val LifecycleKtx = - "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" - - object Compose { - const val UI = "androidx.compose.ui:ui:$androidComposeVersion" - const val UITooling = "androidx.compose.ui:ui-tooling:$androidComposeVersion" - const val Foundation = "androidx.compose.foundation:foundation:$androidComposeVersion" - const val Material = "androidx.compose.material:material:$androidComposeVersion" - const val Icons = "androidx.compose.material:material-icons-core:$androidComposeVersion" - const val ExtendedIcons = "androidx.compose.material:material-icons-extended:$androidComposeVersion" - const val JUnitTests = "androidx.compose.ui:ui-test-junit4:$androidComposeVersion" - const val Activity = "androidx.activity:activity-compose:1.4.0-alpha02" - } - } - - object Bumtech { - const val Glide = - "com.github.bumptech.glide:glide:$glideVersion" - } - } - - object Kapt { - object Bumtech { - const val Glide = "com.github.bumptech.glide:compiler:$glideVersion" - } - } - - object Plugins { - object Configuration { - object Kotlin { - const val Mpp = "k-mpp" - const val Jvm = "k-jvm" - const val Js = "k-js" - - object Android { - const val App = "k-android-app" - const val Library = "k-android-library" - } - } - } - - object KSP { - const val Id = "com.google.devtools.ksp" - const val Classpath = "com.google.devtools.ksp:symbol-processing-gradle-plugin:$kspVersion" - } - - object Dependencies { - const val Id = "dependencies" - const val Classpath = "dependencies:dependencies:SNAPSHOT" - } - - object Kotlin { - const val Multiplatform = "org.jetbrains.kotlin.multiplatform" - const val Jvm = "org.jetbrains.kotlin.jvm" - const val Js = "org.jetbrains.kotlin.js" - const val Android = "org.jetbrains.kotlin.android" - const val Classpath = "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" - } - - object Android { - const val Application = "com.android.application" - const val Library = "com.android.library" - const val Classpath = "com.android.tools.build:gradle:$androidGradlePluginVersion" - } - - object Serialization { - const val Id = "org.jetbrains.kotlin.plugin.serialization" - const val Classpath = "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion" - } - - object Ssh { - const val Id = "org.hidetake.ssh" - const val Classpath = "org.hidetake:gradle-ssh-plugin:$sshVersion" - } - - object Publish { - const val Id = "publish" - const val Classpath = "publish:publish:SNAPSHOT" - } - - object MavenPublish { - const val Id = "maven-publish" - } - - object Application { - const val Id = "application" - } - - object Shadow { - const val Classpath = "gradle.plugin.com.github.jengelman.gradle.plugins:shadow:$shadowVer" - const val Id = "com.github.johnrengelman.shadow" - } - - object Deploy { - const val Id = "library-deploy" - } - } -} diff --git a/build-logic/dependencies/src/main/kotlin/unused/GradlePlugin.kt b/build-logic/dependencies/src/main/kotlin/unused/GradlePlugin.kt deleted file mode 100644 index 9d1e9fd..0000000 --- a/build-logic/dependencies/src/main/kotlin/unused/GradlePlugin.kt +++ /dev/null @@ -1,9 +0,0 @@ -package unused - -import org.gradle.api.Plugin -import org.gradle.api.Project - - -class GradlePlugin : Plugin { - override fun apply(target: Project) {} -} diff --git a/build-logic/library-deploy/build.gradle.kts b/build-logic/library-deploy/build.gradle.kts deleted file mode 100644 index 30b672d..0000000 --- a/build-logic/library-deploy/build.gradle.kts +++ /dev/null @@ -1,25 +0,0 @@ -plugins { - `kotlin-dsl` - id("dependencies") -} - -group = "library-deploy" -version = "SNAPSHOT" - -repositories { - gradlePluginPortal() - mavenCentral() -} - -gradlePlugin { - plugins.register("library-deploy") { - id = "library-deploy" - implementationClass = "DeployPlugin" - } -} - -dependencies { - implementation(Deps.Plugins.Dependencies.Classpath) - implementation(Deps.Plugins.Ssh.Classpath) - implementation(Deps.Plugins.Shadow.Classpath) -} \ No newline at end of file diff --git a/build-logic/library-deploy/src/main/kotlin/DeployExtension.kt b/build-logic/library-deploy/src/main/kotlin/DeployExtension.kt deleted file mode 100644 index 86f0dcb..0000000 --- a/build-logic/library-deploy/src/main/kotlin/DeployExtension.kt +++ /dev/null @@ -1,17 +0,0 @@ -open class DeployExtension { - var ignore = false - - // Required - var host: String? = null - var componentName: String? = null - var group: String? = null - var artifactId: String? = null - var version: String? = null - var name: String? = null - var description: String? = null - var deployPath: String? = null - - // Optional - var user: String? = null - var password: String? = null -} \ No newline at end of file diff --git a/build-logic/library-deploy/src/main/kotlin/DeployPlugin.kt b/build-logic/library-deploy/src/main/kotlin/DeployPlugin.kt deleted file mode 100644 index 28c2d87..0000000 --- a/build-logic/library-deploy/src/main/kotlin/DeployPlugin.kt +++ /dev/null @@ -1,67 +0,0 @@ -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.publish.PublishingExtension -import org.gradle.api.publish.maven.MavenPublication -import org.gradle.kotlin.dsl.apply -import org.gradle.kotlin.dsl.create -import org.gradle.kotlin.dsl.get -import org.gradle.kotlin.dsl.the -import org.gradle.util.GUtil.loadProperties -import java.io.File -import java.util.* - -class DeployPlugin : Plugin { - override fun apply(target: Project) { - target.apply(plugin = Deps.Plugins.MavenPublish.Id) - - val configuration = target.extensions.create(name = "deploy") - - target.afterEvaluate { - configuration.apply { - if (ignore) return@afterEvaluate - host ?: error("`host` should be defined in `deploy`") - deployPath ?: error("`deployPath` should be defined in `deploy`") - componentName ?: error("`componentName` should be defined in `deploy`") - name ?: error("`name` should be defined in `deploy`") - description ?: error("`description` should be defined in `deploy`") - } - - - project.the().apply { - publications { - create("deploy") { - group = configuration.group ?: project.group - artifactId = configuration.artifactId ?: project.name - version = configuration.version ?: error("shouldn't be null") - - pom { - name.set(configuration.name ?: error("shouldn't be null")) - description.set(configuration.description ?: error("shouldn't be null")) - } - from(components[configuration.componentName ?: error("shouldn't be null")]) - } - } - repositories { - maven { - name = configuration.name ?: error("shouldn't be null") - version = configuration.version ?: error("shouldn't be null") - - url = uri( - "sftp://${configuration.host}:22/${configuration.deployPath}" - ) - - credentials { - username = configuration.user - password = configuration.password - } - } - } - } - } - } - - private fun Project.properties(): Properties? = - rootProject.file("deploy.properties") - .takeIf(File::exists) - ?.let(::loadProperties) -} \ No newline at end of file diff --git a/build-logic/library-deploy/src/main/kotlin/SshSessionExtension.kt b/build-logic/library-deploy/src/main/kotlin/SshSessionExtension.kt deleted file mode 100644 index b6b0e4b..0000000 --- a/build-logic/library-deploy/src/main/kotlin/SshSessionExtension.kt +++ /dev/null @@ -1,17 +0,0 @@ -import org.gradle.api.Project -import org.gradle.kotlin.dsl.delegateClosureOf -import org.gradle.kotlin.dsl.the -import org.hidetake.groovy.ssh.core.Remote -import org.hidetake.groovy.ssh.core.RunHandler -import org.hidetake.groovy.ssh.core.Service -import org.hidetake.groovy.ssh.session.SessionHandler - - -open class SshSessionExtension( - private val project: Project, - private val remote: Remote -) { - operator fun invoke(handler: SessionHandler.() -> Unit) = project.the().apply { - run(delegateClosureOf { session(remote, delegateClosureOf(handler)) }) - } -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index c6b4456..213dfc8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,47 +1,32 @@ -import org.jetbrains.kotlin.konan.properties.loadProperties - plugins { - id(Deps.Plugins.Configuration.Kotlin.Mpp) - id(Deps.Plugins.Deploy.Id) + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.google.ksp) apply false + `maven-publish` } -group = AppInfo.PACKAGE -version = AppInfo.VERSION - -allprojects { - repositories { - mavenCentral() - } -} +group = "com.y9vad9.implier" +version = "1.0.5" kotlin { jvm() + jvmToolchain(21) sourceSets { - val commonMain by getting { + commonMain { kotlin.srcDir("src/main/kotlin") resources.srcDir("src/main/resources") } } } -val deployProperties = rootProject.file("deploy.properties") - -deploy { - if (!deployProperties.exists()) - ignore = true - else { - val properties = loadProperties(deployProperties.absolutePath) - host = properties["host"] as String? - user = properties["user"] as String? - password = properties["password"] as String? - deployPath = properties["deployPath"] as String? - - componentName = "kotlin" - group = AppInfo.PACKAGE - version = AppInfo.VERSION - artifactId = "implier" - name = "implier" - description = "Kotlin codegeneration library for Mutable & Immutable objects from interfaces." +publishing { + publications { + create("maven") { + from(components["kotlin"]) + groupId = "com.y9vad9.implier" + artifactId = "implier" + version = "1.0.5" + } } } \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts deleted file mode 100644 index cee47a3..0000000 --- a/buildSrc/build.gradle.kts +++ /dev/null @@ -1,12 +0,0 @@ -plugins { - id("dependencies") - `kotlin-dsl` -} - -repositories { - mavenCentral() -} - -dependencies { - implementation(Deps.Plugins.Dependencies.Classpath) -} diff --git a/gradle.properties b/gradle.properties index 2c12e23..da80a7b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,26 @@ -kotlin.mpp.stability.nowarn=true \ No newline at end of file +# Enable the Gradle daemon to avoid JVM startup cost +org.gradle.daemon=true + +# Run tasks in parallel when possible (independent modules) +org.gradle.parallel=true + +# Avoid configuring all projects if not needed (speeds up configuration) +org.gradle.configureondemand=true + +# Enable Gradle build cache (reuse task outputs) +org.gradle.caching=true + +# Enable incremental Kotlin compilation for faster builds +kotlin.incremental=true + +# Use Kotlin compiler daemon for performance +kotlin.daemon=true + +# Explicitly set Kotlin compilation strategy to daemon +kotlin.compiler.execution.strategy=daemon + +# Enable incremental compilation between modules +kotlin.incremental.compilation=true + +# Set memory options for Gradle daemon +org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..b0c2121 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,18 @@ +[versions] +kotlin = "2.4.20" +ksp = "2.3.12" +kotlinpoet = "2.4.0" +junit = "5.14.4" + +[libraries] +ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } +kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" } +kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinpoet" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlin" } +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } + +[plugins] +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +google-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e750102..79f3414 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/ksp/build.gradle.kts b/ksp/build.gradle.kts index 1b5cf0d..59a8d6c 100644 --- a/ksp/build.gradle.kts +++ b/ksp/build.gradle.kts @@ -1,42 +1,29 @@ plugins { - id(Deps.Plugins.Configuration.Kotlin.Mpp) - id(Deps.Plugins.Deploy.Id) + alias(libs.plugins.kotlin.jvm) + `maven-publish` } +group = "com.y9vad9.implier" +version = "1.0.5" + kotlin { - jvm() - sourceSets { - val jvmMain by getting { - dependencies { - implementation(Deps.Libs.KSP.Api) - implementation(Deps.Libs.KotlinPoet.KotlinPoet) - implementation(Deps.Libs.KotlinPoet.KSP) - implementation(Deps.Libs.Kotlin.Reflection) - implementation(project(":")) - } - kotlin.srcDir("src/main/kotlin") - resources.srcDir("src/main/resources") - } - } + jvmToolchain(21) } -val deployProperties = rootProject.file("deploy.properties") - -deploy { - if (!deployProperties.exists()) - ignore = true - else { - val properties = org.jetbrains.kotlin.konan.properties.loadProperties(deployProperties.absolutePath) - host = properties["host"] as String? - user = properties["user"] as String? - password = properties["password"] as String? - deployPath = properties["deployPath"] as String? +dependencies { + compileOnly(libs.ksp.api) + implementation(libs.kotlinpoet) + implementation(libs.kotlinpoet.ksp) + implementation(project(":")) +} - componentName = "kotlin" - group = AppInfo.PACKAGE - version = AppInfo.VERSION - artifactId = "ksp" - name = "implier ksp-implementation" - description = "Kotlin codegeneration library for Mutable & Immutable objects from interfaces." +publishing { + publications { + create("maven") { + from(components["java"]) + groupId = "com.y9vad9.implier" + artifactId = "ksp" + version = "1.0.5" + } } } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 2813075..68dd582 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,21 +1,28 @@ +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + pluginManagement { repositories { gradlePluginPortal() - google() mavenCentral() + google() } +} + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} - plugins { - id("org.jetbrains.kotlin.multiplatform") version "1.7.20-Beta" - id("com.google.devtools.ksp") version "1.7.20-Beta-1.0.6" +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven("https://jitpack.io") + maven("https://maven.y9vad9.com") } } rootProject.name = "implier" -includeBuild("build-logic/dependencies") -includeBuild("build-logic/configuration") -includeBuild("build-logic/library-deploy") - include(":ksp") include(":test") diff --git a/test/build.gradle.kts b/test/build.gradle.kts index b99f7fe..59a0c90 100644 --- a/test/build.gradle.kts +++ b/test/build.gradle.kts @@ -1,20 +1,18 @@ plugins { - id(Deps.Plugins.KSP.Id) - id(Deps.Plugins.Configuration.Kotlin.Jvm) + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.google.ksp) } -sourceSets { - test { - java { - srcDir("build/generated/ksp/test/kotlin") - } - } +kotlin { + jvmToolchain(21) } dependencies { implementation(project(":")) - implementation(Deps.Libs.Kotlin.JUnit) - kspTest(project(":ksp")) + testImplementation(libs.kotlin.test) + testImplementation(libs.kotlin.test.junit5) + testImplementation(libs.junit.jupiter) + kspTest(projects.ksp) } tasks.withType { From 8a202fb69e206b5c1c385faa5b0c4b71ece593dc Mon Sep 17 00:00:00 2001 From: y9vad9 Date: Sat, 3 Oct 2026 15:25:04 +0200 Subject: [PATCH 2/7] refactor(ksp): consolidate processors into deep coordinator and fix codegen bugs --- .../com/y9vad9/implier/AnnotationsVisitor.kt | 55 ----- .../implier/ImplierAnnotationProcessor.kt | 38 ---- .../com/y9vad9/implier/ImplierProcessor.kt | 190 ++++++++++++++++++ .../implier/ImplierProcessorProvider.kt | 5 +- .../com/y9vad9/implier/KspExtensions.kt | 62 ++++++ .../processor/AnnotatedClassProcessor.kt | 8 - .../BuilderImplAnnotatedClassProcessor.kt | 39 ---- .../DSLImplAnnotatedClassProcessor.kt | 44 ---- .../processor/DtoAnnotatedClassProcessor.kt | 36 ---- .../FactoryFunctionAnnotatedClassProcessor.kt | 43 ---- .../ImmutableAnnotatedClassProcessor.kt | 39 ---- .../MutableAnnotatedClassProcessor.kt | 33 --- .../implier/codegen/BuilderCodeGenerator.kt | 82 ++++++++ .../codegen/BuilderFileCodeGeneration.kt | 74 ------- .../implier/codegen/DSLCodeGenerator.kt | 102 ++++++++++ .../implier/codegen/DSLFileCodeGeneration.kt | 100 --------- .../implier/codegen/DtoCodeGenerator.kt | 82 ++++++++ .../implier/codegen/DtoFileCodeGeneration.kt | 82 -------- .../codegen/FactoryFunctionCodeGenerator.kt | 46 +++++ .../implier/codegen/FileCodeGeneration.kt | 20 -- .../FunctionFactoryFileCodeGeneration.kt | 46 ----- .../implier/codegen/ImmutableCodeGenerator.kt | 69 +++++++ .../ImplementationFileCodeGeneration.kt | 75 ------- .../implier/codegen/MutableCodeGenerator.kt | 65 ++++++ .../kotlin/com/y9vad9/implier/annotations.kt | 64 +++--- 25 files changed, 740 insertions(+), 759 deletions(-) delete mode 100644 ksp/src/main/kotlin/com/y9vad9/implier/AnnotationsVisitor.kt delete mode 100644 ksp/src/main/kotlin/com/y9vad9/implier/ImplierAnnotationProcessor.kt create mode 100644 ksp/src/main/kotlin/com/y9vad9/implier/ImplierProcessor.kt create mode 100644 ksp/src/main/kotlin/com/y9vad9/implier/KspExtensions.kt delete mode 100644 ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/AnnotatedClassProcessor.kt delete mode 100755 ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/BuilderImplAnnotatedClassProcessor.kt delete mode 100755 ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/DSLImplAnnotatedClassProcessor.kt delete mode 100755 ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/DtoAnnotatedClassProcessor.kt delete mode 100755 ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/FactoryFunctionAnnotatedClassProcessor.kt delete mode 100755 ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/ImmutableAnnotatedClassProcessor.kt delete mode 100755 ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/MutableAnnotatedClassProcessor.kt create mode 100644 ksp/src/main/kotlin/com/y9vad9/implier/codegen/BuilderCodeGenerator.kt delete mode 100755 ksp/src/main/kotlin/com/y9vad9/implier/codegen/BuilderFileCodeGeneration.kt create mode 100644 ksp/src/main/kotlin/com/y9vad9/implier/codegen/DSLCodeGenerator.kt delete mode 100755 ksp/src/main/kotlin/com/y9vad9/implier/codegen/DSLFileCodeGeneration.kt create mode 100644 ksp/src/main/kotlin/com/y9vad9/implier/codegen/DtoCodeGenerator.kt delete mode 100755 ksp/src/main/kotlin/com/y9vad9/implier/codegen/DtoFileCodeGeneration.kt create mode 100644 ksp/src/main/kotlin/com/y9vad9/implier/codegen/FactoryFunctionCodeGenerator.kt delete mode 100644 ksp/src/main/kotlin/com/y9vad9/implier/codegen/FileCodeGeneration.kt delete mode 100755 ksp/src/main/kotlin/com/y9vad9/implier/codegen/FunctionFactoryFileCodeGeneration.kt create mode 100644 ksp/src/main/kotlin/com/y9vad9/implier/codegen/ImmutableCodeGenerator.kt delete mode 100755 ksp/src/main/kotlin/com/y9vad9/implier/codegen/ImplementationFileCodeGeneration.kt create mode 100644 ksp/src/main/kotlin/com/y9vad9/implier/codegen/MutableCodeGenerator.kt diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/AnnotationsVisitor.kt b/ksp/src/main/kotlin/com/y9vad9/implier/AnnotationsVisitor.kt deleted file mode 100644 index 701af64..0000000 --- a/ksp/src/main/kotlin/com/y9vad9/implier/AnnotationsVisitor.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.y9vad9.implier - -import com.google.devtools.ksp.KspExperimental -import com.google.devtools.ksp.getAnnotationsByType -import com.google.devtools.ksp.isAnnotationPresent -import com.google.devtools.ksp.processing.CodeGenerator -import com.google.devtools.ksp.symbol.KSClassDeclaration -import com.google.devtools.ksp.symbol.KSVisitorVoid -import com.y9vad9.implier.annotations.processor.* - -class AnnotationsVisitor(private val codeGenerator: CodeGenerator) : KSVisitorVoid() { - @OptIn(KspExperimental::class) - override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) { - super.visitAnnotated(classDeclaration, data) - if (classDeclaration.isAnnotationPresent(MutableImpl::class)) { - MutableAnnotatedClassProcessor.process( - classDeclaration.getAnnotationsByType(MutableImpl::class).first(), - codeGenerator, - classDeclaration - ) - } - if (classDeclaration.isAnnotationPresent(ImmutableImpl::class)) { - ImmutableAnnotatedClassProcessor.process( - classDeclaration.getAnnotationsByType(ImmutableImpl::class).first(), codeGenerator, classDeclaration - ) - } - if (classDeclaration.isAnnotationPresent(FactoryFunctionImpl::class)) { - FactoryFunctionAnnotatedClassProcessor.process( - classDeclaration.getAnnotationsByType(FactoryFunctionImpl::class).first(), - codeGenerator, - classDeclaration - ) - } - if (classDeclaration.isAnnotationPresent(BuilderImpl::class)) { - BuilderImplAnnotatedClassProcessor.process( - classDeclaration.getAnnotationsByType(BuilderImpl::class).first(), - codeGenerator, - classDeclaration - ) - } - if (classDeclaration.isAnnotationPresent(DSLBuilderImpl::class)) { - DSLImplAnnotatedClassProcessor.process( - classDeclaration.getAnnotationsByType(DSLBuilderImpl::class).first(), - codeGenerator, - classDeclaration - ) - } - - if (classDeclaration.isAnnotationPresent(DtoImpl::class)) { - DtoAnnotatedClassProcessor.process( - classDeclaration.getAnnotationsByType(DtoImpl::class).first(), codeGenerator, classDeclaration - ) - } - } -} \ No newline at end of file diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/ImplierAnnotationProcessor.kt b/ksp/src/main/kotlin/com/y9vad9/implier/ImplierAnnotationProcessor.kt deleted file mode 100644 index 41321ce..0000000 --- a/ksp/src/main/kotlin/com/y9vad9/implier/ImplierAnnotationProcessor.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.y9vad9.implier - -import com.google.devtools.ksp.KspExperimental -import com.google.devtools.ksp.isAnnotationPresent -import com.google.devtools.ksp.processing.CodeGenerator -import com.google.devtools.ksp.processing.Resolver -import com.google.devtools.ksp.processing.SymbolProcessor -import com.google.devtools.ksp.symbol.KSAnnotated -import com.google.devtools.ksp.symbol.KSDeclaration - -class ImplierAnnotationProcessor(private val codeGenerator: CodeGenerator) : SymbolProcessor { - private var invoked = false - - @OptIn(KspExperimental::class) - override fun process(resolver: Resolver): List { - val annotations: Sequence = - resolver.getAllFiles().flatMap { it.declarations }.filter { - it.isAnnotationPresent(MutableImpl::class) - || it.isAnnotationPresent(ImmutableImpl::class) - || it.isAnnotationPresent( - FactoryFunctionImpl::class - ) - } - - if (invoked) { - return emptyList() - } - invoked = true - - val visitor = AnnotationsVisitor(codeGenerator) - - annotations.forEach { - it.accept(visitor, Unit) - } - - return listOf() - } -} \ No newline at end of file diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/ImplierProcessor.kt b/ksp/src/main/kotlin/com/y9vad9/implier/ImplierProcessor.kt new file mode 100644 index 0000000..704d07d --- /dev/null +++ b/ksp/src/main/kotlin/com/y9vad9/implier/ImplierProcessor.kt @@ -0,0 +1,190 @@ +package com.y9vad9.implier + +import com.google.devtools.ksp.isAbstract +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.symbol.ClassKind +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.Modifier +import com.google.devtools.ksp.validate +import com.squareup.kotlinpoet.ksp.writeTo +import com.y9vad9.implier.codegen.* + +class ImplierProcessor( + private val codeGenerator: CodeGenerator, + private val logger: KSPLogger, +) : SymbolProcessor { + + companion object { + const val IMMUTABLE_IMPL = "com.y9vad9.implier.ImmutableImpl" + const val MUTABLE_IMPL = "com.y9vad9.implier.MutableImpl" + const val FACTORY_FUNCTION_IMPL = "com.y9vad9.implier.FactoryFunctionImpl" + const val BUILDER_IMPL = "com.y9vad9.implier.BuilderImpl" + const val DSL_BUILDER_IMPL = "com.y9vad9.implier.DSLBuilderImpl" + const val DTO_IMPL = "com.y9vad9.implier.DtoImpl" + + val ALL_ANNOTATIONS = listOf( + IMMUTABLE_IMPL, + MUTABLE_IMPL, + FACTORY_FUNCTION_IMPL, + BUILDER_IMPL, + DSL_BUILDER_IMPL, + DTO_IMPL, + ) + } + + override fun process(resolver: Resolver): List { + val annotatedSymbols = ALL_ANNOTATIONS + .flatMap { resolver.getSymbolsWithAnnotation(it) } + .distinct() + .toList() + + val invalidSymbols = annotatedSymbols.filter { it !is KSClassDeclaration } + for (symbol in invalidSymbols) { + logger.error("Implier annotations can only be applied to interfaces or abstract classes", symbol) + } + + val classDeclarations = annotatedSymbols.filterIsInstance() + val (validDeclarations, unresolvable) = classDeclarations.partition { it.validate(enableNewFeatures = true) } + + for (declaration in validDeclarations) { + processDeclaration(declaration) + } + + return unresolvable + } + + private fun processDeclaration(declaration: KSClassDeclaration) { + val isInterface = declaration.classKind == ClassKind.INTERFACE + val isAbstractClass = declaration.classKind == ClassKind.CLASS && Modifier.ABSTRACT in declaration.modifiers + + if (!isInterface && !isAbstractClass) { + logger.error( + "Unable to create realization from non-abstract class: ${declaration.simpleName.asString()}", + declaration + ) + return + } + + val immutableAnnotation = declaration.findAnnotation(IMMUTABLE_IMPL) + val mutableAnnotation = declaration.findAnnotation(MUTABLE_IMPL) + val factoryAnnotation = declaration.findAnnotation(FACTORY_FUNCTION_IMPL) + val builderAnnotation = declaration.findAnnotation(BUILDER_IMPL) + val dslAnnotation = declaration.findAnnotation(DSL_BUILDER_IMPL) + val dtoAnnotation = declaration.findAnnotation(DTO_IMPL) + + val hasImmutable = immutableAnnotation != null + val hasMutable = mutableAnnotation != null + val hasFactory = factoryAnnotation != null + val hasBuilder = builderAnnotation != null + val hasDsl = dslAnnotation != null + val hasDto = dtoAnnotation != null + + val requiresRealization = hasFactory || hasBuilder || hasDsl || hasDto + if (requiresRealization && !hasImmutable && !hasMutable) { + logger.error( + "Declaration '${declaration.simpleName.asString()}' requires at least one of @ImmutableImpl or @MutableImpl to generate factory, builder, DSL, or DTO implementations", + declaration + ) + return + } + + val properties = declaration.getAllProperties() + .filter { it.isAbstract() } + .distinctBy { it.simpleName.asString() } + .toList() + + val isDataClassEligible = properties.isNotEmpty() && ( + isInterface || ( + declaration.primaryConstructor == null || declaration.primaryConstructor!!.parameters.isEmpty() + ) + ) + + val dependencies = Dependencies( + aggregating = false, + sources = listOfNotNull(declaration.containingFile).toTypedArray() + ) + + if (immutableAnnotation != null) { + val visibility = immutableAnnotation.getVisibility() + val fileSpec = ImmutableCodeGenerator.generate( + declaration = declaration, + properties = properties, + visibility = visibility, + isDataClass = isDataClassEligible, + ) + fileSpec.writeTo(codeGenerator, dependencies) + } + + if (mutableAnnotation != null) { + val visibility = mutableAnnotation.getVisibility() + val fileSpec = MutableCodeGenerator.generate( + declaration = declaration, + properties = properties, + visibility = visibility, + ) + fileSpec.writeTo(codeGenerator, dependencies) + } + + if (factoryAnnotation != null) { + val visibility = factoryAnnotation.getVisibility() + val fileSpec = FactoryFunctionCodeGenerator.generate( + declaration = declaration, + properties = properties, + visibility = visibility, + preferImmutable = hasImmutable, + ) + fileSpec.writeTo(codeGenerator, dependencies) + } + + if (builderAnnotation != null) { + val visibility = builderAnnotation.getVisibility() + val builderType = builderAnnotation.getBuilderType() + val fileSpec = BuilderCodeGenerator.generate( + declaration = declaration, + properties = properties, + visibility = visibility, + builderType = builderType, + preferImmutable = hasImmutable, + ) + fileSpec.writeTo(codeGenerator, dependencies) + } + + if (dslAnnotation != null) { + val visibility = dslAnnotation.getVisibility() + val dslType = dslAnnotation.getDSLBuilderType() + val functionName = dslAnnotation.getDSLFunctionName() + if (functionName.isNullOrBlank()) { + logger.error( + "DSLBuilderImpl on '${declaration.simpleName.asString()}' must specify a non-empty functionName", + declaration + ) + return + } + val fileSpec = DSLCodeGenerator.generate( + declaration = declaration, + properties = properties, + visibility = visibility, + dslType = dslType, + functionName = functionName, + preferImmutable = hasImmutable, + ) + fileSpec.writeTo(codeGenerator, dependencies) + } + + if (dtoAnnotation != null) { + val visibility = dtoAnnotation.getVisibility() + val fileSpec = DtoCodeGenerator.generate( + declaration = declaration, + properties = properties, + visibility = visibility, + preferImmutable = hasImmutable, + ) + fileSpec.writeTo(codeGenerator, dependencies) + } + } +} diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/ImplierProcessorProvider.kt b/ksp/src/main/kotlin/com/y9vad9/implier/ImplierProcessorProvider.kt index 4b6dbac..3f1fc89 100644 --- a/ksp/src/main/kotlin/com/y9vad9/implier/ImplierProcessorProvider.kt +++ b/ksp/src/main/kotlin/com/y9vad9/implier/ImplierProcessorProvider.kt @@ -6,6 +6,9 @@ import com.google.devtools.ksp.processing.SymbolProcessorProvider class ImplierProcessorProvider : SymbolProcessorProvider { override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { - return ImplierAnnotationProcessor(environment.codeGenerator) + return ImplierProcessor( + codeGenerator = environment.codeGenerator, + logger = environment.logger, + ) } } \ No newline at end of file diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/KspExtensions.kt b/ksp/src/main/kotlin/com/y9vad9/implier/KspExtensions.kt new file mode 100644 index 0000000..34fb650 --- /dev/null +++ b/ksp/src/main/kotlin/com/y9vad9/implier/KspExtensions.kt @@ -0,0 +1,62 @@ +package com.y9vad9.implier + +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSClassDeclaration +import java.util.Locale + +internal fun KSAnnotated.findAnnotation(qualifiedName: String): KSAnnotation? { + return annotations.firstOrNull { + it.annotationType.resolve().declaration.qualifiedName?.asString() == qualifiedName + } +} + +internal fun KSAnnotated.hasAnnotation(qualifiedName: String): Boolean { + return findAnnotation(qualifiedName) != null +} + +internal fun KSAnnotation.getArgument(name: String): Any? { + return arguments.firstOrNull { it.name?.asString() == name }?.value +} + +internal fun KSAnnotation.getVisibility(): Visibility { + val value = getArgument("visibility") + val name = when (value) { + is KSClassDeclaration -> value.simpleName.asString() + is Visibility -> value.name + else -> value?.toString()?.substringAfterLast('.') + } + return if (name == "INTERNAL") Visibility.INTERNAL else Visibility.PUBLIC +} + +internal fun KSAnnotation.getBuilderType(): BuilderImpl.Type { + val value = getArgument("type") + val name = when (value) { + is KSClassDeclaration -> value.simpleName.asString() + is BuilderImpl.Type -> value.name + else -> value?.toString()?.substringAfterLast('.') + } + return if (name == "WITH_ACCESSORS") BuilderImpl.Type.WITH_ACCESSORS else BuilderImpl.Type.WITHOUT_ACCESSORS +} + +internal fun KSAnnotation.getDSLBuilderType(): DSLBuilderImpl.Type { + val value = getArgument("type") + val name = when (value) { + is KSClassDeclaration -> value.simpleName.asString() + is DSLBuilderImpl.Type -> value.name + else -> value?.toString()?.substringAfterLast('.') + } + return when (name) { + "WITH_ACCESSORS" -> DSLBuilderImpl.Type.WITH_ACCESSORS + "WITHOUT_ACCESSORS" -> DSLBuilderImpl.Type.WITHOUT_ACCESSORS + else -> DSLBuilderImpl.Type.PROPERTY_ACCESS + } +} + +internal fun KSAnnotation.getDSLFunctionName(): String? { + return getArgument("functionName") as? String +} + +internal fun String.capitalizeFirst(): String { + return replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() } +} diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/AnnotatedClassProcessor.kt b/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/AnnotatedClassProcessor.kt deleted file mode 100644 index 2d5fae5..0000000 --- a/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/AnnotatedClassProcessor.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.y9vad9.implier.annotations.processor - -import com.google.devtools.ksp.processing.CodeGenerator -import com.google.devtools.ksp.symbol.KSClassDeclaration - -interface AnnotatedClassProcessor { - fun process(annotation: T, codeGenerator: CodeGenerator, classDeclaration: KSClassDeclaration) -} \ No newline at end of file diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/BuilderImplAnnotatedClassProcessor.kt b/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/BuilderImplAnnotatedClassProcessor.kt deleted file mode 100755 index 6635194..0000000 --- a/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/BuilderImplAnnotatedClassProcessor.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.y9vad9.implier.annotations.processor - -import com.google.devtools.ksp.KspExperimental -import com.google.devtools.ksp.isAnnotationPresent -import com.google.devtools.ksp.processing.CodeGenerator -import com.google.devtools.ksp.processing.Dependencies -import com.google.devtools.ksp.symbol.KSClassDeclaration -import com.y9vad9.implier.BuilderImpl -import com.y9vad9.implier.ImmutableImpl -import com.y9vad9.implier.MutableImpl -import com.y9vad9.implier.codegen.BuilderFileCodeGeneration -import com.y9vad9.implier.codegen.BuilderFileCodeGeneration.generate -import java.io.OutputStreamWriter - -object BuilderImplAnnotatedClassProcessor : AnnotatedClassProcessor { - @OptIn(KspExperimental::class) - override fun process(annotation: BuilderImpl, codeGenerator: CodeGenerator, classDeclaration: KSClassDeclaration) { - if (!(classDeclaration.isAnnotationPresent(ImmutableImpl::class) - && classDeclaration.isAnnotationPresent(MutableImpl::class)) - ) - throw IllegalStateException("Unable to create builder implementation without Immutable / Mutable implementations.") - codeGenerator.createNewFile( - Dependencies(false), - classDeclaration.packageName.asString(), - classDeclaration.simpleName.asString().plus("Builder") - ).use { output -> - OutputStreamWriter(output).use { writer -> - BuilderFileCodeGeneration.Data( - type = annotation.type, - initVariantCode = (if (!(classDeclaration.isAnnotationPresent(ImmutableImpl::class))) "Immutable" else "Mutable").plus( - classDeclaration.simpleName.asString() - ), - visibility = annotation.visibility, - declaration = classDeclaration - ).generate().writeTo(writer) - } - } - } -} \ No newline at end of file diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/DSLImplAnnotatedClassProcessor.kt b/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/DSLImplAnnotatedClassProcessor.kt deleted file mode 100755 index 867c273..0000000 --- a/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/DSLImplAnnotatedClassProcessor.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.y9vad9.implier.annotations.processor - -import com.google.devtools.ksp.KspExperimental -import com.google.devtools.ksp.isAnnotationPresent -import com.google.devtools.ksp.processing.CodeGenerator -import com.google.devtools.ksp.processing.Dependencies -import com.google.devtools.ksp.symbol.KSClassDeclaration -import com.y9vad9.implier.DSLBuilderImpl -import com.y9vad9.implier.ImmutableImpl -import com.y9vad9.implier.MutableImpl -import com.y9vad9.implier.codegen.DSLFileCodeGeneration -import com.y9vad9.implier.codegen.DSLFileCodeGeneration.generate -import java.io.OutputStreamWriter - -object DSLImplAnnotatedClassProcessor : AnnotatedClassProcessor { - @OptIn(KspExperimental::class) - override fun process( - annotation: DSLBuilderImpl, - codeGenerator: CodeGenerator, - classDeclaration: KSClassDeclaration - ) { - if (!(classDeclaration.isAnnotationPresent(ImmutableImpl::class) - && classDeclaration.isAnnotationPresent(MutableImpl::class)) - ) - throw IllegalStateException("Unable to create builder implementation without Immutable / Mutable implementations.") - codeGenerator.createNewFile( - Dependencies(false), - classDeclaration.packageName.asString(), - classDeclaration.simpleName.asString().plus("BuilderScope") - ).use { output -> - OutputStreamWriter(output).use { writer -> - DSLFileCodeGeneration.Data( - type = annotation.type, - initVariantCode = (if (!(classDeclaration.isAnnotationPresent(ImmutableImpl::class))) "Immutable" else "Mutable").plus( - classDeclaration.simpleName.asString() - ), - functionName = annotation.functionName, - declaration = classDeclaration, - visibility = annotation.visibility - ).generate().writeTo(writer) - } - } - } -} diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/DtoAnnotatedClassProcessor.kt b/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/DtoAnnotatedClassProcessor.kt deleted file mode 100755 index 465f414..0000000 --- a/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/DtoAnnotatedClassProcessor.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.y9vad9.implier.annotations.processor - -import com.google.devtools.ksp.processing.CodeGenerator -import com.google.devtools.ksp.processing.Dependencies -import com.google.devtools.ksp.symbol.ClassKind -import com.google.devtools.ksp.symbol.KSClassDeclaration -import com.google.devtools.ksp.symbol.Modifier -import com.y9vad9.implier.Dto -import com.y9vad9.implier.DtoImpl -import com.y9vad9.implier.codegen.DtoFileCodeGeneration -import com.y9vad9.implier.codegen.DtoFileCodeGeneration.generate -import java.io.OutputStreamWriter - -object DtoAnnotatedClassProcessor : AnnotatedClassProcessor { - override fun process(annotation: DtoImpl, codeGenerator: CodeGenerator, classDeclaration: KSClassDeclaration) { - validateClassDeclaration(classDeclaration) - - codeGenerator.createNewFile( - Dependencies(false), - classDeclaration.packageName.asString(), - "Dto${classDeclaration.simpleName.asString()}" - ).use { output -> - OutputStreamWriter(output).use { writer -> - DtoFileCodeGeneration.Data(Dto::class, annotation.visibility, classDeclaration).generate() - .writeTo(writer) - } - } - } - - private fun validateClassDeclaration(classDeclaration: KSClassDeclaration) { - if (classDeclaration.classKind == ClassKind.CLASS && Modifier.ABSTRACT !in classDeclaration.modifiers) - throw IllegalStateException("Unable to create realization from non-abstract class") - else if (classDeclaration.classKind != ClassKind.INTERFACE) - throw IllegalStateException("Unable to create realization from ${classDeclaration.classKind}.") - } -} \ No newline at end of file diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/FactoryFunctionAnnotatedClassProcessor.kt b/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/FactoryFunctionAnnotatedClassProcessor.kt deleted file mode 100755 index 3a86cbc..0000000 --- a/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/FactoryFunctionAnnotatedClassProcessor.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.y9vad9.implier.annotations.processor - -import com.google.devtools.ksp.KspExperimental -import com.google.devtools.ksp.isAnnotationPresent -import com.google.devtools.ksp.processing.CodeGenerator -import com.google.devtools.ksp.processing.Dependencies -import com.google.devtools.ksp.symbol.KSClassDeclaration -import com.y9vad9.implier.FactoryFunctionImpl -import com.y9vad9.implier.ImmutableImpl -import com.y9vad9.implier.MutableImpl -import com.y9vad9.implier.codegen.FunctionFactoryFileCodeGeneration -import com.y9vad9.implier.codegen.FunctionFactoryFileCodeGeneration.generate -import java.io.OutputStreamWriter - -object FactoryFunctionAnnotatedClassProcessor : AnnotatedClassProcessor { - @OptIn(KspExperimental::class) - override fun process( - annotation: FactoryFunctionImpl, - codeGenerator: CodeGenerator, - classDeclaration: KSClassDeclaration - ) { - val variantName = if (classDeclaration.isAnnotationPresent(ImmutableImpl::class)) - "Immutable" - else if (classDeclaration.isAnnotationPresent(MutableImpl::class)) - "Mutable" - else throw IllegalStateException( - "Unable to create factory function for interface / class that does not have Mutable or Immutable realization" - ) - if (classDeclaration.isAnnotationPresent(ImmutableImpl::class)) { - codeGenerator.createNewFile( - Dependencies(false), - classDeclaration.packageName.asString(), - classDeclaration.simpleName.asString().plus("Factory") - ).use { output -> - OutputStreamWriter(output).use { writer -> - FunctionFactoryFileCodeGeneration.Data(variantName, annotation.visibility, classDeclaration) - .generate() - .writeTo(writer) - } - } - } - } -} \ No newline at end of file diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/ImmutableAnnotatedClassProcessor.kt b/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/ImmutableAnnotatedClassProcessor.kt deleted file mode 100755 index abd4eff..0000000 --- a/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/ImmutableAnnotatedClassProcessor.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.y9vad9.implier.annotations.processor - -import com.google.devtools.ksp.processing.CodeGenerator -import com.google.devtools.ksp.processing.Dependencies -import com.google.devtools.ksp.symbol.ClassKind -import com.google.devtools.ksp.symbol.KSClassDeclaration -import com.google.devtools.ksp.symbol.Modifier -import com.y9vad9.implier.Immutable -import com.y9vad9.implier.ImmutableImpl -import com.y9vad9.implier.codegen.ImplementationFileCodeGeneration -import com.y9vad9.implier.codegen.ImplementationFileCodeGeneration.generate -import java.io.OutputStreamWriter - -object ImmutableAnnotatedClassProcessor : AnnotatedClassProcessor { - override fun process( - annotation: ImmutableImpl, - codeGenerator: CodeGenerator, - classDeclaration: KSClassDeclaration - ) { - if (classDeclaration.classKind == ClassKind.CLASS && Modifier.ABSTRACT !in classDeclaration.modifiers) - throw IllegalStateException("Unable to create realization from non-abstract class") - else if (classDeclaration.classKind != ClassKind.INTERFACE) - throw IllegalStateException("Unable to create realization from ${classDeclaration.classKind}.") - - codeGenerator.createNewFile( - Dependencies(false), - classDeclaration.packageName.asString(), - "Immutable${classDeclaration.simpleName.asString()}" - ).use { output -> - OutputStreamWriter(output).use { writer -> - ImplementationFileCodeGeneration.Data( - marker = Immutable::class, - declaration = classDeclaration, - visibility = annotation.visibility - ).generate().writeTo(writer) - } - } - } -} \ No newline at end of file diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/MutableAnnotatedClassProcessor.kt b/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/MutableAnnotatedClassProcessor.kt deleted file mode 100755 index eeb197e..0000000 --- a/ksp/src/main/kotlin/com/y9vad9/implier/annotations/processor/MutableAnnotatedClassProcessor.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.y9vad9.implier.annotations.processor - -import com.google.devtools.ksp.processing.CodeGenerator -import com.google.devtools.ksp.processing.Dependencies -import com.google.devtools.ksp.symbol.ClassKind -import com.google.devtools.ksp.symbol.KSClassDeclaration -import com.google.devtools.ksp.symbol.Modifier -import com.y9vad9.implier.Mutable -import com.y9vad9.implier.MutableImpl -import com.y9vad9.implier.codegen.ImplementationFileCodeGeneration -import com.y9vad9.implier.codegen.ImplementationFileCodeGeneration.generate -import java.io.OutputStreamWriter - -object MutableAnnotatedClassProcessor : AnnotatedClassProcessor { - override fun process(annotation: MutableImpl, codeGenerator: CodeGenerator, classDeclaration: KSClassDeclaration) { - if (classDeclaration.classKind == ClassKind.CLASS && Modifier.ABSTRACT !in classDeclaration.modifiers) - throw IllegalStateException("Unable to create realization from non-abstract class") - else if (classDeclaration.classKind != ClassKind.INTERFACE) - throw IllegalStateException("Unable to create realization from ${classDeclaration.classKind}.") - - codeGenerator.createNewFile( - Dependencies(false), - classDeclaration.packageName.asString(), - "Mutable${classDeclaration.simpleName.asString()}" - ).use { output -> - OutputStreamWriter(output).use { writer -> - ImplementationFileCodeGeneration.Data(marker = Mutable::class, annotation.visibility, classDeclaration) - .generate() - .writeTo(writer) - } - } - } -} \ No newline at end of file diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/codegen/BuilderCodeGenerator.kt b/ksp/src/main/kotlin/com/y9vad9/implier/codegen/BuilderCodeGenerator.kt new file mode 100644 index 0000000..ecc14a8 --- /dev/null +++ b/ksp/src/main/kotlin/com/y9vad9/implier/codegen/BuilderCodeGenerator.kt @@ -0,0 +1,82 @@ +package com.y9vad9.implier.codegen + +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ksp.toClassName +import com.squareup.kotlinpoet.ksp.toTypeName +import com.y9vad9.implier.BuilderImpl +import com.y9vad9.implier.Visibility +import com.y9vad9.implier.capitalizeFirst + +object BuilderCodeGenerator { + fun generate( + declaration: KSClassDeclaration, + properties: List, + visibility: Visibility, + builderType: BuilderImpl.Type, + preferImmutable: Boolean, + ): FileSpec { + val packageName = declaration.packageName.asString() + val simpleName = declaration.simpleName.asString() + val className = "${simpleName}Builder" + val visibilityModifier = if (visibility == Visibility.PUBLIC) KModifier.PUBLIC else KModifier.INTERNAL + val builderClass = ClassName(packageName, className) + + val targetRealizationName = if (preferImmutable) "Immutable$simpleName" else "Mutable$simpleName" + val targetClass = ClassName(packageName, targetRealizationName) + + val typeSpecBuilder = TypeSpec.classBuilder(className) + .addModifiers(visibilityModifier) + + for (property in properties) { + val propName = property.simpleName.asString() + val resolvedType = property.type.resolve() + val propType = resolvedType.toTypeName() + val isNullable = resolvedType.isMarkedNullable + + val propBuilder = PropertySpec.builder(propName, propType) + .mutable(true) + .addModifiers(KModifier.PRIVATE) + + if (isNullable) { + propBuilder.initializer("null") + } else { + propBuilder.delegate("kotlin.properties.Delegates.notNull()") + } + typeSpecBuilder.addProperty(propBuilder.build()) + + val methodName = if (builderType == BuilderImpl.Type.WITHOUT_ACCESSORS) { + propName + } else { + "set" + propName.capitalizeFirst() + } + + typeSpecBuilder.addFunction( + FunSpec.builder(methodName) + .addModifiers(visibilityModifier) + .addParameter("value", propType) + .returns(builderClass) + .addStatement("this.%N = value", propName) + .addStatement("return this") + .build() + ) + } + + typeSpecBuilder.addFunction( + FunSpec.builder("build") + .addModifiers(visibilityModifier) + .returns(declaration.toClassName()) + .addStatement( + "return %T(%L)", + targetClass, + properties.joinToString(", ") { it.simpleName.asString() } + ) + .build() + ) + + return FileSpec.builder(packageName, className) + .addType(typeSpecBuilder.build()) + .build() + } +} diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/codegen/BuilderFileCodeGeneration.kt b/ksp/src/main/kotlin/com/y9vad9/implier/codegen/BuilderFileCodeGeneration.kt deleted file mode 100755 index b5cc7a8..0000000 --- a/ksp/src/main/kotlin/com/y9vad9/implier/codegen/BuilderFileCodeGeneration.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.y9vad9.implier.codegen - -import com.google.devtools.ksp.symbol.KSClassDeclaration -import com.squareup.kotlinpoet.* -import com.squareup.kotlinpoet.ksp.toClassName -import com.y9vad9.implier.BuilderImpl -import com.y9vad9.implier.Visibility -import java.util.* - -object BuilderFileCodeGeneration : FileCodeGeneration { - class Data( - private val type: BuilderImpl.Type, - val visibility: Visibility, - val initVariantCode: String, - declaration: KSClassDeclaration - ) : - FileCodeGeneration.Data(declaration) { - val name: String get() = simpleName.plus("Builder") - - fun TypeSpec.Builder.applyMembers(): TypeSpec.Builder { - for (member in declaration.getAllProperties()) { - val resolvedMember = member.type.resolve() - addProperty( - PropertySpec.builder(member.simpleName.asString(), resolvedMember.toClassName()) - .mutable(true) - .delegate("kotlin.properties.Delegates.notNull()") - .addModifiers(KModifier.PRIVATE) - .build() - ) - addFunction( - FunSpec.builder( - if (type == BuilderImpl.Type.WITHOUT_ACCESSORS) member.simpleName.asString() else "set${ - member.simpleName.asString() - .replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } - }" - ).addParameter( - "value", - resolvedMember.toClassName() - ) - .returns(ClassName(packageName, name)) - .addCode("${member.simpleName.asString()} = value\n") - .addCode("return this") - .build()) - } - return this - } - } - - override fun Data.generate(): FileSpec { - return FileSpec.builder(packageName, simpleName) - .addType( - TypeSpec.classBuilder(name) - .addModifiers(if (visibility == Visibility.PUBLIC) KModifier.PUBLIC else KModifier.INTERNAL) - .applyMembers() - .addFunction( - FunSpec.builder("build") - .addCode( - "return $initVariantCode(${ - declaration.getAllProperties().joinToString(",") { it.simpleName.asString() } - })" - ) - .returns( - ClassName( - packageName, - simpleName - ) - ) - .build() - ) - .build() - ) - .build() - } -} \ No newline at end of file diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/codegen/DSLCodeGenerator.kt b/ksp/src/main/kotlin/com/y9vad9/implier/codegen/DSLCodeGenerator.kt new file mode 100644 index 0000000..03d7457 --- /dev/null +++ b/ksp/src/main/kotlin/com/y9vad9/implier/codegen/DSLCodeGenerator.kt @@ -0,0 +1,102 @@ +package com.y9vad9.implier.codegen + +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ksp.toClassName +import com.squareup.kotlinpoet.ksp.toTypeName +import com.y9vad9.implier.DSLBuilderImpl +import com.y9vad9.implier.Visibility +import com.y9vad9.implier.capitalizeFirst + +object DSLCodeGenerator { + fun generate( + declaration: KSClassDeclaration, + properties: List, + visibility: Visibility, + dslType: DSLBuilderImpl.Type, + functionName: String, + preferImmutable: Boolean, + ): FileSpec { + val packageName = declaration.packageName.asString() + val simpleName = declaration.simpleName.asString() + val className = "${simpleName}DSLBuilderScope" + val fileName = "${simpleName}BuilderScope" + val visibilityModifier = if (visibility == Visibility.PUBLIC) KModifier.PUBLIC else KModifier.INTERNAL + val scopeClass = ClassName(packageName, className) + + val targetRealizationName = if (preferImmutable) "Immutable$simpleName" else "Mutable$simpleName" + val targetClass = ClassName(packageName, targetRealizationName) + + val typeSpecBuilder = TypeSpec.classBuilder(className) + .addModifiers(visibilityModifier) + + for (property in properties) { + val propName = property.simpleName.asString() + val resolvedType = property.type.resolve() + val propType = resolvedType.toTypeName() + val isNullable = resolvedType.isMarkedNullable + + val propBuilder = PropertySpec.builder(propName, propType) + .mutable(true) + + if (isNullable) { + propBuilder.initializer("null") + } else { + propBuilder.delegate("kotlin.properties.Delegates.notNull()") + } + typeSpecBuilder.addProperty(propBuilder.build()) + + when (dslType) { + DSLBuilderImpl.Type.PROPERTY_ACCESS -> { + // Property access only + } + DSLBuilderImpl.Type.WITHOUT_ACCESSORS -> { + typeSpecBuilder.addFunction( + FunSpec.builder(propName) + .addParameter("value", propType) + .returns(scopeClass) + .addStatement("this.%N = value", propName) + .addStatement("return this") + .build() + ) + } + DSLBuilderImpl.Type.WITH_ACCESSORS -> { + val methodName = "set" + propName.capitalizeFirst() + typeSpecBuilder.addFunction( + FunSpec.builder(methodName) + .addParameter("value", propType) + .returns(scopeClass) + .addStatement("this.%N = value", propName) + .addStatement("return this") + .build() + ) + } + } + } + + val dslFunction = FunSpec.builder(functionName) + .addModifiers(visibilityModifier) + .addParameter( + "block", + LambdaTypeName.get( + receiver = scopeClass, + returnType = Unit::class.asTypeName() + ) + ) + .returns(declaration.toClassName()) + .addStatement("val dslBuilder = %T()", scopeClass) + .addStatement("dslBuilder.apply(block)") + .addStatement( + "return %T(%L)", + targetClass, + properties.joinToString(", ") { "dslBuilder.${it.simpleName.asString()}" } + ) + .build() + + return FileSpec.builder(packageName, fileName) + .addType(typeSpecBuilder.build()) + .addFunction(dslFunction) + .build() + } +} diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/codegen/DSLFileCodeGeneration.kt b/ksp/src/main/kotlin/com/y9vad9/implier/codegen/DSLFileCodeGeneration.kt deleted file mode 100755 index 4a5026b..0000000 --- a/ksp/src/main/kotlin/com/y9vad9/implier/codegen/DSLFileCodeGeneration.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.y9vad9.implier.codegen - -import com.google.devtools.ksp.symbol.KSClassDeclaration -import com.squareup.kotlinpoet.* -import com.squareup.kotlinpoet.ksp.toClassName -import com.y9vad9.implier.DSLBuilderImpl -import com.y9vad9.implier.Visibility -import java.util.* - -object DSLFileCodeGeneration : FileCodeGeneration { - class Data( - private val type: DSLBuilderImpl.Type, - val initVariantCode: String, - val visibility: Visibility, - val functionName: String, - declaration: KSClassDeclaration - ) : - FileCodeGeneration.Data(declaration) { - val name: String get() = simpleName.plus("DSLBuilderScope") - - fun TypeSpec.Builder.applyMembers(): TypeSpec.Builder { - for (member in declaration.getAllProperties()) - when (type) { - DSLBuilderImpl.Type.PROPERTY_ACCESS -> addProperty( - PropertySpec.builder(member.simpleName.asString(), member.type.resolve().toClassName()) - .mutable(true) - .delegate("kotlin.properties.Delegates.notNull()") - .build() - ) - else -> { - val resolvedMember = member.type.resolve() - addProperty( - PropertySpec.builder(member.simpleName.asString(), resolvedMember.toClassName()) - .mutable(true) - .delegate("kotlin.properties.Delegates.notNull()") - .addModifiers(KModifier.PUBLIC) - .build() - ) - addFunction( - FunSpec.builder( - if (type == DSLBuilderImpl.Type.WITHOUT_ACCESSORS) member.simpleName.asString() else "set${ - member.simpleName.asString() - .replaceFirstChar { - if (it.isLowerCase()) it.titlecase(Locale.getDefault()) - else it.toString() - } - }" - ).addParameter( - "value", - resolvedMember.toClassName() - ) - .returns(ClassName(packageName, name)) - .addCode("${member.simpleName.asString()} = value\n") - .addCode("return this") - .build() - ) - } - } - return this - } - - fun FileSpec.Builder.addDSLFunction(): FileSpec.Builder { - addFunction(FunSpec.builder(functionName) - .addModifiers(if (visibility == Visibility.PUBLIC) KModifier.PUBLIC else KModifier.INTERNAL) - .addParameter( - "block", - LambdaTypeName.get( - receiver = ClassName(packageName, name), returnType = Unit::class.asTypeName() - ) - ).addCode( - """ - val dslBuilder = $name() - dslBuilder.apply(block) - return $initVariantCode(${ - declaration.getAllProperties() - .joinToString(",") { "dslBuilder." + it.simpleName.asString() } - }) - """.trimIndent() - ) - .returns( - declaration.toClassName() - ) - .build() - ).build() - return this - } - } - - override fun Data.generate(): FileSpec { - return FileSpec.builder(packageName, name) - .addType( - TypeSpec.classBuilder(name) - .addModifiers(if (visibility == Visibility.PUBLIC) KModifier.PUBLIC else KModifier.INTERNAL) - .applyMembers() - .build() - ) - .addDSLFunction() - .build() - } -} \ No newline at end of file diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/codegen/DtoCodeGenerator.kt b/ksp/src/main/kotlin/com/y9vad9/implier/codegen/DtoCodeGenerator.kt new file mode 100644 index 0000000..b591981 --- /dev/null +++ b/ksp/src/main/kotlin/com/y9vad9/implier/codegen/DtoCodeGenerator.kt @@ -0,0 +1,82 @@ +package com.y9vad9.implier.codegen + +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.ksp.toClassName +import com.squareup.kotlinpoet.ksp.toTypeName +import com.y9vad9.implier.Dto +import com.y9vad9.implier.Visibility + +object DtoCodeGenerator { + fun generate( + declaration: KSClassDeclaration, + properties: List, + visibility: Visibility, + preferImmutable: Boolean, + ): FileSpec { + val packageName = declaration.packageName.asString() + val simpleName = declaration.simpleName.asString() + val className = "Dto$simpleName" + val visibilityModifier = if (visibility == Visibility.PUBLIC) KModifier.PUBLIC else KModifier.INTERNAL + val dtoClass = ClassName(packageName, className) + + val targetRealizationName = if (preferImmutable) "Immutable$simpleName" else "Mutable$simpleName" + val targetClass = ClassName(packageName, targetRealizationName) + + val typeSpecBuilder = TypeSpec.classBuilder(className) + .addModifiers(visibilityModifier) + .addSuperinterface(Dto::class.asClassName().parameterizedBy(declaration.toClassName())) + + val constructorBuilder = FunSpec.constructorBuilder() + for (property in properties) { + val propName = property.simpleName.asString() + val nullablePropType = property.type.resolve().toTypeName().copy(nullable = true) + constructorBuilder.addParameter( + ParameterSpec.builder(propName, nullablePropType) + .defaultValue("null") + .build() + ) + typeSpecBuilder.addProperty( + PropertySpec.builder(propName, nullablePropType) + .mutable(true) + .initializer(propName) + .build() + ) + } + typeSpecBuilder.primaryConstructor(constructorBuilder.build()) + + val toDtoFunction = FunSpec.builder("toDto") + .addModifiers(visibilityModifier) + .receiver(declaration.toClassName()) + .returns(dtoClass) + .addStatement( + "return %T(%L)", + dtoClass, + properties.joinToString(", ") { "${it.simpleName.asString()} = this.${it.simpleName.asString()}" } + ) + .build() + + val toPatchedFunction = FunSpec.builder("toPatched") + .addModifiers(visibilityModifier) + .receiver(declaration.toClassName()) + .addParameter("patch", dtoClass) + .returns(declaration.toClassName()) + .addStatement( + "return %T(%L)", + targetClass, + properties.joinToString(", ") { + val name = it.simpleName.asString() + "$name = patch.$name ?: this.$name" + } + ) + .build() + + return FileSpec.builder(packageName, className) + .addType(typeSpecBuilder.build()) + .addFunction(toDtoFunction) + .addFunction(toPatchedFunction) + .build() + } +} diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/codegen/DtoFileCodeGeneration.kt b/ksp/src/main/kotlin/com/y9vad9/implier/codegen/DtoFileCodeGeneration.kt deleted file mode 100755 index fadaafe..0000000 --- a/ksp/src/main/kotlin/com/y9vad9/implier/codegen/DtoFileCodeGeneration.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.y9vad9.implier.codegen - -import com.google.devtools.ksp.symbol.KSClassDeclaration -import com.squareup.kotlinpoet.* -import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy -import com.squareup.kotlinpoet.ksp.toClassName -import com.y9vad9.implier.Visibility -import kotlin.reflect.KClass - -object DtoFileCodeGeneration : - FileCodeGeneration { - override fun Data.generate(): FileSpec { - return FileSpec.builder(packageName, simpleName) - .addType( - TypeSpec.classBuilder(name) - .addModifiers(if (visibility == Visibility.PUBLIC) KModifier.PUBLIC else KModifier.INTERNAL) - .applyParent() - .applyConstructor() - .build() - ).addFunction( - FunSpec.builder("toDto") - .addModifiers(if (visibility == Visibility.PUBLIC) KModifier.PUBLIC else KModifier.INTERNAL) - .receiver(ClassName(declaration.packageName.asString(), declaration.simpleName.asString())) - .returns(ClassName(declaration.packageName.asString(), name)) - .addCode("return $name(${formatConstructorArguments()})") - .build() - ).addFunction( - FunSpec.builder("toPatched") - .addParameter("patch", className) - .addModifiers(if (visibility == Visibility.PUBLIC) KModifier.PUBLIC else KModifier.INTERNAL) - .receiver(ClassName(declaration.packageName.asString(), declaration.simpleName.asString())) - .returns(className) - .addCode(CodeBlock.builder() - .addStatement("val isMutable = this is Mutable${declaration.simpleName.asString()}") - .addStatement("val base = if(isMutable) this as Mutable${declaration.simpleName.asString()} else this.toMutable()") - .also { builder -> - for (property in declaration.getAllProperties()) { - builder.addStatement("patch.${property.simpleName.asString()}?.let{ ") - .indent().addStatement("base.${property.simpleName.asString()} = it").unindent() - .addStatement("}") - } - } - .addStatement("return if(isMutable) this else base.toImmutable()") - .build()) - .build() - ) - .build() - } - - class Data( - val marker: KClass<*>, - val visibility: Visibility, - declaration: KSClassDeclaration - ) : FileCodeGeneration.Data(declaration) { - val name: String get() = marker.simpleName + simpleName - - fun TypeSpec.Builder.applyParent(): TypeSpec.Builder = - addSuperinterface(marker.asClassName().parameterizedBy(declaration.toClassName())) - - fun TypeSpec.Builder.applyConstructor(): TypeSpec.Builder { - return primaryConstructor(FunSpec.constructorBuilder().apply { - for (property in declaration.getAllProperties()) { - val propertyName = property.simpleName.asString() - val propertyType = property.type.resolve().toClassName().copy(nullable = true) - addParameter( - ParameterSpec.builder(propertyName, propertyType).defaultValue("null").build() - ) - addProperty( - PropertySpec.builder(propertyName, propertyType) - .mutable(true) - .initializer(propertyName) - .build() - ) - } - }.build()) - } - - fun formatConstructorArguments(): String { - return declaration.getAllProperties().joinToString(", ") { it.simpleName.asString() } - } - } -} diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/codegen/FactoryFunctionCodeGenerator.kt b/ksp/src/main/kotlin/com/y9vad9/implier/codegen/FactoryFunctionCodeGenerator.kt new file mode 100644 index 0000000..5726975 --- /dev/null +++ b/ksp/src/main/kotlin/com/y9vad9/implier/codegen/FactoryFunctionCodeGenerator.kt @@ -0,0 +1,46 @@ +package com.y9vad9.implier.codegen + +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ksp.toClassName +import com.squareup.kotlinpoet.ksp.toTypeName +import com.y9vad9.implier.Visibility + +object FactoryFunctionCodeGenerator { + fun generate( + declaration: KSClassDeclaration, + properties: List, + visibility: Visibility, + preferImmutable: Boolean, + ): FileSpec { + val packageName = declaration.packageName.asString() + val simpleName = declaration.simpleName.asString() + val fileName = "${simpleName}Factory" + val visibilityModifier = if (visibility == Visibility.PUBLIC) KModifier.PUBLIC else KModifier.INTERNAL + + val targetRealizationName = if (preferImmutable) "Immutable$simpleName" else "Mutable$simpleName" + val targetClass = ClassName(packageName, targetRealizationName) + + val factoryFunctionBuilder = FunSpec.builder(simpleName) + .addModifiers(visibilityModifier) + .returns(declaration.toClassName()) + + for (property in properties) { + factoryFunctionBuilder.addParameter( + property.simpleName.asString(), + property.type.resolve().toTypeName() + ) + } + + factoryFunctionBuilder.addStatement( + "return %T(%L)", + targetClass, + properties.joinToString(", ") { it.simpleName.asString() } + ) + + return FileSpec.builder(packageName, fileName) + .addFunction(factoryFunctionBuilder.build()) + .build() + } +} diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/codegen/FileCodeGeneration.kt b/ksp/src/main/kotlin/com/y9vad9/implier/codegen/FileCodeGeneration.kt deleted file mode 100644 index e8c8b02..0000000 --- a/ksp/src/main/kotlin/com/y9vad9/implier/codegen/FileCodeGeneration.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.y9vad9.implier.codegen - -import com.google.devtools.ksp.symbol.ClassKind -import com.google.devtools.ksp.symbol.KSClassDeclaration -import com.squareup.kotlinpoet.ClassName -import com.squareup.kotlinpoet.FileSpec - -interface FileCodeGeneration { - fun TData.generate(): FileSpec - - abstract class Data( - val declaration: KSClassDeclaration - ) { - val simpleName: String get() = declaration.simpleName.asString() - val packageName: String get() = declaration.packageName.asString() - val classKind: ClassKind get() = declaration.classKind - - val className: ClassName = ClassName(packageName, simpleName) - } -} \ No newline at end of file diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/codegen/FunctionFactoryFileCodeGeneration.kt b/ksp/src/main/kotlin/com/y9vad9/implier/codegen/FunctionFactoryFileCodeGeneration.kt deleted file mode 100755 index 3070135..0000000 --- a/ksp/src/main/kotlin/com/y9vad9/implier/codegen/FunctionFactoryFileCodeGeneration.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.y9vad9.implier.codegen - -import com.google.devtools.ksp.symbol.KSClassDeclaration -import com.squareup.kotlinpoet.FileSpec -import com.squareup.kotlinpoet.FunSpec -import com.squareup.kotlinpoet.KModifier -import com.squareup.kotlinpoet.ParameterSpec -import com.squareup.kotlinpoet.ksp.toClassName -import com.y9vad9.implier.Visibility -import java.util.* - -object FunctionFactoryFileCodeGeneration : FileCodeGeneration { - class Data(val realizationName: String, val visibility: Visibility, declaration: KSClassDeclaration) : - FileCodeGeneration.Data(declaration) { - val name: String get() = simpleName + "Factory" - - fun formatConstructorArguments(): String { - return declaration.getAllProperties().joinToString(", ") { it.simpleName.asString() } - } - - fun generateParameters(): List = declaration.getAllProperties().map { - ParameterSpec.builder(it.simpleName.asString(), it.type.resolve().toClassName()).build() - }.toList() - } - - override fun Data.generate(): FileSpec { - return FileSpec.builder(packageName, name) - .addFunction( - FunSpec.builder(simpleName) - .addModifiers(if (visibility == Visibility.PUBLIC) KModifier.PUBLIC else KModifier.INTERNAL) - .returns(declaration.toClassName()) - .addParameters(generateParameters()) - .addCode( - "return $realizationName${ - simpleName.replaceFirstChar { - if (it.isLowerCase()) it.titlecase( - Locale.getDefault() - ) else it.toString() - } - }(${formatConstructorArguments()})" - ) - .build() - ) - .build() - } -} \ No newline at end of file diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/codegen/ImmutableCodeGenerator.kt b/ksp/src/main/kotlin/com/y9vad9/implier/codegen/ImmutableCodeGenerator.kt new file mode 100644 index 0000000..cd48975 --- /dev/null +++ b/ksp/src/main/kotlin/com/y9vad9/implier/codegen/ImmutableCodeGenerator.kt @@ -0,0 +1,69 @@ +package com.y9vad9.implier.codegen + +import com.google.devtools.ksp.symbol.ClassKind +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.ksp.toClassName +import com.squareup.kotlinpoet.ksp.toTypeName +import com.y9vad9.implier.Immutable +import com.y9vad9.implier.Visibility + +object ImmutableCodeGenerator { + fun generate( + declaration: KSClassDeclaration, + properties: List, + visibility: Visibility, + isDataClass: Boolean, + ): FileSpec { + val packageName = declaration.packageName.asString() + val simpleName = declaration.simpleName.asString() + val className = "Immutable$simpleName" + val visibilityModifier = if (visibility == Visibility.PUBLIC) KModifier.PUBLIC else KModifier.INTERNAL + + val typeSpecBuilder = TypeSpec.classBuilder(className) + .addModifiers(visibilityModifier) + + if (isDataClass) { + typeSpecBuilder.addModifiers(KModifier.DATA) + } + + when (declaration.classKind) { + ClassKind.INTERFACE -> typeSpecBuilder.addSuperinterface(declaration.toClassName()) + ClassKind.CLASS -> typeSpecBuilder.superclass(declaration.toClassName()) + else -> {} + } + typeSpecBuilder.addSuperinterface(Immutable::class.asClassName().parameterizedBy(declaration.toClassName())) + + val constructorBuilder = FunSpec.constructorBuilder() + for (property in properties) { + val propName = property.simpleName.asString() + val propType = property.type.resolve().toTypeName() + constructorBuilder.addParameter(propName, propType) + typeSpecBuilder.addProperty( + PropertySpec.builder(propName, propType) + .initializer(propName) + .addModifiers(KModifier.OVERRIDE) + .build() + ) + } + typeSpecBuilder.primaryConstructor(constructorBuilder.build()) + + val toImmutableFunction = FunSpec.builder("toImmutable") + .addModifiers(visibilityModifier) + .receiver(declaration.toClassName()) + .returns(ClassName(packageName, className)) + .addStatement( + "return %T(%L)", + ClassName(packageName, className), + properties.joinToString(", ") { it.simpleName.asString() } + ) + .build() + + return FileSpec.builder(packageName, className) + .addType(typeSpecBuilder.build()) + .addFunction(toImmutableFunction) + .build() + } +} diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/codegen/ImplementationFileCodeGeneration.kt b/ksp/src/main/kotlin/com/y9vad9/implier/codegen/ImplementationFileCodeGeneration.kt deleted file mode 100755 index 958fe7c..0000000 --- a/ksp/src/main/kotlin/com/y9vad9/implier/codegen/ImplementationFileCodeGeneration.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.y9vad9.implier.codegen - -import com.google.devtools.ksp.symbol.ClassKind -import com.google.devtools.ksp.symbol.KSClassDeclaration -import com.squareup.kotlinpoet.* -import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy -import com.squareup.kotlinpoet.ksp.toClassName -import com.y9vad9.implier.Dto -import com.y9vad9.implier.Mutable -import com.y9vad9.implier.Visibility -import kotlin.reflect.KClass - -object ImplementationFileCodeGeneration : - FileCodeGeneration { - override fun Data.generate(): FileSpec { - return FileSpec.builder(packageName, simpleName) - .addType( - TypeSpec.classBuilder(name) - .addModifiers(if (visibility == Visibility.PUBLIC) KModifier.PUBLIC else KModifier.INTERNAL) - .applyParent() - .applyConstructor() - .build() - ).addFunction( - FunSpec.builder("to${marker.simpleName}") - .addModifiers(if (visibility == Visibility.PUBLIC) KModifier.PUBLIC else KModifier.INTERNAL) - .receiver(ClassName(declaration.packageName.asString(), declaration.simpleName.asString())) - .returns(ClassName(declaration.packageName.asString(), name)) - .addCode("return $name(${formatConstructorArguments()})") - .build() - ) - .build() - } - - class Data( - val marker: KClass<*>, - val visibility: Visibility, - declaration: KSClassDeclaration - ) : FileCodeGeneration.Data(declaration) { - companion object { - private val mutableMarkers = listOf(Mutable::class, Dto::class).map { it.qualifiedName } - private val nullableMarkers = listOf(Dto::class).map { it.qualifiedName } - } - - val name: String get() = marker.simpleName + simpleName - - fun TypeSpec.Builder.applyParent(): TypeSpec.Builder = when (classKind) { - ClassKind.INTERFACE -> addSuperinterface(declaration.toClassName()) - ClassKind.CLASS -> superclass(declaration.toClassName()) - else -> error("Invalid type") - }.addSuperinterface(marker.asClassName().parameterizedBy(declaration.toClassName())) - - fun TypeSpec.Builder.applyConstructor(): TypeSpec.Builder { - return primaryConstructor(FunSpec.constructorBuilder().apply { - for (property in declaration.getAllProperties()) { - addParameter( - ParameterSpec.builder( - property.simpleName.asString(), property.type.resolve().toClassName() - ).build() - ) - addProperty( - PropertySpec.builder(property.simpleName.asString(), property.type.resolve().toClassName()) - .initializer(property.simpleName.asString()) - .mutable(mutableMarkers.contains(marker.qualifiedName)) - .addModifiers(KModifier.OVERRIDE) - .build() - ) - } - }.build()) - } - - fun formatConstructorArguments(): String { - return declaration.getAllProperties().joinToString(", ") { it.simpleName.asString() } - } - } -} \ No newline at end of file diff --git a/ksp/src/main/kotlin/com/y9vad9/implier/codegen/MutableCodeGenerator.kt b/ksp/src/main/kotlin/com/y9vad9/implier/codegen/MutableCodeGenerator.kt new file mode 100644 index 0000000..f8f41ed --- /dev/null +++ b/ksp/src/main/kotlin/com/y9vad9/implier/codegen/MutableCodeGenerator.kt @@ -0,0 +1,65 @@ +package com.y9vad9.implier.codegen + +import com.google.devtools.ksp.symbol.ClassKind +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.ksp.toClassName +import com.squareup.kotlinpoet.ksp.toTypeName +import com.y9vad9.implier.Mutable +import com.y9vad9.implier.Visibility + +object MutableCodeGenerator { + fun generate( + declaration: KSClassDeclaration, + properties: List, + visibility: Visibility, + ): FileSpec { + val packageName = declaration.packageName.asString() + val simpleName = declaration.simpleName.asString() + val className = "Mutable$simpleName" + val visibilityModifier = if (visibility == Visibility.PUBLIC) KModifier.PUBLIC else KModifier.INTERNAL + + val typeSpecBuilder = TypeSpec.classBuilder(className) + .addModifiers(visibilityModifier) + + when (declaration.classKind) { + ClassKind.INTERFACE -> typeSpecBuilder.addSuperinterface(declaration.toClassName()) + ClassKind.CLASS -> typeSpecBuilder.superclass(declaration.toClassName()) + else -> {} + } + typeSpecBuilder.addSuperinterface(Mutable::class.asClassName().parameterizedBy(declaration.toClassName())) + + val constructorBuilder = FunSpec.constructorBuilder() + for (property in properties) { + val propName = property.simpleName.asString() + val propType = property.type.resolve().toTypeName() + constructorBuilder.addParameter(propName, propType) + typeSpecBuilder.addProperty( + PropertySpec.builder(propName, propType) + .mutable(true) + .initializer(propName) + .addModifiers(KModifier.OVERRIDE) + .build() + ) + } + typeSpecBuilder.primaryConstructor(constructorBuilder.build()) + + val toMutableFunction = FunSpec.builder("toMutable") + .addModifiers(visibilityModifier) + .receiver(declaration.toClassName()) + .returns(ClassName(packageName, className)) + .addStatement( + "return %T(%L)", + ClassName(packageName, className), + properties.joinToString(", ") { it.simpleName.asString() } + ) + .build() + + return FileSpec.builder(packageName, className) + .addType(typeSpecBuilder.build()) + .addFunction(toMutableFunction) + .build() + } +} diff --git a/src/main/kotlin/com/y9vad9/implier/annotations.kt b/src/main/kotlin/com/y9vad9/implier/annotations.kt index 0e3c4d1..bc77717 100755 --- a/src/main/kotlin/com/y9vad9/implier/annotations.kt +++ b/src/main/kotlin/com/y9vad9/implier/annotations.kt @@ -1,53 +1,62 @@ package com.y9vad9.implier /** - * Marks that object should be able to mutate. - * Generates `toMutable()` function for creating mutable variant of object and - * mutable variant of annotated interface. - * @param visibility - Visibility of generated class & function. + * Marks that an object should have a mutable implementation generated. + * Generates `Mutable*` class and `toMutable()` extension function. + * Can be applied to interfaces or abstract classes. + * + * @param visibility Visibility of generated class & extension function. */ @Target(allowedTargets = [AnnotationTarget.CLASS]) annotation class MutableImpl(val visibility: Visibility = Visibility.PUBLIC) /** - * Marks that object should be able to immutate. - * Generates `toImmutable()` function for creating immutable variant of object and - * mutable variant of annotated interface. - * @param visibility - Visibility of generated class & function. + * Marks that an object should have an immutable implementation generated. + * Generates `Immutable*` class (data class for interfaces and no-arg abstract classes) + * and `toImmutable()` extension function. + * Can be applied to interfaces or abstract classes. + * + * @param visibility Visibility of generated class & extension function. */ @Target(allowedTargets = [AnnotationTarget.CLASS]) annotation class ImmutableImpl(val visibility: Visibility = Visibility.PUBLIC) /** - * Marks that object should have factory-function with hidden realization. - * To make it works, you should use at least one of mutable / immutable annotations. + * Marks that an object should have a factory function with hidden realization. + * Requires at least one of [ImmutableImpl] or [MutableImpl] on the same declaration + * (prefers [ImmutableImpl]). + * + * @param visibility Visibility of generated factory function. */ @Target(allowedTargets = [AnnotationTarget.CLASS]) annotation class FactoryFunctionImpl(val visibility: Visibility = Visibility.PUBLIC) /** - * Marks that object should have Builder implementation. - * To make it works, you should use at least one of mutable / immutable - * annotations (type will be hidden behind interface / abstract class). Immutable preferred. + * Marks that an object should have a Builder implementation generated. + * Requires at least one of [ImmutableImpl] or [MutableImpl] on the same declaration + * (prefers [ImmutableImpl]). * - * @param type - methods generation type (with accessors set, or just `property(value)`) - * @param visibility - Visibility of generated class. + * @param type Methods generation type (with accessors `set*`, or just `property(value)`). + * @param visibility Visibility of generated builder class and methods. */ @Target(allowedTargets = [AnnotationTarget.CLASS]) -annotation class BuilderImpl(val type: Type = Type.WITHOUT_ACCESSORS, val visibility: Visibility = Visibility.PUBLIC) { +annotation class BuilderImpl( + val type: Type = Type.WITHOUT_ACCESSORS, + val visibility: Visibility = Visibility.PUBLIC +) { enum class Type { WITH_ACCESSORS, WITHOUT_ACCESSORS } } /** - * Marks that object should have DSL Builder implementation. - * To make it works, you should use at least one of mutable / immutable - * annotations (type will be hidden behind `interface` / `abstract class`). Immutable preferred. + * Marks that an object should have a DSL Builder implementation generated. + * Requires at least one of [ImmutableImpl] or [MutableImpl] on the same declaration + * (prefers [ImmutableImpl]). * - * @param functionName - DSL function name (e.x: `myConfiguration {}`) - * @param type - methods generation type (with accessors set / `property(value) or just property-access`) - * @param visibility - Visibility of generated class & function. + * @param functionName DSL function name (e.g.: `myConfiguration {}`). + * @param type Methods generation type (property-access, accessors `set*`, or `property(value)`). + * @param visibility Visibility of generated builder class and DSL function. */ @Target(allowedTargets = [AnnotationTarget.CLASS]) annotation class DSLBuilderImpl( @@ -61,10 +70,13 @@ annotation class DSLBuilderImpl( } /** - * Marks that object should be able to (partially) mutate using DTOs as patch objects. - * Generates `toDto()` function for creating mutable variant of object - * with nullable members and DTO variant of annotated interface. - * @param visibility - Visibility of generated class & function. + * Marks that an object should be able to (partially) mutate using DTOs as patch objects. + * Generates `toDto()` function for creating DTO variant of object with nullable members, + * and `toPatched(patch: Dto*)` function for applying patches. + * Requires at least one of [ImmutableImpl] or [MutableImpl] on the same declaration + * (prefers [ImmutableImpl]). + * + * @param visibility Visibility of generated DTO class & functions. */ @Target(allowedTargets = [AnnotationTarget.CLASS]) annotation class DtoImpl(val visibility: Visibility = Visibility.PUBLIC) From 0b95c11b1177fba568a2137561464228797da07a Mon Sep 17 00:00:00 2001 From: y9vad9 Date: Sat, 3 Oct 2026 15:25:14 +0200 Subject: [PATCH 3/7] test: add comprehensive test suite for codegen features and edge cases --- test/src/test/kotlin/TestSample.kt | 43 --- test/src/test/kotlin/sample/AbstractSample.kt | 18 + test/src/test/kotlin/sample/ImplierTest.kt | 330 ++++++++++++++++++ test/src/test/kotlin/sample/NestedSample.kt | 13 + test/src/test/kotlin/{ => sample}/Sample.kt | 6 +- test/src/test/kotlin/sample/SubsetSample.kt | 26 ++ 6 files changed, 391 insertions(+), 45 deletions(-) delete mode 100755 test/src/test/kotlin/TestSample.kt create mode 100644 test/src/test/kotlin/sample/AbstractSample.kt create mode 100644 test/src/test/kotlin/sample/ImplierTest.kt create mode 100644 test/src/test/kotlin/sample/NestedSample.kt rename test/src/test/kotlin/{ => sample}/Sample.kt (70%) mode change 100755 => 100644 create mode 100644 test/src/test/kotlin/sample/SubsetSample.kt diff --git a/test/src/test/kotlin/TestSample.kt b/test/src/test/kotlin/TestSample.kt deleted file mode 100755 index 465fc03..0000000 --- a/test/src/test/kotlin/TestSample.kt +++ /dev/null @@ -1,43 +0,0 @@ -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertDoesNotThrow -import org.junit.platform.commons.annotation.Testable -import sample.* - -@Testable -object TestSample { - @Test - fun codegenTest() { - assertDoesNotThrow { - MutableSample("value", 1).toImmutable().toMutable() - } - } - - @Test - fun testFactory() { - assertDoesNotThrow { - Sample("string", 1).toMutable().toImmutable() - } - } - - @Test - fun testBuilder() { - assertDoesNotThrow { - SampleBuilder() - .number(0) - .sample("something") - .build() - .toMutable() - .toImmutable() - } - } - - @Test - fun dslTest() { - assertDoesNotThrow { - sampleDSL { - sample = "Something" - number = Int.MAX_VALUE - }.toImmutable().toMutable() - } - } -} \ No newline at end of file diff --git a/test/src/test/kotlin/sample/AbstractSample.kt b/test/src/test/kotlin/sample/AbstractSample.kt new file mode 100644 index 0000000..7c68114 --- /dev/null +++ b/test/src/test/kotlin/sample/AbstractSample.kt @@ -0,0 +1,18 @@ +package sample + +import com.y9vad9.implier.* + +@ImmutableImpl(Visibility.INTERNAL) +@MutableImpl(Visibility.INTERNAL) +@FactoryFunctionImpl(Visibility.INTERNAL) +@BuilderImpl(type = BuilderImpl.Type.WITH_ACCESSORS, visibility = Visibility.INTERNAL) +@DSLBuilderImpl("abstractSampleDSL", type = DSLBuilderImpl.Type.PROPERTY_ACCESS, visibility = Visibility.INTERNAL) +@DtoImpl(Visibility.INTERNAL) +abstract class AbstractSample { + abstract val title: String + abstract val count: Int + + val display: String get() = "$title: $count" + + fun doubleCount(): Int = count * 2 +} diff --git a/test/src/test/kotlin/sample/ImplierTest.kt b/test/src/test/kotlin/sample/ImplierTest.kt new file mode 100644 index 0000000..225827f --- /dev/null +++ b/test/src/test/kotlin/sample/ImplierTest.kt @@ -0,0 +1,330 @@ +package sample + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class ImplierTest { + + @Test + fun testImmutableRealizationAndDataClassBehavior() { + val sample1 = ImmutableSample( + sample = "hello", + number = 42, + nullableField = null, + list = listOf("a", "b") + ) + val sample2 = ImmutableSample( + sample = "hello", + number = 42, + nullableField = null, + list = listOf("a", "b") + ) + + // Real property assertions + assertEquals("hello", sample1.sample) + assertEquals(42, sample1.number) + assertNull(sample1.nullableField) + assertEquals(listOf("a", "b"), sample1.list) + + // data class equals and hashCode + assertEquals(sample1, sample2) + assertEquals(sample1.hashCode(), sample2.hashCode()) + + // data class toString + assertTrue(sample1.toString().startsWith("ImmutableSample(")) + assertTrue(sample1.toString().contains("sample=hello")) + + // data class copy + val modified = sample1.copy(number = 99, nullableField = "not-null") + assertEquals(99, modified.number) + assertEquals("not-null", modified.nullableField) + assertEquals("hello", modified.sample) + assertNotEquals(sample1, modified) + + // toMutable conversion + val mutable = sample1.toMutable() + assertEquals(sample1.sample, mutable.sample) + assertEquals(sample1.number, mutable.number) + assertEquals(sample1.nullableField, mutable.nullableField) + assertEquals(sample1.list, mutable.list) + } + + @Test + fun testMutableRealization() { + val mutable = MutableSample( + sample = "initial", + number = 10, + nullableField = "value", + list = listOf("item") + ) + + assertEquals("initial", mutable.sample) + assertEquals(10, mutable.number) + assertEquals("value", mutable.nullableField) + assertEquals(listOf("item"), mutable.list) + + // Mutate fields + mutable.sample = "mutated" + mutable.number = 20 + mutable.nullableField = null + mutable.list = listOf("item1", "item2") + + assertEquals("mutated", mutable.sample) + assertEquals(20, mutable.number) + assertNull(mutable.nullableField) + assertEquals(listOf("item1", "item2"), mutable.list) + + // toImmutable conversion + val immutable = mutable.toImmutable() + assertEquals("mutated", immutable.sample) + assertEquals(20, immutable.number) + assertNull(immutable.nullableField) + assertEquals(listOf("item1", "item2"), immutable.list) + } + + @Test + fun testFactoryFunction() { + // Sample() factory prefers Immutable when both @ImmutableImpl and @MutableImpl are present + val created = Sample( + sample = "from-factory", + number = 7, + nullableField = "test", + list = listOf("one") + ) + + assertEquals("from-factory", created.sample) + assertEquals(7, created.number) + assertEquals("test", created.nullableField) + assertEquals(listOf("one"), created.list) + assertTrue(created is ImmutableSample, "Factory should instantiate Immutable realization by default") + } + + @Test + fun testBuilderWithNonNullAndNullableProperties() { + // Build with all properties set + val built = SampleBuilder() + .sample("builder-sample") + .number(123) + .nullableField("custom") + .list(listOf("x", "y")) + .build() + + assertEquals("builder-sample", built.sample) + assertEquals(123, built.number) + assertEquals("custom", built.nullableField) + assertEquals(listOf("x", "y"), built.list) + assertTrue(built is ImmutableSample, "Builder should instantiate ImmutableSample") + + // Build with nullableField omitted (should default to null without throwing) + val withNullOmitted = SampleBuilder() + .sample("omitted-nullable") + .number(456) + .list(emptyList()) + .build() + + assertEquals("omitted-nullable", withNullOmitted.sample) + assertEquals(456, withNullOmitted.number) + assertNull(withNullOmitted.nullableField, "Nullable property should default to null") + + // Non-null property omitted must throw IllegalStateException via Delegates.notNull() + assertThrows { + SampleBuilder() + .number(456) + .list(emptyList()) + .build() + } + } + + @Test + fun testDSLBuilderWithNonNullAndNullableProperties() { + // DSL with WITH_ACCESSORS: supports property access and set* + val result = sampleDSL { + sample = "dsl-sample" + number = 999 + nullableField = "set-in-dsl" + list = listOf("dsl1") + } + + assertEquals("dsl-sample", result.sample) + assertEquals(999, result.number) + assertEquals("set-in-dsl", result.nullableField) + assertEquals(listOf("dsl1"), result.list) + assertTrue(result is ImmutableSample, "DSL should instantiate ImmutableSample") + + // Using set* accessor functions + val resultWithAccessors = sampleDSL { + setSample("accessor-sample") + setNumber(888) + setList(listOf("dsl2")) + } + + assertEquals("accessor-sample", resultWithAccessors.sample) + assertEquals(888, resultWithAccessors.number) + assertNull(resultWithAccessors.nullableField, "Nullable property should default to null in DSL") + + // Omitting non-null property throws IllegalStateException + assertThrows { + sampleDSL { + sample = "only-sample" + } + } + } + + @Test + fun testDtoAndPatching() { + val original = Sample( + sample = "orig-title", + number = 1, + nullableField = "orig-note", + list = listOf("initial") + ) + + // toDto + val dto = original.toDto() + assertEquals("orig-title", dto.sample) + assertEquals(1, dto.number) + assertEquals("orig-note", dto.nullableField) + assertEquals(listOf("initial"), dto.list) + + // Patch only single property + val patch1 = DtoSample(sample = "updated-title") + val patched1 = original.toPatched(patch1) + assertEquals("updated-title", patched1.sample) + assertEquals(1, patched1.number) + assertEquals("orig-note", patched1.nullableField) + assertEquals(listOf("initial"), patched1.list) + + // Patch multiple properties + val patch2 = DtoSample(number = 100, nullableField = "new-note") + val patched2 = patched1.toPatched(patch2) + assertEquals("updated-title", patched2.sample) + assertEquals(100, patched2.number) + assertEquals("new-note", patched2.nullableField) + assertEquals(listOf("initial"), patched2.list) + } + + @Test + fun testAbstractClassSupport() { + val mutable = MutableAbstractSample(title = "AbstractTitle", count = 5) + assertEquals("AbstractTitle", mutable.title) + assertEquals(5, mutable.count) + assertEquals("AbstractTitle: 5", mutable.display) + assertEquals(10, mutable.doubleCount()) + + // Abstract class converts to Immutable + val immutable = mutable.toImmutable() + assertEquals("AbstractTitle", immutable.title) + assertEquals(5, immutable.count) + assertEquals("AbstractTitle: 5", immutable.display) + assertEquals(10, immutable.doubleCount()) + + // Abstract class realization is a data class (has no-arg constructor) + val copy = immutable.copy(count = 10) + assertEquals("AbstractTitle", copy.title) + assertEquals(10, copy.count) + assertEquals(immutable, ImmutableAbstractSample(title = "AbstractTitle", count = 5)) + + // Factory function for abstract class + val fromFactory = AbstractSample(title = "FactoryTitle", count = 42) + assertEquals("FactoryTitle", fromFactory.title) + assertEquals(42, fromFactory.count) + assertTrue(fromFactory is ImmutableAbstractSample) + + // Builder with WITH_ACCESSORS on abstract class + val built = AbstractSampleBuilder() + .setTitle("BuilderAbstract") + .setCount(15) + .build() + assertEquals("BuilderAbstract", built.title) + assertEquals(15, built.count) + + // DSL with PROPERTY_ACCESS on abstract class + val fromDsl = abstractSampleDSL { + title = "DSLAbstract" + count = 25 + } + assertEquals("DSLAbstract", fromDsl.title) + assertEquals(25, fromDsl.count) + + // DTO and patching on abstract class + val dto = fromDsl.toDto() + assertEquals("DSLAbstract", dto.title) + assertEquals(25, dto.count) + + val patched = fromDsl.toPatched(DtoAbstractSample(title = "PatchedAbstract")) + assertEquals("PatchedAbstract", patched.title) + assertEquals(25, patched.count) + } + + @Test + fun testImmutableOnlySubset() { + val built = ImmutableOnlySampleBuilder() + .id(1001L) + .name("Subset") + .build() + + assertEquals(1001L, built.id) + assertEquals("Subset", built.name) + assertTrue(built is ImmutableImmutableOnlySample) + + // ImmutableOnlySample is a data class + val copy = built.copy(name = "Updated") + assertEquals(1001L, copy.id) + assertEquals("Updated", copy.name) + } + + @Test + fun testMutableOnlySubset() { + // Factory with only MutableImpl + val created = MutableOnlySample(score = 98.5, flag = true) + assertEquals(98.5, created.score) + assertEquals(true, created.flag) + assertTrue(created is MutableMutableOnlySample) + + // Builder with only MutableImpl + val built = MutableOnlySampleBuilder() + .score(75.0) + .flag(false) + .build() + assertEquals(75.0, built.score) + assertEquals(false, built.flag) + assertTrue(built is MutableMutableOnlySample) + + // DSL with only MutableImpl + val fromDsl = mutableOnlyDSL { + score = 88.0 + flag = true + } + assertEquals(88.0, fromDsl.score) + assertEquals(true, fromDsl.flag) + assertTrue(fromDsl is MutableMutableOnlySample) + + // DTO and patching with only MutableImpl + val dto = fromDsl.toDto() + assertEquals(88.0, dto.score) + assertEquals(true, dto.flag) + + val patched = fromDsl.toPatched(DtoMutableOnlySample(score = 92.0)) + assertEquals(92.0, patched.score) + assertEquals(true, patched.flag) + assertTrue(patched is MutableMutableOnlySample) + } + + @Test + fun testNestedDeclaration() { + val created = NestedSample(key = "k1", value = "v1") + assertEquals("k1", created.key) + assertEquals("v1", created.value) + assertTrue(created is ImmutableNestedSample) + + val mutable = created.toMutable() + mutable.value = "v2" + assertEquals("v2", mutable.value) + val backToImmutable = mutable.toImmutable() + assertEquals("v2", backToImmutable.value) + } +} diff --git a/test/src/test/kotlin/sample/NestedSample.kt b/test/src/test/kotlin/sample/NestedSample.kt new file mode 100644 index 0000000..e8aa471 --- /dev/null +++ b/test/src/test/kotlin/sample/NestedSample.kt @@ -0,0 +1,13 @@ +package sample + +import com.y9vad9.implier.* + +interface Container { + @ImmutableImpl(Visibility.INTERNAL) + @MutableImpl(Visibility.INTERNAL) + @FactoryFunctionImpl(Visibility.INTERNAL) + interface NestedSample { + val key: String + val value: String + } +} diff --git a/test/src/test/kotlin/Sample.kt b/test/src/test/kotlin/sample/Sample.kt old mode 100755 new mode 100644 similarity index 70% rename from test/src/test/kotlin/Sample.kt rename to test/src/test/kotlin/sample/Sample.kt index 72846df..e0b30b0 --- a/test/src/test/kotlin/Sample.kt +++ b/test/src/test/kotlin/sample/Sample.kt @@ -6,9 +6,11 @@ import com.y9vad9.implier.* @ImmutableImpl(Visibility.INTERNAL) @DtoImpl(Visibility.INTERNAL) @MutableImpl(Visibility.INTERNAL) -@BuilderImpl(visibility = Visibility.INTERNAL) +@BuilderImpl(type = BuilderImpl.Type.WITHOUT_ACCESSORS, visibility = Visibility.INTERNAL) @DSLBuilderImpl("sampleDSL", type = DSLBuilderImpl.Type.WITH_ACCESSORS, visibility = Visibility.INTERNAL) interface Sample { val sample: String val number: Int -} \ No newline at end of file + val nullableField: String? + val list: List +} diff --git a/test/src/test/kotlin/sample/SubsetSample.kt b/test/src/test/kotlin/sample/SubsetSample.kt new file mode 100644 index 0000000..f8c695b --- /dev/null +++ b/test/src/test/kotlin/sample/SubsetSample.kt @@ -0,0 +1,26 @@ +package sample + +import com.y9vad9.implier.* + +/** + * Interface with only @ImmutableImpl and @BuilderImpl. + */ +@ImmutableImpl(Visibility.INTERNAL) +@BuilderImpl(visibility = Visibility.INTERNAL) +interface ImmutableOnlySample { + val id: Long + val name: String +} + +/** + * Interface with only @MutableImpl, @BuilderImpl, @DSLBuilderImpl, @FactoryFunctionImpl, and @DtoImpl. + */ +@MutableImpl(Visibility.INTERNAL) +@BuilderImpl(visibility = Visibility.INTERNAL) +@DSLBuilderImpl("mutableOnlyDSL", visibility = Visibility.INTERNAL) +@FactoryFunctionImpl(Visibility.INTERNAL) +@DtoImpl(Visibility.INTERNAL) +interface MutableOnlySample { + val score: Double + val flag: Boolean +} From d3bbe35a0bae6a2a790767671cebd67617324d85 Mon Sep 17 00:00:00 2001 From: y9vad9 Date: Sat, 3 Oct 2026 15:57:57 +0200 Subject: [PATCH 4/7] test: add compiler diagnostic tests for invalid processor targets and failure modes --- gradle/libs.versions.toml | 6 + test/build.gradle.kts | 3 + .../kotlin/sample/CompilerDiagnosticsTest.kt | 112 ++++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 test/src/test/kotlin/sample/CompilerDiagnosticsTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b0c2121..78d699b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,9 @@ ksp = "2.3.12" kotlinpoet = "2.4.0" junit = "5.14.4" +vanniktech-maven-publish = "0.37.0" +kct = "0.14.0" + [libraries] ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" } @@ -11,8 +14,11 @@ kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlin kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlin" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } +kct-core = { module = "dev.zacsweers.kctfork:core", version.ref = "kct" } +kct-ksp = { module = "dev.zacsweers.kctfork:ksp", version.ref = "kct" } [plugins] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } google-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktech-maven-publish" } diff --git a/test/build.gradle.kts b/test/build.gradle.kts index 59a0c90..edc0216 100644 --- a/test/build.gradle.kts +++ b/test/build.gradle.kts @@ -9,9 +9,12 @@ kotlin { dependencies { implementation(project(":")) + testImplementation(projects.ksp) testImplementation(libs.kotlin.test) testImplementation(libs.kotlin.test.junit5) testImplementation(libs.junit.jupiter) + testImplementation(libs.kct.core) + testImplementation(libs.kct.ksp) kspTest(projects.ksp) } diff --git a/test/src/test/kotlin/sample/CompilerDiagnosticsTest.kt b/test/src/test/kotlin/sample/CompilerDiagnosticsTest.kt new file mode 100644 index 0000000..7a7640a --- /dev/null +++ b/test/src/test/kotlin/sample/CompilerDiagnosticsTest.kt @@ -0,0 +1,112 @@ +@file:OptIn(org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi::class) + +package sample + +import com.tschuchort.compiletesting.JvmCompilationResult +import com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.SourceFile +import com.tschuchort.compiletesting.configureKsp +import com.tschuchort.compiletesting.symbolProcessorProviders +import com.y9vad9.implier.ImplierProcessorProvider +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class CompilerDiagnosticsTest { + + @Test + fun failsWhenAnnotationAppliedToNonAbstractClass() { + val source = SourceFile.kotlin( + "InvalidTarget.kt", + """ + package test + import com.y9vad9.implier.ImmutableImpl + + @ImmutableImpl + class RegularClass(val name: String) + """.trimIndent() + ) + + val result = compile(source) + + assertEquals(KotlinCompilation.ExitCode.COMPILATION_ERROR, result.exitCode) + assertTrue(result.messages.contains("Unable to create realization from non-abstract class: RegularClass")) + } + + @Test + fun failsWhenMissingRequiredRealizationAnnotation() { + val source = SourceFile.kotlin( + "MissingRealization.kt", + """ + package test + import com.y9vad9.implier.BuilderImpl + + @BuilderImpl + interface LonelyInterface { + val value: String + } + """.trimIndent() + ) + + val result = compile(source) + + assertEquals(KotlinCompilation.ExitCode.COMPILATION_ERROR, result.exitCode) + assertTrue(result.messages.contains("requires at least one of @ImmutableImpl or @MutableImpl")) + } + + @Test + fun failsWhenDslFunctionNameIsBlank() { + val source = SourceFile.kotlin( + "BlankDslName.kt", + """ + package test + import com.y9vad9.implier.ImmutableImpl + import com.y9vad9.implier.DSLBuilderImpl + + @ImmutableImpl + @DSLBuilderImpl("") + interface BlankDsl { + val value: String + } + """.trimIndent() + ) + + val result = compile(source) + + assertEquals(KotlinCompilation.ExitCode.COMPILATION_ERROR, result.exitCode) + assertTrue(result.messages.contains("must specify a non-empty functionName")) + } + + @Test + fun succeedsAndGeneratesCodeForValidInterface() { + val source = SourceFile.kotlin( + "ValidSample.kt", + """ + package test + import com.y9vad9.implier.ImmutableImpl + import com.y9vad9.implier.MutableImpl + + @ImmutableImpl + @MutableImpl + interface ValidSample { + val title: String + val count: Int + } + """.trimIndent() + ) + + val result = compile(source) + + assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode) + } + + private fun compile(vararg sources: SourceFile): JvmCompilationResult { + return KotlinCompilation().apply { + this.sources = sources.toList() + configureKsp { + symbolProcessorProviders += ImplierProcessorProvider() + } + inheritClassPath = true + }.compile() + } +} From dbbcd5d89952ca23865b72017fd1f0ccb1bbddca Mon Sep 17 00:00:00 2001 From: y9vad9 Date: Sat, 3 Oct 2026 15:58:07 +0200 Subject: [PATCH 5/7] publish: configure vanniktech maven central publishing and release workflow --- .github/workflows/publish.yml | 33 +++++++++++++++++++++++++ README.md | 8 +++--- build.gradle.kts | 46 +++++++++++++++++++++++++++++------ ksp/build.gradle.kts | 46 +++++++++++++++++++++++++++++------ 4 files changed, 113 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..c99b889 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,33 @@ +name: Publish Release to Maven Central + +on: + release: + types: + - created + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: 'gradle' + + - name: Set Gradle executable permissions + run: chmod +x ./gradlew + + - name: Build and Publish + env: + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }} + LIB_VERSION: ${{ github.ref_name }} + run: ./gradlew publishAllPublicationsToMavenCentralRepository -Pversion=${{ env.LIB_VERSION }} --no-daemon diff --git a/README.md b/README.md index b23c81b..e503445 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![Maven metadata URL](https://img.shields.io/maven-metadata/v?label=%24version&metadataUrl=https%3A%2F%2Fmaven.y9vad9.com%2Fcom%2Fy9vad9%2Fimplier%2Fimplier%2Fmaven-metadata.xml) +[![Maven Central](https://img.shields.io/maven-central/v/com.y9vad9.implier/implier)](https://central.sonatype.com/artifact/com.y9vad9.implier/implier) # implier @@ -72,13 +72,13 @@ class FooDSLBuilderScope { fun foo(builder: FooDSLBuilderScope.() -> Unit): Foo { /* code */ } ``` -## Implementation +## Setup -For first, we need to add repository: +Ensure `mavenCentral()` is present in your repositories: ```kotlin repositories { - maven("https://maven.y9vad9.com") + mavenCentral() } ``` diff --git a/build.gradle.kts b/build.gradle.kts index 213dfc8..9b7a489 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,7 +2,7 @@ plugins { alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.google.ksp) apply false - `maven-publish` + alias(libs.plugins.vanniktech.maven.publish) } group = "com.y9vad9.implier" @@ -20,13 +20,43 @@ kotlin { } } -publishing { - publications { - create("maven") { - from(components["kotlin"]) - groupId = "com.y9vad9.implier" - artifactId = "implier" - version = "1.0.5" +mavenPublishing { + publishToMavenCentral() + signAllPublications() + + coordinates("com.y9vad9.implier", "implier", project.version.toString()) + + pom { + name.set("implier") + description.set("Kotlin codegeneration library for Mutable & Immutable objects from interfaces.") + url.set("https://github.com/y9vad9/implier") + inceptionYear.set("2022") + + licenses { + license { + name.set("The MIT License") + url.set("https://opensource.org/licenses/MIT") + distribution.set("https://opensource.org/licenses/MIT") + } + } + + developers { + developer { + id.set("y9vad9") + name.set("Vadym Yaroshchuk") + url.set("https://github.com/y9vad9/") + } + } + + scm { + url.set("https://github.com/y9vad9/implier") + connection.set("scm:git:git://github.com/y9vad9/implier.git") + developerConnection.set("scm:git:ssh://git@github.com/y9vad9/implier.git") + } + + issueManagement { + system.set("GitHub Issues") + url.set("https://github.com/y9vad9/implier/issues") } } } \ No newline at end of file diff --git a/ksp/build.gradle.kts b/ksp/build.gradle.kts index 59a8d6c..707f3d6 100644 --- a/ksp/build.gradle.kts +++ b/ksp/build.gradle.kts @@ -1,6 +1,6 @@ plugins { alias(libs.plugins.kotlin.jvm) - `maven-publish` + alias(libs.plugins.vanniktech.maven.publish) } group = "com.y9vad9.implier" @@ -17,13 +17,43 @@ dependencies { implementation(project(":")) } -publishing { - publications { - create("maven") { - from(components["java"]) - groupId = "com.y9vad9.implier" - artifactId = "ksp" - version = "1.0.5" +mavenPublishing { + publishToMavenCentral() + signAllPublications() + + coordinates("com.y9vad9.implier", "ksp", project.version.toString()) + + pom { + name.set("implier ksp-implementation") + description.set("Kotlin codegeneration library for Mutable & Immutable objects from interfaces.") + url.set("https://github.com/y9vad9/implier") + inceptionYear.set("2022") + + licenses { + license { + name.set("The MIT License") + url.set("https://opensource.org/licenses/MIT") + distribution.set("https://opensource.org/licenses/MIT") + } + } + + developers { + developer { + id.set("y9vad9") + name.set("Vadym Yaroshchuk") + url.set("https://github.com/y9vad9/") + } + } + + scm { + url.set("https://github.com/y9vad9/implier") + connection.set("scm:git:git://github.com/y9vad9/implier.git") + developerConnection.set("scm:git:ssh://git@github.com/y9vad9/implier.git") + } + + issueManagement { + system.set("GitHub Issues") + url.set("https://github.com/y9vad9/implier/issues") } } } \ No newline at end of file From 9b8aba71dcb2fcdb8da265678797aff3caa642a6 Mon Sep 17 00:00:00 2001 From: y9vad9 Date: Sat, 3 Oct 2026 15:58:14 +0200 Subject: [PATCH 6/7] ci: add github actions workflow for checks and test execution --- .github/workflows/ci.yml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..25a91bb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: + push: + branches: + - main + - latest + - 'refactor/**' + - 'feature/**' + - 'bugfix/**' + pull_request: + branches: + - main + - latest + +permissions: + contents: read + +jobs: + build-and-test: + name: Build and Test + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: 'gradle' + + - name: Validate Gradle Wrapper + uses: gradle/actions/wrapper-validation@v4 + + - name: Run Checks and Tests + run: ./gradlew check --no-daemon From 2a0beeb85a4f194b24b2e4b0f11d7851f15a4ee6 Mon Sep 17 00:00:00 2001 From: y9vad9 Date: Sat, 3 Oct 2026 16:52:17 +0200 Subject: [PATCH 7/7] test: organize ksp unit tests and integration-tests with given-when-then pattern --- {test => integration-tests}/build.gradle.kts | 3 - .../src/test/kotlin/sample/AbstractSample.kt | 0 .../src/test/kotlin/sample/ImplierTest.kt | 204 ++++++++++-------- .../src/test/kotlin/sample/NestedSample.kt | 0 .../src/test/kotlin/sample/Sample.kt | 0 .../src/test/kotlin/sample/SubsetSample.kt | 0 ksp/build.gradle.kts | 10 + .../implier}/CompilerDiagnosticsTest.kt | 15 +- settings.gradle.kts | 2 +- 9 files changed, 139 insertions(+), 95 deletions(-) rename {test => integration-tests}/build.gradle.kts (76%) rename {test => integration-tests}/src/test/kotlin/sample/AbstractSample.kt (100%) rename {test => integration-tests}/src/test/kotlin/sample/ImplierTest.kt (75%) rename {test => integration-tests}/src/test/kotlin/sample/NestedSample.kt (100%) rename {test => integration-tests}/src/test/kotlin/sample/Sample.kt (100%) rename {test => integration-tests}/src/test/kotlin/sample/SubsetSample.kt (100%) rename {test/src/test/kotlin/sample => ksp/src/test/kotlin/com/y9vad9/implier}/CompilerDiagnosticsTest.kt (93%) diff --git a/test/build.gradle.kts b/integration-tests/build.gradle.kts similarity index 76% rename from test/build.gradle.kts rename to integration-tests/build.gradle.kts index edc0216..59a0c90 100644 --- a/test/build.gradle.kts +++ b/integration-tests/build.gradle.kts @@ -9,12 +9,9 @@ kotlin { dependencies { implementation(project(":")) - testImplementation(projects.ksp) testImplementation(libs.kotlin.test) testImplementation(libs.kotlin.test.junit5) testImplementation(libs.junit.jupiter) - testImplementation(libs.kct.core) - testImplementation(libs.kct.ksp) kspTest(projects.ksp) } diff --git a/test/src/test/kotlin/sample/AbstractSample.kt b/integration-tests/src/test/kotlin/sample/AbstractSample.kt similarity index 100% rename from test/src/test/kotlin/sample/AbstractSample.kt rename to integration-tests/src/test/kotlin/sample/AbstractSample.kt diff --git a/test/src/test/kotlin/sample/ImplierTest.kt b/integration-tests/src/test/kotlin/sample/ImplierTest.kt similarity index 75% rename from test/src/test/kotlin/sample/ImplierTest.kt rename to integration-tests/src/test/kotlin/sample/ImplierTest.kt index 225827f..a988470 100644 --- a/test/src/test/kotlin/sample/ImplierTest.kt +++ b/integration-tests/src/test/kotlin/sample/ImplierTest.kt @@ -11,6 +11,7 @@ class ImplierTest { @Test fun testImmutableRealizationAndDataClassBehavior() { + // GIVEN val sample1 = ImmutableSample( sample = "hello", number = 42, @@ -24,29 +25,27 @@ class ImplierTest { list = listOf("a", "b") ) - // Real property assertions + // WHEN + val modified = sample1.copy(number = 99, nullableField = "not-null") + val mutable = sample1.toMutable() + + // THEN assertEquals("hello", sample1.sample) assertEquals(42, sample1.number) assertNull(sample1.nullableField) assertEquals(listOf("a", "b"), sample1.list) - // data class equals and hashCode assertEquals(sample1, sample2) assertEquals(sample1.hashCode(), sample2.hashCode()) - // data class toString assertTrue(sample1.toString().startsWith("ImmutableSample(")) assertTrue(sample1.toString().contains("sample=hello")) - // data class copy - val modified = sample1.copy(number = 99, nullableField = "not-null") assertEquals(99, modified.number) assertEquals("not-null", modified.nullableField) assertEquals("hello", modified.sample) assertNotEquals(sample1, modified) - // toMutable conversion - val mutable = sample1.toMutable() assertEquals(sample1.sample, mutable.sample) assertEquals(sample1.number, mutable.number) assertEquals(sample1.nullableField, mutable.nullableField) @@ -55,6 +54,7 @@ class ImplierTest { @Test fun testMutableRealization() { + // GIVEN val mutable = MutableSample( sample = "initial", number = 10, @@ -62,24 +62,19 @@ class ImplierTest { list = listOf("item") ) - assertEquals("initial", mutable.sample) - assertEquals(10, mutable.number) - assertEquals("value", mutable.nullableField) - assertEquals(listOf("item"), mutable.list) - - // Mutate fields + // WHEN mutable.sample = "mutated" mutable.number = 20 mutable.nullableField = null mutable.list = listOf("item1", "item2") + val immutable = mutable.toImmutable() + // THEN assertEquals("mutated", mutable.sample) assertEquals(20, mutable.number) assertNull(mutable.nullableField) assertEquals(listOf("item1", "item2"), mutable.list) - // toImmutable conversion - val immutable = mutable.toImmutable() assertEquals("mutated", immutable.sample) assertEquals(20, immutable.number) assertNull(immutable.nullableField) @@ -88,7 +83,7 @@ class ImplierTest { @Test fun testFactoryFunction() { - // Sample() factory prefers Immutable when both @ImmutableImpl and @MutableImpl are present + // GIVEN / WHEN val created = Sample( sample = "from-factory", number = 7, @@ -96,52 +91,67 @@ class ImplierTest { list = listOf("one") ) + // THEN assertEquals("from-factory", created.sample) assertEquals(7, created.number) assertEquals("test", created.nullableField) assertEquals(listOf("one"), created.list) - assertTrue(created is ImmutableSample, "Factory should instantiate Immutable realization by default") + assertTrue(created is ImmutableSample) } @Test - fun testBuilderWithNonNullAndNullableProperties() { - // Build with all properties set - val built = SampleBuilder() + fun testBuilderWithAllPropertiesSet() { + // GIVEN + val builder = SampleBuilder() .sample("builder-sample") .number(123) .nullableField("custom") .list(listOf("x", "y")) - .build() + // WHEN + val built = builder.build() + + // THEN assertEquals("builder-sample", built.sample) assertEquals(123, built.number) assertEquals("custom", built.nullableField) assertEquals(listOf("x", "y"), built.list) - assertTrue(built is ImmutableSample, "Builder should instantiate ImmutableSample") + assertTrue(built is ImmutableSample) + } - // Build with nullableField omitted (should default to null without throwing) - val withNullOmitted = SampleBuilder() + @Test + fun testBuilderWithNullablePropertyOmitted() { + // GIVEN + val builder = SampleBuilder() .sample("omitted-nullable") .number(456) .list(emptyList()) - .build() - assertEquals("omitted-nullable", withNullOmitted.sample) - assertEquals(456, withNullOmitted.number) - assertNull(withNullOmitted.nullableField, "Nullable property should default to null") + // WHEN + val built = builder.build() + + // THEN + assertEquals("omitted-nullable", built.sample) + assertEquals(456, built.number) + assertNull(built.nullableField) + } - // Non-null property omitted must throw IllegalStateException via Delegates.notNull() + @Test + fun testBuilderThrowsWhenRequiredPropertyOmitted() { + // GIVEN + val builder = SampleBuilder() + .number(456) + .list(emptyList()) + + // WHEN / THEN assertThrows { - SampleBuilder() - .number(456) - .list(emptyList()) - .build() + builder.build() } } @Test - fun testDSLBuilderWithNonNullAndNullableProperties() { - // DSL with WITH_ACCESSORS: supports property access and set* + fun testDSLBuilderWithPropertyAccess() { + // GIVEN / WHEN val result = sampleDSL { sample = "dsl-sample" number = 999 @@ -149,24 +159,32 @@ class ImplierTest { list = listOf("dsl1") } + // THEN assertEquals("dsl-sample", result.sample) assertEquals(999, result.number) assertEquals("set-in-dsl", result.nullableField) assertEquals(listOf("dsl1"), result.list) - assertTrue(result is ImmutableSample, "DSL should instantiate ImmutableSample") + assertTrue(result is ImmutableSample) + } - // Using set* accessor functions - val resultWithAccessors = sampleDSL { + @Test + fun testDSLBuilderWithAccessorsAndNullableOmitted() { + // GIVEN / WHEN + val result = sampleDSL { setSample("accessor-sample") setNumber(888) setList(listOf("dsl2")) } - assertEquals("accessor-sample", resultWithAccessors.sample) - assertEquals(888, resultWithAccessors.number) - assertNull(resultWithAccessors.nullableField, "Nullable property should default to null in DSL") + // THEN + assertEquals("accessor-sample", result.sample) + assertEquals(888, result.number) + assertNull(result.nullableField) + } - // Omitting non-null property throws IllegalStateException + @Test + fun testDSLBuilderThrowsWhenRequiredPropertyOmitted() { + // GIVEN / WHEN / THEN assertThrows { sampleDSL { sample = "only-sample" @@ -176,31 +194,32 @@ class ImplierTest { @Test fun testDtoAndPatching() { + // GIVEN val original = Sample( sample = "orig-title", number = 1, nullableField = "orig-note", list = listOf("initial") ) + val patch1 = DtoSample(sample = "updated-title") + val patch2 = DtoSample(number = 100, nullableField = "new-note") - // toDto + // WHEN val dto = original.toDto() + val patched1 = original.toPatched(patch1) + val patched2 = patched1.toPatched(patch2) + + // THEN assertEquals("orig-title", dto.sample) assertEquals(1, dto.number) assertEquals("orig-note", dto.nullableField) assertEquals(listOf("initial"), dto.list) - // Patch only single property - val patch1 = DtoSample(sample = "updated-title") - val patched1 = original.toPatched(patch1) assertEquals("updated-title", patched1.sample) assertEquals(1, patched1.number) assertEquals("orig-note", patched1.nullableField) assertEquals(listOf("initial"), patched1.list) - // Patch multiple properties - val patch2 = DtoSample(number = 100, nullableField = "new-note") - val patched2 = patched1.toPatched(patch2) assertEquals("updated-title", patched2.sample) assertEquals(100, patched2.number) assertEquals("new-note", patched2.nullableField) @@ -209,69 +228,71 @@ class ImplierTest { @Test fun testAbstractClassSupport() { + // GIVEN val mutable = MutableAbstractSample(title = "AbstractTitle", count = 5) + + // WHEN + val immutable = mutable.toImmutable() + val copy = immutable.copy(count = 10) + val fromFactory = AbstractSample(title = "FactoryTitle", count = 42) + val built = AbstractSampleBuilder() + .setTitle("BuilderAbstract") + .setCount(15) + .build() + val fromDsl = abstractSampleDSL { + title = "DSLAbstract" + count = 25 + } + val dto = fromDsl.toDto() + val patched = fromDsl.toPatched(DtoAbstractSample(title = "PatchedAbstract")) + + // THEN assertEquals("AbstractTitle", mutable.title) assertEquals(5, mutable.count) assertEquals("AbstractTitle: 5", mutable.display) assertEquals(10, mutable.doubleCount()) - // Abstract class converts to Immutable - val immutable = mutable.toImmutable() assertEquals("AbstractTitle", immutable.title) assertEquals(5, immutable.count) assertEquals("AbstractTitle: 5", immutable.display) assertEquals(10, immutable.doubleCount()) - // Abstract class realization is a data class (has no-arg constructor) - val copy = immutable.copy(count = 10) assertEquals("AbstractTitle", copy.title) assertEquals(10, copy.count) assertEquals(immutable, ImmutableAbstractSample(title = "AbstractTitle", count = 5)) - // Factory function for abstract class - val fromFactory = AbstractSample(title = "FactoryTitle", count = 42) assertEquals("FactoryTitle", fromFactory.title) assertEquals(42, fromFactory.count) assertTrue(fromFactory is ImmutableAbstractSample) - // Builder with WITH_ACCESSORS on abstract class - val built = AbstractSampleBuilder() - .setTitle("BuilderAbstract") - .setCount(15) - .build() assertEquals("BuilderAbstract", built.title) assertEquals(15, built.count) - // DSL with PROPERTY_ACCESS on abstract class - val fromDsl = abstractSampleDSL { - title = "DSLAbstract" - count = 25 - } assertEquals("DSLAbstract", fromDsl.title) assertEquals(25, fromDsl.count) - // DTO and patching on abstract class - val dto = fromDsl.toDto() assertEquals("DSLAbstract", dto.title) assertEquals(25, dto.count) - val patched = fromDsl.toPatched(DtoAbstractSample(title = "PatchedAbstract")) assertEquals("PatchedAbstract", patched.title) assertEquals(25, patched.count) } @Test fun testImmutableOnlySubset() { - val built = ImmutableOnlySampleBuilder() + // GIVEN + val builder = ImmutableOnlySampleBuilder() .id(1001L) .name("Subset") - .build() + // WHEN + val built: ImmutableOnlySample = builder.build() + + // THEN assertEquals(1001L, built.id) assertEquals("Subset", built.name) assertTrue(built is ImmutableImmutableOnlySample) - // ImmutableOnlySample is a data class val copy = built.copy(name = "Updated") assertEquals(1001L, copy.id) assertEquals("Updated", copy.name) @@ -279,36 +300,37 @@ class ImplierTest { @Test fun testMutableOnlySubset() { - // Factory with only MutableImpl + // GIVEN + val builder = MutableOnlySampleBuilder() + .score(75.0) + .flag(false) + + // WHEN val created = MutableOnlySample(score = 98.5, flag = true) + val built = builder.build() + val fromDsl = mutableOnlyDSL { + score = 88.0 + flag = true + } + val dto = fromDsl.toDto() + val patched = fromDsl.toPatched(DtoMutableOnlySample(score = 92.0)) + + // THEN assertEquals(98.5, created.score) assertEquals(true, created.flag) assertTrue(created is MutableMutableOnlySample) - // Builder with only MutableImpl - val built = MutableOnlySampleBuilder() - .score(75.0) - .flag(false) - .build() assertEquals(75.0, built.score) assertEquals(false, built.flag) assertTrue(built is MutableMutableOnlySample) - // DSL with only MutableImpl - val fromDsl = mutableOnlyDSL { - score = 88.0 - flag = true - } assertEquals(88.0, fromDsl.score) assertEquals(true, fromDsl.flag) assertTrue(fromDsl is MutableMutableOnlySample) - // DTO and patching with only MutableImpl - val dto = fromDsl.toDto() assertEquals(88.0, dto.score) assertEquals(true, dto.flag) - val patched = fromDsl.toPatched(DtoMutableOnlySample(score = 92.0)) assertEquals(92.0, patched.score) assertEquals(true, patched.flag) assertTrue(patched is MutableMutableOnlySample) @@ -316,15 +338,19 @@ class ImplierTest { @Test fun testNestedDeclaration() { + // GIVEN val created = NestedSample(key = "k1", value = "v1") - assertEquals("k1", created.key) - assertEquals("v1", created.value) - assertTrue(created is ImmutableNestedSample) + // WHEN val mutable = created.toMutable() mutable.value = "v2" - assertEquals("v2", mutable.value) val backToImmutable = mutable.toImmutable() + + // THEN + assertEquals("k1", created.key) + assertEquals("v1", created.value) + assertTrue(created is ImmutableNestedSample) + assertEquals("v2", mutable.value) assertEquals("v2", backToImmutable.value) } } diff --git a/test/src/test/kotlin/sample/NestedSample.kt b/integration-tests/src/test/kotlin/sample/NestedSample.kt similarity index 100% rename from test/src/test/kotlin/sample/NestedSample.kt rename to integration-tests/src/test/kotlin/sample/NestedSample.kt diff --git a/test/src/test/kotlin/sample/Sample.kt b/integration-tests/src/test/kotlin/sample/Sample.kt similarity index 100% rename from test/src/test/kotlin/sample/Sample.kt rename to integration-tests/src/test/kotlin/sample/Sample.kt diff --git a/test/src/test/kotlin/sample/SubsetSample.kt b/integration-tests/src/test/kotlin/sample/SubsetSample.kt similarity index 100% rename from test/src/test/kotlin/sample/SubsetSample.kt rename to integration-tests/src/test/kotlin/sample/SubsetSample.kt diff --git a/ksp/build.gradle.kts b/ksp/build.gradle.kts index 707f3d6..64de95c 100644 --- a/ksp/build.gradle.kts +++ b/ksp/build.gradle.kts @@ -10,11 +10,21 @@ kotlin { jvmToolchain(21) } +tasks.test { + useJUnitPlatform() +} + dependencies { compileOnly(libs.ksp.api) implementation(libs.kotlinpoet) implementation(libs.kotlinpoet.ksp) implementation(project(":")) + + testImplementation(libs.kotlin.test) + testImplementation(libs.kotlin.test.junit5) + testImplementation(libs.junit.jupiter) + testImplementation(libs.kct.core) + testImplementation(libs.kct.ksp) } mavenPublishing { diff --git a/test/src/test/kotlin/sample/CompilerDiagnosticsTest.kt b/ksp/src/test/kotlin/com/y9vad9/implier/CompilerDiagnosticsTest.kt similarity index 93% rename from test/src/test/kotlin/sample/CompilerDiagnosticsTest.kt rename to ksp/src/test/kotlin/com/y9vad9/implier/CompilerDiagnosticsTest.kt index 7a7640a..35e8b44 100644 --- a/test/src/test/kotlin/sample/CompilerDiagnosticsTest.kt +++ b/ksp/src/test/kotlin/com/y9vad9/implier/CompilerDiagnosticsTest.kt @@ -1,13 +1,12 @@ @file:OptIn(org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi::class) -package sample +package com.y9vad9.implier import com.tschuchort.compiletesting.JvmCompilationResult import com.tschuchort.compiletesting.KotlinCompilation import com.tschuchort.compiletesting.SourceFile import com.tschuchort.compiletesting.configureKsp import com.tschuchort.compiletesting.symbolProcessorProviders -import com.y9vad9.implier.ImplierProcessorProvider import org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -16,6 +15,7 @@ class CompilerDiagnosticsTest { @Test fun failsWhenAnnotationAppliedToNonAbstractClass() { + // GIVEN val source = SourceFile.kotlin( "InvalidTarget.kt", """ @@ -27,14 +27,17 @@ class CompilerDiagnosticsTest { """.trimIndent() ) + // WHEN val result = compile(source) + // THEN assertEquals(KotlinCompilation.ExitCode.COMPILATION_ERROR, result.exitCode) assertTrue(result.messages.contains("Unable to create realization from non-abstract class: RegularClass")) } @Test fun failsWhenMissingRequiredRealizationAnnotation() { + // GIVEN val source = SourceFile.kotlin( "MissingRealization.kt", """ @@ -48,14 +51,17 @@ class CompilerDiagnosticsTest { """.trimIndent() ) + // WHEN val result = compile(source) + // THEN assertEquals(KotlinCompilation.ExitCode.COMPILATION_ERROR, result.exitCode) assertTrue(result.messages.contains("requires at least one of @ImmutableImpl or @MutableImpl")) } @Test fun failsWhenDslFunctionNameIsBlank() { + // GIVEN val source = SourceFile.kotlin( "BlankDslName.kt", """ @@ -71,14 +77,17 @@ class CompilerDiagnosticsTest { """.trimIndent() ) + // WHEN val result = compile(source) + // THEN assertEquals(KotlinCompilation.ExitCode.COMPILATION_ERROR, result.exitCode) assertTrue(result.messages.contains("must specify a non-empty functionName")) } @Test fun succeedsAndGeneratesCodeForValidInterface() { + // GIVEN val source = SourceFile.kotlin( "ValidSample.kt", """ @@ -95,8 +104,10 @@ class CompilerDiagnosticsTest { """.trimIndent() ) + // WHEN val result = compile(source) + // THEN assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 68dd582..f2e5f58 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,4 +25,4 @@ dependencyResolutionManagement { rootProject.name = "implier" include(":ksp") -include(":test") +include(":integration-tests")