Try it on Kotlin Playground https://pl.kotl.in/9oXsIoDNS

or edit the sample below

Build single module

Click "run" (on the right) to see the preview!

fun main() { val modules = config.modules.toProjectModuleList(config) println("This script will create the following ${modules.size} module${if (modules.size == 1) "" else "s"}:") modules.forEach { println( """| |Create module [${it.templateName}] ${it.moduleNameParts.joinToString(" ") { it.capitalize() }} | - destinationDir: ${it.destinationDirPart} | - packageName: ${it.packageName} | - gradlePath: ${it.gradlePath}""".trimMargin() ) } println("\nDo you want to continue?") } //sampleStart val config = ScriptGradleModuleConfig( gradleConfig = GradleConfig( packagePrefix = "com.github.owner.projectname", templatesDirPath = "templates", settingsGradleFilename = "settings.gradle.kts", dependenciesFilename = "buildSrc/src/main/kotlin/Dependencies.kt", dependenciesPrefix = "module", ), modules = listOf(LibAndroid("my-lib")), logLevel = 1, hasPrompt = true, writeOnDisk = true, forceNamingConvention = false, ) //sampleEnd // // ScriptGradleModuleConfig.kt // data class ScriptGradleModuleConfig( val gradleConfig: GradleConfig = GradleConfig("", "", "", "", ""), val modules: List<GradleComponent> = emptyList(), val logLevel: Int = -1, val hasPrompt: Boolean = false, val writeOnDisk: Boolean = false, val forceNamingConvention: Boolean = false, ) data class GradleConfig( val packagePrefix: String, val templatesDirPath: String, val settingsGradleFilename: String, val dependenciesFilename: String, val dependenciesPrefix: String, ) // // GradleComponent.kt // sealed class GradleComponent( open val gradleName: String, open val packageNamePart: String?, open val extra: Map<String, Any> = emptyMap(), ) // GradleComponents data class Group( override val gradleName: String, val modules: List<GradleComponent>, override val packageNamePart: String? = gradleName.toPackageName(), override val extra: Map<String, Any> = emptyMap(), ) : GradleComponent(gradleName, packageNamePart, extra) sealed class GradleModule( gradleName: String, packageNamePart: String?, extra: Map<String, Any>, open val templateName: String, ) : GradleComponent(gradleName, packageNamePart, extra) // GradleModules class App( override val gradleName: String, override val packageNamePart: String? = gradleName.toPackageName(), override val extra: Map<String, Any> = emptyMap(), override val templateName: String = "app", ) : GradleModule(gradleName, packageNamePart, extra, templateName) data class LibAndroid( override val gradleName: String, override val packageNamePart: String? = gradleName.toPackageName(), override val extra: Map<String, Any> = emptyMap(), override val templateName: String = "lib-android", ) : GradleModule(gradleName, packageNamePart, extra, templateName) data class LibKotlin( override val gradleName: String, override val packageNamePart: String? = gradleName.toPackageName(), override val extra: Map<String, Any> = emptyMap(), override val templateName: String = "lib-kotlin", ) : GradleModule(gradleName, packageNamePart, extra, templateName) // Utils fun String.toPackageName(): String = toLowerCase().replace("-", "") // // ProjectModule.kt // data class ProjectModule( val configModule: GradleModule, val configGroups: List<Group>, val config: ScriptGradleModuleConfig, ) { val templateName: String = configModule.templateName val moduleNameParts: List<String> = configGroups.flatMap { it.gradleName.toModuleNameParts() } + configModule.gradleName.toModuleNameParts() val gradlePathParts: List<String> = (configGroups.map { it.gradleName } + configModule.gradleName) .runningReduce { acc, s -> if (!config.forceNamingConvention || s.startsWith("$acc-")) s else "$acc-$s" } /** * Full gradle path, e.g., :feature:navigation:core */ val gradlePath: String = gradlePathParts.joinToString(":", prefix = ":") /** * Full package name, e.g., com.github.owner.projectname.myfeature */ val packageName: String = buildString { val prefix = listOf(config.gradleConfig.packagePrefix) val parts = (configGroups.map { it.packageNamePart } + configModule.packageNamePart).filterNotNull() .filter { it.isNotBlank() } append((prefix + parts).joinToString(".")) } val destinationDirPart: String = gradlePathParts.joinToString("/") private fun String.toModuleNameParts(): List<String> = toLowerCase().split("[^a-z0-9]".toRegex()) } fun List<GradleComponent>.toProjectModuleList(config: ScriptGradleModuleConfig): List<ProjectModule> = flatMap { it.toProjectModuleList(config) } fun GradleComponent.toProjectModuleList(config: ScriptGradleModuleConfig): List<ProjectModule> = when (this) { is Group -> modules.flatMap { it.toProjectModuleList(config) .map { (module, packages) -> ProjectModule(module, listOf(this) + packages, config) } } is GradleModule -> listOf(ProjectModule(this, emptyList(), config)) }

Build multiple modules

Click "run" (on the right) to see the preview!

fun main() { val modules = config.modules.toProjectModuleList(config) println("This script will create the following ${modules.size} module${if (modules.size == 1) "" else "s"}:") modules.forEach { println( """| |Create module [${it.templateName}] ${it.moduleNameParts.joinToString(" ") { it.capitalize() }} | - destinationDir: ${it.destinationDirPart} | - packageName: ${it.packageName} | - gradlePath: ${it.gradlePath}""".trimMargin() ) } println("\nDo you want to continue?") } //sampleStart val config = ScriptGradleModuleConfig( gradleConfig = GradleConfig( packagePrefix = "com.github.owner.projectname", templatesDirPath = "templates", settingsGradleFilename = "settings.gradle.kts", dependenciesFilename = "buildSrc/src/main/kotlin/Dependencies.kt", dependenciesPrefix = "module", ), modules = listOf( Group( gradleName = "feature-name", modules = listOf( App( gradleName = "app", ), LibAndroid( gradleName = "ui", ), LibKotlin( gradleName = "core", ) ) ), LibAndroid( gradleName = "my-lib", ) ), logLevel = 1, hasPrompt = true, writeOnDisk = true, forceNamingConvention = false, ) //sampleEnd // // ScriptGradleModuleConfig.kt // data class ScriptGradleModuleConfig( val gradleConfig: GradleConfig = GradleConfig("", "", "", "", ""), val modules: List<GradleComponent> = emptyList(), val logLevel: Int = -1, val hasPrompt: Boolean = false, val writeOnDisk: Boolean = false, val forceNamingConvention: Boolean = false, ) data class GradleConfig( val packagePrefix: String, val templatesDirPath: String, val settingsGradleFilename: String, val dependenciesFilename: String, val dependenciesPrefix: String, ) // // GradleComponent.kt // sealed class GradleComponent( open val gradleName: String, open val packageNamePart: String?, open val extra: Map<String, Any> = emptyMap(), ) // GradleComponents data class Group( override val gradleName: String, val modules: List<GradleComponent>, override val packageNamePart: String? = gradleName.toPackageName(), override val extra: Map<String, Any> = emptyMap(), ) : GradleComponent(gradleName, packageNamePart, extra) sealed class GradleModule( gradleName: String, packageNamePart: String?, extra: Map<String, Any>, open val templateName: String, ) : GradleComponent(gradleName, packageNamePart, extra) // GradleModules class App( override val gradleName: String, override val packageNamePart: String? = gradleName.toPackageName(), override val extra: Map<String, Any> = emptyMap(), override val templateName: String = "app", ) : GradleModule(gradleName, packageNamePart, extra, templateName) data class LibAndroid( override val gradleName: String, override val packageNamePart: String? = gradleName.toPackageName(), override val extra: Map<String, Any> = emptyMap(), override val templateName: String = "lib-android", ) : GradleModule(gradleName, packageNamePart, extra, templateName) data class LibKotlin( override val gradleName: String, override val packageNamePart: String? = gradleName.toPackageName(), override val extra: Map<String, Any> = emptyMap(), override val templateName: String = "lib-kotlin", ) : GradleModule(gradleName, packageNamePart, extra, templateName) // Utils fun String.toPackageName(): String = toLowerCase().replace("-", "") // // ProjectModule.kt // data class ProjectModule( val configModule: GradleModule, val configGroups: List<Group>, val config: ScriptGradleModuleConfig, ) { val templateName: String = configModule.templateName val moduleNameParts: List<String> = configGroups.flatMap { it.gradleName.toModuleNameParts() } + configModule.gradleName.toModuleNameParts() val gradlePathParts: List<String> = (configGroups.map { it.gradleName } + configModule.gradleName) .runningReduce { acc, s -> if (!config.forceNamingConvention || s.startsWith("$acc-")) s else "$acc-$s" } /** * Full gradle path, e.g., :feature:navigation:core */ val gradlePath: String = gradlePathParts.joinToString(":", prefix = ":") /** * Full package name, e.g., com.github.owner.projectname.myfeature */ val packageName: String = buildString { val prefix = listOf(config.gradleConfig.packagePrefix) val parts = (configGroups.map { it.packageNamePart } + configModule.packageNamePart).filterNotNull() .filter { it.isNotBlank() } append((prefix + parts).joinToString(".")) } val destinationDirPart: String = gradlePathParts.joinToString("/") private fun String.toModuleNameParts(): List<String> = toLowerCase().split("[^a-z0-9]".toRegex()) } fun List<GradleComponent>.toProjectModuleList(config: ScriptGradleModuleConfig): List<ProjectModule> = flatMap { it.toProjectModuleList(config) } fun GradleComponent.toProjectModuleList(config: ScriptGradleModuleConfig): List<ProjectModule> = when (this) { is Group -> modules.flatMap { it.toProjectModuleList(config) .map { (module, packages) -> ProjectModule(module, listOf(this) + packages, config) } } is GradleModule -> listOf(ProjectModule(this, emptyList(), config)) }