mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-20 15:59:23 +00:00
Compare commits
27 Commits
Author | SHA1 | Date | |
---|---|---|---|
2c7fd320eb | |||
88ee82e1c6 | |||
d6402c624e | |||
8b9c93bc10 | |||
4f5e261d01 | |||
cf455aebe6 | |||
1380d5f8e1 | |||
5f65260bfe | |||
11f0dcfc01 | |||
555b7886a4 | |||
3707a6c0ce | |||
4c8d92b4c3 | |||
8bbd33c896 | |||
ac33a3580f | |||
a64a32fbe6 | |||
9493e97a38 | |||
88bd770260 | |||
a7bd33b7bf | |||
73c724a2e5 | |||
d8cf3c6726 | |||
15dee238b5 | |||
c70626734e | |||
5a765ea1bc | |||
8215f9d2c6 | |||
d2e6d2ec80 | |||
3718181e8f | |||
0d825cf424 |
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,5 +1,37 @@
|
||||
# Changelog
|
||||
|
||||
## 0.17.3
|
||||
|
||||
* `Common`:
|
||||
* Add `fixed` extensions for `Float` and `Double`
|
||||
* New function `emptyDiff`
|
||||
* Now you may pass custom `comparisonFun` to all `diff` functions
|
||||
|
||||
## 0.17.2
|
||||
|
||||
* `FSM`:
|
||||
* `DefaultStatesManager.onUpdateContextsConflictResolver` and `DefaultStatesManager.onStartContextsConflictResolver` now return `false` by default
|
||||
|
||||
## 0.17.1
|
||||
|
||||
* **Hotfix** for absence of jvm dependencies in android modules
|
||||
* `Versions`:
|
||||
* `Ktor`: `2.2.3` -> `2.2.4`
|
||||
|
||||
## 0.17.0
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.7.20` -> `1.8.10`
|
||||
* `Serialization`: `1.4.1` -> `1.5.0`
|
||||
* `KSLog`: `0.5.4` -> `1.0.0`
|
||||
* `AppCompat`: `1.6.0` -> `1.6.1`
|
||||
|
||||
## 0.16.13
|
||||
|
||||
* `Repos`:
|
||||
* `Generator`:
|
||||
* Module has been created
|
||||
|
||||
## 0.16.12
|
||||
|
||||
* `Repos`:
|
||||
|
@@ -18,6 +18,7 @@ kotlin {
|
||||
api project(":micro_utils.coroutines")
|
||||
api libs.android.fragment
|
||||
}
|
||||
dependsOn jvmMain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -34,7 +34,11 @@ data class Diff<T> internal constructor(
|
||||
*/
|
||||
val replaced: List<Pair<@Serializable(IndexedValueSerializer::class) IndexedValue<T>, @Serializable(IndexedValueSerializer::class) IndexedValue<T>>>,
|
||||
val added: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>
|
||||
)
|
||||
) {
|
||||
fun isEmpty(): Boolean = removed.isEmpty() && replaced.isEmpty() && added.isEmpty()
|
||||
}
|
||||
|
||||
fun <T> emptyDiff(): Diff<T> = Diff(emptyList(), emptyList(), emptyList())
|
||||
|
||||
private inline fun <T> performChanges(
|
||||
potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>,
|
||||
@@ -43,14 +47,14 @@ private inline fun <T> performChanges(
|
||||
changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>,
|
||||
removedList: MutableList<IndexedValue<T>>,
|
||||
addedList: MutableList<IndexedValue<T>>,
|
||||
strictComparison: Boolean
|
||||
comparisonFun: (T?, T?) -> Boolean
|
||||
) {
|
||||
var i = -1
|
||||
val (oldObject, newObject) = potentialChanges.lastOrNull() ?: return
|
||||
for ((old, new) in potentialChanges.take(potentialChanges.size - 1)) {
|
||||
i++
|
||||
val oldOneEqualToNewObject = old ?.value === newObject ?.value || (old ?.value == newObject ?.value && !strictComparison)
|
||||
val newOneEqualToOldObject = new ?.value === oldObject ?.value || (new ?.value == oldObject ?.value && !strictComparison)
|
||||
val oldOneEqualToNewObject = comparisonFun(old ?.value, newObject ?.value)
|
||||
val newOneEqualToOldObject = comparisonFun(new ?.value, oldObject ?.value)
|
||||
if (oldOneEqualToNewObject || newOneEqualToOldObject) {
|
||||
changedList.addAll(
|
||||
potentialChanges.take(i).mapNotNull {
|
||||
@@ -104,7 +108,7 @@ private inline fun <T> performChanges(
|
||||
*/
|
||||
fun <T> Iterable<T>.calculateDiff(
|
||||
other: Iterable<T>,
|
||||
strictComparison: Boolean = false
|
||||
comparisonFun: (T?, T?) -> Boolean
|
||||
): Diff<T> {
|
||||
var i = -1
|
||||
var j = -1
|
||||
@@ -132,7 +136,7 @@ fun <T> Iterable<T>.calculateDiff(
|
||||
}
|
||||
|
||||
when {
|
||||
oldObject === newObject || (oldObject == newObject && !strictComparison) -> {
|
||||
comparisonFun(oldObject, newObject) -> {
|
||||
changedObjects.addAll(potentiallyChangedObjects.map {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
it as Pair<IndexedValue<T>, IndexedValue<T>>
|
||||
@@ -143,23 +147,49 @@ fun <T> Iterable<T>.calculateDiff(
|
||||
potentiallyChangedObjects.add(oldObject ?.let { IndexedValue(i, oldObject) } to newObject ?.let { IndexedValue(j, newObject) })
|
||||
val previousOldsAdditionsSize = additionalInOld.size
|
||||
val previousNewsAdditionsSize = additionalInNew.size
|
||||
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison)
|
||||
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun)
|
||||
i -= (additionalInOld.size - previousOldsAdditionsSize)
|
||||
j -= (additionalInNew.size - previousNewsAdditionsSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
potentiallyChangedObjects.add(null to null)
|
||||
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison)
|
||||
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun)
|
||||
|
||||
return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList())
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculating [Diff] object
|
||||
*
|
||||
* @param strictComparison If this parameter set to true, objects which are not equal by links will be used as different
|
||||
* objects. For example, in case of two "Example" string they will be equal by value, but CAN be different by links
|
||||
*/
|
||||
fun <T> Iterable<T>.calculateDiff(
|
||||
other: Iterable<T>,
|
||||
strictComparison: Boolean = false
|
||||
): Diff<T> = calculateDiff(
|
||||
other,
|
||||
comparisonFun = if (strictComparison) {
|
||||
{ t1, t2 ->
|
||||
t1 === t2
|
||||
}
|
||||
} else {
|
||||
{ t1, t2 ->
|
||||
t1 === t2 || t1 == t2 // small optimization for cases when t1 and t2 are the same - comparison will be faster potentially
|
||||
}
|
||||
}
|
||||
)
|
||||
inline fun <T> Iterable<T>.diff(
|
||||
other: Iterable<T>,
|
||||
strictComparison: Boolean = false
|
||||
): Diff<T> = calculateDiff(other, strictComparison)
|
||||
inline fun <T> Iterable<T>.diff(
|
||||
other: Iterable<T>,
|
||||
noinline comparisonFun: (T?, T?) -> Boolean
|
||||
): Diff<T> = calculateDiff(other, comparisonFun)
|
||||
|
||||
inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new)
|
||||
inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, strictComparison = false)
|
||||
inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true)
|
||||
|
||||
/**
|
||||
@@ -187,3 +217,22 @@ fun <T> MutableList<T>.applyDiff(
|
||||
set(new.index, new.value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this]
|
||||
* mutable list
|
||||
*/
|
||||
fun <T> MutableList<T>.applyDiff(
|
||||
source: Iterable<T>,
|
||||
comparisonFun: (T?, T?) -> Boolean
|
||||
): Diff<T> = calculateDiff(source, comparisonFun).also {
|
||||
for (i in it.removed.indices.sortedDescending()) {
|
||||
removeAt(it.removed[i].index)
|
||||
}
|
||||
it.added.forEach { (i, t) ->
|
||||
add(i, t)
|
||||
}
|
||||
it.replaced.forEach { (_, new) ->
|
||||
set(new.index, new.value)
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,6 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
val FixedSignsRange = 0 .. 100
|
||||
|
||||
expect fun Float.fixed(signs: Int): Float
|
||||
expect fun Double.fixed(signs: Int): Double
|
@@ -0,0 +1,4 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
actual fun Float.fixed(signs: Int): Float = this.asDynamic().toFixed(signs.coerceIn(FixedSignsRange)).unsafeCast<String>().toFloat()
|
||||
actual fun Double.fixed(signs: Int): Double = this.asDynamic().toFixed(signs.coerceIn(FixedSignsRange)).unsafeCast<String>().toDouble()
|
@@ -0,0 +1,12 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
actual fun Float.fixed(signs: Int): Float = BigDecimal.valueOf(this.toDouble())
|
||||
.setScale(signs.coerceIn(FixedSignsRange), RoundingMode.HALF_UP)
|
||||
.toFloat();
|
||||
|
||||
actual fun Double.fixed(signs: Int): Double = BigDecimal.valueOf(this)
|
||||
.setScale(signs.coerceIn(FixedSignsRange), RoundingMode.HALF_UP)
|
||||
.toDouble();
|
@@ -1,30 +1,5 @@
|
||||
apply plugin: 'com.getkeepsafe.dexcount'
|
||||
|
||||
ext {
|
||||
jvmKotlinFolderFile = {
|
||||
String sep = File.separator
|
||||
return new File("${project.projectDir}${sep}src${sep}jvmMain${sep}kotlin")
|
||||
}
|
||||
|
||||
enableIncludingJvmCodeInAndroidPart = {
|
||||
File jvmKotlinFolder = jvmKotlinFolderFile()
|
||||
if (jvmKotlinFolder.exists()) {
|
||||
android.sourceSets.main.java.srcDirs += jvmKotlinFolder.path
|
||||
}
|
||||
}
|
||||
|
||||
disableIncludingJvmCodeInAndroidPart = {
|
||||
File jvmKotlinFolder = jvmKotlinFolderFile()
|
||||
String[] oldDirs = android.sourceSets.main.java.srcDirs
|
||||
android.sourceSets.main.java.srcDirs = []
|
||||
for (oldDir in oldDirs) {
|
||||
if (oldDir != jvmKotlinFolder.path) {
|
||||
android.sourceSets.main.java.srcDirs += oldDir
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||
buildToolsVersion libs.versions.android.props.buildTools.get()
|
||||
@@ -58,10 +33,4 @@ android {
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
String sep = File.separator
|
||||
main.java.srcDirs += "src${sep}main${sep}kotlin"
|
||||
enableIncludingJvmCodeInAndroidPart()
|
||||
}
|
||||
}
|
||||
|
@@ -48,8 +48,8 @@ interface DefaultStatesManagerRepo<T : State> {
|
||||
*/
|
||||
open class DefaultStatesManager<T : State>(
|
||||
protected val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
|
||||
protected val onStartContextsConflictResolver: suspend (current: T, new: T) -> Boolean = { _, _ -> true },
|
||||
protected val onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
|
||||
protected val onStartContextsConflictResolver: suspend (current: T, new: T) -> Boolean = { _, _ -> false },
|
||||
protected val onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> false }
|
||||
) : StatesManager<T> {
|
||||
protected val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
|
||||
override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow()
|
||||
|
@@ -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.17.3
|
||||
android_code_version=185
|
||||
|
@@ -1,36 +1,36 @@
|
||||
[versions]
|
||||
|
||||
kt = "1.7.20"
|
||||
kt-serialization = "1.4.1"
|
||||
kt = "1.8.10"
|
||||
kt-serialization = "1.5.0"
|
||||
kt-coroutines = "1.6.4"
|
||||
|
||||
kslog = "0.5.4"
|
||||
kslog = "1.0.0"
|
||||
|
||||
jb-compose = "1.2.2"
|
||||
jb-compose = "1.3.1-rc01"
|
||||
jb-exposed = "0.41.1"
|
||||
jb-dokka = "1.7.20"
|
||||
|
||||
klock = "3.4.0"
|
||||
uuid = "0.6.0"
|
||||
uuid = "0.7.0"
|
||||
|
||||
ktor = "2.2.3"
|
||||
ktor = "2.2.4"
|
||||
|
||||
gh-release = "2.4.1"
|
||||
|
||||
koin = "3.3.2"
|
||||
|
||||
ksp = "1.7.20-1.0.8"
|
||||
ksp = "1.8.10-1.0.9"
|
||||
kotlin-poet = "1.12.0"
|
||||
|
||||
android-gradle = "7.3.0"
|
||||
dexcount = "3.1.0"
|
||||
android-gradle = "7.3.1"
|
||||
dexcount = "4.0.0"
|
||||
|
||||
android-coreKtx = "1.9.0"
|
||||
android-recyclerView = "1.2.1"
|
||||
android-appCompat = "1.6.0"
|
||||
android-appCompat = "1.6.1"
|
||||
android-fragment = "1.5.5"
|
||||
android-espresso = "3.4.0"
|
||||
android-test = "1.1.3"
|
||||
android-espresso = "3.5.1"
|
||||
android-test = "1.1.5"
|
||||
|
||||
android-props-minSdk = "21"
|
||||
android-props-compileSdk = "33"
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@@ -15,7 +15,6 @@ kotlin {
|
||||
api libs.ktor.client
|
||||
}
|
||||
}
|
||||
|
||||
androidMain {
|
||||
dependsOn jvmMain
|
||||
}
|
||||
|
@@ -44,7 +44,8 @@ actual suspend fun <T> HttpClient.uniUpload(
|
||||
val withBinary = data.values.any { it is File || it is UniUploadFileInfo }
|
||||
|
||||
val formData = formData {
|
||||
data.forEach { (k, v) ->
|
||||
for (k in data.keys) {
|
||||
val v = data[k] ?: continue
|
||||
when (v) {
|
||||
is File -> append(
|
||||
k,
|
||||
@@ -89,7 +90,7 @@ actual suspend fun <T> HttpClient.uniUpload(
|
||||
submitForm(
|
||||
url,
|
||||
Parameters.build {
|
||||
formData.forEach {
|
||||
for (it in formData) {
|
||||
val formItem = (it as PartData.FormItem)
|
||||
append(it.name!!, it.value)
|
||||
}
|
||||
|
@@ -17,5 +17,8 @@ kotlin {
|
||||
api libs.ktor.io
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependsOn jvmMain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -50,6 +50,8 @@ kotlin {
|
||||
implementation libs.android.espresso
|
||||
}
|
||||
}
|
||||
|
||||
androidMain.dependsOn jvmMain
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -61,6 +61,8 @@ kotlin {
|
||||
implementation libs.android.espresso
|
||||
}
|
||||
}
|
||||
|
||||
androidMain.dependsOn jvmMain
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -32,5 +32,3 @@ kotlin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disableIncludingJvmCodeInAndroidPart()
|
||||
|
@@ -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",
|
||||
|
Reference in New Issue
Block a user