Compare commits

..

113 Commits

Author SHA1 Message Date
c9e320b72a update compose version 2023-03-09 23:22:33 +06:00
555956087d add android manifest to mapper module 2023-03-09 22:23:56 +06:00
b3f468f901 add docs to mapper serialization and allow to use them as supertypes 2023-03-09 22:21:22 +06:00
f5f7511781 add mapper serializer 2023-03-09 21:55:07 +06:00
4be1d93f60 start 0.17.4 2023-03-09 21:49:15 +06:00
7d684608ef Merge pull request #228 from InsanusMokrassar/0.17.3
0.17.3
2023-03-07 23:30:23 +06:00
2c7fd320eb fixed 2023-03-07 22:28:56 +06:00
88ee82e1c6 add Diff#isEmpty 2023-03-07 21:50:23 +06:00
d6402c624e optimization of nonstrict comparison 2023-03-07 19:14:43 +06:00
8b9c93bc10 diffs improvement 2023-03-07 19:12:12 +06:00
4f5e261d01 start 0.17.3 2023-03-07 18:59:34 +06:00
cf455aebe6 Merge pull request #227 from InsanusMokrassar/0.17.2
0.17.2
2023-03-02 21:57:49 +06:00
1380d5f8e1 fill changelog 2023-03-02 21:38:20 +06:00
5f65260bfe Update DefaultStatesManager.kt 2023-03-02 21:35:59 +06:00
11f0dcfc01 Update DefaultStatesManager.kt 2023-03-02 21:32:58 +06:00
555b7886a4 Update gradle.properties 2023-03-02 21:28:50 +06:00
3707a6c0ce Merge pull request #226 from InsanusMokrassar/0.17.1
0.17.1
2023-02-28 19:36:08 +06:00
4c8d92b4c3 update ktor 2023-02-28 19:32:32 +06:00
8bbd33c896 now all android modules depends on jvm 2023-02-28 19:08:39 +06:00
ac33a3580f start 0.17.1 2023-02-28 19:04:04 +06:00
a64a32fbe6 Merge pull request #225 from InsanusMokrassar/0.17.0
0.17.0
2023-02-28 12:15:41 +06:00
9493e97a38 remove redundant part from defaultAndroidSettings.gradle 2023-02-27 22:46:08 +06:00
88bd770260 fill dependencies updates changelog 2023-02-27 22:45:35 +06:00
a7bd33b7bf update kotlin and ksp versions 2023-02-27 17:53:37 +06:00
73c724a2e5 small cleanup in build scripts 2023-02-27 17:03:09 +06:00
d8cf3c6726 update dependencies and at least it is buildable 2023-02-27 16:56:39 +06:00
15dee238b5 start 0.17.0 2023-02-27 15:54:37 +06:00
c70626734e Merge pull request #224 from InsanusMokrassar/0.16.13
0.16.13
2023-02-27 15:51:20 +06:00
5a765ea1bc get back publishing signing 2023-02-27 15:43:52 +06:00
8215f9d2c6 temporal solution of generating problem 2023-02-26 14:37:41 +06:00
d2e6d2ec80 generators for models has been created 2023-02-25 19:56:12 +06:00
3718181e8f start 0.16.13 2023-02-25 18:44:07 +06:00
0d825cf424 Merge pull request #223 from InsanusMokrassar/0.16.12
0.16.12
2023-02-22 10:30:45 +06:00
28a804d988 improve start launcher plugin logs 2023-02-22 10:28:25 +06:00
9e4bb9d678 add readme to generator and add several fixes in processor 2023-02-22 10:20:15 +06:00
9c40d7da3d add generator for koin definitions 2023-02-22 01:34:42 +06:00
2b76ad0aa9 improvements in selectByIds of CommonExposedRepo 2023-02-20 11:55:14 +06:00
e4b619e050 start 0.16.12 2023-02-20 11:54:26 +06:00
36c09feaf2 Merge pull request #222 from InsanusMokrassar/0.16.11
0.16.11
2023-02-20 00:49:12 +06:00
2d68321503 Update build.gradle 2023-02-20 00:19:27 +06:00
85455ab21c improvements in language codes 2023-02-19 19:34:42 +06:00
18d63eb980 start 0.16.11 2023-02-19 19:20:03 +06:00
2e429e9704 Merge pull request #221 from InsanusMokrassar/0.16.10
0.16.10
2023-02-17 13:54:09 +06:00
f4af28059b improvements in ReadKeyValueFromCRUDRepo 2023-02-17 13:45:22 +06:00
c1476bd075 add ReadCRUDFromKeyValueRepo 2023-02-15 20:16:54 +06:00
16c720fddd start 0.16.10 2023-02-15 20:14:55 +06:00
8b4b4a5eca Merge pull request #220 from InsanusMokrassar/0.16.9
0.16.9
2023-02-13 16:09:55 +06:00
32e6e5b7e2 Update CHANGELOG.md 2023-02-13 16:09:44 +06:00
a9f7fd8e32 new extension HttpResponse.bodyOrNullOnNoContent 2023-02-10 15:17:08 +06:00
95be1a26f2 improvements in HttpResponse.bodyOrNull 2023-02-10 15:14:03 +06:00
ef9b31aee0 start 0.16.9 2023-02-05 18:17:20 +06:00
df3c01ff0a Merge pull request #219 from InsanusMokrassar/0.16.8
0.16.8
2023-02-04 01:46:43 +06:00
4704c5a33d Update CHANGELOG.md 2023-02-04 01:39:49 +06:00
225c06550a fixes in FileKeyValueRepo 2023-02-03 10:23:52 +06:00
f0987614c6 fixes in FileKeyValueRepo 2023-02-03 10:07:24 +06:00
269c2876f3 actualize all make it possible to disable clear on actualization 2023-02-02 20:07:31 +06:00
168d6acf7c actualizeAll now is overridable in auto recache repos 2023-02-02 20:05:25 +06:00
a5f718e257 add meppers for kv<->kvs repos 2023-02-02 12:29:00 +06:00
4f68459582 start add kv to kvs transformations 2023-02-02 10:15:37 +06:00
442db122cf remove redundant id getter in keyvalues autocached repo 2023-02-01 23:02:06 +06:00
580d757be2 fixes in download file and upgrade ktor version 2023-02-01 22:28:17 +06:00
47b0f6d2d8 fixes 2023-02-01 13:03:51 +06:00
3f6f6ebc2b realize invalidate method in fallback cache repos 2023-01-31 10:09:55 +06:00
2645ea29d6 Update ActionWrapper.kt 2023-01-31 00:11:45 +06:00
79f2041565 Update AutoRecacheReadCRUDRepo.kt 2023-01-30 23:59:48 +06:00
4a7567f288 Update AutoRecacheWriteCRUDRepo.kt 2023-01-30 23:59:19 +06:00
8a890ed6ed Update AutoRecacheReadKeyValueRepo.kt 2023-01-30 23:58:43 +06:00
3d90df6897 Update AutoRecacheWriteKeyValueRepo.kt 2023-01-30 23:58:11 +06:00
681c13144a Update AutoRecacheReadKeyValuesRepo.kt 2023-01-30 23:57:40 +06:00
b64f2e6d32 Update AutoRecacheWriteKeyValuesRepo.kt 2023-01-30 23:56:57 +06:00
428eabb1bd Create FallbackCacheRepo.kt 2023-01-30 23:54:32 +06:00
2162e83bce fixes in actualizeAll 2023-01-29 22:51:39 +06:00
6142022283 improve actualizeAll to lazily clear repo 2023-01-29 22:46:32 +06:00
e6d9c8250f add all autorecaches repos 2023-01-29 22:18:22 +06:00
46178e723b fixes 2023-01-29 20:54:32 +06:00
605f55acd2 fixes in Write* cache repos 2023-01-29 20:35:10 +06:00
0f8b69aa60 start 0.16.8 2023-01-29 20:35:10 +06:00
551d8ec480 Merge pull request #218 from InsanusMokrassar/0.16.7
0.16.7
2023-01-29 13:20:16 +06:00
fc48446ec4 temporarily remove fallback repo 2023-01-29 13:15:56 +06:00
3644b83ac6 fill changelog 2023-01-29 13:08:41 +06:00
cd73791b6f add docs to the MutableState.asState 2023-01-29 12:46:27 +06:00
03de71df2e add docs to as compose state 2023-01-29 12:43:51 +06:00
83d5d3faf4 improve flow as state functionality 2023-01-29 12:28:22 +06:00
0c8bec4c89 add extension actualizeAll 2023-01-27 15:15:27 +06:00
7fc93817c1 start to add fallback repos 2023-01-27 14:45:31 +06:00
d0a00031a1 Update CHANGELOG.md 2023-01-26 22:54:17 +06:00
6ebc5aa0c2 potential fix of state value change in asMutableComposeState 2023-01-23 15:21:47 +06:00
8a6b4bb49e add alsoIfTrue/alsoIfFalse/letIfTrue/letIfFalse 2023-01-22 23:01:06 +06:00
20799b9a3e add repeat on failure with callback 2023-01-22 22:43:26 +06:00
ec3afc615c add serializable diff 2023-01-20 13:29:45 +06:00
da692ccfc3 add ifTrue/ifFalse 2023-01-19 20:21:35 +06:00
53b89f3a18 start 0.16.7 2023-01-19 20:19:21 +06:00
58cded28d3 Merge pull request #217 from InsanusMokrassar/0.16.6
0.16.6
2023-01-18 22:28:44 +06:00
592c5f3732 Column extensions eqOrIsNull and neqOrIsNotNull 2023-01-14 20:17:57 +06:00
f44a78a5f5 small improvement of openBaseWebSocketFlow 2023-01-14 15:20:52 +06:00
e0bdd5dfdc improvements in microutils 2023-01-14 13:22:07 +06:00
99c0f06b72 repos update 2023-01-14 13:15:59 +06:00
66fc6df3d7 Improvements in 'StartLauncherPlugin#start' methods 2023-01-08 13:52:53 +06:00
a36425a905 start 0.16.6 2023-01-08 13:51:34 +06:00
d920fee6d4 Merge pull request #216 from InsanusMokrassar/0.16.5
0.16.5
2023-01-04 20:21:15 +06:00
23590be5de Update CHANGELOG.md 2023-01-04 20:20:25 +06:00
94acc3c93b Update libs.versions.toml 2023-01-04 08:56:46 +06:00
5616326a3b start 0.16.5 2023-01-04 08:50:19 +06:00
7601860c5c Merge pull request #214 from InsanusMokrassar/0.16.4
0.16.4
2022-12-27 19:03:35 +06:00
8b43d785cc update changelog 2022-12-27 18:51:01 +06:00
b62d3a0b7d launchInCurrentThread 2022-12-27 18:50:41 +06:00
fad73c7213 start 0.16.4 2022-12-27 18:49:04 +06:00
2403c7c2b0 Merge pull request #213 from InsanusMokrassar/0.16.3
0.16.3
2022-12-25 13:24:37 +06:00
fa090bf920 Update StartLauncherPlugin.kt 2022-12-25 10:29:16 +06:00
a83ee86340 improvements in startup launcher 2022-12-25 10:26:44 +06:00
204955bcce add template for startup and readme 2022-12-19 17:51:05 +06:00
ee56e9543a start 0.16.3 2022-12-17 17:27:15 +06:00
96fdff6ffd Merge pull request #212 from InsanusMokrassar/0.16.2
0.16.2
2022-12-16 13:59:47 +06:00
133 changed files with 3693 additions and 263 deletions

View File

@@ -1,5 +1,148 @@
# Changelog
## 0.17.4
* `Serialization`:
* `Mapper`:
* Module inited
* `Versions`:
* `Compose`: `1.3.1-rc02` -> `1.3.1`
## 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`:
* `Exposed`:
* `CommonExposedRepo.selectByIds` uses `foldRight` by default instead of raw foreach
* `Koin`:
* `Generator`:
* Module has been created
## 0.16.11
* `LanguageCodes`:
* In android and JVM targets now available `toJavaLocale` and from Java `Locale` conversations from/to
`IetfLanguageCode`
## 0.16.10
* `Repos`:
* `Cache`:
* New transformer type: `ReadCRUDFromKeyValueRepo`
* New transformer type: `ReadKeyValueFromCRUDRepo`
* `Pagination`:
* New `paginate` extensions with `reversed` support for `List`/`Set`
## 0.16.9
* `Versions`:
* `Koin`: `3.2.2` -> `3.3.2`
* `AppCompat`: `1.5.1` -> `1.6.0`
* `Ktor`:
* `Client`
* `HttpResponse.bodyOrNull` now retrieve callback to check if body should be received or null
* New extension `HttpResponse.bodyOrNullOnNoContent`
## 0.16.8
* `Versions`:
* `Ktor`: `2.2.2` -> `2.2.3`
* `Ktor`:
* `Client`
* Fixes in `HttpClient.uniUpload`
* `Server`
* Fixes in `PartData.FileItem.download`
* `Repos`:
* `Cache`:
* New type of caches: `FallbackCacheRepo`
* Fixes in `Write*` variants of cached repos
* New type `ActionWrapper`
* New `AutoRecache*` classes for all types of repos as `FallbackCacheRepo`s
* `Common`:
* New transformations for key-value and key-values vice-verse
* Fixes in `FileReadKeyValueRepo`
## 0.16.7
* `Common`:
* New extensions `ifTrue`/`ifFalse`/`alsoIfTrue`/`alsoIfFalse`/`letIfTrue`/`letIfFalse`
* `Diff` now is serializable
* Add `IndexedValue` serializer
* `repeatOnFailure` extending: now you may pass any lambda to check if continue to try/do something
* `Compose`:
* New extension `MutableState.asState`
* `Coroutines`:
* `Compose`:
* All the `Flow` conversations to compose `State`/`MutableState`/`SnapshotStateList`/`List` got several new
parameters
* `Flow.toMutableState` now is deprecated in favor to `asMutableComposeState`
* `Repos`:
* `Cache`:
* New type `FullCacheRepo`
* New type `CommonCacheRepo`
* `CacheRepo` got `invalidate` method. It will fully reload `FullCacheRepo` and just clear `CommonCacheRepo`
* New extensions `KVCache.actualizeAll`
## 0.16.6
* `Startup`:
* `Launcher`:
* Improvements in `StartLauncherPlugin#start` methods
* Add opportunity to pass second argument on `JVM` platform as log level
* `Repos`:
* `Ktor`:
* `Client`:
* All clients repos got opportunity to customize their flows
* `Exposed`:
* Extensions `eqOrIsNull` and `neqOrIsNotNull` for `Column`
## 0.16.5
* `Versions`:
* `Ktor`: `2.2.1` -> `2.2.2`
## 0.16.4
* `Coroutines`:
* Create `launchInCurrentThread`
## 0.16.3
* `Startup`:
* `Launcher`:
* All starting API have been moved into `StartLauncherPlugin` and do not require serialize/deserialize cycle for now
## 0.16.2
* `Versions`:

View File

@@ -9,6 +9,7 @@ buildscript {
dependencies {
classpath libs.buildscript.kt.gradle
classpath libs.buildscript.kt.serialization
classpath libs.buildscript.kt.ksp
classpath libs.buildscript.jb.dokka
classpath libs.buildscript.gh.release
classpath libs.buildscript.android.gradle

View File

@@ -18,6 +18,7 @@ kotlin {
api project(":micro_utils.coroutines")
api libs.android.fragment
}
dependsOn jvmMain
}
}
}

View File

@@ -0,0 +1,10 @@
package dev.inmo.micro_utils.common.compose
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
/**
* Converts current [MutableState] to immutable [State] using [derivedStateOf]
*/
fun <T> MutableState<T>.asState(): State<T> = derivedStateOf { this.value }

View File

@@ -2,6 +2,8 @@
package dev.inmo.micro_utils.common
import kotlinx.serialization.Serializable
private inline fun <T> getObject(
additional: MutableList<T>,
iterator: Iterator<T>
@@ -24,14 +26,19 @@ private inline fun <T> getObject(
*
* @see calculateDiff
*/
@Serializable
data class Diff<T> internal constructor(
val removed: List<IndexedValue<T>>,
val removed: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>,
/**
* Old-New values pairs
*/
val replaced: List<Pair<IndexedValue<T>, IndexedValue<T>>>,
val added: List<IndexedValue<T>>
)
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>?>>,
@@ -40,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 {
@@ -101,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
@@ -129,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>>
@@ -140,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)
/**
@@ -184,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)
}
}

View File

@@ -0,0 +1,43 @@
package dev.inmo.micro_utils.common
inline fun <T> Boolean.letIfTrue(block: () -> T): T? {
return if (this) {
block()
} else {
null
}
}
inline fun <T> Boolean.letIfFalse(block: () -> T): T? {
return if (this) {
null
} else {
block()
}
}
inline fun Boolean.alsoIfTrue(block: () -> Unit): Boolean {
letIfTrue(block)
return this
}
inline fun Boolean.alsoIfFalse(block: () -> Unit): Boolean {
letIfFalse(block)
return this
}
inline fun <T> Boolean.ifTrue(block: () -> T): T? {
return if (this) {
block()
} else {
null
}
}
inline fun <T> Boolean.ifFalse(block: () -> T): T? {
return if (this) {
null
} else {
block()
}
}

View File

@@ -0,0 +1,30 @@
package dev.inmo.micro_utils.common
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer
import kotlinx.serialization.builtins.PairSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
class IndexedValueSerializer<T>(private val subSerializer: KSerializer<T>) : KSerializer<IndexedValue<T>> {
private val originalSerializer = PairSerializer(Int.serializer(), subSerializer)
override val descriptor: SerialDescriptor
get() = originalSerializer.descriptor
override fun deserialize(decoder: Decoder): IndexedValue<T> {
val pair = originalSerializer.deserialize(decoder)
return IndexedValue(
pair.first,
pair.second
)
}
override fun serialize(encoder: Encoder, value: IndexedValue<T>) {
originalSerializer.serialize(
encoder,
Pair(value.index, value.value)
)
}
}

View File

@@ -1,5 +1,27 @@
package dev.inmo.micro_utils.common
/**
* Executes the given [action] until getting of successful result specified number of [times].
*
* A zero-based index of current iteration is passed as a parameter to [action].
*/
inline fun <R> repeatOnFailure(
onFailure: (Throwable) -> Boolean,
action: () -> R
): Result<R> {
do {
runCatching {
action()
}.onFailure {
if (!onFailure(it)) {
return Result.failure(it)
}
}.onSuccess {
return Result.success(it)
}
} while (true)
}
/**
* Executes the given [action] until getting of successful result specified number of [times].
*
@@ -10,12 +32,23 @@ inline fun <R> repeatOnFailure(
onEachFailure: (Throwable) -> Unit = {},
action: (Int) -> R
): Optional<R> {
repeat(times) {
runCatching {
action(it)
}.onFailure(onEachFailure).onSuccess {
return Optional.presented(it)
var i = 0
val result = repeatOnFailure(
{
onEachFailure(it)
if (i < times) {
i++
true
} else {
false
}
}
) {
action(i)
}
return if (result.isSuccess) {
Optional.presented(result.getOrThrow())
} else {
Optional.absent()
}
return Optional.absent()
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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();

View File

@@ -22,6 +22,7 @@ kotlin {
dependencies {
api libs.kt.coroutines.android
}
dependsOn(jvmMain)
}
}
}

View File

@@ -3,24 +3,58 @@ package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import dev.inmo.micro_utils.common.applyDiff
import dev.inmo.micro_utils.coroutines.ExceptionHandler
import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandlerWithNull
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
/**
* Each value of [this] [Flow] will trigger [applyDiff] to the result [SnapshotStateList]
*
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [SnapshotStateList]
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
*/
@Suppress("NOTHING_TO_INLINE")
inline fun <reified T> Flow<List<T>>.asMutableComposeListState(
scope: CoroutineScope
scope: CoroutineScope,
useContextOnChange: CoroutineContext? = Dispatchers.Main,
noinline onException: ExceptionHandler<List<T>?> = defaultSafelyWithoutExceptionHandlerWithNull,
): SnapshotStateList<T> {
val state = mutableStateListOf<T>()
subscribeSafelyWithoutExceptions(scope) {
val changeBlock: suspend (List<T>) -> Unit = useContextOnChange ?.let {
{
withContext(useContextOnChange) {
state.applyDiff(it)
}
}
} ?: {
state.applyDiff(it)
}
subscribeSafelyWithoutExceptions(scope, onException, changeBlock)
return state
}
/**
* In fact, it is just classcast of [asMutableComposeListState] to [List]
*
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [List]
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
*
* @return Changing in time [List] which follow [Flow] values
*/
@Suppress("NOTHING_TO_INLINE")
inline fun <reified T> Flow<List<T>>.asComposeList(
scope: CoroutineScope
): List<T> = asMutableComposeListState(scope)
scope: CoroutineScope,
useContextOnChange: CoroutineContext? = Dispatchers.Main,
noinline onException: ExceptionHandler<List<T>?> = defaultSafelyWithoutExceptionHandlerWithNull,
): List<T> = asMutableComposeListState(scope, useContextOnChange, onException)

View File

@@ -1,35 +1,94 @@
package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.*
import dev.inmo.micro_utils.common.compose.asState
import dev.inmo.micro_utils.coroutines.ExceptionHandler
import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandlerWithNull
import dev.inmo.micro_utils.coroutines.doInUI
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
/**
* Will map [this] [Flow] as [MutableState]. Returned [MutableState] WILL NOT change source [Flow]
*
* @param initial First value which will be passed to the result [MutableState]
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [MutableState]
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
*/
fun <T> Flow<T>.asMutableComposeState(
initial: T,
scope: CoroutineScope
scope: CoroutineScope,
useContextOnChange: CoroutineContext? = Dispatchers.Main,
onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
): MutableState<T> {
val state = mutableStateOf(initial)
subscribeSafelyWithoutExceptions(scope) { state.value = it }
val changeBlock: suspend (T) -> Unit = useContextOnChange ?.let {
{
withContext(useContextOnChange) {
state.value = it
}
}
} ?: {
state.value = it
}
subscribeSafelyWithoutExceptions(scope, onException, block = changeBlock)
return state
}
/**
* Will map [this] [StateFlow] as [MutableState]. Returned [MutableState] WILL NOT change source [StateFlow].
* This conversation will pass its [StateFlow.value] as the first value
*
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [MutableState]
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
*/
@Suppress("NOTHING_TO_INLINE")
inline fun <T> StateFlow<T>.asMutableComposeState(
scope: CoroutineScope
): MutableState<T> = asMutableComposeState(value, scope)
scope: CoroutineScope,
useContextOnChange: CoroutineContext? = Dispatchers.Main,
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
): MutableState<T> = asMutableComposeState(value, scope, useContextOnChange, onException)
/**
* Will create [MutableState] using [asMutableComposeState] and use [asState] to convert it as immutable state
*
* @param initial First value which will be passed to the result [State]
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [State]
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
*/
fun <T> Flow<T>.asComposeState(
initial: T,
scope: CoroutineScope
scope: CoroutineScope,
useContextOnChange: CoroutineContext? = Dispatchers.Main,
onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
): State<T> {
val state = asMutableComposeState(initial, scope)
return derivedStateOf { state.value }
val state = asMutableComposeState(initial, scope, useContextOnChange, onException)
return state.asState()
}
/**
* Will map [this] [StateFlow] as [State]. This conversation will pass its [StateFlow.value] as the first value
*
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [State]
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
*/
@Suppress("NOTHING_TO_INLINE")
inline fun <T> StateFlow<T>.asComposeState(
scope: CoroutineScope
): State<T> = asComposeState(value, scope)
scope: CoroutineScope,
useContextOnChange: CoroutineContext? = Dispatchers.Main,
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
): State<T> = asComposeState(value, scope, useContextOnChange, onException)

View File

@@ -7,17 +7,15 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@Deprecated("Duplicated functionality", ReplaceWith("asMutableComposeState(initial, scope)", "dev.inmo.micro_utils.coroutines.compose.asMutableComposeState"))
fun <T> Flow<T>.toMutableState(
initial: T,
scope: CoroutineScope
): MutableState<T> {
val state = mutableStateOf(initial)
subscribeSafelyWithoutExceptions(scope) { state.value = it }
return state
}
): MutableState<T> = asMutableComposeState(initial, scope)
@Deprecated("Duplicated functionality", ReplaceWith("asMutableComposeState(scope)", "dev.inmo.micro_utils.coroutines.compose.asMutableComposeState"))
@Suppress("NOTHING_TO_INLINE")
inline fun <T> StateFlow<T>.toMutableState(
scope: CoroutineScope
): MutableState<T> = toMutableState(value, scope)
): MutableState<T> = asMutableComposeState(scope)

View File

@@ -0,0 +1,9 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
fun <T> launchInCurrentThread(block: suspend CoroutineScope.() -> T): T {
val scope = CoroutineScope(Dispatchers.Unconfined)
return scope.launchSynchronously(block)
}

View File

@@ -6,7 +6,7 @@ fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T
var result: Result<T>? = null
val objectToSynchronize = Object()
synchronized(objectToSynchronize) {
launch {
launch(start = CoroutineStart.UNDISPATCHED) {
result = safelyWithResult(block)
}.invokeOnCompletion {
synchronized(objectToSynchronize) {

View File

@@ -0,0 +1,47 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlin.test.Test
import kotlin.test.assertEquals
class LaunchInCurrentThreadTests {
@Test
fun simpleTestThatLaunchInCurrentThreadWorks() {
val expectedResult = 10
val result = launchInCurrentThread {
expectedResult
}
assertEquals(expectedResult, result)
}
@Test
fun simpleTestThatSeveralLaunchInCurrentThreadWorks() {
val testData = 0 until 100
testData.forEach {
val result = launchInCurrentThread {
it
}
assertEquals(it, result)
}
}
@Test
fun simpleTestThatLaunchInCurrentThreadWillCorrectlyHandleSuspensionsWorks() {
val testData = 0 until 100
suspend fun test(data: Any): Any {
return withContext(Dispatchers.Default) {
delay(1)
data
}
}
testData.forEach {
val result = launchInCurrentThread {
test(it)
}
assertEquals(it, result)
}
}
}

View File

@@ -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()
}
}

View File

@@ -29,6 +29,6 @@ allprojects {
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle"
publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle"
publishMavenPath = "${rootProject.projectDir.absolutePath}/maven.publish.gradle"
publishJvmOnlyPath = "${rootProject.projectDir.absolutePath}/jvm.publish.gradle"
}
}

View File

@@ -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()

View File

@@ -14,5 +14,5 @@ crypto_js_version=4.1.1
# Project data
group=dev.inmo
version=0.16.2
android_code_version=170
version=0.17.4
android_code_version=186

View File

@@ -1,37 +1,40 @@
[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"
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.1"
ktor = "2.2.4"
gh-release = "2.4.1"
koin = "3.2.2"
koin = "3.3.2"
android-gradle = "7.3.0"
dexcount = "3.1.0"
ksp = "1.8.10-1.0.9"
kotlin-poet = "1.12.0"
android-gradle = "7.3.1"
dexcount = "4.0.0"
android-coreKtx = "1.9.0"
android-recyclerView = "1.2.1"
android-appCompat = "1.5.1"
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"
android-props-buildTools = "33.0.0"
android-props-buildTools = "33.0.1"
[libraries]
@@ -83,9 +86,16 @@ android-test-junit = { module = "androidx.test.ext:junit", version.ref = "androi
kt-test-js = { module = "org.jetbrains.kotlin:kotlin-test-js", version.ref = "kt" }
kt-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kt" }
# ksp dependencies
kotlin-poet = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlin-poet" }
ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
# Buildscript
buildscript-kt-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kt" }
buildscript-kt-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kt" }
buildscript-kt-ksp = { module = "com.google.devtools.ksp:symbol-processing-gradle-plugin", version.ref = "ksp" }
buildscript-jb-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "jb-dokka" }
buildscript-gh-release = { module = "com.github.breadmoirai:github-release", version.ref = "gh-release" }
buildscript-android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle" }

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

118
jvm.publish.gradle Normal file
View File

@@ -0,0 +1,118 @@
apply plugin: 'maven-publish'
task javadocJar(type: Jar) {
from javadoc
classifier = 'javadoc'
}
task sourcesJar(type: Jar) {
from sourceSets.main.allSource
classifier = 'sources'
}
publishing {
publications {
maven(MavenPublication) {
from components.java
artifact javadocJar
artifact sourcesJar
pom {
resolveStrategy = Closure.DELEGATE_FIRST
description = "It is set of projects with micro tools for avoiding of routines coding"
name = "${project.name}"
url = "https://github.com/InsanusMokrassar/MicroUtils/"
scm {
developerConnection = "scm:git:[fetch=]https://github.com/InsanusMokrassar/MicroUtils.git[push=]https://github.com/InsanusMokrassar/MicroUtils.git"
url = "https://github.com/InsanusMokrassar/MicroUtils.git"
}
developers {
developer {
id = "InsanusMokrassar"
name = "Aleksei Ovsiannikov"
email = "ovsyannikov.alexey95@gmail.com"
}
developer {
id = "000Sanya"
name = "Syrov Aleksandr"
email = "000sanya.000sanya@gmail.com"
}
}
licenses {
license {
name = "Apache Software License 2.0"
url = "https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"
}
}
}
repositories {
if ((project.hasProperty('GITHUBPACKAGES_USER') || System.getenv('GITHUBPACKAGES_USER') != null) && (project.hasProperty('GITHUBPACKAGES_PASSWORD') || System.getenv('GITHUBPACKAGES_PASSWORD') != null)) {
maven {
name = "GithubPackages"
url = uri("https://maven.pkg.github.com/InsanusMokrassar/MicroUtils")
credentials {
username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER')
password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD')
}
}
}
if (project.hasProperty('GITEA_TOKEN') || System.getenv('GITEA_TOKEN') != null) {
maven {
name = "Gitea"
url = uri("https://git.inmo.dev/api/packages/InsanusMokrassar/maven")
credentials(HttpHeaderCredentials) {
name = "Authorization"
value = project.hasProperty('GITEA_TOKEN') ? project.property('GITEA_TOKEN') : System.getenv('GITEA_TOKEN')
}
authentication {
header(HttpHeaderAuthentication)
}
}
}
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
maven {
name = "sonatype"
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
credentials {
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
}
}
}
}
}
}
}
if (project.hasProperty("signing.gnupg.keyName")) {
apply plugin: 'signing'
signing {
useGpgCmd()
sign publishing.publications
}
task signAll {
tasks.withType(Sign).forEach {
dependsOn(it)
}
}
}

1
jvm.publish.kpsb Normal file
View File

@@ -0,0 +1 @@
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"Gitea","url":"https://git.inmo.dev/api/packages/InsanusMokrassar/maven","credsType":{"type":"dev.inmo.kmppscriptbuilder.core.models.MavenPublishingRepository.CredentialsType.HttpHeaderCredentials","headerName":"Authorization","headerValueProperty":"GITEA_TOKEN"}},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}},"type":"JVM"}

100
koin/generator/README.md Normal file
View File

@@ -0,0 +1,100 @@
# Koin generator
It is Kotlin Symbol Processing generator for `Koin` module in `MicroUtils`.
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
Let's imagine you want to have shortcuts in koin, to get something easily:
```kotlin
val koin: Koin// some initialization
val someUrl = koin.serverUrl
```
So, in that case you may mark containing file with next annotation (in the beginning of file):
```kotlin
@file:GenerateKoinDefinition("serverUrl", String::class, nullable = false)
```
If file is called like `Sample.kt`, will be generated file `GeneratedDefinitionsSample.kt` with next content:
```kotlin
public val Scope.serverUrl: String
get() = get(named("serverUrl"))
public val Koin.serverUrl: String
get() = get(named("serverUrl"))
public fun Module.serverUrlSingle(createdAtStart: Boolean = false,
definition: Definition<String>): KoinDefinition<String> =
single(named("serverUrl"), createdAtStart = createdAtStart, definition = definition)
public fun Module.serverUrlFactory(definition: Definition<String>):
KoinDefinition<String> = factory(named("serverUrl"), definition = definition)
```
Besides, you may use the generics:
```kotlin
@file:GenerateKoinDefinition("sampleInfo", Sample::class, G1::class, G2::class, nullable = false)
```
Will generate:
```kotlin
public val Scope.sampleInfo: Sample<G1, G2>
get() = get(named("sampleInfo"))
public val Koin.sampleInfo: Sample<G1, G2>
get() = get(named("sampleInfo"))
public fun Module.sampleInfoSingle(createdAtStart: Boolean = false,
definition: Definition<Sample<G1, G2>>): KoinDefinition<Sample<G1, G2>> =
single(named("sampleInfo"), createdAtStart = createdAtStart, definition = definition)
public fun Module.sampleInfoFactory(definition: Definition<Sample<G1, G2>>):
KoinDefinition<Sample<G1, G2>> = factory(named("sampleInfo"), definition = definition)
```
In case you wish not to generate single:
```kotlin
@file:GenerateKoinDefinition("sampleInfo", Sample::class, G1::class, G2::class, nullable = false, generateSingle = false)
```
And you will take next code:
```kotlin
public val Scope.sampleInfo: Sample<G1, G2>
get() = get(named("sampleInfo"))
public val Koin.sampleInfo: Sample<G1, G2>
get() = get(named("sampleInfo"))
public fun Module.sampleInfoFactory(definition: Definition<Sample<G1, G2>>):
KoinDefinition<Sample<G1, G2>> = factory(named("sampleInfo"), definition = definition)
```
## 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.koin.generator:$microutils_version`:
```groovy
dependencies {
add("kspCommonMainMetadata", "dev.inmo:micro_utils.koin.generator:$microutils_version") // will work in commonMain of your multiplatform module
add("kspJvm", "dev.inmo:micro_utils.koin.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
}
```

View File

@@ -0,0 +1,15 @@
plugins {
id "org.jetbrains.kotlin.jvm"
}
apply from: "$publishJvmOnlyPath"
repositories {
mavenCentral()
}
dependencies {
api project(":micro_utils.koin")
api libs.kotlin.poet
api libs.ksp
}

View File

@@ -0,0 +1,178 @@
package dev.inmo.micro_utils.koin.generator
import com.google.devtools.ksp.KSTypeNotPresentException
import com.google.devtools.ksp.KSTypesNotPresentException
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getAnnotationsByType
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.KSFile
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.asTypeName
import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.toTypeName
import com.squareup.kotlinpoet.ksp.writeTo
import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition
import org.koin.core.Koin
import org.koin.core.module.Module
import org.koin.core.scope.Scope
import java.io.File
import kotlin.reflect.KClass
class Processor(
private val codeGenerator: CodeGenerator
) : SymbolProcessor {
private val definitionClassName = ClassName("org.koin.core.definition", "Definition")
private val koinDefinitionClassName = ClassName("org.koin.core.definition", "KoinDefinition")
@OptIn(KspExperimental::class)
override fun process(resolver: Resolver): List<KSAnnotated> {
resolver.getSymbolsWithAnnotation(
GenerateKoinDefinition::class.qualifiedName!!
).filterIsInstance<KSFile>().forEach { ksFile ->
FileSpec.builder(
ksFile.packageName.asString(),
"GeneratedDefinitions${ksFile.fileName.removeSuffix(".kt")}"
).apply {
addFileComment(
"""
THIS CODE HAVE BEEN GENERATED AUTOMATICALLY
TO REGENERATE IT JUST DELETE FILE
ORIGINAL FILE: ${ksFile.fileName}
""".trimIndent()
)
ksFile.getAnnotationsByType(GenerateKoinDefinition::class).forEach {
val type = runCatching {
it.type.asTypeName()
}.getOrElse { e ->
if (e is KSTypeNotPresentException) {
e.ksType.toClassName()
} else {
throw e
}
}
val targetType = runCatching {
type.parameterizedBy(*(it.typeArgs.takeIf { it.isNotEmpty() } ?.map { it.asTypeName() } ?.toTypedArray() ?: return@runCatching type))
}.getOrElse { e ->
when (e) {
is KSTypeNotPresentException -> e.ksType.toClassName()
}
if (e is KSTypesNotPresentException) {
type.parameterizedBy(*e.ksTypes.map { it.toTypeName() }.toTypedArray())
} else {
throw e
}
}.copy(
nullable = it.nullable
)
fun addGetterProperty(
receiver: KClass<*>
) {
addProperty(
PropertySpec.builder(
it.name,
targetType,
).apply {
addKdoc(
"""
@return Definition by key "${it.name}"
""".trimIndent()
)
getter(
FunSpec.getterBuilder().apply {
addCode(
"return " + (if (it.nullable) {
"getOrNull"
} else {
"get"
}) + "(named(\"${it.name}\"))"
)
}.build()
)
receiver(receiver)
}.build()
)
}
addGetterProperty(Scope::class)
addGetterProperty(Koin::class)
if (it.generateSingle) {
addFunction(
FunSpec.builder("${it.name}Single").apply {
addKdoc(
"""
Will register [definition] with [org.koin.core.module.Module.single] and key "${it.name}"
""".trimIndent()
)
receiver(Module::class)
addParameter(
ParameterSpec.builder(
"createdAtStart",
Boolean::class
).apply {
defaultValue("false")
}.build()
)
addParameter(
ParameterSpec.builder(
"definition",
definitionClassName.parameterizedBy(targetType.copy(nullable = false))
).build()
)
returns(koinDefinitionClassName.parameterizedBy(targetType.copy(nullable = false)))
addCode(
"return single(named(\"${it.name}\"), createdAtStart = createdAtStart, definition = definition)"
)
}.build()
)
}
if (it.generateFactory) {
addFunction(
FunSpec.builder("${it.name}Factory").apply {
addKdoc(
"""
Will register [definition] with [org.koin.core.module.Module.factory] and key "${it.name}"
""".trimIndent()
)
receiver(Module::class)
addParameter(
ParameterSpec.builder(
"definition",
definitionClassName.parameterizedBy(targetType.copy(nullable = false))
).build()
)
returns(koinDefinitionClassName.parameterizedBy(targetType.copy(nullable = false)))
addCode(
"return factory(named(\"${it.name}\"), definition = definition)"
)
}.build()
)
}
addImport("org.koin.core.qualifier", "named")
}
}.build().let {
File(
File(ksFile.filePath).parent,
"GeneratedDefinitions${ksFile.fileName}"
).takeIf { !it.exists() } ?.apply {
parentFile.mkdirs()
writer().use { writer ->
it.writeTo(writer)
}
}
}
}
return emptyList()
}
}

View File

@@ -0,0 +1,11 @@
package dev.inmo.micro_utils.koin.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
)
}

View File

@@ -0,0 +1 @@
dev.inmo.micro_utils.koin.generator.Provider

View 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.koin")
}
}
}
}
dependencies {
add("kspCommonMainMetadata", project(":micro_utils.koin.generator"))
}
ksp {
}

View File

@@ -0,0 +1,38 @@
// THIS CODE HAVE BEEN GENERATED AUTOMATICALLY
// TO REGENERATE IT JUST DELETE FILE
// ORIGINAL FILE: Test.kt
package dev.inmo.micro_utils.koin.generator.test
import kotlin.Boolean
import kotlin.String
import org.koin.core.Koin
import org.koin.core.definition.Definition
import org.koin.core.definition.KoinDefinition
import org.koin.core.module.Module
import org.koin.core.qualifier.named
import org.koin.core.scope.Scope
/**
* @return Definition by key "sampleInfo"
*/
public val Scope.sampleInfo: Test<String>
get() = get(named("sampleInfo"))
/**
* @return Definition by key "sampleInfo"
*/
public val Koin.sampleInfo: Test<String>
get() = get(named("sampleInfo"))
/**
* Will register [definition] with [org.koin.core.module.Module.single] and key "sampleInfo"
*/
public fun Module.sampleInfoSingle(createdAtStart: Boolean = false,
definition: Definition<Test<String>>): KoinDefinition<Test<String>> =
single(named("sampleInfo"), createdAtStart = createdAtStart, definition = definition)
/**
* Will register [definition] with [org.koin.core.module.Module.factory] and key "sampleInfo"
*/
public fun Module.sampleInfoFactory(definition: Definition<Test<String>>):
KoinDefinition<Test<String>> = factory(named("sampleInfo"), definition = definition)

View File

@@ -0,0 +1,13 @@
@file:GenerateKoinDefinition("sampleInfo", Test::class, String::class, nullable = false)
package dev.inmo.micro_utils.koin.generator.test
import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition
import org.koin.core.Koin
class Test<T>(
koin: Koin
) {
init {
koin.sampleInfo
}
}

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.koin.generator.test"/>

View File

@@ -0,0 +1,28 @@
package dev.inmo.micro_utils.koin.annotations
import kotlin.reflect.KClass
/**
* Use this annotation to mark files near to which generator should place generated extensions for koin [org.koin.core.scope.Scope]
* and [org.koin.core.Koin]
*
* @param name Name for definitions. This name will be available as extension for [org.koin.core.scope.Scope] and [org.koin.core.Koin]
* @param type Type of extensions. It is base star-typed class
* @param typeArgs Generic types for [type]. For example, if [type] == `Something::class` and [typeArgs] == `G1::class,
* G2::class`, the result type will be `Something<G1, G2>`
* @param nullable In case when true, extension will not throw error when definition has not been registered in koin
* @param generateSingle Generate definition factory with [org.koin.core.module.Module.single]. You will be able to use
* the extension [org.koin.core.module.Module].[name]Single(createdAtStart/* default false */) { /* your definition */ }
* @param generateFactory Generate definition factory with [org.koin.core.module.Module.factory]. You will be able to use
* the extension [org.koin.core.module.Module].[name]Factory { /* your definition */ }
*/
@Target(AnnotationTarget.FILE)
@Repeatable
annotation class GenerateKoinDefinition(
val name: String,
val type: KClass<*>,
vararg val typeArgs: KClass<*>,
val nullable: Boolean = true,
val generateSingle: Boolean = true,
val generateFactory: Boolean = true
)

View File

@@ -1,6 +1,7 @@
package dev.inmo.micro_utils.koin
import org.koin.core.definition.Definition
import org.koin.core.definition.KoinDefinition
import org.koin.core.instance.InstanceFactory
import org.koin.core.module.Module
import org.koin.core.qualifier.Qualifier
@@ -13,7 +14,7 @@ inline fun <reified T : Any> Module.factoryWithBinds(
qualifier: Qualifier? = null,
bindFilter: (KClass<*>) -> Boolean = { true },
noinline definition: Definition<T>
): Pair<Module, InstanceFactory<*>> {
): KoinDefinition<*> {
return factory(qualifier, definition) binds (T::class.allSuperclasses.filter(bindFilter).toTypedArray())
}
@@ -21,7 +22,7 @@ inline fun <reified T : Any> Module.factoryWithBinds(
qualifier: String,
bindFilter: (KClass<*>) -> Boolean = { true },
noinline definition: Definition<T>
): Pair<Module, InstanceFactory<*>> {
): KoinDefinition<*> {
return factory(qualifier, definition) binds (T::class.allSuperclasses.filter(bindFilter).toTypedArray())
}

View File

@@ -1,6 +1,7 @@
package dev.inmo.micro_utils.koin
import org.koin.core.definition.Definition
import org.koin.core.definition.KoinDefinition
import org.koin.core.instance.InstanceFactory
import org.koin.core.module.Module
import kotlin.reflect.KClass
@@ -8,6 +9,6 @@ import kotlin.reflect.KClass
inline fun <reified T : Any> Module.factoryWithRandomQualifierAndBinds(
bindFilter: (KClass<*>) -> Boolean = { true },
noinline definition: Definition<T>
): Pair<Module, InstanceFactory<*>> {
): KoinDefinition<*> {
return factoryWithBinds(RandomQualifier(), bindFilter, definition)
}

View File

@@ -1,6 +1,7 @@
package dev.inmo.micro_utils.koin
import org.koin.core.definition.Definition
import org.koin.core.definition.KoinDefinition
import org.koin.core.instance.InstanceFactory
import org.koin.core.module.Module
import org.koin.core.qualifier.Qualifier
@@ -14,7 +15,7 @@ inline fun <reified T : Any> Module.singleWithBinds(
createdAtStart: Boolean = false,
bindFilter: (KClass<*>) -> Boolean = { true },
noinline definition: Definition<T>
): Pair<Module, InstanceFactory<*>> {
): KoinDefinition<*> {
return single(qualifier, createdAtStart, definition) binds (T::class.allSuperclasses.filter(bindFilter).toTypedArray())
}
@@ -24,7 +25,7 @@ inline fun <reified T : Any> Module.singleWithBinds(
createdAtStart: Boolean = false,
bindFilter: (KClass<*>) -> Boolean = { true },
noinline definition: Definition<T>
): Pair<Module, InstanceFactory<*>> {
): KoinDefinition<*> {
return single(qualifier, createdAtStart, definition) binds (T::class.allSuperclasses.filter(bindFilter).toTypedArray())
}

View File

@@ -1,6 +1,7 @@
package dev.inmo.micro_utils.koin
import org.koin.core.definition.Definition
import org.koin.core.definition.KoinDefinition
import org.koin.core.instance.InstanceFactory
import org.koin.core.module.Module
import kotlin.reflect.KClass
@@ -9,6 +10,6 @@ inline fun <reified T : Any> Module.singleWithRandomQualifierAndBinds(
createdAtStart: Boolean = false,
bindFilter: (KClass<*>) -> Boolean = { true },
noinline definition: Definition<T>
): Pair<Module, InstanceFactory<*>> {
): KoinDefinition<*> {
return singleWithBinds(RandomQualifier(), createdAtStart, bindFilter, definition)
}

View File

@@ -15,7 +15,6 @@ kotlin {
api libs.ktor.client
}
}
androidMain {
dependsOn jvmMain
}

View File

@@ -4,6 +4,10 @@ import io.ktor.client.call.body
import io.ktor.client.statement.HttpResponse
import io.ktor.http.HttpStatusCode
suspend inline fun <reified T : Any> HttpResponse.bodyOrNull() = takeIf {
status == HttpStatusCode.OK
} ?.body<T>()
suspend inline fun <reified T : Any> HttpResponse.bodyOrNull(
statusFilter: (HttpResponse) -> Boolean = { it.status == HttpStatusCode.OK }
) = takeIf(statusFilter) ?.body<T>()
suspend inline fun <reified T : Any> HttpResponse.bodyOrNullOnNoContent() = bodyOrNull<T> {
it.status != HttpStatusCode.NoContent
}

View File

@@ -19,7 +19,7 @@ import kotlinx.coroutines.isActive
* connection. Must return true in case if must be reconnected. By default always reconnecting
*/
@Warning("This feature is internal and should not be used directly. It is can be changed without any notification and warranty on compile-time or other guaranties")
inline fun <reified T : Any> openBaseWebSocketFlow(
inline fun <T : Any> openBaseWebSocketFlow(
noinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
noinline webSocketSessionRequest: suspend SendChannel<T>.() -> Unit
): Flow<T> {
@@ -57,7 +57,7 @@ inline fun <reified T : Any> HttpClient.openWebSocketFlow(
): Flow<T> {
pluginOrNull(WebSockets) ?: error("Plugin $WebSockets must be installed for using createStandardWebsocketFlow")
return openBaseWebSocketFlow<T>(checkReconnection) {
return openBaseWebSocketFlow(checkReconnection) {
val block: suspend DefaultClientWebSocketSession.() -> Unit = {
while (isActive) {
send(receiveDeserialized<T>())

View File

@@ -17,8 +17,11 @@ import io.ktor.http.HttpStatusCode
import io.ktor.http.Parameters
import io.ktor.http.content.PartData
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.StringFormat
import kotlinx.serialization.encodeToString
import kotlinx.serialization.serializer
import java.io.File
/**
@@ -29,6 +32,7 @@ import java.io.File
* in case you wish to pass other source of multipart binary data than regular file
* @suppress
*/
@OptIn(InternalSerializationApi::class)
actual suspend fun <T> HttpClient.uniUpload(
url: String,
data: Map<String, Any>,
@@ -40,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,
@@ -60,7 +65,7 @@ actual suspend fun <T> HttpClient.uniUpload(
)
else -> append(
k,
stringFormat.encodeToString(v)
stringFormat.encodeToString(v::class.serializer() as SerializationStrategy<in Any>, v)
)
}
}
@@ -85,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)
}

View File

@@ -17,5 +17,8 @@ kotlin {
api libs.ktor.io
}
}
androidMain {
dependsOn jvmMain
}
}
}

View File

@@ -10,8 +10,8 @@ import java.io.File
fun PartData.FileItem.download(target: File) {
provider().use { input ->
target.outputStream().use {
input.copyTo(it.asOutput())
target.outputStream().asOutput().use {
input.copyTo(it)
}
}
}

View File

@@ -5,3 +5,11 @@ plugins {
}
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
androidMain {
dependsOn jvmMain
}
}
}

View File

@@ -0,0 +1,8 @@
package dev.inmo.micro_utils.language_codes
import java.util.Locale
fun IetfLanguageCode.toJavaLocale(): Locale = Locale.forLanguageTag(code)
fun IetfLanguageCode?.toJavaLocaleOrDefault(): Locale = this ?.toJavaLocale() ?: Locale.getDefault()
fun Locale.toIetfLanguageCode(): IetfLanguageCode = IetfLanguageCode(toLanguageTag())

View File

@@ -50,6 +50,8 @@ kotlin {
implementation libs.android.espresso
}
}
androidMain.dependsOn jvmMain
}
}

View File

@@ -61,6 +61,8 @@ kotlin {
implementation libs.android.espresso
}
}
androidMain.dependsOn jvmMain
}
}

View File

@@ -0,0 +1,29 @@
package dev.inmo.micro_utils.pagination.utils
fun <T> Iterable<T>.optionallyReverse(reverse: Boolean): Iterable<T> = when (this) {
is List<T> -> optionallyReverse(reverse)
is Set<T> -> optionallyReverse(reverse)
else -> if (reverse) {
reversed()
} else {
this
}
}
fun <T> List<T>.optionallyReverse(reverse: Boolean): List<T> = if (reverse) {
reversed()
} else {
this
}
fun <T> Set<T>.optionallyReverse(reverse: Boolean): Set<T> = if (reverse) {
reversed().toSet()
} else {
this
}
inline fun <reified T> Array<T>.optionallyReverse(reverse: Boolean) = if (reverse) {
Array(size) {
get(lastIndex - it)
}
} else {
this
}

View File

@@ -32,6 +32,24 @@ fun <T> List<T>.paginate(with: Pagination): PaginationResult<T> {
)
}
fun <T> List<T>.paginate(with: Pagination, reversed: Boolean): PaginationResult<T> {
val actualPagination = with.optionallyReverse(
size,
reversed
)
val firstIndex = maxOf(actualPagination.firstIndex, 0)
val lastIndex = minOf(actualPagination.lastIndexExclusive, size)
if (firstIndex > lastIndex) {
return emptyPaginationResult()
}
return subList(firstIndex, lastIndex).optionallyReverse(reversed).createPaginationResult(
with,
size.toLong()
)
}
fun <T> Set<T>.paginate(with: Pagination): PaginationResult<T> {
return this.drop(with.firstIndex).take(with.size).createPaginationResult(
with,
@@ -39,30 +57,20 @@ fun <T> Set<T>.paginate(with: Pagination): PaginationResult<T> {
)
}
fun <T> Iterable<T>.optionallyReverse(reverse: Boolean): Iterable<T> = when (this) {
is List<T> -> optionallyReverse(reverse)
is Set<T> -> optionallyReverse(reverse)
else -> if (reverse) {
reversed()
} else {
this
}
}
fun <T> List<T>.optionallyReverse(reverse: Boolean): List<T> = if (reverse) {
reversed()
} else {
this
}
fun <T> Set<T>.optionallyReverse(reverse: Boolean): Set<T> = if (reverse) {
reversed().toSet()
} else {
this
}
fun <T> Set<T>.paginate(with: Pagination, reversed: Boolean): PaginationResult<T> {
val actualPagination = with.optionallyReverse(
size,
reversed
)
inline fun <reified T> Array<T>.optionallyReverse(reverse: Boolean) = if (reverse) {
Array(size) {
get(lastIndex - it)
val firstIndex = maxOf(actualPagination.firstIndex, 0)
val lastIndex = minOf(actualPagination.lastIndexExclusive, size)
if (firstIndex > lastIndex) {
return emptyPaginationResult()
}
} else {
this
return this.drop(firstIndex).take(lastIndex - firstIndex).optionallyReverse(reversed).createPaginationResult(
with,
size.toLong()
)
}

View File

@@ -10,12 +10,14 @@ open class ReadCRUDCacheRepo<ObjectType, IdType>(
protected open val parentRepo: ReadCRUDRepo<ObjectType, IdType>,
protected open val kvCache: KVCache<IdType, ObjectType>,
protected open val idGetter: (ObjectType) -> IdType
) : ReadCRUDRepo<ObjectType, IdType> by parentRepo, CacheRepo {
) : ReadCRUDRepo<ObjectType, IdType> by parentRepo, CommonCacheRepo {
override suspend fun getById(id: IdType): ObjectType? = kvCache.get(id) ?: (parentRepo.getById(id) ?.also {
kvCache.set(id, it)
})
override suspend fun contains(id: IdType): Boolean = kvCache.contains(id) || parentRepo.contains(id)
override suspend fun invalidate() = kvCache.clear()
}
fun <ObjectType, IdType> ReadCRUDRepo<ObjectType, IdType>.cached(
@@ -28,7 +30,7 @@ open class WriteCRUDCacheRepo<ObjectType, IdType, InputValueType>(
protected open val kvCache: KVCache<IdType, ObjectType>,
protected open val scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
protected open val idGetter: (ObjectType) -> IdType
) : WriteCRUDRepo<ObjectType, IdType, InputValueType>, CacheRepo {
) : WriteCRUDRepo<ObjectType, IdType, InputValueType>, CommonCacheRepo {
override val newObjectsFlow: Flow<ObjectType> by parentRepo::newObjectsFlow
override val updatedObjectsFlow: Flow<ObjectType> by parentRepo::updatedObjectsFlow
override val deletedObjectsIdsFlow: Flow<IdType> by parentRepo::deletedObjectsIdsFlow
@@ -72,6 +74,8 @@ open class WriteCRUDCacheRepo<ObjectType, IdType, InputValueType>(
return created
}
override suspend fun invalidate() = kvCache.clear()
}
fun <ObjectType, IdType, InputType> WriteCRUDRepo<ObjectType, IdType, InputType>.caching(

View File

@@ -1,3 +1,5 @@
package dev.inmo.micro_utils.repos.cache
interface CacheRepo
interface CacheRepo {
suspend fun invalidate()
}

View File

@@ -0,0 +1,7 @@
package dev.inmo.micro_utils.repos.cache
/**
* Any inheritor of this should work with next logic: try to take data from some [dev.inmo.micro_utils.repos.cache.cache.KVCache] and,
* if not exists, take from origin and save to the cache for future reuse
*/
interface CommonCacheRepo : CacheRepo

View File

@@ -0,0 +1,7 @@
package dev.inmo.micro_utils.repos.cache
/**
* Any inheritor of this should work with next logic: try to take data from their original repo, if successful - save data to internal
* [dev.inmo.micro_utils.repos.cache.cache.FullKVCache] or try to take data from that internal cache
*/
interface FallbackCacheRepo : CacheRepo

View File

@@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.*
open class ReadKeyValueCacheRepo<Key,Value>(
protected open val parentRepo: ReadKeyValueRepo<Key, Value>,
protected open val kvCache: KVCache<Key, Value>,
) : ReadKeyValueRepo<Key,Value> by parentRepo, CacheRepo {
) : ReadKeyValueRepo<Key,Value> by parentRepo, CommonCacheRepo {
override suspend fun get(k: Key): Value? = kvCache.get(k) ?: parentRepo.get(k) ?.also { kvCache.set(k, it) }
override suspend fun contains(key: Key): Boolean = kvCache.contains(key) || parentRepo.contains(key)
@@ -23,6 +23,8 @@ open class ReadKeyValueCacheRepo<Key,Value>(
)
}
}
override suspend fun invalidate() = kvCache.clear()
}
fun <Key, Value> ReadKeyValueRepo<Key, Value>.cached(
@@ -33,9 +35,11 @@ open class KeyValueCacheRepo<Key,Value>(
parentRepo: KeyValueRepo<Key, Value>,
kvCache: KVCache<Key, Value>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) : ReadKeyValueCacheRepo<Key,Value>(parentRepo, kvCache), KeyValueRepo<Key,Value>, WriteKeyValueRepo<Key, Value> by parentRepo, CacheRepo {
) : ReadKeyValueCacheRepo<Key,Value>(parentRepo, kvCache), KeyValueRepo<Key,Value>, WriteKeyValueRepo<Key, Value> by parentRepo, CommonCacheRepo {
protected val onNewJob = parentRepo.onNewValue.onEach { kvCache.set(it.first, it.second) }.launchIn(scope)
protected val onRemoveJob = parentRepo.onValueRemoved.onEach { kvCache.unset(it) }.launchIn(scope)
override suspend fun invalidate() = kvCache.clear()
}
fun <Key, Value> KeyValueRepo<Key, Value>.cached(

View File

@@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.*
open class ReadKeyValuesCacheRepo<Key,Value>(
protected open val parentRepo: ReadKeyValuesRepo<Key, Value>,
protected open val kvCache: KVCache<Key, List<Value>>
) : ReadKeyValuesRepo<Key,Value> by parentRepo, CacheRepo {
) : ReadKeyValuesRepo<Key,Value> by parentRepo, CommonCacheRepo {
override suspend fun get(k: Key, pagination: Pagination, reversed: Boolean): PaginationResult<Value> {
return getAll(k, reversed).paginate(
pagination
@@ -30,6 +30,8 @@ open class ReadKeyValuesCacheRepo<Key,Value>(
}
})
override suspend fun contains(k: Key): Boolean = kvCache.contains(k) || parentRepo.contains(k)
override suspend fun invalidate() = kvCache.clear()
}
fun <Key, Value> ReadKeyValuesRepo<Key, Value>.cached(
@@ -40,7 +42,7 @@ open class KeyValuesCacheRepo<Key,Value>(
parentRepo: KeyValuesRepo<Key, Value>,
kvCache: KVCache<Key, List<Value>>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) : ReadKeyValuesCacheRepo<Key,Value>(parentRepo, kvCache), KeyValuesRepo<Key,Value>, WriteKeyValuesRepo<Key,Value> by parentRepo, CacheRepo {
) : ReadKeyValuesCacheRepo<Key,Value>(parentRepo, kvCache), KeyValuesRepo<Key,Value>, WriteKeyValuesRepo<Key,Value> by parentRepo, CommonCacheRepo {
protected val onNewJob = parentRepo.onNewValue.onEach { (k, v) ->
kvCache.set(
k,
@@ -56,6 +58,8 @@ open class KeyValuesCacheRepo<Key,Value>(
protected val onDataClearedJob = parentRepo.onDataCleared.onEach {
kvCache.unset(it)
}.launchIn(scope)
override suspend fun invalidate() = kvCache.clear()
}
fun <Key, Value> KeyValuesRepo<Key, Value>.cached(

View File

@@ -0,0 +1,37 @@
package dev.inmo.micro_utils.repos.cache.fallback
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import kotlinx.coroutines.withTimeout
/**
* Realizations should [wrap] the work with some conditions like retries on exceptions, calling timeout, etc.
*
* @see Timeouted
* @see Direct
*/
interface ActionWrapper {
/**
* Should execute [block] to take the result [T], but may return failure in case when something went wrong.
* This method should never throw any [Exception]
*/
suspend fun <T> wrap(block: suspend () -> T): Result<T>
/**
* This type of [ActionWrapper]s will use [withTimeout]([timeoutMillis]) and if original call
* will not return anything in that timeout just return [Result] with failure
*/
class Timeouted(private val timeoutMillis: Long) : ActionWrapper {
override suspend fun <T> wrap(block: suspend () -> T): Result<T> = runCatchingSafely {
withTimeout(timeoutMillis) {
block()
}
}
}
/**
* It is passthrough variant of [ActionWrapper] which will just call incoming block with wrapping into [runCatchingSafely]
*/
object Direct : ActionWrapper {
override suspend fun <T> wrap(block: suspend () -> T): Result<T> = runCatchingSafely { block() }
}
}

View File

@@ -0,0 +1,36 @@
package dev.inmo.micro_utils.repos.cache.fallback.crud
import dev.inmo.micro_utils.repos.CRUDRepo
import dev.inmo.micro_utils.repos.WriteCRUDRepo
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper
import kotlinx.coroutines.CoroutineScope
import kotlin.time.Duration.Companion.seconds
open class AutoRecacheCRUDRepo<RegisteredObject, Id, InputObject>(
originalRepo: CRUDRepo<RegisteredObject, Id, InputObject>,
scope: CoroutineScope,
kvCache: FullKVCache<Id, RegisteredObject> = FullKVCache(),
recacheDelay: Long = 60.seconds.inWholeMilliseconds,
actionWrapper: ActionWrapper = ActionWrapper.Direct,
idGetter: (RegisteredObject) -> Id
) : AutoRecacheReadCRUDRepo<RegisteredObject, Id>(
originalRepo,
scope,
kvCache,
recacheDelay,
actionWrapper,
idGetter
),
WriteCRUDRepo<RegisteredObject, Id, InputObject> by AutoRecacheWriteCRUDRepo(originalRepo, scope, kvCache, idGetter),
CRUDRepo<RegisteredObject, Id, InputObject> {
constructor(
originalRepo: CRUDRepo<RegisteredObject, Id, InputObject>,
scope: CoroutineScope,
originalCallTimeoutMillis: Long,
kvCache: FullKVCache<Id, RegisteredObject> = FullKVCache(),
recacheDelay: Long = 60.seconds.inWholeMilliseconds,
idGetter: (RegisteredObject) -> Id
) : this(originalRepo, scope, kvCache, recacheDelay, ActionWrapper.Timeouted(originalCallTimeoutMillis), idGetter)
}

View File

@@ -0,0 +1,86 @@
package dev.inmo.micro_utils.repos.cache.fallback.crud
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.repos.ReadCRUDRepo
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper
import dev.inmo.micro_utils.repos.cache.util.actualizeAll
import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo
import dev.inmo.micro_utils.repos.set
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
open class AutoRecacheReadCRUDRepo<RegisteredObject, Id>(
protected open val originalRepo: ReadCRUDRepo<RegisteredObject, Id>,
protected val scope: CoroutineScope,
protected val kvCache: FullKVCache<Id, RegisteredObject> = FullKVCache(),
protected val recacheDelay: Long = 60.seconds.inWholeMilliseconds,
protected val actionWrapper: ActionWrapper = ActionWrapper.Direct,
protected val idGetter: (RegisteredObject) -> Id
) : ReadCRUDRepo<RegisteredObject, Id>, FallbackCacheRepo {
val autoUpdateJob = scope.launch {
while (isActive) {
actualizeAll()
delay(recacheDelay)
}
}
constructor(
originalRepo: ReadCRUDRepo<RegisteredObject, Id>,
scope: CoroutineScope,
originalCallTimeoutMillis: Long,
kvCache: FullKVCache<Id, RegisteredObject> = FullKVCache(),
recacheDelay: Long = 60.seconds.inWholeMilliseconds,
idGetter: (RegisteredObject) -> Id
) : this(originalRepo, scope, kvCache, recacheDelay, ActionWrapper.Timeouted(originalCallTimeoutMillis), idGetter)
protected open suspend fun actualizeAll(): Result<Unit> {
return runCatchingSafely {
kvCache.actualizeAll(originalRepo)
}
}
override suspend fun contains(id: Id): Boolean = actionWrapper.wrap {
originalRepo.contains(id)
}.getOrElse {
kvCache.contains(id)
}
override suspend fun count(): Long = actionWrapper.wrap {
originalRepo.count()
}.getOrElse {
kvCache.count()
}
override suspend fun getByPagination(
pagination: Pagination
): PaginationResult<RegisteredObject> = actionWrapper.wrap {
originalRepo.getByPagination(pagination)
}.getOrNull() ?.also {
it.results.forEach {
kvCache.set(idGetter(it), it)
}
} ?: kvCache.values(pagination)
override suspend fun getIdsByPagination(
pagination: Pagination
): PaginationResult<Id> = actionWrapper.wrap {
originalRepo.getIdsByPagination(pagination)
}.getOrElse { kvCache.keys(pagination) }
override suspend fun getById(id: Id): RegisteredObject? = actionWrapper.wrap {
originalRepo.getById(id)
}.getOrNull() ?.also {
kvCache.set(idGetter(it), it)
} ?: kvCache.get(id)
override suspend fun invalidate() {
actualizeAll()
}
}

View File

@@ -0,0 +1,65 @@
package dev.inmo.micro_utils.repos.cache.fallback.crud
import dev.inmo.micro_utils.coroutines.plus
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.repos.UpdatedValuePair
import dev.inmo.micro_utils.repos.WriteCRUDRepo
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo
import dev.inmo.micro_utils.repos.set
import dev.inmo.micro_utils.repos.unset
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
open class AutoRecacheWriteCRUDRepo<RegisteredObject, Id, InputObject>(
protected val originalRepo: WriteCRUDRepo<RegisteredObject, Id, InputObject>,
protected val scope: CoroutineScope,
protected val kvCache: FullKVCache<Id, RegisteredObject> = FullKVCache(),
protected val idGetter: (RegisteredObject) -> Id
) : WriteCRUDRepo<RegisteredObject, Id, InputObject>, FallbackCacheRepo {
override val deletedObjectsIdsFlow: Flow<Id>
get() = (originalRepo.deletedObjectsIdsFlow + kvCache.onValueRemoved).distinctUntilChanged()
override val newObjectsFlow: Flow<RegisteredObject>
get() = (originalRepo.newObjectsFlow + kvCache.onNewValue.map { it.second }).distinctUntilChanged()
override val updatedObjectsFlow: Flow<RegisteredObject>
get() = originalRepo.updatedObjectsFlow
private val onRemovingUpdatesListeningJob = originalRepo.deletedObjectsIdsFlow.subscribeSafelyWithoutExceptions(scope) {
kvCache.unset(it)
}
private val onNewAndUpdatedObjectsListeningJob = merge(
originalRepo.newObjectsFlow,
originalRepo.updatedObjectsFlow,
).subscribeSafelyWithoutExceptions(scope) {
kvCache.set(idGetter(it), it)
}
override suspend fun update(
values: List<UpdatedValuePair<Id, InputObject>>
): List<RegisteredObject> = originalRepo.update(values).onEach {
kvCache.set(idGetter(it), it)
}
override suspend fun update(
id: Id,
value: InputObject
): RegisteredObject? = originalRepo.update(id, value) ?.also {
kvCache.set(idGetter(it), it)
}
override suspend fun deleteById(ids: List<Id>) = originalRepo.deleteById(ids).also {
kvCache.unset(ids)
}
override suspend fun create(values: List<InputObject>): List<RegisteredObject> = originalRepo.create(values).onEach {
kvCache.set(idGetter(it), it)
}
override suspend fun invalidate() {
kvCache.clear()
}
}

View File

@@ -0,0 +1,42 @@
package dev.inmo.micro_utils.repos.cache.fallback.keyvalue
import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.WriteKeyValueRepo
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper
import kotlinx.coroutines.CoroutineScope
import kotlin.time.Duration.Companion.seconds
open class AutoRecacheKeyValueRepo<Id, RegisteredObject>(
override val originalRepo: KeyValueRepo<Id, RegisteredObject>,
scope: CoroutineScope,
kvCache: FullKVCache<Id, RegisteredObject> = FullKVCache(),
recacheDelay: Long = 60.seconds.inWholeMilliseconds,
actionWrapper: ActionWrapper = ActionWrapper.Direct,
idGetter: (RegisteredObject) -> Id
) : AutoRecacheReadKeyValueRepo<Id, RegisteredObject> (
originalRepo,
scope,
kvCache,
recacheDelay,
actionWrapper,
idGetter
),
WriteKeyValueRepo<Id, RegisteredObject> by AutoRecacheWriteKeyValueRepo(originalRepo, scope, kvCache),
KeyValueRepo<Id, RegisteredObject> {
constructor(
originalRepo: KeyValueRepo<Id, RegisteredObject>,
scope: CoroutineScope,
originalCallTimeoutMillis: Long,
kvCache: FullKVCache<Id, RegisteredObject> = FullKVCache(),
recacheDelay: Long = 60.seconds.inWholeMilliseconds,
idGetter: (RegisteredObject) -> Id
) : this(originalRepo, scope, kvCache, recacheDelay, ActionWrapper.Timeouted(originalCallTimeoutMillis), idGetter)
override suspend fun unsetWithValues(toUnset: List<RegisteredObject>) = originalRepo.unsetWithValues(
toUnset
).also {
kvCache.unsetWithValues(toUnset)
}
}

View File

@@ -0,0 +1,96 @@
package dev.inmo.micro_utils.repos.cache.fallback.keyvalue
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.repos.ReadKeyValueRepo
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper
import dev.inmo.micro_utils.repos.cache.util.actualizeAll
import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo
import dev.inmo.micro_utils.repos.set
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
open class AutoRecacheReadKeyValueRepo<Id, RegisteredObject>(
protected open val originalRepo: ReadKeyValueRepo<Id, RegisteredObject>,
protected val scope: CoroutineScope,
protected val kvCache: FullKVCache<Id, RegisteredObject> = FullKVCache(),
protected val recacheDelay: Long = 60.seconds.inWholeMilliseconds,
protected val actionWrapper: ActionWrapper = ActionWrapper.Direct,
protected val idGetter: (RegisteredObject) -> Id
) : ReadKeyValueRepo<Id, RegisteredObject>, FallbackCacheRepo {
val autoUpdateJob = scope.launch {
while (isActive) {
actualizeAll()
delay(recacheDelay)
}
}
constructor(
originalRepo: ReadKeyValueRepo<Id, RegisteredObject>,
scope: CoroutineScope,
originalCallTimeoutMillis: Long,
kvCache: FullKVCache<Id, RegisteredObject> = FullKVCache(),
recacheDelay: Long = 60.seconds.inWholeMilliseconds,
idGetter: (RegisteredObject) -> Id
) : this(originalRepo, scope, kvCache, recacheDelay, ActionWrapper.Timeouted(originalCallTimeoutMillis), idGetter)
protected open suspend fun actualizeAll(): Result<Unit> {
return runCatchingSafely {
kvCache.actualizeAll(originalRepo)
}
}
override suspend fun contains(key: Id): Boolean = actionWrapper.wrap {
originalRepo.contains(key)
}.getOrElse {
kvCache.contains(key)
}
override suspend fun count(): Long = actionWrapper.wrap {
originalRepo.count()
}.getOrElse {
kvCache.count()
}
override suspend fun get(k: Id): RegisteredObject? = actionWrapper.wrap {
originalRepo.get(k)
}.getOrNull() ?.also {
kvCache.set(k, it)
} ?: kvCache.get(k)
override suspend fun values(
pagination: Pagination,
reversed: Boolean
): PaginationResult<RegisteredObject> = actionWrapper.wrap {
originalRepo.values(pagination, reversed)
}.getOrNull() ?.also {
it.results.forEach {
kvCache.set(idGetter(it), it)
}
} ?: kvCache.values(pagination, reversed)
override suspend fun keys(
pagination: Pagination,
reversed: Boolean
): PaginationResult<Id> = actionWrapper.wrap {
originalRepo.keys(pagination, reversed)
}.getOrElse { kvCache.keys(pagination, reversed) }
override suspend fun keys(
v: RegisteredObject,
pagination: Pagination,
reversed: Boolean
): PaginationResult<Id> = actionWrapper.wrap {
originalRepo.keys(v, pagination, reversed)
}.getOrElse { kvCache.keys(v, pagination, reversed) }
override suspend fun invalidate() {
actualizeAll()
}
}

View File

@@ -0,0 +1,54 @@
package dev.inmo.micro_utils.repos.cache.fallback.keyvalue
import dev.inmo.micro_utils.coroutines.plus
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.repos.WriteKeyValueRepo
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo
import dev.inmo.micro_utils.repos.set
import dev.inmo.micro_utils.repos.unset
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
open class AutoRecacheWriteKeyValueRepo<Id, RegisteredObject>(
protected val originalRepo: WriteKeyValueRepo<Id, RegisteredObject>,
protected val scope: CoroutineScope,
protected val kvCache: FullKVCache<Id, RegisteredObject> = FullKVCache()
) : WriteKeyValueRepo<Id, RegisteredObject>, FallbackCacheRepo {
override val onValueRemoved: Flow<Id>
get() = (originalRepo.onValueRemoved + kvCache.onValueRemoved).distinctUntilChanged()
override val onNewValue: Flow<Pair<Id, RegisteredObject>>
get() = (originalRepo.onNewValue + kvCache.onNewValue).distinctUntilChanged()
private val onRemovingUpdatesListeningJob = originalRepo.onValueRemoved.subscribeSafelyWithoutExceptions(scope) {
kvCache.unset(it)
}
private val onNewAndUpdatedObjectsListeningJob = originalRepo.onNewValue.subscribeSafelyWithoutExceptions(scope) {
kvCache.set(it.first, it.second)
}
override suspend fun unsetWithValues(toUnset: List<RegisteredObject>) = originalRepo.unsetWithValues(
toUnset
).also {
kvCache.unsetWithValues(toUnset)
}
override suspend fun unset(toUnset: List<Id>) = originalRepo.unset(
toUnset
).also {
kvCache.unset(toUnset)
}
override suspend fun set(toSet: Map<Id, RegisteredObject>) = originalRepo.set(
toSet
).also {
kvCache.set(toSet)
}
override suspend fun invalidate() {
kvCache.clear()
}
}

View File

@@ -0,0 +1,37 @@
package dev.inmo.micro_utils.repos.cache.fallback.keyvalues
import dev.inmo.micro_utils.repos.KeyValuesRepo
import dev.inmo.micro_utils.repos.WriteKeyValuesRepo
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper
import kotlinx.coroutines.CoroutineScope
import kotlin.time.Duration.Companion.seconds
open class AutoRecacheKeyValuesRepo<Id, RegisteredObject>(
override val originalRepo: KeyValuesRepo<Id, RegisteredObject>,
scope: CoroutineScope,
kvCache: FullKVCache<Id, List<RegisteredObject>> = FullKVCache(),
recacheDelay: Long = 60.seconds.inWholeMilliseconds,
actionWrapper: ActionWrapper = ActionWrapper.Direct
) : AutoRecacheReadKeyValuesRepo<Id, RegisteredObject> (
originalRepo,
scope,
kvCache,
recacheDelay,
actionWrapper
),
WriteKeyValuesRepo<Id, RegisteredObject> by AutoRecacheWriteKeyValuesRepo(originalRepo, scope, kvCache),
KeyValuesRepo<Id, RegisteredObject> {
constructor(
originalRepo: KeyValuesRepo<Id, RegisteredObject>,
scope: CoroutineScope,
originalCallTimeoutMillis: Long,
kvCache: FullKVCache<Id, List<RegisteredObject>> = FullKVCache(),
recacheDelay: Long = 60.seconds.inWholeMilliseconds
) : this(originalRepo, scope, kvCache, recacheDelay, ActionWrapper.Timeouted(originalCallTimeoutMillis))
override suspend fun clearWithValue(v: RegisteredObject) {
super.clearWithValue(v)
}
}

View File

@@ -0,0 +1,145 @@
package dev.inmo.micro_utils.repos.cache.fallback.keyvalues
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.pagination.changeResultsUnchecked
import dev.inmo.micro_utils.pagination.createPaginationResult
import dev.inmo.micro_utils.pagination.emptyPaginationResult
import dev.inmo.micro_utils.pagination.firstIndex
import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging
import dev.inmo.micro_utils.pagination.utils.optionallyReverse
import dev.inmo.micro_utils.pagination.utils.paginate
import dev.inmo.micro_utils.repos.ReadKeyValuesRepo
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper
import dev.inmo.micro_utils.repos.cache.util.actualizeAll
import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo
import dev.inmo.micro_utils.repos.set
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
open class AutoRecacheReadKeyValuesRepo<Id, RegisteredObject>(
protected open val originalRepo: ReadKeyValuesRepo<Id, RegisteredObject>,
protected val scope: CoroutineScope,
protected val kvCache: FullKVCache<Id, List<RegisteredObject>> = FullKVCache(),
protected val recacheDelay: Long = 60.seconds.inWholeMilliseconds,
protected val actionWrapper: ActionWrapper = ActionWrapper.Direct
) : ReadKeyValuesRepo<Id, RegisteredObject>, FallbackCacheRepo {
val autoUpdateJob = scope.launch {
while (isActive) {
actualizeAll()
delay(recacheDelay)
}
}
constructor(
originalRepo: ReadKeyValuesRepo<Id, RegisteredObject>,
scope: CoroutineScope,
originalCallTimeoutMillis: Long,
kvCache: FullKVCache<Id, List<RegisteredObject>> = FullKVCache(),
recacheDelay: Long = 60.seconds.inWholeMilliseconds
) : this(originalRepo, scope, kvCache, recacheDelay, ActionWrapper.Timeouted(originalCallTimeoutMillis))
protected open suspend fun actualizeAll(): Result<Unit> {
return runCatchingSafely {
kvCache.actualizeAll(originalRepo)
}
}
override suspend fun contains(k: Id): Boolean = actionWrapper.wrap {
originalRepo.contains(k)
}.getOrElse {
kvCache.contains(k)
}
override suspend fun count(): Long = actionWrapper.wrap {
originalRepo.count()
}.getOrElse {
kvCache.count()
}
override suspend fun keys(
v: RegisteredObject,
pagination: Pagination,
reversed: Boolean
): PaginationResult<Id> = actionWrapper.wrap {
originalRepo.keys(v, pagination, reversed)
}.getOrElse {
val results = mutableListOf<Id>()
val toSkip = pagination.firstIndex
var count = 0
doForAllWithNextPaging {
kvCache.keys(pagination, reversed).also {
it.results.forEach {
if (kvCache.get(it) ?.contains(v) == true) {
count++
if (count < toSkip || results.size >= pagination.size) {
return@forEach
} else {
results.add(it)
}
}
}
}
}
return@getOrElse results.createPaginationResult(
pagination,
count.toLong()
)
}
override suspend fun keys(
pagination: Pagination,
reversed: Boolean
): PaginationResult<Id> = actionWrapper.wrap {
originalRepo.keys(pagination, reversed)
}.getOrElse { kvCache.keys(pagination, reversed) }
override suspend fun get(
k: Id,
pagination: Pagination,
reversed: Boolean
): PaginationResult<RegisteredObject> = actionWrapper.wrap {
originalRepo.get(k, pagination, reversed)
}.getOrNull() ?.also {
it.results.forEach {
kvCache.set(k, ((kvCache.get(k) ?: return@also) + it).distinct())
}
} ?: kvCache.get(k) ?.run {
paginate(pagination.optionallyReverse(size, reversed)).let {
if (reversed) {
it.changeResultsUnchecked(
it.results.reversed()
)
} else {
it
}
}
} ?: emptyPaginationResult()
override suspend fun count(k: Id): Long = actionWrapper.wrap {
originalRepo.count(k)
}.getOrElse {
kvCache.get(k) ?.size ?.toLong() ?: 0L
}
override suspend fun contains(k: Id, v: RegisteredObject): Boolean {
return (actionWrapper.wrap {
originalRepo.contains(k, v)
}.getOrNull() ?.also {
kvCache.set(k, ((kvCache.get(k) ?: return@also) + v).distinct())
}) ?: (kvCache.get(k) ?.contains(v) == true)
}
override suspend fun invalidate() {
actualizeAll()
}
}

View File

@@ -0,0 +1,86 @@
package dev.inmo.micro_utils.repos.cache.fallback.keyvalues
import dev.inmo.micro_utils.coroutines.plus
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.pagination.FirstPagePagination
import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging
import dev.inmo.micro_utils.repos.WriteKeyValuesRepo
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo
import dev.inmo.micro_utils.repos.set
import dev.inmo.micro_utils.repos.unset
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
open class AutoRecacheWriteKeyValuesRepo<Id, RegisteredObject>(
protected val originalRepo: WriteKeyValuesRepo<Id, RegisteredObject>,
protected val scope: CoroutineScope,
protected val kvCache: FullKVCache<Id, List<RegisteredObject>> = FullKVCache()
) : WriteKeyValuesRepo<Id, RegisteredObject>, FallbackCacheRepo {
override val onValueRemoved: Flow<Pair<Id, RegisteredObject>>
get() = originalRepo.onValueRemoved
override val onNewValue: Flow<Pair<Id, RegisteredObject>>
get() = originalRepo.onNewValue
override val onDataCleared: Flow<Id>
get() = (originalRepo.onDataCleared + kvCache.onValueRemoved).distinctUntilChanged()
private val onDataClearedListeningJob = originalRepo.onDataCleared.subscribeSafelyWithoutExceptions(scope) {
kvCache.unset(it)
}
private val onRemovingUpdatesListeningJob = originalRepo.onValueRemoved.subscribeSafelyWithoutExceptions(scope) {
kvCache.set(
it.first,
(kvCache.get(
it.first
) ?: return@subscribeSafelyWithoutExceptions) - it.second
)
}
private val onNewAndUpdatedObjectsListeningJob = originalRepo.onNewValue.subscribeSafelyWithoutExceptions(scope) {
kvCache.set(
it.first,
(kvCache.get(
it.first
) ?: return@subscribeSafelyWithoutExceptions) + it.second
)
}
override suspend fun clearWithValue(v: RegisteredObject) {
originalRepo.clearWithValue(v)
doForAllWithNextPaging(FirstPagePagination(kvCache.count().takeIf { it < Int.MAX_VALUE } ?.toInt() ?: Int.MAX_VALUE)) {
kvCache.keys(it).also {
it.results.forEach { id ->
kvCache.get(id) ?.takeIf { it.contains(v) } ?.let {
kvCache.unset(id)
}
}
}
}
}
override suspend fun clear(k: Id) {
originalRepo.clear(k)
kvCache.unset(k)
}
override suspend fun remove(toRemove: Map<Id, List<RegisteredObject>>) {
originalRepo.remove(toRemove)
toRemove.forEach { (k, v) ->
kvCache.set(k, (kvCache.get(k) ?: return@forEach) - v)
}
}
override suspend fun add(toAdd: Map<Id, List<RegisteredObject>>) {
originalRepo.add(toAdd)
toAdd.forEach { (k, v) ->
kvCache.set(k, (kvCache.get(k) ?: return@forEach) + v)
}
}
override suspend fun invalidate() {
kvCache.clear()
}
}

View File

@@ -3,11 +3,10 @@ package dev.inmo.micro_utils.repos.cache.full
import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging
import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.cache.*
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.cache.KVCache
import dev.inmo.micro_utils.repos.cache.util.actualizeAll
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -32,12 +31,7 @@ open class FullReadCRUDCacheRepo<ObjectType, IdType>(
}
protected open suspend fun actualizeAll() {
kvCache.clear()
doForAllWithNextPaging {
parentRepo.getByPagination(it).also {
kvCache.set(it.results.associateBy { idGetter(it) })
}
}
kvCache.actualizeAll(parentRepo)
}
override suspend fun getByPagination(pagination: Pagination): PaginationResult<ObjectType> = doOrTakeAndActualize(
@@ -69,6 +63,10 @@ open class FullReadCRUDCacheRepo<ObjectType, IdType>(
{ getById(id) },
{ it ?.let { set(idGetter(it), it) } }
)
override suspend fun invalidate() {
actualizeAll()
}
}
fun <ObjectType, IdType> ReadCRUDRepo<ObjectType, IdType>.cached(
@@ -92,7 +90,11 @@ open class FullCRUDCacheRepo<ObjectType, IdType, InputValueType>(
scope,
idGetter
),
CRUDRepo<ObjectType, IdType, InputValueType>
CRUDRepo<ObjectType, IdType, InputValueType> {
override suspend fun invalidate() {
actualizeAll()
}
}
fun <ObjectType, IdType, InputType> CRUDRepo<ObjectType, IdType, InputType>.cached(
kvCache: FullKVCache<IdType, ObjectType>,

View File

@@ -5,9 +5,11 @@ import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.util.actualizeAll
import dev.inmo.micro_utils.repos.pagination.getAll
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
open class FullReadKeyValueCacheRepo<Key,Value>(
@@ -68,6 +70,10 @@ open class FullReadKeyValueCacheRepo<Key,Value>(
{ parentRepo.keys(v, pagination, reversed) },
{ if (it.results.isNotEmpty()) actualizeAll() }
)
override suspend fun invalidate() {
actualizeAll()
}
}
fun <Key, Value> ReadKeyValueRepo<Key, Value>.cached(
@@ -75,12 +81,16 @@ fun <Key, Value> ReadKeyValueRepo<Key, Value>.cached(
) = FullReadKeyValueCacheRepo(this, kvCache)
open class FullWriteKeyValueCacheRepo<Key,Value>(
protected open val parentRepo: WriteKeyValueRepo<Key, Value>,
parentRepo: WriteKeyValueRepo<Key, Value>,
protected open val kvCache: FullKVCache<Key, Value>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) : WriteKeyValueRepo<Key, Value> by parentRepo, FullCacheRepo {
protected val onNewJob = parentRepo.onNewValue.onEach { kvCache.set(it.first, it.second) }.launchIn(scope)
protected val onRemoveJob = parentRepo.onValueRemoved.onEach { kvCache.unset(it) }.launchIn(scope)
override suspend fun invalidate() {
kvCache.clear()
}
}
fun <Key, Value> WriteKeyValueRepo<Key, Value>.caching(
@@ -89,13 +99,17 @@ fun <Key, Value> WriteKeyValueRepo<Key, Value>.caching(
) = FullWriteKeyValueCacheRepo(this, kvCache, scope)
open class FullKeyValueCacheRepo<Key,Value>(
parentRepo: KeyValueRepo<Key, Value>,
protected open val parentRepo: KeyValueRepo<Key, Value>,
kvCache: FullKVCache<Key, Value>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) : FullWriteKeyValueCacheRepo<Key,Value>(parentRepo, kvCache, scope),
KeyValueRepo<Key,Value>,
ReadKeyValueRepo<Key, Value> by FullReadKeyValueCacheRepo(parentRepo, kvCache) {
override suspend fun unsetWithValues(toUnset: List<Value>) = parentRepo.unsetWithValues(toUnset)
override suspend fun invalidate() {
kvCache.actualizeAll(parentRepo)
}
}
fun <Key, Value> KeyValueRepo<Key, Value>.cached(

View File

@@ -5,6 +5,7 @@ import dev.inmo.micro_utils.pagination.*
import dev.inmo.micro_utils.pagination.utils.*
import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.util.actualizeAll
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
@@ -33,8 +34,7 @@ open class FullReadKeyValuesCacheRepo<Key,Value>(
}
protected open suspend fun actualizeAll() {
doAllWithCurrentPaging { kvCache.keys(it).also { kvCache.unset(it.results) } }
kvCache.set(parentRepo.getAll())
kvCache.actualizeAll(parentRepo)
}
override suspend fun get(k: Key, pagination: Pagination, reversed: Boolean): PaginationResult<Value> {
@@ -102,6 +102,9 @@ open class FullReadKeyValuesCacheRepo<Key,Value>(
{ if (it.results.isNotEmpty()) actualizeAll() }
)
override suspend fun invalidate() {
actualizeAll()
}
}
fun <Key, Value> ReadKeyValuesRepo<Key, Value>.cached(
@@ -109,7 +112,7 @@ fun <Key, Value> ReadKeyValuesRepo<Key, Value>.cached(
) = FullReadKeyValuesCacheRepo(this, kvCache)
open class FullWriteKeyValuesCacheRepo<Key,Value>(
protected open val parentRepo: WriteKeyValuesRepo<Key, Value>,
parentRepo: WriteKeyValuesRepo<Key, Value>,
protected open val kvCache: FullKVCache<Key, List<Value>>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) : WriteKeyValuesRepo<Key, Value> by parentRepo, FullCacheRepo {
@@ -125,6 +128,10 @@ open class FullWriteKeyValuesCacheRepo<Key,Value>(
kvCache.get(it.first) ?.minus(it.second) ?: return@onEach
)
}.launchIn(scope)
override suspend fun invalidate() {
kvCache.clear()
}
}
fun <Key, Value> WriteKeyValuesRepo<Key, Value>.caching(
@@ -133,7 +140,7 @@ fun <Key, Value> WriteKeyValuesRepo<Key, Value>.caching(
) = FullWriteKeyValuesCacheRepo(this, kvCache, scope)
open class FullKeyValuesCacheRepo<Key,Value>(
parentRepo: KeyValuesRepo<Key, Value>,
protected open val parentRepo: KeyValuesRepo<Key, Value>,
kvCache: FullKVCache<Key, List<Value>>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) : FullWriteKeyValuesCacheRepo<Key, Value>(parentRepo, kvCache, scope),
@@ -146,6 +153,10 @@ open class FullKeyValuesCacheRepo<Key,Value>(
}
}
}
override suspend fun invalidate() {
kvCache.actualizeAll(parentRepo)
}
}
fun <Key, Value> KeyValuesRepo<Key, Value>.caching(

View File

@@ -0,0 +1,53 @@
package dev.inmo.micro_utils.repos.cache.util
import dev.inmo.micro_utils.pagination.FirstPagePagination
import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging
import dev.inmo.micro_utils.pagination.utils.getAllByWithNextPaging
import dev.inmo.micro_utils.repos.ReadCRUDRepo
import dev.inmo.micro_utils.repos.ReadKeyValueRepo
import dev.inmo.micro_utils.repos.ReadKeyValuesRepo
import dev.inmo.micro_utils.repos.cache.cache.KVCache
import dev.inmo.micro_utils.repos.pagination.getAll
import dev.inmo.micro_utils.repos.set
suspend inline fun <K, V> KVCache<K, V>.actualizeAll(
clear: Boolean = true,
getAll: () -> Map<K, V>
) {
set(
getAll().also {
if (clear) {
clear()
}
}
)
}
suspend inline fun <K, V> KVCache<K, V>.actualizeAll(
repo: ReadKeyValueRepo<K, V>,
clear: Boolean = true,
) {
actualizeAll(clear) {
repo.getAll { keys(it) }.toMap()
}
}
suspend inline fun <K, V> KVCache<K, List<V>>.actualizeAll(
repo: ReadKeyValuesRepo<K, V>,
clear: Boolean = true,
) {
actualizeAll(clear) {
repo.getAll { keys(it) }.toMap()
}
}
suspend inline fun <K, V> KVCache<K, V>.actualizeAll(
repo: ReadCRUDRepo<V, K>,
clear: Boolean = true,
) {
actualizeAll(clear) {
repo.getAllByWithNextPaging {
getIdsByPagination(it)
}.mapNotNull { it to (repo.getById(it) ?: return@mapNotNull null) }.toMap()
}
}

View File

@@ -32,5 +32,3 @@ kotlin {
}
}
}
disableIncludingJvmCodeInAndroidPart()

View File

@@ -2,6 +2,8 @@ package dev.inmo.micro_utils.repos
import dev.inmo.micro_utils.pagination.*
import dev.inmo.micro_utils.pagination.utils.doAllWithCurrentPaging
import dev.inmo.micro_utils.pagination.utils.getAllWithNextPaging
import dev.inmo.micro_utils.pagination.utils.paginate
import kotlinx.coroutines.flow.Flow
/**
@@ -17,24 +19,32 @@ interface ReadKeyValueRepo<Key, Value> : Repo {
suspend fun get(k: Key): Value?
/**
* This method should use sorted by [Key]s search and take the [PaginationResult]. By default, it should use
* This method should use sorted by [Key]s search and return the [PaginationResult]. By default, it should use
* ascending sort for [Key]s
*/
suspend fun values(pagination: Pagination, reversed: Boolean = false): PaginationResult<Value>
/**
* This method should use sorted by [Key]s search and take the [PaginationResult]. By default, it should use
* This method should use sorted by [Key]s search and return the [PaginationResult]. By default, it should use
* ascending sort for [Key]s
*/
suspend fun keys(pagination: Pagination, reversed: Boolean = false): PaginationResult<Key>
/**
* This method should use sorted by [Key]s search and take the [PaginationResult]. By default, it should use
* ascending sort for [Key]s
* This method should use sorted by [Key]s search and return the [PaginationResult]. By default, it should use
* ascending sort for [Key]s.
*
* **DEFAULT REALIZATION IS NOT OPTIMAL AND HAS BEEN ADDED TO COVER CASES OF DIFFERENT COMMON MAPPINGS AND TRANSFORMATIONS**
*
* @param v This value should be used to exclude from search the items with different [Value]s
*/
suspend fun keys(v: Value, pagination: Pagination, reversed: Boolean = false): PaginationResult<Key>
suspend fun keys(v: Value, pagination: Pagination, reversed: Boolean = false): PaginationResult<Key> {
return getAllWithNextPaging {
keys(it)
}.filter {
get(it) == v
}.paginate(pagination, reversed)
}
/**
* @return true if [key] is presented in current collection or false otherwise
@@ -93,6 +103,10 @@ suspend inline fun <Key, Value> WriteKeyValueRepo<Key, Value>.set(
vararg toSet: Pair<Key, Value>
) = set(toSet.toMap())
suspend inline fun <Key, Value> WriteKeyValueRepo<Key, Value>.set(
toSet: List<Pair<Key, Value>>
) = set(toSet.toMap())
suspend inline fun <Key, Value> WriteKeyValueRepo<Key, Value>.set(
k: Key, v: Value
) = set(k to v)
@@ -125,7 +139,11 @@ interface KeyValueRepo<Key, Value> : ReadKeyValueRepo<Key, Value>, WriteKeyValue
* By default, will remove all the data of current repo using [doAllWithCurrentPaging], [keys] and [unset]
*/
suspend fun clear() {
doAllWithCurrentPaging { keys(it).also { unset(it.results) } }
var count: Int
do {
count = count().takeIf { it < Int.MAX_VALUE } ?.toInt() ?: Int.MAX_VALUE
keys(FirstPagePagination(count)).also { unset(it.results) }
} while(count > 0)
}
}
typealias StandardKeyValueRepo<Key,Value> = KeyValueRepo<Key, Value>

View File

@@ -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

View File

@@ -0,0 +1,10 @@
package dev.inmo.micro_utils.repos.transforms.crud
import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.KeyValuesRepo
import dev.inmo.micro_utils.repos.ReadKeyValueRepo
import dev.inmo.micro_utils.repos.ReadKeyValuesRepo
import kotlin.js.JsName
import kotlin.jvm.JvmName
fun <K, V> ReadKeyValueRepo<K, V>.asReadCRUDRepo() = ReadCRUDFromKeyValueRepo(this)

View File

@@ -0,0 +1,20 @@
package dev.inmo.micro_utils.repos.transforms.crud
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.repos.ReadCRUDRepo
import dev.inmo.micro_utils.repos.ReadKeyValueRepo
open class ReadCRUDFromKeyValueRepo<RegisteredType, IdType>(
protected open val original: ReadKeyValueRepo<IdType, RegisteredType>
) : ReadCRUDRepo<RegisteredType, IdType> {
override suspend fun contains(id: IdType): Boolean = original.contains(id)
override suspend fun count(): Long = original.count()
override suspend fun getByPagination(pagination: Pagination): PaginationResult<RegisteredType> = original.values(pagination)
override suspend fun getIdsByPagination(pagination: Pagination): PaginationResult<IdType> = original.keys(pagination)
override suspend fun getById(id: IdType): RegisteredType? = original.get(id)
}

View File

@@ -0,0 +1,15 @@
package dev.inmo.micro_utils.repos.transforms.kv
import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.KeyValuesRepo
import dev.inmo.micro_utils.repos.ReadCRUDRepo
import dev.inmo.micro_utils.repos.ReadKeyValueRepo
import dev.inmo.micro_utils.repos.ReadKeyValuesRepo
import kotlin.js.JsName
import kotlin.jvm.JvmName
fun <K, V> ReadKeyValuesRepo<K, V>.asReadKeyValueRepo() = ReadKeyValueFromKeyValuesRepo(this)
fun <K, V> KeyValuesRepo<K, V>.asKeyValueRepo() = KeyValueFromKeyValuesRepo(this)
fun <K, V> ReadCRUDRepo<K, V>.asReadKeyValueRepo() = ReadKeyValueFromCRUDRepo(this)

View File

@@ -0,0 +1,41 @@
package dev.inmo.micro_utils.repos.transforms.kv
import dev.inmo.micro_utils.pagination.FirstPagePagination
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.pagination.changeResults
import dev.inmo.micro_utils.pagination.changeResultsUnchecked
import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging
import dev.inmo.micro_utils.pagination.utils.optionallyReverse
import dev.inmo.micro_utils.pagination.utils.paginate
import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.KeyValuesRepo
import dev.inmo.micro_utils.repos.ReadKeyValueRepo
import dev.inmo.micro_utils.repos.ReadKeyValuesRepo
import dev.inmo.micro_utils.repos.transforms.kvs.ReadKeyValuesFromKeyValueRepo
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge
open class KeyValueFromKeyValuesRepo<Key, Value>(
private val original: KeyValuesRepo<Key, Value>
) : KeyValueRepo<Key, List<Value>>, ReadKeyValueFromKeyValuesRepo<Key, Value>(original) {
override val onNewValue: Flow<Pair<Key, List<Value>>> = merge(
original.onNewValue,
original.onValueRemoved
).mapNotNull {
it.first to (get(it.first) ?: return@mapNotNull null)
}
override val onValueRemoved: Flow<Key> = original.onDataCleared
override suspend fun unset(toUnset: List<Key>) {
toUnset.forEach {
original.clear(it)
}
}
override suspend fun set(toSet: Map<Key, List<Value>>) {
original.set(toSet)
}
}

View File

@@ -0,0 +1,46 @@
package dev.inmo.micro_utils.repos.transforms.kv
import dev.inmo.micro_utils.pagination.FirstPagePagination
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.pagination.changeResults
import dev.inmo.micro_utils.pagination.changeResultsUnchecked
import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging
import dev.inmo.micro_utils.pagination.utils.optionallyReverse
import dev.inmo.micro_utils.pagination.utils.paginate
import dev.inmo.micro_utils.repos.ReadCRUDRepo
import dev.inmo.micro_utils.repos.ReadKeyValueRepo
import dev.inmo.micro_utils.repos.ReadKeyValuesRepo
import dev.inmo.micro_utils.repos.transforms.kvs.ReadKeyValuesFromKeyValueRepo
import kotlin.jvm.JvmInline
@JvmInline
value class ReadKeyValueFromCRUDRepo<Key, Value>(
private val original: ReadCRUDRepo<Value, Key>
) : ReadKeyValueRepo<Key, Value> {
override suspend fun get(k: Key): Value? = original.getById(k)
override suspend fun values(pagination: Pagination, reversed: Boolean): PaginationResult<Value> = original.getByPagination(
pagination.optionallyReverse(count(), reversed)
).let {
if (reversed) {
it.changeResultsUnchecked(it.results.reversed())
} else {
it
}
}
override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult<Key> = original.getIdsByPagination(
pagination.optionallyReverse(count(), reversed)
).let {
if (reversed) {
it.changeResultsUnchecked(it.results.reversed())
} else {
it
}
}
override suspend fun count(): Long = original.count()
override suspend fun contains(key: Key): Boolean = original.contains(key)
}

View File

@@ -0,0 +1,67 @@
package dev.inmo.micro_utils.repos.transforms.kv
import dev.inmo.micro_utils.pagination.FirstPagePagination
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.pagination.changeResults
import dev.inmo.micro_utils.pagination.changeResultsUnchecked
import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging
import dev.inmo.micro_utils.pagination.utils.optionallyReverse
import dev.inmo.micro_utils.pagination.utils.paginate
import dev.inmo.micro_utils.repos.ReadKeyValueRepo
import dev.inmo.micro_utils.repos.ReadKeyValuesRepo
import dev.inmo.micro_utils.repos.transforms.kvs.ReadKeyValuesFromKeyValueRepo
open class ReadKeyValueFromKeyValuesRepo<Key, Value>(
private val original: ReadKeyValuesRepo<Key, Value>
) : ReadKeyValueRepo<Key, List<Value>> {
override suspend fun get(k: Key): List<Value>? = original.getAll(k)
override suspend fun values(
pagination: Pagination,
reversed: Boolean
): PaginationResult<List<Value>> {
val keys = keys(pagination, reversed)
return keys.changeResults(
keys.results.mapNotNull {
get(it)
}
)
}
override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult<Key> {
return original.keys(pagination, reversed)
}
override suspend fun count(): Long {
return original.count()
}
override suspend fun contains(key: Key): Boolean {
return original.contains(key)
}
override suspend fun keys(v: List<Value>, pagination: Pagination, reversed: Boolean): PaginationResult<Key> {
val keys = mutableSetOf<Key>()
doForAllWithNextPaging(FirstPagePagination(count().toInt())) {
original.keys(it).also {
it.results.forEach {
val values = get(it) ?: return@forEach
if (values.containsAll(v) && v.containsAll(values)) {
keys.add(it)
}
}
}
}
val paginated = keys.paginate(
pagination.optionallyReverse(keys.count(), reversed)
)
return if (reversed) {
paginated.changeResultsUnchecked(paginated.results.reversed())
} else {
paginated
}
}
}

View File

@@ -0,0 +1,21 @@
package dev.inmo.micro_utils.repos.transforms.kvs
import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.ReadKeyValueRepo
import kotlin.js.JsName
import kotlin.jvm.JvmName
fun <K, V, VI : Iterable<V>> ReadKeyValueRepo<K, VI>.asReadKeyValuesRepo() = ReadKeyValuesFromKeyValueRepo(this)
fun <K, V, VI : Iterable<V>> KeyValueRepo<K, VI>.asKeyValuesRepo(
listToValuesIterable: suspend (List<V>) -> VI
): KeyValuesFromKeyValueRepo<K, V, VI> = KeyValuesFromKeyValueRepo(this, listToValuesIterable)
@JvmName("asListKeyValuesRepo")
@JsName("asListKeyValuesRepo")
fun <K, V> KeyValueRepo<K, List<V>>.asKeyValuesRepo(): KeyValuesFromKeyValueRepo<K, V, List<V>> = asKeyValuesRepo { it }
@JvmName("asSetKeyValuesRepo")
@JsName("asSetKeyValuesRepo")
fun <K, V> KeyValueRepo<K, Set<V>>.asKeyValuesRepo(): KeyValuesFromKeyValueRepo<K, V, Set<V>> = asKeyValuesRepo { it.toSet() }

View File

@@ -0,0 +1,77 @@
package dev.inmo.micro_utils.repos.transforms.kvs
import dev.inmo.micro_utils.pagination.FirstPagePagination
import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging
import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.KeyValuesRepo
import dev.inmo.micro_utils.repos.unset
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlin.js.JsName
import kotlin.jvm.JvmName
open class KeyValuesFromKeyValueRepo<Key, Value, ValuesIterable : Iterable<Value>>(
private val original: KeyValueRepo<Key, ValuesIterable>,
private val listToValuesIterable: suspend (List<Value>) -> ValuesIterable
) : KeyValuesRepo<Key, Value>, ReadKeyValuesFromKeyValueRepo<Key, Value, ValuesIterable>(original) {
private val _onNewValue = MutableSharedFlow<Pair<Key, Value>>()
private val _onValueRemoved = MutableSharedFlow<Pair<Key, Value>>()
override val onNewValue: Flow<Pair<Key, Value>> = _onNewValue.asSharedFlow()
override val onValueRemoved: Flow<Pair<Key, Value>> = _onValueRemoved.asSharedFlow()
override val onDataCleared: Flow<Key> = original.onValueRemoved
override suspend fun clearWithValue(v: Value) {
val keys = mutableSetOf<Key>()
doForAllWithNextPaging(FirstPagePagination(count().toInt())) {
original.keys(it).also {
it.results.forEach {
if (contains(it, v)) {
keys.add(it)
}
}
}
}
original.unset(keys.toList())
}
override suspend fun clear(k: Key) {
original.unset(k)
}
override suspend fun remove(toRemove: Map<Key, List<Value>>) {
original.set(
toRemove.mapNotNull { (k, removing) ->
val exists = original.get(k) ?: return@mapNotNull null
k to listToValuesIterable(exists - removing).also {
if (it.firstOrNull() == null) {
original.unset(k)
return@mapNotNull null
}
}
}.toMap()
)
toRemove.forEach { (k, v) ->
v.forEach {
_onValueRemoved.emit(k to it)
}
}
}
override suspend fun add(toAdd: Map<Key, List<Value>>) {
original.set(
toAdd.mapNotNull { (k, adding) ->
val exists = original.get(k) ?: emptyList()
k to listToValuesIterable(exists + adding)
}.toMap()
)
toAdd.forEach { (k, v) ->
v.forEach {
_onNewValue.emit(k to it)
}
}
}
}

View File

@@ -0,0 +1,72 @@
package dev.inmo.micro_utils.repos.transforms.kvs
import dev.inmo.micro_utils.pagination.FirstPagePagination
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.pagination.changeResultsUnchecked
import dev.inmo.micro_utils.pagination.emptyPaginationResult
import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging
import dev.inmo.micro_utils.pagination.utils.optionallyReverse
import dev.inmo.micro_utils.pagination.utils.paginate
import dev.inmo.micro_utils.repos.ReadKeyValueRepo
import dev.inmo.micro_utils.repos.ReadKeyValuesRepo
open class ReadKeyValuesFromKeyValueRepo<Key, Value, ValuesIterable : Iterable<Value>>(
private val original: ReadKeyValueRepo<Key, ValuesIterable>
) : ReadKeyValuesRepo<Key, Value> {
override suspend fun get(
k: Key,
pagination: Pagination,
reversed: Boolean
): PaginationResult<Value> {
val iterable = original.get(k) ?: return emptyPaginationResult(pagination)
val paginated = iterable.paginate(
pagination.optionallyReverse(iterable.count(), reversed)
)
return if (reversed) {
paginated.changeResultsUnchecked(paginated.results.reversed())
} else {
paginated
}
}
override suspend fun keys(
pagination: Pagination,
reversed: Boolean
): PaginationResult<Key> = original.keys(pagination, reversed)
override suspend fun count(): Long = original.count()
override suspend fun count(k: Key): Long = original.get(k) ?.count() ?.toLong() ?: 0L
override suspend fun contains(k: Key, v: Value): Boolean = original.get(k) ?.contains(v) == true
override suspend fun contains(k: Key): Boolean = original.contains(k)
override suspend fun keys(
v: Value,
pagination: Pagination,
reversed: Boolean
): PaginationResult<Key> {
val keys = mutableSetOf<Key>()
doForAllWithNextPaging(FirstPagePagination(count().toInt())) {
original.keys(it).also {
it.results.forEach {
if (contains(it, v)) {
keys.add(it)
}
}
}
}
val paginated = keys.paginate(
pagination.optionallyReverse(keys.count(), reversed)
)
return if (reversed) {
paginated.changeResultsUnchecked(paginated.results.reversed())
} else {
paginated
}
}
}

View File

@@ -31,11 +31,19 @@ class FileReadKeyValueRepo(
override suspend fun values(pagination: Pagination, reversed: Boolean): PaginationResult<File> {
val count = count()
val resultPagination = if (reversed) pagination.reverse(count) else pagination
val filesPaths = folder.list() ?.copyOfRange(resultPagination.firstIndex, resultPagination.lastIndexExclusive) ?: return emptyPaginationResult()
if (reversed) {
filesPaths.reverse()
val filesList = folder.list()
val files: Array<String> = if (resultPagination.firstIndex < count) {
val filesPaths = filesList.copyOfRange(resultPagination.firstIndex, resultPagination.lastIndexExclusive.coerceAtMost(filesList.size))
if (reversed) {
filesPaths.reversedArray()
} else {
filesPaths
}
} else {
emptyArray<String>()
}
return filesPaths.map { File(folder, it) }.createPaginationResult(
return files.map { File(folder, it) }.createPaginationResult(
resultPagination,
count
)
@@ -44,11 +52,21 @@ class FileReadKeyValueRepo(
override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult<String> {
val count = count()
val resultPagination = if (reversed) pagination.reverse(count) else pagination
val filesPaths = folder.list() ?.copyOfRange(resultPagination.firstIndex, resultPagination.lastIndexExclusive) ?: return emptyPaginationResult()
if (reversed) {
filesPaths.reverse()
val filesList = folder.list()
val files: Array<String> = if (resultPagination.firstIndex < count) {
val filesPaths = filesList.copyOfRange(resultPagination.firstIndex, resultPagination.lastIndexExclusive.coerceAtMost(filesList.size))
if (reversed) {
filesPaths.reversedArray()
} else {
filesPaths
}
} else {
emptyArray<String>()
}
return filesPaths.toList().createPaginationResult(
return files.toList().createPaginationResult(
resultPagination,
count
)

View File

@@ -0,0 +1,23 @@
package dev.inmo.micro_utils.repos.exposed
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.isNotNull
import org.jetbrains.exposed.sql.SqlExpressionBuilder.isNull
import org.jetbrains.exposed.sql.SqlExpressionBuilder.neq
fun <T> Column<T?>.eqOrIsNull(
value: T?
) = if (value == null) {
isNull()
} else {
eq(value)
}
fun <T> Column<T?>.neqOrIsNotNull(
value: T?
) = if (value == null) {
isNotNull()
} else {
neq(value)
}

View File

@@ -7,15 +7,9 @@ interface CommonExposedRepo<IdType, ObjectType> : ExposedRepo {
val ResultRow.asId: IdType
val selectById: ISqlExpressionBuilder.(IdType) -> Op<Boolean>
val selectByIds: ISqlExpressionBuilder.(List<IdType>) -> Op<Boolean>
get() = { list ->
if (list.isEmpty()) {
Op.FALSE
} else {
var op = selectById(list.first())
(1 until list.size).forEach {
op = op.or(selectById(list[it]))
}
op
}
get() = {
it.foldRight<IdType, Op<Boolean>?>(null) { id, acc ->
acc ?.or(selectById(id)) ?: selectById(id)
} ?: Op.FALSE
}
}

154
repos/generator/README.md Normal file
View 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
}
```

View 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
}

View 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
}
}

View 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
)
}

View File

@@ -0,0 +1 @@
dev.inmo.micro_utils.repos.generator.Provider

View 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 {
}

View File

@@ -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)

View 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
}

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.repos.generator.test"/>

View File

@@ -1,12 +1,17 @@
package dev.inmo.micro_utils.repos.ktor.client.crud
import dev.inmo.micro_utils.ktor.client.createStandardWebsocketFlow
import dev.inmo.micro_utils.ktor.common.*
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.ktor.common.crud.deletedObjectsIdsFlowRouting
import dev.inmo.micro_utils.repos.ktor.common.crud.newObjectsFlowRouting
import dev.inmo.micro_utils.repos.ktor.common.crud.updatedObjectsFlowRouting
import io.ktor.client.HttpClient
import io.ktor.http.ContentType
import io.ktor.util.reflect.TypeInfo
import io.ktor.util.reflect.typeInfo
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.*
class KtorCRUDRepoClient<ObjectType, IdType, InputValue> (
@@ -21,6 +26,15 @@ class KtorCRUDRepoClient<ObjectType, IdType, InputValue> (
baseUrl: String,
httpClient: HttpClient,
contentType: ContentType,
newObjectsFlow: Flow<ObjectType> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, newObjectsFlowRouting),
),
updatedObjectsFlow: Flow<ObjectType> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, updatedObjectsFlowRouting),
),
deletedObjectsIdsFlow: Flow<IdType> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, deletedObjectsIdsFlowRouting),
),
noinline idSerializer: suspend (IdType) -> String
) = KtorCRUDRepoClient(
KtorReadCRUDRepoClient(
@@ -35,7 +49,10 @@ class KtorCRUDRepoClient<ObjectType, IdType, InputValue> (
KtorWriteCrudRepoClient<ObjectType, IdType, InputValue>(
baseUrl,
httpClient,
contentType
contentType,
newObjectsFlow,
updatedObjectsFlow,
deletedObjectsIdsFlow
)
)
@@ -44,11 +61,23 @@ class KtorCRUDRepoClient<ObjectType, IdType, InputValue> (
subpart: String,
httpClient: HttpClient,
contentType: ContentType,
newObjectsFlow: Flow<ObjectType> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, newObjectsFlowRouting),
),
updatedObjectsFlow: Flow<ObjectType> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, updatedObjectsFlowRouting),
),
deletedObjectsIdsFlow: Flow<IdType> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, deletedObjectsIdsFlowRouting),
),
noinline idSerializer: suspend (IdType) -> String
) = KtorCRUDRepoClient<ObjectType, IdType, InputValue>(
buildStandardUrl(baseUrl, subpart),
httpClient,
contentType,
newObjectsFlow,
updatedObjectsFlow,
deletedObjectsIdsFlow,
idSerializer
)
}
@@ -80,11 +109,23 @@ inline fun <reified ObjectType, reified IdType, reified InputValue> KtorCRUDRepo
subpart: String,
httpClient: HttpClient,
contentType: ContentType,
newObjectsFlow: Flow<ObjectType> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, newObjectsFlowRouting),
),
updatedObjectsFlow: Flow<ObjectType> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, updatedObjectsFlowRouting),
),
deletedObjectsIdsFlow: Flow<IdType> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, deletedObjectsIdsFlowRouting),
),
noinline idSerializer: suspend (IdType) -> String
) = KtorCRUDRepoClient<ObjectType, IdType, InputValue>(
buildStandardUrl(baseUrl, subpart),
httpClient,
contentType,
newObjectsFlow,
updatedObjectsFlow,
deletedObjectsIdsFlow,
idSerializer
)

View File

@@ -53,19 +53,22 @@ class KtorWriteCrudRepoClient<ObjectType, IdType, InputValue> (
inline operator fun <reified ObjectType, reified IdType, reified InputValue> invoke(
baseUrl: String,
httpClient: HttpClient,
contentType: ContentType
contentType: ContentType,
newObjectsFlow: Flow<ObjectType> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, newObjectsFlowRouting),
),
updatedObjectsFlow: Flow<ObjectType> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, updatedObjectsFlowRouting),
),
deletedObjectsIdsFlow: Flow<IdType> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, deletedObjectsIdsFlowRouting),
),
) = KtorWriteCrudRepoClient<ObjectType, IdType, InputValue>(
baseUrl,
httpClient,
httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, newObjectsFlowRouting),
),
httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, updatedObjectsFlowRouting),
),
httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, deletedObjectsIdsFlowRouting),
),
newObjectsFlow,
updatedObjectsFlow,
deletedObjectsIdsFlow,
{
contentType(contentType)
setBody(it)

View File

@@ -1,10 +1,14 @@
package dev.inmo.micro_utils.repos.ktor.client.key.value
import dev.inmo.micro_utils.ktor.client.createStandardWebsocketFlow
import dev.inmo.micro_utils.ktor.common.*
import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.ktor.common.key_value.onNewValueRoute
import dev.inmo.micro_utils.repos.ktor.common.key_value.onValueRemovedRoute
import io.ktor.client.HttpClient
import io.ktor.http.ContentType
import io.ktor.http.encodeURLQueryComponent
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.*
class KtorKeyValueRepoClient<Key, Value> (
@@ -20,6 +24,12 @@ class KtorKeyValueRepoClient<Key, Value> (
httpClient: HttpClient,
contentType: ContentType,
noinline idSerializer: suspend (Key) -> String,
onNewValue: Flow<Pair<Key, Value>> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onNewValueRoute),
),
onValueRemoved: Flow<Key> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onValueRemovedRoute),
),
noinline valueSerializer: suspend (Value) -> String
) = KtorKeyValueRepoClient(
KtorReadKeyValueRepoClient(
@@ -28,7 +38,9 @@ class KtorKeyValueRepoClient<Key, Value> (
KtorWriteKeyValueRepoClient(
baseUrl,
httpClient,
contentType
contentType,
onNewValue,
onValueRemoved
)
)
inline operator fun <reified Key, reified Value> invoke(
@@ -37,12 +49,20 @@ class KtorKeyValueRepoClient<Key, Value> (
httpClient: HttpClient,
contentType: ContentType,
noinline idSerializer: suspend (Key) -> String,
onNewValue: Flow<Pair<Key, Value>> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onNewValueRoute),
),
onValueRemoved: Flow<Key> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onValueRemovedRoute),
),
noinline valueSerializer: suspend (Value) -> String
) = KtorKeyValueRepoClient(
buildStandardUrl(baseUrl, subpart),
httpClient,
contentType,
idSerializer,
onNewValue,
onValueRemoved,
valueSerializer
)
}

View File

@@ -60,17 +60,19 @@ class KtorWriteKeyValueRepoClient<Key, Value>(
inline operator fun <reified Key, reified Value> invoke(
baseUrl: String,
httpClient: HttpClient,
contentType: ContentType
contentType: ContentType,
onNewValue: Flow<Pair<Key, Value>> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onNewValueRoute),
),
onValueRemoved: Flow<Key> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onValueRemovedRoute),
),
) = KtorWriteKeyValueRepoClient<Key, Value>(
baseUrl,
httpClient,
contentType,
httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onNewValueRoute),
),
httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onValueRemovedRoute),
),
onNewValue,
onValueRemoved,
typeInfo<List<Key>>(),
typeInfo<List<Value>>(),
typeInfo<Map<Key, Value>>()

Some files were not shown because too many files have changed in this diff Show More