mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 22:39:25 +00:00
Compare commits
96 Commits
Author | SHA1 | Date | |
---|---|---|---|
f4ec1a4c60 | |||
c1c33cceb1 | |||
a3e975b2ba | |||
06e705a687 | |||
d56eb6c867 | |||
9cbca864e3 | |||
abb4378694 | |||
0eb698d9a4 | |||
15ea9f2093 | |||
d47aca0923 | |||
1ac50e9959 | |||
6adfbe3a96 | |||
59f36e62e9 | |||
54af116009 | |||
38fbec8e3b | |||
babbfc55e4 | |||
2511e18d69 | |||
29658c70a0 | |||
96311ee43d | |||
bd33b09052 | |||
8055003b47 | |||
1257492f85 | |||
1107b7f4ef | |||
a1a1171240 | |||
46c02e5df1 | |||
2e9efc57de | |||
acecadef17 | |||
19394b5e69 | |||
de999e197f | |||
9d95687d3c | |||
aa9dfb4ab8 | |||
9c5b44efb3 | |||
ac587a67e6 | |||
59428140a8 | |||
60bdb59d71 | |||
be52871de8 | |||
b7934cf357 | |||
dbfbeef90a | |||
00943c9cdf | |||
8745c6a16a | |||
433ba4b58f | |||
d40376e524 | |||
a2982f88f5 | |||
1642f7abd9 | |||
a10d2184ff | |||
522435f096 | |||
79b30290c0 | |||
f8b8626859 | |||
b061b85a08 | |||
3870db1c88 | |||
1be1070eb4 | |||
2696e663cf | |||
1e1f7df86d | |||
1d8ded8fd3 | |||
197825123a | |||
422b2e6db1 | |||
1973e0b5bf | |||
8258cf93a9 | |||
1d49bd5947 | |||
44317d1519 | |||
48e08fcc69 | |||
1a3ce6e623 | |||
fb122f3e70 | |||
7cca6039cc | |||
118e3dba39 | |||
87070710fa | |||
38499c3d4a | |||
5d754d968b | |||
d543d436bc | |||
12b54f99af | |||
2a95d7e643 | |||
e3d3cacfa4 | |||
4b13491a0e | |||
85d516d1e9 | |||
ac58b6a7e3 | |||
2cc6126765 | |||
f94b085850 | |||
c9822a491b | |||
23b2d60295 | |||
f4bc9eed39 | |||
e310f188b0 | |||
6c1571188c | |||
945d2fa284 | |||
020095f1ff | |||
b165a76e62 | |||
03c8830672 | |||
38448da89b | |||
2ade5aff91 | |||
29bf6e80ec | |||
027e927e1b | |||
fa061f88e2 | |||
1d01b65b5f | |||
2c2b364167 | |||
7c5fc9bf7c | |||
193d22ff20 | |||
0f3b553ba2 |
153
CHANGELOG.md
153
CHANGELOG.md
@@ -1,5 +1,158 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
**ALL DEPRECATIONS HAVE BEEN REMOVED**
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `1.7.10` -> `1.7.20`
|
||||||
|
* `Klock`: `3.3.0` -> `3.3.1`
|
||||||
|
* `Compose`: `1.2.0` -> `1.2.1`
|
||||||
|
* `Exposed`: `0.39.2` -> `0.40.1`
|
||||||
|
|
||||||
|
## 0.13.2
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Klock`: `3.1.0` -> `3.3.0`
|
||||||
|
* `Ktor`: `2.1.2` -> `2.1.3`
|
||||||
|
|
||||||
|
## 0.13.1
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Exposed`:
|
||||||
|
* `AbstractExposedWriteCRUDRepo#createAndInsertId` now is optional and returns nullable value
|
||||||
|
|
||||||
|
## 0.13.0
|
||||||
|
|
||||||
|
**ALL DEPRECATIONS HAVE BEEN REMOVED**
|
||||||
|
**A LOT OF KTOR METHODS RELATED TO UnifierRouter/UnifiedRequester HAVE BEEN REMOVED**
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Exposed`:
|
||||||
|
* `AbstractExposedWriteCRUDRepo` got two new methods: `update` with `it` as `UpdateBuilder<Int>` and `createAndInsertId`
|
||||||
|
* Old `update` method has been deprecated and not recommended to override anymore in realizations
|
||||||
|
* Old `insert` method now is `open` instead of `abstract` and can be omitted
|
||||||
|
* `AbstractExposedKeyValueRepo` got two new methods: `update` with `it` as `UpdateBuilder<Int>` and `insertKey`
|
||||||
|
* Old `update` method has been deprecated and not recommended to override anymore
|
||||||
|
* Old `insert` method now is `open` instead of `abstract` and can be omitted in realizations
|
||||||
|
|
||||||
|
## 0.12.17
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `JB Compose`: `1.2.0-alpha01-dev774` -> `1.2.0-beta02`
|
||||||
|
* `Ktor`: `2.1.1` -> `2.1.2`
|
||||||
|
* `Koin`: `3.2.0` -> `3.2.2`
|
||||||
|
|
||||||
|
## 0.12.16
|
||||||
|
|
||||||
|
* `Coroutines`:
|
||||||
|
* `Android`:
|
||||||
|
* Add class `FlowOnHierarchyChangeListener`
|
||||||
|
* Add `ViewGroup#setOnHierarchyChangeListenerRecursively(OnHierarchyChangeListener)`
|
||||||
|
|
||||||
|
## 0.12.15
|
||||||
|
|
||||||
|
* `Common`:
|
||||||
|
* `applyDiff` will return `Diff` object since this release
|
||||||
|
* `Android`:
|
||||||
|
* New functions/extensions `findViewsByTag` and `findViewsByTagInActivity`
|
||||||
|
* `Coroutines`:
|
||||||
|
* Add `Flow` extensions `flatMap`, `flatMapNotNull` and `flatten`
|
||||||
|
* Add `Flow` extensions `takeNotNull` and `filterNotNull`
|
||||||
|
|
||||||
|
## 0.12.14
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Android CoreKTX`: `1.8.0` -> `1.9.0`
|
||||||
|
* `Android AppCompat`: `1.4.2` -> `1.5.1`
|
||||||
|
* Android Compile SDK: 32 -> 33
|
||||||
|
* Android Build Tools: 32.0.0 -> 33.0.0
|
||||||
|
* `Common`:
|
||||||
|
* `Android`:
|
||||||
|
* Add `argumentOrNull`/`argumentOrThrow` delegates for fragments
|
||||||
|
* `Coroutines`:
|
||||||
|
* Rewrite `awaitFirstWithDeferred` onto `CompletableDeferred` instead of coroutines suspending
|
||||||
|
|
||||||
|
## 0.12.13
|
||||||
|
|
||||||
|
* `Coroutines`:
|
||||||
|
* Add opportunity to use markers in actors (solution of [#160](https://github.com/InsanusMokrassar/MicroUtils/issues/160))
|
||||||
|
* `Koin`:
|
||||||
|
* Module inited :)
|
||||||
|
* `Repos`:
|
||||||
|
* `Android`:
|
||||||
|
* Add typealias `KeyValueSPRepo` and opportunity to create shared preferences `KeyValue` repo with `KeyValueStore(...)` (fix of [#155](https://github.com/InsanusMokrassar/MicroUtils/issues/155))
|
||||||
|
|
||||||
|
## 0.12.12
|
||||||
|
|
||||||
|
* `Common`:
|
||||||
|
* `Compose`:
|
||||||
|
* `JS`:
|
||||||
|
* Add `SkeletonAnimation` stylesheet
|
||||||
|
|
||||||
|
## 0.12.11
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Cache`:
|
||||||
|
* Override `KeyValue` cache method `values`
|
||||||
|
|
||||||
|
## 0.12.10
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Cache`:
|
||||||
|
* Hotfix in key values `get`
|
||||||
|
|
||||||
|
## 0.12.9
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Klock`: `3.0.0` -> `3.1.0`
|
||||||
|
* `Repos`:
|
||||||
|
* `Cache`:
|
||||||
|
* Fixes in key values cache
|
||||||
|
|
||||||
|
## 0.12.8
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Ktor`: `2.1.0` -> `2.1.1`
|
||||||
|
* `Compose`: `1.2.0-alpha01-dev764` -> `1.2.0-alpha01-dev774`
|
||||||
|
* `Ktor`:
|
||||||
|
* `Client`:
|
||||||
|
* New extension `HttpClient#bodyOrNull` which returns `null` in case when server responded with `No Content` (204)
|
||||||
|
* `Server`:
|
||||||
|
* New extension `ApplicationCall#respondOrNoContent` which responds `No Content` (204) when passed data is null
|
||||||
|
|
||||||
|
## 0.12.7
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Cache`:
|
||||||
|
* Force `WriteCRUDCacheRepo` to subscribe on new and updated objects of parent repo
|
||||||
|
* `Pagination`:
|
||||||
|
* New function `changeResultsUnchecked(Pagination)`
|
||||||
|
|
||||||
|
## 0.12.6
|
||||||
|
|
||||||
|
* `MimeeTypes>`:
|
||||||
|
* Fixed absence of `image/*` in known mime types
|
||||||
|
|
||||||
|
## 0.12.5
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Exposed`:
|
||||||
|
* Fixes in `paginate` extensions
|
||||||
|
|
||||||
|
## 0.12.4
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `1.7.0` -> `1.7.10`
|
||||||
|
* `Compose`: `1.2.0-alpha01-dev755` -> `1.2.0-alpha01-dev764`
|
||||||
|
|
||||||
|
## 0.12.3
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Exposed`:
|
||||||
|
* Add abstract exposed variants of `KeyValue` and `KeyValues` repos
|
||||||
|
* Add new extension `Query#selectPaginated`
|
||||||
|
|
||||||
## 0.12.2
|
## 0.12.2
|
||||||
|
|
||||||
* `Versions`:
|
* `Versions`:
|
||||||
|
@@ -16,6 +16,7 @@ kotlin {
|
|||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api project(":micro_utils.coroutines")
|
api project(":micro_utils.coroutines")
|
||||||
|
api libs.android.fragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,43 @@
|
|||||||
|
package dev.inmo.micro_utils.common.compose
|
||||||
|
|
||||||
|
import org.jetbrains.compose.web.css.*
|
||||||
|
|
||||||
|
object SkeletonAnimation : StyleSheet() {
|
||||||
|
val skeletonKeyFrames: CSSNamedKeyframes by keyframes {
|
||||||
|
to { backgroundPosition("-20% 0") }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CSSBuilder.includeSkeletonStyle(
|
||||||
|
duration: CSSSizeValue<out CSSUnitTime> = 2.s,
|
||||||
|
timingFunction: AnimationTimingFunction = AnimationTimingFunction.EaseInOut,
|
||||||
|
iterationCount: Int? = null,
|
||||||
|
direction: AnimationDirection = AnimationDirection.Normal,
|
||||||
|
keyFrames: CSSNamedKeyframes = skeletonKeyFrames,
|
||||||
|
hideChildren: Boolean = true,
|
||||||
|
hideText: Boolean = hideChildren
|
||||||
|
) {
|
||||||
|
backgroundImage("linear-gradient(110deg, rgb(236, 236, 236) 40%, rgb(245, 245, 245) 50%, rgb(236, 236, 236) 65%)")
|
||||||
|
backgroundSize("200% 100%")
|
||||||
|
backgroundPosition("180% 0")
|
||||||
|
animation(keyFrames) {
|
||||||
|
duration(duration)
|
||||||
|
timingFunction(timingFunction)
|
||||||
|
iterationCount(iterationCount)
|
||||||
|
direction(direction)
|
||||||
|
}
|
||||||
|
if (hideText) {
|
||||||
|
property("color", "${Color.transparent} !important")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hideChildren) {
|
||||||
|
child(self, universal) style {
|
||||||
|
property("visibility", "hidden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val skeleton by style {
|
||||||
|
includeSkeletonStyle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@@ -14,6 +14,14 @@ private inline fun <T> getObject(
|
|||||||
/**
|
/**
|
||||||
* Diff object which contains information about differences between two [Iterable]s
|
* Diff object which contains information about differences between two [Iterable]s
|
||||||
*
|
*
|
||||||
|
* See tests for more info
|
||||||
|
*
|
||||||
|
* @param removed The objects which has been presented in the old collection but absent in new one. Index here is the index in the old collection
|
||||||
|
* @param added The object which appear in new collection only. Indexes here show the index in the new collection
|
||||||
|
* @param replaced Pair of old-new changes. First object has been presented in the old collection on its
|
||||||
|
* [IndexedValue.index] place, the second one is the object in new collection. Both have indexes due to the fact that in
|
||||||
|
* case when some value has been replaced after adds or removes in original collection the object index will be changed
|
||||||
|
*
|
||||||
* @see calculateDiff
|
* @see calculateDiff
|
||||||
*/
|
*/
|
||||||
data class Diff<T> internal constructor(
|
data class Diff<T> internal constructor(
|
||||||
@@ -165,7 +173,7 @@ inline fun <T> Iterable<T>.calculateStrictDiff(
|
|||||||
fun <T> MutableList<T>.applyDiff(
|
fun <T> MutableList<T>.applyDiff(
|
||||||
source: Iterable<T>,
|
source: Iterable<T>,
|
||||||
strictComparison: Boolean = false
|
strictComparison: Boolean = false
|
||||||
) = calculateDiff(source, strictComparison).let {
|
): Diff<T> = calculateDiff(source, strictComparison).also {
|
||||||
for (i in it.removed.indices.sortedDescending()) {
|
for (i in it.removed.indices.sortedDescending()) {
|
||||||
removeAt(it.removed[i].index)
|
removeAt(it.removed[i].index)
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,76 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import java.io.Serializable
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
object ArgumentPropertyNullableDelegate {
|
||||||
|
operator fun <T: Any> getValue(thisRef: Fragment, property: KProperty<*>): T? {
|
||||||
|
val arguments = thisRef.arguments ?: return null
|
||||||
|
val key = property.name
|
||||||
|
return when (property.getter.returnType.classifier) {
|
||||||
|
// Scalars
|
||||||
|
String::class -> arguments.getString(key)
|
||||||
|
Boolean::class -> arguments.getBoolean(key)
|
||||||
|
Byte::class -> arguments.getByte(key)
|
||||||
|
Char::class -> arguments.getChar(key)
|
||||||
|
Double::class -> arguments.getDouble(key)
|
||||||
|
Float::class -> arguments.getFloat(key)
|
||||||
|
Int::class -> arguments.getInt(key)
|
||||||
|
Long::class -> arguments.getLong(key)
|
||||||
|
Short::class -> arguments.getShort(key)
|
||||||
|
|
||||||
|
// References
|
||||||
|
Bundle::class -> arguments.getBundle(key)
|
||||||
|
CharSequence::class -> arguments.getCharSequence(key)
|
||||||
|
Parcelable::class -> arguments.getParcelable(key)
|
||||||
|
|
||||||
|
// Scalar arrays
|
||||||
|
BooleanArray::class -> arguments.getBooleanArray(key)
|
||||||
|
ByteArray::class -> arguments.getByteArray(key)
|
||||||
|
CharArray::class -> arguments.getCharArray(key)
|
||||||
|
DoubleArray::class -> arguments.getDoubleArray(key)
|
||||||
|
FloatArray::class -> arguments.getFloatArray(key)
|
||||||
|
IntArray::class -> arguments.getIntArray(key)
|
||||||
|
LongArray::class -> arguments.getLongArray(key)
|
||||||
|
ShortArray::class -> arguments.getShortArray(key)
|
||||||
|
Array::class -> {
|
||||||
|
val componentType = property.returnType.classifier ?.javaClass ?.componentType!!
|
||||||
|
@Suppress("UNCHECKED_CAST") // Checked by reflection.
|
||||||
|
when {
|
||||||
|
Parcelable::class.java.isAssignableFrom(componentType) -> {
|
||||||
|
arguments.getParcelableArray(key)
|
||||||
|
}
|
||||||
|
String::class.java.isAssignableFrom(componentType) -> {
|
||||||
|
arguments.getStringArray(key)
|
||||||
|
}
|
||||||
|
CharSequence::class.java.isAssignableFrom(componentType) -> {
|
||||||
|
arguments.getCharSequenceArray(key)
|
||||||
|
}
|
||||||
|
Serializable::class.java.isAssignableFrom(componentType) -> {
|
||||||
|
arguments.getSerializable(key)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val valueType = componentType.canonicalName
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Illegal value array type $valueType for key \"$key\""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Serializable::class -> arguments.getSerializable(key)
|
||||||
|
else -> null
|
||||||
|
} as? T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object ArgumentPropertyNonNullableDelegate {
|
||||||
|
operator fun <T: Any> getValue(thisRef: Fragment, property: KProperty<*>): T {
|
||||||
|
return ArgumentPropertyNullableDelegate.getValue<T>(thisRef, property)!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun argumentOrNull() = ArgumentPropertyNullableDelegate
|
||||||
|
fun argumentOrThrow() = ArgumentPropertyNonNullableDelegate
|
@@ -0,0 +1,61 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.children
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
|
||||||
|
fun findViewsByTag(viewGroup: ViewGroup, tag: Any?): List<View> {
|
||||||
|
return viewGroup.children.flatMap {
|
||||||
|
findViewsByTag(it, tag)
|
||||||
|
}.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findViewsByTag(viewGroup: ViewGroup, key: Int, tag: Any?): List<View> {
|
||||||
|
return viewGroup.children.flatMap {
|
||||||
|
findViewsByTag(it, key, tag)
|
||||||
|
}.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findViewsByTag(view: View, tag: Any?): List<View> {
|
||||||
|
val result = mutableListOf<View>()
|
||||||
|
if (view.tag == tag) {
|
||||||
|
result.add(view)
|
||||||
|
}
|
||||||
|
if (view is ViewGroup) {
|
||||||
|
result.addAll(findViewsByTag(view, tag))
|
||||||
|
}
|
||||||
|
return result.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findViewsByTag(view: View, key: Int, tag: Any?): List<View> {
|
||||||
|
val result = mutableListOf<View>()
|
||||||
|
if (view.getTag(key) == tag) {
|
||||||
|
result.add(view)
|
||||||
|
}
|
||||||
|
if (view is ViewGroup) {
|
||||||
|
result.addAll(findViewsByTag(view, key, tag))
|
||||||
|
}
|
||||||
|
return result.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Activity.findViewsByTag(tag: Any?) = rootView ?.let {
|
||||||
|
findViewsByTag(it, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Activity.findViewsByTag(key: Int, tag: Any?) = rootView ?.let {
|
||||||
|
findViewsByTag(it, key, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Fragment.findViewsByTag(tag: Any?) = view ?.let {
|
||||||
|
findViewsByTag(it, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Fragment.findViewsByTag(key: Int, tag: Any?) = view ?.let {
|
||||||
|
findViewsByTag(it, key, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Fragment.findViewsByTagInActivity(tag: Any?) = activity ?.findViewsByTag(tag)
|
||||||
|
|
||||||
|
fun Fragment.findViewsByTagInActivity(key: Int, tag: Any?) = activity ?.findViewsByTag(key, tag)
|
@@ -0,0 +1,7 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.view.View
|
||||||
|
|
||||||
|
val Activity.rootView: View?
|
||||||
|
get() = findViewById<View?>(android.R.id.content) ?.rootView ?: window.decorView.findViewById<View?>(android.R.id.content) ?.rootView
|
@@ -1,19 +1,15 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.flow.consumeAsFlow
|
||||||
|
|
||||||
fun <T> CoroutineScope.actor(
|
fun <T> CoroutineScope.actor(
|
||||||
channelCapacity: Int = Channel.UNLIMITED,
|
channelCapacity: Int = Channel.UNLIMITED,
|
||||||
block: suspend (T) -> Unit
|
block: suspend (T) -> Unit
|
||||||
): Channel<T> {
|
): Channel<T> {
|
||||||
val channel = Channel<T>(channelCapacity)
|
val channel = Channel<T>(channelCapacity)
|
||||||
launch {
|
channel.consumeAsFlow().subscribe(this, block)
|
||||||
for (data in channel) {
|
|
||||||
block(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return channel
|
return channel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,30 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.consumeAsFlow
|
||||||
|
|
||||||
|
fun <T> CoroutineScope.actorAsync(
|
||||||
|
channelCapacity: Int = Channel.UNLIMITED,
|
||||||
|
markerFactory: suspend (T) -> Any? = { null },
|
||||||
|
block: suspend (T) -> Unit
|
||||||
|
): Channel<T> {
|
||||||
|
val channel = Channel<T>(channelCapacity)
|
||||||
|
channel.consumeAsFlow().subscribeAsync(this, markerFactory, block)
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> CoroutineScope.safeActorAsync(
|
||||||
|
channelCapacity: Int = Channel.UNLIMITED,
|
||||||
|
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||||
|
noinline markerFactory: suspend (T) -> Any? = { null },
|
||||||
|
crossinline block: suspend (T) -> Unit
|
||||||
|
): Channel<T> = actorAsync(
|
||||||
|
channelCapacity,
|
||||||
|
markerFactory
|
||||||
|
) {
|
||||||
|
safely(onException) {
|
||||||
|
block(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@@ -6,23 +6,19 @@ import kotlin.coroutines.*
|
|||||||
suspend fun <T> Iterable<Deferred<T>>.awaitFirstWithDeferred(
|
suspend fun <T> Iterable<Deferred<T>>.awaitFirstWithDeferred(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
cancelOnResult: Boolean = true
|
cancelOnResult: Boolean = true
|
||||||
): Pair<Deferred<T>, T> = suspendCoroutine<Pair<Deferred<T>, T>> { continuation ->
|
): Pair<Deferred<T>, T> {
|
||||||
scope.launch(SupervisorJob()) {
|
val resultDeferred = CompletableDeferred<Pair<Deferred<T>, T>>()
|
||||||
val scope = this
|
val scope = scope.LinkedSupervisorScope()
|
||||||
forEach {
|
forEach {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
continuation.resume(it to it.await())
|
resultDeferred.complete(it to it.await())
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.also {
|
return resultDeferred.await().also {
|
||||||
if (cancelOnResult) {
|
if (cancelOnResult) {
|
||||||
forEach {
|
forEach {
|
||||||
try {
|
runCatchingSafely { it.cancel() }
|
||||||
it.cancel()
|
|
||||||
} catch (e: IllegalStateException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,39 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlin.js.JsName
|
||||||
|
import kotlin.jvm.JvmName
|
||||||
|
|
||||||
|
inline fun <T, R> Flow<Flow<T>>.flatMap(
|
||||||
|
crossinline mapper: suspend (T) -> R
|
||||||
|
) = flow {
|
||||||
|
collect {
|
||||||
|
it.collect {
|
||||||
|
emit(mapper(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsName("flatMapIterable")
|
||||||
|
@JvmName("flatMapIterable")
|
||||||
|
inline fun <T, R> Flow<Iterable<T>>.flatMap(
|
||||||
|
crossinline mapper: suspend (T) -> R
|
||||||
|
) = map {
|
||||||
|
it.asFlow()
|
||||||
|
}.flatMap(mapper)
|
||||||
|
|
||||||
|
inline fun <T, R> Flow<Flow<T>>.flatMapNotNull(
|
||||||
|
crossinline mapper: suspend (T) -> R
|
||||||
|
) = flatMap(mapper).takeNotNull()
|
||||||
|
|
||||||
|
@JsName("flatMapNotNullIterable")
|
||||||
|
@JvmName("flatMapNotNullIterable")
|
||||||
|
inline fun <T, R> Flow<Iterable<T>>.flatMapNotNull(
|
||||||
|
crossinline mapper: suspend (T) -> R
|
||||||
|
) = flatMap(mapper).takeNotNull()
|
||||||
|
|
||||||
|
fun <T> Flow<Flow<T>>.flatten() = flatMap { it }
|
||||||
|
|
||||||
|
@JsName("flattenIterable")
|
||||||
|
@JvmName("flattenIterable")
|
||||||
|
fun <T> Flow<Iterable<T>>.flatten() = flatMap { it }
|
@@ -0,0 +1,6 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
|
||||||
|
fun <T> Flow<T>.takeNotNull() = mapNotNull { it }
|
||||||
|
fun <T> Flow<T>.filterNotNull() = takeNotNull()
|
50
coroutines/src/main/kotlin/FlowOnHierarchyChangeListener.kt
Normal file
50
coroutines/src/main/kotlin/FlowOnHierarchyChangeListener.kt
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [kotlinx.coroutines.flow.Flow]-based [android.view.ViewGroup.OnHierarchyChangeListener]
|
||||||
|
*
|
||||||
|
* @param recursive If set, any call of [onChildViewAdded] will check if child [View] is [ViewGroup] and subscribe to this
|
||||||
|
* [ViewGroup] too
|
||||||
|
* @param [_onChildViewAdded] Internal [MutableSharedFlow] which will be used to pass data to [onChildViewAdded] flow
|
||||||
|
* @param [_onChildViewRemoved] Internal [MutableSharedFlow] which will be used to pass data to [onChildViewRemoved] flow
|
||||||
|
*/
|
||||||
|
class FlowOnHierarchyChangeListener(
|
||||||
|
private val recursive: Boolean = false,
|
||||||
|
private val _onChildViewAdded: MutableSharedFlow<Pair<View, View>> = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE),
|
||||||
|
private val _onChildViewRemoved: MutableSharedFlow<Pair<View, View>> = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE)
|
||||||
|
) : ViewGroup.OnHierarchyChangeListener {
|
||||||
|
val onChildViewAdded = _onChildViewAdded.asSharedFlow()
|
||||||
|
val onChildViewRemoved = _onChildViewRemoved.asSharedFlow()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will emit data into [onChildViewAdded] flow. If [recursive] is true and [child] is [ViewGroup] will also
|
||||||
|
* subscribe to [child] hierarchy changes.
|
||||||
|
*
|
||||||
|
* Due to the fact that this method is not suspendable, [FlowOnHierarchyChangeListener] will use
|
||||||
|
* [MutableSharedFlow.tryEmit] to send data into [_onChildViewAdded]. That is why its default extraBufferCapacity is
|
||||||
|
* [Int.MAX_VALUE]
|
||||||
|
*/
|
||||||
|
override fun onChildViewAdded(parent: View, child: View) {
|
||||||
|
_onChildViewAdded.tryEmit(parent to child)
|
||||||
|
|
||||||
|
if (recursive && child is ViewGroup) {
|
||||||
|
child.setOnHierarchyChangeListener(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Just emit data into [onChildViewRemoved]
|
||||||
|
*
|
||||||
|
* Due to the fact that this method is not suspendable, [FlowOnHierarchyChangeListener] will use
|
||||||
|
* [MutableSharedFlow.tryEmit] to send data into [_onChildViewRemoved]. That is why its default extraBufferCapacity is
|
||||||
|
* [Int.MAX_VALUE]
|
||||||
|
*/
|
||||||
|
override fun onChildViewRemoved(parent: View, child: View) {
|
||||||
|
_onChildViewRemoved.tryEmit(parent to child)
|
||||||
|
}
|
||||||
|
}
|
17
coroutines/src/main/kotlin/RecursiveHierarchySubscriber.kt
Normal file
17
coroutines/src/main/kotlin/RecursiveHierarchySubscriber.kt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewGroup.OnHierarchyChangeListener
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use [ViewGroup.setOnHierarchyChangeListener] recursively for all available [ViewGroup]s starting with [this].
|
||||||
|
* This extension DO NOT guarantee that recursive subscription will happen after this method call
|
||||||
|
*/
|
||||||
|
fun ViewGroup.setOnHierarchyChangeListenerRecursively(
|
||||||
|
listener: OnHierarchyChangeListener
|
||||||
|
) {
|
||||||
|
setOnHierarchyChangeListener(listener)
|
||||||
|
(0 until childCount).forEach {
|
||||||
|
(getChildAt(it) as? ViewGroup) ?.setOnHierarchyChangeListenerRecursively(listener)
|
||||||
|
}
|
||||||
|
}
|
@@ -14,5 +14,5 @@ crypto_js_version=4.1.1
|
|||||||
# Project data
|
# Project data
|
||||||
|
|
||||||
group=dev.inmo
|
group=dev.inmo
|
||||||
version=0.12.2
|
version=0.14.0
|
||||||
android_code_version=141
|
android_code_version=161
|
||||||
|
@@ -1,36 +1,40 @@
|
|||||||
[versions]
|
[versions]
|
||||||
|
|
||||||
kt = "1.7.0"
|
kt = "1.7.20"
|
||||||
kt-serialization = "1.4.0"
|
kt-serialization = "1.4.1"
|
||||||
kt-coroutines = "1.6.4"
|
kt-coroutines = "1.6.4"
|
||||||
|
|
||||||
jb-compose = "1.2.0-alpha01-dev755"
|
jb-compose = "1.2.1"
|
||||||
jb-exposed = "0.39.2"
|
jb-exposed = "0.40.1"
|
||||||
jb-dokka = "1.7.0"
|
jb-dokka = "1.7.20"
|
||||||
|
|
||||||
klock = "3.0.0"
|
klock = "3.3.1"
|
||||||
uuid = "0.5.0"
|
uuid = "0.5.0"
|
||||||
|
|
||||||
ktor = "2.1.0"
|
ktor = "2.1.3"
|
||||||
|
|
||||||
gh-release = "2.4.1"
|
gh-release = "2.4.1"
|
||||||
|
|
||||||
|
koin = "3.2.2"
|
||||||
|
|
||||||
android-gradle = "7.2.2"
|
android-gradle = "7.2.2"
|
||||||
dexcount = "3.1.0"
|
dexcount = "3.1.0"
|
||||||
|
|
||||||
android-coreKtx = "1.8.0"
|
android-coreKtx = "1.9.0"
|
||||||
android-recyclerView = "1.2.1"
|
android-recyclerView = "1.2.1"
|
||||||
android-appCompat = "1.4.2"
|
android-appCompat = "1.5.1"
|
||||||
|
android-fragment = "1.5.3"
|
||||||
android-espresso = "3.4.0"
|
android-espresso = "3.4.0"
|
||||||
android-test = "1.1.3"
|
android-test = "1.1.3"
|
||||||
|
|
||||||
android-props-minSdk = "21"
|
android-props-minSdk = "21"
|
||||||
android-props-compileSdk = "32"
|
android-props-compileSdk = "33"
|
||||||
android-props-buildTools = "32.0.0"
|
android-props-buildTools = "33.0.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
|
||||||
kt-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kt" }
|
kt-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kt" }
|
||||||
|
kt-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kt" }
|
||||||
|
|
||||||
kt-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kt-serialization" }
|
kt-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kt-serialization" }
|
||||||
kt-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kt-serialization" }
|
kt-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kt-serialization" }
|
||||||
@@ -59,6 +63,7 @@ ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negoti
|
|||||||
|
|
||||||
klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "klock" }
|
klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "klock" }
|
||||||
uuid = { module = "com.benasher44:uuid", version.ref = "uuid" }
|
uuid = { module = "com.benasher44:uuid", version.ref = "uuid" }
|
||||||
|
koin = { module = "io.insert-koin:koin-core", version.ref = "koin" }
|
||||||
|
|
||||||
|
|
||||||
jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-exposed" }
|
jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-exposed" }
|
||||||
@@ -67,6 +72,7 @@ jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-
|
|||||||
android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
|
android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
|
||||||
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }
|
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }
|
||||||
android-appCompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "android-appCompat" }
|
android-appCompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "android-appCompat" }
|
||||||
|
android-fragment = { module = "androidx.fragment:fragment", version.ref = "android-fragment" }
|
||||||
android-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" }
|
android-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" }
|
||||||
android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" }
|
android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" }
|
||||||
|
|
||||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
28
koin/build.gradle
Normal file
28
koin/build.gradle
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
|
id "com.android.library"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api libs.koin
|
||||||
|
api libs.uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jvmMain {
|
||||||
|
dependencies {
|
||||||
|
api libs.kt.reflect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
androidMain {
|
||||||
|
dependencies {
|
||||||
|
api libs.kt.reflect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
koin/src/commonMain/kotlin/FactoryWithRandomQualifier.kt
Normal file
12
koin/src/commonMain/kotlin/FactoryWithRandomQualifier.kt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package dev.inmo.micro_utils.koin
|
||||||
|
|
||||||
|
import org.koin.core.definition.Definition
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will be useful in case you need to declare some singles with one type several types, but need to separate them and do
|
||||||
|
* not care about how :)
|
||||||
|
*/
|
||||||
|
inline fun <reified T : Any> Module.factoryWithRandomQualifier(
|
||||||
|
noinline definition: Definition<T>
|
||||||
|
) = factory(RandomQualifier(), definition)
|
12
koin/src/commonMain/kotlin/FactoryWithStringQualifier.kt
Normal file
12
koin/src/commonMain/kotlin/FactoryWithStringQualifier.kt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package dev.inmo.micro_utils.koin
|
||||||
|
|
||||||
|
import org.koin.core.definition.Definition
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
import org.koin.core.qualifier.StringQualifier
|
||||||
|
|
||||||
|
inline fun <reified T : Any> Module.factory(
|
||||||
|
qualifier: String,
|
||||||
|
noinline definition: Definition<T>
|
||||||
|
) = factory(StringQualifier(qualifier), definition)
|
||||||
|
|
||||||
|
|
8
koin/src/commonMain/kotlin/GetAllDistinct.kt
Normal file
8
koin/src/commonMain/kotlin/GetAllDistinct.kt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package dev.inmo.micro_utils.koin
|
||||||
|
|
||||||
|
import org.koin.core.Koin
|
||||||
|
import org.koin.core.scope.Scope
|
||||||
|
|
||||||
|
inline fun <reified T : Any> Scope.getAllDistinct() = getAll<T>().distinct()
|
||||||
|
inline fun <reified T : Any> Koin.getAllDistinct() = getAll<T>().distinct()
|
||||||
|
|
8
koin/src/commonMain/kotlin/GetAny.kt
Normal file
8
koin/src/commonMain/kotlin/GetAny.kt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package dev.inmo.micro_utils.koin
|
||||||
|
|
||||||
|
import org.koin.core.Koin
|
||||||
|
import org.koin.core.scope.Scope
|
||||||
|
|
||||||
|
inline fun <reified T : Any> Scope.getAny() = getAll<T>().first()
|
||||||
|
inline fun <reified T : Any> Koin.getAny() = getAll<T>().first()
|
||||||
|
|
6
koin/src/commonMain/kotlin/RandomQualifier.kt
Normal file
6
koin/src/commonMain/kotlin/RandomQualifier.kt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package dev.inmo.micro_utils.koin
|
||||||
|
|
||||||
|
import com.benasher44.uuid.uuid4
|
||||||
|
import org.koin.core.qualifier.StringQualifier
|
||||||
|
|
||||||
|
fun RandomQualifier(randomFun: () -> String = { uuid4().toString() }) = StringQualifier(randomFun())
|
13
koin/src/commonMain/kotlin/SingleWithRandomQualifier.kt
Normal file
13
koin/src/commonMain/kotlin/SingleWithRandomQualifier.kt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package dev.inmo.micro_utils.koin
|
||||||
|
|
||||||
|
import org.koin.core.definition.Definition
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will be useful in case you need to declare some singles with one type several types, but need to separate them and do
|
||||||
|
* not care about how :)
|
||||||
|
*/
|
||||||
|
inline fun <reified T : Any> Module.singleWithRandomQualifier(
|
||||||
|
createdAtStart: Boolean = false,
|
||||||
|
noinline definition: Definition<T>
|
||||||
|
) = single(RandomQualifier(), createdAtStart, definition)
|
12
koin/src/commonMain/kotlin/SingleWithStringQualifier.kt
Normal file
12
koin/src/commonMain/kotlin/SingleWithStringQualifier.kt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package dev.inmo.micro_utils.koin
|
||||||
|
|
||||||
|
import org.koin.core.definition.Definition
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
import org.koin.core.qualifier.StringQualifier
|
||||||
|
|
||||||
|
inline fun <reified T : Any> Module.single(
|
||||||
|
qualifier: String,
|
||||||
|
createdAtStart: Boolean = false,
|
||||||
|
noinline definition: Definition<T>
|
||||||
|
) = single(StringQualifier(qualifier), createdAtStart, definition)
|
||||||
|
|
27
koin/src/jvmMain/kotlin/FactoryWithBinds.kt
Normal file
27
koin/src/jvmMain/kotlin/FactoryWithBinds.kt
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package dev.inmo.micro_utils.koin
|
||||||
|
|
||||||
|
import org.koin.core.definition.Definition
|
||||||
|
import org.koin.core.instance.InstanceFactory
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
import org.koin.core.qualifier.Qualifier
|
||||||
|
import org.koin.dsl.binds
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.full.allSuperclasses
|
||||||
|
|
||||||
|
|
||||||
|
inline fun <reified T : Any> Module.factoryWithBinds(
|
||||||
|
qualifier: Qualifier? = null,
|
||||||
|
bindFilter: (KClass<*>) -> Boolean = { true },
|
||||||
|
noinline definition: Definition<T>
|
||||||
|
): Pair<Module, InstanceFactory<*>> {
|
||||||
|
return factory(qualifier, definition) binds (T::class.allSuperclasses.filter(bindFilter).toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : Any> Module.factoryWithBinds(
|
||||||
|
qualifier: String,
|
||||||
|
bindFilter: (KClass<*>) -> Boolean = { true },
|
||||||
|
noinline definition: Definition<T>
|
||||||
|
): Pair<Module, InstanceFactory<*>> {
|
||||||
|
return factory(qualifier, definition) binds (T::class.allSuperclasses.filter(bindFilter).toTypedArray())
|
||||||
|
}
|
||||||
|
|
@@ -0,0 +1,13 @@
|
|||||||
|
package dev.inmo.micro_utils.koin
|
||||||
|
|
||||||
|
import org.koin.core.definition.Definition
|
||||||
|
import org.koin.core.instance.InstanceFactory
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
inline fun <reified T : Any> Module.factoryWithRandomQualifierAndBinds(
|
||||||
|
bindFilter: (KClass<*>) -> Boolean = { true },
|
||||||
|
noinline definition: Definition<T>
|
||||||
|
): Pair<Module, InstanceFactory<*>> {
|
||||||
|
return factoryWithBinds(RandomQualifier(), bindFilter, definition)
|
||||||
|
}
|
30
koin/src/jvmMain/kotlin/SignleWithBinds.kt
Normal file
30
koin/src/jvmMain/kotlin/SignleWithBinds.kt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package dev.inmo.micro_utils.koin
|
||||||
|
|
||||||
|
import org.koin.core.definition.Definition
|
||||||
|
import org.koin.core.instance.InstanceFactory
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
import org.koin.core.qualifier.Qualifier
|
||||||
|
import org.koin.dsl.binds
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.full.allSuperclasses
|
||||||
|
|
||||||
|
|
||||||
|
inline fun <reified T : Any> Module.singleWithBinds(
|
||||||
|
qualifier: Qualifier? = null,
|
||||||
|
createdAtStart: Boolean = false,
|
||||||
|
bindFilter: (KClass<*>) -> Boolean = { true },
|
||||||
|
noinline definition: Definition<T>
|
||||||
|
): Pair<Module, InstanceFactory<*>> {
|
||||||
|
return single(qualifier, createdAtStart, definition) binds (T::class.allSuperclasses.filter(bindFilter).toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
inline fun <reified T : Any> Module.singleWithBinds(
|
||||||
|
qualifier: String,
|
||||||
|
createdAtStart: Boolean = false,
|
||||||
|
bindFilter: (KClass<*>) -> Boolean = { true },
|
||||||
|
noinline definition: Definition<T>
|
||||||
|
): Pair<Module, InstanceFactory<*>> {
|
||||||
|
return single(qualifier, createdAtStart, definition) binds (T::class.allSuperclasses.filter(bindFilter).toTypedArray())
|
||||||
|
}
|
||||||
|
|
14
koin/src/jvmMain/kotlin/SingleWithBindsAndRandomQualifier.kt
Normal file
14
koin/src/jvmMain/kotlin/SingleWithBindsAndRandomQualifier.kt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package dev.inmo.micro_utils.koin
|
||||||
|
|
||||||
|
import org.koin.core.definition.Definition
|
||||||
|
import org.koin.core.instance.InstanceFactory
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
inline fun <reified T : Any> Module.singleWithRandomQualifierAndBinds(
|
||||||
|
createdAtStart: Boolean = false,
|
||||||
|
bindFilter: (KClass<*>) -> Boolean = { true },
|
||||||
|
noinline definition: Definition<T>
|
||||||
|
): Pair<Module, InstanceFactory<*>> {
|
||||||
|
return singleWithBinds(RandomQualifier(), createdAtStart, bindFilter, definition)
|
||||||
|
}
|
1
koin/src/main/AndroidManifest.xml
Normal file
1
koin/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest package="dev.inmo.micro_utils.koin"/>
|
@@ -0,0 +1,9 @@
|
|||||||
|
package dev.inmo.micro_utils.ktor.client
|
||||||
|
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.statement.HttpResponse
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
|
||||||
|
suspend inline fun <reified T : Any> HttpResponse.bodyOrNull() = takeIf {
|
||||||
|
status == HttpStatusCode.OK
|
||||||
|
} ?.body<T>()
|
@@ -1,82 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.ktor.client
|
|
||||||
|
|
||||||
import dev.inmo.micro_utils.coroutines.runCatchingSafely
|
|
||||||
import dev.inmo.micro_utils.coroutines.safely
|
|
||||||
import dev.inmo.micro_utils.ktor.common.*
|
|
||||||
import io.ktor.client.HttpClient
|
|
||||||
import io.ktor.client.plugins.pluginOrNull
|
|
||||||
import io.ktor.client.plugins.websocket.WebSockets
|
|
||||||
import io.ktor.client.plugins.websocket.ws
|
|
||||||
import io.ktor.client.request.HttpRequestBuilder
|
|
||||||
import io.ktor.websocket.Frame
|
|
||||||
import io.ktor.websocket.readBytes
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.serialization.DeserializationStrategy
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
|
|
||||||
* connection. Must return true in case if must be reconnected. By default always reconnecting
|
|
||||||
*/
|
|
||||||
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
|
|
||||||
inline fun <T> HttpClient.createStandardWebsocketFlow(
|
|
||||||
url: String,
|
|
||||||
crossinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
|
|
||||||
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
|
||||||
crossinline conversation: suspend (StandardKtorSerialInputData) -> T
|
|
||||||
): Flow<T> {
|
|
||||||
pluginOrNull(WebSockets) ?: error("Plugin $WebSockets must be installed for using createStandardWebsocketFlow")
|
|
||||||
|
|
||||||
val correctedUrl = url.asCorrectWebSocketUrl
|
|
||||||
|
|
||||||
return channelFlow {
|
|
||||||
do {
|
|
||||||
val reconnect = runCatchingSafely {
|
|
||||||
ws(correctedUrl, requestBuilder) {
|
|
||||||
for (received in incoming) {
|
|
||||||
when (received) {
|
|
||||||
is Frame.Binary -> send(conversation(received.data))
|
|
||||||
else -> {
|
|
||||||
close()
|
|
||||||
return@ws
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checkReconnection(null)
|
|
||||||
}.getOrElse { e ->
|
|
||||||
checkReconnection(e).also {
|
|
||||||
if (!it) {
|
|
||||||
close(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (reconnect && isActive)
|
|
||||||
|
|
||||||
if (isActive) {
|
|
||||||
safely {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
|
|
||||||
* connection. Must return true in case if must be reconnected. By default always reconnecting
|
|
||||||
*/
|
|
||||||
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
|
|
||||||
inline fun <T> HttpClient.createStandardWebsocketFlow(
|
|
||||||
url: String,
|
|
||||||
deserializer: DeserializationStrategy<T>,
|
|
||||||
crossinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
|
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
|
||||||
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
|
||||||
) = createStandardWebsocketFlow(
|
|
||||||
url,
|
|
||||||
checkReconnection,
|
|
||||||
requestBuilder
|
|
||||||
) {
|
|
||||||
serialFormat.decodeDefault(deserializer, it)
|
|
||||||
}
|
|
@@ -1,260 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.ktor.client
|
|
||||||
|
|
||||||
import dev.inmo.micro_utils.common.MPPFile
|
|
||||||
import dev.inmo.micro_utils.common.filename
|
|
||||||
import dev.inmo.micro_utils.ktor.common.*
|
|
||||||
import io.ktor.client.HttpClient
|
|
||||||
import io.ktor.client.call.body
|
|
||||||
import io.ktor.client.request.*
|
|
||||||
import io.ktor.client.request.forms.*
|
|
||||||
import io.ktor.client.statement.readBytes
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.utils.io.core.ByteReadPacket
|
|
||||||
import kotlinx.serialization.*
|
|
||||||
|
|
||||||
@Deprecated("This class will be removed soon. It is now recommended to use built-in ktor features instead")
|
|
||||||
class UnifiedRequester(
|
|
||||||
val client: HttpClient = HttpClient(),
|
|
||||||
val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
|
||||||
) {
|
|
||||||
suspend fun <ResultType> uniget(
|
|
||||||
url: String,
|
|
||||||
resultDeserializer: DeserializationStrategy<ResultType>
|
|
||||||
): ResultType = client.uniget(url, resultDeserializer, serialFormat)
|
|
||||||
|
|
||||||
fun <T> encodeUrlQueryValue(
|
|
||||||
serializationStrategy: SerializationStrategy<T>,
|
|
||||||
value: T
|
|
||||||
) = serializationStrategy.encodeUrlQueryValue(
|
|
||||||
value,
|
|
||||||
serialFormat
|
|
||||||
)
|
|
||||||
|
|
||||||
suspend fun <BodyType, ResultType> unipost(
|
|
||||||
url: String,
|
|
||||||
bodyInfo: Pair<SerializationStrategy<BodyType>, BodyType>,
|
|
||||||
resultDeserializer: DeserializationStrategy<ResultType>
|
|
||||||
) = client.unipost(url, bodyInfo, resultDeserializer, serialFormat)
|
|
||||||
|
|
||||||
suspend fun <ResultType> unimultipart(
|
|
||||||
url: String,
|
|
||||||
filename: String,
|
|
||||||
inputProvider: InputProvider,
|
|
||||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
|
||||||
mimetype: String = "*/*",
|
|
||||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
|
||||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
|
||||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
|
||||||
): ResultType = client.unimultipart(url, filename, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat)
|
|
||||||
|
|
||||||
suspend fun <BodyType, ResultType> unimultipart(
|
|
||||||
url: String,
|
|
||||||
filename: String,
|
|
||||||
inputProvider: InputProvider,
|
|
||||||
otherData: Pair<SerializationStrategy<BodyType>, BodyType>,
|
|
||||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
|
||||||
mimetype: String = "*/*",
|
|
||||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
|
||||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
|
||||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
|
||||||
): ResultType = client.unimultipart(url, filename, otherData, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat)
|
|
||||||
|
|
||||||
suspend fun <ResultType> unimultipart(
|
|
||||||
url: String,
|
|
||||||
mppFile: MPPFile,
|
|
||||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
|
||||||
mimetype: String = "*/*",
|
|
||||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
|
||||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
|
||||||
requestBuilder: HttpRequestBuilder.() -> Unit = {}
|
|
||||||
): ResultType = client.unimultipart(
|
|
||||||
url, mppFile, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat
|
|
||||||
)
|
|
||||||
|
|
||||||
suspend fun <BodyType, ResultType> unimultipart(
|
|
||||||
url: String,
|
|
||||||
mppFile: MPPFile,
|
|
||||||
otherData: Pair<SerializationStrategy<BodyType>, BodyType>,
|
|
||||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
|
||||||
mimetype: String = "*/*",
|
|
||||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
|
||||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
|
||||||
requestBuilder: HttpRequestBuilder.() -> Unit = {}
|
|
||||||
): ResultType = client.unimultipart(
|
|
||||||
url, mppFile, otherData, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat
|
|
||||||
)
|
|
||||||
|
|
||||||
fun <T> createStandardWebsocketFlow(
|
|
||||||
url: String,
|
|
||||||
checkReconnection: suspend (Throwable?) -> Boolean,
|
|
||||||
deserializer: DeserializationStrategy<T>,
|
|
||||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
|
||||||
) = client.createStandardWebsocketFlow(url, deserializer, checkReconnection, serialFormat, requestBuilder)
|
|
||||||
|
|
||||||
fun <T> createStandardWebsocketFlow(
|
|
||||||
url: String,
|
|
||||||
deserializer: DeserializationStrategy<T>,
|
|
||||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
|
||||||
) = createStandardWebsocketFlow(url, { true }, deserializer, requestBuilder)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("This property will be removed soon. It is now recommended to use built-in ktor features instead")
|
|
||||||
val defaultRequester = UnifiedRequester()
|
|
||||||
|
|
||||||
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
|
|
||||||
suspend fun <ResultType> HttpClient.uniget(
|
|
||||||
url: String,
|
|
||||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
|
||||||
) = get(url).let {
|
|
||||||
serialFormat.decodeDefault(resultDeserializer, it.body<StandardKtorSerialInputData>())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
|
|
||||||
fun <T> SerializationStrategy<T>.encodeUrlQueryValue(
|
|
||||||
value: T,
|
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
|
||||||
) = serialFormat.encodeHex(
|
|
||||||
this,
|
|
||||||
value
|
|
||||||
)
|
|
||||||
|
|
||||||
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
|
|
||||||
suspend fun <BodyType, ResultType> HttpClient.unipost(
|
|
||||||
url: String,
|
|
||||||
bodyInfo: Pair<SerializationStrategy<BodyType>, BodyType>,
|
|
||||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
|
||||||
) = post(url) {
|
|
||||||
setBody(
|
|
||||||
serialFormat.encodeDefault(bodyInfo.first, bodyInfo.second)
|
|
||||||
)
|
|
||||||
}.let {
|
|
||||||
serialFormat.decodeDefault(resultDeserializer, it.body<StandardKtorSerialInputData>())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
|
|
||||||
suspend fun <ResultType> HttpClient.unimultipart(
|
|
||||||
url: String,
|
|
||||||
filename: String,
|
|
||||||
inputProvider: InputProvider,
|
|
||||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
|
||||||
mimetype: String = "*/*",
|
|
||||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
|
||||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
|
||||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
|
||||||
): ResultType = submitFormWithBinaryData(
|
|
||||||
url,
|
|
||||||
formData = formData {
|
|
||||||
append(
|
|
||||||
"bytes",
|
|
||||||
inputProvider,
|
|
||||||
Headers.build {
|
|
||||||
append(HttpHeaders.ContentType, mimetype)
|
|
||||||
append(HttpHeaders.ContentDisposition, "filename=\"$filename\"")
|
|
||||||
dataHeadersBuilder()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
additionalParametersBuilder()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
requestBuilder()
|
|
||||||
}.let { serialFormat.decodeDefault(resultDeserializer, it.body<StandardKtorSerialInputData>()) }
|
|
||||||
|
|
||||||
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
|
|
||||||
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
|
|
||||||
url: String,
|
|
||||||
filename: String,
|
|
||||||
otherData: Pair<SerializationStrategy<BodyType>, BodyType>,
|
|
||||||
inputProvider: InputProvider,
|
|
||||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
|
||||||
mimetype: String = "*/*",
|
|
||||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
|
||||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
|
||||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
|
||||||
): ResultType = unimultipart(
|
|
||||||
url,
|
|
||||||
filename,
|
|
||||||
inputProvider,
|
|
||||||
resultDeserializer,
|
|
||||||
mimetype,
|
|
||||||
additionalParametersBuilder = {
|
|
||||||
val serialized = serialFormat.encodeDefault(otherData.first, otherData.second)
|
|
||||||
append(
|
|
||||||
"data",
|
|
||||||
InputProvider(serialized.size.toLong()) {
|
|
||||||
ByteReadPacket(serialized)
|
|
||||||
},
|
|
||||||
Headers.build {
|
|
||||||
append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType)
|
|
||||||
append(HttpHeaders.ContentDisposition, "filename=data.bytes")
|
|
||||||
dataHeadersBuilder()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
additionalParametersBuilder()
|
|
||||||
},
|
|
||||||
dataHeadersBuilder,
|
|
||||||
requestBuilder,
|
|
||||||
serialFormat
|
|
||||||
)
|
|
||||||
|
|
||||||
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
|
|
||||||
suspend fun <ResultType> HttpClient.unimultipart(
|
|
||||||
url: String,
|
|
||||||
mppFile: MPPFile,
|
|
||||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
|
||||||
mimetype: String = "*/*",
|
|
||||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
|
||||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
|
||||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
|
||||||
): ResultType = unimultipart(
|
|
||||||
url,
|
|
||||||
mppFile.filename.string,
|
|
||||||
mppFile.inputProvider(),
|
|
||||||
resultDeserializer,
|
|
||||||
mimetype,
|
|
||||||
additionalParametersBuilder,
|
|
||||||
dataHeadersBuilder,
|
|
||||||
requestBuilder,
|
|
||||||
serialFormat
|
|
||||||
)
|
|
||||||
|
|
||||||
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
|
|
||||||
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
|
|
||||||
url: String,
|
|
||||||
mppFile: MPPFile,
|
|
||||||
otherData: Pair<SerializationStrategy<BodyType>, BodyType>,
|
|
||||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
|
||||||
mimetype: String = "*/*",
|
|
||||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
|
||||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
|
||||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
|
||||||
): ResultType = unimultipart(
|
|
||||||
url,
|
|
||||||
mppFile,
|
|
||||||
resultDeserializer,
|
|
||||||
mimetype,
|
|
||||||
additionalParametersBuilder = {
|
|
||||||
val serialized = serialFormat.encodeDefault(otherData.first, otherData.second)
|
|
||||||
append(
|
|
||||||
"data",
|
|
||||||
InputProvider(serialized.size.toLong()) {
|
|
||||||
ByteReadPacket(serialized)
|
|
||||||
},
|
|
||||||
Headers.build {
|
|
||||||
append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType)
|
|
||||||
append(HttpHeaders.ContentDisposition, "filename=data.bytes")
|
|
||||||
dataHeadersBuilder()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
additionalParametersBuilder()
|
|
||||||
},
|
|
||||||
dataHeadersBuilder,
|
|
||||||
requestBuilder,
|
|
||||||
serialFormat
|
|
||||||
)
|
|
@@ -9,11 +9,3 @@ expect suspend fun HttpClient.tempUpload(
|
|||||||
file: MPPFile,
|
file: MPPFile,
|
||||||
onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> }
|
onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> }
|
||||||
): TemporalFileId
|
): TemporalFileId
|
||||||
|
|
||||||
suspend fun UnifiedRequester.tempUpload(
|
|
||||||
fullTempUploadDraftPath: String,
|
|
||||||
file: MPPFile,
|
|
||||||
onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> }
|
|
||||||
): TemporalFileId = client.tempUpload(
|
|
||||||
fullTempUploadDraftPath, file, onUpload
|
|
||||||
)
|
|
||||||
|
@@ -1,61 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.ktor.server
|
|
||||||
|
|
||||||
import dev.inmo.micro_utils.coroutines.safely
|
|
||||||
import dev.inmo.micro_utils.ktor.common.*
|
|
||||||
import io.ktor.http.URLProtocol
|
|
||||||
import io.ktor.server.application.install
|
|
||||||
import io.ktor.server.application.pluginOrNull
|
|
||||||
import io.ktor.server.routing.Route
|
|
||||||
import io.ktor.server.routing.application
|
|
||||||
import io.ktor.server.websocket.*
|
|
||||||
import io.ktor.websocket.send
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.serialization.SerializationStrategy
|
|
||||||
|
|
||||||
@Deprecated("This method will be removed soon")
|
|
||||||
fun <T> Route.includeWebsocketHandling(
|
|
||||||
suburl: String,
|
|
||||||
flow: Flow<T>,
|
|
||||||
protocol: URLProtocol? = null,
|
|
||||||
converter: suspend WebSocketServerSession.(T) -> StandardKtorSerialInputData?
|
|
||||||
) {
|
|
||||||
application.apply {
|
|
||||||
pluginOrNull(WebSockets) ?: install(WebSockets)
|
|
||||||
}
|
|
||||||
webSocket(suburl, protocol ?.name) {
|
|
||||||
safely {
|
|
||||||
flow.collect {
|
|
||||||
converter(it) ?.let { data ->
|
|
||||||
send(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("This method will be removed soon")
|
|
||||||
fun <T> Route.includeWebsocketHandling(
|
|
||||||
suburl: String,
|
|
||||||
flow: Flow<T>,
|
|
||||||
serializer: SerializationStrategy<T>,
|
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
|
||||||
protocol: URLProtocol? = null,
|
|
||||||
filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null
|
|
||||||
) = includeWebsocketHandling(
|
|
||||||
suburl,
|
|
||||||
flow,
|
|
||||||
protocol,
|
|
||||||
converter = if (filter == null) {
|
|
||||||
{
|
|
||||||
serialFormat.encodeDefault(serializer, it)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
{
|
|
||||||
if (filter(it)) {
|
|
||||||
serialFormat.encodeDefault(serializer, it)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
@@ -0,0 +1,15 @@
|
|||||||
|
package dev.inmo.micro_utils.ktor.server
|
||||||
|
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.server.application.ApplicationCall
|
||||||
|
import io.ktor.server.response.respond
|
||||||
|
|
||||||
|
suspend inline fun <reified T : Any> ApplicationCall.respondOrNoContent(
|
||||||
|
data: T?
|
||||||
|
) {
|
||||||
|
if (data == null) {
|
||||||
|
respond(HttpStatusCode.NoContent)
|
||||||
|
} else {
|
||||||
|
respond(data)
|
||||||
|
}
|
||||||
|
}
|
@@ -19,106 +19,6 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.serialization.DeserializationStrategy
|
import kotlinx.serialization.DeserializationStrategy
|
||||||
import kotlinx.serialization.SerializationStrategy
|
import kotlinx.serialization.SerializationStrategy
|
||||||
|
|
||||||
@Deprecated("This class method will be removed soon. It is now recommended to use built-in ktor features instead")
|
|
||||||
class UnifiedRouter(
|
|
||||||
val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
|
||||||
val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
|
|
||||||
) {
|
|
||||||
fun <T> Route.includeWebsocketHandling(
|
|
||||||
suburl: String,
|
|
||||||
flow: Flow<T>,
|
|
||||||
serializer: SerializationStrategy<T>,
|
|
||||||
protocol: URLProtocol? = null,
|
|
||||||
filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null
|
|
||||||
) = includeWebsocketHandling(suburl, flow, serializer, serialFormat, protocol, filter)
|
|
||||||
|
|
||||||
suspend fun <T> PipelineContext<*, ApplicationCall>.unianswer(
|
|
||||||
answerSerializer: SerializationStrategy<T>,
|
|
||||||
answer: T
|
|
||||||
) {
|
|
||||||
call.respondBytes (
|
|
||||||
serialFormat.encodeDefault(answerSerializer, answer),
|
|
||||||
serialFormatContentType
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun <T> PipelineContext<*, ApplicationCall>.uniload(
|
|
||||||
deserializer: DeserializationStrategy<T>
|
|
||||||
) = safely {
|
|
||||||
serialFormat.decodeDefault(
|
|
||||||
deserializer,
|
|
||||||
call.receive()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun PipelineContext<*, ApplicationCall>.getParameterOrSendError(
|
|
||||||
field: String
|
|
||||||
) = call.parameters[field].also {
|
|
||||||
if (it == null) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, "Request must contains $field")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun PipelineContext<*, ApplicationCall>.getQueryParameter(
|
|
||||||
field: String
|
|
||||||
) = call.request.queryParameters[field]
|
|
||||||
|
|
||||||
suspend fun PipelineContext<*, ApplicationCall>.getQueryParameterOrSendError(
|
|
||||||
field: String
|
|
||||||
) = getQueryParameter(field).also {
|
|
||||||
if (it == null) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> PipelineContext<*, ApplicationCall>.decodeUrlQueryValue(
|
|
||||||
field: String,
|
|
||||||
deserializer: DeserializationStrategy<T>
|
|
||||||
) = getQueryParameter(field) ?.let {
|
|
||||||
serialFormat.decodeHex(
|
|
||||||
deserializer,
|
|
||||||
it
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun <T> PipelineContext<*, ApplicationCall>.decodeUrlQueryValueOrSendError(
|
|
||||||
field: String,
|
|
||||||
deserializer: DeserializationStrategy<T>
|
|
||||||
) = decodeUrlQueryValue(field, deserializer).also {
|
|
||||||
if (it == null) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val default
|
|
||||||
get() = defaultUnifiedRouter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val defaultUnifiedRouter = UnifiedRouter()
|
|
||||||
|
|
||||||
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
|
|
||||||
suspend fun <T> ApplicationCall.unianswer(
|
|
||||||
answerSerializer: SerializationStrategy<T>,
|
|
||||||
answer: T
|
|
||||||
) {
|
|
||||||
respondBytes (
|
|
||||||
standardKtorSerialFormat.encodeDefault(answerSerializer, answer),
|
|
||||||
standardKtorSerialFormatContentType
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
|
|
||||||
suspend fun <T> ApplicationCall.uniload(
|
|
||||||
deserializer: DeserializationStrategy<T>
|
|
||||||
) = safely {
|
|
||||||
standardKtorSerialFormat.decodeDefault(
|
|
||||||
deserializer,
|
|
||||||
receive()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun ApplicationCall.uniloadMultipart(
|
suspend fun ApplicationCall.uniloadMultipart(
|
||||||
onFormItem: (PartData.FormItem) -> Unit = {},
|
onFormItem: (PartData.FormItem) -> Unit = {},
|
||||||
onCustomFileItem: (PartData.FileItem) -> Unit = {},
|
onCustomFileItem: (PartData.FileItem) -> Unit = {},
|
||||||
@@ -146,82 +46,6 @@ suspend fun ApplicationCall.uniloadMultipart(
|
|||||||
resultInput ?: error("Bytes has not been received")
|
resultInput ?: error("Bytes has not been received")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
|
|
||||||
suspend fun <T> ApplicationCall.uniloadMultipart(
|
|
||||||
deserializer: DeserializationStrategy<T>,
|
|
||||||
onFormItem: (PartData.FormItem) -> Unit = {},
|
|
||||||
onCustomFileItem: (PartData.FileItem) -> Unit = {},
|
|
||||||
onBinaryChannelItem: (PartData.BinaryChannelItem) -> Unit = {},
|
|
||||||
onBinaryContent: (PartData.BinaryItem) -> Unit = {}
|
|
||||||
): Pair<Input, T> {
|
|
||||||
var data: Optional<T>? = null
|
|
||||||
val resultInput = uniloadMultipart(
|
|
||||||
onFormItem,
|
|
||||||
{
|
|
||||||
if (it.name == "data") {
|
|
||||||
data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional
|
|
||||||
} else {
|
|
||||||
onCustomFileItem(it)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onBinaryChannelItem,
|
|
||||||
onBinaryContent
|
|
||||||
)
|
|
||||||
|
|
||||||
val completeData = data ?: error("Data has not been received")
|
|
||||||
return resultInput to (completeData.dataOrNull().let { it as T })
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
|
|
||||||
suspend fun <T> ApplicationCall.uniloadMultipartFile(
|
|
||||||
deserializer: DeserializationStrategy<T>,
|
|
||||||
onFormItem: (PartData.FormItem) -> Unit = {},
|
|
||||||
onCustomFileItem: (PartData.FileItem) -> Unit = {},
|
|
||||||
onBinaryChannelItem: (PartData.BinaryChannelItem) -> Unit = {},
|
|
||||||
onBinaryContent: (PartData.BinaryItem) -> Unit = {},
|
|
||||||
) = safely {
|
|
||||||
val multipartData = receiveMultipart()
|
|
||||||
|
|
||||||
var resultInput: MPPFile? = null
|
|
||||||
var data: Optional<T>? = null
|
|
||||||
|
|
||||||
multipartData.forEachPart {
|
|
||||||
when (it) {
|
|
||||||
is PartData.FormItem -> onFormItem(it)
|
|
||||||
is PartData.FileItem -> {
|
|
||||||
when (it.name) {
|
|
||||||
"bytes" -> {
|
|
||||||
val name = FileName(it.originalFileName ?: error("File name is unknown for default part"))
|
|
||||||
resultInput = MPPFile.createTempFile(
|
|
||||||
name.nameWithoutExtension.let {
|
|
||||||
var resultName = it
|
|
||||||
while (resultName.length < 3) {
|
|
||||||
resultName += "_"
|
|
||||||
}
|
|
||||||
resultName
|
|
||||||
},
|
|
||||||
".${name.extension}"
|
|
||||||
).apply {
|
|
||||||
outputStream().use { fileStream ->
|
|
||||||
it.streamProvider().use {
|
|
||||||
it.copyTo(fileStream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"data" -> data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional
|
|
||||||
else -> onCustomFileItem(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is PartData.BinaryItem -> onBinaryContent(it)
|
|
||||||
is PartData.BinaryChannelItem -> onBinaryChannelItem(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val completeData = data ?: error("Data has not been received")
|
|
||||||
(resultInput ?: error("Bytes has not been received")) to (completeData.dataOrNull().let { it as T })
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun ApplicationCall.uniloadMultipartFile(
|
suspend fun ApplicationCall.uniloadMultipartFile(
|
||||||
onFormItem: (PartData.FormItem) -> Unit = {},
|
onFormItem: (PartData.FormItem) -> Unit = {},
|
||||||
onCustomFileItem: (PartData.FileItem) -> Unit = {},
|
onCustomFileItem: (PartData.FileItem) -> Unit = {},
|
||||||
@@ -285,24 +109,3 @@ suspend fun ApplicationCall.getQueryParameterOrSendError(
|
|||||||
respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
|
respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
|
|
||||||
fun <T> ApplicationCall.decodeUrlQueryValue(
|
|
||||||
field: String,
|
|
||||||
deserializer: DeserializationStrategy<T>
|
|
||||||
) = getQueryParameter(field) ?.let {
|
|
||||||
standardKtorSerialFormat.decodeHex(
|
|
||||||
deserializer,
|
|
||||||
it
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
|
|
||||||
suspend fun <T> ApplicationCall.decodeUrlQueryValueOrSendError(
|
|
||||||
field: String,
|
|
||||||
deserializer: DeserializationStrategy<T>
|
|
||||||
) = decodeUrlQueryValue(field, deserializer).also {
|
|
||||||
if (it == null) {
|
|
||||||
respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -26,7 +26,6 @@ import java.nio.file.attribute.FileTime
|
|||||||
|
|
||||||
class TemporalFilesRoutingConfigurator(
|
class TemporalFilesRoutingConfigurator(
|
||||||
private val subpath: String = DefaultTemporalFilesSubPath,
|
private val subpath: String = DefaultTemporalFilesSubPath,
|
||||||
private val unifiedRouter: UnifiedRouter = UnifiedRouter.default,
|
|
||||||
private val temporalFilesUtilizer: TemporalFilesUtilizer = TemporalFilesUtilizer
|
private val temporalFilesUtilizer: TemporalFilesUtilizer = TemporalFilesUtilizer
|
||||||
) : ApplicationRoutingConfigurator.Element {
|
) : ApplicationRoutingConfigurator.Element {
|
||||||
interface TemporalFilesUtilizer {
|
interface TemporalFilesUtilizer {
|
||||||
@@ -80,42 +79,40 @@ class TemporalFilesRoutingConfigurator(
|
|||||||
|
|
||||||
override fun Route.invoke() {
|
override fun Route.invoke() {
|
||||||
post(subpath) {
|
post(subpath) {
|
||||||
unifiedRouter.apply {
|
val multipart = call.receiveMultipart()
|
||||||
val multipart = call.receiveMultipart()
|
|
||||||
|
|
||||||
var fileInfo: Pair<TemporalFileId, MPPFile>? = null
|
var fileInfo: Pair<TemporalFileId, MPPFile>? = null
|
||||||
var part = multipart.readPart()
|
var part = multipart.readPart()
|
||||||
|
|
||||||
while (part != null) {
|
while (part != null) {
|
||||||
if (part is PartData.FileItem) {
|
if (part is PartData.FileItem) {
|
||||||
break
|
break
|
||||||
}
|
|
||||||
part = multipart.readPart()
|
|
||||||
}
|
}
|
||||||
|
part = multipart.readPart()
|
||||||
part ?.let {
|
|
||||||
if (it is PartData.FileItem) {
|
|
||||||
val fileId = TemporalFileId(uuid4().toString())
|
|
||||||
val fileName = it.originalFileName ?.let { FileName(it) } ?: return@let
|
|
||||||
fileInfo = fileId to File.createTempFile(fileId.string, ".${fileName.extension}").apply {
|
|
||||||
outputStream().use { outputStream ->
|
|
||||||
it.streamProvider().use {
|
|
||||||
it.copyTo(outputStream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deleteOnExit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInfo ?.also { (fileId, file) ->
|
|
||||||
temporalFilesMutex.withLock {
|
|
||||||
temporalFilesMap[fileId] = file
|
|
||||||
}
|
|
||||||
call.respondText(fileId.string)
|
|
||||||
launchSafelyWithoutExceptions { filesFlow.emit(fileId) }
|
|
||||||
} ?: call.respond(HttpStatusCode.BadRequest)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
part ?.let {
|
||||||
|
if (it is PartData.FileItem) {
|
||||||
|
val fileId = TemporalFileId(uuid4().toString())
|
||||||
|
val fileName = it.originalFileName ?.let { FileName(it) } ?: return@let
|
||||||
|
fileInfo = fileId to File.createTempFile(fileId.string, ".${fileName.extension}").apply {
|
||||||
|
outputStream().use { outputStream ->
|
||||||
|
it.streamProvider().use {
|
||||||
|
it.copyTo(outputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deleteOnExit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInfo ?.also { (fileId, file) ->
|
||||||
|
temporalFilesMutex.withLock {
|
||||||
|
temporalFilesMap[fileId] = file
|
||||||
|
}
|
||||||
|
call.respondText(fileId.string)
|
||||||
|
launchSafelyWithoutExceptions { filesFlow.emit(fileId) }
|
||||||
|
} ?: call.respond(HttpStatusCode.BadRequest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2017,6 +2017,7 @@ internal val knownMimeTypes: Set<MimeType> = setOf(
|
|||||||
KnownMimeTypes.Chemical.XCml,
|
KnownMimeTypes.Chemical.XCml,
|
||||||
KnownMimeTypes.Chemical.XCsml,
|
KnownMimeTypes.Chemical.XCsml,
|
||||||
KnownMimeTypes.Chemical.XXyz,
|
KnownMimeTypes.Chemical.XXyz,
|
||||||
|
KnownMimeTypes.Image.Any,
|
||||||
KnownMimeTypes.Image.Bmp,
|
KnownMimeTypes.Image.Bmp,
|
||||||
KnownMimeTypes.Image.Cgm,
|
KnownMimeTypes.Image.Cgm,
|
||||||
KnownMimeTypes.Image.G3fax,
|
KnownMimeTypes.Image.G3fax,
|
||||||
|
@@ -33,21 +33,17 @@ data class PaginationResult<T>(
|
|||||||
results,
|
results,
|
||||||
(pagesNumber * size).toLong()
|
(pagesNumber * size).toLong()
|
||||||
)
|
)
|
||||||
@Deprecated("Replace with The other order of incoming parameters or objectsCount parameter")
|
|
||||||
constructor(
|
|
||||||
page: Int,
|
|
||||||
pagesNumber: Int,
|
|
||||||
results: List<T>,
|
|
||||||
size: Int
|
|
||||||
) : this(
|
|
||||||
page,
|
|
||||||
results,
|
|
||||||
pagesNumber,
|
|
||||||
size
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> emptyPaginationResult() = PaginationResult<T>(0, 0, emptyList(), 0L)
|
fun <T> emptyPaginationResult() = PaginationResult<T>(0, 0, emptyList(), 0L)
|
||||||
|
fun <T> emptyPaginationResult(
|
||||||
|
basePagination: Pagination
|
||||||
|
) = PaginationResult<T>(
|
||||||
|
basePagination.page,
|
||||||
|
basePagination.size,
|
||||||
|
emptyList(),
|
||||||
|
0L
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return New [PaginationResult] with [data] without checking of data sizes equality
|
* @return New [PaginationResult] with [data] without checking of data sizes equality
|
||||||
|
@@ -4,11 +4,7 @@ import org.jetbrains.exposed.sql.*
|
|||||||
|
|
||||||
fun Query.paginate(with: Pagination, orderBy: Pair<Expression<*>, SortOrder>? = null) = limit(
|
fun Query.paginate(with: Pagination, orderBy: Pair<Expression<*>, SortOrder>? = null) = limit(
|
||||||
with.size,
|
with.size,
|
||||||
(if (orderBy ?.second == SortOrder.DESC) {
|
with.firstIndex.toLong()
|
||||||
with.lastIndex
|
|
||||||
} else {
|
|
||||||
with.firstIndex
|
|
||||||
}).toLong()
|
|
||||||
).let {
|
).let {
|
||||||
if (orderBy != null) {
|
if (orderBy != null) {
|
||||||
it.orderBy(
|
it.orderBy(
|
||||||
|
@@ -33,6 +33,14 @@ open class WriteCRUDCacheRepo<ObjectType, IdType, InputValueType>(
|
|||||||
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
|
||||||
|
|
||||||
|
val createdObjectsFlowJob = parentRepo.newObjectsFlow.onEach {
|
||||||
|
kvCache.set(idGetter(it), it)
|
||||||
|
}.launchIn(scope)
|
||||||
|
|
||||||
|
val updatedObjectsFlowJob = parentRepo.updatedObjectsFlow.onEach {
|
||||||
|
kvCache.set(idGetter(it), it)
|
||||||
|
}.launchIn(scope)
|
||||||
|
|
||||||
val deletedObjectsFlowJob = parentRepo.deletedObjectsIdsFlow.onEach {
|
val deletedObjectsFlowJob = parentRepo.deletedObjectsIdsFlow.onEach {
|
||||||
kvCache.unset(it)
|
kvCache.unset(it)
|
||||||
}.launchIn(scope)
|
}.launchIn(scope)
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package dev.inmo.micro_utils.repos.cache
|
package dev.inmo.micro_utils.repos.cache
|
||||||
|
|
||||||
import dev.inmo.micro_utils.pagination.Pagination
|
import dev.inmo.micro_utils.pagination.*
|
||||||
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.KVCache
|
import dev.inmo.micro_utils.repos.cache.cache.KVCache
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -14,6 +13,16 @@ open class ReadKeyValueCacheRepo<Key,Value>(
|
|||||||
) : ReadKeyValueRepo<Key,Value> by parentRepo, CacheRepo {
|
) : ReadKeyValueRepo<Key,Value> by parentRepo, CacheRepo {
|
||||||
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)
|
||||||
|
|
||||||
|
override suspend fun values(pagination: Pagination, reversed: Boolean): PaginationResult<Value> {
|
||||||
|
return keys(pagination, reversed).let {
|
||||||
|
it.changeResultsUnchecked(
|
||||||
|
it.results.mapNotNull {
|
||||||
|
get(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <Key, Value> ReadKeyValueRepo<Key, Value>.cached(
|
fun <Key, Value> ReadKeyValueRepo<Key, Value>.cached(
|
||||||
|
@@ -1,9 +1,7 @@
|
|||||||
package dev.inmo.micro_utils.repos.cache
|
package dev.inmo.micro_utils.repos.cache
|
||||||
|
|
||||||
import dev.inmo.micro_utils.pagination.Pagination
|
import dev.inmo.micro_utils.pagination.*
|
||||||
import dev.inmo.micro_utils.pagination.PaginationResult
|
import dev.inmo.micro_utils.pagination.utils.*
|
||||||
import dev.inmo.micro_utils.pagination.utils.paginate
|
|
||||||
import dev.inmo.micro_utils.pagination.utils.reverse
|
|
||||||
import dev.inmo.micro_utils.repos.*
|
import dev.inmo.micro_utils.repos.*
|
||||||
import dev.inmo.micro_utils.repos.cache.cache.KVCache
|
import dev.inmo.micro_utils.repos.cache.cache.KVCache
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -15,18 +13,22 @@ open class ReadKeyValuesCacheRepo<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, CacheRepo {
|
||||||
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 kvCache.get(k) ?.paginate(
|
return getAll(k, reversed).paginate(
|
||||||
pagination.let { if (reversed) it.reverse(count(k)) else it }
|
pagination
|
||||||
) ?.let {
|
)
|
||||||
if (reversed) it.copy(results = it.results.reversed()) else it
|
|
||||||
} ?: parentRepo.get(k, pagination, reversed)
|
|
||||||
}
|
}
|
||||||
override suspend fun getAll(k: Key, reversed: Boolean): List<Value> {
|
override suspend fun getAll(k: Key, reversed: Boolean): List<Value> {
|
||||||
return kvCache.get(k) ?.let {
|
return kvCache.get(k) ?.let {
|
||||||
if (reversed) it.reversed() else it
|
if (reversed) it.reversed() else it
|
||||||
} ?: parentRepo.getAll(k, reversed)
|
} ?: parentRepo.getAll(k, reversed).also {
|
||||||
|
kvCache.set(k, it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
override suspend fun contains(k: Key, v: Value): Boolean = kvCache.get(k) ?.contains(v) ?: parentRepo.contains(k, v)
|
override suspend fun contains(k: Key, v: Value): Boolean = kvCache.get(k) ?.contains(v) ?: (parentRepo.contains(k, v).also {
|
||||||
|
if (it) {
|
||||||
|
kvCache.unset(k) // clear as invalid
|
||||||
|
}
|
||||||
|
})
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,9 +41,21 @@ open class KeyValuesCacheRepo<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, CacheRepo {
|
||||||
protected val onNewJob = parentRepo.onNewValue.onEach { kvCache.set(it.first, kvCache.get(it.first) ?.plus(it.second) ?: listOf(it.second)) }.launchIn(scope)
|
protected val onNewJob = parentRepo.onNewValue.onEach { (k, v) ->
|
||||||
protected val onRemoveJob = parentRepo.onValueRemoved.onEach { kvCache.set(it.first, kvCache.get(it.first) ?.minus(it.second) ?: return@onEach) }.launchIn(scope)
|
kvCache.set(
|
||||||
protected val onDataClearedJob = parentRepo.onDataCleared.onEach { kvCache.unset(it) }.launchIn(scope)
|
k,
|
||||||
|
kvCache.get(k) ?.plus(v) ?: return@onEach
|
||||||
|
)
|
||||||
|
}.launchIn(scope)
|
||||||
|
protected val onRemoveJob = parentRepo.onValueRemoved.onEach { (k, v) ->
|
||||||
|
kvCache.set(
|
||||||
|
k,
|
||||||
|
kvCache.get(k) ?.minus(v) ?: return@onEach
|
||||||
|
)
|
||||||
|
}.launchIn(scope)
|
||||||
|
protected val onDataClearedJob = parentRepo.onDataCleared.onEach {
|
||||||
|
kvCache.unset(it)
|
||||||
|
}.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <Key, Value> KeyValuesRepo<Key, Value>.cached(
|
fun <Key, Value> KeyValuesRepo<Key, Value>.cached(
|
||||||
|
@@ -25,6 +25,9 @@ open class SimpleKVCache<K, V>(
|
|||||||
kvParent.unset(it)
|
kvParent.unset(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
do {
|
||||||
|
val removed = cacheQueue.remove(k)
|
||||||
|
} while (removed)
|
||||||
cacheQueue.addLast(k)
|
cacheQueue.addLast(k)
|
||||||
kvParent.set(k, v)
|
kvParent.set(k, v)
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,138 @@
|
|||||||
|
package dev.inmo.micro_utils.repos
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.pagination.*
|
||||||
|
import dev.inmo.micro_utils.pagination.utils.doAllWithCurrentPaging
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read part of [KeyValueRepo]
|
||||||
|
*
|
||||||
|
* @param Key This type will be used as key in all operations related to searches of data
|
||||||
|
* @param Value This type will be used as returning data in most "get" operations
|
||||||
|
*/
|
||||||
|
interface ReadKeyValueRepo<Key, Value> : Repo {
|
||||||
|
/**
|
||||||
|
* @return Result [Value] in case when it is presented in repo by its [k] or null otherwise
|
||||||
|
*/
|
||||||
|
suspend fun get(k: Key): Value?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method should use sorted by [Key]s search and take the [PaginationResult]. By default, it should use
|
||||||
|
* ascending sort for [Key]s
|
||||||
|
*/
|
||||||
|
suspend fun values(pagination: Pagination, reversed: Boolean = false): PaginationResult<Value>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method should use sorted by [Key]s search and take the [PaginationResult]. By default, it should use
|
||||||
|
* ascending sort for [Key]s
|
||||||
|
*/
|
||||||
|
suspend fun keys(pagination: Pagination, reversed: Boolean = false): PaginationResult<Key>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method should use sorted by [Key]s search and take the [PaginationResult]. By default, it should use
|
||||||
|
* ascending sort for [Key]s
|
||||||
|
*
|
||||||
|
* @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>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if [key] is presented in current collection or false otherwise
|
||||||
|
*/
|
||||||
|
suspend fun contains(key: Key): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return count of all collection objects
|
||||||
|
*/
|
||||||
|
suspend fun count(): Long
|
||||||
|
}
|
||||||
|
typealias ReadStandardKeyValueRepo<Key,Value> = ReadKeyValueRepo<Key, Value>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write part of [KeyValueRepo]
|
||||||
|
*
|
||||||
|
* @param Key This type will be used as key in all operations related to changes of data
|
||||||
|
* @param Value This type will be used as incoming data in most operations
|
||||||
|
*/
|
||||||
|
interface WriteKeyValueRepo<Key, Value> : Repo {
|
||||||
|
/**
|
||||||
|
* This flow must emit data each time when data by [Key] has been changed with [set] method or in any other way
|
||||||
|
* excluding cases of data removing
|
||||||
|
*
|
||||||
|
* @see onValueRemoved
|
||||||
|
*/
|
||||||
|
val onNewValue: Flow<Pair<Key, Value>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This flow must emit data each time when data by [Key] has been removed with [unset]/[unsetWithValues] methods or
|
||||||
|
* in any other way
|
||||||
|
*
|
||||||
|
* @see onNewValue
|
||||||
|
*/
|
||||||
|
val onValueRemoved: Flow<Key>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will set as batch [toSet] data in current repo. Must pass the data which were successfully updated in repo to
|
||||||
|
* [onNewValue]
|
||||||
|
*/
|
||||||
|
suspend fun set(toSet: Map<Key, Value>)
|
||||||
|
/**
|
||||||
|
* Will unset as batch data with keys from [toUnset]. Must pass the [Key]s which were successfully removed in repo to
|
||||||
|
* [onValueRemoved]
|
||||||
|
*/
|
||||||
|
suspend fun unset(toUnset: List<Key>)
|
||||||
|
/**
|
||||||
|
* Will unset as batch data with values from [toUnset]. Must pass the [Key]s which were successfully removed in repo
|
||||||
|
* to [onValueRemoved]
|
||||||
|
*/
|
||||||
|
suspend fun unsetWithValues(toUnset: List<Value>)
|
||||||
|
}
|
||||||
|
typealias WriteStandardKeyValueRepo<Key,Value> = WriteKeyValueRepo<Key, Value>
|
||||||
|
|
||||||
|
suspend inline fun <Key, Value> WriteKeyValueRepo<Key, Value>.set(
|
||||||
|
vararg toSet: Pair<Key, Value>
|
||||||
|
) = set(toSet.toMap())
|
||||||
|
|
||||||
|
suspend inline fun <Key, Value> WriteKeyValueRepo<Key, Value>.set(
|
||||||
|
k: Key, v: Value
|
||||||
|
) = set(k to v)
|
||||||
|
|
||||||
|
suspend inline fun <Key, Value> WriteKeyValueRepo<Key, Value>.unset(
|
||||||
|
vararg k: Key
|
||||||
|
) = unset(k.toList())
|
||||||
|
|
||||||
|
suspend inline fun <Key, Value> WriteKeyValueRepo<Key, Value>.unsetWithValues(
|
||||||
|
vararg v: Value
|
||||||
|
) = unsetWithValues(v.toList())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full version of standard key-value repository with all set/unset/clear/get methods
|
||||||
|
*/
|
||||||
|
interface KeyValueRepo<Key, Value> : ReadKeyValueRepo<Key, Value>, WriteKeyValueRepo<Key, Value> {
|
||||||
|
/**
|
||||||
|
* By default, will walk throw all the [keys] with [Value]s from [toUnset] and run [doAllWithCurrentPaging] with
|
||||||
|
* [unset] of found data [Key]s
|
||||||
|
*/
|
||||||
|
override suspend fun unsetWithValues(toUnset: List<Value>) = toUnset.forEach { v ->
|
||||||
|
doAllWithCurrentPaging {
|
||||||
|
keys(v, it).also {
|
||||||
|
unset(it.results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default, will remove all the data of current repo using [doAllWithCurrentPaging], [keys] and [unset]
|
||||||
|
*/
|
||||||
|
suspend fun clear() {
|
||||||
|
doAllWithCurrentPaging { keys(it).also { unset(it.results) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
typealias StandardKeyValueRepo<Key,Value> = KeyValueRepo<Key, Value>
|
||||||
|
|
||||||
|
class DelegateBasedKeyValueRepo<Key, Value>(
|
||||||
|
readDelegate: ReadKeyValueRepo<Key, Value>,
|
||||||
|
writeDelegate: WriteKeyValueRepo<Key, Value>
|
||||||
|
) : KeyValueRepo<Key, Value>,
|
||||||
|
ReadKeyValueRepo<Key, Value> by readDelegate,
|
||||||
|
WriteKeyValueRepo<Key, Value> by writeDelegate
|
@@ -1,63 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.repos
|
|
||||||
|
|
||||||
import dev.inmo.micro_utils.pagination.*
|
|
||||||
import dev.inmo.micro_utils.pagination.utils.doAllWithCurrentPaging
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
interface ReadKeyValueRepo<Key, Value> : Repo {
|
|
||||||
suspend fun get(k: Key): Value?
|
|
||||||
suspend fun values(pagination: Pagination, reversed: Boolean = false): PaginationResult<Value>
|
|
||||||
suspend fun keys(pagination: Pagination, reversed: Boolean = false): PaginationResult<Key>
|
|
||||||
suspend fun keys(v: Value, pagination: Pagination, reversed: Boolean = false): PaginationResult<Key>
|
|
||||||
suspend fun contains(key: Key): Boolean
|
|
||||||
suspend fun count(): Long
|
|
||||||
}
|
|
||||||
typealias ReadStandardKeyValueRepo<Key,Value> = ReadKeyValueRepo<Key, Value>
|
|
||||||
|
|
||||||
interface WriteKeyValueRepo<Key, Value> : Repo {
|
|
||||||
val onNewValue: Flow<Pair<Key, Value>>
|
|
||||||
val onValueRemoved: Flow<Key>
|
|
||||||
|
|
||||||
suspend fun set(toSet: Map<Key, Value>)
|
|
||||||
suspend fun unset(toUnset: List<Key>)
|
|
||||||
suspend fun unsetWithValues(toUnset: List<Value>)
|
|
||||||
}
|
|
||||||
typealias WriteStandardKeyValueRepo<Key,Value> = WriteKeyValueRepo<Key, Value>
|
|
||||||
|
|
||||||
suspend inline fun <Key, Value> WriteKeyValueRepo<Key, Value>.set(
|
|
||||||
vararg toSet: Pair<Key, Value>
|
|
||||||
) = set(toSet.toMap())
|
|
||||||
|
|
||||||
suspend inline fun <Key, Value> WriteKeyValueRepo<Key, Value>.set(
|
|
||||||
k: Key, v: Value
|
|
||||||
) = set(k to v)
|
|
||||||
|
|
||||||
suspend inline fun <Key, Value> WriteKeyValueRepo<Key, Value>.unset(
|
|
||||||
vararg k: Key
|
|
||||||
) = unset(k.toList())
|
|
||||||
|
|
||||||
suspend inline fun <Key, Value> WriteKeyValueRepo<Key, Value>.unsetWithValues(
|
|
||||||
vararg v: Value
|
|
||||||
) = unsetWithValues(v.toList())
|
|
||||||
|
|
||||||
interface KeyValueRepo<Key, Value> : ReadKeyValueRepo<Key, Value>, WriteKeyValueRepo<Key, Value> {
|
|
||||||
override suspend fun unsetWithValues(toUnset: List<Value>) = toUnset.forEach { v ->
|
|
||||||
doAllWithCurrentPaging {
|
|
||||||
keys(v, it).also {
|
|
||||||
unset(it.results)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun clear() {
|
|
||||||
doAllWithCurrentPaging { keys(it).also { unset(it.results) } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
typealias StandardKeyValueRepo<Key,Value> = KeyValueRepo<Key, Value>
|
|
||||||
|
|
||||||
class DelegateBasedKeyValueRepo<Key, Value>(
|
|
||||||
readDelegate: ReadKeyValueRepo<Key, Value>,
|
|
||||||
writeDelegate: WriteKeyValueRepo<Key, Value>
|
|
||||||
) : KeyValueRepo<Key, Value>,
|
|
||||||
ReadKeyValueRepo<Key, Value> by readDelegate,
|
|
||||||
WriteKeyValueRepo<Key, Value> by writeDelegate
|
|
@@ -17,7 +17,7 @@ fun <T : Any> Context.keyValueStore(
|
|||||||
): KeyValueRepo<String, T> {
|
): KeyValueRepo<String, T> {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
return cache.getOrPut(name) {
|
return cache.getOrPut(name) {
|
||||||
KeyValueStore<T>(this, name, cacheValues)
|
KeyValueStore<T>(c = this, preferencesName = name, useCache = cacheValues)
|
||||||
} as KeyValueStore<T>
|
} as KeyValueStore<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +149,14 @@ class KeyValueStore<T : Any> internal constructor (
|
|||||||
_onValueRemovedFlow.emit(it)
|
_onValueRemovedFlow.emit(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
operator fun <T : Any> invoke(
|
||||||
|
context: Context,
|
||||||
|
name: String = "default",
|
||||||
|
cacheValues: Boolean = false
|
||||||
|
) = context.keyValueStore<T>(name, cacheValues)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <T : Any> SharedPreferencesKeyValueRepo(
|
inline fun <T : Any> SharedPreferencesKeyValueRepo(
|
||||||
@@ -156,3 +164,5 @@ inline fun <T : Any> SharedPreferencesKeyValueRepo(
|
|||||||
name: String = "default",
|
name: String = "default",
|
||||||
cacheValues: Boolean = false
|
cacheValues: Boolean = false
|
||||||
) = context.keyValueStore<T>(name, cacheValues)
|
) = context.keyValueStore<T>(name, cacheValues)
|
||||||
|
|
||||||
|
typealias KeyValueSPRepo<T> = KeyValueStore<T>
|
||||||
|
@@ -4,9 +4,9 @@ import dev.inmo.micro_utils.repos.UpdatedValuePair
|
|||||||
import dev.inmo.micro_utils.repos.WriteCRUDRepo
|
import dev.inmo.micro_utils.repos.WriteCRUDRepo
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
import org.jetbrains.exposed.sql.statements.InsertStatement
|
import org.jetbrains.exposed.sql.statements.*
|
||||||
import org.jetbrains.exposed.sql.statements.UpdateStatement
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import java.util.Objects
|
||||||
|
|
||||||
abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
|
abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
|
||||||
flowsChannelsSize: Int = 0,
|
flowsChannelsSize: Int = 0,
|
||||||
@@ -26,12 +26,40 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
|
|||||||
override val deletedObjectsIdsFlow: Flow<IdType> = _deletedObjectsIdsFlow.asSharedFlow()
|
override val deletedObjectsIdsFlow: Flow<IdType> = _deletedObjectsIdsFlow.asSharedFlow()
|
||||||
|
|
||||||
protected abstract fun InsertStatement<Number>.asObject(value: InputValueType): ObjectType
|
protected abstract fun InsertStatement<Number>.asObject(value: InputValueType): ObjectType
|
||||||
abstract val selectByIds: SqlExpressionBuilder.(List<IdType>) -> Op<Boolean>
|
|
||||||
|
|
||||||
protected abstract fun insert(value: InputValueType, it: InsertStatement<Number>)
|
/**
|
||||||
protected abstract fun update(id: IdType, value: InputValueType, it: UpdateStatement)
|
* @param id Can be null only if [createAndInsertId] have returned null (it can be useful when you have
|
||||||
|
* autoincrement identifier)
|
||||||
|
* @param it Will be [UpdateStatement] when it is called from [update] method or [InsertStatement] from the [create]
|
||||||
|
* one. Anyway, it is main method where you should put the logic of table filling by [value] data
|
||||||
|
*
|
||||||
|
* @see createAndInsertId
|
||||||
|
*/
|
||||||
|
protected abstract fun update(id: IdType?, value: InputValueType, it: UpdateBuilder<Int>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override this method to interact with [it] ([InsertStatement]) and put there new id with [IdType].
|
||||||
|
*
|
||||||
|
* By default, have null value due to the fact that in the most cases users have [autoIncrement]ing id columns
|
||||||
|
*
|
||||||
|
* @return In case when id for the model has been created new [IdType] should be returned
|
||||||
|
*/
|
||||||
|
protected open fun createAndInsertId(value: InputValueType, it: InsertStatement<Number>): IdType? = null
|
||||||
|
|
||||||
|
protected open fun insert(value: InputValueType, it: InsertStatement<Number>) {
|
||||||
|
val id = createAndInsertId(value, it)
|
||||||
|
update(id, value, it as UpdateBuilder<Int>)
|
||||||
|
}
|
||||||
|
|
||||||
protected open suspend fun onBeforeCreate(value: List<InputValueType>) {}
|
protected open suspend fun onBeforeCreate(value: List<InputValueType>) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this method to do the something with [values]. You may change and output values in that list and return
|
||||||
|
* changed list of pairs
|
||||||
|
*/
|
||||||
|
protected open suspend fun onAfterCreate(
|
||||||
|
values: List<Pair<InputValueType, ObjectType>>
|
||||||
|
): List<ObjectType> = values.map { it.second }
|
||||||
private fun createWithoutNotification(value: InputValueType): ObjectType {
|
private fun createWithoutNotification(value: InputValueType): ObjectType {
|
||||||
return transaction(database) {
|
return transaction(database) {
|
||||||
insert { insert(value, it) }.asObject(value)
|
insert { insert(value, it) }.asObject(value)
|
||||||
@@ -41,13 +69,18 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
|
|||||||
override suspend fun create(values: List<InputValueType>): List<ObjectType> {
|
override suspend fun create(values: List<InputValueType>): List<ObjectType> {
|
||||||
onBeforeCreate(values)
|
onBeforeCreate(values)
|
||||||
return transaction(db = database) {
|
return transaction(db = database) {
|
||||||
values.map { value -> createWithoutNotification(value) }
|
values.map { value -> value to createWithoutNotification(value) }
|
||||||
|
}.let {
|
||||||
|
onAfterCreate(it)
|
||||||
}.onEach {
|
}.onEach {
|
||||||
_newObjectsFlow.emit(it)
|
_newObjectsFlow.emit(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open suspend fun onBeforeUpdate(value: List<UpdatedValuePair<IdType, InputValueType>>) {}
|
protected open suspend fun onBeforeUpdate(value: List<UpdatedValuePair<IdType, InputValueType>>) {}
|
||||||
|
protected open suspend fun onAfterUpdate(
|
||||||
|
value: List<UpdatedValuePair<InputValueType, ObjectType>>
|
||||||
|
): List<ObjectType> = value.map { it.second }
|
||||||
private fun updateWithoutNotification(id: IdType, value: InputValueType): ObjectType? {
|
private fun updateWithoutNotification(id: IdType, value: InputValueType): ObjectType? {
|
||||||
return transaction(db = database) {
|
return transaction(db = database) {
|
||||||
update(
|
update(
|
||||||
@@ -55,7 +88,7 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
|
|||||||
selectById(this, id)
|
selectById(this, id)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
update(id, value, it)
|
update(id, value, it as UpdateBuilder<Int>)
|
||||||
}
|
}
|
||||||
}.let {
|
}.let {
|
||||||
if (it > 0) {
|
if (it > 0) {
|
||||||
@@ -72,7 +105,9 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
|
|||||||
|
|
||||||
override suspend fun update(id: IdType, value: InputValueType): ObjectType? {
|
override suspend fun update(id: IdType, value: InputValueType): ObjectType? {
|
||||||
onBeforeUpdate(listOf(id to value))
|
onBeforeUpdate(listOf(id to value))
|
||||||
return updateWithoutNotification(id, value).also {
|
return updateWithoutNotification(id, value).let {
|
||||||
|
onAfterUpdate(listOf(value to (it ?: return@let emptyList())))
|
||||||
|
}.firstOrNull().also {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
_updatedObjectsFlow.emit(it)
|
_updatedObjectsFlow.emit(it)
|
||||||
}
|
}
|
||||||
@@ -82,9 +117,11 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
|
|||||||
onBeforeUpdate(values)
|
onBeforeUpdate(values)
|
||||||
return (
|
return (
|
||||||
transaction(db = database) {
|
transaction(db = database) {
|
||||||
values.map { (id, value) -> updateWithoutNotification(id, value) }
|
values.mapNotNull { (id, value) -> value to (updateWithoutNotification(id, value) ?: return@mapNotNull null) }
|
||||||
}.filterNotNull()
|
}
|
||||||
).onEach {
|
).let {
|
||||||
|
onAfterUpdate(it)
|
||||||
|
}.onEach {
|
||||||
_updatedObjectsFlow.emit(it)
|
_updatedObjectsFlow.emit(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,9 +129,7 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
|
|||||||
override suspend fun deleteById(ids: List<IdType>) {
|
override suspend fun deleteById(ids: List<IdType>) {
|
||||||
onBeforeDelete(ids)
|
onBeforeDelete(ids)
|
||||||
transaction(db = database) {
|
transaction(db = database) {
|
||||||
val deleted = deleteWhere(null, null) {
|
val deleted = deleteWhere(null, null) { selectByIds(it, ids) }
|
||||||
selectByIds(ids)
|
|
||||||
}
|
|
||||||
if (deleted == ids.size) {
|
if (deleted == ids.size) {
|
||||||
ids
|
ids
|
||||||
} else {
|
} else {
|
||||||
|
@@ -0,0 +1,20 @@
|
|||||||
|
package dev.inmo.micro_utils.repos.exposed
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.sql.*
|
||||||
|
|
||||||
|
interface CommonExposedRepo<IdType, ObjectType> : ExposedRepo {
|
||||||
|
val ResultRow.asObject: ObjectType
|
||||||
|
val selectById: ISqlExpressionBuilder.(IdType) -> Op<Boolean>
|
||||||
|
val selectByIds: ISqlExpressionBuilder.(List<IdType>) -> Op<Boolean>
|
||||||
|
get() = { list ->
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
Op.FALSE
|
||||||
|
} else {
|
||||||
|
var op = selectById(list.first())
|
||||||
|
(1 until list.size).forEach {
|
||||||
|
op = op.or(selectById(list[it]))
|
||||||
|
}
|
||||||
|
op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -2,7 +2,4 @@ package dev.inmo.micro_utils.repos.exposed
|
|||||||
|
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
|
|
||||||
interface ExposedCRUDRepo<ObjectType, IdType> : ExposedRepo {
|
interface ExposedCRUDRepo<ObjectType, IdType> : CommonExposedRepo<IdType, ObjectType>
|
||||||
val ResultRow.asObject: ObjectType
|
|
||||||
val selectById: SqlExpressionBuilder.(IdType) -> Op<Boolean>
|
|
||||||
}
|
|
||||||
|
@@ -0,0 +1,76 @@
|
|||||||
|
package dev.inmo.micro_utils.repos.exposed.keyvalue
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.repos.KeyValueRepo
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import org.jetbrains.exposed.sql.statements.*
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
|
abstract class AbstractExposedKeyValueRepo<Key, Value>(
|
||||||
|
override val database: Database,
|
||||||
|
tableName: String? = null
|
||||||
|
) : KeyValueRepo<Key, Value>, AbstractExposedReadKeyValueRepo<Key, Value>(
|
||||||
|
database,
|
||||||
|
tableName
|
||||||
|
) {
|
||||||
|
protected val _onNewValue = MutableSharedFlow<Pair<Key, Value>>()
|
||||||
|
protected val _onValueRemoved = MutableSharedFlow<Key>()
|
||||||
|
|
||||||
|
override val onNewValue: Flow<Pair<Key, Value>> = _onNewValue.asSharedFlow()
|
||||||
|
override val onValueRemoved: Flow<Key> = _onValueRemoved.asSharedFlow()
|
||||||
|
|
||||||
|
protected abstract fun update(k: Key, v: Value, it: UpdateBuilder<Int>)
|
||||||
|
protected abstract fun insertKey(k: Key, v: Value, it: InsertStatement<Number>)
|
||||||
|
|
||||||
|
protected open fun insert(k: Key, v: Value, it: InsertStatement<Number>) {
|
||||||
|
insertKey(k, v, it)
|
||||||
|
update(k, v, it as UpdateBuilder<Int>)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun set(toSet: Map<Key, Value>) {
|
||||||
|
transaction(database) {
|
||||||
|
toSet.mapNotNull { (k, v) ->
|
||||||
|
if (update({ selectById(k) }) { update(k, v, it as UpdateBuilder<Int>) } > 0) {
|
||||||
|
k to v
|
||||||
|
} else {
|
||||||
|
val inserted = insert {
|
||||||
|
insert(k, v, it)
|
||||||
|
}.getOrNull(keyColumn) != null
|
||||||
|
if (inserted) {
|
||||||
|
k to v
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.forEach {
|
||||||
|
_onNewValue.emit(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun unset(toUnset: List<Key>) {
|
||||||
|
transaction(database) {
|
||||||
|
toUnset.mapNotNull { item ->
|
||||||
|
if (deleteWhere { selectById(it, item) } > 0) {
|
||||||
|
item
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.forEach {
|
||||||
|
_onValueRemoved.emit(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun unsetWithValues(toUnset: List<Value>) {
|
||||||
|
transaction(database) {
|
||||||
|
toUnset.flatMap {
|
||||||
|
val keys = select { selectByValue(it) }.mapNotNull { it.asKey }
|
||||||
|
deleteWhere { selectByIds(it, keys) }
|
||||||
|
keys
|
||||||
|
}
|
||||||
|
}.distinct().forEach {
|
||||||
|
_onValueRemoved.emit(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,60 @@
|
|||||||
|
package dev.inmo.micro_utils.repos.exposed.keyvalue
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.pagination.*
|
||||||
|
import dev.inmo.micro_utils.repos.ReadKeyValueRepo
|
||||||
|
import dev.inmo.micro_utils.repos.exposed.*
|
||||||
|
import dev.inmo.micro_utils.repos.exposed.utils.selectPaginated
|
||||||
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
|
abstract class AbstractExposedReadKeyValueRepo<Key, Value>(
|
||||||
|
override val database: Database,
|
||||||
|
tableName: String? = null
|
||||||
|
) : ReadKeyValueRepo<Key, Value>,
|
||||||
|
CommonExposedRepo<Key, Value>,
|
||||||
|
Table(tableName ?: "") {
|
||||||
|
abstract val keyColumn: Column<*>
|
||||||
|
abstract val ResultRow.asKey: Key
|
||||||
|
abstract val selectByValue: ISqlExpressionBuilder.(Value) -> Op<Boolean>
|
||||||
|
|
||||||
|
override suspend fun get(k: Key): Value? = transaction(database) {
|
||||||
|
select { selectById(k) }.limit(1).firstOrNull() ?.asObject
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun contains(key: Key): Boolean = transaction(database) {
|
||||||
|
select { selectById(key) }.limit(1).any()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun count(): Long = transaction(database) { selectAll().count() }
|
||||||
|
|
||||||
|
override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult<Key> = transaction(database) {
|
||||||
|
selectAll().selectPaginated(
|
||||||
|
pagination,
|
||||||
|
keyColumn,
|
||||||
|
reversed
|
||||||
|
) {
|
||||||
|
it.asKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun keys(v: Value, pagination: Pagination, reversed: Boolean): PaginationResult<Key> = transaction(database) {
|
||||||
|
select { selectByValue(v) }.selectPaginated(
|
||||||
|
pagination,
|
||||||
|
keyColumn,
|
||||||
|
reversed
|
||||||
|
) {
|
||||||
|
it.asKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun values(pagination: Pagination, reversed: Boolean): PaginationResult<Value> = transaction(database) {
|
||||||
|
selectAll().selectPaginated(
|
||||||
|
pagination,
|
||||||
|
keyColumn,
|
||||||
|
reversed
|
||||||
|
) {
|
||||||
|
it.asObject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -4,6 +4,8 @@ import dev.inmo.micro_utils.repos.KeyValueRepo
|
|||||||
import dev.inmo.micro_utils.repos.exposed.ColumnAllocator
|
import dev.inmo.micro_utils.repos.exposed.ColumnAllocator
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
open class ExposedKeyValueRepo<Key, Value>(
|
open class ExposedKeyValueRepo<Key, Value>(
|
||||||
@@ -47,9 +49,9 @@ open class ExposedKeyValueRepo<Key, Value>(
|
|||||||
|
|
||||||
override suspend fun unset(toUnset: List<Key>) {
|
override suspend fun unset(toUnset: List<Key>) {
|
||||||
transaction(database) {
|
transaction(database) {
|
||||||
toUnset.mapNotNull {
|
toUnset.mapNotNull { item ->
|
||||||
if (deleteWhere { keyColumn.eq(it) } > 0) {
|
if (deleteWhere { keyColumn.eq(item) } > 0) {
|
||||||
it
|
item
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
@@ -3,50 +3,29 @@ package dev.inmo.micro_utils.repos.exposed.keyvalue
|
|||||||
import dev.inmo.micro_utils.pagination.*
|
import dev.inmo.micro_utils.pagination.*
|
||||||
import dev.inmo.micro_utils.repos.ReadKeyValueRepo
|
import dev.inmo.micro_utils.repos.ReadKeyValueRepo
|
||||||
import dev.inmo.micro_utils.repos.exposed.*
|
import dev.inmo.micro_utils.repos.exposed.*
|
||||||
|
import dev.inmo.micro_utils.repos.exposed.utils.selectPaginated
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import org.jetbrains.exposed.sql.statements.InsertStatement
|
||||||
|
import org.jetbrains.exposed.sql.statements.UpdateBuilder
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
open class ExposedReadKeyValueRepo<Key, Value>(
|
open class ExposedReadKeyValueRepo<Key, Value>(
|
||||||
override val database: Database,
|
database: Database,
|
||||||
keyColumnAllocator: ColumnAllocator<Key>,
|
keyColumnAllocator: ColumnAllocator<Key>,
|
||||||
valueColumnAllocator: ColumnAllocator<Value>,
|
valueColumnAllocator: ColumnAllocator<Value>,
|
||||||
tableName: String? = null
|
tableName: String? = null
|
||||||
) : ReadKeyValueRepo<Key, Value>, ExposedRepo, Table(tableName ?: "") {
|
) : ReadKeyValueRepo<Key, Value>, ExposedRepo, AbstractExposedReadKeyValueRepo<Key, Value>(database, tableName) {
|
||||||
val keyColumn: Column<Key> = keyColumnAllocator()
|
|
||||||
|
override val keyColumn: Column<Key> = keyColumnAllocator()
|
||||||
val valueColumn: Column<Value> = valueColumnAllocator()
|
val valueColumn: Column<Value> = valueColumnAllocator()
|
||||||
override val primaryKey: PrimaryKey = PrimaryKey(keyColumn, valueColumn)
|
override val ResultRow.asKey: Key
|
||||||
|
get() = get(keyColumn)
|
||||||
|
override val selectByValue: ISqlExpressionBuilder.(Value) -> Op<Boolean> = { valueColumn.eq(it) }
|
||||||
|
override val ResultRow.asObject: Value
|
||||||
|
get() = get(valueColumn)
|
||||||
|
override val selectById: ISqlExpressionBuilder.(Key) -> Op<Boolean> = { keyColumn.eq(it) }
|
||||||
|
override val primaryKey: Table.PrimaryKey
|
||||||
|
get() = PrimaryKey(keyColumn, valueColumn)
|
||||||
|
|
||||||
init { initTable() }
|
init { initTable() }
|
||||||
|
|
||||||
override suspend fun get(k: Key): Value? = transaction(database) {
|
|
||||||
select { keyColumn.eq(k) }.limit(1).firstOrNull() ?.getOrNull(valueColumn)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun contains(key: Key): Boolean = transaction(database) {
|
|
||||||
select { keyColumn.eq(key) }.limit(1).any()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun count(): Long = transaction(database) { selectAll().count() }
|
|
||||||
|
|
||||||
override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult<Key> = transaction(database) {
|
|
||||||
selectAll().paginate(pagination, keyColumn to if (reversed) SortOrder.DESC else SortOrder.ASC).map {
|
|
||||||
it[keyColumn]
|
|
||||||
}
|
|
||||||
}.createPaginationResult(pagination, count())
|
|
||||||
|
|
||||||
override suspend fun keys(v: Value, pagination: Pagination, reversed: Boolean): PaginationResult<Key> = transaction(database) {
|
|
||||||
select { valueColumn.eq(v) }.let {
|
|
||||||
it.count() to it.paginate(pagination, keyColumn to if (reversed) SortOrder.DESC else SortOrder.ASC).map {
|
|
||||||
it[keyColumn]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.let { (count, list) ->
|
|
||||||
list.createPaginationResult(pagination, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun values(pagination: Pagination, reversed: Boolean): PaginationResult<Value> = transaction(database) {
|
|
||||||
selectAll().paginate(pagination, keyColumn to if (reversed) SortOrder.DESC else SortOrder.ASC).map {
|
|
||||||
it[valueColumn]
|
|
||||||
}
|
|
||||||
}.createPaginationResult(pagination, count())
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,69 @@
|
|||||||
|
package dev.inmo.micro_utils.repos.exposed.onetomany
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.repos.KeyValuesRepo
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import org.jetbrains.exposed.sql.statements.InsertStatement
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
|
abstract class AbstractExposedKeyValuesRepo<Key, Value>(
|
||||||
|
override val database: Database,
|
||||||
|
tableName: String? = null
|
||||||
|
) : KeyValuesRepo<Key, Value>, AbstractExposedReadKeyValuesRepo<Key, Value>(
|
||||||
|
database,
|
||||||
|
tableName
|
||||||
|
) {
|
||||||
|
protected val _onNewValue: MutableSharedFlow<Pair<Key, Value>> = MutableSharedFlow()
|
||||||
|
override val onNewValue: Flow<Pair<Key, Value>>
|
||||||
|
get() = _onNewValue.asSharedFlow()
|
||||||
|
protected val _onValueRemoved: MutableSharedFlow<Pair<Key, Value>> = MutableSharedFlow()
|
||||||
|
override val onValueRemoved: Flow<Pair<Key, Value>>
|
||||||
|
get() = _onValueRemoved.asSharedFlow()
|
||||||
|
protected val _onDataCleared: MutableSharedFlow<Key> = MutableSharedFlow()
|
||||||
|
override val onDataCleared: Flow<Key>
|
||||||
|
get() = _onDataCleared.asSharedFlow()
|
||||||
|
|
||||||
|
protected abstract fun insert(k: Key, v: Value, it: InsertStatement<Number>)
|
||||||
|
|
||||||
|
override suspend fun add(toAdd: Map<Key, List<Value>>) {
|
||||||
|
transaction(database) {
|
||||||
|
toAdd.keys.flatMap { k ->
|
||||||
|
toAdd[k] ?.mapNotNull { v ->
|
||||||
|
if (select { selectById(k).and(selectByValue(v)) }.limit(1).any()) {
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
val insertResult = insert {
|
||||||
|
insert(k, v, it)
|
||||||
|
}
|
||||||
|
if (insertResult.insertedCount > 0) {
|
||||||
|
k to v
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} ?: emptyList()
|
||||||
|
}
|
||||||
|
}.forEach { _onNewValue.emit(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun remove(toRemove: Map<Key, List<Value>>) {
|
||||||
|
transaction(database) {
|
||||||
|
toRemove.keys.flatMap { k ->
|
||||||
|
toRemove[k] ?.mapNotNull { v ->
|
||||||
|
if (deleteWhere { selectById(it, k).and(SqlExpressionBuilder.selectByValue(v)) } > 0 ) {
|
||||||
|
k to v
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} ?: emptyList()
|
||||||
|
}
|
||||||
|
}.forEach {
|
||||||
|
_onValueRemoved.emit(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun clear(k: Key) {
|
||||||
|
transaction(database) {
|
||||||
|
deleteWhere { selectById(it, k) }
|
||||||
|
}.also { _onDataCleared.emit(k) }
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,74 @@
|
|||||||
|
package dev.inmo.micro_utils.repos.exposed.onetomany
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.pagination.*
|
||||||
|
import dev.inmo.micro_utils.repos.ReadKeyValueRepo
|
||||||
|
import dev.inmo.micro_utils.repos.ReadKeyValuesRepo
|
||||||
|
import dev.inmo.micro_utils.repos.exposed.*
|
||||||
|
import dev.inmo.micro_utils.repos.exposed.utils.selectPaginated
|
||||||
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
|
abstract class AbstractExposedReadKeyValuesRepo<Key, Value>(
|
||||||
|
override val database: Database,
|
||||||
|
tableName: String? = null
|
||||||
|
) : ReadKeyValuesRepo<Key, Value>,
|
||||||
|
CommonExposedRepo<Key, Value>,
|
||||||
|
Table(tableName ?: "") {
|
||||||
|
abstract val keyColumn: Column<*>
|
||||||
|
abstract val ResultRow.asKey: Key
|
||||||
|
abstract val selectByValue: ISqlExpressionBuilder.(Value) -> Op<Boolean>
|
||||||
|
|
||||||
|
override suspend fun count(k: Key): Long = transaction(database) { select { selectById(k) }.count() }
|
||||||
|
|
||||||
|
override suspend fun count(): Long = transaction(database) { selectAll().count() }
|
||||||
|
|
||||||
|
override suspend fun get(
|
||||||
|
k: Key,
|
||||||
|
pagination: Pagination,
|
||||||
|
reversed: Boolean
|
||||||
|
): PaginationResult<Value> = transaction(database) {
|
||||||
|
select { selectById(k) }.selectPaginated(
|
||||||
|
pagination,
|
||||||
|
keyColumn,
|
||||||
|
reversed
|
||||||
|
) {
|
||||||
|
it.asObject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun keys(
|
||||||
|
pagination: Pagination,
|
||||||
|
reversed: Boolean
|
||||||
|
): PaginationResult<Key> = transaction(database) {
|
||||||
|
selectAll().selectPaginated(
|
||||||
|
pagination,
|
||||||
|
keyColumn,
|
||||||
|
reversed
|
||||||
|
) {
|
||||||
|
it.asKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun keys(
|
||||||
|
v: Value,
|
||||||
|
pagination: Pagination,
|
||||||
|
reversed: Boolean
|
||||||
|
): PaginationResult<Key> = transaction(database) {
|
||||||
|
select { selectByValue(v) }.selectPaginated(
|
||||||
|
pagination,
|
||||||
|
keyColumn,
|
||||||
|
reversed
|
||||||
|
) {
|
||||||
|
it.asKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun contains(k: Key): Boolean = transaction(database) {
|
||||||
|
select { selectById(k) }.limit(1).any()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun contains(k: Key, v: Value): Boolean = transaction(database) {
|
||||||
|
select { selectById(k).and(selectByValue(v)) }.limit(1).any()
|
||||||
|
}
|
||||||
|
}
|
@@ -4,6 +4,7 @@ import dev.inmo.micro_utils.repos.KeyValuesRepo
|
|||||||
import dev.inmo.micro_utils.repos.exposed.ColumnAllocator
|
import dev.inmo.micro_utils.repos.exposed.ColumnAllocator
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
typealias ExposedOneToManyKeyValueRepo1<Key, Value> = ExposedKeyValuesRepo<Key, Value>
|
typealias ExposedOneToManyKeyValueRepo1<Key, Value> = ExposedKeyValuesRepo<Key, Value>
|
||||||
|
@@ -1,10 +1,8 @@
|
|||||||
package dev.inmo.micro_utils.repos.exposed.onetomany
|
package dev.inmo.micro_utils.repos.exposed.onetomany
|
||||||
|
|
||||||
import dev.inmo.micro_utils.pagination.*
|
|
||||||
import dev.inmo.micro_utils.repos.ReadKeyValuesRepo
|
import dev.inmo.micro_utils.repos.ReadKeyValuesRepo
|
||||||
import dev.inmo.micro_utils.repos.exposed.*
|
import dev.inmo.micro_utils.repos.exposed.*
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
|
|
||||||
typealias ExposedReadOneToManyKeyValueRepo<Key, Value> = ExposedReadKeyValuesRepo<Key, Value>
|
typealias ExposedReadOneToManyKeyValueRepo<Key, Value> = ExposedReadKeyValuesRepo<Key, Value>
|
||||||
|
|
||||||
@@ -13,54 +11,15 @@ open class ExposedReadKeyValuesRepo<Key, Value>(
|
|||||||
keyColumnAllocator: ColumnAllocator<Key>,
|
keyColumnAllocator: ColumnAllocator<Key>,
|
||||||
valueColumnAllocator: ColumnAllocator<Value>,
|
valueColumnAllocator: ColumnAllocator<Value>,
|
||||||
tableName: String? = null
|
tableName: String? = null
|
||||||
) : ReadKeyValuesRepo<Key, Value>, ExposedRepo, Table(tableName ?: "") {
|
) : ReadKeyValuesRepo<Key, Value>, ExposedRepo, AbstractExposedReadKeyValuesRepo<Key, Value>(database, tableName) {
|
||||||
val keyColumn: Column<Key> = keyColumnAllocator()
|
override val keyColumn: Column<Key> = keyColumnAllocator()
|
||||||
|
override val ResultRow.asKey: Key
|
||||||
|
get() = get(keyColumn)
|
||||||
|
override val selectByValue: ISqlExpressionBuilder.(Value) -> Op<Boolean> = { valueColumn.eq(it) }
|
||||||
|
override val ResultRow.asObject: Value
|
||||||
|
get() = get(valueColumn)
|
||||||
|
override val selectById: ISqlExpressionBuilder.(Key) -> Op<Boolean> = { keyColumn.eq(it) }
|
||||||
val valueColumn: Column<Value> = valueColumnAllocator()
|
val valueColumn: Column<Value> = valueColumnAllocator()
|
||||||
|
|
||||||
init { initTable() }
|
init { initTable() }
|
||||||
|
|
||||||
override suspend fun count(k: Key): Long = transaction(database) { select { keyColumn.eq(k) }.count() }
|
|
||||||
|
|
||||||
override suspend fun count(): Long = transaction(database) { selectAll().count() }
|
|
||||||
|
|
||||||
override suspend fun get(
|
|
||||||
k: Key,
|
|
||||||
pagination: Pagination,
|
|
||||||
reversed: Boolean
|
|
||||||
): PaginationResult<Value> = transaction(database) {
|
|
||||||
select { keyColumn.eq(k) }.paginate(pagination, keyColumn, reversed).map { it[valueColumn] }
|
|
||||||
}.createPaginationResult(
|
|
||||||
pagination,
|
|
||||||
count(k)
|
|
||||||
)
|
|
||||||
|
|
||||||
override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult<Key> = transaction(database) {
|
|
||||||
selectAll().paginate(pagination, keyColumn, reversed).map { it[keyColumn] }
|
|
||||||
}.createPaginationResult(
|
|
||||||
pagination,
|
|
||||||
count()
|
|
||||||
)
|
|
||||||
|
|
||||||
override suspend fun keys(
|
|
||||||
v: Value,
|
|
||||||
pagination: Pagination,
|
|
||||||
reversed: Boolean
|
|
||||||
): PaginationResult<Key> = transaction(database) {
|
|
||||||
select { valueColumn.eq(v) }.let {
|
|
||||||
it.count() to it.paginate(pagination, keyColumn, reversed).map { it[keyColumn] }
|
|
||||||
}
|
|
||||||
}.let { (count, list) ->
|
|
||||||
list.createPaginationResult(
|
|
||||||
pagination,
|
|
||||||
count
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun contains(k: Key): Boolean = transaction(database) {
|
|
||||||
select { keyColumn.eq(k) }.limit(1).any()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun contains(k: Key, v: Value): Boolean = transaction(database) {
|
|
||||||
select { keyColumn.eq(k).and(valueColumn.eq(v)) }.limit(1).any()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,21 @@
|
|||||||
|
package dev.inmo.micro_utils.repos.exposed.utils
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.pagination.*
|
||||||
|
import org.jetbrains.exposed.sql.*
|
||||||
|
|
||||||
|
fun <T> Query.selectPaginated(
|
||||||
|
pagination: Pagination,
|
||||||
|
orderBy: Pair<Expression<*>, SortOrder>? = null,
|
||||||
|
createResult: (ResultRow) -> T
|
||||||
|
): PaginationResult<T> {
|
||||||
|
val count = count()
|
||||||
|
val list = paginate(pagination, orderBy).map(createResult)
|
||||||
|
return list.createPaginationResult(pagination, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Query.selectPaginated(
|
||||||
|
pagination: Pagination,
|
||||||
|
orderBy: Expression<*>?,
|
||||||
|
reversed: Boolean = false,
|
||||||
|
createResult: (ResultRow) -> T
|
||||||
|
) = selectPaginated(pagination, orderBy ?.let { it to if (reversed) SortOrder.DESC else SortOrder.ASC }, createResult)
|
@@ -1,83 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.repos.ktor.server.crud
|
|
||||||
|
|
||||||
import dev.inmo.micro_utils.ktor.common.StandardKtorSerialFormat
|
|
||||||
import dev.inmo.micro_utils.ktor.common.standardKtorSerialFormat
|
|
||||||
import dev.inmo.micro_utils.ktor.server.*
|
|
||||||
import dev.inmo.micro_utils.pagination.PaginationResult
|
|
||||||
import dev.inmo.micro_utils.pagination.extractPagination
|
|
||||||
import dev.inmo.micro_utils.repos.ReadCRUDRepo
|
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.countRouting
|
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.crud.*
|
|
||||||
import io.ktor.http.ContentType
|
|
||||||
import io.ktor.server.application.call
|
|
||||||
import io.ktor.server.routing.Route
|
|
||||||
import io.ktor.server.routing.get
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.builtins.serializer
|
|
||||||
|
|
||||||
fun <ObjectType, IdType> Route.configureReadCRUDRepoRoutes(
|
|
||||||
originalRepo: ReadCRUDRepo<ObjectType, IdType>,
|
|
||||||
objectsSerializer: KSerializer<ObjectType>,
|
|
||||||
objectsNullableSerializer: KSerializer<ObjectType?>,
|
|
||||||
idsSerializer: KSerializer<IdType>,
|
|
||||||
unifiedRouter: UnifiedRouter
|
|
||||||
) {
|
|
||||||
val paginationResultSerializer = PaginationResult.serializer(objectsSerializer)
|
|
||||||
|
|
||||||
get(getByPaginationRouting) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val pagination = call.request.queryParameters.extractPagination
|
|
||||||
|
|
||||||
unianswer(
|
|
||||||
paginationResultSerializer,
|
|
||||||
originalRepo.getByPagination(pagination)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get(getByIdRouting) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val id = decodeUrlQueryValueOrSendError(
|
|
||||||
"id",
|
|
||||||
idsSerializer
|
|
||||||
) ?: return@get
|
|
||||||
|
|
||||||
unianswer(
|
|
||||||
objectsNullableSerializer,
|
|
||||||
originalRepo.getById(id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get(containsRouting) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val id = decodeUrlQueryValueOrSendError(
|
|
||||||
"id",
|
|
||||||
idsSerializer
|
|
||||||
) ?: return@get
|
|
||||||
|
|
||||||
unianswer(
|
|
||||||
Boolean.serializer(),
|
|
||||||
originalRepo.contains(id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get(countRouting) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
unianswer(
|
|
||||||
Long.serializer(),
|
|
||||||
originalRepo.count()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <ObjectType, IdType> Route.configureReadCRUDRepoRoutes(
|
|
||||||
originalRepo: ReadCRUDRepo<ObjectType, IdType>,
|
|
||||||
objectsSerializer: KSerializer<ObjectType>,
|
|
||||||
objectsNullableSerializer: KSerializer<ObjectType?>,
|
|
||||||
idsSerializer: KSerializer<IdType>,
|
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
|
||||||
serialFormatContentType: ContentType = standardKtorSerialFormatContentType
|
|
||||||
) = configureReadCRUDRepoRoutes(originalRepo, objectsSerializer, objectsNullableSerializer, idsSerializer, UnifiedRouter(serialFormat, serialFormatContentType))
|
|
@@ -1,39 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.repos.ktor.server.crud
|
|
||||||
|
|
||||||
import dev.inmo.micro_utils.ktor.common.StandardKtorSerialFormat
|
|
||||||
import dev.inmo.micro_utils.ktor.common.standardKtorSerialFormat
|
|
||||||
import dev.inmo.micro_utils.ktor.server.UnifiedRouter
|
|
||||||
import dev.inmo.micro_utils.ktor.server.standardKtorSerialFormatContentType
|
|
||||||
import dev.inmo.micro_utils.repos.CRUDRepo
|
|
||||||
import io.ktor.http.ContentType
|
|
||||||
import io.ktor.server.routing.Route
|
|
||||||
import io.ktor.server.routing.route
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
|
|
||||||
fun <ObjectType, IdType, InputValue> Route.configureCRUDRepoRoutes(
|
|
||||||
baseSubpart: String,
|
|
||||||
originalRepo: CRUDRepo<ObjectType, IdType, InputValue>,
|
|
||||||
objectsSerializer: KSerializer<ObjectType>,
|
|
||||||
objectsNullableSerializer: KSerializer<ObjectType?>,
|
|
||||||
inputsSerializer: KSerializer<InputValue>,
|
|
||||||
idsSerializer: KSerializer<IdType>,
|
|
||||||
unifiedRouter: UnifiedRouter
|
|
||||||
) {
|
|
||||||
route(baseSubpart) {
|
|
||||||
configureReadCRUDRepoRoutes(originalRepo, objectsSerializer, objectsNullableSerializer, idsSerializer, unifiedRouter)
|
|
||||||
configureWriteCRUDRepoRoutes(originalRepo, objectsSerializer, objectsNullableSerializer, inputsSerializer, idsSerializer, unifiedRouter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <ObjectType, IdType, InputValue> Route.configureCRUDRepoRoutes(
|
|
||||||
baseSubpart: String,
|
|
||||||
originalRepo: CRUDRepo<ObjectType, IdType, InputValue>,
|
|
||||||
objectsSerializer: KSerializer<ObjectType>,
|
|
||||||
objectsNullableSerializer: KSerializer<ObjectType?>,
|
|
||||||
inputsSerializer: KSerializer<InputValue>,
|
|
||||||
idsSerializer: KSerializer<IdType>,
|
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
|
||||||
serialFormatContentType: ContentType = standardKtorSerialFormatContentType
|
|
||||||
) = configureCRUDRepoRoutes(
|
|
||||||
baseSubpart, originalRepo, objectsSerializer, objectsNullableSerializer, inputsSerializer, idsSerializer, UnifiedRouter(serialFormat, serialFormatContentType)
|
|
||||||
)
|
|
@@ -1,107 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.repos.ktor.server.crud
|
|
||||||
|
|
||||||
import dev.inmo.micro_utils.ktor.common.StandardKtorSerialFormat
|
|
||||||
import dev.inmo.micro_utils.ktor.common.standardKtorSerialFormat
|
|
||||||
import dev.inmo.micro_utils.ktor.server.*
|
|
||||||
import dev.inmo.micro_utils.repos.WriteCRUDRepo
|
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.crud.*
|
|
||||||
import io.ktor.http.ContentType
|
|
||||||
import io.ktor.server.routing.Route
|
|
||||||
import io.ktor.server.routing.post
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.builtins.*
|
|
||||||
|
|
||||||
fun <ObjectType, IdType, InputValue> Route.configureWriteCRUDRepoRoutes(
|
|
||||||
originalRepo: WriteCRUDRepo<ObjectType, IdType, InputValue>,
|
|
||||||
objectsSerializer: KSerializer<ObjectType>,
|
|
||||||
objectsNullableSerializer: KSerializer<ObjectType?>,
|
|
||||||
inputsSerializer: KSerializer<InputValue>,
|
|
||||||
idsSerializer: KSerializer<IdType>,
|
|
||||||
unifiedRouter: UnifiedRouter
|
|
||||||
) {
|
|
||||||
val listObjectsSerializer = ListSerializer(objectsSerializer)
|
|
||||||
val listInputSerializer = ListSerializer(inputsSerializer)
|
|
||||||
val listIdsSerializer = ListSerializer(idsSerializer)
|
|
||||||
val inputUpdateSerializer = PairSerializer(
|
|
||||||
idsSerializer,
|
|
||||||
inputsSerializer
|
|
||||||
)
|
|
||||||
val listInputUpdateSerializer = ListSerializer(inputUpdateSerializer)
|
|
||||||
|
|
||||||
unifiedRouter.apply {
|
|
||||||
includeWebsocketHandling(
|
|
||||||
newObjectsFlowRouting,
|
|
||||||
originalRepo.newObjectsFlow,
|
|
||||||
objectsSerializer
|
|
||||||
)
|
|
||||||
includeWebsocketHandling(
|
|
||||||
updatedObjectsFlowRouting,
|
|
||||||
originalRepo.updatedObjectsFlow,
|
|
||||||
objectsSerializer
|
|
||||||
)
|
|
||||||
includeWebsocketHandling(
|
|
||||||
deletedObjectsIdsFlowRouting,
|
|
||||||
originalRepo.deletedObjectsIdsFlow,
|
|
||||||
idsSerializer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
post(createRouting) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
unianswer(
|
|
||||||
listObjectsSerializer,
|
|
||||||
originalRepo.create(
|
|
||||||
uniload(listInputSerializer)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
post(updateRouting) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val (id, input) = uniload(inputUpdateSerializer)
|
|
||||||
unianswer(
|
|
||||||
objectsNullableSerializer,
|
|
||||||
originalRepo.update(
|
|
||||||
id, input
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
post(updateManyRouting) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val updates = uniload(listInputUpdateSerializer)
|
|
||||||
unianswer(
|
|
||||||
listObjectsSerializer,
|
|
||||||
originalRepo.update(
|
|
||||||
updates
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
post(deleteByIdRouting) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val ids = uniload(listIdsSerializer)
|
|
||||||
unianswer(
|
|
||||||
Unit.serializer(),
|
|
||||||
originalRepo.deleteById(
|
|
||||||
ids
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <ObjectType, IdType, InputValue> Route.configureWriteCRUDRepoRoutes(
|
|
||||||
originalRepo: WriteCRUDRepo<ObjectType, IdType, InputValue>,
|
|
||||||
objectsSerializer: KSerializer<ObjectType>,
|
|
||||||
objectsNullableSerializer: KSerializer<ObjectType?>,
|
|
||||||
inputsSerializer: KSerializer<InputValue>,
|
|
||||||
idsSerializer: KSerializer<IdType>,
|
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
|
||||||
serialFormatContentType: ContentType = standardKtorSerialFormatContentType
|
|
||||||
) = configureWriteCRUDRepoRoutes(
|
|
||||||
originalRepo, objectsSerializer, objectsNullableSerializer, inputsSerializer, idsSerializer, UnifiedRouter(serialFormat, serialFormatContentType)
|
|
||||||
)
|
|
@@ -1,46 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.repos.ktor.server.key_value
|
|
||||||
|
|
||||||
import dev.inmo.micro_utils.ktor.common.StandardKtorSerialFormat
|
|
||||||
import dev.inmo.micro_utils.ktor.common.standardKtorSerialFormat
|
|
||||||
import dev.inmo.micro_utils.ktor.server.UnifiedRouter
|
|
||||||
import dev.inmo.micro_utils.ktor.server.standardKtorSerialFormatContentType
|
|
||||||
import dev.inmo.micro_utils.repos.KeyValueRepo
|
|
||||||
import io.ktor.http.ContentType
|
|
||||||
import io.ktor.server.routing.Route
|
|
||||||
import io.ktor.server.routing.route
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
|
|
||||||
fun <K, V> Route.configureKeyValueRepoRoutes(
|
|
||||||
baseSubpart: String,
|
|
||||||
originalRepo: KeyValueRepo<K, V>,
|
|
||||||
keySerializer: KSerializer<K>,
|
|
||||||
valueSerializer: KSerializer<V>,
|
|
||||||
valueNullableSerializer: KSerializer<V?>,
|
|
||||||
unifiedRouter: UnifiedRouter
|
|
||||||
) {
|
|
||||||
route(baseSubpart) {
|
|
||||||
configureReadStandartKeyValueRepoRoutes(
|
|
||||||
originalRepo,
|
|
||||||
keySerializer,
|
|
||||||
valueSerializer,
|
|
||||||
valueNullableSerializer,
|
|
||||||
unifiedRouter
|
|
||||||
)
|
|
||||||
configureWriteKeyValueRepoRoutes(
|
|
||||||
originalRepo,
|
|
||||||
keySerializer,
|
|
||||||
valueSerializer,
|
|
||||||
unifiedRouter
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <K, V> Route.configureStandartKeyValueRepoRoutes(
|
|
||||||
baseSubpart: String,
|
|
||||||
originalRepo: KeyValueRepo<K, V>,
|
|
||||||
keySerializer: KSerializer<K>,
|
|
||||||
valueSerializer: KSerializer<V>,
|
|
||||||
valueNullableSerializer: KSerializer<V?>,
|
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
|
||||||
serialFormatContentType: ContentType = standardKtorSerialFormatContentType
|
|
||||||
) = configureKeyValueRepoRoutes(baseSubpart, originalRepo, keySerializer, valueSerializer, valueNullableSerializer, UnifiedRouter(serialFormat, serialFormatContentType))
|
|
@@ -1,107 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.repos.ktor.server.key_value
|
|
||||||
|
|
||||||
import dev.inmo.micro_utils.ktor.common.StandardKtorSerialFormat
|
|
||||||
import dev.inmo.micro_utils.ktor.common.standardKtorSerialFormat
|
|
||||||
import dev.inmo.micro_utils.ktor.server.*
|
|
||||||
import dev.inmo.micro_utils.pagination.PaginationResult
|
|
||||||
import dev.inmo.micro_utils.pagination.extractPagination
|
|
||||||
import dev.inmo.micro_utils.repos.ReadKeyValueRepo
|
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.*
|
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.containsRoute
|
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.countRoute
|
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.key_value.*
|
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.key_value.keyParameterName
|
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.key_value.reversedParameterName
|
|
||||||
import io.ktor.http.ContentType
|
|
||||||
import io.ktor.server.application.call
|
|
||||||
import io.ktor.server.routing.Route
|
|
||||||
import io.ktor.server.routing.get
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.builtins.serializer
|
|
||||||
|
|
||||||
fun <K, V> Route.configureReadStandartKeyValueRepoRoutes (
|
|
||||||
originalRepo: ReadKeyValueRepo<K, V>,
|
|
||||||
keySerializer: KSerializer<K>,
|
|
||||||
valueSerializer: KSerializer<V>,
|
|
||||||
valueNullableSerializer: KSerializer<V?>,
|
|
||||||
unifiedRouter: UnifiedRouter
|
|
||||||
) {
|
|
||||||
get(getRoute) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val key = decodeUrlQueryValueOrSendError(
|
|
||||||
keyParameterName,
|
|
||||||
keySerializer
|
|
||||||
) ?: return@get
|
|
||||||
|
|
||||||
unianswer(
|
|
||||||
valueNullableSerializer,
|
|
||||||
originalRepo.get(key)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get(valuesRoute) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val parination = call.request.queryParameters.extractPagination;
|
|
||||||
val reversed = decodeUrlQueryValueOrSendError(
|
|
||||||
reversedParameterName,
|
|
||||||
Boolean.serializer()
|
|
||||||
) ?: return@get
|
|
||||||
|
|
||||||
unianswer(
|
|
||||||
PaginationResult.serializer(valueSerializer),
|
|
||||||
originalRepo.values(parination, reversed)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get(keysRoute) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val parination = call.request.queryParameters.extractPagination;
|
|
||||||
val reversed = decodeUrlQueryValueOrSendError(
|
|
||||||
reversedParameterName,
|
|
||||||
Boolean.serializer()
|
|
||||||
) ?: return@get
|
|
||||||
val value = decodeUrlQueryValue(valueParameterName, valueSerializer)
|
|
||||||
|
|
||||||
unianswer(
|
|
||||||
PaginationResult.serializer(keySerializer),
|
|
||||||
value?.let { originalRepo.keys(value, parination, reversed) } ?: originalRepo.keys(parination, reversed)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get(containsRoute) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val key = decodeUrlQueryValueOrSendError(
|
|
||||||
keyParameterName,
|
|
||||||
keySerializer
|
|
||||||
) ?: return@get
|
|
||||||
|
|
||||||
unianswer(
|
|
||||||
Boolean.serializer(),
|
|
||||||
originalRepo.contains(key)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get(countRoute) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
unianswer(
|
|
||||||
Long.serializer(),
|
|
||||||
originalRepo.count()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <K, V> Route.configureReadStandartKeyValueRepoRoutes (
|
|
||||||
originalRepo: ReadKeyValueRepo<K, V>,
|
|
||||||
keySerializer: KSerializer<K>,
|
|
||||||
valueSerializer: KSerializer<V>,
|
|
||||||
valueNullableSerializer: KSerializer<V?>,
|
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
|
||||||
serialFormatContentType: ContentType = standardKtorSerialFormatContentType
|
|
||||||
) = configureReadStandartKeyValueRepoRoutes(
|
|
||||||
originalRepo, keySerializer, valueSerializer, valueNullableSerializer, UnifiedRouter(serialFormat, serialFormatContentType)
|
|
||||||
)
|
|
@@ -1,70 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.repos.ktor.server.key_value
|
|
||||||
|
|
||||||
import dev.inmo.micro_utils.ktor.common.StandardKtorSerialFormat
|
|
||||||
import dev.inmo.micro_utils.ktor.common.standardKtorSerialFormat
|
|
||||||
import dev.inmo.micro_utils.ktor.server.*
|
|
||||||
import dev.inmo.micro_utils.repos.WriteKeyValueRepo
|
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.key_value.*
|
|
||||||
import io.ktor.http.ContentType
|
|
||||||
import io.ktor.server.routing.Route
|
|
||||||
import io.ktor.server.routing.post
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.builtins.*
|
|
||||||
|
|
||||||
fun <K, V> Route.configureWriteKeyValueRepoRoutes (
|
|
||||||
originalRepo: WriteKeyValueRepo<K, V>,
|
|
||||||
keySerializer: KSerializer<K>,
|
|
||||||
valueSerializer: KSerializer<V>,
|
|
||||||
unifiedRouter: UnifiedRouter
|
|
||||||
) {
|
|
||||||
val keyValueMapSerializer = MapSerializer(keySerializer, valueSerializer)
|
|
||||||
val keysListSerializer = ListSerializer(keySerializer)
|
|
||||||
val valuesListSerializer = ListSerializer(valueSerializer)
|
|
||||||
unifiedRouter.apply {
|
|
||||||
includeWebsocketHandling(
|
|
||||||
onNewValueRoute,
|
|
||||||
originalRepo.onNewValue,
|
|
||||||
PairSerializer(keySerializer, valueSerializer)
|
|
||||||
)
|
|
||||||
|
|
||||||
includeWebsocketHandling(
|
|
||||||
onValueRemovedRoute,
|
|
||||||
originalRepo.onValueRemoved,
|
|
||||||
keySerializer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
post(setRoute) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val toSet = uniload(
|
|
||||||
keyValueMapSerializer
|
|
||||||
)
|
|
||||||
|
|
||||||
unianswer(Unit.serializer(), originalRepo.set(toSet))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
post(unsetRoute) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val toUnset = uniload(keysListSerializer)
|
|
||||||
|
|
||||||
unianswer(Unit.serializer(), originalRepo.unset(toUnset))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
post(unsetWithValuesRoute) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val toUnset = uniload(valuesListSerializer)
|
|
||||||
|
|
||||||
unianswer(Unit.serializer(), originalRepo.unsetWithValues(toUnset))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <K, V> Route.configureWriteStandartKeyValueRepoRoutes (
|
|
||||||
originalRepo: WriteKeyValueRepo<K, V>,
|
|
||||||
keySerializer: KSerializer<K>,
|
|
||||||
valueSerializer: KSerializer<V>,
|
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
|
||||||
serialFormatContentType: ContentType = standardKtorSerialFormatContentType
|
|
||||||
) = configureWriteKeyValueRepoRoutes(originalRepo, keySerializer, valueSerializer, UnifiedRouter(serialFormat, serialFormatContentType))
|
|
@@ -1,35 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.repos.ktor.server.one_to_many
|
|
||||||
|
|
||||||
import dev.inmo.micro_utils.ktor.common.StandardKtorSerialFormat
|
|
||||||
import dev.inmo.micro_utils.ktor.common.standardKtorSerialFormat
|
|
||||||
import dev.inmo.micro_utils.ktor.server.UnifiedRouter
|
|
||||||
import dev.inmo.micro_utils.ktor.server.standardKtorSerialFormatContentType
|
|
||||||
import dev.inmo.micro_utils.repos.KeyValuesRepo
|
|
||||||
import io.ktor.http.ContentType
|
|
||||||
import io.ktor.server.routing.Route
|
|
||||||
import io.ktor.server.routing.route
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
|
|
||||||
fun <Key, Value> Route.configureOneToManyKeyValueRepoRoutes(
|
|
||||||
baseSubpart: String,
|
|
||||||
originalRepo: KeyValuesRepo<Key, Value>,
|
|
||||||
keySerializer: KSerializer<Key>,
|
|
||||||
valueSerializer: KSerializer<Value>,
|
|
||||||
unifiedRouter: UnifiedRouter
|
|
||||||
) {
|
|
||||||
route(baseSubpart) {
|
|
||||||
configureOneToManyReadKeyValueRepoRoutes(originalRepo, keySerializer, valueSerializer, unifiedRouter)
|
|
||||||
configureOneToManyWriteKeyValueRepoRoutes(originalRepo, keySerializer, valueSerializer, unifiedRouter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <Key, Value> Route.configureOneToManyKeyValueRepoRoutes(
|
|
||||||
baseSubpart: String,
|
|
||||||
originalRepo: KeyValuesRepo<Key, Value>,
|
|
||||||
keySerializer: KSerializer<Key>,
|
|
||||||
valueSerializer: KSerializer<Value>,
|
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
|
||||||
serialFormatContentType: ContentType = standardKtorSerialFormatContentType
|
|
||||||
) = configureOneToManyKeyValueRepoRoutes(
|
|
||||||
baseSubpart, originalRepo, keySerializer, valueSerializer, UnifiedRouter(serialFormat, serialFormatContentType)
|
|
||||||
)
|
|
@@ -1,129 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.repos.ktor.server.one_to_many
|
|
||||||
|
|
||||||
import dev.inmo.micro_utils.ktor.common.StandardKtorSerialFormat
|
|
||||||
import dev.inmo.micro_utils.ktor.common.standardKtorSerialFormat
|
|
||||||
import dev.inmo.micro_utils.ktor.server.*
|
|
||||||
import dev.inmo.micro_utils.pagination.PaginationResult
|
|
||||||
import dev.inmo.micro_utils.pagination.extractPagination
|
|
||||||
import dev.inmo.micro_utils.repos.ReadKeyValuesRepo
|
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.keyParameterName
|
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.*
|
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.valueParameterName
|
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.reversedParameterName
|
|
||||||
import io.ktor.http.ContentType
|
|
||||||
import io.ktor.server.application.call
|
|
||||||
import io.ktor.server.routing.Route
|
|
||||||
import io.ktor.server.routing.get
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.builtins.serializer
|
|
||||||
|
|
||||||
fun <Key, Value> Route.configureOneToManyReadKeyValueRepoRoutes(
|
|
||||||
originalRepo: ReadKeyValuesRepo<Key, Value>,
|
|
||||||
keySerializer: KSerializer<Key>,
|
|
||||||
valueSerializer: KSerializer<Value>,
|
|
||||||
unifiedRouter: UnifiedRouter
|
|
||||||
) {
|
|
||||||
val paginationKeyResult = PaginationResult.serializer(keySerializer)
|
|
||||||
val paginationValueResult = PaginationResult.serializer(valueSerializer)
|
|
||||||
|
|
||||||
get(getRoute) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val pagination = call.request.queryParameters.extractPagination
|
|
||||||
val key = decodeUrlQueryValueOrSendError(
|
|
||||||
keyParameterName,
|
|
||||||
keySerializer
|
|
||||||
) ?: return@get
|
|
||||||
val reversed = decodeUrlQueryValue(
|
|
||||||
reversedParameterName,
|
|
||||||
Boolean.serializer()
|
|
||||||
) ?: false
|
|
||||||
|
|
||||||
unianswer(
|
|
||||||
paginationValueResult,
|
|
||||||
originalRepo.get(key, pagination, reversed)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get(keysRoute) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val pagination = call.request.queryParameters.extractPagination
|
|
||||||
val reversed = decodeUrlQueryValue(
|
|
||||||
reversedParameterName,
|
|
||||||
Boolean.serializer()
|
|
||||||
) ?: false
|
|
||||||
val value: Value? = decodeUrlQueryValue(
|
|
||||||
valueParameterName,
|
|
||||||
valueSerializer
|
|
||||||
)
|
|
||||||
|
|
||||||
unianswer(
|
|
||||||
paginationKeyResult,
|
|
||||||
value?.let { originalRepo.keys(value, pagination, reversed) } ?: originalRepo.keys(pagination, reversed)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get(containsByKeyRoute) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val key = decodeUrlQueryValueOrSendError(
|
|
||||||
keyParameterName,
|
|
||||||
keySerializer
|
|
||||||
) ?: return@get
|
|
||||||
|
|
||||||
unianswer(
|
|
||||||
Boolean.serializer(),
|
|
||||||
originalRepo.contains(key)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get(containsByKeyValueRoute) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val key = decodeUrlQueryValueOrSendError(
|
|
||||||
keyParameterName,
|
|
||||||
keySerializer
|
|
||||||
) ?: return@get
|
|
||||||
val value = decodeUrlQueryValueOrSendError(
|
|
||||||
valueParameterName,
|
|
||||||
valueSerializer
|
|
||||||
) ?: return@get
|
|
||||||
|
|
||||||
unianswer(
|
|
||||||
Boolean.serializer(),
|
|
||||||
originalRepo.contains(key, value)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get(countByKeyRoute) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val key = decodeUrlQueryValueOrSendError(
|
|
||||||
keyParameterName,
|
|
||||||
keySerializer
|
|
||||||
) ?: return@get
|
|
||||||
|
|
||||||
unianswer(
|
|
||||||
Long.serializer(),
|
|
||||||
originalRepo.count(key)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get(countRoute) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
unianswer(
|
|
||||||
Long.serializer(),
|
|
||||||
originalRepo.count()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <Key, Value> Route.configureOneToManyReadKeyValueRepoRoutes(
|
|
||||||
originalRepo: ReadKeyValuesRepo<Key, Value>,
|
|
||||||
keySerializer: KSerializer<Key>,
|
|
||||||
valueSerializer: KSerializer<Value>,
|
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
|
||||||
serialFormatContentType: ContentType = standardKtorSerialFormatContentType
|
|
||||||
) = configureOneToManyReadKeyValueRepoRoutes(originalRepo, keySerializer, valueSerializer, UnifiedRouter(serialFormat, serialFormatContentType))
|
|
@@ -1,103 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.repos.ktor.server.one_to_many
|
|
||||||
|
|
||||||
import dev.inmo.micro_utils.ktor.common.StandardKtorSerialFormat
|
|
||||||
import dev.inmo.micro_utils.ktor.common.standardKtorSerialFormat
|
|
||||||
import dev.inmo.micro_utils.ktor.server.*
|
|
||||||
import dev.inmo.micro_utils.repos.WriteKeyValuesRepo
|
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.*
|
|
||||||
import io.ktor.http.ContentType
|
|
||||||
import io.ktor.server.routing.Route
|
|
||||||
import io.ktor.server.routing.post
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.builtins.*
|
|
||||||
|
|
||||||
fun <Key, Value> Route.configureOneToManyWriteKeyValueRepoRoutes(
|
|
||||||
originalRepo: WriteKeyValuesRepo<Key, Value>,
|
|
||||||
keySerializer: KSerializer<Key>,
|
|
||||||
valueSerializer: KSerializer<Value>,
|
|
||||||
unifiedRouter: UnifiedRouter
|
|
||||||
) {
|
|
||||||
val keyValueSerializer = PairSerializer(keySerializer, valueSerializer)
|
|
||||||
val keyValueMapSerializer = MapSerializer(keySerializer, ListSerializer(valueSerializer))
|
|
||||||
|
|
||||||
unifiedRouter.apply {
|
|
||||||
includeWebsocketHandling(
|
|
||||||
onNewValueRoute,
|
|
||||||
originalRepo.onNewValue,
|
|
||||||
keyValueSerializer
|
|
||||||
)
|
|
||||||
includeWebsocketHandling(
|
|
||||||
onValueRemovedRoute,
|
|
||||||
originalRepo.onValueRemoved,
|
|
||||||
keyValueSerializer
|
|
||||||
)
|
|
||||||
includeWebsocketHandling(
|
|
||||||
onDataClearedRoute,
|
|
||||||
originalRepo.onDataCleared,
|
|
||||||
keySerializer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
post(addRoute) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val obj = uniload(keyValueMapSerializer)
|
|
||||||
|
|
||||||
unianswer(
|
|
||||||
Unit.serializer(),
|
|
||||||
originalRepo.add(obj)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
post(removeRoute) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val obj = uniload(keyValueMapSerializer)
|
|
||||||
|
|
||||||
unianswer(
|
|
||||||
Unit.serializer(),
|
|
||||||
originalRepo.remove(obj),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
post(clearRoute) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val key = uniload(keySerializer)
|
|
||||||
|
|
||||||
unianswer(
|
|
||||||
Unit.serializer(),
|
|
||||||
originalRepo.clear(key),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
post(clearWithValueRoute) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val v = uniload(valueSerializer)
|
|
||||||
|
|
||||||
unianswer(
|
|
||||||
Unit.serializer(),
|
|
||||||
originalRepo.clearWithValue(v),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
post(setRoute) {
|
|
||||||
unifiedRouter.apply {
|
|
||||||
val obj = uniload(keyValueMapSerializer)
|
|
||||||
|
|
||||||
unianswer(
|
|
||||||
Unit.serializer(),
|
|
||||||
originalRepo.set(obj)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <Key, Value> Route.configureOneToManyWriteKeyValueRepoRoutes(
|
|
||||||
originalRepo: WriteKeyValuesRepo<Key, Value>,
|
|
||||||
keySerializer: KSerializer<Key>,
|
|
||||||
valueSerializer: KSerializer<Value>,
|
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
|
||||||
serialFormatContentType: ContentType = standardKtorSerialFormatContentType
|
|
||||||
) = configureOneToManyWriteKeyValueRepoRoutes(originalRepo, keySerializer, valueSerializer, UnifiedRouter(serialFormat, serialFormatContentType))
|
|
@@ -5,6 +5,7 @@ String[] includes = [
|
|||||||
":common:compose",
|
":common:compose",
|
||||||
":matrix",
|
":matrix",
|
||||||
":crypto",
|
":crypto",
|
||||||
|
":koin",
|
||||||
":selector:common",
|
":selector:common",
|
||||||
":pagination:common",
|
":pagination:common",
|
||||||
":pagination:exposed",
|
":pagination:exposed",
|
||||||
|
Reference in New Issue
Block a user