Compare commits

...

101 Commits

Author SHA1 Message Date
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
58b007cbb3 fill changelog 2022-12-16 13:59:24 +06:00
4f0c139889 remove redundant stop koin expect-actual 2022-12-16 11:58:18 +06:00
c584c24fce fixes and improvements 2022-12-15 15:07:10 +06:00
85e5cee154 replace createStartupPluginAndRegister 2022-12-15 14:59:07 +06:00
5af91981f1 upgrades and filling of README 2022-12-15 10:26:31 +06:00
2fe4f08059 update compose 2022-12-15 08:51:53 +06:00
83796f345a start fix startup 2022-12-14 22:26:23 +06:00
c1e21364a6 start 0.16.2 2022-12-14 21:40:18 +06:00
067d9d0d3b Merge pull request #211 from InsanusMokrassar/0.16.1
0.16.1
2022-12-09 19:58:54 +06:00
127 changed files with 3648 additions and 249 deletions

View File

@@ -1,5 +1,129 @@
# Changelog # Changelog
## 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`:
* `Compose`: `1.2.1` -> `1.2.2`
* `Startup`:
* Module become available on `JS` target
## 0.16.1 ## 0.16.1
* `Coroutines`: * `Coroutines`:

View File

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

View File

@@ -18,6 +18,7 @@ kotlin {
api project(":micro_utils.coroutines") api project(":micro_utils.coroutines")
api libs.android.fragment 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 package dev.inmo.micro_utils.common
import kotlinx.serialization.Serializable
private inline fun <T> getObject( private inline fun <T> getObject(
additional: MutableList<T>, additional: MutableList<T>,
iterator: Iterator<T> iterator: Iterator<T>
@@ -24,13 +26,14 @@ private inline fun <T> getObject(
* *
* @see calculateDiff * @see calculateDiff
*/ */
@Serializable
data class Diff<T> internal constructor( data class Diff<T> internal constructor(
val removed: List<IndexedValue<T>>, val removed: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>,
/** /**
* Old-New values pairs * Old-New values pairs
*/ */
val replaced: List<Pair<IndexedValue<T>, IndexedValue<T>>>, val replaced: List<Pair<@Serializable(IndexedValueSerializer::class) IndexedValue<T>, @Serializable(IndexedValueSerializer::class) IndexedValue<T>>>,
val added: List<IndexedValue<T>> val added: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>
) )
private inline fun <T> performChanges( private inline fun <T> performChanges(

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 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]. * Executes the given [action] until getting of successful result specified number of [times].
* *
@@ -10,12 +32,23 @@ inline fun <R> repeatOnFailure(
onEachFailure: (Throwable) -> Unit = {}, onEachFailure: (Throwable) -> Unit = {},
action: (Int) -> R action: (Int) -> R
): Optional<R> { ): Optional<R> {
repeat(times) { var i = 0
runCatching { val result = repeatOnFailure(
action(it) {
}.onFailure(onEachFailure).onSuccess { onEachFailure(it)
return Optional.presented(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

@@ -22,6 +22,7 @@ kotlin {
dependencies { dependencies {
api libs.kt.coroutines.android 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.*
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import dev.inmo.micro_utils.common.applyDiff 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 dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow 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") @Suppress("NOTHING_TO_INLINE")
inline fun <reified T> Flow<List<T>>.asMutableComposeListState( inline fun <reified T> Flow<List<T>>.asMutableComposeListState(
scope: CoroutineScope scope: CoroutineScope,
useContextOnChange: CoroutineContext? = Dispatchers.Main,
noinline onException: ExceptionHandler<List<T>?> = defaultSafelyWithoutExceptionHandlerWithNull,
): SnapshotStateList<T> { ): SnapshotStateList<T> {
val state = mutableStateListOf<T>() val state = mutableStateListOf<T>()
subscribeSafelyWithoutExceptions(scope) { val changeBlock: suspend (List<T>) -> Unit = useContextOnChange ?.let {
{
withContext(useContextOnChange) {
state.applyDiff(it)
}
}
} ?: {
state.applyDiff(it) state.applyDiff(it)
} }
subscribeSafelyWithoutExceptions(scope, onException, changeBlock)
return state 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") @Suppress("NOTHING_TO_INLINE")
inline fun <reified T> Flow<List<T>>.asComposeList( inline fun <reified T> Flow<List<T>>.asComposeList(
scope: CoroutineScope scope: CoroutineScope,
): List<T> = asMutableComposeListState(scope) 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 package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.* 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 dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow 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( fun <T> Flow<T>.asMutableComposeState(
initial: T, initial: T,
scope: CoroutineScope scope: CoroutineScope,
useContextOnChange: CoroutineContext? = Dispatchers.Main,
onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
): MutableState<T> { ): MutableState<T> {
val state = mutableStateOf(initial) 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 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") @Suppress("NOTHING_TO_INLINE")
inline fun <T> StateFlow<T>.asMutableComposeState( inline fun <T> StateFlow<T>.asMutableComposeState(
scope: CoroutineScope scope: CoroutineScope,
): MutableState<T> = asMutableComposeState(value, scope) 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( fun <T> Flow<T>.asComposeState(
initial: T, initial: T,
scope: CoroutineScope scope: CoroutineScope,
useContextOnChange: CoroutineContext? = Dispatchers.Main,
onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
): State<T> { ): State<T> {
val state = asMutableComposeState(initial, scope) val state = asMutableComposeState(initial, scope, useContextOnChange, onException)
return derivedStateOf { state.value } 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") @Suppress("NOTHING_TO_INLINE")
inline fun <T> StateFlow<T>.asComposeState( inline fun <T> StateFlow<T>.asComposeState(
scope: CoroutineScope scope: CoroutineScope,
): State<T> = asComposeState(value, scope) 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.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@Deprecated("Duplicated functionality", ReplaceWith("asMutableComposeState(initial, scope)", "dev.inmo.micro_utils.coroutines.compose.asMutableComposeState"))
fun <T> Flow<T>.toMutableState( fun <T> Flow<T>.toMutableState(
initial: T, initial: T,
scope: CoroutineScope scope: CoroutineScope
): MutableState<T> { ): MutableState<T> = asMutableComposeState(initial, scope)
val state = mutableStateOf(initial)
subscribeSafelyWithoutExceptions(scope) { state.value = it }
return state
}
@Deprecated("Duplicated functionality", ReplaceWith("asMutableComposeState(scope)", "dev.inmo.micro_utils.coroutines.compose.asMutableComposeState"))
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
inline fun <T> StateFlow<T>.toMutableState( inline fun <T> StateFlow<T>.toMutableState(
scope: CoroutineScope 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 var result: Result<T>? = null
val objectToSynchronize = Object() val objectToSynchronize = Object()
synchronized(objectToSynchronize) { synchronized(objectToSynchronize) {
launch { launch(start = CoroutineStart.UNDISPATCHED) {
result = safelyWithResult(block) result = safelyWithResult(block)
}.invokeOnCompletion { }.invokeOnCompletion {
synchronized(objectToSynchronize) { 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' 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 { android {
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger() compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
buildToolsVersion libs.versions.android.props.buildTools.get() buildToolsVersion libs.versions.android.props.buildTools.get()
@@ -58,10 +33,4 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_1_8.toString()
} }
sourceSets {
String sep = File.separator
main.java.srcDirs += "src${sep}main${sep}kotlin"
enableIncludingJvmCodeInAndroidPart()
}
} }

View File

@@ -23,11 +23,12 @@ allprojects {
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle" mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle" mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle"
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle" mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
mppJsAndJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJsAndJavaProject.gradle"
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle" mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle" defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle"
publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle" publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle"
publishMavenPath = "${rootProject.projectDir.absolutePath}/maven.publish.gradle" publishJvmOnlyPath = "${rootProject.projectDir.absolutePath}/jvm.publish.gradle"
} }
} }

View File

@@ -14,5 +14,5 @@ crypto_js_version=4.1.1
# Project data # Project data
group=dev.inmo group=dev.inmo
version=0.16.1 version=0.17.0
android_code_version=169 android_code_version=182

View File

@@ -1,37 +1,40 @@
[versions] [versions]
kt = "1.7.20" kt = "1.8.10"
kt-serialization = "1.4.1" kt-serialization = "1.5.0"
kt-coroutines = "1.6.4" kt-coroutines = "1.6.4"
kslog = "0.5.4" kslog = "1.0.0"
jb-compose = "1.2.1" jb-compose = "1.3.1-rc01"
jb-exposed = "0.41.1" jb-exposed = "0.41.1"
jb-dokka = "1.7.20" jb-dokka = "1.7.20"
klock = "3.4.0" klock = "3.4.0"
uuid = "0.6.0" uuid = "0.7.0"
ktor = "2.2.1" ktor = "2.2.3"
gh-release = "2.4.1" gh-release = "2.4.1"
koin = "3.2.2" koin = "3.3.2"
android-gradle = "7.3.0" ksp = "1.8.10-1.0.9"
dexcount = "3.1.0" kotlin-poet = "1.12.0"
android-gradle = "7.3.1"
dexcount = "4.0.0"
android-coreKtx = "1.9.0" android-coreKtx = "1.9.0"
android-recyclerView = "1.2.1" android-recyclerView = "1.2.1"
android-appCompat = "1.5.1" android-appCompat = "1.6.1"
android-fragment = "1.5.5" android-fragment = "1.5.5"
android-espresso = "3.4.0" android-espresso = "3.5.1"
android-test = "1.1.3" android-test = "1.1.5"
android-props-minSdk = "21" android-props-minSdk = "21"
android-props-compileSdk = "33" android-props-compileSdk = "33"
android-props-buildTools = "33.0.0" android-props-buildTools = "33.0.1"
[libraries] [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-js = { module = "org.jetbrains.kotlin:kotlin-test-js", version.ref = "kt" }
kt-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", 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-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-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-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-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" } buildscript-android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle" }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,10 @@ import io.ktor.client.call.body
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
suspend inline fun <reified T : Any> HttpResponse.bodyOrNull() = takeIf { suspend inline fun <reified T : Any> HttpResponse.bodyOrNull(
status == HttpStatusCode.OK statusFilter: (HttpResponse) -> Boolean = { it.status == HttpStatusCode.OK }
} ?.body<T>() ) = 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 * 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") @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 checkReconnection: suspend (Throwable?) -> Boolean = { true },
noinline webSocketSessionRequest: suspend SendChannel<T>.() -> Unit noinline webSocketSessionRequest: suspend SendChannel<T>.() -> Unit
): Flow<T> { ): Flow<T> {
@@ -57,7 +57,7 @@ inline fun <reified T : Any> HttpClient.openWebSocketFlow(
): Flow<T> { ): Flow<T> {
pluginOrNull(WebSockets) ?: error("Plugin $WebSockets must be installed for using createStandardWebsocketFlow") pluginOrNull(WebSockets) ?: error("Plugin $WebSockets must be installed for using createStandardWebsocketFlow")
return openBaseWebSocketFlow<T>(checkReconnection) { return openBaseWebSocketFlow(checkReconnection) {
val block: suspend DefaultClientWebSocketSession.() -> Unit = { val block: suspend DefaultClientWebSocketSession.() -> Unit = {
while (isActive) { while (isActive) {
send(receiveDeserialized<T>()) send(receiveDeserialized<T>())

View File

@@ -17,8 +17,11 @@ import io.ktor.http.HttpStatusCode
import io.ktor.http.Parameters import io.ktor.http.Parameters
import io.ktor.http.content.PartData import io.ktor.http.content.PartData
import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.StringFormat import kotlinx.serialization.StringFormat
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.serializer
import java.io.File 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 * in case you wish to pass other source of multipart binary data than regular file
* @suppress * @suppress
*/ */
@OptIn(InternalSerializationApi::class)
actual suspend fun <T> HttpClient.uniUpload( actual suspend fun <T> HttpClient.uniUpload(
url: String, url: String,
data: Map<String, Any>, 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 withBinary = data.values.any { it is File || it is UniUploadFileInfo }
val formData = formData { val formData = formData {
data.forEach { (k, v) -> for (k in data.keys) {
val v = data[k] ?: continue
when (v) { when (v) {
is File -> append( is File -> append(
k, k,
@@ -60,7 +65,7 @@ actual suspend fun <T> HttpClient.uniUpload(
) )
else -> append( else -> append(
k, 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( submitForm(
url, url,
Parameters.build { Parameters.build {
formData.forEach { for (it in formData) {
val formItem = (it as PartData.FormItem) val formItem = (it as PartData.FormItem)
append(it.name!!, it.value) append(it.name!!, it.value)
} }

View File

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

View File

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

View File

@@ -5,3 +5,11 @@ plugins {
} }
apply from: "$mppProjectWithSerializationPresetPath" 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

@@ -0,0 +1,49 @@
project.version = "$version"
project.group = "$group"
apply from: "$publishGradlePath"
kotlin {
jvm {
compilations.main {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
js (IR) {
browser()
nodejs()
}
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
}
}
jvmTest {
dependencies {
implementation kotlin('test-junit')
}
}
jsTest {
dependencies {
implementation kotlin('test-js')
implementation kotlin('test-junit')
}
}
}
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

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> { fun <T> Set<T>.paginate(with: Pagination): PaginationResult<T> {
return this.drop(with.firstIndex).take(with.size).createPaginationResult( return this.drop(with.firstIndex).take(with.size).createPaginationResult(
with, 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) { fun <T> Set<T>.paginate(with: Pagination, reversed: Boolean): PaginationResult<T> {
is List<T> -> optionallyReverse(reverse) val actualPagination = with.optionallyReverse(
is Set<T> -> optionallyReverse(reverse) size,
else -> if (reverse) { reversed
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) { val firstIndex = maxOf(actualPagination.firstIndex, 0)
Array(size) { val lastIndex = minOf(actualPagination.lastIndexExclusive, size)
get(lastIndex - it) 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 parentRepo: ReadCRUDRepo<ObjectType, IdType>,
protected open val kvCache: KVCache<IdType, ObjectType>, protected open val kvCache: KVCache<IdType, ObjectType>,
protected open val idGetter: (ObjectType) -> IdType 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 { override suspend fun getById(id: IdType): ObjectType? = kvCache.get(id) ?: (parentRepo.getById(id) ?.also {
kvCache.set(id, it) kvCache.set(id, it)
}) })
override suspend fun contains(id: IdType): Boolean = kvCache.contains(id) || parentRepo.contains(id) 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( 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 kvCache: KVCache<IdType, ObjectType>,
protected open val scope: CoroutineScope = CoroutineScope(Dispatchers.Default), protected open val scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
protected open val idGetter: (ObjectType) -> IdType 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 newObjectsFlow: Flow<ObjectType> by parentRepo::newObjectsFlow
override val updatedObjectsFlow: Flow<ObjectType> by parentRepo::updatedObjectsFlow override val updatedObjectsFlow: Flow<ObjectType> by parentRepo::updatedObjectsFlow
override val deletedObjectsIdsFlow: Flow<IdType> by parentRepo::deletedObjectsIdsFlow override val deletedObjectsIdsFlow: Flow<IdType> by parentRepo::deletedObjectsIdsFlow
@@ -72,6 +74,8 @@ open class WriteCRUDCacheRepo<ObjectType, IdType, InputValueType>(
return created return created
} }
override suspend fun invalidate() = kvCache.clear()
} }
fun <ObjectType, IdType, InputType> WriteCRUDRepo<ObjectType, IdType, InputType>.caching( fun <ObjectType, IdType, InputType> WriteCRUDRepo<ObjectType, IdType, InputType>.caching(

View File

@@ -1,3 +1,5 @@
package dev.inmo.micro_utils.repos.cache 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>( open class ReadKeyValueCacheRepo<Key,Value>(
protected open val parentRepo: ReadKeyValueRepo<Key, Value>, protected open val parentRepo: ReadKeyValueRepo<Key, Value>,
protected open val kvCache: KVCache<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 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) 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( fun <Key, Value> ReadKeyValueRepo<Key, Value>.cached(
@@ -33,9 +35,11 @@ open class KeyValueCacheRepo<Key,Value>(
parentRepo: KeyValueRepo<Key, Value>, parentRepo: KeyValueRepo<Key, Value>,
kvCache: KVCache<Key, Value>, kvCache: KVCache<Key, Value>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default) 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 onNewJob = parentRepo.onNewValue.onEach { kvCache.set(it.first, it.second) }.launchIn(scope)
protected val onRemoveJob = parentRepo.onValueRemoved.onEach { kvCache.unset(it) }.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( fun <Key, Value> KeyValueRepo<Key, Value>.cached(

View File

@@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.*
open class ReadKeyValuesCacheRepo<Key,Value>( open class ReadKeyValuesCacheRepo<Key,Value>(
protected open val parentRepo: ReadKeyValuesRepo<Key, Value>, protected open val parentRepo: ReadKeyValuesRepo<Key, Value>,
protected open val kvCache: KVCache<Key, List<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> { override suspend fun get(k: Key, pagination: Pagination, reversed: Boolean): PaginationResult<Value> {
return getAll(k, reversed).paginate( return getAll(k, reversed).paginate(
pagination 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 contains(k: Key): Boolean = kvCache.contains(k) || parentRepo.contains(k)
override suspend fun invalidate() = kvCache.clear()
} }
fun <Key, Value> ReadKeyValuesRepo<Key, Value>.cached( fun <Key, Value> ReadKeyValuesRepo<Key, Value>.cached(
@@ -40,7 +42,7 @@ open class KeyValuesCacheRepo<Key,Value>(
parentRepo: KeyValuesRepo<Key, Value>, parentRepo: KeyValuesRepo<Key, Value>,
kvCache: KVCache<Key, List<Value>>, kvCache: KVCache<Key, List<Value>>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default) 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) -> protected val onNewJob = parentRepo.onNewValue.onEach { (k, v) ->
kvCache.set( kvCache.set(
k, k,
@@ -56,6 +58,8 @@ open class KeyValuesCacheRepo<Key,Value>(
protected val onDataClearedJob = parentRepo.onDataCleared.onEach { protected val onDataClearedJob = parentRepo.onDataCleared.onEach {
kvCache.unset(it) kvCache.unset(it)
}.launchIn(scope) }.launchIn(scope)
override suspend fun invalidate() = kvCache.clear()
} }
fun <Key, Value> KeyValuesRepo<Key, Value>.cached( 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.common.*
import dev.inmo.micro_utils.pagination.Pagination import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.PaginationResult 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.*
import dev.inmo.micro_utils.repos.cache.* import dev.inmo.micro_utils.repos.cache.*
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -32,12 +31,7 @@ open class FullReadCRUDCacheRepo<ObjectType, IdType>(
} }
protected open suspend fun actualizeAll() { protected open suspend fun actualizeAll() {
kvCache.clear() kvCache.actualizeAll(parentRepo)
doForAllWithNextPaging {
parentRepo.getByPagination(it).also {
kvCache.set(it.results.associateBy { idGetter(it) })
}
}
} }
override suspend fun getByPagination(pagination: Pagination): PaginationResult<ObjectType> = doOrTakeAndActualize( override suspend fun getByPagination(pagination: Pagination): PaginationResult<ObjectType> = doOrTakeAndActualize(
@@ -69,6 +63,10 @@ open class FullReadCRUDCacheRepo<ObjectType, IdType>(
{ getById(id) }, { getById(id) },
{ it ?.let { set(idGetter(it), it) } } { it ?.let { set(idGetter(it), it) } }
) )
override suspend fun invalidate() {
actualizeAll()
}
} }
fun <ObjectType, IdType> ReadCRUDRepo<ObjectType, IdType>.cached( fun <ObjectType, IdType> ReadCRUDRepo<ObjectType, IdType>.cached(
@@ -92,7 +90,11 @@ open class FullCRUDCacheRepo<ObjectType, IdType, InputValueType>(
scope, scope,
idGetter idGetter
), ),
CRUDRepo<ObjectType, IdType, InputValueType> CRUDRepo<ObjectType, IdType, InputValueType> {
override suspend fun invalidate() {
actualizeAll()
}
}
fun <ObjectType, IdType, InputType> CRUDRepo<ObjectType, IdType, InputType>.cached( fun <ObjectType, IdType, InputType> CRUDRepo<ObjectType, IdType, InputType>.cached(
kvCache: FullKVCache<IdType, ObjectType>, 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.pagination.PaginationResult
import dev.inmo.micro_utils.repos.* import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache 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 dev.inmo.micro_utils.repos.pagination.getAll
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
open class FullReadKeyValueCacheRepo<Key,Value>( open class FullReadKeyValueCacheRepo<Key,Value>(
@@ -68,6 +70,10 @@ open class FullReadKeyValueCacheRepo<Key,Value>(
{ parentRepo.keys(v, pagination, reversed) }, { parentRepo.keys(v, pagination, reversed) },
{ if (it.results.isNotEmpty()) actualizeAll() } { if (it.results.isNotEmpty()) actualizeAll() }
) )
override suspend fun invalidate() {
actualizeAll()
}
} }
fun <Key, Value> ReadKeyValueRepo<Key, Value>.cached( fun <Key, Value> ReadKeyValueRepo<Key, Value>.cached(
@@ -75,12 +81,16 @@ fun <Key, Value> ReadKeyValueRepo<Key, Value>.cached(
) = FullReadKeyValueCacheRepo(this, kvCache) ) = FullReadKeyValueCacheRepo(this, kvCache)
open class FullWriteKeyValueCacheRepo<Key,Value>( open class FullWriteKeyValueCacheRepo<Key,Value>(
protected open val parentRepo: WriteKeyValueRepo<Key, Value>, parentRepo: WriteKeyValueRepo<Key, Value>,
protected open val kvCache: FullKVCache<Key, Value>, protected open val kvCache: FullKVCache<Key, Value>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default) scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) : WriteKeyValueRepo<Key, Value> by parentRepo, FullCacheRepo { ) : WriteKeyValueRepo<Key, Value> by parentRepo, FullCacheRepo {
protected val onNewJob = parentRepo.onNewValue.onEach { kvCache.set(it.first, it.second) }.launchIn(scope) 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) protected val onRemoveJob = parentRepo.onValueRemoved.onEach { kvCache.unset(it) }.launchIn(scope)
override suspend fun invalidate() {
kvCache.clear()
}
} }
fun <Key, Value> WriteKeyValueRepo<Key, Value>.caching( fun <Key, Value> WriteKeyValueRepo<Key, Value>.caching(
@@ -89,13 +99,17 @@ fun <Key, Value> WriteKeyValueRepo<Key, Value>.caching(
) = FullWriteKeyValueCacheRepo(this, kvCache, scope) ) = FullWriteKeyValueCacheRepo(this, kvCache, scope)
open class FullKeyValueCacheRepo<Key,Value>( open class FullKeyValueCacheRepo<Key,Value>(
parentRepo: KeyValueRepo<Key, Value>, protected open val parentRepo: KeyValueRepo<Key, Value>,
kvCache: FullKVCache<Key, Value>, kvCache: FullKVCache<Key, Value>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default) scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) : FullWriteKeyValueCacheRepo<Key,Value>(parentRepo, kvCache, scope), ) : FullWriteKeyValueCacheRepo<Key,Value>(parentRepo, kvCache, scope),
KeyValueRepo<Key,Value>, KeyValueRepo<Key,Value>,
ReadKeyValueRepo<Key, Value> by FullReadKeyValueCacheRepo(parentRepo, kvCache) { ReadKeyValueRepo<Key, Value> by FullReadKeyValueCacheRepo(parentRepo, kvCache) {
override suspend fun unsetWithValues(toUnset: List<Value>) = parentRepo.unsetWithValues(toUnset) override suspend fun unsetWithValues(toUnset: List<Value>) = parentRepo.unsetWithValues(toUnset)
override suspend fun invalidate() {
kvCache.actualizeAll(parentRepo)
}
} }
fun <Key, Value> KeyValueRepo<Key, Value>.cached( 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.pagination.utils.*
import dev.inmo.micro_utils.repos.* import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@@ -33,8 +34,7 @@ open class FullReadKeyValuesCacheRepo<Key,Value>(
} }
protected open suspend fun actualizeAll() { protected open suspend fun actualizeAll() {
doAllWithCurrentPaging { kvCache.keys(it).also { kvCache.unset(it.results) } } kvCache.actualizeAll(parentRepo)
kvCache.set(parentRepo.getAll())
} }
override suspend fun get(k: Key, pagination: Pagination, reversed: Boolean): PaginationResult<Value> { 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() } { if (it.results.isNotEmpty()) actualizeAll() }
) )
override suspend fun invalidate() {
actualizeAll()
}
} }
fun <Key, Value> ReadKeyValuesRepo<Key, Value>.cached( fun <Key, Value> ReadKeyValuesRepo<Key, Value>.cached(
@@ -109,7 +112,7 @@ fun <Key, Value> ReadKeyValuesRepo<Key, Value>.cached(
) = FullReadKeyValuesCacheRepo(this, kvCache) ) = FullReadKeyValuesCacheRepo(this, kvCache)
open class FullWriteKeyValuesCacheRepo<Key,Value>( open class FullWriteKeyValuesCacheRepo<Key,Value>(
protected open val parentRepo: WriteKeyValuesRepo<Key, Value>, parentRepo: WriteKeyValuesRepo<Key, Value>,
protected open val kvCache: FullKVCache<Key, List<Value>>, protected open val kvCache: FullKVCache<Key, List<Value>>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default) scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) : WriteKeyValuesRepo<Key, Value> by parentRepo, FullCacheRepo { ) : WriteKeyValuesRepo<Key, Value> by parentRepo, FullCacheRepo {
@@ -125,6 +128,10 @@ open class FullWriteKeyValuesCacheRepo<Key,Value>(
kvCache.get(it.first) ?.minus(it.second) ?: return@onEach kvCache.get(it.first) ?.minus(it.second) ?: return@onEach
) )
}.launchIn(scope) }.launchIn(scope)
override suspend fun invalidate() {
kvCache.clear()
}
} }
fun <Key, Value> WriteKeyValuesRepo<Key, Value>.caching( fun <Key, Value> WriteKeyValuesRepo<Key, Value>.caching(
@@ -133,7 +140,7 @@ fun <Key, Value> WriteKeyValuesRepo<Key, Value>.caching(
) = FullWriteKeyValuesCacheRepo(this, kvCache, scope) ) = FullWriteKeyValuesCacheRepo(this, kvCache, scope)
open class FullKeyValuesCacheRepo<Key,Value>( open class FullKeyValuesCacheRepo<Key,Value>(
parentRepo: KeyValuesRepo<Key, Value>, protected open val parentRepo: KeyValuesRepo<Key, Value>,
kvCache: FullKVCache<Key, List<Value>>, kvCache: FullKVCache<Key, List<Value>>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default) scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) : FullWriteKeyValuesCacheRepo<Key, Value>(parentRepo, kvCache, scope), ) : 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( 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.*
import dev.inmo.micro_utils.pagination.utils.doAllWithCurrentPaging 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 import kotlinx.coroutines.flow.Flow
/** /**
@@ -17,24 +19,32 @@ interface ReadKeyValueRepo<Key, Value> : Repo {
suspend fun get(k: Key): Value? 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 * ascending sort for [Key]s
*/ */
suspend fun values(pagination: Pagination, reversed: Boolean = false): PaginationResult<Value> 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 * ascending sort for [Key]s
*/ */
suspend fun keys(pagination: Pagination, reversed: Boolean = false): PaginationResult<Key> 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 * This method should use sorted by [Key]s search and return the [PaginationResult]. By default, it should use
* ascending sort for [Key]s * 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 * @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 * @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> vararg toSet: Pair<Key, Value>
) = set(toSet.toMap()) ) = 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( suspend inline fun <Key, Value> WriteKeyValueRepo<Key, Value>.set(
k: Key, v: Value k: Key, v: Value
) = set(k to v) ) = 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] * By default, will remove all the data of current repo using [doAllWithCurrentPaging], [keys] and [unset]
*/ */
suspend fun clear() { 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> 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> { override suspend fun values(pagination: Pagination, reversed: Boolean): PaginationResult<File> {
val count = count() val count = count()
val resultPagination = if (reversed) pagination.reverse(count) else pagination val resultPagination = if (reversed) pagination.reverse(count) else pagination
val filesPaths = folder.list() ?.copyOfRange(resultPagination.firstIndex, resultPagination.lastIndexExclusive) ?: return emptyPaginationResult() val filesList = folder.list()
if (reversed) { val files: Array<String> = if (resultPagination.firstIndex < count) {
filesPaths.reverse() 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, resultPagination,
count count
) )
@@ -44,11 +52,21 @@ class FileReadKeyValueRepo(
override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult<String> { override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult<String> {
val count = count() val count = count()
val resultPagination = if (reversed) pagination.reverse(count) else pagination val resultPagination = if (reversed) pagination.reverse(count) else pagination
val filesPaths = folder.list() ?.copyOfRange(resultPagination.firstIndex, resultPagination.lastIndexExclusive) ?: return emptyPaginationResult() val filesList = folder.list()
if (reversed) {
filesPaths.reverse() 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, resultPagination,
count 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 ResultRow.asId: IdType
val selectById: ISqlExpressionBuilder.(IdType) -> Op<Boolean> val selectById: ISqlExpressionBuilder.(IdType) -> Op<Boolean>
val selectByIds: ISqlExpressionBuilder.(List<IdType>) -> Op<Boolean> val selectByIds: ISqlExpressionBuilder.(List<IdType>) -> Op<Boolean>
get() = { list -> get() = {
if (list.isEmpty()) { it.foldRight<IdType, Op<Boolean>?>(null) { id, acc ->
Op.FALSE acc ?.or(selectById(id)) ?: selectById(id)
} else { } ?: Op.FALSE
var op = selectById(list.first())
(1 until list.size).forEach {
op = op.or(selectById(list[it]))
}
op
}
} }
} }

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,15 @@
package dev.inmo.micro_utils.repos.ktor.client.key.values package dev.inmo.micro_utils.repos.ktor.client.key.values
import dev.inmo.micro_utils.ktor.client.createStandardWebsocketFlow
import dev.inmo.micro_utils.ktor.common.* import dev.inmo.micro_utils.ktor.common.*
import dev.inmo.micro_utils.repos.* import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.onDataClearedRoute
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.onNewValueRoute
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.onValueRemovedRoute
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.http.encodeURLQueryComponent import io.ktor.http.encodeURLQueryComponent
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.* import kotlinx.serialization.*
class KtorKeyValuesRepoClient<Key, Value> ( class KtorKeyValuesRepoClient<Key, Value> (
@@ -20,6 +25,15 @@ class KtorKeyValuesRepoClient<Key, Value> (
httpClient: HttpClient, httpClient: HttpClient,
contentType: ContentType, contentType: ContentType,
noinline keySerializer: suspend (Key) -> String, noinline keySerializer: suspend (Key) -> String,
onNewValue: Flow<Pair<Key, Value>> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onNewValueRoute),
),
onValueRemoved: Flow<Pair<Key, Value>> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onValueRemovedRoute),
),
onDataCleared: Flow<Key> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onDataClearedRoute),
),
noinline valueSerializer: suspend (Value) -> String noinline valueSerializer: suspend (Value) -> String
) = KtorKeyValuesRepoClient( ) = KtorKeyValuesRepoClient(
KtorReadKeyValuesRepoClient( KtorReadKeyValuesRepoClient(
@@ -32,7 +46,10 @@ class KtorKeyValuesRepoClient<Key, Value> (
KtorWriteKeyValuesRepoClient( KtorWriteKeyValuesRepoClient(
baseUrl, baseUrl,
httpClient, httpClient,
contentType contentType,
onNewValue,
onValueRemoved,
onDataCleared
) )
) )
inline operator fun <reified Key : Any, reified Value : Any> invoke( inline operator fun <reified Key : Any, reified Value : Any> invoke(
@@ -41,12 +58,24 @@ class KtorKeyValuesRepoClient<Key, Value> (
httpClient: HttpClient, httpClient: HttpClient,
contentType: ContentType, contentType: ContentType,
noinline keySerializer: suspend (Key) -> String, noinline keySerializer: suspend (Key) -> String,
onNewValue: Flow<Pair<Key, Value>> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onNewValueRoute),
),
onValueRemoved: Flow<Pair<Key, Value>> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onValueRemovedRoute),
),
onDataCleared: Flow<Key> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onDataClearedRoute),
),
noinline valueSerializer: suspend (Value) -> String noinline valueSerializer: suspend (Value) -> String
) = KtorKeyValuesRepoClient( ) = KtorKeyValuesRepoClient(
buildStandardUrl(baseUrl, subpart), buildStandardUrl(baseUrl, subpart),
httpClient, httpClient,
contentType, contentType,
keySerializer, keySerializer,
onNewValue,
onValueRemoved,
onDataCleared,
valueSerializer valueSerializer
) )
} }
@@ -59,13 +88,25 @@ inline fun <reified Key : Any, reified Value : Any> KtorKeyValuesRepoClient(
keySerializer: SerializationStrategy<Key>, keySerializer: SerializationStrategy<Key>,
valueSerializer: SerializationStrategy<Value>, valueSerializer: SerializationStrategy<Value>,
serialFormat: StringFormat, serialFormat: StringFormat,
onNewValue: Flow<Pair<Key, Value>> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onNewValueRoute),
),
onValueRemoved: Flow<Pair<Key, Value>> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onValueRemovedRoute),
),
onDataCleared: Flow<Key> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onDataClearedRoute),
),
) = KtorKeyValuesRepoClient<Key, Value>( ) = KtorKeyValuesRepoClient<Key, Value>(
baseUrl, baseUrl,
httpClient, httpClient,
contentType, contentType,
{ {
serialFormat.encodeToString(keySerializer, it).encodeURLQueryComponent() serialFormat.encodeToString(keySerializer, it).encodeURLQueryComponent()
} },
onNewValue,
onValueRemoved,
onDataCleared
) { ) {
serialFormat.encodeToString(valueSerializer, it).encodeURLQueryComponent() serialFormat.encodeToString(valueSerializer, it).encodeURLQueryComponent()
} }
@@ -77,13 +118,25 @@ inline fun <reified Key : Any, reified Value : Any> KtorKeyValuesRepoClient(
keySerializer: SerializationStrategy<Key>, keySerializer: SerializationStrategy<Key>,
valueSerializer: SerializationStrategy<Value>, valueSerializer: SerializationStrategy<Value>,
serialFormat: BinaryFormat, serialFormat: BinaryFormat,
onNewValue: Flow<Pair<Key, Value>> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onNewValueRoute),
),
onValueRemoved: Flow<Pair<Key, Value>> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onValueRemovedRoute),
),
onDataCleared: Flow<Key> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onDataClearedRoute),
),
) = KtorKeyValuesRepoClient<Key, Value>( ) = KtorKeyValuesRepoClient<Key, Value>(
baseUrl, baseUrl,
httpClient, httpClient,
contentType, contentType,
{ {
serialFormat.encodeHex(keySerializer, it) serialFormat.encodeHex(keySerializer, it)
} },
onNewValue,
onValueRemoved,
onDataCleared
) { ) {
serialFormat.encodeHex(valueSerializer, it) serialFormat.encodeHex(valueSerializer, it)
} }

View File

@@ -84,20 +84,23 @@ class KtorWriteKeyValuesRepoClient<Key : Any, Value : Any>(
inline operator fun <reified Key : Any, reified Value : Any> invoke( inline operator fun <reified Key : Any, reified Value : Any> invoke(
baseUrl: String, baseUrl: String,
httpClient: HttpClient, httpClient: HttpClient,
contentType: ContentType contentType: ContentType,
onNewValue: Flow<Pair<Key, Value>> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onNewValueRoute),
),
onValueRemoved: Flow<Pair<Key, Value>> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onValueRemovedRoute),
),
onDataCleared: Flow<Key> = httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onDataClearedRoute),
),
) = KtorWriteKeyValuesRepoClient<Key, Value>( ) = KtorWriteKeyValuesRepoClient<Key, Value>(
baseUrl, baseUrl,
httpClient, httpClient,
contentType, contentType,
httpClient.createStandardWebsocketFlow( onNewValue,
buildStandardUrl(baseUrl, onNewValueRoute), onValueRemoved,
), onDataCleared,
httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onValueRemovedRoute),
),
httpClient.createStandardWebsocketFlow(
buildStandardUrl(baseUrl, onDataClearedRoute),
),
typeInfo<Key>(), typeInfo<Key>(),
typeInfo<Value>(), typeInfo<Value>(),
typeInfo<Map<Key, List<Value>>>() typeInfo<Map<Key, List<Value>>>()

View File

@@ -7,6 +7,8 @@ String[] includes = [
":safe_wrapper", ":safe_wrapper",
":crypto", ":crypto",
":koin", ":koin",
":koin:generator",
":koin:generator:test",
":selector:common", ":selector:common",
":pagination:common", ":pagination:common",
":pagination:exposed", ":pagination:exposed",
@@ -16,6 +18,8 @@ String[] includes = [
":language_codes", ":language_codes",
":language_codes:generator", ":language_codes:generator",
":repos:common", ":repos:common",
":repos:generator",
":repos:generator:test",
":repos:cache", ":repos:cache",
":repos:exposed", ":repos:exposed",
":repos:inmemory", ":repos:inmemory",

View File

@@ -0,0 +1,92 @@
# Startup Plugin Launcher
This module contains tools to start your plugin system.
## Config
Base config is pretty simple:
```json
{
"plugins": [
"dev.inmo.micro_utils.startup.launcher.HelloWorldPlugin"
]
}
```
So, `"dev.inmo.micro_utils.startup.launcher.HelloWorldPlugin"` is the fully qualified name of plugin you wish to be
included in the server.
> JS note: In JS there are no opportunity to determine object type by its full name. Because of it, in JS developers
> should prefer to use `Config` in their kotlin code directly instead of json config passing. More info see in [JS](#js)
> section
## JVM
For JVM target you may use main class by path: `dev.inmo.micro_utils.startup.launcher.MainKt`
It is expected, that you will pass the main ONE argument with path to the config json. Sample of launching:
```bash
./gradlew run --args="sample.config.json"
```
Content of `sample.config.json` described in [Config](#config) section.
You may build runnable app using:
```bash
./gradlew assembleDist
```
In that case in `build/distributions` folder you will be able to find zip and tar files with all required
tools for application running (via their `bin/app_name` binary). In that case yoy will not need to pass
`--args=...` and launch will look like `./bin/app_name sample.config.json`
## JS
In JS for starting of your plugins app, you should use `PluginsStarter` in your code:
```kotlin
PluginsStarter.startPlugins(
Config(HelloWorldPlugin)
)
```
`Config` here is deserialized variant from [Config](#config) section. As was said in [Config](#config) section, in JS
there is no way to find classes/objects by their full qualifiers. Because of it you should use some way to register your
plugins in `StartPluginSerializer` or use the code like in the snippet above: there plugins will be registered
automatically.
In case you wish to register your plugins manually and run server from config, you should use one of the ways to register
plugin on start.
Sample with `EagerInitialization`: [Kotlin JS doc about lazy initialization](https://kotlinlang.org/docs/js-ir-compiler.html#incremental-compilation-for-development-binaries),
[@EagerInitialization docs](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.js/-eager-initialization/):
```kotlin
@ExperimentalStdlibApi
@EagerInitialization
val plugin = createStartupPluginAndRegister("PluginNameToUseInConfig") {
// Your plugin creation. For example:
HelloWorldPlugin
}
```
So, in that case you will be able to load plugins list as `JsonObject` from anywhere and start plugins app with it:
```kotlin
PluginsStarter.startPlugins(
jsonObject
)
```
It will load `HelloWorldPlugin` if `jsonObject` have next content:
```json
{
"plugins": [
"PluginNameToUseInConfig"
]
}
```

View File

@@ -4,7 +4,7 @@ plugins {
id "application" id "application"
} }
apply from: "$mppJavaProjectPresetPath" apply from: "$mppJsAndJavaProjectPresetPath"
kotlin { kotlin {
sourceSets { sourceSets {
@@ -13,7 +13,7 @@ kotlin {
api internalProject("micro_utils.startup.plugin") api internalProject("micro_utils.startup.plugin")
} }
} }
jvmTest { commonTest {
dependencies { dependencies {
implementation libs.kt.coroutines.test implementation libs.kt.coroutines.test
} }

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