dotanuki-labs/magic-modules

View on GitHub
magic-modules-plugin/src/main/kotlin/io/labs/dotanuki/magicmodules/internal/ModuleNamesWriter.kt

Summary

Maintainability
A
1 hr
Test Coverage
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()
    }
}