mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2024-11-23 02:28:47 +00:00
commit
c70626734e
@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 0.16.13
|
||||
|
||||
* `Repos`:
|
||||
* `Generator`:
|
||||
* Module has been created
|
||||
|
||||
## 0.16.12
|
||||
|
||||
* `Repos`:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
154
repos/generator/README.md
Normal file
154
repos/generator/README.md
Normal file
@ -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
|
||||
}
|
||||
```
|
16
repos/generator/build.gradle
Normal file
16
repos/generator/build.gradle
Normal file
@ -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
|
||||
}
|
217
repos/generator/src/main/kotlin/Processor.kt
Normal file
217
repos/generator/src/main/kotlin/Processor.kt
Normal file
@ -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<KSAnnotated> {
|
||||
val toRetry = resolver.getSymbolsWithAnnotation(
|
||||
GenerateCRUDModel::class.qualifiedName!!
|
||||
).filterIsInstance<KSClassDeclaration>().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<KSType>).map { it.declaration as KSClassDeclaration }
|
||||
}.toList()
|
||||
|
||||
|
||||
val registeredTypesProperties: List<KSPropertyDeclaration> = registeredSupertypes.flatMap { registeredType ->
|
||||
registeredType.getAllProperties()
|
||||
}.filter {
|
||||
it.simpleName.asString() !in excludedKSClassPropertiesNames && it.getAnnotationsByType(GenerateCRUDModelExcludeOverride::class).none()
|
||||
}
|
||||
val allProperties: List<KSPropertyDeclaration> = 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
|
||||
}
|
||||
}
|
11
repos/generator/src/main/kotlin/Provider.kt
Normal file
11
repos/generator/src/main/kotlin/Provider.kt
Normal file
@ -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
|
||||
)
|
||||
}
|
@ -0,0 +1 @@
|
||||
dev.inmo.micro_utils.repos.generator.Provider
|
27
repos/generator/test/build.gradle
Normal file
27
repos/generator/test/build.gradle
Normal file
@ -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 {
|
||||
}
|
@ -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)
|
33
repos/generator/test/src/commonMain/kotlin/Test.kt
Normal file
33
repos/generator/test/src/commonMain/kotlin/Test.kt
Normal file
@ -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
|
||||
}
|
1
repos/generator/test/src/main/AndroidManifest.xml
Normal file
1
repos/generator/test/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.repos.generator.test"/>
|
@ -18,6 +18,8 @@ String[] includes = [
|
||||
":language_codes",
|
||||
":language_codes:generator",
|
||||
":repos:common",
|
||||
":repos:generator",
|
||||
":repos:generator:test",
|
||||
":repos:cache",
|
||||
":repos:exposed",
|
||||
":repos:inmemory",
|
||||
|
Loading…
Reference in New Issue
Block a user