mirror of
				https://github.com/InsanusMokrassar/MicroUtils.git
				synced 2025-10-30 03:30:19 +00:00 
			
		
		
		
	Compare commits
	
		
			52 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 438fefa7a3 | |||
| 5d74eac814 | |||
| 9fb62e1e25 | |||
| 3e366ea73b | |||
| 0ff895bffa | |||
| c5bb120280 | |||
| 4b26a92b37 | |||
| 0a8453b4d2 | |||
| c9872a61b6 | |||
| 149a1aa278 | |||
| 13d0e1b682 | |||
| 6f9be2a9f8 | |||
| 93ba98d993 | |||
| 940ee3df06 | |||
| 2e7bab10fd | |||
|  | 3ed70a37ea | ||
| fe8f80b9d9 | |||
| d81fb32fb9 | |||
| 2877b5532c | |||
| b938b21395 | |||
| 58836359cc | |||
| 5edb0e1331 | |||
| 0f0d0b5d58 | |||
| 46c1887cbe | |||
| 5f231c2212 | |||
| 4e97ce86aa | |||
| 315a7cb29e | |||
| aa7cc503f2 | |||
| 4bbe7e5a80 | |||
| d9c05f38d2 | |||
| cd0c4c9650 | |||
| fc3407f104 | |||
| 3a5544206b | |||
| e17e2f7fb8 | |||
|  | d32c95f143 | ||
| 6d8a8ab018 | |||
| a7dce8fa78 | |||
| ca73ff8e19 | |||
| d01ad10d7d | |||
| 81041ee43c | |||
|  | 6e004c2ae4 | ||
| 0e2fac5b22 | |||
| 269da7f155 | |||
| 3cb6b73ee0 | |||
| a938ee1efb | |||
| 6ea5e2e5a6 | |||
| 617dfb54e0 | |||
| d23e005985 | |||
| e5207f5bc5 | |||
| c96cea8db0 | |||
| 0a8e71d76a | |||
| cf1fd32b08 | 
							
								
								
									
										50
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,5 +1,55 @@ | |||||||
| # Changelog | # Changelog | ||||||
|  |  | ||||||
|  | ## 0.18.2 | ||||||
|  |  | ||||||
|  | * `Startup`: | ||||||
|  |     * Now internal `Json` is fully customizable | ||||||
|  |  | ||||||
|  | ## 0.18.1 | ||||||
|  |  | ||||||
|  | * `Common`: | ||||||
|  |     * Add `MapDiff` | ||||||
|  | * `Coroutines`: | ||||||
|  |     * Add `SmartMutex` | ||||||
|  |  | ||||||
|  | ## 0.18.0 | ||||||
|  |  | ||||||
|  | **ALL PREVIOUSLY DEPRECATED FUNCTIONALITY HAVE BEEN REMOVED** | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Android Fragments`: `1.5.6` -> `1.5.7` | ||||||
|  | * `Ktor`: | ||||||
|  |     * `Server`: | ||||||
|  |         * Now it is possible to take query parameters as list | ||||||
|  | * `Repos`: | ||||||
|  |     * `Common`: | ||||||
|  |         * New `WriteKeyValuesRepo.removeWithValue` | ||||||
|  |     * `Cache`: | ||||||
|  |         * Rename full caching factories functions to `fullyCached` | ||||||
|  |  | ||||||
|  | ## 0.17.8 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Ktor`: `2.2.4` -> `2.3.0` | ||||||
|  |  | ||||||
|  | ## 0.17.7 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Android CoreKtx`: `1.9.0` -> `1.10.0` | ||||||
|  | * `Startup`: | ||||||
|  |     * Add support of `linuxX64` and `mingwX64` platforms | ||||||
|  |  | ||||||
|  | ## 0.17.6 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Kotlin`: `1.8.10` -> `1.8.20` | ||||||
|  |     * `KSLog`: `1.0.0` -> `1.1.1` | ||||||
|  |     * `Compose`: `1.3.1` -> `1.4.0` | ||||||
|  |     * `Koin`: `3.3.2` -> `3.4.0` | ||||||
|  |     * `RecyclerView`: `1.2.1` -> `1.3.0` | ||||||
|  |     * `Fragment`: `1.5.5` -> `1.5.6` | ||||||
|  | * Experimentally (`!!!`) add `linuxX64` and `mingwX64` targets | ||||||
|  |  | ||||||
| ## 0.17.5 | ## 0.17.5 | ||||||
|  |  | ||||||
| * `Common`: | * `Common`: | ||||||
|   | |||||||
| @@ -23,6 +23,7 @@ allprojects { | |||||||
|         mavenCentral() |         mavenCentral() | ||||||
|         google() |         google() | ||||||
|         maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" } |         maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" } | ||||||
|  |         maven { url "https://git.inmo.dev/api/packages/InsanusMokrassar/maven" } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // temporal crutch until legacy tests will be stabled or legacy target will be removed |     // temporal crutch until legacy tests will be stabled or legacy target will be removed | ||||||
|   | |||||||
| @@ -20,5 +20,16 @@ kotlin { | |||||||
|             } |             } | ||||||
|             dependsOn jvmMain |             dependsOn jvmMain | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         linuxX64Main { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.okio | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         mingwX64Main { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.okio | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -200,20 +200,18 @@ inline fun <T> Iterable<T>.calculateStrictDiff( | |||||||
| ) = calculateDiff(other, strictComparison = true) | ) = calculateDiff(other, strictComparison = true) | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this] |  * Applies [diff] to [this] [MutableList] | ||||||
|  * mutable list |  | ||||||
|  */ |  */ | ||||||
| fun <T> MutableList<T>.applyDiff( | fun <T> MutableList<T>.applyDiff( | ||||||
|     source: Iterable<T>, |     diff: Diff<T> | ||||||
|     strictComparison: Boolean = false | ) { | ||||||
| ): Diff<T> = calculateDiff(source, strictComparison).also { |     for (i in diff.removed.indices.sortedDescending()) { | ||||||
|     for (i in it.removed.indices.sortedDescending()) { |         removeAt(diff.removed[i].index) | ||||||
|         removeAt(it.removed[i].index) |  | ||||||
|     } |     } | ||||||
|     it.added.forEach { (i, t) -> |     diff.added.forEach { (i, t) -> | ||||||
|         add(i, t) |         add(i, t) | ||||||
|     } |     } | ||||||
|     it.replaced.forEach { (_, new) -> |     diff.replaced.forEach { (_, new) -> | ||||||
|         set(new.index, new.value) |         set(new.index, new.value) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -222,17 +220,30 @@ fun <T> MutableList<T>.applyDiff( | |||||||
|  * This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this] |  * This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this] | ||||||
|  * mutable list |  * mutable list | ||||||
|  */ |  */ | ||||||
|  | fun <T> MutableList<T>.applyDiff( | ||||||
|  |     source: Iterable<T>, | ||||||
|  |     strictComparison: Boolean = false | ||||||
|  | ): Diff<T> = calculateDiff(source, strictComparison).also { | ||||||
|  |     applyDiff(it) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This method call [calculateDiff] and then apply differences to [this] | ||||||
|  |  * mutable list | ||||||
|  |  */ | ||||||
| fun <T> MutableList<T>.applyDiff( | fun <T> MutableList<T>.applyDiff( | ||||||
|     source: Iterable<T>, |     source: Iterable<T>, | ||||||
|     comparisonFun: (T?, T?) -> Boolean |     comparisonFun: (T?, T?) -> Boolean | ||||||
| ): Diff<T> = calculateDiff(source, comparisonFun).also { | ): Diff<T> = calculateDiff(source, comparisonFun).also { | ||||||
|     for (i in it.removed.indices.sortedDescending()) { |     applyDiff(it) | ||||||
|         removeAt(it.removed[i].index) |  | ||||||
|     } |  | ||||||
|     it.added.forEach { (i, t) -> |  | ||||||
|         add(i, t) |  | ||||||
|     } |  | ||||||
|     it.replaced.forEach { (_, new) -> |  | ||||||
|         set(new.index, new.value) |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Reverse [this] [Diff]. Result will contain [Diff.added] on [Diff.removed] (and vice-verse), all the | ||||||
|  |  * [Diff.replaced] values will be reversed too | ||||||
|  |  */ | ||||||
|  | fun <T> Diff<T>.reversed() = Diff( | ||||||
|  |     removed = added, | ||||||
|  |     replaced = replaced.map { it.second to it.first }, | ||||||
|  |     added = removed | ||||||
|  | ) | ||||||
|   | |||||||
| @@ -0,0 +1,135 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Contains diff based on the comparison of objects with the same [K]. | ||||||
|  |  * | ||||||
|  |  * @param removed Contains map with keys removed from parent map | ||||||
|  |  * @param changed Contains map with keys values changed new map in comparison with old one | ||||||
|  |  * @param added Contains map with new keys and values | ||||||
|  |  */ | ||||||
|  | data class MapDiff<K, V> @Warning(warning) constructor( | ||||||
|  |     val removed: Map<K, V>, | ||||||
|  |     val changed: Map<K, Pair<V, V>>, | ||||||
|  |     val added: Map<K, V> | ||||||
|  | ) { | ||||||
|  |     fun isEmpty() = removed.isEmpty() && changed.isEmpty() && added.isEmpty() | ||||||
|  |     inline fun isNotEmpty() = !isEmpty() | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private const val warning = "This feature can be changed without any warranties. Use with caution and only in case you know what you are doing" | ||||||
|  |         fun <K, V> empty() = MapDiff<K, V>(emptyMap(), emptyMap(), emptyMap()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | private inline fun <K, V> createCompareFun( | ||||||
|  |     strictComparison: Boolean | ||||||
|  | ): (K, V, V) -> Boolean = if (strictComparison) { | ||||||
|  |     { _, first, second -> first === second } | ||||||
|  | } else { | ||||||
|  |     { _, first, second -> first == second } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Compare [this] [Map] with the [other] one in principle when [other] is newer than [this] | ||||||
|  |  * | ||||||
|  |  * @param compareFun Will be used to determine changed values | ||||||
|  |  */ | ||||||
|  | fun <K, V> Map<K, V>.diff( | ||||||
|  |     other: Map<K, V>, | ||||||
|  |     compareFun: (K, V, V) -> Boolean | ||||||
|  | ): MapDiff<K, V> { | ||||||
|  |     val removed: Map<K, V> = (keys - other.keys).associateWith { | ||||||
|  |         getValue(it) | ||||||
|  |     } | ||||||
|  |     val added: Map<K, V> = (other.keys - keys).associateWith { | ||||||
|  |         other.getValue(it) | ||||||
|  |     } | ||||||
|  |     val changed = keys.intersect(other.keys).mapNotNull { | ||||||
|  |         val old = getValue(it) | ||||||
|  |         val new = other.getValue(it) | ||||||
|  |         if (compareFun(it, old, new)) { | ||||||
|  |             return@mapNotNull null | ||||||
|  |         } else { | ||||||
|  |             it to (old to new) | ||||||
|  |         } | ||||||
|  |     }.toMap() | ||||||
|  |  | ||||||
|  |     return MapDiff( | ||||||
|  |         removed, | ||||||
|  |         changed, | ||||||
|  |         added | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Compare [this] [Map] with the [other] one in principle when [other] is newer than [this] | ||||||
|  |  * | ||||||
|  |  * @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard | ||||||
|  |  * `equals` will be used | ||||||
|  |  */ | ||||||
|  | fun <K, V> Map<K, V>.diff( | ||||||
|  |     other: Map<K, V>, | ||||||
|  |     strictComparison: Boolean = false | ||||||
|  | ): MapDiff<K, V> = diff( | ||||||
|  |     other, | ||||||
|  |     compareFun = createCompareFun(strictComparison) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will apply [mapDiff] to [this] [MutableMap] | ||||||
|  |  */ | ||||||
|  | fun <K, V> MutableMap<K, V>.applyDiff( | ||||||
|  |     mapDiff: MapDiff<K, V> | ||||||
|  | ) { | ||||||
|  |     mapDiff.apply { | ||||||
|  |         removed.keys.forEach { remove(it) } | ||||||
|  |         changed.forEach { (k, oldNew) -> | ||||||
|  |             put(k, oldNew.second) | ||||||
|  |         } | ||||||
|  |         added.forEach { (k, new) -> | ||||||
|  |             put(k, new) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will apply changes with [from] map into [this] one | ||||||
|  |  * | ||||||
|  |  * @param compareFun Will be used to determine changed values | ||||||
|  |  * | ||||||
|  |  * @return [MapDiff] applied to [this] [MutableMap] | ||||||
|  |  */ | ||||||
|  | fun <K, V> MutableMap<K, V>.applyDiff( | ||||||
|  |     from: Map<K, V>, | ||||||
|  |     compareFun: (K, V, V) -> Boolean | ||||||
|  | ): MapDiff<K, V> { | ||||||
|  |     return diff(from, compareFun).also { | ||||||
|  |         applyDiff(it) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will apply changes with [from] map into [this] one | ||||||
|  |  * | ||||||
|  |  * @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard | ||||||
|  |  * `equals` will be used | ||||||
|  |  * | ||||||
|  |  * @return [MapDiff] applied to [this] [MutableMap] | ||||||
|  |  */ | ||||||
|  | fun <K, V> MutableMap<K, V>.applyDiff( | ||||||
|  |     from: Map<K, V>, | ||||||
|  |     strictComparison: Boolean = false | ||||||
|  | ): MapDiff<K, V> = applyDiff( | ||||||
|  |     from, | ||||||
|  |     compareFun = createCompareFun(strictComparison) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Reverse [this] [MapDiff]. Result will contain [MapDiff.added] on [MapDiff.removed] (and vice-verse), all the | ||||||
|  |  * [MapDiff.changed] values will be reversed too | ||||||
|  |  */ | ||||||
|  | fun <K, V> MapDiff<K, V>.reversed(): MapDiff<K, V> = MapDiff( | ||||||
|  |     removed = added, | ||||||
|  |     changed = changed.mapValues { (_, oldNew) -> oldNew.second to oldNew.first }, | ||||||
|  |     added = removed | ||||||
|  | ) | ||||||
							
								
								
									
										36
									
								
								common/src/linuxX64Main/kotlin/ActualMPPFile.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								common/src/linuxX64Main/kotlin/ActualMPPFile.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import okio.FileSystem | ||||||
|  | import okio.Path | ||||||
|  | import okio.use | ||||||
|  |  | ||||||
|  | actual typealias MPPFile = Path | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.filename: FileName | ||||||
|  |     get() = FileName(toString()) | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.filesize: Long | ||||||
|  |     get() = FileSystem.SYSTEM.openReadOnly(this).use { | ||||||
|  |         it.size() | ||||||
|  |     } | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator | ||||||
|  |     get() = { | ||||||
|  |         FileSystem.SYSTEM.read(this) { | ||||||
|  |             readByteArray() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator | ||||||
|  |     get() = { | ||||||
|  |         bytesAllocatorSync() | ||||||
|  |     } | ||||||
							
								
								
									
										26
									
								
								common/src/linuxX64Main/kotlin/fixed.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								common/src/linuxX64Main/kotlin/fixed.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.cinterop.ByteVar | ||||||
|  | import kotlinx.cinterop.allocArray | ||||||
|  | import kotlinx.cinterop.memScoped | ||||||
|  | import kotlinx.cinterop.toKString | ||||||
|  | import platform.posix.snprintf | ||||||
|  | import platform.posix.sprintf | ||||||
|  |  | ||||||
|  | actual fun Float.fixed(signs: Int): Float { | ||||||
|  |     return memScoped { | ||||||
|  |         val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2) | ||||||
|  |  | ||||||
|  |         sprintf(buff, "%.${signs}f", this@fixed) | ||||||
|  |         buff.toKString().toFloat() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | actual fun Double.fixed(signs: Int): Double { | ||||||
|  |     return memScoped { | ||||||
|  |         val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2) | ||||||
|  |  | ||||||
|  |         sprintf(buff, "%.${signs}f", this@fixed) | ||||||
|  |         buff.toKString().toDouble() | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										36
									
								
								common/src/mingwX64Main/kotlin/ActualMPPFile.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								common/src/mingwX64Main/kotlin/ActualMPPFile.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import okio.FileSystem | ||||||
|  | import okio.Path | ||||||
|  | import okio.use | ||||||
|  |  | ||||||
|  | actual typealias MPPFile = Path | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.filename: FileName | ||||||
|  |     get() = FileName(toString()) | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.filesize: Long | ||||||
|  |     get() = FileSystem.SYSTEM.openReadOnly(this).use { | ||||||
|  |         it.size() | ||||||
|  |     } | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator | ||||||
|  |     get() = { | ||||||
|  |         FileSystem.SYSTEM.read(this) { | ||||||
|  |             readByteArray() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator | ||||||
|  |     get() = { | ||||||
|  |         bytesAllocatorSync() | ||||||
|  |     } | ||||||
							
								
								
									
										26
									
								
								common/src/mingwX64Main/kotlin/fixed.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								common/src/mingwX64Main/kotlin/fixed.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.cinterop.ByteVar | ||||||
|  | import kotlinx.cinterop.allocArray | ||||||
|  | import kotlinx.cinterop.memScoped | ||||||
|  | import kotlinx.cinterop.toKString | ||||||
|  | import platform.posix.snprintf | ||||||
|  | import platform.posix.sprintf | ||||||
|  |  | ||||||
|  | actual fun Float.fixed(signs: Int): Float { | ||||||
|  |     return memScoped { | ||||||
|  |         val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2) | ||||||
|  |  | ||||||
|  |         sprintf(buff, "%.${signs}f", this@fixed) | ||||||
|  |         buff.toKString().toFloat() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | actual fun Double.fixed(signs: Int): Double { | ||||||
|  |     return memScoped { | ||||||
|  |         val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2) | ||||||
|  |  | ||||||
|  |         sprintf(buff, "%.${signs}f", this@fixed) | ||||||
|  |         buff.toKString().toDouble() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,21 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.coroutines.compose |  | ||||||
|  |  | ||||||
| import androidx.compose.runtime.MutableState |  | ||||||
| import androidx.compose.runtime.mutableStateOf |  | ||||||
| import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions |  | ||||||
| import kotlinx.coroutines.CoroutineScope |  | ||||||
| import kotlinx.coroutines.flow.Flow |  | ||||||
| import kotlinx.coroutines.flow.StateFlow |  | ||||||
|  |  | ||||||
| @Deprecated("Duplicated functionality", ReplaceWith("asMutableComposeState(initial, scope)", "dev.inmo.micro_utils.coroutines.compose.asMutableComposeState")) |  | ||||||
| fun <T> Flow<T>.toMutableState( |  | ||||||
|     initial: T, |  | ||||||
|     scope: CoroutineScope |  | ||||||
| ): MutableState<T> = asMutableComposeState(initial, scope) |  | ||||||
|  |  | ||||||
| @Deprecated("Duplicated functionality", ReplaceWith("asMutableComposeState(scope)", "dev.inmo.micro_utils.coroutines.compose.asMutableComposeState")) |  | ||||||
| @Suppress("NOTHING_TO_INLINE") |  | ||||||
| inline fun <T> StateFlow<T>.toMutableState( |  | ||||||
|     scope: CoroutineScope |  | ||||||
| ): MutableState<T> = asMutableComposeState(scope) |  | ||||||
|  |  | ||||||
| @@ -0,0 +1,136 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.currentCoroutineContext | ||||||
|  | import kotlinx.coroutines.flow.MutableStateFlow | ||||||
|  | import kotlinx.coroutines.flow.StateFlow | ||||||
|  | import kotlinx.coroutines.flow.asStateFlow | ||||||
|  | import kotlinx.coroutines.flow.first | ||||||
|  | import kotlinx.coroutines.isActive | ||||||
|  | import kotlinx.coroutines.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  | import kotlin.contracts.ExperimentalContracts | ||||||
|  | import kotlin.contracts.InvocationKind | ||||||
|  | import kotlin.contracts.contract | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * It is interface which will work like classic [Mutex], but in difference have [lockStateFlow] for listening of the | ||||||
|  |  * [SmartMutex] state. | ||||||
|  |  * | ||||||
|  |  * There is [Mutable] and [Immutable] realizations. In case you are owner and manager current state of lock, you need | ||||||
|  |  * [Mutable] [SmartMutex]. Otherwise, [Immutable]. | ||||||
|  |  * | ||||||
|  |  * Any [Mutable] [SmartMutex] may produce its [Immutable] variant which will contains [lockStateFlow] equal to its | ||||||
|  |  * [Mutable] creator | ||||||
|  |  */ | ||||||
|  | sealed interface SmartMutex { | ||||||
|  |     val lockStateFlow: StateFlow<Boolean> | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * * True - locked | ||||||
|  |      * * False - unlocked | ||||||
|  |      */ | ||||||
|  |     val isLocked: Boolean | ||||||
|  |         get() = lockStateFlow.value | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Immutable variant of [SmartMutex]. In fact will depend on the owner of [lockStateFlow] | ||||||
|  |      */ | ||||||
|  |     class Immutable(override val lockStateFlow: StateFlow<Boolean>) : SmartMutex | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Mutable variant of [SmartMutex]. With that variant you may [lock] and [unlock]. Besides, you may create | ||||||
|  |      * [Immutable] variant of [this] instance with [immutable] factory | ||||||
|  |      * | ||||||
|  |      * @param locked Preset state of [isLocked] and its internal [_lockStateFlow] | ||||||
|  |      */ | ||||||
|  |     class Mutable(locked: Boolean = false) : SmartMutex { | ||||||
|  |         private val _lockStateFlow = MutableStateFlow<Boolean>(locked) | ||||||
|  |         override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow() | ||||||
|  |  | ||||||
|  |         private val internalChangesMutex = Mutex() | ||||||
|  |  | ||||||
|  |         fun immutable() = Immutable(lockStateFlow) | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * Holds call until this [SmartMutex] will be re-locked. That means that while [isLocked] == true, [holds] will | ||||||
|  |          * wait for [isLocked] == false and then try to lock | ||||||
|  |          */ | ||||||
|  |         suspend fun lock() { | ||||||
|  |             do { | ||||||
|  |                 waitUnlock() | ||||||
|  |                 val shouldContinue = internalChangesMutex.withLock { | ||||||
|  |                     if (_lockStateFlow.value) { | ||||||
|  |                         true | ||||||
|  |                     } else { | ||||||
|  |                         _lockStateFlow.value = true | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } while (shouldContinue && currentCoroutineContext().isActive) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * Will try to lock this [SmartMutex] immediataly | ||||||
|  |          * | ||||||
|  |          * @return True if lock was successful. False otherwise | ||||||
|  |          */ | ||||||
|  |         suspend fun tryLock(): Boolean { | ||||||
|  |             return if (!_lockStateFlow.value) { | ||||||
|  |                 internalChangesMutex.withLock { | ||||||
|  |                     if (!_lockStateFlow.value) { | ||||||
|  |                         _lockStateFlow.value = true | ||||||
|  |                         true | ||||||
|  |                     } else { | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 false | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * If [isLocked] == true - will change it to false and return true. If current call will not unlock this | ||||||
|  |          * [SmartMutex] - false | ||||||
|  |          */ | ||||||
|  |         suspend fun unlock(): Boolean { | ||||||
|  |             return if (_lockStateFlow.value) { | ||||||
|  |                 internalChangesMutex.withLock { | ||||||
|  |                     if (_lockStateFlow.value) { | ||||||
|  |                         _lockStateFlow.value = false | ||||||
|  |                         true | ||||||
|  |                     } else { | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 false | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [SmartMutex.Mutable.lock], then execute [action] and return the result after [SmartMutex.Mutable.unlock] | ||||||
|  |  */ | ||||||
|  | @OptIn(ExperimentalContracts::class) | ||||||
|  | suspend inline fun <T> SmartMutex.Mutable.withLock(action: () -> T): T { | ||||||
|  |     contract { | ||||||
|  |         callsInPlace(action, InvocationKind.EXACTLY_ONCE) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     lock() | ||||||
|  |     try { | ||||||
|  |         return action() | ||||||
|  |     } finally { | ||||||
|  |         unlock() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will wait until the [SmartMutex.lockStateFlow] of [this] instance will be false. | ||||||
|  |  * | ||||||
|  |  * Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact | ||||||
|  |  * that some other parties may lock it again | ||||||
|  |  */ | ||||||
|  | suspend fun SmartMutex.waitUnlock() = lockStateFlow.first { !it } | ||||||
| @@ -0,0 +1,26 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.CancellationException | ||||||
|  | import kotlinx.coroutines.Job | ||||||
|  | import org.w3c.dom.Image | ||||||
|  |  | ||||||
|  | suspend fun preloadImage(src: String): Image { | ||||||
|  |     val image = Image() | ||||||
|  |     image.src = src | ||||||
|  |  | ||||||
|  |     val job = Job() | ||||||
|  |  | ||||||
|  |     image.addEventListener("load", { | ||||||
|  |         runCatching { job.complete() } | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     runCatchingSafely { | ||||||
|  |         job.join() | ||||||
|  |     }.onFailure { | ||||||
|  |         if (it is CancellationException) { | ||||||
|  |             image.src = "" | ||||||
|  |         } | ||||||
|  |     }.getOrThrow() | ||||||
|  |  | ||||||
|  |     return image | ||||||
|  | } | ||||||
| @@ -11,6 +11,7 @@ kotlin { | |||||||
|         commonMain { |         commonMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api project(":micro_utils.common") |                 api project(":micro_utils.common") | ||||||
|  |                 api libs.krypto | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         jsMain { |         jsMain { | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
| package dev.inmo.micro_utils.crypto | package dev.inmo.micro_utils.crypto | ||||||
|  |  | ||||||
|  | import com.soywiz.krypto.md5 | ||||||
|  |  | ||||||
| typealias MD5 = String | typealias MD5 = String | ||||||
|  |  | ||||||
| expect fun SourceBytes.md5(): MD5 | fun SourceBytes.md5(): MD5 = md5().hexLower | ||||||
| fun SourceString.md5(): MD5 = encodeToByteArray().md5() | fun SourceString.md5(): MD5 = encodeToByteArray().md5().hexLower | ||||||
|   | |||||||
| @@ -1,6 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.crypto |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @suppress |  | ||||||
|  */ |  | ||||||
| actual fun SourceBytes.md5(): MD5 = CryptoJS.MD5(decodeToString()) |  | ||||||
| @@ -1,12 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.crypto |  | ||||||
|  |  | ||||||
| import java.math.BigInteger |  | ||||||
| import java.security.MessageDigest |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @suppress |  | ||||||
|  */ |  | ||||||
| actual fun SourceBytes.md5(): MD5 = BigInteger( |  | ||||||
|     1, |  | ||||||
|     MessageDigest.getInstance("MD5").digest(this) |  | ||||||
| ).toString(16) |  | ||||||
| @@ -23,7 +23,7 @@ allprojects { | |||||||
|         mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle" |         mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle" | ||||||
|         mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle" |         mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle" | ||||||
|         mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle" |         mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle" | ||||||
|         mppJsAndJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJsAndJavaProject.gradle" |         mppJvmJsLinuxMingwProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsLinuxMingwProject.gradle" | ||||||
|         mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle" |         mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle" | ||||||
|  |  | ||||||
|         defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle" |         defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle" | ||||||
|   | |||||||
| @@ -14,5 +14,5 @@ crypto_js_version=4.1.1 | |||||||
| # Project data | # Project data | ||||||
|  |  | ||||||
| group=dev.inmo | group=dev.inmo | ||||||
| version=0.17.5 | version=0.18.2 | ||||||
| android_code_version=187 | android_code_version=193 | ||||||
|   | |||||||
| @@ -1,40 +1,42 @@ | |||||||
| [versions] | [versions] | ||||||
|  |  | ||||||
| kt = "1.8.10" | kt = "1.8.20" | ||||||
| kt-serialization = "1.5.0" | kt-serialization = "1.5.0" | ||||||
| kt-coroutines = "1.6.4" | kt-coroutines = "1.6.4" | ||||||
|  |  | ||||||
| kslog = "1.0.0" | kslog = "1.1.1" | ||||||
|  |  | ||||||
| jb-compose = "1.3.1" | jb-compose = "1.4.0" | ||||||
| jb-exposed = "0.41.1" | jb-exposed = "0.41.1" | ||||||
| jb-dokka = "1.8.10" | jb-dokka = "1.8.10" | ||||||
|  |  | ||||||
| klock = "3.4.0" | korlibs = "3.4.0" | ||||||
| uuid = "0.7.0" | uuid = "0.7.0" | ||||||
|  |  | ||||||
| ktor = "2.2.4" | ktor = "2.3.0" | ||||||
|  |  | ||||||
| gh-release = "2.4.1" | gh-release = "2.4.1" | ||||||
|  |  | ||||||
| koin = "3.3.2" | koin = "3.4.0" | ||||||
|  |  | ||||||
| ksp = "1.8.10-1.0.9" | okio = "3.3.0" | ||||||
| kotlin-poet = "1.12.0" |  | ||||||
|  |  | ||||||
| android-gradle = "7.3.1" | ksp = "1.8.20-1.0.11" | ||||||
|  | kotlin-poet = "1.13.0" | ||||||
|  |  | ||||||
|  | android-gradle = "7.4.2" | ||||||
| dexcount = "4.0.0" | dexcount = "4.0.0" | ||||||
|  |  | ||||||
| android-coreKtx = "1.9.0" | android-coreKtx = "1.10.0" | ||||||
| android-recyclerView = "1.2.1" | android-recyclerView = "1.3.0" | ||||||
| android-appCompat = "1.6.1" | android-appCompat = "1.6.1" | ||||||
| android-fragment = "1.5.5" | android-fragment = "1.5.7" | ||||||
| android-espresso = "3.5.1" | android-espresso = "3.5.1" | ||||||
| android-test = "1.1.5" | android-test = "1.1.5" | ||||||
|  |  | ||||||
| android-props-minSdk = "21" | android-props-minSdk = "21" | ||||||
| android-props-compileSdk = "33" | android-props-compileSdk = "33" | ||||||
| android-props-buildTools = "33.0.1" | android-props-buildTools = "33.0.2" | ||||||
|  |  | ||||||
| [libraries] | [libraries] | ||||||
|  |  | ||||||
| @@ -67,7 +69,8 @@ ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negoti | |||||||
|  |  | ||||||
| kslog = { module = "dev.inmo:kslog", version.ref = "kslog" } | kslog = { module = "dev.inmo:kslog", version.ref = "kslog" } | ||||||
|  |  | ||||||
| klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "klock" } | klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "korlibs" } | ||||||
|  | krypto = { module = "com.soywiz.korlibs.krypto:krypto", version.ref = "korlibs" } | ||||||
| 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" } | koin = { module = "io.insert-koin:koin-core", version.ref = "koin" } | ||||||
|  |  | ||||||
| @@ -91,6 +94,8 @@ kt-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref | |||||||
| kotlin-poet = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlin-poet" } | kotlin-poet = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlin-poet" } | ||||||
| ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } | ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } | ||||||
|  |  | ||||||
|  | okio = { module = "com.squareup.okio:okio", version.ref = "okio" } | ||||||
|  |  | ||||||
| # Buildscript | # Buildscript | ||||||
|  |  | ||||||
| buildscript-kt-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kt" } | buildscript-kt-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kt" } | ||||||
|   | |||||||
| @@ -18,5 +18,17 @@ kotlin { | |||||||
|         androidMain { |         androidMain { | ||||||
|             dependsOn jvmMain |             dependsOn jvmMain | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         linuxX64Main { | ||||||
|  |             dependencies { | ||||||
|  |                 api internalProject("micro_utils.mime_types") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         mingwX64Main { | ||||||
|  |             dependencies { | ||||||
|  |                 api internalProject("micro_utils.mime_types") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,10 @@ | |||||||
| package dev.inmo.micro_utils.ktor.client | package dev.inmo.micro_utils.ktor.client | ||||||
|  |  | ||||||
| import dev.inmo.micro_utils.common.MPPFile | import dev.inmo.micro_utils.common.MPPFile | ||||||
|  | import dev.inmo.micro_utils.common.filesize | ||||||
|  | import dev.inmo.micro_utils.ktor.common.input | ||||||
| import io.ktor.client.request.forms.InputProvider | import io.ktor.client.request.forms.InputProvider | ||||||
|  |  | ||||||
| expect suspend fun MPPFile.inputProvider(): InputProvider | fun MPPFile.inputProvider(): InputProvider = InputProvider(filesize) { | ||||||
|  |     input() | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,11 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.ktor.client |  | ||||||
|  |  | ||||||
| import dev.inmo.micro_utils.common.* |  | ||||||
| import io.ktor.client.request.forms.InputProvider |  | ||||||
| import io.ktor.utils.io.core.ByteReadPacket |  | ||||||
|  |  | ||||||
| actual suspend fun MPPFile.inputProvider(): InputProvider = bytes().let { |  | ||||||
|     InputProvider(it.size.toLong()) { |  | ||||||
|         ByteReadPacket(it) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -7,5 +7,3 @@ import io.ktor.utils.io.streams.asInput | |||||||
| fun MPPFile.inputProviderSync(): InputProvider = InputProvider(length()) { | fun MPPFile.inputProviderSync(): InputProvider = InputProvider(length()) { | ||||||
|     inputStream().asInput() |     inputStream().asInput() | ||||||
| } | } | ||||||
|  |  | ||||||
| actual suspend fun MPPFile.inputProvider(): InputProvider = inputProviderSync() |  | ||||||
|   | |||||||
							
								
								
									
										40
									
								
								ktor/client/src/linuxX64Main/kotlin/ActualTemporalUpload.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								ktor/client/src/linuxX64Main/kotlin/ActualTemporalUpload.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | 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.TemporalFileId | ||||||
|  | import dev.inmo.micro_utils.mime_types.getMimeTypeOrAny | ||||||
|  | import io.ktor.client.HttpClient | ||||||
|  | import io.ktor.client.plugins.onUpload | ||||||
|  | import io.ktor.client.request.forms.formData | ||||||
|  | import io.ktor.client.request.forms.submitFormWithBinaryData | ||||||
|  | import io.ktor.client.statement.bodyAsText | ||||||
|  | import io.ktor.http.Headers | ||||||
|  | import io.ktor.http.HttpHeaders | ||||||
|  |  | ||||||
|  | internal val MPPFile.mimeType: String | ||||||
|  |     get() = getMimeTypeOrAny(filename.extension).raw | ||||||
|  |  | ||||||
|  | actual suspend fun HttpClient.tempUpload( | ||||||
|  |     fullTempUploadDraftPath: String, | ||||||
|  |     file: MPPFile, | ||||||
|  |     onUpload: OnUploadCallback | ||||||
|  | ): TemporalFileId { | ||||||
|  |     val inputProvider = file.inputProvider() | ||||||
|  |     val fileId = submitFormWithBinaryData( | ||||||
|  |         fullTempUploadDraftPath, | ||||||
|  |         formData = formData { | ||||||
|  |             append( | ||||||
|  |                 "data", | ||||||
|  |                 inputProvider, | ||||||
|  |                 Headers.build { | ||||||
|  |                     append(HttpHeaders.ContentType, file.mimeType) | ||||||
|  |                     append(HttpHeaders.ContentDisposition, "filename=\"${file.filename.string}\"") | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|  |         onUpload(onUpload) | ||||||
|  |     }.bodyAsText() | ||||||
|  |     return TemporalFileId(fileId) | ||||||
|  | } | ||||||
							
								
								
									
										107
									
								
								ktor/client/src/linuxX64Main/kotlin/ActualUniUpload.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								ktor/client/src/linuxX64Main/kotlin/ActualUniUpload.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | |||||||
|  | package dev.inmo.micro_utils.ktor.client | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.MPPFile | ||||||
|  | import dev.inmo.micro_utils.common.Progress | ||||||
|  | import io.ktor.client.HttpClient | ||||||
|  | import io.ktor.client.engine.mergeHeaders | ||||||
|  | import io.ktor.client.plugins.onUpload | ||||||
|  | import io.ktor.client.request.HttpRequestBuilder | ||||||
|  | import io.ktor.client.request.forms.InputProvider | ||||||
|  | import io.ktor.client.request.forms.formData | ||||||
|  | import io.ktor.client.request.forms.submitForm | ||||||
|  | import io.ktor.client.request.forms.submitFormWithBinaryData | ||||||
|  | import io.ktor.client.request.headers | ||||||
|  | import io.ktor.client.statement.bodyAsText | ||||||
|  | import io.ktor.http.Headers | ||||||
|  | import io.ktor.http.HttpHeaders | ||||||
|  | import io.ktor.http.HttpStatusCode | ||||||
|  | import io.ktor.http.Parameters | ||||||
|  | import io.ktor.http.content.PartData | ||||||
|  | import kotlinx.serialization.DeserializationStrategy | ||||||
|  | import kotlinx.serialization.InternalSerializationApi | ||||||
|  | import kotlinx.serialization.SerializationStrategy | ||||||
|  | import kotlinx.serialization.StringFormat | ||||||
|  | import kotlinx.serialization.encodeToString | ||||||
|  | import kotlinx.serialization.serializer | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will execute submitting of multipart data request | ||||||
|  |  * | ||||||
|  |  * @param data [Map] where keys will be used as names for multipart parts and values as values. If you will pass | ||||||
|  |  * [dev.inmo.micro_utils.common.MPPFile] (File from JS or JVM platform). Also you may pass [UniUploadFileInfo] as value | ||||||
|  |  * in case you wish to pass other source of multipart binary data than regular file | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | @OptIn(InternalSerializationApi::class) | ||||||
|  | actual suspend fun <T> HttpClient.uniUpload( | ||||||
|  |     url: String, | ||||||
|  |     data: Map<String, Any>, | ||||||
|  |     resultDeserializer: DeserializationStrategy<T>, | ||||||
|  |     headers: Headers, | ||||||
|  |     stringFormat: StringFormat, | ||||||
|  |     onUpload: OnUploadCallback | ||||||
|  | ): T? { | ||||||
|  |     val withBinary = data.values.any { it is MPPFile || it is UniUploadFileInfo } | ||||||
|  |  | ||||||
|  |     val formData = formData { | ||||||
|  |         for (k in data.keys) { | ||||||
|  |             val v = data[k] ?: continue | ||||||
|  |             when (v) { | ||||||
|  |                 is MPPFile -> append( | ||||||
|  |                     k, | ||||||
|  |                     v.inputProvider(), | ||||||
|  |                     Headers.build { | ||||||
|  |                         append(HttpHeaders.ContentType, v.mimeType) | ||||||
|  |                         append(HttpHeaders.ContentDisposition, "filename=\"${v.name}\"") | ||||||
|  |                     } | ||||||
|  |                 ) | ||||||
|  |                 is UniUploadFileInfo -> append( | ||||||
|  |                     k, | ||||||
|  |                     InputProvider(block = v.inputAllocator), | ||||||
|  |                     Headers.build { | ||||||
|  |                         append(HttpHeaders.ContentType, v.mimeType) | ||||||
|  |                         append(HttpHeaders.ContentDisposition, "filename=\"${v.fileName.name}\"") | ||||||
|  |                     } | ||||||
|  |                 ) | ||||||
|  |                 else -> append( | ||||||
|  |                     k, | ||||||
|  |                     stringFormat.encodeToString(v::class.serializer() as SerializationStrategy<in Any>, v) | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val requestBuilder: HttpRequestBuilder.() -> Unit = { | ||||||
|  |         headers { | ||||||
|  |             appendAll(headers) | ||||||
|  |         } | ||||||
|  |         onUpload { bytesSentTotal, contentLength -> | ||||||
|  |             onUpload(bytesSentTotal, contentLength) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val response = if (withBinary) { | ||||||
|  |         submitFormWithBinaryData( | ||||||
|  |             url, | ||||||
|  |             formData, | ||||||
|  |             block = requestBuilder | ||||||
|  |         ) | ||||||
|  |     } else { | ||||||
|  |         submitForm( | ||||||
|  |             url, | ||||||
|  |             Parameters.build { | ||||||
|  |                 for (it in formData) { | ||||||
|  |                     val formItem = (it as PartData.FormItem) | ||||||
|  |                     append(it.name!!, it.value) | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             block = requestBuilder | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return if (response.status == HttpStatusCode.OK) { | ||||||
|  |         stringFormat.decodeFromString(resultDeserializer, response.bodyAsText()) | ||||||
|  |     } else { | ||||||
|  |         null | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										40
									
								
								ktor/client/src/mingwX64Main/kotlin/ActualTemporalUpload.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								ktor/client/src/mingwX64Main/kotlin/ActualTemporalUpload.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | 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.TemporalFileId | ||||||
|  | import dev.inmo.micro_utils.mime_types.getMimeTypeOrAny | ||||||
|  | import io.ktor.client.HttpClient | ||||||
|  | import io.ktor.client.plugins.onUpload | ||||||
|  | import io.ktor.client.request.forms.formData | ||||||
|  | import io.ktor.client.request.forms.submitFormWithBinaryData | ||||||
|  | import io.ktor.client.statement.bodyAsText | ||||||
|  | import io.ktor.http.Headers | ||||||
|  | import io.ktor.http.HttpHeaders | ||||||
|  |  | ||||||
|  | internal val MPPFile.mimeType: String | ||||||
|  |     get() = getMimeTypeOrAny(filename.extension).raw | ||||||
|  |  | ||||||
|  | actual suspend fun HttpClient.tempUpload( | ||||||
|  |     fullTempUploadDraftPath: String, | ||||||
|  |     file: MPPFile, | ||||||
|  |     onUpload: OnUploadCallback | ||||||
|  | ): TemporalFileId { | ||||||
|  |     val inputProvider = file.inputProvider() | ||||||
|  |     val fileId = submitFormWithBinaryData( | ||||||
|  |         fullTempUploadDraftPath, | ||||||
|  |         formData = formData { | ||||||
|  |             append( | ||||||
|  |                 "data", | ||||||
|  |                 inputProvider, | ||||||
|  |                 Headers.build { | ||||||
|  |                     append(HttpHeaders.ContentType, file.mimeType) | ||||||
|  |                     append(HttpHeaders.ContentDisposition, "filename=\"${file.filename.string}\"") | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|  |         onUpload(onUpload) | ||||||
|  |     }.bodyAsText() | ||||||
|  |     return TemporalFileId(fileId) | ||||||
|  | } | ||||||
							
								
								
									
										107
									
								
								ktor/client/src/mingwX64Main/kotlin/ActualUniUpload.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								ktor/client/src/mingwX64Main/kotlin/ActualUniUpload.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | |||||||
|  | package dev.inmo.micro_utils.ktor.client | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.MPPFile | ||||||
|  | import dev.inmo.micro_utils.common.Progress | ||||||
|  | import io.ktor.client.HttpClient | ||||||
|  | import io.ktor.client.engine.mergeHeaders | ||||||
|  | import io.ktor.client.plugins.onUpload | ||||||
|  | import io.ktor.client.request.HttpRequestBuilder | ||||||
|  | import io.ktor.client.request.forms.InputProvider | ||||||
|  | import io.ktor.client.request.forms.formData | ||||||
|  | import io.ktor.client.request.forms.submitForm | ||||||
|  | import io.ktor.client.request.forms.submitFormWithBinaryData | ||||||
|  | import io.ktor.client.request.headers | ||||||
|  | import io.ktor.client.statement.bodyAsText | ||||||
|  | import io.ktor.http.Headers | ||||||
|  | import io.ktor.http.HttpHeaders | ||||||
|  | import io.ktor.http.HttpStatusCode | ||||||
|  | import io.ktor.http.Parameters | ||||||
|  | import io.ktor.http.content.PartData | ||||||
|  | import kotlinx.serialization.DeserializationStrategy | ||||||
|  | import kotlinx.serialization.InternalSerializationApi | ||||||
|  | import kotlinx.serialization.SerializationStrategy | ||||||
|  | import kotlinx.serialization.StringFormat | ||||||
|  | import kotlinx.serialization.encodeToString | ||||||
|  | import kotlinx.serialization.serializer | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will execute submitting of multipart data request | ||||||
|  |  * | ||||||
|  |  * @param data [Map] where keys will be used as names for multipart parts and values as values. If you will pass | ||||||
|  |  * [dev.inmo.micro_utils.common.MPPFile] (File from JS or JVM platform). Also you may pass [UniUploadFileInfo] as value | ||||||
|  |  * in case you wish to pass other source of multipart binary data than regular file | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | @OptIn(InternalSerializationApi::class) | ||||||
|  | actual suspend fun <T> HttpClient.uniUpload( | ||||||
|  |     url: String, | ||||||
|  |     data: Map<String, Any>, | ||||||
|  |     resultDeserializer: DeserializationStrategy<T>, | ||||||
|  |     headers: Headers, | ||||||
|  |     stringFormat: StringFormat, | ||||||
|  |     onUpload: OnUploadCallback | ||||||
|  | ): T? { | ||||||
|  |     val withBinary = data.values.any { it is MPPFile || it is UniUploadFileInfo } | ||||||
|  |  | ||||||
|  |     val formData = formData { | ||||||
|  |         for (k in data.keys) { | ||||||
|  |             val v = data[k] ?: continue | ||||||
|  |             when (v) { | ||||||
|  |                 is MPPFile -> append( | ||||||
|  |                     k, | ||||||
|  |                     v.inputProvider(), | ||||||
|  |                     Headers.build { | ||||||
|  |                         append(HttpHeaders.ContentType, v.mimeType) | ||||||
|  |                         append(HttpHeaders.ContentDisposition, "filename=\"${v.name}\"") | ||||||
|  |                     } | ||||||
|  |                 ) | ||||||
|  |                 is UniUploadFileInfo -> append( | ||||||
|  |                     k, | ||||||
|  |                     InputProvider(block = v.inputAllocator), | ||||||
|  |                     Headers.build { | ||||||
|  |                         append(HttpHeaders.ContentType, v.mimeType) | ||||||
|  |                         append(HttpHeaders.ContentDisposition, "filename=\"${v.fileName.name}\"") | ||||||
|  |                     } | ||||||
|  |                 ) | ||||||
|  |                 else -> append( | ||||||
|  |                     k, | ||||||
|  |                     stringFormat.encodeToString(v::class.serializer() as SerializationStrategy<in Any>, v) | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val requestBuilder: HttpRequestBuilder.() -> Unit = { | ||||||
|  |         headers { | ||||||
|  |             appendAll(headers) | ||||||
|  |         } | ||||||
|  |         onUpload { bytesSentTotal, contentLength -> | ||||||
|  |             onUpload(bytesSentTotal, contentLength) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val response = if (withBinary) { | ||||||
|  |         submitFormWithBinaryData( | ||||||
|  |             url, | ||||||
|  |             formData, | ||||||
|  |             block = requestBuilder | ||||||
|  |         ) | ||||||
|  |     } else { | ||||||
|  |         submitForm( | ||||||
|  |             url, | ||||||
|  |             Parameters.build { | ||||||
|  |                 for (it in formData) { | ||||||
|  |                     val formItem = (it as PartData.FormItem) | ||||||
|  |                     append(it.name!!, it.value) | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             block = requestBuilder | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return if (response.status == HttpStatusCode.OK) { | ||||||
|  |         stringFormat.decodeFromString(resultDeserializer, response.bodyAsText()) | ||||||
|  |     } else { | ||||||
|  |         null | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								ktor/common/src/linuxX64Main/kotlin/ActualMPPFileInput.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								ktor/common/src/linuxX64Main/kotlin/ActualMPPFileInput.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | package dev.inmo.micro_utils.ktor.common | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.MPPFile | ||||||
|  | import dev.inmo.micro_utils.common.bytesAllocatorSync | ||||||
|  | import io.ktor.utils.io.core.ByteReadPacket | ||||||
|  | import io.ktor.utils.io.core.Input | ||||||
|  |  | ||||||
|  | actual fun MPPFile.input(): Input { | ||||||
|  |     return ByteReadPacket(bytesAllocatorSync()) | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										10
									
								
								ktor/common/src/mingwX64Main/kotlin/ActualMPPFileInput.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								ktor/common/src/mingwX64Main/kotlin/ActualMPPFileInput.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | package dev.inmo.micro_utils.ktor.common | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.MPPFile | ||||||
|  | import dev.inmo.micro_utils.common.bytesAllocatorSync | ||||||
|  | import io.ktor.utils.io.core.ByteReadPacket | ||||||
|  | import io.ktor.utils.io.core.Input | ||||||
|  |  | ||||||
|  | actual fun MPPFile.input(): Input { | ||||||
|  |     return ByteReadPacket(bytesAllocatorSync()) | ||||||
|  | } | ||||||
| @@ -12,10 +12,22 @@ suspend fun ApplicationCall.getParameterOrSendError( | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | suspend fun ApplicationCall.getParametersOrSendError( | ||||||
|  |     field: String | ||||||
|  | ) = parameters.getAll(field).also { | ||||||
|  |     if (it == null) { | ||||||
|  |         respond(HttpStatusCode.BadRequest, "Request must contains $field") | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| fun ApplicationCall.getQueryParameter( | fun ApplicationCall.getQueryParameter( | ||||||
|     field: String |     field: String | ||||||
| ) = request.queryParameters[field] | ) = request.queryParameters[field] | ||||||
|  |  | ||||||
|  | fun ApplicationCall.getQueryParameters( | ||||||
|  |     field: String | ||||||
|  | ) = request.queryParameters.getAll(field) | ||||||
|  |  | ||||||
| suspend fun ApplicationCall.getQueryParameterOrSendError( | suspend fun ApplicationCall.getQueryParameterOrSendError( | ||||||
|     field: String |     field: String | ||||||
| ) = getQueryParameter(field).also { | ) = getQueryParameter(field).also { | ||||||
| @@ -23,3 +35,11 @@ suspend fun ApplicationCall.getQueryParameterOrSendError( | |||||||
|         respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field") |         respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field") | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | suspend fun ApplicationCall.getQueryParametersOrSendError( | ||||||
|  |     field: String | ||||||
|  | ) = getQueryParameters(field).also { | ||||||
|  |     if (it == null) { | ||||||
|  |         respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field") | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | import math | ||||||
|  |  | ||||||
| import requests | import requests | ||||||
| from bs4 import BeautifulSoup | from bs4 import BeautifulSoup | ||||||
| import pandas as pd | import pandas as pd | ||||||
| @@ -17,33 +19,45 @@ def fix_name(category, raw_name): | |||||||
|         result += out1 |         result += out1 | ||||||
|     return result |     return result | ||||||
|  |  | ||||||
|  | def remove_prefix(text, prefix): | ||||||
|  |     if text.startswith(prefix): | ||||||
|  |         return text[len(prefix):] | ||||||
|  |     return text  # or whatever | ||||||
|  |  | ||||||
|  | def extensionPreparationFun(extension): | ||||||
|  |     return "\"%s\"" % (remove_prefix(extension, ".")) | ||||||
|  |  | ||||||
| # https://www.freeformatter.com/mime-types-list.html | # https://www.freeformatter.com/mime-types-list.html | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|     df = pd.read_html(open('table.html', 'r')) |     df = pd.read_html(open('local.table.html', 'r')) | ||||||
|     mimes = [] |     mimes = [] | ||||||
|     for row in df[0].drop_duplicates(subset=['MIME Type / Internet Media Type'], keep='first').iterrows(): |     for row in df[0].drop_duplicates(subset=['MIME Type / Internet Media Type'], keep='first').iterrows(): | ||||||
|         mime = row[1][1] |         mime = row[1][1] | ||||||
|  |         extensions = list() | ||||||
|  |         if isinstance(row[1][2], str): | ||||||
|  |             extensions = list(map(extensionPreparationFun, row[1][2].split(", "))) | ||||||
|         mime_category = mime.split('/', 1)[0] |         mime_category = mime.split('/', 1)[0] | ||||||
|         mime_name = mime.split('/', 1)[1] |         mime_name = mime.split('/', 1)[1] | ||||||
|         mimes.append({ |         mimes.append([ | ||||||
|             'mime_category': mime_category, |             mime_category, | ||||||
|             'mime_name': mime_name, |             mime_name, | ||||||
|         }) |             extensions | ||||||
|  |         ]) | ||||||
|  |  | ||||||
|     # codegen |     # codegen | ||||||
|  |  | ||||||
|     mimes.sort(key=lambda x: x['mime_category']) |     mimes.sort(key=lambda x: x[0]) | ||||||
|     grouped = itertools.groupby(mimes, lambda x: x['mime_category']) |     grouped = itertools.groupby(mimes, lambda x: x[0]) | ||||||
|     code = '' |     code = '' | ||||||
|     code2 = 'internal val knownMimeTypes: Set<MimeType> = setOf(\n' |     code2 = 'internal val knownMimeTypes: Set<MimeType> = setOf(\n' | ||||||
|     code2 += '    KnownMimeTypes.Any,\n' |     code2 += '    KnownMimeTypes.Any,\n' | ||||||
|     for key, group in grouped: |     for key, group in grouped: | ||||||
|         group_name = fix_name(group, key) |         group_name = fix_name(group, key) | ||||||
|         code += '@Serializable(MimeTypeSerializer::class)\nsealed class %s(raw: String) : MimeType, KnownMimeTypes(raw) {\n' % group_name |         code += '@Serializable(MimeTypeSerializer::class)\nsealed class %s(raw: String, extensions: Array<String> = emptyArray()) : MimeType, KnownMimeTypes(raw, extensions) {\n' % group_name | ||||||
|         code += '    @Serializable(MimeTypeSerializer::class)\n    object Any: %s ("%s/*")\n' % (group_name, key) |         code += '    @Serializable(MimeTypeSerializer::class)\n    object Any: %s ("%s/*")\n' % (group_name, key) | ||||||
|         for mime in group: |         for mime in group: | ||||||
|             name = fix_name(mime['mime_category'], mime['mime_name']) |             name = fix_name(mime[0], mime[1]) | ||||||
|             code += '    @Serializable(MimeTypeSerializer::class)\n    object %s: %s ("%s/%s")\n' % (name, group_name, mime['mime_category'], mime['mime_name']) |             code += '    @Serializable(MimeTypeSerializer::class)\n    object %s: %s ("%s/%s", arrayOf(%s))\n' % (name, group_name, mime[0], mime[1], ", ".join(mime[2])) | ||||||
|             code2 += '    KnownMimeTypes.%s.%s,\n' % (group_name, name) |             code2 += '    KnownMimeTypes.%s.%s,\n' % (group_name, name) | ||||||
|         code += '}\n\n' |         code += '}\n\n' | ||||||
|     code2 += ')\n' |     code2 += ')\n' | ||||||
|   | |||||||
| @@ -0,0 +1,24 @@ | |||||||
|  | package dev.inmo.micro_utils.mime_types | ||||||
|  |  | ||||||
|  | val mimeTypesByExtensions: Map<String, Array<MimeType>> by lazy { | ||||||
|  |     val extensionsMap = mutableMapOf<String, MutableList<MimeType>>() | ||||||
|  |  | ||||||
|  |     knownMimeTypes.forEach { mimeType -> | ||||||
|  |         mimeType.extensions.forEach { | ||||||
|  |             extensionsMap.getOrPut(it) { mutableListOf() }.add(mimeType) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     extensionsMap.mapValues { | ||||||
|  |         it.value.toTypedArray() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun getMimeType( | ||||||
|  |     stringWithExtension: String, | ||||||
|  |     selector: (Array<MimeType>) -> MimeType? = { it.firstOrNull() } | ||||||
|  | ) = mimeTypesByExtensions[stringWithExtension.takeLastWhile { it != '.' }] ?.takeIf { it.isNotEmpty() } ?.let(selector) | ||||||
|  | inline fun getMimeTypeOrAny( | ||||||
|  |     stringWithExtension: String, | ||||||
|  |     selector: (Array<MimeType>) -> MimeType? = { it.firstOrNull() } | ||||||
|  | ) = getMimeType(stringWithExtension, selector) ?: KnownMimeTypes.Any | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -5,4 +5,6 @@ import kotlinx.serialization.Serializable | |||||||
| @Serializable(MimeTypeSerializer::class) | @Serializable(MimeTypeSerializer::class) | ||||||
| interface MimeType { | interface MimeType { | ||||||
|     val raw: String |     val raw: String | ||||||
|  |     val extensions: Array<String> | ||||||
|  |         get() = emptyArray() | ||||||
| } | } | ||||||
|   | |||||||
| @@ -15,6 +15,8 @@ kotlin { | |||||||
|         browser() |         browser() | ||||||
|         nodejs() |         nodejs() | ||||||
|     } |     } | ||||||
|  |     linuxX64() | ||||||
|  |     mingwX64() | ||||||
| 
 | 
 | ||||||
|     sourceSets { |     sourceSets { | ||||||
|         commonMain { |         commonMain { | ||||||
| @@ -18,6 +18,8 @@ kotlin { | |||||||
|     android { |     android { | ||||||
|         publishAllLibraryVariants() |         publishAllLibraryVariants() | ||||||
|     } |     } | ||||||
|  |     linuxX64() | ||||||
|  |     mingwX64() | ||||||
|  |  | ||||||
|     sourceSets { |     sourceSets { | ||||||
|         commonMain { |         commonMain { | ||||||
| @@ -50,6 +52,16 @@ kotlin { | |||||||
|                 implementation libs.android.espresso |                 implementation libs.android.espresso | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |         mingwX64Test { | ||||||
|  |             dependencies { | ||||||
|  |                 implementation kotlin('test-junit') | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         linuxX64Test { | ||||||
|  |             dependencies { | ||||||
|  |                 implementation kotlin('test-junit') | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|         androidMain.dependsOn jvmMain |         androidMain.dependsOn jvmMain | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								renovate.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								renovate.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | { | ||||||
|  |   "$schema": "https://docs.renovatebot.com/renovate-schema.json", | ||||||
|  |   "extends": [ | ||||||
|  |     "config:base" | ||||||
|  |   ] | ||||||
|  | } | ||||||
| @@ -21,9 +21,9 @@ open class AutoRecacheWriteCRUDRepo<RegisteredObject, Id, InputObject>( | |||||||
|     protected val idGetter: (RegisteredObject) -> Id |     protected val idGetter: (RegisteredObject) -> Id | ||||||
| ) : WriteCRUDRepo<RegisteredObject, Id, InputObject>, FallbackCacheRepo { | ) : WriteCRUDRepo<RegisteredObject, Id, InputObject>, FallbackCacheRepo { | ||||||
|     override val deletedObjectsIdsFlow: Flow<Id> |     override val deletedObjectsIdsFlow: Flow<Id> | ||||||
|         get() = (originalRepo.deletedObjectsIdsFlow + kvCache.onValueRemoved).distinctUntilChanged() |         get() = (originalRepo.deletedObjectsIdsFlow).distinctUntilChanged() | ||||||
|     override val newObjectsFlow: Flow<RegisteredObject> |     override val newObjectsFlow: Flow<RegisteredObject> | ||||||
|         get() = (originalRepo.newObjectsFlow + kvCache.onNewValue.map { it.second }).distinctUntilChanged() |         get() = (originalRepo.newObjectsFlow).distinctUntilChanged() | ||||||
|     override val updatedObjectsFlow: Flow<RegisteredObject> |     override val updatedObjectsFlow: Flow<RegisteredObject> | ||||||
|         get() = originalRepo.updatedObjectsFlow |         get() = originalRepo.updatedObjectsFlow | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,10 +17,10 @@ open class AutoRecacheWriteKeyValueRepo<Id, RegisteredObject>( | |||||||
|     protected val kvCache: FullKVCache<Id, RegisteredObject> = FullKVCache() |     protected val kvCache: FullKVCache<Id, RegisteredObject> = FullKVCache() | ||||||
| ) : WriteKeyValueRepo<Id, RegisteredObject>, FallbackCacheRepo { | ) : WriteKeyValueRepo<Id, RegisteredObject>, FallbackCacheRepo { | ||||||
|     override val onValueRemoved: Flow<Id> |     override val onValueRemoved: Flow<Id> | ||||||
|         get() = (originalRepo.onValueRemoved + kvCache.onValueRemoved).distinctUntilChanged() |         get() = (originalRepo.onValueRemoved).distinctUntilChanged() | ||||||
|  |  | ||||||
|     override val onNewValue: Flow<Pair<Id, RegisteredObject>> |     override val onNewValue: Flow<Pair<Id, RegisteredObject>> | ||||||
|         get() = (originalRepo.onNewValue + kvCache.onNewValue).distinctUntilChanged() |         get() = (originalRepo.onNewValue).distinctUntilChanged() | ||||||
|  |  | ||||||
|     private val onRemovingUpdatesListeningJob = originalRepo.onValueRemoved.subscribeSafelyWithoutExceptions(scope) { |     private val onRemovingUpdatesListeningJob = originalRepo.onValueRemoved.subscribeSafelyWithoutExceptions(scope) { | ||||||
|         kvCache.unset(it) |         kvCache.unset(it) | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging | |||||||
| import dev.inmo.micro_utils.repos.WriteKeyValuesRepo | import dev.inmo.micro_utils.repos.WriteKeyValuesRepo | ||||||
| import dev.inmo.micro_utils.repos.cache.cache.FullKVCache | import dev.inmo.micro_utils.repos.cache.cache.FullKVCache | ||||||
| import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo | import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo | ||||||
|  | import dev.inmo.micro_utils.repos.pagination.maxPagePagination | ||||||
| import dev.inmo.micro_utils.repos.set | import dev.inmo.micro_utils.repos.set | ||||||
| import dev.inmo.micro_utils.repos.unset | import dev.inmo.micro_utils.repos.unset | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
| @@ -24,7 +25,7 @@ open class AutoRecacheWriteKeyValuesRepo<Id, RegisteredObject>( | |||||||
|     override val onNewValue: Flow<Pair<Id, RegisteredObject>> |     override val onNewValue: Flow<Pair<Id, RegisteredObject>> | ||||||
|         get() = originalRepo.onNewValue |         get() = originalRepo.onNewValue | ||||||
|     override val onDataCleared: Flow<Id> |     override val onDataCleared: Flow<Id> | ||||||
|         get() = (originalRepo.onDataCleared + kvCache.onValueRemoved).distinctUntilChanged() |         get() = (originalRepo.onDataCleared).distinctUntilChanged() | ||||||
|  |  | ||||||
|     private val onDataClearedListeningJob = originalRepo.onDataCleared.subscribeSafelyWithoutExceptions(scope) { |     private val onDataClearedListeningJob = originalRepo.onDataCleared.subscribeSafelyWithoutExceptions(scope) { | ||||||
|         kvCache.unset(it) |         kvCache.unset(it) | ||||||
| @@ -50,7 +51,7 @@ open class AutoRecacheWriteKeyValuesRepo<Id, RegisteredObject>( | |||||||
|  |  | ||||||
|     override suspend fun clearWithValue(v: RegisteredObject) { |     override suspend fun clearWithValue(v: RegisteredObject) { | ||||||
|         originalRepo.clearWithValue(v) |         originalRepo.clearWithValue(v) | ||||||
|         doForAllWithNextPaging(FirstPagePagination(kvCache.count().takeIf { it < Int.MAX_VALUE } ?.toInt() ?: Int.MAX_VALUE)) { |         doForAllWithNextPaging(kvCache.maxPagePagination()) { | ||||||
|             kvCache.keys(it).also { |             kvCache.keys(it).also { | ||||||
|                 it.results.forEach { id -> |                 it.results.forEach { id -> | ||||||
|                     kvCache.get(id) ?.takeIf { it.contains(v) } ?.let { |                     kvCache.get(id) ?.takeIf { it.contains(v) } ?.let { | ||||||
| @@ -73,6 +74,19 @@ open class AutoRecacheWriteKeyValuesRepo<Id, RegisteredObject>( | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     override suspend fun removeWithValue(v: RegisteredObject) { | ||||||
|  |         originalRepo.removeWithValue(v) | ||||||
|  |         doForAllWithNextPaging(kvCache.maxPagePagination()) { | ||||||
|  |             kvCache.keys(it).also { | ||||||
|  |                 it.results.forEach { id -> | ||||||
|  |                     kvCache.get(id) ?.takeIf { it.contains(v) } ?.let { | ||||||
|  |                         kvCache.set(id, it - v) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     override suspend fun add(toAdd: Map<Id, List<RegisteredObject>>) { |     override suspend fun add(toAdd: Map<Id, List<RegisteredObject>>) { | ||||||
|         originalRepo.add(toAdd) |         originalRepo.add(toAdd) | ||||||
|         toAdd.forEach { (k, v) -> |         toAdd.forEach { (k, v) -> | ||||||
|   | |||||||
| @@ -102,8 +102,15 @@ open class FullCRUDCacheRepo<ObjectType, IdType, InputValueType>( | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | fun <ObjectType, IdType, InputType> CRUDRepo<ObjectType, IdType, InputType>.fullyCached( | ||||||
|  |     kvCache: FullKVCache<IdType, ObjectType> = FullKVCache(), | ||||||
|  |     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), | ||||||
|  |     idGetter: (ObjectType) -> IdType | ||||||
|  | ) = FullCRUDCacheRepo(this, kvCache, scope, idGetter) | ||||||
|  |  | ||||||
|  | @Deprecated("Renamed", ReplaceWith("this.fullyCached(kvCache, scope, idGetter)", "dev.inmo.micro_utils.repos.cache.full.fullyCached")) | ||||||
| fun <ObjectType, IdType, InputType> CRUDRepo<ObjectType, IdType, InputType>.cached( | fun <ObjectType, IdType, InputType> CRUDRepo<ObjectType, IdType, InputType>.cached( | ||||||
|     kvCache: FullKVCache<IdType, ObjectType>, |     kvCache: FullKVCache<IdType, ObjectType>, | ||||||
|     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), |     scope: CoroutineScope = CoroutineScope(Dispatchers.Default), | ||||||
|     idGetter: (ObjectType) -> IdType |     idGetter: (ObjectType) -> IdType | ||||||
| ) = FullCRUDCacheRepo(this, kvCache, scope, idGetter) | ) = fullyCached(kvCache, scope, idGetter) | ||||||
|   | |||||||
| @@ -117,7 +117,13 @@ open class FullKeyValueCacheRepo<Key,Value>( | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | fun <Key, Value> KeyValueRepo<Key, Value>.fullyCached( | ||||||
|  |     kvCache: FullKVCache<Key, Value> = FullKVCache(), | ||||||
|  |     scope: CoroutineScope = CoroutineScope(Dispatchers.Default) | ||||||
|  | ) = FullKeyValueCacheRepo(this, kvCache, scope) | ||||||
|  |  | ||||||
|  | @Deprecated("Renamed", ReplaceWith("this.fullyCached(kvCache, scope)", "dev.inmo.micro_utils.repos.cache.full.fullyCached")) | ||||||
| fun <Key, Value> KeyValueRepo<Key, Value>.cached( | fun <Key, Value> KeyValueRepo<Key, Value>.cached( | ||||||
|     kvCache: FullKVCache<Key, Value>, |     kvCache: FullKVCache<Key, Value>, | ||||||
|     scope: CoroutineScope = CoroutineScope(Dispatchers.Default) |     scope: CoroutineScope = CoroutineScope(Dispatchers.Default) | ||||||
| ) = FullKeyValueCacheRepo(this, kvCache, scope) | ) = fullyCached(kvCache, scope) | ||||||
|   | |||||||
| @@ -157,8 +157,18 @@ open class FullKeyValuesCacheRepo<Key,Value>( | |||||||
|     override suspend fun invalidate() { |     override suspend fun invalidate() { | ||||||
|         kvCache.actualizeAll(parentRepo) |         kvCache.actualizeAll(parentRepo) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     override suspend fun removeWithValue(v: Value) { | ||||||
|  |         super<FullWriteKeyValuesCacheRepo>.removeWithValue(v) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | fun <Key, Value> KeyValuesRepo<Key, Value>.fullyCached( | ||||||
|  |     kvCache: FullKVCache<Key, List<Value>> = FullKVCache(), | ||||||
|  |     scope: CoroutineScope = CoroutineScope(Dispatchers.Default) | ||||||
|  | ) = FullKeyValuesCacheRepo(this, kvCache, scope) | ||||||
|  |  | ||||||
|  | @Deprecated("Renamed", ReplaceWith("this.fullyCached(kvCache, scope)", "dev.inmo.micro_utils.repos.cache.full.fullyCached")) | ||||||
| fun <Key, Value> KeyValuesRepo<Key, Value>.caching( | fun <Key, Value> KeyValuesRepo<Key, Value>.caching( | ||||||
|     kvCache: FullKVCache<Key, List<Value>>, |     kvCache: FullKVCache<Key, List<Value>>, | ||||||
|     scope: CoroutineScope = CoroutineScope(Dispatchers.Default) |     scope: CoroutineScope = CoroutineScope(Dispatchers.Default) | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| package dev.inmo.micro_utils.repos | package dev.inmo.micro_utils.repos | ||||||
|  |  | ||||||
| import dev.inmo.micro_utils.pagination.* | import dev.inmo.micro_utils.pagination.* | ||||||
|  | import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging | ||||||
| import dev.inmo.micro_utils.pagination.utils.getAllWithNextPaging | import dev.inmo.micro_utils.pagination.utils.getAllWithNextPaging | ||||||
| import kotlinx.coroutines.flow.Flow | import kotlinx.coroutines.flow.Flow | ||||||
|  |  | ||||||
| @@ -44,9 +45,23 @@ interface WriteKeyValuesRepo<Key, Value> : Repo { | |||||||
|  |  | ||||||
|     suspend fun add(toAdd: Map<Key, List<Value>>) |     suspend fun add(toAdd: Map<Key, List<Value>>) | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Removes [Value]s by passed [Key]s without full clear of all data by [Key] | ||||||
|  |      */ | ||||||
|     suspend fun remove(toRemove: Map<Key, List<Value>>) |     suspend fun remove(toRemove: Map<Key, List<Value>>) | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Removes [v] without full clear of all data by [Key]s with [v] | ||||||
|  |      */ | ||||||
|  |     suspend fun removeWithValue(v: Value) | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Fully clear all data by [k] | ||||||
|  |      */ | ||||||
|     suspend fun clear(k: Key) |     suspend fun clear(k: Key) | ||||||
|  |     /** | ||||||
|  |      * Clear [v] **with** full clear of all data by [Key]s with [v] | ||||||
|  |      */ | ||||||
|     suspend fun clearWithValue(v: Value) |     suspend fun clearWithValue(v: Value) | ||||||
|  |  | ||||||
|     suspend fun set(toSet: Map<Key, List<Value>>) { |     suspend fun set(toSet: Map<Key, List<Value>>) { | ||||||
| @@ -100,6 +115,21 @@ interface KeyValuesRepo<Key, Value> : ReadKeyValuesRepo<Key, Value>, WriteKeyVal | |||||||
|             keysResult.currentPageIfNotEmpty() |             keysResult.currentPageIfNotEmpty() | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |     suspend override fun removeWithValue(v: Value) { | ||||||
|  |         val toRemove = mutableMapOf<Key, List<Value>>() | ||||||
|  |  | ||||||
|  |         doForAllWithNextPaging { | ||||||
|  |             keys(it).also { | ||||||
|  |                 it.results.forEach { | ||||||
|  |                     if (contains(it, v)) { | ||||||
|  |                         toRemove[it] = listOf(v) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         remove(toRemove) | ||||||
|  |     } | ||||||
| } | } | ||||||
| typealias OneToManyKeyValueRepo<Key,Value> = KeyValuesRepo<Key, Value> | typealias OneToManyKeyValueRepo<Key,Value> = KeyValuesRepo<Key, Value> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -97,6 +97,8 @@ open class MapperWriteKeyValuesRepo<FromKey, FromValue, ToKey, ToValue>( | |||||||
|         }.toMap() |         }.toMap() | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |     override suspend fun removeWithValue(v: FromValue) = to.removeWithValue(v.toOutValue()) | ||||||
|  |  | ||||||
|     override suspend fun set(toSet: Map<FromKey, List<FromValue>>) { |     override suspend fun set(toSet: Map<FromKey, List<FromValue>>) { | ||||||
|         to.set( |         to.set( | ||||||
|             toSet.map { (k, vs) -> k.toOutKey() to vs.map { v -> v.toOutValue() } }.toMap() |             toSet.map { (k, vs) -> k.toOutKey() to vs.map { v -> v.toOutValue() } }.toMap() | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import dev.inmo.micro_utils.pagination.FirstPagePagination | |||||||
| import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging | import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging | ||||||
| import dev.inmo.micro_utils.repos.KeyValueRepo | import dev.inmo.micro_utils.repos.KeyValueRepo | ||||||
| import dev.inmo.micro_utils.repos.KeyValuesRepo | import dev.inmo.micro_utils.repos.KeyValuesRepo | ||||||
|  | import dev.inmo.micro_utils.repos.set | ||||||
| import dev.inmo.micro_utils.repos.unset | import dev.inmo.micro_utils.repos.unset | ||||||
| import kotlinx.coroutines.flow.Flow | import kotlinx.coroutines.flow.Flow | ||||||
| import kotlinx.coroutines.flow.MutableSharedFlow | import kotlinx.coroutines.flow.MutableSharedFlow | ||||||
| @@ -60,6 +61,24 @@ open class KeyValuesFromKeyValueRepo<Key, Value, ValuesIterable : Iterable<Value | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     override suspend fun removeWithValue(v: Value) { | ||||||
|  |         val toRemove = mutableMapOf<Key, List<Value>>() | ||||||
|  |  | ||||||
|  |         doForAllWithNextPaging { | ||||||
|  |             original.keys(it).also { | ||||||
|  |                 it.results.forEach { | ||||||
|  |                     val data = original.get(it) ?: return@forEach | ||||||
|  |  | ||||||
|  |                     if (v in data) { | ||||||
|  |                         toRemove[it] = listOf(v) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         remove(toRemove) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     override suspend fun add(toAdd: Map<Key, List<Value>>) { |     override suspend fun add(toAdd: Map<Key, List<Value>>) { | ||||||
|         original.set( |         original.set( | ||||||
|             toAdd.mapNotNull { (k, adding) -> |             toAdd.mapNotNull { (k, adding) -> | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import dev.inmo.micro_utils.common.mapNotNullA | |||||||
| import dev.inmo.micro_utils.pagination.* | import dev.inmo.micro_utils.pagination.* | ||||||
| import dev.inmo.micro_utils.pagination.utils.reverse | 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.crud.asId | ||||||
| import kotlinx.coroutines.flow.Flow | import kotlinx.coroutines.flow.Flow | ||||||
| import kotlinx.coroutines.flow.MutableSharedFlow | import kotlinx.coroutines.flow.MutableSharedFlow | ||||||
| import kotlinx.coroutines.flow.asSharedFlow | import kotlinx.coroutines.flow.asSharedFlow | ||||||
| @@ -260,6 +261,19 @@ class OneToManyAndroidRepo<Key, Value>( | |||||||
|             _onValueRemoved.emit(k to v) |             _onValueRemoved.emit(k to v) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     override suspend fun removeWithValue(v: Value) { | ||||||
|  |         helper.blockingWritableTransaction { | ||||||
|  |             val keys = select(tableName, idColumnArray, "$valueColumnName=?", arrayOf(v.valueAsString())).map { | ||||||
|  |                 it.asId.keyFromString() | ||||||
|  |             } | ||||||
|  |             keys.filter { | ||||||
|  |                 delete(tableName, "$idColumnName=? AND $valueColumnName=?", arrayOf(it.keyAsString(), v.valueAsString())) > 0 | ||||||
|  |             } | ||||||
|  |         }.forEach { k -> | ||||||
|  |             _onValueRemoved.emit(k to v) | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| fun <Key, Value> OneToManyAndroidRepo( | fun <Key, Value> OneToManyAndroidRepo( | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ 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.eq | ||||||
|  | import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList | ||||||
| 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> | ||||||
| @@ -66,9 +67,36 @@ open class ExposedKeyValuesRepo<Key, Value>( | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     override suspend fun removeWithValue(v: Value) { | ||||||
|  |         transaction(database) { | ||||||
|  |             val keys = select { selectByValue(v) }.map { it.asKey } | ||||||
|  |             deleteWhere { SqlExpressionBuilder.selectByValue(v) } | ||||||
|  |             keys | ||||||
|  |         }.forEach { | ||||||
|  |             _onValueRemoved.emit(it to v) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     override suspend fun clear(k: Key) { |     override suspend fun clear(k: Key) { | ||||||
|         transaction(database) { |         transaction(database) { | ||||||
|             deleteWhere { keyColumn.eq(k) } |             deleteWhere { keyColumn.eq(k) } | ||||||
|         }.also { _onDataCleared.emit(k) } |         }.also { _onDataCleared.emit(k) } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     override suspend fun clearWithValue(v: Value) { | ||||||
|  |         transaction(database) { | ||||||
|  |             val toClear = select { selectByValue(v) } | ||||||
|  |                 .asSequence() | ||||||
|  |                 .map { it.asKey to it.asObject } | ||||||
|  |                 .groupBy { it.first } | ||||||
|  |                 .mapValues { it.value.map { it.second } } | ||||||
|  |             deleteWhere { keyColumn.inList(toClear.keys) } | ||||||
|  |             toClear | ||||||
|  |         }.forEach { | ||||||
|  |             it.value.forEach { v -> | ||||||
|  |                 _onValueRemoved.emit(it.key to v) | ||||||
|  |             } | ||||||
|  |             _onDataCleared.emit(it.key) | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -87,13 +87,27 @@ class MapWriteKeyValuesRepo<Key, Value>( | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     override suspend fun removeWithValue(v: Value) { | ||||||
|  |         map.forEach { (k, values) -> | ||||||
|  |             if (values.remove(v)) { | ||||||
|  |                 _onValueRemoved.emit(k to v) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     override suspend fun clear(k: Key) { |     override suspend fun clear(k: Key) { | ||||||
|         map.remove(k) ?.also { _onDataCleared.emit(k) } |         map.remove(k) ?.also { _onDataCleared.emit(k) } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override suspend fun clearWithValue(v: Value) { |     override suspend fun clearWithValue(v: Value) { | ||||||
|         map.forEach { (k, values) -> |         map.filter { (_, values) -> | ||||||
|             if (values.remove(v)) _onValueRemoved.emit(k to v) |             values.contains(v) | ||||||
|  |         }.forEach { | ||||||
|  |             map.remove(it.key) ?.onEach { v -> | ||||||
|  |                 _onValueRemoved.emit(it.key to v) | ||||||
|  |             } ?.also { _ -> | ||||||
|  |                 _onDataCleared.emit(it.key) | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -47,6 +47,17 @@ class KtorWriteKeyValuesRepoClient<Key : Any, Value : Any>( | |||||||
|         }.throwOnUnsuccess { "Unable to remove $toRemove" } |         }.throwOnUnsuccess { "Unable to remove $toRemove" } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @OptIn(InternalAPI::class) | ||||||
|  |     override suspend fun removeWithValue(v: Value) { | ||||||
|  |         httpClient.post( | ||||||
|  |             buildStandardUrl(baseUrl, removeWithValueRoute) | ||||||
|  |         ) { | ||||||
|  |             body = v | ||||||
|  |             bodyType = valueTypeInfo | ||||||
|  |             contentType(contentType) | ||||||
|  |         }.throwOnUnsuccess { "Unable to remove $v" } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @OptIn(InternalAPI::class) |     @OptIn(InternalAPI::class) | ||||||
|     override suspend fun clear(k: Key) { |     override suspend fun clear(k: Key) { | ||||||
|         httpClient.post( |         httpClient.post( | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ const val onDataClearedRoute = "onDataCleared" | |||||||
|  |  | ||||||
| const val addRoute = "add" | const val addRoute = "add" | ||||||
| const val removeRoute = "remove" | const val removeRoute = "remove" | ||||||
|  | const val removeWithValueRoute = "removeWithValue" | ||||||
| const val clearRoute = "clear" | const val clearRoute = "clear" | ||||||
| const val clearWithValueRoute = "clearWithValue" | const val clearWithValueRoute = "clearWithValue" | ||||||
| const val setRoute = "set" | const val setRoute = "set" | ||||||
|   | |||||||
| @@ -46,6 +46,11 @@ inline fun <reified Key : Any, reified Value : Any> Route.configureWriteKeyValue | |||||||
|         call.respond(HttpStatusCode.OK) |         call.respond(HttpStatusCode.OK) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     post(removeWithValueRoute) { | ||||||
|  |         originalRepo.removeWithValue(call.receive()) | ||||||
|  |         call.respond(HttpStatusCode.OK) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     post(clearRoute) { |     post(clearRoute) { | ||||||
|         originalRepo.clear(call.receive()) |         originalRepo.clear(call.receive()) | ||||||
|         call.respond(HttpStatusCode.OK) |         call.respond(HttpStatusCode.OK) | ||||||
|   | |||||||
| @@ -2,15 +2,17 @@ plugins { | |||||||
|     id "org.jetbrains.kotlin.multiplatform" |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|     id "org.jetbrains.kotlin.plugin.serialization" |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|     id "application" |     id "application" | ||||||
|  |     id "com.google.devtools.ksp" | ||||||
| } | } | ||||||
|  |  | ||||||
| apply from: "$mppJsAndJavaProjectPresetPath" | apply from: "$mppJvmJsLinuxMingwProjectPresetPath" | ||||||
|  |  | ||||||
| kotlin { | kotlin { | ||||||
|     sourceSets { |     sourceSets { | ||||||
|         commonMain { |         commonMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api internalProject("micro_utils.startup.plugin") |                 api internalProject("micro_utils.startup.plugin") | ||||||
|  |                 api internalProject("micro_utils.koin") | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         commonTest { |         commonTest { | ||||||
| @@ -29,3 +31,10 @@ java { | |||||||
|     sourceCompatibility = JavaVersion.VERSION_1_8 |     sourceCompatibility = JavaVersion.VERSION_1_8 | ||||||
|     targetCompatibility = JavaVersion.VERSION_1_8 |     targetCompatibility = JavaVersion.VERSION_1_8 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | dependencies { | ||||||
|  |     add("kspCommonMainMetadata", project(":micro_utils.koin.generator")) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ksp { | ||||||
|  | } | ||||||
|   | |||||||
| @@ -0,0 +1,38 @@ | |||||||
|  | // THIS CODE HAVE BEEN GENERATED AUTOMATICALLY | ||||||
|  | // TO REGENERATE IT JUST DELETE FILE | ||||||
|  | // ORIGINAL FILE: StartLauncherPlugin.kt | ||||||
|  | package dev.inmo.micro_utils.startup.launcher | ||||||
|  |  | ||||||
|  | import kotlin.Boolean | ||||||
|  | import kotlinx.serialization.json.Json | ||||||
|  | import org.koin.core.Koin | ||||||
|  | import org.koin.core.definition.Definition | ||||||
|  | import org.koin.core.definition.KoinDefinition | ||||||
|  | import org.koin.core.module.Module | ||||||
|  | import org.koin.core.qualifier.named | ||||||
|  | import org.koin.core.scope.Scope | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @return Definition by key "baseJsonProvider" | ||||||
|  |  */ | ||||||
|  | public val Scope.baseJsonProvider: Json? | ||||||
|  |   get() = getOrNull(named("baseJsonProvider")) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @return Definition by key "baseJsonProvider" | ||||||
|  |  */ | ||||||
|  | public val Koin.baseJsonProvider: Json? | ||||||
|  |   get() = getOrNull(named("baseJsonProvider")) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will register [definition] with [org.koin.core.module.Module.single] and key "baseJsonProvider" | ||||||
|  |  */ | ||||||
|  | public fun Module.baseJsonProviderSingle(createdAtStart: Boolean = false, definition: Definition<Json>): | ||||||
|  |     KoinDefinition<Json> = single(named("baseJsonProvider"), createdAtStart = createdAtStart, definition | ||||||
|  |     = definition) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will register [definition] with [org.koin.core.module.Module.factory] and key "baseJsonProvider" | ||||||
|  |  */ | ||||||
|  | public fun Module.baseJsonProviderFactory(definition: Definition<Json>): KoinDefinition<Json> = | ||||||
|  |     factory(named("baseJsonProvider"), definition = definition) | ||||||
| @@ -1,17 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.startup.launcher |  | ||||||
|  |  | ||||||
| import dev.inmo.micro_utils.startup.launcher.StartLauncherPlugin.setupDI |  | ||||||
| import kotlinx.serialization.json.JsonObject |  | ||||||
| import org.koin.core.KoinApplication |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Will create [KoinApplication], init, load modules using [StartLauncherPlugin] and start plugins using the same base |  | ||||||
|  * plugin |  | ||||||
|  * |  | ||||||
|  * @param rawConfig It is expected that this [JsonObject] will contain serialized [Config] ([StartLauncherPlugin] will |  | ||||||
|  * deserialize it in its [StartLauncherPlugin.setupDI] |  | ||||||
|  */ |  | ||||||
| @Deprecated("Fully replaced with StartLauncherPlugin#start", ReplaceWith("StartLauncherPlugin.start(rawConfig)", "dev.inmo.micro_utils.startup.launcher.StartLauncherPlugin")) |  | ||||||
| suspend fun start(rawConfig: JsonObject) { |  | ||||||
|     StartLauncherPlugin.start(rawConfig) |  | ||||||
| } |  | ||||||
| @@ -1,9 +1,11 @@ | |||||||
|  | @file:GenerateKoinDefinition("baseJsonProvider", Json::class) | ||||||
| package dev.inmo.micro_utils.startup.launcher | package dev.inmo.micro_utils.startup.launcher | ||||||
|  |  | ||||||
| import dev.inmo.kslog.common.i | import dev.inmo.kslog.common.i | ||||||
| import dev.inmo.kslog.common.taggedLogger | import dev.inmo.kslog.common.taggedLogger | ||||||
| import dev.inmo.kslog.common.w | import dev.inmo.kslog.common.w | ||||||
| import dev.inmo.micro_utils.coroutines.runCatchingSafely | import dev.inmo.micro_utils.coroutines.runCatchingSafely | ||||||
|  | import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition | ||||||
| import dev.inmo.micro_utils.startup.launcher.StartLauncherPlugin.setupDI | import dev.inmo.micro_utils.startup.launcher.StartLauncherPlugin.setupDI | ||||||
| import dev.inmo.micro_utils.startup.launcher.StartLauncherPlugin.startPlugin | import dev.inmo.micro_utils.startup.launcher.StartLauncherPlugin.startPlugin | ||||||
| import dev.inmo.micro_utils.startup.plugin.StartPlugin | import dev.inmo.micro_utils.startup.plugin.StartPlugin | ||||||
| @@ -13,9 +15,10 @@ import kotlinx.coroutines.joinAll | |||||||
| import kotlinx.coroutines.launch | import kotlinx.coroutines.launch | ||||||
| import kotlinx.serialization.SerialFormat | import kotlinx.serialization.SerialFormat | ||||||
| import kotlinx.serialization.StringFormat | import kotlinx.serialization.StringFormat | ||||||
|  | import kotlinx.serialization.json.Json | ||||||
| import kotlinx.serialization.json.JsonObject | import kotlinx.serialization.json.JsonObject | ||||||
| import kotlinx.serialization.json.decodeFromJsonElement |  | ||||||
| import kotlinx.serialization.json.jsonObject | import kotlinx.serialization.json.jsonObject | ||||||
|  | import kotlinx.serialization.modules.SerializersModule | ||||||
| import org.koin.core.Koin | import org.koin.core.Koin | ||||||
| import org.koin.core.KoinApplication | import org.koin.core.KoinApplication | ||||||
| import org.koin.core.context.startKoin | import org.koin.core.context.startKoin | ||||||
| @@ -35,7 +38,20 @@ object StartLauncherPlugin : StartPlugin { | |||||||
|         single { rawJsonObject } |         single { rawJsonObject } | ||||||
|         single { config } |         single { config } | ||||||
|         single { CoroutineScope(Dispatchers.Default) } |         single { CoroutineScope(Dispatchers.Default) } | ||||||
|         single { defaultJson } binds arrayOf(StringFormat::class, SerialFormat::class) |         single { | ||||||
|  |             val serializersModules = getAll<SerializersModule>().distinct() | ||||||
|  |             val baseJson = baseJsonProvider ?: defaultJson | ||||||
|  |             if (serializersModules.isEmpty()) { | ||||||
|  |                 baseJson | ||||||
|  |             } else { | ||||||
|  |                 Json(baseJson) { | ||||||
|  |                     serializersModule = SerializersModule { | ||||||
|  |                         include(baseJson.serializersModule) | ||||||
|  |                         serializersModules.forEach { include(it) } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } binds arrayOf(StringFormat::class, SerialFormat::class) | ||||||
|  |  | ||||||
|         includes( |         includes( | ||||||
|             config.plugins.mapNotNull { |             config.plugins.mapNotNull { | ||||||
|   | |||||||
| @@ -1,25 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.startup.launcher |  | ||||||
|  |  | ||||||
| import dev.inmo.kslog.common.KSLog |  | ||||||
| import dev.inmo.kslog.common.i |  | ||||||
| import kotlinx.serialization.json.JsonObject |  | ||||||
| import kotlinx.serialization.json.jsonObject |  | ||||||
|  |  | ||||||
| @Deprecated("Useless due to including of the same functionality in StrtLauncherPlugin") |  | ||||||
| object PluginsStarter { |  | ||||||
|     init { |  | ||||||
|         KSLog.default = KSLog("Launcher") |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * It is expected that you have registered all the [dev.inmo.micro_utils.startup.plugin.StartPlugin]s of your JS |  | ||||||
|      * app inside of [dev.inmo.micro_utils.startup.plugin.StartPluginSerializer] using its |  | ||||||
|      * [dev.inmo.micro_utils.startup.plugin.StartPluginSerializer.registerPlugin] method |  | ||||||
|      */ |  | ||||||
|     suspend fun startPlugins(json: JsonObject) = StartLauncherPlugin.start(json) |  | ||||||
|     /** |  | ||||||
|      * Will convert [config] to [JsonObject] with auto registration of [dev.inmo.micro_utils.startup.plugin.StartPlugin]s |  | ||||||
|      * in [dev.inmo.micro_utils.startup.plugin.StartPluginSerializer] |  | ||||||
|      */ |  | ||||||
|     suspend fun startPlugins(config: Config) = StartLauncherPlugin.start(config) |  | ||||||
| } |  | ||||||
| @@ -3,7 +3,7 @@ plugins { | |||||||
|     id "org.jetbrains.kotlin.plugin.serialization" |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
| } | } | ||||||
|  |  | ||||||
| apply from: "$mppJsAndJavaProjectPresetPath" | apply from: "$mppJvmJsLinuxMingwProjectPresetPath" | ||||||
|  |  | ||||||
| kotlin { | kotlin { | ||||||
|     sourceSets { |     sourceSets { | ||||||
| @@ -21,5 +21,15 @@ kotlin { | |||||||
|                 api libs.uuid |                 api libs.uuid | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |         linuxX64Main { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.uuid | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         mingwX64Main { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.uuid | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,36 @@ | |||||||
|  | package dev.inmo.micro_utils.startup.plugin | ||||||
|  |  | ||||||
|  | import com.benasher44.uuid.uuid4 | ||||||
|  | import kotlinx.serialization.KSerializer | ||||||
|  | import kotlinx.serialization.builtins.serializer | ||||||
|  | import kotlinx.serialization.descriptors.SerialDescriptor | ||||||
|  | import kotlinx.serialization.encoding.Decoder | ||||||
|  | import kotlinx.serialization.encoding.Encoder | ||||||
|  | import kotlin.reflect.KClass | ||||||
|  |  | ||||||
|  | actual object StartPluginSerializer : KSerializer<StartPlugin> { | ||||||
|  |     private val registeredPlugins = mutableMapOf<String, StartPlugin>() | ||||||
|  |     private val registeredPluginsByPlugin = mutableMapOf<StartPlugin, String>() | ||||||
|  |     override val descriptor: SerialDescriptor = String.serializer().descriptor | ||||||
|  |  | ||||||
|  |     override fun deserialize(decoder: Decoder): StartPlugin { | ||||||
|  |         val name = decoder.decodeString() | ||||||
|  |         return registeredPlugins[name] ?: error("Unable to find startup plugin for $name") | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun serialize(encoder: Encoder, value: StartPlugin) { | ||||||
|  |         val name = registeredPluginsByPlugin[value] ?: uuid4().toString().also { | ||||||
|  |             registeredPlugins[it] = value | ||||||
|  |             registeredPluginsByPlugin[value] = it | ||||||
|  |         } | ||||||
|  |         encoder.encodeString(name) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Register plugin inside of this [KSerializer]. Since plugin has been registered, you may use its [name] in any | ||||||
|  |      * serialized [dev.inmo.micro_utils.startup.launcher.Config] to retrieve [plugin] you passed here | ||||||
|  |      */ | ||||||
|  |     fun registerPlugin(name: String, plugin: StartPlugin) { | ||||||
|  |         registeredPlugins[name] = plugin | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,36 @@ | |||||||
|  | package dev.inmo.micro_utils.startup.plugin | ||||||
|  |  | ||||||
|  | import com.benasher44.uuid.uuid4 | ||||||
|  | import kotlinx.serialization.KSerializer | ||||||
|  | import kotlinx.serialization.builtins.serializer | ||||||
|  | import kotlinx.serialization.descriptors.SerialDescriptor | ||||||
|  | import kotlinx.serialization.encoding.Decoder | ||||||
|  | import kotlinx.serialization.encoding.Encoder | ||||||
|  | import kotlin.reflect.KClass | ||||||
|  |  | ||||||
|  | actual object StartPluginSerializer : KSerializer<StartPlugin> { | ||||||
|  |     private val registeredPlugins = mutableMapOf<String, StartPlugin>() | ||||||
|  |     private val registeredPluginsByPlugin = mutableMapOf<StartPlugin, String>() | ||||||
|  |     override val descriptor: SerialDescriptor = String.serializer().descriptor | ||||||
|  |  | ||||||
|  |     override fun deserialize(decoder: Decoder): StartPlugin { | ||||||
|  |         val name = decoder.decodeString() | ||||||
|  |         return registeredPlugins[name] ?: error("Unable to find startup plugin for $name") | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun serialize(encoder: Encoder, value: StartPlugin) { | ||||||
|  |         val name = registeredPluginsByPlugin[value] ?: uuid4().toString().also { | ||||||
|  |             registeredPlugins[it] = value | ||||||
|  |             registeredPluginsByPlugin[value] = it | ||||||
|  |         } | ||||||
|  |         encoder.encodeString(name) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Register plugin inside of this [KSerializer]. Since plugin has been registered, you may use its [name] in any | ||||||
|  |      * serialized [dev.inmo.micro_utils.startup.launcher.Config] to retrieve [plugin] you passed here | ||||||
|  |      */ | ||||||
|  |     fun registerPlugin(name: String, plugin: StartPlugin) { | ||||||
|  |         registeredPlugins[name] = plugin | ||||||
|  |     } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user