mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-18 06:49:20 +00:00
Compare commits
35 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 |
38
CHANGELOG.md
38
CHANGELOG.md
@@ -1,5 +1,43 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
* `Common`:
|
||||
|
@@ -23,6 +23,7 @@ allprojects {
|
||||
mavenCentral()
|
||||
google()
|
||||
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
|
||||
|
@@ -20,5 +20,16 @@ kotlin {
|
||||
}
|
||||
dependsOn jvmMain
|
||||
}
|
||||
|
||||
linuxX64Main {
|
||||
dependencies {
|
||||
api libs.okio
|
||||
}
|
||||
}
|
||||
mingwX64Main {
|
||||
dependencies {
|
||||
api libs.okio
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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,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 {
|
||||
dependencies {
|
||||
api project(":micro_utils.common")
|
||||
api libs.krypto
|
||||
}
|
||||
}
|
||||
jsMain {
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
import com.soywiz.krypto.md5
|
||||
|
||||
typealias MD5 = String
|
||||
|
||||
expect fun SourceBytes.md5(): MD5
|
||||
fun SourceString.md5(): MD5 = encodeToByteArray().md5()
|
||||
fun SourceBytes.md5(): MD5 = md5().hexLower
|
||||
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"
|
||||
mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.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"
|
||||
|
||||
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle"
|
||||
|
@@ -14,5 +14,5 @@ crypto_js_version=4.1.1
|
||||
# Project data
|
||||
|
||||
group=dev.inmo
|
||||
version=0.17.5
|
||||
android_code_version=187
|
||||
version=0.18.0
|
||||
android_code_version=191
|
||||
|
@@ -1,40 +1,42 @@
|
||||
[versions]
|
||||
|
||||
kt = "1.8.10"
|
||||
kt = "1.8.20"
|
||||
kt-serialization = "1.5.0"
|
||||
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-dokka = "1.8.10"
|
||||
|
||||
klock = "3.4.0"
|
||||
korlibs = "3.4.0"
|
||||
uuid = "0.7.0"
|
||||
|
||||
ktor = "2.2.4"
|
||||
ktor = "2.3.0"
|
||||
|
||||
gh-release = "2.4.1"
|
||||
|
||||
koin = "3.3.2"
|
||||
koin = "3.4.0"
|
||||
|
||||
ksp = "1.8.10-1.0.9"
|
||||
kotlin-poet = "1.12.0"
|
||||
okio = "3.3.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"
|
||||
|
||||
android-coreKtx = "1.9.0"
|
||||
android-recyclerView = "1.2.1"
|
||||
android-coreKtx = "1.10.0"
|
||||
android-recyclerView = "1.3.0"
|
||||
android-appCompat = "1.6.1"
|
||||
android-fragment = "1.5.5"
|
||||
android-fragment = "1.5.7"
|
||||
android-espresso = "3.5.1"
|
||||
android-test = "1.1.5"
|
||||
|
||||
android-props-minSdk = "21"
|
||||
android-props-compileSdk = "33"
|
||||
android-props-buildTools = "33.0.1"
|
||||
android-props-buildTools = "33.0.2"
|
||||
|
||||
[libraries]
|
||||
|
||||
@@ -67,7 +69,8 @@ ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negoti
|
||||
|
||||
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" }
|
||||
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" }
|
||||
ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
|
||||
|
||||
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
|
||||
|
||||
# Buildscript
|
||||
|
||||
buildscript-kt-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kt" }
|
||||
|
@@ -18,5 +18,17 @@ kotlin {
|
||||
androidMain {
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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()) {
|
||||
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(
|
||||
field: String
|
||||
) = request.queryParameters[field]
|
||||
|
||||
fun ApplicationCall.getQueryParameters(
|
||||
field: String
|
||||
) = request.queryParameters.getAll(field)
|
||||
|
||||
suspend fun ApplicationCall.getQueryParameterOrSendError(
|
||||
field: String
|
||||
) = getQueryParameter(field).also {
|
||||
@@ -23,3 +35,11 @@ suspend fun ApplicationCall.getQueryParameterOrSendError(
|
||||
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
|
||||
from bs4 import BeautifulSoup
|
||||
import pandas as pd
|
||||
@@ -17,33 +19,45 @@ def fix_name(category, raw_name):
|
||||
result += out1
|
||||
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
|
||||
if __name__ == '__main__':
|
||||
df = pd.read_html(open('table.html', 'r'))
|
||||
df = pd.read_html(open('local.table.html', 'r'))
|
||||
mimes = []
|
||||
for row in df[0].drop_duplicates(subset=['MIME Type / Internet Media Type'], keep='first').iterrows():
|
||||
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_name = mime.split('/', 1)[1]
|
||||
mimes.append({
|
||||
'mime_category': mime_category,
|
||||
'mime_name': mime_name,
|
||||
})
|
||||
mimes.append([
|
||||
mime_category,
|
||||
mime_name,
|
||||
extensions
|
||||
])
|
||||
|
||||
# codegen
|
||||
|
||||
mimes.sort(key=lambda x: x['mime_category'])
|
||||
grouped = itertools.groupby(mimes, lambda x: x['mime_category'])
|
||||
mimes.sort(key=lambda x: x[0])
|
||||
grouped = itertools.groupby(mimes, lambda x: x[0])
|
||||
code = ''
|
||||
code2 = 'internal val knownMimeTypes: Set<MimeType> = setOf(\n'
|
||||
code2 += ' KnownMimeTypes.Any,\n'
|
||||
for key, group in grouped:
|
||||
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)
|
||||
for mime in group:
|
||||
name = fix_name(mime['mime_category'], mime['mime_name'])
|
||||
code += ' @Serializable(MimeTypeSerializer::class)\n object %s: %s ("%s/%s")\n' % (name, group_name, mime['mime_category'], mime['mime_name'])
|
||||
name = fix_name(mime[0], mime[1])
|
||||
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)
|
||||
code += '}\n\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)
|
||||
interface MimeType {
|
||||
val raw: String
|
||||
val extensions: Array<String>
|
||||
get() = emptyArray()
|
||||
}
|
||||
|
@@ -15,6 +15,8 @@ kotlin {
|
||||
browser()
|
||||
nodejs()
|
||||
}
|
||||
linuxX64()
|
||||
mingwX64()
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
@@ -18,6 +18,8 @@ kotlin {
|
||||
android {
|
||||
publishAllLibraryVariants()
|
||||
}
|
||||
linuxX64()
|
||||
mingwX64()
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
@@ -50,6 +52,16 @@ kotlin {
|
||||
implementation libs.android.espresso
|
||||
}
|
||||
}
|
||||
mingwX64Test {
|
||||
dependencies {
|
||||
implementation kotlin('test-junit')
|
||||
}
|
||||
}
|
||||
linuxX64Test {
|
||||
dependencies {
|
||||
implementation kotlin('test-junit')
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
) : WriteCRUDRepo<RegisteredObject, Id, InputObject>, FallbackCacheRepo {
|
||||
override val deletedObjectsIdsFlow: Flow<Id>
|
||||
get() = (originalRepo.deletedObjectsIdsFlow + kvCache.onValueRemoved).distinctUntilChanged()
|
||||
get() = (originalRepo.deletedObjectsIdsFlow).distinctUntilChanged()
|
||||
override val newObjectsFlow: Flow<RegisteredObject>
|
||||
get() = (originalRepo.newObjectsFlow + kvCache.onNewValue.map { it.second }).distinctUntilChanged()
|
||||
get() = (originalRepo.newObjectsFlow).distinctUntilChanged()
|
||||
override val updatedObjectsFlow: Flow<RegisteredObject>
|
||||
get() = originalRepo.updatedObjectsFlow
|
||||
|
||||
|
@@ -17,10 +17,10 @@ open class AutoRecacheWriteKeyValueRepo<Id, RegisteredObject>(
|
||||
protected val kvCache: FullKVCache<Id, RegisteredObject> = FullKVCache()
|
||||
) : WriteKeyValueRepo<Id, RegisteredObject>, FallbackCacheRepo {
|
||||
override val onValueRemoved: Flow<Id>
|
||||
get() = (originalRepo.onValueRemoved + kvCache.onValueRemoved).distinctUntilChanged()
|
||||
get() = (originalRepo.onValueRemoved).distinctUntilChanged()
|
||||
|
||||
override val onNewValue: Flow<Pair<Id, RegisteredObject>>
|
||||
get() = (originalRepo.onNewValue + kvCache.onNewValue).distinctUntilChanged()
|
||||
get() = (originalRepo.onNewValue).distinctUntilChanged()
|
||||
|
||||
private val onRemovingUpdatesListeningJob = originalRepo.onValueRemoved.subscribeSafelyWithoutExceptions(scope) {
|
||||
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.cache.cache.FullKVCache
|
||||
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.unset
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -24,7 +25,7 @@ open class AutoRecacheWriteKeyValuesRepo<Id, RegisteredObject>(
|
||||
override val onNewValue: Flow<Pair<Id, RegisteredObject>>
|
||||
get() = originalRepo.onNewValue
|
||||
override val onDataCleared: Flow<Id>
|
||||
get() = (originalRepo.onDataCleared + kvCache.onValueRemoved).distinctUntilChanged()
|
||||
get() = (originalRepo.onDataCleared).distinctUntilChanged()
|
||||
|
||||
private val onDataClearedListeningJob = originalRepo.onDataCleared.subscribeSafelyWithoutExceptions(scope) {
|
||||
kvCache.unset(it)
|
||||
@@ -50,7 +51,7 @@ open class AutoRecacheWriteKeyValuesRepo<Id, RegisteredObject>(
|
||||
|
||||
override suspend fun clearWithValue(v: RegisteredObject) {
|
||||
originalRepo.clearWithValue(v)
|
||||
doForAllWithNextPaging(FirstPagePagination(kvCache.count().takeIf { it < Int.MAX_VALUE } ?.toInt() ?: Int.MAX_VALUE)) {
|
||||
doForAllWithNextPaging(kvCache.maxPagePagination()) {
|
||||
kvCache.keys(it).also {
|
||||
it.results.forEach { id ->
|
||||
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>>) {
|
||||
originalRepo.add(toAdd)
|
||||
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(
|
||||
kvCache: FullKVCache<IdType, ObjectType>,
|
||||
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
|
||||
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(
|
||||
kvCache: FullKVCache<Key, Value>,
|
||||
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() {
|
||||
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(
|
||||
kvCache: FullKVCache<Key, List<Value>>,
|
||||
scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package dev.inmo.micro_utils.repos
|
||||
|
||||
import dev.inmo.micro_utils.pagination.*
|
||||
import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging
|
||||
import dev.inmo.micro_utils.pagination.utils.getAllWithNextPaging
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -44,9 +45,23 @@ interface WriteKeyValuesRepo<Key, Value> : Repo {
|
||||
|
||||
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>>)
|
||||
|
||||
/**
|
||||
* 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)
|
||||
/**
|
||||
* Clear [v] **with** full clear of all data by [Key]s with [v]
|
||||
*/
|
||||
suspend fun clearWithValue(v: Value)
|
||||
|
||||
suspend fun set(toSet: Map<Key, List<Value>>) {
|
||||
@@ -100,6 +115,21 @@ interface KeyValuesRepo<Key, Value> : ReadKeyValuesRepo<Key, Value>, WriteKeyVal
|
||||
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>
|
||||
|
||||
|
@@ -97,6 +97,8 @@ open class MapperWriteKeyValuesRepo<FromKey, FromValue, ToKey, ToValue>(
|
||||
}.toMap()
|
||||
)
|
||||
|
||||
override suspend fun removeWithValue(v: FromValue) = to.removeWithValue(v.toOutValue())
|
||||
|
||||
override suspend fun set(toSet: Map<FromKey, List<FromValue>>) {
|
||||
to.set(
|
||||
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.repos.KeyValueRepo
|
||||
import dev.inmo.micro_utils.repos.KeyValuesRepo
|
||||
import dev.inmo.micro_utils.repos.set
|
||||
import dev.inmo.micro_utils.repos.unset
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
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>>) {
|
||||
original.set(
|
||||
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.utils.reverse
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import dev.inmo.micro_utils.repos.crud.asId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
@@ -260,6 +261,19 @@ class OneToManyAndroidRepo<Key, Value>(
|
||||
_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(
|
||||
|
@@ -5,6 +5,7 @@ import dev.inmo.micro_utils.repos.exposed.ColumnAllocator
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
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) {
|
||||
transaction(database) {
|
||||
deleteWhere { keyColumn.eq(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) {
|
||||
map.remove(k) ?.also { _onDataCleared.emit(k) }
|
||||
}
|
||||
|
||||
override suspend fun clearWithValue(v: Value) {
|
||||
map.forEach { (k, values) ->
|
||||
if (values.remove(v)) _onValueRemoved.emit(k to v)
|
||||
map.filter { (_, values) ->
|
||||
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" }
|
||||
}
|
||||
|
||||
@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)
|
||||
override suspend fun clear(k: Key) {
|
||||
httpClient.post(
|
||||
|
@@ -13,6 +13,7 @@ const val onDataClearedRoute = "onDataCleared"
|
||||
|
||||
const val addRoute = "add"
|
||||
const val removeRoute = "remove"
|
||||
const val removeWithValueRoute = "removeWithValue"
|
||||
const val clearRoute = "clear"
|
||||
const val clearWithValueRoute = "clearWithValue"
|
||||
const val setRoute = "set"
|
||||
|
@@ -46,6 +46,11 @@ inline fun <reified Key : Any, reified Value : Any> Route.configureWriteKeyValue
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
|
||||
post(removeWithValueRoute) {
|
||||
originalRepo.removeWithValue(call.receive())
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
|
||||
post(clearRoute) {
|
||||
originalRepo.clear(call.receive())
|
||||
call.respond(HttpStatusCode.OK)
|
||||
|
@@ -4,7 +4,7 @@ plugins {
|
||||
id "application"
|
||||
}
|
||||
|
||||
apply from: "$mppJsAndJavaProjectPresetPath"
|
||||
apply from: "$mppJvmJsLinuxMingwProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
|
@@ -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,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"
|
||||
}
|
||||
|
||||
apply from: "$mppJsAndJavaProjectPresetPath"
|
||||
apply from: "$mppJvmJsLinuxMingwProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
@@ -21,5 +21,15 @@ kotlin {
|
||||
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