diff --git a/gradle/plugins/src/main/java/apigen/ApiGeneratorAndroidPlugin.kt b/gradle/plugins/src/main/java/apigen/ApiGeneratorAndroidPlugin.kt index ea06f40..9511c26 100644 --- a/gradle/plugins/src/main/java/apigen/ApiGeneratorAndroidPlugin.kt +++ b/gradle/plugins/src/main/java/apigen/ApiGeneratorAndroidPlugin.kt @@ -6,6 +6,7 @@ import org.gradle.api.Project import org.gradle.api.tasks.compile.JavaCompile import org.gradle.kotlin.dsl.findByType import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import static_analysis.utils.getApiGeneratorExtension class ApiGeneratorAndroidPlugin : ApiGeneratorPlugin() { @@ -13,11 +14,10 @@ class ApiGeneratorAndroidPlugin : ApiGeneratorPlugin() { super.apply(target) with(target) { - val extension = getExtension() + val extension = getApiGeneratorExtension() val outputDir = getDirectoryForGeneration() extension.outputDirPath = outputDir.path - extension.recreateOutputDir = true afterEvaluate { extensions.findByType()?.apply { diff --git a/gradle/plugins/src/main/java/apigen/ApiGeneratorBackendPlugin.kt b/gradle/plugins/src/main/java/apigen/ApiGeneratorBackendPlugin.kt index b4a8723..21e7077 100644 --- a/gradle/plugins/src/main/java/apigen/ApiGeneratorBackendPlugin.kt +++ b/gradle/plugins/src/main/java/apigen/ApiGeneratorBackendPlugin.kt @@ -1,13 +1,14 @@ package apigen import org.gradle.api.Project +import static_analysis.utils.getApiGeneratorExtension class ApiGeneratorBackendPlugin : ApiGeneratorPlugin() { override fun apply(target: Project) { super.apply(target) - val extension = target.getExtension() + val extension = target.getApiGeneratorExtension() extension.outputDirPath = target.file("src/main/kotlin").path extension.recreateOutputDir = false diff --git a/gradle/plugins/src/main/java/apigen/ApiGeneratorPlugin.kt b/gradle/plugins/src/main/java/apigen/ApiGeneratorPlugin.kt index fcaf17a..44faac4 100644 --- a/gradle/plugins/src/main/java/apigen/ApiGeneratorPlugin.kt +++ b/gradle/plugins/src/main/java/apigen/ApiGeneratorPlugin.kt @@ -5,6 +5,7 @@ import org.gradle.api.Project import org.gradle.api.Task import org.gradle.kotlin.dsl.create import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.register import org.gradle.kotlin.dsl.repositories abstract class ApiGeneratorPlugin : Plugin { @@ -13,6 +14,8 @@ abstract class ApiGeneratorPlugin : Plugin { const val API_GENERATOR_CONFIG = "apiGenerator" const val API_GENERATOR_EXT_NAME = "apiGenerator" const val API_GENERATOR_DEFAULT_VERSION = "1.4.0-beta10" + + const val API_GENERATOR_CLEAN_TEMP_DIR_TASK = "apiGeneratorCleanTempDir" } override fun apply(target: Project) { @@ -34,7 +37,8 @@ abstract class ApiGeneratorPlugin : Plugin { extensions.create(API_GENERATOR_EXT_NAME) - val apiGenTask = createApiGeneratorTask() + val cleanTask = tasks.create(API_GENERATOR_CLEAN_TEMP_DIR_TASK) + val apiGenTask = getApiGenTask(cleanTask) gradle.projectsEvaluated { tasks.getByName("preBuild").dependsOn(apiGenTask) @@ -42,35 +46,15 @@ abstract class ApiGeneratorPlugin : Plugin { } } - protected fun Project.getExtension(): ApiGeneratorExtension = extensions.getByName(API_GENERATOR_EXT_NAME) as ApiGeneratorExtension + private fun Project.getApiGenTask(cleanTask: Task) = tasks.register(API_GENERATOR_CONFIG, ApiGeneratorTask::class) { + val apiGeneratorTempDir = temporaryDir + tempApiDirDirectory.set(apiGeneratorTempDir.absolutePath) - private fun Project.createApiGeneratorTask(): Task = tasks.create(API_GENERATOR_CONFIG).doLast { - - val extension = getExtension() - - val pathToApiSchemes = extension.pathToApiSchemes ?: throw IllegalStateException("Configure path to api schemes for api generator plugin") - val outputLanguage = extension.outputLanguage ?: throw IllegalStateException("Configure output language code for api generator plugin") - - javaexec { - main = "-jar" - workingDir = rootDir - args = listOfNotNull( - configurations.getByName("apiGenerator").asPath, - "generate-client-code", - "--output-language", - outputLanguage.argName, - "--specification-path", - pathToApiSchemes, - "--kotlin-methods-generation-mode".takeIf { outputLanguage.methodOutputType != null }, - outputLanguage.methodOutputType?.argName, - "--output-path", - extension.outputDirPath, - "--package-name", - extension.outputPackageName, - "--recreate_output_dirs", - extension.recreateOutputDir.toString() - ) - } + // Нужно для удаления временной директории даже в случае краша во время выполнения таски. + finalizedBy( + cleanTask.doFirst { + apiGeneratorTempDir.deleteRecursively() + } + ) } - } diff --git a/gradle/plugins/src/main/java/apigen/ApiGeneratorTask.kt b/gradle/plugins/src/main/java/apigen/ApiGeneratorTask.kt new file mode 100644 index 0000000..01d671d --- /dev/null +++ b/gradle/plugins/src/main/java/apigen/ApiGeneratorTask.kt @@ -0,0 +1,156 @@ +package apigen + +import apigen.depencency_resolver.ApiModelsDependencyResolver +import apigen.depencency_resolver.CopyApiModelsToTempDirTask +import apigen.depencency_resolver.FileParser +import org.gradle.api.DefaultTask +import org.gradle.api.file.FileTree +import org.gradle.api.file.FileType +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFiles +import org.gradle.api.tasks.TaskAction +import org.gradle.work.ChangeType +import org.gradle.work.Incremental +import org.gradle.work.InputChanges +import org.gradle.workers.WorkerExecutor +import static_analysis.utils.getApiGeneratorExtension +import static_analysis.utils.toPath +import java.io.File +import java.nio.file.Path +import javax.inject.Inject + + +abstract class ApiGeneratorTask: DefaultTask() { + + @Internal + val extension: ApiGeneratorExtension = project.getApiGeneratorExtension() + + @Internal + val pathToApiScheme: String = extension.pathToApiSchemes + ?: throw IllegalStateException("Configure path to api schemes for api generator plugin") + + @Internal + val outputLanguage: OutputLanguage = extension.outputLanguage + ?: throw IllegalStateException("Configure output language code for api generator plugin") + + @Inject + abstract fun getWorkerExecutor(): WorkerExecutor + + @Incremental + @InputFiles + val inputFiles: FileTree = project.files(pathToApiScheme).asFileTree + + @OutputFiles + val outputFiles: FileTree = project.files(extension.outputDirPath).asFileTree + + @get:Input + abstract val tempApiDirDirectory: Property + + @TaskAction + fun execute(inputChanges: InputChanges): Unit = with(project) { + if (inputChanges.isIncremental) { + handleInputChanges(inputChanges) + } else { + generateApi( + pathToApi = pathToApiScheme, + outputLanguage = outputLanguage, + recreateOutputDir = true + ) + } + } + + private fun generateApi( + pathToApi: String, + outputLanguage: OutputLanguage, + recreateOutputDir: Boolean + ) = with(project) { + javaexec { + main = "-jar" + workingDir = rootDir + args = listOfNotNull( + configurations.getByName("apiGenerator").asPath, + "generate-client-code", + "--output-language", + outputLanguage.argName, + "--specification-path", + pathToApi, + "--kotlin-methods-generation-mode".takeIf { outputLanguage.methodOutputType != null }, + outputLanguage.methodOutputType?.argName, + "--output-path", + extension.outputDirPath, + "--package-name", + extension.outputPackageName, + "--recreate_output_dirs", + recreateOutputDir.toString() + ) + } + } + + private fun handleInputChanges(inputChanges: InputChanges) { + val modifiedFiles = hashSetOf() + + inputChanges.getFileChanges(inputFiles) + .filter { it.fileType == FileType.FILE } + .forEach { change -> + when (change.changeType) { + ChangeType.REMOVED -> { + outputFiles.find { it.nameWithoutExtension == change.file.nameWithoutExtension }?.delete() + } + + else -> modifiedFiles.add(change.file) + } + } + + if (modifiedFiles.isEmpty()) return + + handleModifiedFiles(modifiedFiles) + } + + private fun handleModifiedFiles(modifiedFiles: MutableCollection) { + val dependencyResolver = ApiModelsDependencyResolver( + pathToApi = pathToApiScheme, + fileParser = FileParser() + ) + + modifiedFiles.addAll( + dependencyResolver.getDependenciesFromFiles(modifiedFiles) + ) + + val tempApiDirectory = tempApiDirDirectory.getOrElse("") + + if (tempApiDirectory.isBlank()) { + throw IllegalStateException("TempApiDirDirectory is blank or not specified.") + } + + copyAllFilesToTempDir( + tempDirPath = tempApiDirectory, + files = modifiedFiles + ) + + generateApi( + pathToApi = tempApiDirectory, + outputLanguage = outputLanguage, + recreateOutputDir = false + ) + } + + private fun copyAllFilesToTempDir(tempDirPath: String, files: Collection) { + val workQueue = getWorkerExecutor().noIsolation() + + files.forEach { file -> + val newFilePath = pathToApiScheme.toPath() + .relativize(file.absolutePath.toPath()).toString() + + val newFileFullPath = Path.of(tempDirPath, newFilePath).toString() + + workQueue.submit(CopyApiModelsToTempDirTask::class.java) { + getSourceFile().set(file) + getTargetFilePath().set(newFileFullPath) + } + } + + } +} diff --git a/gradle/plugins/src/main/java/apigen/depencency_resolver/ApiModelsDependencyResolver.kt b/gradle/plugins/src/main/java/apigen/depencency_resolver/ApiModelsDependencyResolver.kt new file mode 100644 index 0000000..e36237d --- /dev/null +++ b/gradle/plugins/src/main/java/apigen/depencency_resolver/ApiModelsDependencyResolver.kt @@ -0,0 +1,59 @@ +package apigen.depencency_resolver + +import apigen.depencency_resolver.FileParser.Companion.NAME_JSON_KEY +import groovy.json.JsonSlurper +import java.io.File + +/** + * Класс для разрешения зависимостей АПИ моделей от других АПИ моделей. + * + * @param pathToApi путь к директории АПИ, где будут искаться зависимости + * @param fileParser класс, ответственный за поиск типов в АПИ модельке. + */ +@Suppress("UNCHECKED_CAST") +class ApiModelsDependencyResolver( + pathToApi: String, + private val fileParser: FileParser +) { + + // Мапа для хранения и быстрого поиска типа модельки по файлам, т.к. не всегда имя файла совпадает с типом модельки. + private val modelTypes: HashMap = hashMapOf() + private val handledFiles = hashSetOf() + + init { + File(pathToApi) + .walk() + .filter { it.isFile } + .forEach { file -> + val json = JsonSlurper().parse(file) as? Map + val type = json?.getOrDefault(NAME_JSON_KEY, file.nameWithoutExtension).toString() + + modelTypes[type] = file + } + } + + fun getDependenciesFromFiles(files: Collection): Set = + files.fold(setOf()) { dependencies, file -> + dependencies + getFileDependencies(file) + } + + private fun getFileDependencies(file: File): Set { + if (handledFiles.contains(file)) { + return emptySet() + } + + handledFiles.add(file) + + val json = JsonSlurper().parse(file) as Map + val files = fileParser.getAllTypeNamesFromJson(json) + .map { type -> + modelTypes[type] + ?: throw IllegalArgumentException("Couldn't resolve $type in ${file.name}") + } + .toSet() + + return files.fold(files) { dependencies, myFile -> + dependencies + getFileDependencies(myFile) + } + } +} diff --git a/gradle/plugins/src/main/java/apigen/depencency_resolver/CopyApiModelsToTempDirTask.kt b/gradle/plugins/src/main/java/apigen/depencency_resolver/CopyApiModelsToTempDirTask.kt new file mode 100644 index 0000000..fddb08a --- /dev/null +++ b/gradle/plugins/src/main/java/apigen/depencency_resolver/CopyApiModelsToTempDirTask.kt @@ -0,0 +1,22 @@ +package apigen.depencency_resolver + +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import java.io.File + +abstract class CopyApiModelsToTempDirTask: WorkAction { + + override fun execute() { + val sourceFile = parameters.getSourceFile().asFile.get() + val target = File(parameters.getTargetFilePath().get()) + + sourceFile.copyTo(target = target) + } +} + +interface CopyApiModelsToTempDirTaskParams: WorkParameters { + fun getSourceFile(): RegularFileProperty + fun getTargetFilePath(): Property +} diff --git a/gradle/plugins/src/main/java/apigen/depencency_resolver/FileParser.kt b/gradle/plugins/src/main/java/apigen/depencency_resolver/FileParser.kt new file mode 100644 index 0000000..861a8ce --- /dev/null +++ b/gradle/plugins/src/main/java/apigen/depencency_resolver/FileParser.kt @@ -0,0 +1,103 @@ +package apigen.depencency_resolver + +@Suppress("UNCHECKED_CAST") +class FileParser { + + companion object { + const val NAME_JSON_KEY = "name" + const val PARENT_JSON_KEY = "parent" + const val TYPE_JSON_KEY = "type" + } + + private val defaultTypes = hashSetOf( + "Bool", + "String", + "DateTime", + "Long", + "Int", + "Double", + "Decimal", + "Date", + "DateTimeTimestamp", + "Color", + "StringDecimal", + "Url", + "Map", + "null" // Костыль, чтобы не писать проверки на null при получении типа из мапы + ) + + fun getAllTypeNamesFromJson(model: Map): Set { + val currentModelType = Type.getType(model[NAME_JSON_KEY].toString()) + + var generic = "" + + if (currentModelType is TypeWithGeneric) { + generic = currentModelType.getGenericTypeName() + } + + val parentRawTypeName = model[PARENT_JSON_KEY].toString() + + return model.entries + .fold(setOf(parentRawTypeName)) { foundRawTypeNames: Set, jsonEntry: Map.Entry -> + foldDependencies(foundRawTypeNames, jsonEntry) + } + .minus(defaultTypes) + .minus(generic) + .fold(setOf()) { foundTypeNames, rawTypeName -> foundTypeNames + getTypeFromRawTypeNames(rawTypeName) } + } + + private fun foldDependencies(foundTypes: Set, jsonEntry: Map.Entry): Set { + val value = jsonEntry.value + + val newTypes: Set = when { + (value as? Collection>) != null -> { + getTypeFromCollection(value) + } + + (value as? Map) != null && jsonEntry.key == TYPE_JSON_KEY -> { + val typeName = value[NAME_JSON_KEY].toString() + val parentTypeName = value[PARENT_JSON_KEY].toString() + + setOf(typeName, parentTypeName) + } + + else -> setOf() + } + + return if (newTypes.isEmpty()) { + foundTypes + } else { + foundTypes + newTypes + } + } + + private fun getTypeFromCollection(value: Collection>): Set = + value.fold(setOf()) { types: Set, collectionEntry: Map -> + types + getAllTypeNamesFromJson(collectionEntry) + } + + private fun getTypeFromRawTypeNames(typeName: String): Set { + val type = Type.getType(typeName) + val newTypeNames = mutableSetOf() + + when (type) { + is SimpleType -> { + newTypeNames.add(type.getMainTypeName()) + } + + is ArrayType -> { + newTypeNames.add(type.getMainTypeName()) + } + + is MapType -> { + newTypeNames.addAll(type.getSubTypeNames()) + } + + is TypeWithGeneric -> { + newTypeNames.add(type.getMainTypeName()) + } + } + + return newTypeNames + } +} \ No newline at end of file diff --git a/gradle/plugins/src/main/java/apigen/depencency_resolver/Type.kt b/gradle/plugins/src/main/java/apigen/depencency_resolver/Type.kt new file mode 100644 index 0000000..e636612 --- /dev/null +++ b/gradle/plugins/src/main/java/apigen/depencency_resolver/Type.kt @@ -0,0 +1,57 @@ +package apigen.depencency_resolver + +sealed class Type(protected val typeName: String) { + + companion object { + private val simpleTypeRegex = Regex("\\w*") + private val mapRegex = Regex("Map<.*?,.*?>") + private val arrayRegex = Regex(".*?\\[?]") + private val typeWithGenericRegex = Regex(".*?<.*?>") + + fun getType(typeName: String): Type = when { + typeName.matches(mapRegex) -> MapType(typeName) + typeName.matches(arrayRegex) -> ArrayType(typeName) + typeName.matches(typeWithGenericRegex) -> TypeWithGeneric(typeName) + typeName.matches(simpleTypeRegex) -> SimpleType(typeName) + else -> throw IllegalArgumentException("Cannot define type for $typeName") + } + } + + abstract fun getMainTypeName(): String +} + +// CardV2 +class SimpleType(typeName: String) : Type(typeName) { + + override fun getMainTypeName(): String = typeName +} + +// String[] +class ArrayType(typeName: String): Type(typeName) { + + override fun getMainTypeName(): String = typeName.dropLast(2) +} + +// BaseResponse +class TypeWithGeneric(typeName: String): Type(typeName) { + + override fun getMainTypeName(): String = typeName + .substringBefore("<") + + fun getGenericTypeName(): String = typeName + .substringAfter("<") + .dropLast(1) +} + +// Subscription +class MapType(typeName: String): Type(typeName) { + + override fun getMainTypeName(): String = typeName.substringBefore("<") + + fun getSubTypeNames(): Set = typeName + .substringAfter("<") + .dropLast(1) + .split(",") + .map { it.trim() } + .toSet() +} diff --git a/gradle/plugins/src/main/java/static_analysis/utils/Project.kt b/gradle/plugins/src/main/java/static_analysis/utils/Project.kt index f1c46cf..2d11e3a 100644 --- a/gradle/plugins/src/main/java/static_analysis/utils/Project.kt +++ b/gradle/plugins/src/main/java/static_analysis/utils/Project.kt @@ -1,5 +1,7 @@ package static_analysis.utils +import apigen.ApiGeneratorExtension +import apigen.ApiGeneratorPlugin import org.gradle.api.Project import org.gradle.api.file.FileTree import java.io.File @@ -25,3 +27,8 @@ fun Project.getSources(excludes: String): FileTree = files( .filter { it.exists() && it.isDirectory } .map { it.path } ).asFileTree + +fun Project.getApiGeneratorExtension(): ApiGeneratorExtension = + extensions.getByName(ApiGeneratorPlugin.API_GENERATOR_EXT_NAME) as ApiGeneratorExtension + +fun Project.getProjectName() = rootDir.absolutePath.substringAfterLast(File.separator) \ No newline at end of file diff --git a/gradle/plugins/src/main/java/static_analysis/utils/String.kt b/gradle/plugins/src/main/java/static_analysis/utils/String.kt index a2077ba..07b8a3a 100644 --- a/gradle/plugins/src/main/java/static_analysis/utils/String.kt +++ b/gradle/plugins/src/main/java/static_analysis/utils/String.kt @@ -2,6 +2,7 @@ package static_analysis.utils import java.io.File import java.io.IOException +import java.nio.file.Path import java.util.concurrent.TimeUnit fun String.runCommand( @@ -23,3 +24,5 @@ fun String.runCommand( null } } + +fun String.toPath(): Path = Path.of(this) diff --git a/gradle/scripts/stringGenerator.gradle b/gradle/scripts/stringGenerator.gradle index 9c47e90..d87b477 100644 --- a/gradle/scripts/stringGenerator.gradle +++ b/gradle/scripts/stringGenerator.gradle @@ -1,9 +1,15 @@ import groovy.json.JsonSlurper import groovy.xml.MarkupBuilder -task stringGenerator { - generate(android.languageMap, project) - println("Strings generated!") +tasks.register('stringGenerator') { + def sources = android.languageMap + + outputs.files(sources) + + doLast { + generate(sources, project) + println("Strings generated!") + } } private def generate(Map sources, Project project) {