mirror of
				https://github.com/InsanusMokrassar/MicroUtils.git
				synced 2025-11-04 14:14:51 +00:00 
			
		
		
		
	Compare commits
	
		
			43 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 3a45e5dc70 | |||
| 73190518d5 | |||
| 03f78180dc | |||
| 1c0b8cf842 | |||
| a1624ea2a9 | |||
| 23a050cf1e | |||
| 916f2f96f4 | |||
| 00cc214754 | |||
| b2e38f72b9 | |||
| e7107d238d | |||
| ed9ebdbd1a | |||
| e80676d3d2 | |||
| 02d02fa8f2 | |||
| bd783fb74f | |||
| 50386adf70 | |||
| f4ee6c2890 | |||
| d45aef9fe5 | |||
| a56cd3dddd | |||
| 419e7070ee | |||
| 612cf40b5f | |||
| 8b39882e83 | |||
| e639ae172b | |||
| d0446850ae | |||
| c48465b90b | |||
| f419fd03d2 | |||
| 494812a660 | |||
| eb78f21eec | |||
| 4bda70268b | |||
| f037ce4371 | |||
| 3d2196e35d | |||
| a74f061b02 | |||
| 11ade14676 | |||
| eb562d8784 | |||
| 1ee5b4bfd4 | |||
| d97892080b | |||
| 6f37125724 | |||
| ed1baaade7 | |||
| bb9669f8fd | |||
| bdac715d48 | |||
| acf4971298 | |||
| 249bc83a8c | |||
| 0fbb92f03f | |||
| ca27cb3f82 | 
							
								
								
									
										6
									
								
								.github/workflows/dokka_push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/dokka_push.yml
									
									
									
									
										vendored
									
									
								
							@@ -10,10 +10,10 @@ jobs:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - uses: actions/setup-java@v1
 | 
			
		||||
        with:
 | 
			
		||||
          java-version: 1.8
 | 
			
		||||
      - name: Fix android 31.0.0 dx
 | 
			
		||||
          java-version: 11
 | 
			
		||||
      - name: Fix android 32.0.0 dx
 | 
			
		||||
        continue-on-error: true
 | 
			
		||||
        run: cd /usr/local/lib/android/sdk/build-tools/31.0.0/ && mv d8 dx && cd lib  && mv d8.jar dx.jar
 | 
			
		||||
        run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib  && mv d8.jar dx.jar
 | 
			
		||||
      - name: Build
 | 
			
		||||
        run: ./gradlew dokkaHtml
 | 
			
		||||
      - name: Publish KDocs
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								.github/workflows/packages_push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/packages_push.yml
									
									
									
									
										vendored
									
									
								
							@@ -8,10 +8,10 @@ jobs:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - uses: actions/setup-java@v1
 | 
			
		||||
        with:
 | 
			
		||||
          java-version: 1.8
 | 
			
		||||
      - name: Fix android 31.0.0 dx
 | 
			
		||||
          java-version: 11
 | 
			
		||||
      - name: Fix android 32.0.0 dx
 | 
			
		||||
        continue-on-error: true
 | 
			
		||||
        run: cd /usr/local/lib/android/sdk/build-tools/31.0.0/ && mv d8 dx && cd lib  && mv d8.jar dx.jar
 | 
			
		||||
        run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib  && mv d8.jar dx.jar
 | 
			
		||||
      - name: Rewrite version
 | 
			
		||||
        run: |
 | 
			
		||||
          branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -11,5 +11,6 @@ out/
 | 
			
		||||
 | 
			
		||||
secret.gradle
 | 
			
		||||
local.properties
 | 
			
		||||
kotlin-js-store
 | 
			
		||||
 | 
			
		||||
publishing.sh
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										67
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										67
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,5 +1,72 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
## 0.9.3
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `UUID`: `0.3.1` -> `0.4.0`
 | 
			
		||||
 | 
			
		||||
## 0.9.2
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `Klock`: `2.4.10` -> `2.4.12`
 | 
			
		||||
 | 
			
		||||
## 0.9.1
 | 
			
		||||
 | 
			
		||||
* `Repos`:
 | 
			
		||||
    * `Exposed`:
 | 
			
		||||
        * Default realizations of standard interfaces for exposed DB are using public fields for now:
 | 
			
		||||
            * `ExposedReadKeyValueRepo`
 | 
			
		||||
            * `ExposedReadOneToManyKeyValueRepo`
 | 
			
		||||
            * `ExposedStandardVersionsRepoProxy`
 | 
			
		||||
        * New typealiases for one to many exposed realizations:
 | 
			
		||||
            * `ExposedReadKeyValuesRepo`
 | 
			
		||||
            * `ExposedKeyValuesRepo`
 | 
			
		||||
 | 
			
		||||
## 0.9.0
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `Kotlin`: `1.5.31` -> `1.6.10`
 | 
			
		||||
    * `Coroutines`: `1.5.2` -> `1.6.0`
 | 
			
		||||
    * `Serialization`: `1.3.1` -> `1.3.2`
 | 
			
		||||
    * `Exposed`: `0.36.2` -> `0.37.2`
 | 
			
		||||
    * `Ktor`: `1.6.5` -> `1.6.7`
 | 
			
		||||
    * `Klock`: `2.4.8` -> `2.4.10`
 | 
			
		||||
 | 
			
		||||
## 0.8.9
 | 
			
		||||
 | 
			
		||||
* `Ktor`:
 | 
			
		||||
    * `Server`:
 | 
			
		||||
        * Fixes in `uniloadMultipart`
 | 
			
		||||
    * `Client`:
 | 
			
		||||
        * Fixes in `unimultipart`
 | 
			
		||||
* `FSM`:
 | 
			
		||||
    * Fixes in `DefaultUpdatableStatesMachine`
 | 
			
		||||
 | 
			
		||||
## 0.8.8
 | 
			
		||||
 | 
			
		||||
* `Versions`:
 | 
			
		||||
    * `AppCompat`: `1.3.1` -> `1.4.0`
 | 
			
		||||
    * Android Compile SDK: `31.0.0` -> `32.0.0`
 | 
			
		||||
* `FSM`:
 | 
			
		||||
    * `DefaultStatesMachine` now is extendable
 | 
			
		||||
    * New type `UpdatableStatesMachine` with default realization`DefaultUpdatableStatesMachine`
 | 
			
		||||
 | 
			
		||||
## 0.8.7
 | 
			
		||||
 | 
			
		||||
* `Ktor`:
 | 
			
		||||
    * `Client`:
 | 
			
		||||
        * `UnifiedRequester` now have no private fields
 | 
			
		||||
        * Add preview work with multipart
 | 
			
		||||
    * `Server`
 | 
			
		||||
        * `UnifiedRouter` now have no private fields
 | 
			
		||||
        * Add preview work with multipart
 | 
			
		||||
 | 
			
		||||
## 0.8.6
 | 
			
		||||
 | 
			
		||||
* `Common`:
 | 
			
		||||
    * `Either` extensions `onFirst` and `onSecond` now accept not `crossinline` callbacks
 | 
			
		||||
    * All `joinTo` now accept not `crossinline` callbacks
 | 
			
		||||
 | 
			
		||||
## 0.8.5
 | 
			
		||||
 | 
			
		||||
* `Common`:
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ buildscript {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dependencies {
 | 
			
		||||
        classpath 'com.android.tools.build:gradle:4.1.3'
 | 
			
		||||
        classpath 'com.android.tools.build:gradle:7.0.4'
 | 
			
		||||
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
 | 
			
		||||
        classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
 | 
			
		||||
        classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
 | 
			
		||||
 
 | 
			
		||||
@@ -32,8 +32,7 @@ class EitherSerializer<T1, T2>(
 | 
			
		||||
    t1Serializer: KSerializer<T1>,
 | 
			
		||||
    t2Serializer: KSerializer<T2>,
 | 
			
		||||
) : KSerializer<Either<T1, T2>> {
 | 
			
		||||
    @ExperimentalSerializationApi
 | 
			
		||||
    @InternalSerializationApi
 | 
			
		||||
    @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
 | 
			
		||||
    override val descriptor: SerialDescriptor = buildSerialDescriptor(
 | 
			
		||||
        "TypedSerializer",
 | 
			
		||||
        SerialKind.CONTEXTUAL
 | 
			
		||||
@@ -44,8 +43,7 @@ class EitherSerializer<T1, T2>(
 | 
			
		||||
    private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer)
 | 
			
		||||
    private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer)
 | 
			
		||||
 | 
			
		||||
    @ExperimentalSerializationApi
 | 
			
		||||
    @InternalSerializationApi
 | 
			
		||||
    @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
 | 
			
		||||
    override fun deserialize(decoder: Decoder): Either<T1, T2> {
 | 
			
		||||
        return decoder.decodeStructure(descriptor) {
 | 
			
		||||
            var type: String? = null
 | 
			
		||||
@@ -77,8 +75,7 @@ class EitherSerializer<T1, T2>(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @ExperimentalSerializationApi
 | 
			
		||||
    @InternalSerializationApi
 | 
			
		||||
    @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
 | 
			
		||||
    override fun serialize(encoder: Encoder, value: Either<T1, T2>) {
 | 
			
		||||
        encoder.encodeStructure(descriptor) {
 | 
			
		||||
            when (value) {
 | 
			
		||||
@@ -129,7 +126,7 @@ inline fun <T1, T2> Either.Companion.second(t2: T2): Either<T1, T2> = EitherSeco
 | 
			
		||||
/**
 | 
			
		||||
 * Will call [block] in case when [Either.t1] of [this] is not null
 | 
			
		||||
 */
 | 
			
		||||
inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(crossinline block: (T1) -> Unit): E {
 | 
			
		||||
inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(block: (T1) -> Unit): E {
 | 
			
		||||
    val t1 = t1
 | 
			
		||||
    t1 ?.let(block)
 | 
			
		||||
    return this
 | 
			
		||||
@@ -138,7 +135,7 @@ inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(crossinline block: (T1) -> Uni
 | 
			
		||||
/**
 | 
			
		||||
 * Will call [block] in case when [Either.t2] of [this] is not null
 | 
			
		||||
 */
 | 
			
		||||
inline fun <T1, T2, E : Either<T1, T2>> E.onSecond(crossinline block: (T2) -> Unit): E {
 | 
			
		||||
inline fun <T1, T2, E : Either<T1, T2>> E.onSecond(block: (T2) -> Unit): E {
 | 
			
		||||
    val t2 = t2
 | 
			
		||||
    t2 ?.let(block)
 | 
			
		||||
    return this
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
package dev.inmo.micro_utils.common
 | 
			
		||||
 | 
			
		||||
inline fun <I, R> Iterable<I>.joinTo(
 | 
			
		||||
    crossinline separatorFun: (I) -> R?,
 | 
			
		||||
    separatorFun: (I) -> R?,
 | 
			
		||||
    prefix: R? = null,
 | 
			
		||||
    postfix: R? = null,
 | 
			
		||||
    crossinline transform: (I) -> R?
 | 
			
		||||
    transform: (I) -> R?
 | 
			
		||||
): List<R> {
 | 
			
		||||
    val result = mutableListOf<R>()
 | 
			
		||||
    val iterator = iterator()
 | 
			
		||||
@@ -29,11 +29,11 @@ inline fun <I, R> Iterable<I>.joinTo(
 | 
			
		||||
    separator: R? = null,
 | 
			
		||||
    prefix: R? = null,
 | 
			
		||||
    postfix: R? = null,
 | 
			
		||||
    crossinline transform: (I) -> R?
 | 
			
		||||
    transform: (I) -> R?
 | 
			
		||||
): List<R> = joinTo({ separator }, prefix, postfix, transform)
 | 
			
		||||
 | 
			
		||||
inline fun <I> Iterable<I>.joinTo(
 | 
			
		||||
    crossinline separatorFun: (I) -> I?,
 | 
			
		||||
    separatorFun: (I) -> I?,
 | 
			
		||||
    prefix: I? = null,
 | 
			
		||||
    postfix: I? = null
 | 
			
		||||
): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it }
 | 
			
		||||
@@ -45,15 +45,15 @@ inline fun <I> Iterable<I>.joinTo(
 | 
			
		||||
): List<I> = joinTo<I>({ separator }, prefix, postfix)
 | 
			
		||||
 | 
			
		||||
inline fun <I, reified R> Array<I>.joinTo(
 | 
			
		||||
    crossinline separatorFun: (I) -> R?,
 | 
			
		||||
    separatorFun: (I) -> R?,
 | 
			
		||||
    prefix: R? = null,
 | 
			
		||||
    postfix: R? = null,
 | 
			
		||||
    crossinline transform: (I) -> R?
 | 
			
		||||
    transform: (I) -> R?
 | 
			
		||||
): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray()
 | 
			
		||||
 | 
			
		||||
inline fun <I, reified R> Array<I>.joinTo(
 | 
			
		||||
    separator: R? = null,
 | 
			
		||||
    prefix: R? = null,
 | 
			
		||||
    postfix: R? = null,
 | 
			
		||||
    crossinline transform: (I) -> R?
 | 
			
		||||
    transform: (I) -> R?
 | 
			
		||||
): Array<R> = asIterable().joinTo(separator, prefix, postfix, transform).toTypedArray()
 | 
			
		||||
 
 | 
			
		||||
@@ -23,11 +23,12 @@ value class FileName(val string: String) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@PreviewFeature
 | 
			
		||||
expect class MPPFile
 | 
			
		||||
 | 
			
		||||
expect val MPPFile.filename: FileName
 | 
			
		||||
expect val MPPFile.filesize: Long
 | 
			
		||||
expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator
 | 
			
		||||
expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator
 | 
			
		||||
fun MPPFile.bytesSync() = bytesAllocatorSync()
 | 
			
		||||
suspend fun MPPFile.bytes() = bytesAllocator()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,8 +21,10 @@ import kotlinx.serialization.Serializable
 | 
			
		||||
 */
 | 
			
		||||
@Serializable
 | 
			
		||||
data class Optional<T> internal constructor(
 | 
			
		||||
    internal val data: T?,
 | 
			
		||||
    internal val dataPresented: Boolean
 | 
			
		||||
    @Warning("It is unsafe to use this data directly")
 | 
			
		||||
    val data: T?,
 | 
			
		||||
    @Warning("It is unsafe to use this data directly")
 | 
			
		||||
    val dataPresented: Boolean
 | 
			
		||||
) {
 | 
			
		||||
    companion object {
 | 
			
		||||
        /**
 | 
			
		||||
@@ -42,17 +44,31 @@ inline val <T> T.optional
 | 
			
		||||
/**
 | 
			
		||||
 * Will call [block] when data presented ([Optional.dataPresented] == true)
 | 
			
		||||
 */
 | 
			
		||||
fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply {
 | 
			
		||||
inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply {
 | 
			
		||||
    if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Will call [block] when data presented ([Optional.dataPresented] == true)
 | 
			
		||||
 */
 | 
			
		||||
inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run {
 | 
			
		||||
    if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Will call [block] when data absent ([Optional.dataPresented] == false)
 | 
			
		||||
 */
 | 
			
		||||
fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply {
 | 
			
		||||
inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply {
 | 
			
		||||
    if (!dataPresented) { block() }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Will call [block] when data presented ([Optional.dataPresented] == true)
 | 
			
		||||
 */
 | 
			
		||||
inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run {
 | 
			
		||||
    if (!dataPresented) { @Suppress("UNCHECKED_CAST") block() } else null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise
 | 
			
		||||
 */
 | 
			
		||||
@@ -67,9 +83,10 @@ fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = if (dataPresented) @Supp
 | 
			
		||||
/**
 | 
			
		||||
 * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
 | 
			
		||||
 */
 | 
			
		||||
fun <T> Optional<T>.dataOrElse(block: () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
 | 
			
		||||
inline fun <T> Optional<T>.dataOrElse(block: () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
 | 
			
		||||
 */
 | 
			
		||||
@Deprecated("dataOrElse now is inline", ReplaceWith("dataOrElse", "dev.inmo.micro_utils.common.dataOrElse"))
 | 
			
		||||
suspend fun <T> Optional<T>.dataOrElseSuspendable(block: suspend () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,7 @@ package dev.inmo.micro_utils.common
 | 
			
		||||
 | 
			
		||||
import org.khronos.webgl.ArrayBuffer
 | 
			
		||||
import org.w3c.dom.ErrorEvent
 | 
			
		||||
import org.w3c.files.File
 | 
			
		||||
import org.w3c.files.FileReader
 | 
			
		||||
import org.w3c.files.*
 | 
			
		||||
import kotlin.js.Promise
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -24,6 +23,11 @@ fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure ->
 | 
			
		||||
    reader.readAsArrayBuffer(this)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun MPPFile.readBytes(): ByteArray {
 | 
			
		||||
    val reader = FileReaderSync()
 | 
			
		||||
    return reader.readAsArrayBuffer(this).toByteArray()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await()
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -40,5 +44,11 @@ actual val MPPFile.filesize: Long
 | 
			
		||||
 * @suppress
 | 
			
		||||
 */
 | 
			
		||||
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
 | 
			
		||||
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
 | 
			
		||||
    get() = ::readBytes
 | 
			
		||||
/**
 | 
			
		||||
 * @suppress
 | 
			
		||||
 */
 | 
			
		||||
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
 | 
			
		||||
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
 | 
			
		||||
    get() = ::dirtyReadBytes
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,11 @@ actual val MPPFile.filesize: Long
 | 
			
		||||
/**
 | 
			
		||||
 * @suppress
 | 
			
		||||
 */
 | 
			
		||||
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
 | 
			
		||||
    get() = ::readBytes
 | 
			
		||||
/**
 | 
			
		||||
 * @suppress
 | 
			
		||||
 */
 | 
			
		||||
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
 | 
			
		||||
    get() = {
 | 
			
		||||
        doInIO {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ kotlin {
 | 
			
		||||
    sourceSets {
 | 
			
		||||
        commonMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                api project(":micro_utils.common")
 | 
			
		||||
                api project(":micro_utils.coroutines")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,11 @@
 | 
			
		||||
package dev.inmo.micro_utils.fsm.common
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
 | 
			
		||||
import dev.inmo.micro_utils.common.Optional
 | 
			
		||||
import dev.inmo.micro_utils.common.onPresented
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.*
 | 
			
		||||
import kotlinx.coroutines.*
 | 
			
		||||
import kotlinx.coroutines.sync.Mutex
 | 
			
		||||
import kotlinx.coroutines.sync.withLock
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Default [StatesMachine] may [startChain] and use inside logic for handling [State]s. By default you may use
 | 
			
		||||
@@ -42,17 +45,53 @@ interface StatesMachine<T : State> : StatesHandler<T, T> {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Default realization of [StatesMachine]. It uses [statesManager] for incapsulation of [State]s storing and contexts
 | 
			
		||||
 * resolving, and uses [launchStateHandling] for [State] handling
 | 
			
		||||
 * resolving, and uses [launchStateHandling] for [State] handling.
 | 
			
		||||
 *
 | 
			
		||||
 * This class suppose to be extended in case you wish some custom behaviour inside of [launchStateHandling], for example
 | 
			
		||||
 */
 | 
			
		||||
class DefaultStatesMachine <T: State>(
 | 
			
		||||
    private val statesManager: StatesManager<T>,
 | 
			
		||||
    private val handlers: List<CheckableHandlerHolder<in T, T>>
 | 
			
		||||
open class DefaultStatesMachine <T: State>(
 | 
			
		||||
    protected val statesManager: StatesManager<T>,
 | 
			
		||||
    protected val handlers: List<CheckableHandlerHolder<in T, T>>,
 | 
			
		||||
) : StatesMachine<T> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Will call [launchStateHandling] for state handling
 | 
			
		||||
     */
 | 
			
		||||
    override suspend fun StatesMachine<in T>.handleState(state: T): T? = launchStateHandling(state, handlers)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * This
 | 
			
		||||
     */
 | 
			
		||||
    protected val statesJobs = mutableMapOf<T, Job>()
 | 
			
		||||
    protected val statesJobsMutex = Mutex()
 | 
			
		||||
 | 
			
		||||
    protected open suspend fun performUpdate(state: T) {
 | 
			
		||||
        val newState = launchStateHandling(state, handlers)
 | 
			
		||||
        if (newState != null) {
 | 
			
		||||
            statesManager.update(state, newState)
 | 
			
		||||
        } else {
 | 
			
		||||
            statesManager.endChain(state)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    open suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
 | 
			
		||||
        statesJobsMutex.withLock {
 | 
			
		||||
            statesJobs[actualState] ?.cancel()
 | 
			
		||||
            statesJobs[actualState] = scope.launch {
 | 
			
		||||
                performUpdate(actualState)
 | 
			
		||||
            }.also { job ->
 | 
			
		||||
                job.invokeOnCompletion { _ ->
 | 
			
		||||
                    scope.launch {
 | 
			
		||||
                        statesJobsMutex.withLock {
 | 
			
		||||
                            if (statesJobs[actualState] == job) {
 | 
			
		||||
                                statesJobs.remove(actualState)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Launch handling of states. On [statesManager] [StatesManager.onStartChain],
 | 
			
		||||
     * [statesManager] [StatesManager.onChainStateUpdated] will be called lambda with performing of state. If
 | 
			
		||||
@@ -60,23 +99,15 @@ class DefaultStatesMachine <T: State>(
 | 
			
		||||
     * [StatesManager.endChain].
 | 
			
		||||
     */
 | 
			
		||||
    override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
 | 
			
		||||
        val statePerformer: suspend (T) -> Unit = { state: T ->
 | 
			
		||||
            val newState = launchStateHandling(state, handlers)
 | 
			
		||||
            if (newState != null) {
 | 
			
		||||
                statesManager.update(state, newState)
 | 
			
		||||
            } else {
 | 
			
		||||
                statesManager.endChain(state)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) {
 | 
			
		||||
            launch { statePerformer(it) }
 | 
			
		||||
            launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
 | 
			
		||||
        }
 | 
			
		||||
        statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) {
 | 
			
		||||
            launch { statePerformer(it.second) }
 | 
			
		||||
            launch { performStateUpdate(Optional.presented(it.first), it.second, scope.LinkedSupervisorScope()) }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        statesManager.getActiveStates().forEach {
 | 
			
		||||
            launch { statePerformer(it) }
 | 
			
		||||
            launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,60 @@
 | 
			
		||||
package dev.inmo.micro_utils.fsm.common
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.common.*
 | 
			
		||||
import kotlinx.coroutines.*
 | 
			
		||||
import kotlinx.coroutines.sync.withLock
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This extender of [StatesMachine] interface declare one new function [updateChain]. Realizations of this interface
 | 
			
		||||
 * must be able to perform update of chain in internal [StatesManager]
 | 
			
		||||
 */
 | 
			
		||||
interface UpdatableStatesMachine<T : State> : StatesMachine<T> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Update chain with current state equal to [currentState] with [newState]. Behaviour of this update preforming
 | 
			
		||||
     * in cases when [currentState] does not exist in [StatesManager] must be declared inside of realization of
 | 
			
		||||
     * [StatesManager.update] function
 | 
			
		||||
     */
 | 
			
		||||
    suspend fun updateChain(currentState: T, newState: T)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
open class DefaultUpdatableStatesMachine<T : State>(
 | 
			
		||||
    statesManager: StatesManager<T>,
 | 
			
		||||
    handlers: List<CheckableHandlerHolder<in T, T>>,
 | 
			
		||||
) : DefaultStatesMachine<T>(
 | 
			
		||||
    statesManager,
 | 
			
		||||
    handlers
 | 
			
		||||
), UpdatableStatesMachine<T> {
 | 
			
		||||
    protected val jobsStates = mutableMapOf<Job, T>()
 | 
			
		||||
 | 
			
		||||
    override suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
 | 
			
		||||
        statesJobsMutex.withLock {
 | 
			
		||||
            if (compare(previousState, actualState)) {
 | 
			
		||||
                statesJobs[actualState] ?.cancel()
 | 
			
		||||
            }
 | 
			
		||||
            val job = previousState.mapOnPresented {
 | 
			
		||||
                statesJobs.remove(it)
 | 
			
		||||
            } ?.takeIf { it.isActive } ?: scope.launch {
 | 
			
		||||
                performUpdate(actualState)
 | 
			
		||||
            }.also { job ->
 | 
			
		||||
                job.invokeOnCompletion { _ ->
 | 
			
		||||
                    scope.launch {
 | 
			
		||||
                        statesJobsMutex.withLock {
 | 
			
		||||
                            statesJobs.remove(
 | 
			
		||||
                                jobsStates[job] ?: return@withLock
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            jobsStates.remove(job)
 | 
			
		||||
            statesJobs[actualState] = job
 | 
			
		||||
            jobsStates[job] = actualState
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected open suspend fun compare(previous: Optional<T>, new: T): Boolean = previous.dataOrNull() != new
 | 
			
		||||
 | 
			
		||||
    override suspend fun updateChain(currentState: T, newState: T) {
 | 
			
		||||
        statesManager.update(currentState, newState)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -7,6 +7,12 @@ import kotlin.reflect.KClass
 | 
			
		||||
 | 
			
		||||
class FSMBuilder<T : State>(
 | 
			
		||||
    var statesManager: StatesManager<T> = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()),
 | 
			
		||||
    val fsmBuilder: (statesManager: StatesManager<T>, states: List<CheckableHandlerHolder<T, T>>) -> StatesMachine<T> = { statesManager, states ->
 | 
			
		||||
        StatesMachine(
 | 
			
		||||
            statesManager,
 | 
			
		||||
            states
 | 
			
		||||
        )
 | 
			
		||||
    },
 | 
			
		||||
    var defaultStateHandler: StatesHandler<T, T>? = StatesHandler { null }
 | 
			
		||||
) {
 | 
			
		||||
    private var states = mutableListOf<CheckableHandlerHolder<T, T>>()
 | 
			
		||||
@@ -42,7 +48,7 @@ class FSMBuilder<T : State>(
 | 
			
		||||
        add(filter, handler)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun build() = StatesMachine(
 | 
			
		||||
    fun build() = fsmBuilder(
 | 
			
		||||
        statesManager,
 | 
			
		||||
        states.toList().let { list ->
 | 
			
		||||
            defaultStateHandler ?.let { list + it.holder { true } } ?: list
 | 
			
		||||
 
 | 
			
		||||
@@ -7,29 +7,29 @@ android.useAndroidX=true
 | 
			
		||||
android.enableJetifier=true
 | 
			
		||||
org.gradle.jvmargs=-Xmx2g
 | 
			
		||||
 | 
			
		||||
kotlin_version=1.5.31
 | 
			
		||||
kotlin_coroutines_version=1.5.2
 | 
			
		||||
kotlin_serialisation_core_version=1.3.1
 | 
			
		||||
kotlin_exposed_version=0.36.2
 | 
			
		||||
kotlin_version=1.6.10
 | 
			
		||||
kotlin_coroutines_version=1.6.0
 | 
			
		||||
kotlin_serialisation_core_version=1.3.2
 | 
			
		||||
kotlin_exposed_version=0.37.2
 | 
			
		||||
 | 
			
		||||
ktor_version=1.6.5
 | 
			
		||||
ktor_version=1.6.7
 | 
			
		||||
 | 
			
		||||
klockVersion=2.4.8
 | 
			
		||||
klockVersion=2.4.12
 | 
			
		||||
 | 
			
		||||
github_release_plugin_version=2.2.12
 | 
			
		||||
 | 
			
		||||
uuidVersion=0.3.1
 | 
			
		||||
uuidVersion=0.4.0
 | 
			
		||||
 | 
			
		||||
# ANDROID
 | 
			
		||||
 | 
			
		||||
core_ktx_version=1.7.0
 | 
			
		||||
androidx_recycler_version=1.2.1
 | 
			
		||||
appcompat_version=1.3.1
 | 
			
		||||
appcompat_version=1.4.0
 | 
			
		||||
 | 
			
		||||
android_minSdkVersion=19
 | 
			
		||||
android_compileSdkVersion=31
 | 
			
		||||
android_buildToolsVersion=31.0.0
 | 
			
		||||
dexcount_version=3.0.0
 | 
			
		||||
android_compileSdkVersion=32
 | 
			
		||||
android_buildToolsVersion=32.0.0
 | 
			
		||||
dexcount_version=3.0.1
 | 
			
		||||
junit_version=4.12
 | 
			
		||||
test_ext_junit_version=1.1.2
 | 
			
		||||
espresso_core=3.3.0
 | 
			
		||||
@@ -40,10 +40,10 @@ crypto_js_version=4.1.1
 | 
			
		||||
 | 
			
		||||
# Dokka
 | 
			
		||||
 | 
			
		||||
dokka_version=1.5.31
 | 
			
		||||
dokka_version=1.6.0
 | 
			
		||||
 | 
			
		||||
# Project data
 | 
			
		||||
 | 
			
		||||
group=dev.inmo
 | 
			
		||||
version=0.8.5
 | 
			
		||||
android_code_version=85
 | 
			
		||||
version=0.9.3
 | 
			
		||||
android_code_version=93
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,6 @@
 | 
			
		||||
package dev.inmo.micro_utils.ktor.client
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.common.MPPFile
 | 
			
		||||
import io.ktor.client.request.forms.InputProvider
 | 
			
		||||
 | 
			
		||||
expect suspend fun MPPFile.inputProvider(): InputProvider
 | 
			
		||||
@@ -1,16 +1,20 @@
 | 
			
		||||
package dev.inmo.micro_utils.ktor.client
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.common.MPPFile
 | 
			
		||||
import dev.inmo.micro_utils.common.filename
 | 
			
		||||
import dev.inmo.micro_utils.ktor.common.*
 | 
			
		||||
import io.ktor.client.HttpClient
 | 
			
		||||
import io.ktor.client.request.get
 | 
			
		||||
import io.ktor.client.request.post
 | 
			
		||||
import io.ktor.client.request.*
 | 
			
		||||
import io.ktor.client.request.forms.*
 | 
			
		||||
import io.ktor.http.*
 | 
			
		||||
import io.ktor.utils.io.core.ByteReadPacket
 | 
			
		||||
import kotlinx.serialization.*
 | 
			
		||||
 | 
			
		||||
typealias BodyPair<T> = Pair<SerializationStrategy<T>, T>
 | 
			
		||||
 | 
			
		||||
class UnifiedRequester(
 | 
			
		||||
    private val client: HttpClient = HttpClient(),
 | 
			
		||||
    private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
 | 
			
		||||
    val client: HttpClient = HttpClient(),
 | 
			
		||||
    val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
 | 
			
		||||
) {
 | 
			
		||||
    suspend fun <ResultType> uniget(
 | 
			
		||||
        url: String,
 | 
			
		||||
@@ -31,6 +35,54 @@ class UnifiedRequester(
 | 
			
		||||
        resultDeserializer: DeserializationStrategy<ResultType>
 | 
			
		||||
    ) = client.unipost(url, bodyInfo, resultDeserializer, serialFormat)
 | 
			
		||||
 | 
			
		||||
    suspend fun <ResultType> unimultipart(
 | 
			
		||||
        url: String,
 | 
			
		||||
        filename: String,
 | 
			
		||||
        inputProvider: InputProvider,
 | 
			
		||||
        resultDeserializer: DeserializationStrategy<ResultType>,
 | 
			
		||||
        mimetype: String = "*/*",
 | 
			
		||||
        additionalParametersBuilder: FormBuilder.() -> Unit = {},
 | 
			
		||||
        dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
 | 
			
		||||
        requestBuilder: HttpRequestBuilder.() -> Unit = {},
 | 
			
		||||
    ): ResultType = client.unimultipart(url, filename, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat)
 | 
			
		||||
 | 
			
		||||
    suspend fun <BodyType, ResultType> unimultipart(
 | 
			
		||||
        url: String,
 | 
			
		||||
        filename: String,
 | 
			
		||||
        inputProvider: InputProvider,
 | 
			
		||||
        otherData: BodyPair<BodyType>,
 | 
			
		||||
        resultDeserializer: DeserializationStrategy<ResultType>,
 | 
			
		||||
        mimetype: String = "*/*",
 | 
			
		||||
        additionalParametersBuilder: FormBuilder.() -> Unit = {},
 | 
			
		||||
        dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
 | 
			
		||||
        requestBuilder: HttpRequestBuilder.() -> Unit = {},
 | 
			
		||||
    ): ResultType = client.unimultipart(url, filename, otherData, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat)
 | 
			
		||||
 | 
			
		||||
    suspend fun <ResultType> unimultipart(
 | 
			
		||||
        url: String,
 | 
			
		||||
        mppFile: MPPFile,
 | 
			
		||||
        resultDeserializer: DeserializationStrategy<ResultType>,
 | 
			
		||||
        mimetype: String = "*/*",
 | 
			
		||||
        additionalParametersBuilder: FormBuilder.() -> Unit = {},
 | 
			
		||||
        dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
 | 
			
		||||
        requestBuilder: HttpRequestBuilder.() -> Unit = {}
 | 
			
		||||
    ): ResultType = client.unimultipart(
 | 
			
		||||
        url, mppFile, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    suspend fun <BodyType, ResultType> unimultipart(
 | 
			
		||||
        url: String,
 | 
			
		||||
        mppFile: MPPFile,
 | 
			
		||||
        otherData: BodyPair<BodyType>,
 | 
			
		||||
        resultDeserializer: DeserializationStrategy<ResultType>,
 | 
			
		||||
        mimetype: String = "*/*",
 | 
			
		||||
        additionalParametersBuilder: FormBuilder.() -> Unit = {},
 | 
			
		||||
        dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
 | 
			
		||||
        requestBuilder: HttpRequestBuilder.() -> Unit = {}
 | 
			
		||||
    ): ResultType = client.unimultipart(
 | 
			
		||||
        url, mppFile, otherData, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    fun <T> createStandardWebsocketFlow(
 | 
			
		||||
        url: String,
 | 
			
		||||
        checkReconnection: (Throwable?) -> Boolean = { true },
 | 
			
		||||
@@ -69,3 +121,124 @@ suspend fun <BodyType, ResultType> HttpClient.unipost(
 | 
			
		||||
}.let {
 | 
			
		||||
    serialFormat.decodeDefault(resultDeserializer, it)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun <ResultType> HttpClient.unimultipart(
 | 
			
		||||
    url: String,
 | 
			
		||||
    filename: String,
 | 
			
		||||
    inputProvider: InputProvider,
 | 
			
		||||
    resultDeserializer: DeserializationStrategy<ResultType>,
 | 
			
		||||
    mimetype: String = "*/*",
 | 
			
		||||
    additionalParametersBuilder: FormBuilder.() -> Unit = {},
 | 
			
		||||
    dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
 | 
			
		||||
    requestBuilder: HttpRequestBuilder.() -> Unit = {},
 | 
			
		||||
    serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
 | 
			
		||||
): ResultType = submitFormWithBinaryData<StandardKtorSerialInputData>(
 | 
			
		||||
    url,
 | 
			
		||||
    formData = formData {
 | 
			
		||||
        append(
 | 
			
		||||
            "bytes",
 | 
			
		||||
            inputProvider,
 | 
			
		||||
            Headers.build {
 | 
			
		||||
                append(HttpHeaders.ContentType, mimetype)
 | 
			
		||||
                append(HttpHeaders.ContentDisposition, "filename=\"$filename\"")
 | 
			
		||||
                dataHeadersBuilder()
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        additionalParametersBuilder()
 | 
			
		||||
    }
 | 
			
		||||
) {
 | 
			
		||||
    requestBuilder()
 | 
			
		||||
}.let { serialFormat.decodeDefault(resultDeserializer, it) }
 | 
			
		||||
 | 
			
		||||
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
 | 
			
		||||
    url: String,
 | 
			
		||||
    filename: String,
 | 
			
		||||
    otherData: BodyPair<BodyType>,
 | 
			
		||||
    inputProvider: InputProvider,
 | 
			
		||||
    resultDeserializer: DeserializationStrategy<ResultType>,
 | 
			
		||||
    mimetype: String = "*/*",
 | 
			
		||||
    additionalParametersBuilder: FormBuilder.() -> Unit = {},
 | 
			
		||||
    dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
 | 
			
		||||
    requestBuilder: HttpRequestBuilder.() -> Unit = {},
 | 
			
		||||
    serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
 | 
			
		||||
): ResultType = unimultipart(
 | 
			
		||||
    url,
 | 
			
		||||
    filename,
 | 
			
		||||
    inputProvider,
 | 
			
		||||
    resultDeserializer,
 | 
			
		||||
    mimetype,
 | 
			
		||||
    additionalParametersBuilder = {
 | 
			
		||||
        val serialized = serialFormat.encodeDefault(otherData.first, otherData.second)
 | 
			
		||||
        append(
 | 
			
		||||
            "data",
 | 
			
		||||
            InputProvider(serialized.size.toLong()) {
 | 
			
		||||
                ByteReadPacket(serialized)
 | 
			
		||||
            },
 | 
			
		||||
            Headers.build {
 | 
			
		||||
                append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType)
 | 
			
		||||
                append(HttpHeaders.ContentDisposition, "filename=data.bytes")
 | 
			
		||||
                dataHeadersBuilder()
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        additionalParametersBuilder()
 | 
			
		||||
    },
 | 
			
		||||
    dataHeadersBuilder,
 | 
			
		||||
    requestBuilder,
 | 
			
		||||
    serialFormat
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
suspend fun <ResultType> HttpClient.unimultipart(
 | 
			
		||||
    url: String,
 | 
			
		||||
    mppFile: MPPFile,
 | 
			
		||||
    resultDeserializer: DeserializationStrategy<ResultType>,
 | 
			
		||||
    mimetype: String = "*/*",
 | 
			
		||||
    additionalParametersBuilder: FormBuilder.() -> Unit = {},
 | 
			
		||||
    dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
 | 
			
		||||
    requestBuilder: HttpRequestBuilder.() -> Unit = {},
 | 
			
		||||
    serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
 | 
			
		||||
): ResultType = unimultipart(
 | 
			
		||||
    url,
 | 
			
		||||
    mppFile.filename.string,
 | 
			
		||||
    mppFile.inputProvider(),
 | 
			
		||||
    resultDeserializer,
 | 
			
		||||
    mimetype,
 | 
			
		||||
    additionalParametersBuilder,
 | 
			
		||||
    dataHeadersBuilder,
 | 
			
		||||
    requestBuilder,
 | 
			
		||||
    serialFormat
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
 | 
			
		||||
    url: String,
 | 
			
		||||
    mppFile: MPPFile,
 | 
			
		||||
    otherData: BodyPair<BodyType>,
 | 
			
		||||
    resultDeserializer: DeserializationStrategy<ResultType>,
 | 
			
		||||
    mimetype: String = "*/*",
 | 
			
		||||
    additionalParametersBuilder: FormBuilder.() -> Unit = {},
 | 
			
		||||
    dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
 | 
			
		||||
    requestBuilder: HttpRequestBuilder.() -> Unit = {},
 | 
			
		||||
    serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
 | 
			
		||||
): ResultType = unimultipart(
 | 
			
		||||
    url,
 | 
			
		||||
    mppFile,
 | 
			
		||||
    resultDeserializer,
 | 
			
		||||
    mimetype,
 | 
			
		||||
    additionalParametersBuilder = {
 | 
			
		||||
        val serialized = serialFormat.encodeDefault(otherData.first, otherData.second)
 | 
			
		||||
        append(
 | 
			
		||||
            "data",
 | 
			
		||||
            InputProvider(serialized.size.toLong()) {
 | 
			
		||||
                ByteReadPacket(serialized)
 | 
			
		||||
            },
 | 
			
		||||
            Headers.build {
 | 
			
		||||
                append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType)
 | 
			
		||||
                append(HttpHeaders.ContentDisposition, "filename=data.bytes")
 | 
			
		||||
                dataHeadersBuilder()
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        additionalParametersBuilder()
 | 
			
		||||
    },
 | 
			
		||||
    dataHeadersBuilder,
 | 
			
		||||
    requestBuilder,
 | 
			
		||||
    serialFormat
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,11 @@
 | 
			
		||||
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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
package dev.inmo.micro_utils.ktor.client
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.common.MPPFile
 | 
			
		||||
import io.ktor.client.request.forms.InputProvider
 | 
			
		||||
import io.ktor.utils.io.streams.asInput
 | 
			
		||||
 | 
			
		||||
actual suspend fun MPPFile.inputProvider(): InputProvider = InputProvider(length()) {
 | 
			
		||||
    inputStream().asInput()
 | 
			
		||||
}
 | 
			
		||||
@@ -10,6 +10,7 @@ kotlin {
 | 
			
		||||
    sourceSets {
 | 
			
		||||
        commonMain {
 | 
			
		||||
            dependencies {
 | 
			
		||||
                api internalProject("micro_utils.common")
 | 
			
		||||
                api "org.jetbrains.kotlinx:kotlinx-serialization-cbor:$kotlin_serialisation_core_version"
 | 
			
		||||
                api "com.soywiz.korlibs.klock:klock:$klockVersion"
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,31 @@
 | 
			
		||||
package dev.inmo.micro_utils.ktor.server
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.common.*
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.safely
 | 
			
		||||
import dev.inmo.micro_utils.ktor.common.*
 | 
			
		||||
import io.ktor.application.ApplicationCall
 | 
			
		||||
import io.ktor.application.call
 | 
			
		||||
import io.ktor.http.ContentType
 | 
			
		||||
import io.ktor.http.HttpStatusCode
 | 
			
		||||
import io.ktor.http.content.PartData
 | 
			
		||||
import io.ktor.http.content.forEachPart
 | 
			
		||||
import io.ktor.request.receive
 | 
			
		||||
import io.ktor.request.receiveMultipart
 | 
			
		||||
import io.ktor.response.respond
 | 
			
		||||
import io.ktor.response.respondBytes
 | 
			
		||||
import io.ktor.routing.Route
 | 
			
		||||
import io.ktor.util.asStream
 | 
			
		||||
import io.ktor.util.cio.writeChannel
 | 
			
		||||
import io.ktor.util.pipeline.PipelineContext
 | 
			
		||||
import io.ktor.utils.io.core.*
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
import kotlinx.serialization.*
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.io.File.createTempFile
 | 
			
		||||
 | 
			
		||||
class UnifiedRouter(
 | 
			
		||||
    private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
 | 
			
		||||
    private val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
 | 
			
		||||
    val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
 | 
			
		||||
    val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
 | 
			
		||||
) {
 | 
			
		||||
    fun <T> Route.includeWebsocketHandling(
 | 
			
		||||
        suburl: String,
 | 
			
		||||
@@ -104,6 +113,139 @@ suspend fun <T> ApplicationCall.uniload(
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun ApplicationCall.uniloadMultipart(
 | 
			
		||||
    onFormItem: (PartData.FormItem) -> Unit = {},
 | 
			
		||||
    onCustomFileItem: (PartData.FileItem) -> Unit = {},
 | 
			
		||||
    onBinaryContent: (PartData.BinaryItem) -> Unit = {}
 | 
			
		||||
) = safely {
 | 
			
		||||
    val multipartData = receiveMultipart()
 | 
			
		||||
 | 
			
		||||
    var resultInput: Input? = null
 | 
			
		||||
 | 
			
		||||
    multipartData.forEachPart {
 | 
			
		||||
        when (it) {
 | 
			
		||||
            is PartData.FormItem -> onFormItem(it)
 | 
			
		||||
            is PartData.FileItem -> {
 | 
			
		||||
                when (it.name) {
 | 
			
		||||
                    "bytes" -> resultInput = it.provider()
 | 
			
		||||
                    else -> onCustomFileItem(it)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            is PartData.BinaryItem -> onBinaryContent(it)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    resultInput ?: error("Bytes has not been received")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun <T> ApplicationCall.uniloadMultipart(
 | 
			
		||||
    deserializer: DeserializationStrategy<T>,
 | 
			
		||||
    onFormItem: (PartData.FormItem) -> Unit = {},
 | 
			
		||||
    onCustomFileItem: (PartData.FileItem) -> Unit = {},
 | 
			
		||||
    onBinaryContent: (PartData.BinaryItem) -> Unit = {}
 | 
			
		||||
): Pair<Input, T> {
 | 
			
		||||
    var data: Optional<T>? = null
 | 
			
		||||
    val resultInput = uniloadMultipart(
 | 
			
		||||
        onFormItem,
 | 
			
		||||
        {
 | 
			
		||||
            if (it.name == "data") {
 | 
			
		||||
                data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional
 | 
			
		||||
            } else {
 | 
			
		||||
                onCustomFileItem(it)
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        onBinaryContent
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    val completeData = data ?: error("Data has not been received")
 | 
			
		||||
    return resultInput to (completeData.dataOrNull().let { it as T })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun <T> ApplicationCall.uniloadMultipartFile(
 | 
			
		||||
    deserializer: DeserializationStrategy<T>,
 | 
			
		||||
    onFormItem: (PartData.FormItem) -> Unit = {},
 | 
			
		||||
    onCustomFileItem: (PartData.FileItem) -> Unit = {},
 | 
			
		||||
    onBinaryContent: (PartData.BinaryItem) -> Unit = {},
 | 
			
		||||
) = safely {
 | 
			
		||||
    val multipartData = receiveMultipart()
 | 
			
		||||
 | 
			
		||||
    var resultInput: MPPFile? = null
 | 
			
		||||
    var data: Optional<T>? = null
 | 
			
		||||
 | 
			
		||||
    multipartData.forEachPart {
 | 
			
		||||
        when (it) {
 | 
			
		||||
            is PartData.FormItem -> onFormItem(it)
 | 
			
		||||
            is PartData.FileItem -> {
 | 
			
		||||
                when (it.name) {
 | 
			
		||||
                    "bytes" -> {
 | 
			
		||||
                        val name = FileName(it.originalFileName ?: error("File name is unknown for default part"))
 | 
			
		||||
                        resultInput = MPPFile.createTempFile(
 | 
			
		||||
                            name.nameWithoutExtension.let {
 | 
			
		||||
                                var resultName = it
 | 
			
		||||
                                while (resultName.length < 3) {
 | 
			
		||||
                                    resultName += "_"
 | 
			
		||||
                                }
 | 
			
		||||
                                resultName
 | 
			
		||||
                            },
 | 
			
		||||
                            ".${name.extension}"
 | 
			
		||||
                        ).apply {
 | 
			
		||||
                            outputStream().use { fileStream ->
 | 
			
		||||
                                it.provider().asStream().copyTo(fileStream)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    "data" -> data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional
 | 
			
		||||
                    else -> onCustomFileItem(it)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            is PartData.BinaryItem -> onBinaryContent(it)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val completeData = data ?: error("Data has not been received")
 | 
			
		||||
    (resultInput ?: error("Bytes has not been received")) to (completeData.dataOrNull().let { it as T })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun ApplicationCall.uniloadMultipartFile(
 | 
			
		||||
    onFormItem: (PartData.FormItem) -> Unit = {},
 | 
			
		||||
    onCustomFileItem: (PartData.FileItem) -> Unit = {},
 | 
			
		||||
    onBinaryContent: (PartData.BinaryItem) -> Unit = {},
 | 
			
		||||
) = safely {
 | 
			
		||||
    val multipartData = receiveMultipart()
 | 
			
		||||
 | 
			
		||||
    var resultInput: MPPFile? = null
 | 
			
		||||
 | 
			
		||||
    multipartData.forEachPart {
 | 
			
		||||
        when (it) {
 | 
			
		||||
            is PartData.FormItem -> onFormItem(it)
 | 
			
		||||
            is PartData.FileItem -> {
 | 
			
		||||
                if (it.name == "bytes") {
 | 
			
		||||
                    val name = FileName(it.originalFileName ?: error("File name is unknown for default part"))
 | 
			
		||||
                    resultInput = MPPFile.createTempFile(
 | 
			
		||||
                        name.nameWithoutExtension.let {
 | 
			
		||||
                            var resultName = it
 | 
			
		||||
                            while (resultName.length < 3) {
 | 
			
		||||
                                resultName += "_"
 | 
			
		||||
                            }
 | 
			
		||||
                            resultName
 | 
			
		||||
                        },
 | 
			
		||||
                        ".${name.extension}"
 | 
			
		||||
                    ).apply {
 | 
			
		||||
                        outputStream().use { fileStream ->
 | 
			
		||||
                            it.provider().asStream().copyTo(fileStream)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    onCustomFileItem(it)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            is PartData.BinaryItem -> onBinaryContent(it)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    resultInput ?: error("Bytes has not been received")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun ApplicationCall.getParameterOrSendError(
 | 
			
		||||
    field: String
 | 
			
		||||
) = parameters[field].also {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
apply plugin: 'maven-publish'
 | 
			
		||||
apply plugin: 'signing'
 | 
			
		||||
 | 
			
		||||
task javadocsJar(type: Jar) {
 | 
			
		||||
    classifier = 'javadoc'
 | 
			
		||||
@@ -70,7 +69,18 @@ publishing {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
    
 | 
			
		||||
signing {
 | 
			
		||||
if (project.hasProperty("signing.gnupg.keyName")) {
 | 
			
		||||
    apply plugin: 'signing'
 | 
			
		||||
    
 | 
			
		||||
    signing {
 | 
			
		||||
        useGpgCmd()
 | 
			
		||||
    
 | 
			
		||||
        sign publishing.publications
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    task signAll {
 | 
			
		||||
        tasks.withType(Sign).forEach {
 | 
			
		||||
            dependsOn(it)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +1 @@
 | 
			
		||||
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","includeGpgSigning":true,"developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}]}}
 | 
			
		||||
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}}}
 | 
			
		||||
@@ -12,8 +12,8 @@ open class ExposedReadKeyValueRepo<Key, Value>(
 | 
			
		||||
    valueColumnAllocator: ColumnAllocator<Value>,
 | 
			
		||||
    tableName: String? = null
 | 
			
		||||
) : ReadStandardKeyValueRepo<Key, Value>, ExposedRepo, Table(tableName ?: "") {
 | 
			
		||||
    protected val keyColumn: Column<Key> = keyColumnAllocator()
 | 
			
		||||
    protected val valueColumn: Column<Value> = valueColumnAllocator()
 | 
			
		||||
    val keyColumn: Column<Key> = keyColumnAllocator()
 | 
			
		||||
    val valueColumn: Column<Value> = valueColumnAllocator()
 | 
			
		||||
    override val primaryKey: PrimaryKey = PrimaryKey(keyColumn, valueColumn)
 | 
			
		||||
 | 
			
		||||
    init { initTable() }
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.*
 | 
			
		||||
import org.jetbrains.exposed.sql.*
 | 
			
		||||
import org.jetbrains.exposed.sql.transactions.transaction
 | 
			
		||||
 | 
			
		||||
typealias ExposedKeyValuesRepo<Key, Value> = ExposedOneToManyKeyValueRepo<Key, Value>
 | 
			
		||||
open class ExposedOneToManyKeyValueRepo<Key, Value>(
 | 
			
		||||
    database: Database,
 | 
			
		||||
    keyColumnAllocator: ColumnAllocator<Key>,
 | 
			
		||||
 
 | 
			
		||||
@@ -3,17 +3,20 @@ package dev.inmo.micro_utils.repos.exposed.onetomany
 | 
			
		||||
import dev.inmo.micro_utils.pagination.*
 | 
			
		||||
import dev.inmo.micro_utils.repos.ReadOneToManyKeyValueRepo
 | 
			
		||||
import dev.inmo.micro_utils.repos.exposed.*
 | 
			
		||||
import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedReadKeyValueRepo
 | 
			
		||||
import org.jetbrains.exposed.sql.*
 | 
			
		||||
import org.jetbrains.exposed.sql.transactions.transaction
 | 
			
		||||
 | 
			
		||||
typealias ExposedReadKeyValuesRepo<Key, Value> = ExposedReadOneToManyKeyValueRepo<Key, Value>
 | 
			
		||||
 | 
			
		||||
open class ExposedReadOneToManyKeyValueRepo<Key, Value>(
 | 
			
		||||
    override val database: Database,
 | 
			
		||||
    keyColumnAllocator: ColumnAllocator<Key>,
 | 
			
		||||
    valueColumnAllocator: ColumnAllocator<Value>,
 | 
			
		||||
    tableName: String? = null
 | 
			
		||||
) : ReadOneToManyKeyValueRepo<Key, Value>, ExposedRepo, Table(tableName ?: "") {
 | 
			
		||||
    protected val keyColumn: Column<Key> = keyColumnAllocator()
 | 
			
		||||
    protected val valueColumn: Column<Value> = valueColumnAllocator()
 | 
			
		||||
    val keyColumn: Column<Key> = keyColumnAllocator()
 | 
			
		||||
    val valueColumn: Column<Value> = valueColumnAllocator()
 | 
			
		||||
 | 
			
		||||
    init { initTable() }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,8 @@ inline fun versionsRepo(database: Database): VersionsRepo<Database> = StandardVe
 | 
			
		||||
class ExposedStandardVersionsRepoProxy(
 | 
			
		||||
    override val database: Database
 | 
			
		||||
) : StandardVersionsRepoProxy<Database>, Table("ExposedVersionsProxy"), ExposedRepo {
 | 
			
		||||
    private val tableNameColumn = text("tableName")
 | 
			
		||||
    private val tableVersionColumn = integer("tableName")
 | 
			
		||||
    val tableNameColumn = text("tableName")
 | 
			
		||||
    val tableVersionColumn = integer("tableName")
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        initTable()
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,7 @@ open class TypedSerializer<T : Any>(
 | 
			
		||||
    presetSerializers: Map<String, KSerializer<out T>> = emptyMap(),
 | 
			
		||||
) : KSerializer<T> {
 | 
			
		||||
    protected val serializers = presetSerializers.toMutableMap()
 | 
			
		||||
    @ExperimentalSerializationApi
 | 
			
		||||
    @InternalSerializationApi
 | 
			
		||||
    @OptIn(InternalSerializationApi::class)
 | 
			
		||||
    override val descriptor: SerialDescriptor = buildSerialDescriptor(
 | 
			
		||||
        "TypedSerializer",
 | 
			
		||||
        SerialKind.CONTEXTUAL
 | 
			
		||||
@@ -21,8 +20,7 @@ open class TypedSerializer<T : Any>(
 | 
			
		||||
        element("value", ContextualSerializer(kClass).descriptor)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @ExperimentalSerializationApi
 | 
			
		||||
    @InternalSerializationApi
 | 
			
		||||
    @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
 | 
			
		||||
    override fun deserialize(decoder: Decoder): T {
 | 
			
		||||
        return decoder.decodeStructure(descriptor) {
 | 
			
		||||
            var type: String? = null
 | 
			
		||||
@@ -46,14 +44,12 @@ open class TypedSerializer<T : Any>(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @ExperimentalSerializationApi
 | 
			
		||||
    @InternalSerializationApi
 | 
			
		||||
    @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
 | 
			
		||||
    protected open fun <O: T> CompositeEncoder.encode(value: O) {
 | 
			
		||||
        encodeSerializableElement(descriptor, 1, value::class.serializer() as KSerializer<O>, value)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @ExperimentalSerializationApi
 | 
			
		||||
    @InternalSerializationApi
 | 
			
		||||
    @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
 | 
			
		||||
    override fun serialize(encoder: Encoder, value: T) {
 | 
			
		||||
        encoder.encodeStructure(descriptor) {
 | 
			
		||||
            val valueSerializer = value::class.serializer()
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user