diff --git a/CHANGELOG.md b/CHANGELOG.md index 8795d05b210..58477f114b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.16.13 + +* `Repos`: + * `Generator`: + * Module has been created + ## 0.16.12 * `Repos`: diff --git a/gradle.properties b/gradle.properties index 9a4f1279a67..10a4b54dcbf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,5 +14,5 @@ crypto_js_version=4.1.1 # Project data group=dev.inmo -version=0.16.12 -android_code_version=180 +version=0.16.13 +android_code_version=181 diff --git a/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/annotations/GenerateCRUDModel.kt b/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/annotations/GenerateCRUDModel.kt new file mode 100644 index 00000000000..d77e5ee8641 --- /dev/null +++ b/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/annotations/GenerateCRUDModel.kt @@ -0,0 +1,35 @@ +package dev.inmo.micro_utils.repos.annotations + +import kotlin.reflect.KClass + +/** + * Use this annotation and ksp generator (module `micro_utils.repos.generator`) to create the next hierarchy of models: + * + * * New model. For example: data class NewTest + * * Registered model. For example: data class RegisteredTest + * + * @param registeredSupertypes These [KClass]es will be used as supertypes for registered model + * @param serializable If true (default) will generate @[kotlinx.serialization.Serializable] for models. Affects [generateSerialName] + * @param serializable If true (default) will generate @[kotlinx.serialization.SerialName] for models with their names as values + * + * @see GenerateCRUDModelExcludeOverride + */ +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.CLASS) +annotation class GenerateCRUDModel( + vararg val registeredSupertypes: KClass<*>, + val serializable: Boolean = true, + val generateSerialName: Boolean = true +) + + +/** + * Use this annotation on properties which should be excluded from overriding in models. + * + * @see GenerateCRUDModel + */ +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.PROPERTY) +annotation class GenerateCRUDModelExcludeOverride + + diff --git a/repos/generator/README.md b/repos/generator/README.md new file mode 100644 index 00000000000..d23a6233625 --- /dev/null +++ b/repos/generator/README.md @@ -0,0 +1,154 @@ +# Koin generator + +It is Kotlin Symbol Processing generator for simple creating of typical models: `New` and `Registered`. + +1. [What may do this generator](#what-may-do-this-generator) +2. [How to add generator](#how-to-add-generator) + +## What may do this generator + +So, you have several known things related to models: + +* Interface with all necessary properties +* Id class or some registered marker + +Minimal sample will be next: + +```kotlin +@GenerateCRUDModel +interface Sample { + val property1: String + val property2: Int +} +``` + +And generator will create: + +```kotlin +@Serializable +@SerialName("NewSample") +data class NewSample( + override val property1: String, + override val property2: Int, +) : Sample + +@Serializable +@SerialName("RegisteredSample") +data class RegisteredSample( + override val property1: String, + override val property2: Int, +) : Sample + +fun Sample.asNew(): NewSample = NewSample(property1, property2) + +fun Sample.asRegistered(): RegisteredSample = RegisteredSample(property1, property2) +``` + +But in most cases you will need to create some id class and registered interface: + +```kotlin +@Serializable +@JvmInline +value class SampleId( + val long: Long +) + +sealed interface IRegisteredSample : Sample { + val id: SampleId + + @GenerateCRUDModelExcludeOverride + val excludedProperty2: Boolean + get() = false +} +``` + +As you may see, we have added `GenerateCRUDModelExcludeOverride` annotation. Properties marked with this annotation +WILL NOT be inclued into overriding in registered class (or your base interface if used there). So, if you will wish to +create model with id, use next form: + +```kotlin +@GenerateCRUDModel(IRegisteredSample::class) +interface Sample { + val property1: String + val property2: Int +} +``` + +And generated registered class will be changed: + +```kotlin +@Serializable +@SerialName(value = "NewSample") +data class NewSample( + override val property1: String, + override val property2: Int, +) : Sample + +@Serializable +@SerialName(value = "RegisteredSample") +data class RegisteredSample( + override val id: SampleId, + override val property1: String, + override val property2: Int, +) : Sample, IRegisteredSample + +fun Sample.asNew(): NewSample = NewSample(property1, property2) + +fun Sample.asRegistered(id: SampleId): RegisteredSample = RegisteredSample(id, property1, property2) +``` + +So, full sample will look like: + +```kotlin +/** + * Your id value class. In fact, but it is not necessary + */ +@Serializable +@JvmInline +value class SampleId( + val long: Long +) + +@GenerateCRUDModel(IRegisteredSample::class) +sealed interface Sample { + val property1: String + val property2: Int + + @GenerateCRUDModelExcludeOverride + val excludedProperty: String + get() = "excluded" +} + +sealed interface IRegisteredSample : Sample { + val id: SampleId + + @GenerateCRUDModelExcludeOverride + val excludedProperty2: Boolean + get() = false +} +``` + +You always may: + +* Use any number of registered classes +* Disable serialization for models +* Disable serial names generation + +## How to add generator + +**Note: $ksp_version in the samples above is equal to supported `ksp` version presented in `/gradle/libs.versions.toml` of project** + +**Note: $microutils_version in the version of MicroUtils library in your project** + +1. Add `classpath` in `build.gradle` (`classpath "com.google.devtools.ksp:symbol-processing-gradle-plugin:$ksp_version"`) +2. Add plugin to the plugins list of your module: `id "com.google.devtools.ksp"` +3. In `dependencies` block add to the required target/compile the dependency `dev.inmo:micro_utils.repos.generator:$microutils_version`: + ```groovy + dependencies { + add("kspCommonMainMetadata", "dev.inmo:micro_utils.repos.generator:$microutils_version") // will work in commonMain of your multiplatform module + add("kspJvm", "dev.inmo:micro_utils.repos.generator:$microutils_version") // will work in main of your JVM module + } + + ksp { // this generator do not require any arguments and we should left `ksp` empty + } + ``` diff --git a/repos/generator/build.gradle b/repos/generator/build.gradle new file mode 100644 index 00000000000..112b1af1eac --- /dev/null +++ b/repos/generator/build.gradle @@ -0,0 +1,16 @@ +plugins { + id "org.jetbrains.kotlin.jvm" +} + +apply from: "$publishJvmOnlyPath" + +repositories { + mavenCentral() +} + +dependencies { + api libs.kt.reflect + api project(":micro_utils.repos.common") + api libs.kotlin.poet + api libs.ksp +} diff --git a/repos/generator/src/main/kotlin/Processor.kt b/repos/generator/src/main/kotlin/Processor.kt new file mode 100644 index 00000000000..9e8780b826c --- /dev/null +++ b/repos/generator/src/main/kotlin/Processor.kt @@ -0,0 +1,217 @@ +package dev.inmo.micro_utils.repos.generator + +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.getAnnotationsByType +import com.google.devtools.ksp.isAnnotationPresent +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSClassifierReference +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.google.devtools.ksp.symbol.KSReferenceElement +import com.google.devtools.ksp.symbol.KSType +import com.google.devtools.ksp.symbol.KSTypeAlias +import com.google.devtools.ksp.symbol.KSValueArgument +import com.google.devtools.ksp.symbol.Nullability +import com.squareup.kotlinpoet.AnnotationSpec +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.PropertySpec +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.asTypeName +import com.squareup.kotlinpoet.ksp.toAnnotationSpec +import com.squareup.kotlinpoet.ksp.toClassName +import com.squareup.kotlinpoet.ksp.toTypeName +import dev.inmo.micro_utils.repos.annotations.GenerateCRUDModel +import dev.inmo.micro_utils.repos.annotations.GenerateCRUDModelExcludeOverride +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.io.File +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.memberProperties + +private fun KSClassifierReference.quilifiedName(): String = "${qualifier ?.let { "${it.quilifiedName()}." } ?: ""}${referencedName()}" + +class Processor( + private val codeGenerator: CodeGenerator +) : SymbolProcessor { + private val KSPropertyDeclaration.typeName: TypeName + get() { + return runCatching { + type.toTypeName() + }.getOrElse { + val element = type.element as KSClassifierReference + (type.element as KSClassifierReference).let { + ClassName( + element.qualifier ?.quilifiedName() ?: "", + element.referencedName() + ) + } + } + } + @OptIn(KspExperimental::class) + override fun process(resolver: Resolver): List { + val toRetry = resolver.getSymbolsWithAnnotation( + GenerateCRUDModel::class.qualifiedName!! + ).filterIsInstance().filterNot { ksClassDeclaration -> + val ksFile = ksClassDeclaration.containingFile ?: return@filterNot false + runCatching { + FileSpec.builder( + ksClassDeclaration.packageName.asString(), + "GeneratedModels${ksFile.fileName.removeSuffix(".kt")}" + ).apply { + val annotation = ksClassDeclaration.getAnnotationsByType(GenerateCRUDModel::class).first() + addFileComment( + """ + THIS CODE HAVE BEEN GENERATED AUTOMATICALLY + TO REGENERATE IT JUST DELETE FILE + ORIGINAL FILE: ${ksFile.fileName} + """.trimIndent() + ) + val newName = "New${ksClassDeclaration.simpleName.getShortName()}" + val registeredName = "Registered${ksClassDeclaration.simpleName.getShortName()}" + + val allKSClassProperties = ksClassDeclaration.getAllProperties() + val excludedKSClassProperties = allKSClassProperties.filter { + it.isAnnotationPresent(GenerateCRUDModelExcludeOverride::class) + } + val excludedKSClassPropertiesNames = excludedKSClassProperties.map { it.simpleName.asString() } + val ksClassProperties = allKSClassProperties.filter { + it !in excludedKSClassProperties + } + val ksClassPropertiesNames = ksClassProperties.map { it.simpleName.asString() } + val newNewType = TypeSpec.classBuilder(newName).apply { + val typeBuilder = this + addSuperinterface(ksClassDeclaration.toClassName()) + addModifiers(KModifier.DATA) + if (annotation.serializable) { + addAnnotation(Serializable::class) + if (annotation.generateSerialName) { + addAnnotation(AnnotationSpec.get(SerialName(newName))) + } + } + primaryConstructor( + FunSpec.constructorBuilder().apply { + ksClassProperties.forEach { + addParameter(it.simpleName.getShortName(), it.typeName) + typeBuilder.addProperty( + PropertySpec.builder(it.simpleName.getShortName(), it.typeName, KModifier.OVERRIDE).apply { + initializer(it.simpleName.getShortName()) + }.build() + ) + } + }.build() + ) + }.build() + addType( + newNewType + ) + + val registeredSupertypes = ksClassDeclaration.annotations.filter { + it.shortName.asString() == GenerateCRUDModel::class.simpleName && + it.annotationType.resolve().declaration.qualifiedName ?.asString() == GenerateCRUDModel::class.qualifiedName + }.flatMap { + (it.arguments.first().value as List).map { it.declaration as KSClassDeclaration } + }.toList() + + + val registeredTypesProperties: List = registeredSupertypes.flatMap { registeredType -> + registeredType.getAllProperties() + }.filter { + it.simpleName.asString() !in excludedKSClassPropertiesNames && it.getAnnotationsByType(GenerateCRUDModelExcludeOverride::class).none() + } + val allProperties: List = ksClassProperties.toList() + registeredTypesProperties + val propertiesToOverrideInRegistered = allProperties.distinctBy { it.simpleName.asString() }.sortedBy { property -> + val name = property.simpleName.asString() + + ksClassPropertiesNames.indexOf(name).takeIf { it > -1 } ?.let { + it + allProperties.size + } ?: allProperties.indexOfFirst { it.simpleName.asString() == name } + } + + val newRegisteredType = TypeSpec.classBuilder(registeredName).apply { + val typeBuilder = this + addSuperinterface(ksClassDeclaration.toClassName()) + if (annotation.serializable) { + addAnnotation(Serializable::class) + + if (annotation.generateSerialName) { + addAnnotation( + AnnotationSpec.get(SerialName(registeredName)) + ) + } + } + addSuperinterfaces(registeredSupertypes.map { it.toClassName() }) + addModifiers(KModifier.DATA) + primaryConstructor( + FunSpec.constructorBuilder().apply { + propertiesToOverrideInRegistered.forEach { + addParameter( + ParameterSpec.builder(it.simpleName.getShortName(), it.typeName).apply { + annotations += it.annotations.map { it.toAnnotationSpec() } + }.build() + ) + typeBuilder.addProperty( + PropertySpec.builder(it.simpleName.getShortName(), it.typeName, KModifier.OVERRIDE).apply { + initializer(it.simpleName.getShortName()) + }.build() + ) + } + }.build() + ) + }.build() + addType( + newRegisteredType + ) + + addFunction( + FunSpec.builder("asNew").apply { + receiver(ksClassDeclaration.toClassName()) + addCode( + CodeBlock.of( + "return ${newNewType.name}(${newNewType.propertySpecs.joinToString { it.name }})" + ) + ) + returns(ClassName(packageName, newNewType.name!!)) + }.build() + ) + + addFunction( + FunSpec.builder("asRegistered").apply { + receiver(ksClassDeclaration.toClassName()) + (registeredTypesProperties.filter { it.simpleName.asString() !in ksClassPropertiesNames }).forEach { + addParameter(it.simpleName.asString(), it.typeName) + } + addCode( + CodeBlock.of( + "return ${newRegisteredType.name}(${newRegisteredType.propertySpecs.joinToString { it.name }})" + ) + ) + returns(ClassName(packageName, newRegisteredType.name!!)) + }.build() + ) + }.build().let { + File( + File(ksFile.filePath).parent, + "GeneratedModels${ksFile.fileName}" + ).takeIf { !it.exists() } ?.apply { + parentFile.mkdirs() + + writer().use { writer -> + it.writeTo(writer) + } + } + } + }.isSuccess + }.toList() + + return toRetry + } +} diff --git a/repos/generator/src/main/kotlin/Provider.kt b/repos/generator/src/main/kotlin/Provider.kt new file mode 100644 index 00000000000..9610778e062 --- /dev/null +++ b/repos/generator/src/main/kotlin/Provider.kt @@ -0,0 +1,11 @@ +package dev.inmo.micro_utils.repos.generator + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +class Provider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = Processor( + environment.codeGenerator + ) +} diff --git a/repos/generator/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/repos/generator/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 00000000000..a40559b08b3 --- /dev/null +++ b/repos/generator/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +dev.inmo.micro_utils.repos.generator.Provider diff --git a/repos/generator/test/build.gradle b/repos/generator/test/build.gradle new file mode 100644 index 00000000000..84a7b71ace9 --- /dev/null +++ b/repos/generator/test/build.gradle @@ -0,0 +1,27 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" + id "com.google.devtools.ksp" +} + +apply from: "$mppProjectWithSerializationPresetPath" + + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":micro_utils.repos.common") + } + } + } +} + + +dependencies { + add("kspCommonMainMetadata", project(":micro_utils.repos.generator")) +} + +ksp { +} diff --git a/repos/generator/test/src/commonMain/kotlin/GeneratedModelsTest.kt b/repos/generator/test/src/commonMain/kotlin/GeneratedModelsTest.kt new file mode 100644 index 00000000000..27111d44cc8 --- /dev/null +++ b/repos/generator/test/src/commonMain/kotlin/GeneratedModelsTest.kt @@ -0,0 +1,31 @@ +// THIS CODE HAVE BEEN GENERATED AUTOMATICALLY +// TO REGENERATE IT JUST DELETE FILE +// ORIGINAL FILE: Test.kt +package dev.inmo.micro_utils.repos.generator.test + +import kotlin.Int +import kotlin.String +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName(value = "NewTest") +public data class NewTest( + public override val property1: String, + public override val property2: Int, + public override val parent: ParentTypeId?, +) : Test + +@Serializable +@SerialName(value = "RegisteredTest") +public data class RegisteredTest( + public override val id: TestId, + public override val property1: String, + public override val property2: Int, + public override val parent: ParentTypeId?, +) : Test, IRegisteredTest + +public fun Test.asNew(): NewTest = NewTest(property1, property2, parent) + +public fun Test.asRegistered(id: TestId): RegisteredTest = RegisteredTest(id, property1, property2, + parent) diff --git a/repos/generator/test/src/commonMain/kotlin/Test.kt b/repos/generator/test/src/commonMain/kotlin/Test.kt new file mode 100644 index 00000000000..24bc9a92f4b --- /dev/null +++ b/repos/generator/test/src/commonMain/kotlin/Test.kt @@ -0,0 +1,33 @@ +package dev.inmo.micro_utils.repos.generator.test + +import dev.inmo.micro_utils.repos.annotations.GenerateCRUDModel +import dev.inmo.micro_utils.repos.annotations.GenerateCRUDModelExcludeOverride +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmInline + +@Serializable +@JvmInline +value class TestId( + val long: Long +) + +typealias ParentTypeId = TestId + +@GenerateCRUDModel(IRegisteredTest::class) +sealed interface Test { + val property1: String + val property2: Int + val parent: ParentTypeId? + + @GenerateCRUDModelExcludeOverride + val excludedProperty: String + get() = "excluded" +} + +sealed interface IRegisteredTest : Test { + val id: TestId + + @GenerateCRUDModelExcludeOverride + val excludedProperty2: Boolean + get() = false +} diff --git a/repos/generator/test/src/main/AndroidManifest.xml b/repos/generator/test/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..22910dac229 --- /dev/null +++ b/repos/generator/test/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/settings.gradle b/settings.gradle index 3ffca7736c8..6d422fe4e63 100644 --- a/settings.gradle +++ b/settings.gradle @@ -18,6 +18,8 @@ String[] includes = [ ":language_codes", ":language_codes:generator", ":repos:common", + ":repos:generator", + ":repos:generator:test", ":repos:cache", ":repos:exposed", ":repos:inmemory",