All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.avito.android.GenerateOwnersTask.kt Maven / Gradle / Ivy

package com.avito.android

import com.avito.android.model.AvitoCodeOwner
import com.avito.android.model.Owner
import com.avito.android.model.Team
import com.avito.android.model.Type
import com.avito.android.model.Unit
import com.avito.android.model.network.AvitoOwner
import com.avito.android.model.network.AvitoOwnersClient
import com.avito.android.model.network.OwnerType
import com.avito.android.utils.cyrillicToLatinAlphabet
import com.avito.utils.ProcessRunner
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.asClassName
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.time.Duration
import java.util.Locale

@CacheableTask
internal abstract class GenerateOwnersTask : DefaultTask() {

    @get:InputDirectory
    @get:PathSensitive(PathSensitivity.RELATIVE)
    internal abstract val moduleDir: DirectoryProperty

    @get:Internal
    internal abstract val avitoOwnersClient: Property

    @get:Input
    internal abstract val modulePathToOwners: MapProperty>

    @get:OutputFile
    internal abstract val bitbucketCodeOwnershipFile: RegularFileProperty

    @TaskAction
    fun generate() {
        val moduleDir = moduleDir.orNull?.asFile
        if (moduleDir == null || !moduleDir.exists()) {
            error(
                "The directory of the module for owners generation does not exist. Input: $moduleDir. " +
                    "Please, setup correct Directory object. " +
                    "For example \"project(\":common:code-owners\").layout.projectDirectory.\""
            )
        }

        val remoteOwners = getRemoteOwners()

        val ownersClassName = ClassName("com.avito.android.ownership", "Owners")

        val typeClassName = Type::class.asClassName()
        val unitClassName = Unit::class.asClassName()
        val teamClassName = Team::class.asClassName()

        val ownersEnum = createOwnersEnumBuilder(ownersClassName, typeClassName)
            .addRemoteOwnersToEnum(remoteOwners, unitClassName, teamClassName)
            .build()

        writeIntoFile(
            fileSpec = FileSpec.builder(ownersClassName.packageName, ownersClassName.simpleName)
                .addImport(unitClassName.packageName, unitClassName.simpleName)
                .addImport(teamClassName.packageName, teamClassName.simpleName)
                .indent("    ")
                .addType(ownersEnum)
                .build(),
            moduleDir = moduleDir
        )

        generateCodeOwnershipFile(remoteOwners)
    }

    private fun generateCodeOwnershipFile(remoteOwners: List) {
        val moduleToEmailPairs = modulePathToOwners.get().map { mapEntry ->
            val modulePath = mapEntry.key.replace(":", "/")
            val moduleOwners = mapEntry.value.filterIsInstance()
            val emails = moduleOwners.flatMap { localOwner ->
                findRemoteOwnerByIdRecursive(localOwner.type.id, remoteOwners)?.let { remoteOwner ->
                    (remoteOwner.children + remoteOwner).flatMap { it.people }.map { it.email }
                } ?: setOf()
            }
                .toSet()
                .sorted()
            modulePath to emails
        }
        writeIntoCodeOwnersFile(moduleToEmailPairs)
    }

    private fun findRemoteOwnerByIdRecursive(ownerId: String, owners: List): AvitoOwner? {
        owners.forEach { owner ->
            if (owner.id == ownerId) {
                return owner
            }
            findRemoteOwnerByIdRecursive(ownerId, owner.children)?.let { ownerFoundInChildren ->
                return ownerFoundInChildren
            }
        }
        return null
    }

    private fun writeIntoCodeOwnersFile(pairs: List>>) {
        val file = bitbucketCodeOwnershipFile.get().asFile

        file.writeText(
            pairs.joinToString("\n") { pair ->
                "${pair.first}/ ${pair.second.joinToString(" ")}"
            }
        )
    }

    private fun TypeSpec.Builder.addRemoteOwnersToEnum(
        remoteOwners: List,
        unitClassName: ClassName,
        teamClassName: ClassName
    ): TypeSpec.Builder {
        val allUnits = mutableListOf()
        val allTeams = mutableListOf()

        remoteOwners.forEachIndexed { _, owner ->
            if (owner.type == OwnerType.Unit) {
                val originalUnitName = owner.name.normalizeName()
                val finalUnitName = originalUnitName + "_Unit"

                allUnits.add(finalUnitName)

                val unitEnumParamsCode = CodeBlock.builder()
                    .unindent()
                    .addStatement(
                        """
                            
                            %1L(
                                name = %2S,
                                id = %3S
                            ),
                            chatChannels = setOf(%4L)
                            """.trimIndent(),
                        unitClassName.simpleName,
                        owner.name,
                        owner.id,
                        owner.channels.joinToString(separator = ", ", transform = { "\"$it\"" })
                    )
                    .indent()

                addEnumConstant(
                    finalUnitName,
                    TypeSpec.anonymousClassBuilder()
                        .addSuperclassConstructorParameter(unitEnumParamsCode.build())
                        .build()
                )

                owner.children.map { child ->
                    val originalTeamName = child.name.normalizeName()
                    val finalTeamName =
                        if (originalTeamName in allTeams) {
                            "$originalUnitName$originalTeamName"
                        } else {
                            originalTeamName
                        }
                    allTeams.add(finalTeamName)

                    val teamEnumParamsCode = CodeBlock.builder()
                        .unindent()
                        .addStatement(
                            """
                                
                                %1L(
                                    name = %2S,
                                    id = %3S,
                                    unit = %4L,
                                ),
                                chatChannels = setOf(%5L)
                            """.trimIndent(),
                            teamClassName.simpleName,
                            child.name,
                            child.id,
                            finalUnitName,
                            child.channels.joinToString(separator = ", ", transform = { "\"$it\"" })
                        )
                        .indent()

                    addEnumConstant(
                        finalTeamName + "_Team",
                        TypeSpec.anonymousClassBuilder()
                            .addSuperclassConstructorParameter(teamEnumParamsCode.build())
                            .build()
                    )
                }
            }
        }

        return this
    }

    private fun getRemoteOwners() = avitoOwnersClient.get().getAvitoOwners()

    private fun createOwnersEnumBuilder(ownersClassName: ClassName, typeClassName: ClassName): TypeSpec.Builder {
        val ownerTypeParam = ParameterSpec.builder(OWNER_TYPE, typeClassName)
        val chatChannelsType = Set::class.asClassName()
            .parameterizedBy(String::class.asClassName())

        val ownersEnum = TypeSpec.enumBuilder(ownersClassName)
            .addKdoc(COMMENT)
            .addSuperinterface(AvitoCodeOwner::class)
            .primaryConstructor(
                FunSpec.constructorBuilder()
                    .addParameter(ownerTypeParam.build())
                    .addParameter(
                        ParameterSpec.builder(
                            OWNER_CHAT_CHANNELS,
                            chatChannelsType
                        )
                            .defaultValue("setOf()")
                            .build()
                    )
                    .build()
            )
            .addProperty(
                PropertySpec.builder(OWNER_TYPE, typeClassName)
                    .initializer(OWNER_TYPE)
                    .addModifiers(KModifier.OVERRIDE)
                    .build()
            )
            .addProperty(
                PropertySpec.builder(OWNER_CHAT_CHANNELS, chatChannelsType)
                    .initializer(OWNER_CHAT_CHANNELS)
                    .build()
            )

        return ownersEnum
    }

    private fun writeIntoFile(fileSpec: FileSpec, moduleDir: File) {
        val file = File(moduleDir, "/src/main/kotlin")

        if (!file.exists()) {
            file.mkdirs()
        }

        val owners = File(file, fileSpec.toJavaFileObject().name)

        val doesFileExist = owners.exists()

        fileSpec.writeTo(file)

        if (!doesFileExist) {
            ProcessRunner.create(moduleDir).run(command = "git add ${owners.path}", Duration.ofSeconds(10))
        }
    }

    private fun String.normalizeName(): String {
        val words = replace("&", "_And_")
            .replace("Ƞ", "Eta")
            .replace("Ω", "Omega")
            .replace("\t", "")
            .replace(".", "")
            .replace("(", "")
            .replace(")", "")
            .split(" ", "/", "\\", "_", "-")
        return words.joinToString("_") {
            val sb = StringBuilder()
            it.forEachIndexed { index, c ->
                val currentChar = c.toString()
                val cyrillicChar: String? = cyrillicToLatinAlphabet[currentChar]
                val newChar = cyrillicChar ?: currentChar
                sb.append(if (index == 0) newChar.uppercase(Locale.ROOT) else newChar)
            }
            sb.toString()
        }
    }

    companion object {
        const val OWNER_CHAT_CHANNELS = "chatChannels"
        const val OWNER_TYPE = "type"

        val COMMENT = """
            !!! This file is autogenerated. Do not modify it by hands. !!!
            
            Use {@code ./gradlew generateCodeOwnersFile} command to update the owners.
    """.trimIndent()
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy