Hello there. I have just published two plugins to ...
# community-support
t
Hello there. I have just published two plugins to the Gradle Plugin Portal, but I'm unable to apply them in project, throws the following errors:
Copy code
A problem occurred configuring root project 'Test'.
> Could not resolve all files for configuration ':classpath'.
   > Could not find clean-wizard-root.gradle-plugins:api:unspecified.
     Searched in the following locations:
       - <https://plugins.gradle.org/m2/clean-wizard-root/gradle-plugins/api/unspecified/api-unspecified.pom>
     Required by:
         project : > io.github.timbermir.cleanwizard.multimodule:io.github.timbermir.cleanwizard.multimodule.gradle.plugin:2.0.0-RC1 > io.github.timbermir:implementation:2.0.0-RC1
Those are my plugins: 1. https://plugins.gradle.org/plugin/io.github.timbermir.cleanwizard 2. https://plugins.gradle.org/plugin/io.github.timbermir.cleanwizard.multimodule You can see the Gradle plugin implementation class here
t
Your plugin project (build-logic/convention) has a dependency on another project, and Gradle will then look at that project's group, name and version to generate the dependency in the POM. It will specifically not look at Maven publications of that project, which is where you configure the Maven coordinates, so the generated POM does not link to the appropriate artifact. …and that other artifact hasn't apparently been published to Maven Central anyway, so even with the appropriate coordinates in the POM it wouldn't work either.
Fwiw, from that declaration:
Copy code
plugins {
  id("io.github.timbermir.cleanwizard.multimodule") version "2.0.0-RC1"
}
Gradle will look for https://plugins.gradle.org/m2/io/github/timbermir/cleanwizard/multimodule/io.github.ti[…]mbermir.cleanwizard.multimodule.gradle.plugin-2.0.0-RC1.pom (known as a "plugin marker") which has a dependency on the actual project artifact, whose POM is at https://plugins.gradle.org/m2/io.github.timbermir/implementation/2.0.0-RC1/implementation-2.0.0-RC1.pom (Gradle will actually load the module metadata at https://plugins.gradle.org/m2/io.github.timbermir/implementation/2.0.0-RC1/implementation-2.0.0-RC1.module)
t
I have already resolved it. now it is this problem here: Unable to load class 'corp.tbm.cleanwizard.gradle.api.extensions.CleanWizardExtension'.
plugin-api.build.gradle.kts
Copy code
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar

plugins {
    alias(libs.plugins.kotlin.dsl)
    alias(libs.plugins.google.devtools.ksp)
    alias(libs.plugins.kotlin.serialization)
    alias(libs.plugins.gradle.publish)
    alias(libs.plugins.gradle.signing)
    alias(libs.plugins.shadow)
}

tasks.withType<ShadowJar> {
    archiveClassifier.set("")
}

dependencies {
    compileOnly(project(":gradle-plugins:api"))
    implementation(libs.kotlinx.serialization.json)
    implementation(libs.google.gson)
    implementation(libs.vanniktech.maven.publish)
    compileOnly(libs.kotlin.gradle.plugin)
    compileOnly(libs.google.devtools.ksp)
    compileOnly(files(ksp.javaClass.superclass.protectionDomain.codeSource.location))
    compileOnly(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
    compileOnly(files(projectConfig.javaClass.superclass.protectionDomain.codeSource.location))
}

group = projectConfig.versions.group.get()
version = projectConfig.versions.version.get()

repositories {
    mavenCentral()
}

gradlePlugin {

    website.set("<https://github.com/Timbermir/clean-wizard>")
    vcsUrl.set("<https://github.com/Timbermir/clean-wizard.git>")

    plugins {
        libs.plugins.cleanwizard.apply {

            create("cleanwizard-gradle-plugin") {
                id = core.pluginId
                implementationClass = pluginConfig.versions.cleanwizard.root.second.implementation.get()
                displayName = "Clean Wizard Root Project Plugin"
                description =
                    "This plugin is required for Clean Wizard Kotlin Symbol Processor advanced configuration. " +
                            "Using the following plugin you are able to configure how classes are generated," +
                            " for instance, using this plugin, you are able to change the DTO classes prefix from DTO to Dto."
                tags.set(
                    listOf(
                        "kotlin",
                        "ksp",
                        "kotlin-symbol-processing",
                        "android",
                        "clean-architecture",
                        "domain-model",
                        "ui-model",
                        "dto-entity-mapper",
                    )
                )
            }

            create("cleanwizard-multimodule-gradle-plugin") {
                id = multimodule.pluginId
                implementationClass = pluginConfig.versions.cleanwizard.multimodule.second.implementation.get()
                displayName = "Clean Wizard Multi Module Plugin"
                description =
                    "This plugin is required for Clean Wizard Kotlin Symbol Processor multi-module class generation. " +
                            "If your project is single-module, no need to apply it" +
                            ". It allows you" +
                            "to generate classes in various modules, so if you have data, domain and presentation modules and want to generate classes" +
                            " for each module, use this plugin. "
                tags.set(
                    listOf(
                        "kotlin",
                        "ksp",
                        "kotlin-symbol-processing",
                        "android",
                        "clean-architecture",
                        "domain-model",
                        "ui-model",
                        "dto-entity-mapper",
                    )
                )
            }
        }
    }
}

internal val Provider<PluginDependency>.pluginId
    get() = get().pluginId
api.build.gradle.kts
Copy code
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    alias(libs.plugins.kotlin.dsl)
    alias(libs.plugins.kotlin.serialization)
    alias(libs.plugins.shadow)
    alias(libs.plugins.maven.publish)
    alias(libs.plugins.maven.signing)
}

tasks.withType<ShadowJar> {
    archiveClassifier.set("")
}
dependencies {
    implementation(libs.kotlinx.serialization.json)
    implementation(libs.google.gson)
    implementation(libs.squareup.moshi)
}

tasks.withType<KotlinCompile>().configureEach {
    compilerOptions {
        languageVersion =
            org.jetbrains.kotlin.gradle.dsl.KotlinVersion.values()
                .first { it.version == projectConfig.versions.kotlin.get() }
    }
}
publishing {
    publications {
        create<MavenPublication>("shadow") {
            groupId = projectConfig.versions.group.get()
            artifactId = projectConfig.versions.artifact.get() + "-gradle-plugin-api"
            version = projectConfig.versions.version.get()

            artifact(tasks["shadowJar"])

            pom {
                inceptionYear.set("2024")
                name.set("io.github.timbermir.cleanwizard:gradle-plugin-api")
                url.set("<https://github.com/Timbermir/clean-wizard>")
                description.set("Clean Wizard Plugin API")

                licenses {
                    license {
                        name.set("The Apache Software License, Version 2.0")
                        url.set("<https://www.apache.org/licenses/LICENSE-2.0.txt>")
                    }
                }

                scm {
                    url.set("<https://github.com/Timbermir/clean-wizard>")
                    connection.set("scm:git:<git://github.com/Timbermir/clean-wizard.git>")
                    developerConnection.set("scm:git@github.com:Timbermir/clean-wizard.git")
                }

                issueManagement {
                    system.set("GitHub Issues")
                    url.set("<https://github.com/Timbermir/clean-wizard/issues/>")
                }

                pom.organization {
                    name.set("Timbermir")
                    url.set("<https://github.com/Timbermir>")
                }

                developers {
                    developer {
                        id.set(findProperty("developerId").toString())
                        name.set("timplifier")
                        email.set("<mailto:timplifier@gmail.com|timplifier@gmail.com>")
                        url.set("<https://github.com/timplifier>")
                    }
                }
            }
        }
    }
}
j
Unrelated to Gradle, but what is the point of duplicating the domain model two times (data and UI) instead of just using the domain one? Domain models should be your domain representation, not the backend one. And the UI one a way to map domain models to something based, overall, on strings or something resolvable to strings so you can draw it easily on a screen. All of the above cannot be autogenerated 🤔
1
t
@Javi This is just how Clean Architecture works, isn't it ? You have separate data classes for separate modules. Each module has his corresponding data classes to work with. I don't see how domain models represent backend model in any way. They are not saved in the database, they don't have any serialization annotations, this domain module doesn't even have any dependencies that data would require. If you have just two models instead of three, I don't think that it is according to Clean Architecture. Moreover, everyone has his own definition of Clean Architecture so I think there is no point in arguing here.
j
As you are generating automatically the models, your domain model properties will be the same than the backend and the UI ones, that breaks the utility of having those models. For example, imagine backend send you this model:
Copy code
{
    "amount": {
        "valueInCents": 230
        "currency": "EUR"
    }
}
Your backend model could be, simplifying units to Int and String:
Copy code
data class MoneyDTO(
    val valueInCents: Int, 
    val currency: String,
)
But I don't want to have that model in the domain, I would prefer to have something like
Copy code
value class Money(private val value: String) {
    val amount: Double = ...
    val valueInCents: Int = ...
    val currency: Currency = ...

    enum class Currency(val code: String) {
         EUR(code = "EUR"),
         ....
    }
}
It is not possible to generate it as it is not a 1:1 mapper. That is the same for the UI (I don't like UI models; they are overkill to me). UI model should expose everything (or almost everything) as
String
, as you are going to draw it, so your UI model would be
Copy code
// depending on you need to print both, amount and currency at the same time or in two different components
data class MoneyUI(val amount: String, val currency: String, val vamountAndCurrency: String)
So for this simple example, you can see I don't want a 1:1 map in the three models, indeed, if you are going to map 1:1, wouldn't it be simple to create just the domain model and in data you can autogenerate a serializer (supposing you are using Kotlin Serialization for example) so you don't break clean arch as you are not annotating/extending your domain model. UI model can just be deleted, it does not provide any value and you can use the domain directly in the UI.
The reason I don't like UI models and I think they are an overkill is because domain model + format extension function should be enough to print anything on the screen:
Copy code
@Composable
fun SomeScreen(money: Money) {
    Text(text = money.format())
}
Creating a copy of
Money
with the name
MoneyUI
, providing a
String
value, just to remove the
format
extension function is overkill IMO, as, indeed, you are just moving the
format
call to a
toMoneyUI
call elsewhere.
The most important thing is... it is being complex, it is not more testable (indeed it is less testable, as testing can became tedious because you need to instantiate the “same” model three times)
t
sorry for late answer. I guess you are right in all directions. I will add a feature to disable UI mapping, there is no problem here, but the thing is that should I just then create value class as you have provided one and then make all the values as string ? It is not hard to add and could be an option. Main problem of this library for now is the lack of external configuration. I completely rely on my internal implementation and I designed it in the way most of the people would use it, so in order to use this processor you just need to add dependencies, plugin, configure it slightly, like approximately 30 seconds and then use it. I tried to make it as less reliable on external configuration as possible. If you have free time, you can create issues on how to improve it and make it better so it is comfortable for you to use it. However, your
format
function requires external configuration because I don't know how to generate it due to lack of the context, though you can create an extension function for this class and then do whatever you want. Moreover, you can generate what you want and skip generation if it doesn't fit your requirements. I guess this library fits most of the cases, because it is 1:1 mapping and you can do whatever you want with generated classes, except, of course editing them. I hope you understand me that it is really hard to do those kind of things with generation where external configuration is a must. Hope you got me.
👍 1