magic-modules-plugin/src/main/kotlin/io/labs/dotanuki/magicmodules/internal/ModuleNamesWriter.kt
package io.labs.dotanuki.magicmodules.internal
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
import io.labs.dotanuki.magicmodules.internal.model.CanonicalModuleName
import io.labs.dotanuki.magicmodules.internal.model.GradleModuleInclude
import io.labs.dotanuki.magicmodules.internal.model.GradleModuleType
import io.labs.dotanuki.magicmodules.internal.util.i
import io.labs.dotanuki.magicmodules.internal.util.logger
import java.io.File
internal object ModuleNamesWriter {
fun write(
folder: File,
moduleType: GradleModuleType,
coordinates: Map<GradleModuleInclude, List<CanonicalModuleName>>
) {
when {
folder.isFile -> throw MagicModulesError.CantWriteConstantsFile
coordinates.isEmpty() ->
if (moduleType != GradleModuleType.JAVA_LIBRARY) throw MagicModulesError.CantAcceptModulesNames
else -> generateAndWriteKotlinCode(moduleType.conventionFileName(), coordinates, folder)
}
}
private fun generateAndWriteKotlinCode(
filename: String,
coordinates: Map<GradleModuleInclude, List<CanonicalModuleName>>,
target: File
) {
val root = PathNode(
value = filename,
typeSpec = TypeSpec.objectBuilder(filename)
)
for ((modulePath, modules) in coordinates) {
var currentNode = root
for (moduleIndex in modules.indices) {
when (moduleIndex) {
modules.lastIndex -> {
val propertySpec = modules[moduleIndex].toConstantPropertySpec(modulePath)
currentNode.constants.add(propertySpec.name)
currentNode.typeSpec.addProperty(propertySpec)
}
else -> currentNode = currentNode.getCandidateToBeParent(
module = modules[moduleIndex]
)
}
}
}
val fileSpec = FileSpec.builder(ROOT_PACKAGE, filename)
.addComment(DO_NOT_EDIT)
.addType(root.buildAndConnectTypes())
.indent(FOUR_SPACES)
.build()
fileSpec.writeTo(target)
logger().i("Writter :: Wrote $filename.kt at $target")
}
private fun Set<String>.asListPropertySpec(): PropertySpec =
ClassName("kotlin.collections", "List")
.parameterizedBy(ClassName("kotlin", "String"))
.let { parametrizedStringList ->
val lineByLine = joinToString(
separator = LINE_BY_LINE_SEPARATOR,
prefix = LINE_BY_LINE_PREFIX,
postfix = LINE_BY_LINE_POSTFIX
)
val formatted = "\n${ALL_NAMES_TEMPLATE.replace("<items>", lineByLine).trimIndent()}"
PropertySpec.builder("allAvailable", parametrizedStringList)
.initializer(formatted)
.build()
}
private fun PathNode.buildAndConnectTypes(): TypeSpec {
children.forEach {
typeSpec.addType(it.buildAndConnectTypes())
}
if (constants.isNotEmpty()) {
typeSpec.addProperty(constants.asListPropertySpec())
}
return typeSpec.build()
}
private fun PathNode.getCandidateToBeParent(module: CanonicalModuleName): PathNode {
val pathNode = PathNode(
value = module.value,
typeSpec = module.toObjectTypeSpecWithoutBuild()
)
return when (val childIndex = children.indexOf(pathNode)) {
-1 -> {
children.add(pathNode)
pathNode
}
else -> children[childIndex]
}
}
private fun CanonicalModuleName.toConstantPropertySpec(module: GradleModuleInclude): PropertySpec =
PropertySpec.builder(value.replace("-", "_").toUpperCase(), String::class, KModifier.CONST)
.initializer("%S", module.value)
.build()
private fun CanonicalModuleName.toObjectTypeSpecWithoutBuild(): TypeSpec.Builder =
TypeSpec.objectBuilder(
value.split(NO_NUMBERS_AND_NO_LETTERS)
.reduce { accumulated, name -> accumulated + name.capitalize() }
.capitalize()
)
private const val FOUR_SPACES = " "
private const val LINE_BY_LINE_SEPARATOR = ",\n$FOUR_SPACES"
private const val LINE_BY_LINE_PREFIX = "\n$FOUR_SPACES"
private const val LINE_BY_LINE_POSTFIX = "\n"
private const val ROOT_PACKAGE = ""
private const val ALL_NAMES_TEMPLATE = "listOf(<items>)"
private const val DO_NOT_EDIT = "Generated by MagicModules plugin. Mind your Linters!"
private val NO_NUMBERS_AND_NO_LETTERS = """[^0-9A-Za-z]""".toRegex()
private data class PathNode(
val value: String,
val typeSpec: TypeSpec.Builder,
val children: MutableList<PathNode> = mutableListOf(),
val constants: MutableSet<String> = mutableSetOf()
) {
override fun equals(other: Any?): Boolean =
this === other || (other is PathNode && other.value == value)
override fun hashCode(): Int = value.hashCode()
}
}