mirror of
				https://github.com/InsanusMokrassar/MicroUtils.git
				synced 2025-10-31 12:10:29 +00:00 
			
		
		
		
	Compare commits
	
		
			104 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 3a5771a0cc | |||
| 527a2a91ac | |||
| 6763e5c4c6 | |||
| 06918d8310 | |||
| 89ccaa1b57 | |||
| 5d0bdb9bcf | |||
| 31fdcf74a5 | |||
| afca09cc1d | |||
| 531d89d9db | |||
| 6bbbea0bc3 | |||
| e337cd98c8 | |||
| bcbab3b380 | |||
| fb63de7568 | |||
| aa45a4ab13 | |||
| 2af7e2f681 | |||
| 34fd9edce0 | |||
| 2a4cb8c5f9 | |||
| 50ea40bc3a | |||
| a77654052d | |||
| 88aafce552 | |||
| 4e95d6bfff | |||
| 38d0e34fb5 | |||
| 8fbc6b9041 | |||
| e8219d6cf4 | |||
| 6c20fc4ca6 | |||
| 85cd975492 | |||
| 1171a717fe | |||
| bbe5320312 | |||
| 00acb9fddd | |||
| de3d14dc41 | |||
| 67ff9cc9b3 | |||
| af132103a0 | |||
| 3b1124a804 | |||
| f226c2dfd6 | |||
| 69d6e63846 | |||
| 02c3d397ad | |||
| 67a1050646 | |||
| 8cd0775a6c | |||
| 162294d6c6 | |||
| c4dd19dd00 | |||
| d2314422f1 | |||
| 6fedd6f859 | |||
| e52b59665f | |||
| cda9d09689 | |||
| c9237b3f00 | |||
| 18bba66c4a | |||
| 63418c4a8a | |||
| 2e66c6f4e3 | |||
| e9c5df4c13 | |||
| bc7789ad2c | |||
| e3da761249 | |||
| 4082f65afa | |||
| 5d1cab075d | |||
| bcf67f7e59 | |||
| 7d3b1f8e75 | |||
| 119a0588cc | |||
| fab789d9c0 | |||
| ceba81c08f | |||
| a061af0558 | |||
| c7a53846ad | |||
| a683cccf0c | |||
| 50d41e35c1 | |||
| aa0e831cea | |||
| 44e26ccb4f | |||
| 2a783f6e2b | |||
| 6058d6a724 | |||
| 2e9c7eb5fa | |||
| e75465ad10 | |||
| de01ad54e9 | |||
| eeea7ddbe3 | |||
| e0b18bec05 | |||
| 410e89bba9 | |||
| 9ef19dc42b | |||
| 0337d1b82d | |||
| f5bd4c5ccb | |||
| 630f9bc0d4 | |||
| 18b4ffece1 | |||
| f64e1effa3 | |||
| 847fcbb488 | 
							
								
								
									
										12
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,12 +0,0 @@ | |||||||
| name: Regular build |  | ||||||
| on: [push] |  | ||||||
| jobs: |  | ||||||
|   build: |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v2 |  | ||||||
|       - uses: actions/setup-java@v1 |  | ||||||
|         with: |  | ||||||
|           java-version: 1.8 |  | ||||||
|       - name: Build |  | ||||||
|         run: ./gradlew build |  | ||||||
							
								
								
									
										3
									
								
								.github/workflows/dokka_push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/dokka_push.yml
									
									
									
									
										vendored
									
									
								
							| @@ -11,6 +11,9 @@ jobs: | |||||||
|       - uses: actions/setup-java@v1 |       - uses: actions/setup-java@v1 | ||||||
|         with: |         with: | ||||||
|           java-version: 1.8 |           java-version: 1.8 | ||||||
|  |       - name: Fix android 32.0.0 dx | ||||||
|  |         continue-on-error: true | ||||||
|  |         run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib  && mv d8.jar dx.jar | ||||||
|       - name: Build |       - name: Build | ||||||
|         run: ./gradlew dokkaHtml |         run: ./gradlew dokkaHtml | ||||||
|       - name: Publish KDocs |       - name: Publish KDocs | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								.github/workflows/packages_push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/packages_push.yml
									
									
									
									
										vendored
									
									
								
							| @@ -9,6 +9,9 @@ jobs: | |||||||
|       - uses: actions/setup-java@v1 |       - uses: actions/setup-java@v1 | ||||||
|         with: |         with: | ||||||
|           java-version: 1.8 |           java-version: 1.8 | ||||||
|  |       - name: Fix android 32.0.0 dx | ||||||
|  |         continue-on-error: true | ||||||
|  |         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 |       - name: Rewrite version | ||||||
|         run: | |         run: | | ||||||
|           branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" |           branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" | ||||||
| @@ -18,6 +21,7 @@ jobs: | |||||||
|       - name: Build |       - name: Build | ||||||
|         run: ./gradlew build |         run: ./gradlew build | ||||||
|       - name: Publish |       - name: Publish | ||||||
|  |         continue-on-error: true | ||||||
|         run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidDebugPublication -x signAndroidReleasePublication -x signKotlinMultiplatformPublication |         run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidDebugPublication -x signAndroidReleasePublication -x signKotlinMultiplatformPublication | ||||||
|         env: |         env: | ||||||
|           GITHUBPACKAGES_USER: ${{ github.actor }} |           GITHUBPACKAGES_USER: ${{ github.actor }} | ||||||
|   | |||||||
							
								
								
									
										161
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										161
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,5 +1,166 @@ | |||||||
| # Changelog | # Changelog | ||||||
|  |  | ||||||
|  | ## 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`: | ||||||
|  |     * `repeatOnFailure` | ||||||
|  |  | ||||||
|  | ## 0.8.4 | ||||||
|  |  | ||||||
|  | * `Ktor`: | ||||||
|  |     * `Server`: | ||||||
|  |         * Several new `createKtorServer` | ||||||
|  |  | ||||||
|  | ## 0.8.3 | ||||||
|  |  | ||||||
|  | * `Common`: | ||||||
|  |     * Ranges intersection functionality | ||||||
|  |     * New type `Optional` | ||||||
|  | * `Pagination`: | ||||||
|  |     * `Pagination` now extends `ClosedRange<Int>` | ||||||
|  |     * `Pagination` intersection functionality | ||||||
|  |  | ||||||
|  | ## 0.8.2 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Klock`: `2.4.7` -> `2.4.8` | ||||||
|  |     * `Serialization`: `1.3.0` -> `1.3.1` | ||||||
|  | * `FSM`: | ||||||
|  |     * Now it is possible to pass any `CheckableHandlerHolder` in `FSMBuilder` | ||||||
|  |     * Now `StatesMachine` works with `CheckableHandlerHolder` instead of `CustomizableHandlerHolder` | ||||||
|  |  | ||||||
|  | ## 0.8.1 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Exposed`: `0.36.1` -> `0.36.2` | ||||||
|  |     * `Core KTX`: `1.6.0` -> `1.7.0` | ||||||
|  |  | ||||||
|  | ## 0.8.0 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Klock`: `2.4.6` -> `2.4.7` | ||||||
|  |     * `Ktor`: `1.6.4` -> `1.6.5` | ||||||
|  |     * `Exposed`: `0.35.3` -> `0.36.1` | ||||||
|  | * `Common`: | ||||||
|  |     * Type `Either` got its own serializer | ||||||
|  | * `FSM`: | ||||||
|  |     * `Common`: | ||||||
|  |         * Full rework of FSM: | ||||||
|  |             * Now it is more flexible for checking of handler opportunity to handle state | ||||||
|  |             * Now machine and states managers are type-oriented | ||||||
|  |             * `StateHandlerHolder` has been renamed to `CheckableHandlerHolder` | ||||||
|  |         * Add opportunity for comfortable adding default state handler | ||||||
|  |  | ||||||
|  | ## 0.7.4 | ||||||
|  |  | ||||||
|  | * `Common`: | ||||||
|  |     * New type `Either` | ||||||
|  | * `Serialization`: | ||||||
|  |     * `TypedSerializer` | ||||||
|  |         * New factory fun which accept vararg pairs of type and its serializer | ||||||
|  | * `Repos`: | ||||||
|  |     * `Common` (`Android`): | ||||||
|  |         * `AbstractMutableAndroidCRUDRepo` flows now will have extra buffer capacity instead of reply. It means that | ||||||
|  |           android crud repo _WILL NOT_ send previous events to the  | ||||||
|  |     * `Exposed`: | ||||||
|  |         * New parameter `AbstractExposedWriteCRUDRepo#replyCacheInFlows` | ||||||
|  |         * KeyValue realization `ExposedKeyValueRepo` properties `_onNewValue` and `_onValueRemoved` now are available in | ||||||
|  |           inheritors | ||||||
|  | * `Pagination`: | ||||||
|  |     * `Common`: | ||||||
|  |         * New types `getAllBy*` for current, next and custom paging | ||||||
|  |  | ||||||
|  | ## 0.7.3 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Exposed`: `0.35.2` -> `0.35.3` | ||||||
|  |  | ||||||
|  | ## 0.7.2 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Klock`: `2.4.5` -> `2.4.6` | ||||||
|  |  | ||||||
|  | ## 0.7.1 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Klock`: `2.4.3` -> `2.4.5` | ||||||
|  |     * `Exposed`: `0.35.1` -> `0.35.2` | ||||||
|  | * `Coroutines`: | ||||||
|  |     * `Common`: | ||||||
|  |         * New `Flow` - `AccumulatorFlow` | ||||||
|  | * `FSM`: | ||||||
|  |     * `Common`: | ||||||
|  |         * `InMemoryStatesManager` has been replaced | ||||||
|  |         * `StatesMachine` became an interface | ||||||
|  |         * New manager `DefaultStatesManager` with `DefaultStatesManagerRepo` for abstraction of manager and storing of | ||||||
|  |           data info | ||||||
|  |  | ||||||
|  | ## 0.7.0 | ||||||
|  |  | ||||||
|  | **THIS VERSION HAS MIGRATED FROM KOTLINX DATETIME TO KORLIBS KLOCK. CAREFUL** | ||||||
|  |  | ||||||
|  | * `Versions` | ||||||
|  |     * `kotlinx.datetime` -> `Klock` | ||||||
|  |  | ||||||
|  | ## 0.6.0 DO NOT RECOMMENDED | ||||||
|  |  | ||||||
|  | **THIS VERSION HAS MIGRATED FROM KORLIBS KLOCK TO KOTLINX DATETIME. CAREFUL** | ||||||
|  | **ALL DEPRECATION HAVE BEEN REMOVED** | ||||||
|  |  | ||||||
|  | * `Versions` | ||||||
|  |     * `Klock` -> `kotlinx.datetime` | ||||||
|  |  | ||||||
|  | ## 0.5.31 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Klock`: `2.4.2` -> `2.4.3` | ||||||
|  |     * `Ktor`: `1.6.3` -> `1.6.4` | ||||||
|  |  | ||||||
|  | ## 0.5.30 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Serialization`: `1.2.2` -> `1.3.0` | ||||||
|  |  | ||||||
|  | ## 0.5.29 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Exposed`: `0.34.2` -> `0.35.1` | ||||||
|  |  | ||||||
| ## 0.5.28 | ## 0.5.28 | ||||||
|  |  | ||||||
| * `Versions`: | * `Versions`: | ||||||
|   | |||||||
| @@ -1,5 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.common |  | ||||||
|  |  | ||||||
| @Deprecated("Redundant", ReplaceWith("coerceIn(min, max)")) |  | ||||||
| @Suppress("NOTHING_TO_INLINE") |  | ||||||
| inline fun <T : Comparable<T>> T.clamp(min: T, max: T): T = coerceIn(min, max) |  | ||||||
| @@ -2,8 +2,6 @@ | |||||||
|  |  | ||||||
| package dev.inmo.micro_utils.common | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
| import kotlin.jvm.JvmInline |  | ||||||
|  |  | ||||||
| private inline fun <T> getObject( | private inline fun <T> getObject( | ||||||
|     additional: MutableList<T>, |     additional: MutableList<T>, | ||||||
|     iterator: Iterator<T> |     iterator: Iterator<T> | ||||||
|   | |||||||
| @@ -0,0 +1,151 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.serialization.* | ||||||
|  | import kotlinx.serialization.builtins.serializer | ||||||
|  | import kotlinx.serialization.descriptors.* | ||||||
|  | import kotlinx.serialization.encoding.* | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Realization of this interface will contains at least one not null - [t1] or [t2] | ||||||
|  |  * | ||||||
|  |  * @see EitherFirst | ||||||
|  |  * @see EitherSecond | ||||||
|  |  * @see Either.Companion.first | ||||||
|  |  * @see Either.Companion.second | ||||||
|  |  * @see Either.onFirst | ||||||
|  |  * @see Either.onSecond | ||||||
|  |  */ | ||||||
|  | @Serializable(EitherSerializer::class) | ||||||
|  | sealed interface Either<T1, T2> { | ||||||
|  |     val t1: T1? | ||||||
|  |     val t2: T2? | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         fun <T1, T2> serializer( | ||||||
|  |             t1Serializer: KSerializer<T1>, | ||||||
|  |             t2Serializer: KSerializer<T2>, | ||||||
|  |         ): KSerializer<Either<T1, T2>> = EitherSerializer(t1Serializer, t2Serializer) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class EitherSerializer<T1, T2>( | ||||||
|  |     t1Serializer: KSerializer<T1>, | ||||||
|  |     t2Serializer: KSerializer<T2>, | ||||||
|  | ) : KSerializer<Either<T1, T2>> { | ||||||
|  |     @ExperimentalSerializationApi | ||||||
|  |     @InternalSerializationApi | ||||||
|  |     override val descriptor: SerialDescriptor = buildSerialDescriptor( | ||||||
|  |         "TypedSerializer", | ||||||
|  |         SerialKind.CONTEXTUAL | ||||||
|  |     ) { | ||||||
|  |         element("type", String.serializer().descriptor) | ||||||
|  |         element("value", ContextualSerializer(Either::class).descriptor) | ||||||
|  |     } | ||||||
|  |     private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer) | ||||||
|  |     private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer) | ||||||
|  |  | ||||||
|  |     @ExperimentalSerializationApi | ||||||
|  |     @InternalSerializationApi | ||||||
|  |     override fun deserialize(decoder: Decoder): Either<T1, T2> { | ||||||
|  |         return decoder.decodeStructure(descriptor) { | ||||||
|  |             var type: String? = null | ||||||
|  |             lateinit var result: Either<T1, T2> | ||||||
|  |             while (true) { | ||||||
|  |                 when (val index = decodeElementIndex(descriptor)) { | ||||||
|  |                     0 -> type = decodeStringElement(descriptor, 0) | ||||||
|  |                     1 -> { | ||||||
|  |                         result = when (type) { | ||||||
|  |                             "t1" -> decodeSerializableElement( | ||||||
|  |                                 descriptor, | ||||||
|  |                                 1, | ||||||
|  |                                 t1EitherSerializer | ||||||
|  |                             ) | ||||||
|  |                             "t2" -> decodeSerializableElement( | ||||||
|  |                                 descriptor, | ||||||
|  |                                 1, | ||||||
|  |                                 t2EitherSerializer | ||||||
|  |                             ) | ||||||
|  |                             else -> error("Unknown type of either: $type") | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     CompositeDecoder.DECODE_DONE -> break | ||||||
|  |                     else -> error("Unexpected index: $index") | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             result | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     @ExperimentalSerializationApi | ||||||
|  |     @InternalSerializationApi | ||||||
|  |     override fun serialize(encoder: Encoder, value: Either<T1, T2>) { | ||||||
|  |         encoder.encodeStructure(descriptor) { | ||||||
|  |             when (value) { | ||||||
|  |                 is EitherFirst -> { | ||||||
|  |                     encodeStringElement(descriptor, 0, "t1") | ||||||
|  |                     encodeSerializableElement(descriptor, 1, t1EitherSerializer, value) | ||||||
|  |                 } | ||||||
|  |                 is EitherSecond -> { | ||||||
|  |                     encodeStringElement(descriptor, 0, "t2") | ||||||
|  |                     encodeSerializableElement(descriptor, 1, t2EitherSerializer, value) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This type [Either] will always have not nullable [t1] | ||||||
|  |  */ | ||||||
|  | @Serializable | ||||||
|  | data class EitherFirst<T1, T2>( | ||||||
|  |     override val t1: T1 | ||||||
|  | ) : Either<T1, T2> { | ||||||
|  |     override val t2: T2? | ||||||
|  |         get() = null | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This type [Either] will always have not nullable [t2] | ||||||
|  |  */ | ||||||
|  | @Serializable | ||||||
|  | data class EitherSecond<T1, T2>( | ||||||
|  |     override val t2: T2 | ||||||
|  | ) : Either<T1, T2> { | ||||||
|  |     override val t1: T1? | ||||||
|  |         get() = null | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @return New instance of [EitherFirst] | ||||||
|  |  */ | ||||||
|  | inline fun <T1, T2> Either.Companion.first(t1: T1): Either<T1, T2> = EitherFirst(t1) | ||||||
|  | /** | ||||||
|  |  * @return New instance of [EitherSecond] | ||||||
|  |  */ | ||||||
|  | inline fun <T1, T2> Either.Companion.second(t2: T2): Either<T1, T2> = EitherSecond(t2) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [block] in case when [Either.t1] of [this] is not null | ||||||
|  |  */ | ||||||
|  | inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(block: (T1) -> Unit): E { | ||||||
|  |     val t1 = t1 | ||||||
|  |     t1 ?.let(block) | ||||||
|  |     return this | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [block] in case when [Either.t2] of [this] is not null | ||||||
|  |  */ | ||||||
|  | inline fun <T1, T2, E : Either<T1, T2>> E.onSecond(block: (T2) -> Unit): E { | ||||||
|  |     val t2 = t2 | ||||||
|  |     t2 ?.let(block) | ||||||
|  |     return this | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <reified T1, reified T2> Any.either() = when (this) { | ||||||
|  |     is T1 -> Either.first<T1, T2>(this) | ||||||
|  |     is T2 -> Either.second<T1, T2>(this) | ||||||
|  |     else -> error("Incorrect type of either argument $this") | ||||||
|  | } | ||||||
| @@ -1,10 +1,10 @@ | |||||||
| package dev.inmo.micro_utils.common | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
| inline fun <I, R> Iterable<I>.joinTo( | inline fun <I, R> Iterable<I>.joinTo( | ||||||
|     crossinline separatorFun: (I) -> R?, |     separatorFun: (I) -> R?, | ||||||
|     prefix: R? = null, |     prefix: R? = null, | ||||||
|     postfix: R? = null, |     postfix: R? = null, | ||||||
|     crossinline transform: (I) -> R? |     transform: (I) -> R? | ||||||
| ): List<R> { | ): List<R> { | ||||||
|     val result = mutableListOf<R>() |     val result = mutableListOf<R>() | ||||||
|     val iterator = iterator() |     val iterator = iterator() | ||||||
| @@ -29,11 +29,11 @@ inline fun <I, R> Iterable<I>.joinTo( | |||||||
|     separator: R? = null, |     separator: R? = null, | ||||||
|     prefix: R? = null, |     prefix: R? = null, | ||||||
|     postfix: R? = null, |     postfix: R? = null, | ||||||
|     crossinline transform: (I) -> R? |     transform: (I) -> R? | ||||||
| ): List<R> = joinTo({ separator }, prefix, postfix, transform) | ): List<R> = joinTo({ separator }, prefix, postfix, transform) | ||||||
|  |  | ||||||
| inline fun <I> Iterable<I>.joinTo( | inline fun <I> Iterable<I>.joinTo( | ||||||
|     crossinline separatorFun: (I) -> I?, |     separatorFun: (I) -> I?, | ||||||
|     prefix: I? = null, |     prefix: I? = null, | ||||||
|     postfix: I? = null |     postfix: I? = null | ||||||
| ): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it } | ): 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) | ): List<I> = joinTo<I>({ separator }, prefix, postfix) | ||||||
|  |  | ||||||
| inline fun <I, reified R> Array<I>.joinTo( | inline fun <I, reified R> Array<I>.joinTo( | ||||||
|     crossinline separatorFun: (I) -> R?, |     separatorFun: (I) -> R?, | ||||||
|     prefix: R? = null, |     prefix: R? = null, | ||||||
|     postfix: R? = null, |     postfix: R? = null, | ||||||
|     crossinline transform: (I) -> R? |     transform: (I) -> R? | ||||||
| ): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray() | ): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray() | ||||||
|  |  | ||||||
| inline fun <I, reified R> Array<I>.joinTo( | inline fun <I, reified R> Array<I>.joinTo( | ||||||
|     separator: R? = null, |     separator: R? = null, | ||||||
|     prefix: R? = null, |     prefix: R? = null, | ||||||
|     postfix: R? = null, |     postfix: R? = null, | ||||||
|     crossinline transform: (I) -> R? |     transform: (I) -> R? | ||||||
| ): Array<R> = asIterable().joinTo(separator, prefix, postfix, transform).toTypedArray() | ): Array<R> = asIterable().joinTo(separator, prefix, postfix, transform).toTypedArray() | ||||||
|   | |||||||
| @@ -23,11 +23,12 @@ value class FileName(val string: String) { | |||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @PreviewFeature |  | ||||||
| expect class MPPFile | expect class MPPFile | ||||||
|  |  | ||||||
| expect val MPPFile.filename: FileName | expect val MPPFile.filename: FileName | ||||||
| expect val MPPFile.filesize: Long | expect val MPPFile.filesize: Long | ||||||
|  | expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator | ||||||
| expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator | expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator | ||||||
|  | fun MPPFile.bytesSync() = bytesAllocatorSync() | ||||||
| suspend fun MPPFile.bytes() = bytesAllocator() | suspend fun MPPFile.bytes() = bytesAllocator() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -0,0 +1,92 @@ | |||||||
|  | @file:Suppress("unused") | ||||||
|  |  | ||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This type represents [T] as not only potentially nullable data, but also as a data which can not be presented. This | ||||||
|  |  * type will be useful in cases when [T] is nullable and null as valuable data too in time of data absence should be | ||||||
|  |  * presented by some third type. | ||||||
|  |  * | ||||||
|  |  * Let's imagine, you have nullable name in some database. In case when name is not nullable everything is clear - null | ||||||
|  |  * will represent absence of row in the database. In case when name is nullable null will be a little bit dual-meaning, | ||||||
|  |  * cause this null will say nothing about availability of the row (of course, it is exaggerated example) | ||||||
|  |  * | ||||||
|  |  * @see Optional.presented | ||||||
|  |  * @see Optional.absent | ||||||
|  |  * @see Optional.optional | ||||||
|  |  * @see Optional.onPresented | ||||||
|  |  * @see Optional.onAbsent | ||||||
|  |  */ | ||||||
|  | @Serializable | ||||||
|  | data class Optional<T> internal constructor( | ||||||
|  |     @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 { | ||||||
|  |         /** | ||||||
|  |          * Will create [Optional] with presented data | ||||||
|  |          */ | ||||||
|  |         fun <T> presented(data: T) = Optional(data, true) | ||||||
|  |         /** | ||||||
|  |          * Will create [Optional] without data | ||||||
|  |          */ | ||||||
|  |         fun <T> absent() = Optional<T>(null, false) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline val <T> T.optional | ||||||
|  |     get() = Optional.presented(this) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||||
|  |  */ | ||||||
|  | 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) | ||||||
|  |  */ | ||||||
|  | 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 | ||||||
|  |  */ | ||||||
|  | fun <T> Optional<T>.dataOrNull() = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or throw [throwable] otherwise | ||||||
|  |  */ | ||||||
|  | fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it | ||||||
|  |  */ | ||||||
|  | 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() | ||||||
| @@ -0,0 +1,19 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | fun <T : Comparable<T>> ClosedRange<T>.intersect(other: ClosedRange<T>): Pair<T, T>? = when { | ||||||
|  |     start == other.start && endInclusive == other.endInclusive -> start to endInclusive | ||||||
|  |     start > other.endInclusive || other.start > endInclusive -> null | ||||||
|  |     else -> maxOf(start, other.start) to minOf(endInclusive, other.endInclusive) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun IntRange.intersect( | ||||||
|  |     other: IntRange | ||||||
|  | ): IntRange? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let { | ||||||
|  |     it.first .. it.second | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun LongRange.intersect( | ||||||
|  |     other: LongRange | ||||||
|  | ): LongRange? = (this as ClosedRange<Long>).intersect(other as ClosedRange<Long>) ?.let { | ||||||
|  |     it.first .. it.second | ||||||
|  | } | ||||||
| @@ -0,0 +1,21 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Executes the given [action] until getting of successful result specified number of [times]. | ||||||
|  |  * | ||||||
|  |  * A zero-based index of current iteration is passed as a parameter to [action]. | ||||||
|  |  */ | ||||||
|  | inline fun <R> repeatOnFailure( | ||||||
|  |     times: Int, | ||||||
|  |     onEachFailure: (Throwable) -> Unit = {}, | ||||||
|  |     action: (Int) -> R | ||||||
|  | ): Optional<R> { | ||||||
|  |     repeat(times) { | ||||||
|  |         runCatching { | ||||||
|  |             action(it) | ||||||
|  |         }.onFailure(onEachFailure).onSuccess { | ||||||
|  |             return Optional.presented(it) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return Optional.absent() | ||||||
|  | } | ||||||
| @@ -2,10 +2,12 @@ package dev.inmo.micro_utils.common | |||||||
|  |  | ||||||
| import org.khronos.webgl.ArrayBuffer | import org.khronos.webgl.ArrayBuffer | ||||||
| import org.w3c.dom.ErrorEvent | import org.w3c.dom.ErrorEvent | ||||||
| import org.w3c.files.File | import org.w3c.files.* | ||||||
| import org.w3c.files.FileReader |  | ||||||
| import kotlin.js.Promise | import kotlin.js.Promise | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
| actual typealias MPPFile = File | actual typealias MPPFile = File | ||||||
|  |  | ||||||
| fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure -> | fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure -> | ||||||
| @@ -21,12 +23,32 @@ fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure -> | |||||||
|     reader.readAsArrayBuffer(this) |     reader.readAsArrayBuffer(this) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | fun MPPFile.readBytes(): ByteArray { | ||||||
|  |     val reader = FileReaderSync() | ||||||
|  |     return reader.readAsArrayBuffer(this).toByteArray() | ||||||
|  | } | ||||||
|  |  | ||||||
| private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await() | private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await() | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
| actual val MPPFile.filename: FileName | actual val MPPFile.filename: FileName | ||||||
|     get() = FileName(name) |     get() = FileName(name) | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
| actual val MPPFile.filesize: Long | actual val MPPFile.filesize: Long | ||||||
|     get() = size.toLong() |     get() = size.toLong() | ||||||
|  | /** | ||||||
|  |  * @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") | @Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can") | ||||||
| actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator | actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator | ||||||
|     get() = ::dirtyReadBytes |     get() = ::dirtyReadBytes | ||||||
|   | |||||||
| @@ -4,12 +4,29 @@ import dev.inmo.micro_utils.coroutines.doInIO | |||||||
| import dev.inmo.micro_utils.coroutines.doOutsideOfCoroutine | import dev.inmo.micro_utils.coroutines.doOutsideOfCoroutine | ||||||
| import java.io.File | import java.io.File | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
| actual typealias MPPFile = File | actual typealias MPPFile = File | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
| actual val MPPFile.filename: FileName | actual val MPPFile.filename: FileName | ||||||
|     get() = FileName(name) |     get() = FileName(name) | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
| actual val MPPFile.filesize: Long | actual val MPPFile.filesize: Long | ||||||
|     get() = length() |     get() = length() | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator | ||||||
|  |     get() = ::readBytes | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
| actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator | actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator | ||||||
|     get() = { |     get() = { | ||||||
|         doInIO { |         doInIO { | ||||||
|   | |||||||
| @@ -0,0 +1,94 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.channels.BufferOverflow | ||||||
|  | import kotlinx.coroutines.channels.Channel | ||||||
|  | import kotlinx.coroutines.flow.* | ||||||
|  | import kotlinx.coroutines.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  |  | ||||||
|  | private sealed interface AccumulatorFlowStep | ||||||
|  | private data class DataRetrievedAccumulatorFlowStep(val data: Any) : AccumulatorFlowStep | ||||||
|  | private data class SubscribeAccumulatorFlowStep(val channel: Channel<Any>) : AccumulatorFlowStep | ||||||
|  | private data class UnsubscribeAccumulatorFlowStep(val channel: Channel<Any>) : AccumulatorFlowStep | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This [Flow] will have behaviour very similar to [SharedFlow], but there are several differences: | ||||||
|  |  * | ||||||
|  |  * * All unhandled by [FlowCollector] data will not be removed from [AccumulatorFlow] and will be sent to new | ||||||
|  |  * [FlowCollector]s until anybody will handle it | ||||||
|  |  * * Here there are an [activeData] where data [T] will be stored until somebody will handle it | ||||||
|  |  */ | ||||||
|  | class AccumulatorFlow<T>( | ||||||
|  |     sourceDataFlow: Flow<T>, | ||||||
|  |     scope: CoroutineScope | ||||||
|  | ) : AbstractFlow<T>() { | ||||||
|  |     private val subscope = scope.LinkedSupervisorScope() | ||||||
|  |     private val activeData = ArrayDeque<T>() | ||||||
|  |     private val dataMutex = Mutex() | ||||||
|  |     private val channelsForBroadcast = mutableListOf<Channel<Any>>() | ||||||
|  |     private val channelsMutex = Mutex() | ||||||
|  |     private val steps = subscope.actor<AccumulatorFlowStep> { step -> | ||||||
|  |         when (step) { | ||||||
|  |             is DataRetrievedAccumulatorFlowStep -> { | ||||||
|  |                 if (activeData.first() === step.data) { | ||||||
|  |                     dataMutex.withLock { | ||||||
|  |                         activeData.removeFirst() | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             is SubscribeAccumulatorFlowStep -> channelsMutex.withLock { | ||||||
|  |                 channelsForBroadcast.add(step.channel) | ||||||
|  |                 dataMutex.withLock { | ||||||
|  |                     val dataToSend = activeData.toList() | ||||||
|  |                     safelyWithoutExceptions { | ||||||
|  |                         dataToSend.forEach { step.channel.send(it as Any) } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             is UnsubscribeAccumulatorFlowStep -> channelsMutex.withLock { | ||||||
|  |                 channelsForBroadcast.remove(step.channel) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     private val subscriptionJob = sourceDataFlow.subscribeSafelyWithoutExceptions(subscope) { | ||||||
|  |         dataMutex.withLock { | ||||||
|  |             activeData.addLast(it) | ||||||
|  |         } | ||||||
|  |         channelsMutex.withLock { | ||||||
|  |             channelsForBroadcast.forEach { channel -> | ||||||
|  |                 safelyWithResult { | ||||||
|  |                     channel.send(it as Any) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override suspend fun collectSafely(collector: FlowCollector<T>) { | ||||||
|  |         val channel = Channel<Any>(Channel.UNLIMITED, BufferOverflow.SUSPEND) | ||||||
|  |         steps.send(SubscribeAccumulatorFlowStep(channel)) | ||||||
|  |         for (data in channel) { | ||||||
|  |             try { | ||||||
|  |                 collector.emit(data as T) | ||||||
|  |                 steps.send(DataRetrievedAccumulatorFlowStep(data)) | ||||||
|  |             } finally { | ||||||
|  |                 channel.cancel() | ||||||
|  |                 steps.send(UnsubscribeAccumulatorFlowStep(channel)) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Creates [AccumulatorFlow] using [this] as base [Flow] | ||||||
|  |  */ | ||||||
|  | fun <T> Flow<T>.accumulatorFlow(scope: CoroutineScope): Flow<T> { | ||||||
|  |     return AccumulatorFlow(this, scope) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Creates [AccumulatorFlow] using [this] with [receiveAsFlow] to get | ||||||
|  |  */ | ||||||
|  | fun <T> Channel<T>.accumulatorFlow(scope: CoroutineScope): Flow<T> { | ||||||
|  |     return receiveAsFlow().accumulatorFlow(scope) | ||||||
|  | } | ||||||
| @@ -1,3 +1,6 @@ | |||||||
| package dev.inmo.micro_utils.crypto | package dev.inmo.micro_utils.crypto | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
| actual fun SourceBytes.md5(): MD5 = CryptoJS.MD5(decodeToString()) | actual fun SourceBytes.md5(): MD5 = CryptoJS.MD5(decodeToString()) | ||||||
|   | |||||||
| @@ -3,6 +3,9 @@ package dev.inmo.micro_utils.crypto | |||||||
| import java.math.BigInteger | import java.math.BigInteger | ||||||
| import java.security.MessageDigest | import java.security.MessageDigest | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
| actual fun SourceBytes.md5(): MD5 = BigInteger( | actual fun SourceBytes.md5(): MD5 = BigInteger( | ||||||
|     1, |     1, | ||||||
|     MessageDigest.getInstance("MD5").digest(this) |     MessageDigest.getInstance("MD5").digest(this) | ||||||
|   | |||||||
| @@ -13,10 +13,10 @@ repositories { | |||||||
|  |  | ||||||
| kotlin { | kotlin { | ||||||
|     jvm() |     jvm() | ||||||
|     js(IR) { | //    js(IR) { | ||||||
|         browser() | //        browser() | ||||||
|         nodejs() | //        nodejs() | ||||||
|     } | //    } | ||||||
|     android {} |     android {} | ||||||
|  |  | ||||||
|     sourceSets { |     sourceSets { | ||||||
| @@ -29,7 +29,7 @@ kotlin { | |||||||
|                         it != project |                         it != project | ||||||
|                         && it.hasProperty("kotlin") |                         && it.hasProperty("kotlin") | ||||||
|                         && it.kotlin.sourceSets.any { it.name.contains("commonMain") } |                         && it.kotlin.sourceSets.any { it.name.contains("commonMain") } | ||||||
|                         && it.kotlin.sourceSets.any { it.name.contains("jsMain") } | //                        && it.kotlin.sourceSets.any { it.name.contains("jsMain") } | ||||||
|                         && it.kotlin.sourceSets.any { it.name.contains("jvmMain") } |                         && it.kotlin.sourceSets.any { it.name.contains("jvmMain") } | ||||||
|                         && it.kotlin.sourceSets.any { it.name.contains("androidMain") } |                         && it.kotlin.sourceSets.any { it.name.contains("androidMain") } | ||||||
|                     ) { |                     ) { | ||||||
| @@ -38,22 +38,22 @@ kotlin { | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         jsMain { | //        jsMain { | ||||||
|             dependencies { | //            dependencies { | ||||||
|                 implementation kotlin('stdlib') | //                implementation kotlin('stdlib') | ||||||
|  |  | ||||||
|                 project.parent.subprojects.forEach { | //                project.parent.subprojects.forEach { | ||||||
|                     if ( | //                    if ( | ||||||
|                         it != project | //                        it != project | ||||||
|                         && it.hasProperty("kotlin") | //                        && it.hasProperty("kotlin") | ||||||
|                         && it.kotlin.sourceSets.any { it.name.contains("commonMain") } | //                        && it.kotlin.sourceSets.any { it.name.contains("commonMain") } | ||||||
|                         && it.kotlin.sourceSets.any { it.name.contains("jsMain") } | //                        && it.kotlin.sourceSets.any { it.name.contains("jsMain") } | ||||||
|                     ) { | //                    ) { | ||||||
|                         api it | //                        api it | ||||||
|                     } | //                    } | ||||||
|                 } | //                } | ||||||
|             } | //            } | ||||||
|         } | //        } | ||||||
|         jvmMain { |         jvmMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 implementation kotlin('stdlib') |                 implementation kotlin('stdlib') | ||||||
| @@ -116,9 +116,9 @@ tasks.dokkaHtml { | |||||||
|             sourceRoots.setFrom(findSourcesWithName("commonMain")) |             sourceRoots.setFrom(findSourcesWithName("commonMain")) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         named("jsMain") { | //        named("jsMain") { | ||||||
|             sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain")) | //            sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain")) | ||||||
|         } | //        } | ||||||
|  |  | ||||||
|         named("jvmMain") { |         named("jvmMain") { | ||||||
|             sourceRoots.setFrom(findSourcesWithName("jvmMain", "commonMain")) |             sourceRoots.setFrom(findSourcesWithName("jvmMain", "commonMain")) | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ kotlin { | |||||||
|     sourceSets { |     sourceSets { | ||||||
|         commonMain { |         commonMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|  |                 api project(":micro_utils.common") | ||||||
|                 api project(":micro_utils.coroutines") |                 api project(":micro_utils.coroutines") | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -0,0 +1,81 @@ | |||||||
|  | package dev.inmo.micro_utils.fsm.common | ||||||
|  |  | ||||||
|  | import kotlin.reflect.KClass | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Define checkable holder which can be used to precheck that this handler may handle incoming [State] | ||||||
|  |  */ | ||||||
|  | interface CheckableHandlerHolder<I : State, O : State> : StatesHandler<I, O> { | ||||||
|  |     suspend fun checkHandleable(state: O): Boolean | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Default realization of [StatesHandler]. It will incapsulate checking of [State] type in [checkHandleable] and class | ||||||
|  |  * casting in [handleState] | ||||||
|  |  */ | ||||||
|  | class CustomizableHandlerHolder<I : O, O : State>( | ||||||
|  |     private val delegateTo: StatesHandler<I, O>, | ||||||
|  |     private val filter: suspend (state: O) -> Boolean | ||||||
|  | ) : CheckableHandlerHolder<I, O> { | ||||||
|  |     /** | ||||||
|  |      * Checks that [state] can be handled by [delegateTo]. Under the hood it will check exact equality of [state] | ||||||
|  |      * [KClass] and use [KClass.isInstance] of [inputKlass] if [strict] == false | ||||||
|  |      */ | ||||||
|  |     override suspend fun checkHandleable(state: O) = filter(state) | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Calls [delegateTo] method [StatesHandler.handleState] with [state] casted to [I]. Use [checkHandleable] | ||||||
|  |      * to be sure that this [StatesHandlerHolder] will be able to handle [state] | ||||||
|  |      */ | ||||||
|  |     override suspend fun StatesMachine<in O>.handleState(state: I): O? { | ||||||
|  |         return delegateTo.run { handleState(state) } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun <I : O, O : State> CheckableHandlerHolder( | ||||||
|  |     inputKlass: KClass<I>, | ||||||
|  |     strict: Boolean = false, | ||||||
|  |     delegateTo: StatesHandler<I, O> | ||||||
|  | ) = CustomizableHandlerHolder( | ||||||
|  |     StatesHandler<O, O> { | ||||||
|  |         delegateTo.run { handleState(it as I) } | ||||||
|  |     }, | ||||||
|  |     if (strict) { | ||||||
|  |         { it::class == inputKlass } | ||||||
|  |     } else { | ||||||
|  |         { inputKlass.isInstance(it) } | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | @Deprecated("Renamed", ReplaceWith("CheckableHandlerHolder")) | ||||||
|  | fun <I : O, O : State> StateHandlerHolder( | ||||||
|  |     inputKlass: KClass<I>, | ||||||
|  |     strict: Boolean = false, | ||||||
|  |     delegateTo: StatesHandler<I, O> | ||||||
|  | ) = CheckableHandlerHolder(inputKlass, strict, delegateTo) | ||||||
|  |  | ||||||
|  | inline fun <reified I : O, O : State> CheckableHandlerHolder( | ||||||
|  |     strict: Boolean = false, | ||||||
|  |     delegateTo: StatesHandler<I, O> | ||||||
|  | ) = CheckableHandlerHolder(I::class, strict, delegateTo) | ||||||
|  |  | ||||||
|  | @Deprecated("Renamed", ReplaceWith("CheckableHandlerHolder")) | ||||||
|  | inline fun <reified I : O, O : State> StateHandlerHolder( | ||||||
|  |     strict: Boolean = false, | ||||||
|  |     delegateTo: StatesHandler<I, O> | ||||||
|  | ) = CheckableHandlerHolder(strict, delegateTo) | ||||||
|  |  | ||||||
|  | inline fun <reified I : O, O: State> StatesHandler<I, O>.holder( | ||||||
|  |     strict: Boolean = true | ||||||
|  | ) = CheckableHandlerHolder<I, O>( | ||||||
|  |     I::class, | ||||||
|  |     strict, | ||||||
|  |     this | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | inline fun <I : O, O: State> StatesHandler<I, O>.holder( | ||||||
|  |     noinline filter: suspend (state: State) -> Boolean | ||||||
|  | ) = CustomizableHandlerHolder<O, O>( | ||||||
|  |     { this@holder.run { handleState(it as I) } }, | ||||||
|  |     filter | ||||||
|  | ) | ||||||
| @@ -1,15 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.fsm.common |  | ||||||
|  |  | ||||||
| import kotlin.reflect.KClass |  | ||||||
|  |  | ||||||
| class StateHandlerHolder<I : State>( |  | ||||||
|     private val inputKlass: KClass<I>, |  | ||||||
|     private val strict: Boolean = false, |  | ||||||
|     private val delegateTo: StatesHandler<I> |  | ||||||
| ) : StatesHandler<State> { |  | ||||||
|     fun checkHandleable(state: State) = state::class == inputKlass || (!strict && inputKlass.isInstance(state)) |  | ||||||
|  |  | ||||||
|     override suspend fun StatesMachine.handleState(state: State): State? { |  | ||||||
|         return delegateTo.run { handleState(state as I) } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,5 +1,12 @@ | |||||||
| package dev.inmo.micro_utils.fsm.common | package dev.inmo.micro_utils.fsm.common | ||||||
|  |  | ||||||
| fun interface StatesHandler<I : State> { | /** | ||||||
|     suspend fun StatesMachine.handleState(state: I): State? |  * Default realization of states handler | ||||||
|  |  */ | ||||||
|  | fun interface StatesHandler<I : State, O: State> { | ||||||
|  |     /** | ||||||
|  |      * Main handling of [state]. In case when this [state] leads to another [State] and [handleState] returns not null | ||||||
|  |      * [State] it is assumed that chain is not completed. | ||||||
|  |      */ | ||||||
|  |     suspend fun StatesMachine<in O>.handleState(state: I): O? | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,46 +1,120 @@ | |||||||
| package dev.inmo.micro_utils.fsm.common | package dev.inmo.micro_utils.fsm.common | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.Optional | ||||||
|  | import dev.inmo.micro_utils.common.onPresented | ||||||
| import dev.inmo.micro_utils.coroutines.* | import dev.inmo.micro_utils.coroutines.* | ||||||
| import kotlinx.coroutines.* | import kotlinx.coroutines.* | ||||||
| import kotlinx.coroutines.flow.asFlow | import kotlinx.coroutines.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  |  | ||||||
| private suspend fun <I : State> StatesMachine.launchStateHandling( | /** | ||||||
|     state: State, |  * Default [StatesMachine] may [startChain] and use inside logic for handling [State]s. By default you may use | ||||||
|     handlers: List<StateHandlerHolder<out I>> |  * [DefaultStatesMachine] or build it with [dev.inmo.micro_utils.fsm.common.dsl.buildFSM]. Implementers MUST NOT start | ||||||
| ): State? { |  * handling until [start] method will be called | ||||||
|     return handlers.firstOrNull { it.checkHandleable(state) } ?.run { |  */ | ||||||
|         handleState(state) | interface StatesMachine<T : State> : StatesHandler<T, T> { | ||||||
|  |     suspend fun launchStateHandling( | ||||||
|  |         state: T, | ||||||
|  |         handlers: List<CheckableHandlerHolder<in T, T>> | ||||||
|  |     ): T? { | ||||||
|  |         return handlers.firstOrNull { it.checkHandleable(state) } ?.run { | ||||||
|  |             handleState(state) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Starts handling of [State]s | ||||||
|  |      */ | ||||||
|  |     fun start(scope: CoroutineScope): Job | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Start chain of [State]s witn [state] | ||||||
|  |      */ | ||||||
|  |     suspend fun startChain(state: T) | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         /** | ||||||
|  |          * Creates [DefaultStatesMachine] | ||||||
|  |          */ | ||||||
|  |         operator fun <T: State> invoke( | ||||||
|  |             statesManager: StatesManager<T>, | ||||||
|  |             handlers: List<CheckableHandlerHolder<in T, T>> | ||||||
|  |         ) = DefaultStatesMachine(statesManager, handlers) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| class StatesMachine ( | /** | ||||||
|     private val statesManager: StatesManager, |  * Default realization of [StatesMachine]. It uses [statesManager] for incapsulation of [State]s storing and contexts | ||||||
|     private val handlers: List<StateHandlerHolder<*>> |  * resolving, and uses [launchStateHandling] for [State] handling. | ||||||
| ) : StatesHandler<State> { |  * | ||||||
|     override suspend fun StatesMachine.handleState(state: State): State? = launchStateHandling(state, handlers) |  * This class suppose to be extended in case you wish some custom behaviour inside of [launchStateHandling], for example | ||||||
|  |  */ | ||||||
|  | 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) | ||||||
|  |  | ||||||
|     fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions { |     /** | ||||||
|         val statePerformer: suspend (State) -> Unit = { state: State -> |      * This | ||||||
|             val newState = launchStateHandling(state, handlers) |      */ | ||||||
|             if (newState != null) { |     protected val statesJobs = mutableMapOf<T, Job>() | ||||||
|                 statesManager.update(state, newState) |     protected val statesJobsMutex = Mutex() | ||||||
|             } else { |  | ||||||
|                 statesManager.endChain(state) |     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 | ||||||
|  |      * [launchStateHandling] will returns some [State] then [statesManager] [StatesManager.update] will be used, otherwise | ||||||
|  |      * [StatesManager.endChain]. | ||||||
|  |      */ | ||||||
|  |     override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions { | ||||||
|         statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) { |         statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) { | ||||||
|             launch { statePerformer(it) } |             launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) } | ||||||
|         } |         } | ||||||
|         statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) { |         statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) { | ||||||
|             launch { statePerformer(it.second) } |             launch { performStateUpdate(Optional.presented(it.first), it.second, scope.LinkedSupervisorScope()) } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         statesManager.getActiveStates().forEach { |         statesManager.getActiveStates().forEach { | ||||||
|             launch { statePerformer(it) } |             launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     suspend fun startChain(state: State) { |     /** | ||||||
|  |      * Just calls [StatesManager.startChain] of [statesManager] | ||||||
|  |      */ | ||||||
|  |     override suspend fun startChain(state: T) { | ||||||
|         statesManager.startChain(state) |         statesManager.startChain(state) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,92 +1,30 @@ | |||||||
| package dev.inmo.micro_utils.fsm.common | package dev.inmo.micro_utils.fsm.common | ||||||
|  |  | ||||||
| import kotlinx.coroutines.flow.* | import kotlinx.coroutines.flow.Flow | ||||||
| import kotlinx.coroutines.sync.Mutex |  | ||||||
| import kotlinx.coroutines.sync.withLock |  | ||||||
|  |  | ||||||
| interface StatesManager { | interface StatesManager<T : State> { | ||||||
|     val onChainStateUpdated: Flow<Pair<State, State>> |     val onChainStateUpdated: Flow<Pair<T, T>> | ||||||
|     val onStartChain: Flow<State> |     val onStartChain: Flow<T> | ||||||
|     val onEndChain: Flow<State> |     val onEndChain: Flow<T> | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Must set current set using [State.context] |      * Must set current set using [State.context] | ||||||
|      */ |      */ | ||||||
|     suspend fun update(old: State, new: State) |     suspend fun update(old: T, new: T) | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Starts chain with [state] as first [State]. May returns false in case of [State.context] of [state] is already |      * Starts chain with [state] as first [State]. May returns false in case of [State.context] of [state] is already | ||||||
|      * busy by the other [State] |      * busy by the other [State] | ||||||
|      */ |      */ | ||||||
|     suspend fun startChain(state: State) |     suspend fun startChain(state: T) | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Ends chain with context from [state]. In case when [State.context] of [state] is absent, [state] should be just |      * Ends chain with context from [state]. In case when [State.context] of [state] is absent, [state] should be just | ||||||
|      * ignored |      * ignored | ||||||
|      */ |      */ | ||||||
|     suspend fun endChain(state: State) |     suspend fun endChain(state: T) | ||||||
|  |  | ||||||
|     suspend fun getActiveStates(): List<State> |     suspend fun getActiveStates(): List<T> | ||||||
| } | } | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context] |  | ||||||
|  * key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by |  | ||||||
|  * new state by using [endChain] with that state |  | ||||||
|  */ |  | ||||||
| class InMemoryStatesManager( |  | ||||||
|     private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true } |  | ||||||
| ) : StatesManager { |  | ||||||
|     private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0) |  | ||||||
|     override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow() |  | ||||||
|     private val _onStartChain = MutableSharedFlow<State>(0) |  | ||||||
|     override val onStartChain: Flow<State> = _onStartChain.asSharedFlow() |  | ||||||
|     private val _onEndChain = MutableSharedFlow<State>(0) |  | ||||||
|     override val onEndChain: Flow<State> = _onEndChain.asSharedFlow() |  | ||||||
|  |  | ||||||
|     private val contextsToStates = mutableMapOf<Any, State>() |  | ||||||
|     private val mapMutex = Mutex() |  | ||||||
|  |  | ||||||
|     override suspend fun update(old: State, new: State) = mapMutex.withLock { |  | ||||||
|         when { |  | ||||||
|             contextsToStates[old.context] != old -> return@withLock |  | ||||||
|             old.context == new.context || !contextsToStates.containsKey(new.context) -> { |  | ||||||
|                 contextsToStates[old.context] = new |  | ||||||
|                 _onChainStateUpdated.emit(old to new) |  | ||||||
|             } |  | ||||||
|             else -> { |  | ||||||
|                 val stateOnNewOneContext = contextsToStates.getValue(new.context) |  | ||||||
|                 if (onContextsConflictResolver(old, new, stateOnNewOneContext)) { |  | ||||||
|                     endChainWithoutLock(stateOnNewOneContext) |  | ||||||
|                     contextsToStates.remove(old.context) |  | ||||||
|                     contextsToStates[new.context] = new |  | ||||||
|                     _onChainStateUpdated.emit(old to new) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override suspend fun startChain(state: State) = mapMutex.withLock { |  | ||||||
|         if (!contextsToStates.containsKey(state.context)) { |  | ||||||
|             contextsToStates[state.context] = state |  | ||||||
|             _onStartChain.emit(state) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private suspend fun endChainWithoutLock(state: State) { |  | ||||||
|         if (contextsToStates[state.context] == state) { |  | ||||||
|             contextsToStates.remove(state.context) |  | ||||||
|             _onEndChain.emit(state) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override suspend fun endChain(state: State) { |  | ||||||
|         mapMutex.withLock { |  | ||||||
|             endChainWithoutLock(state) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override suspend fun getActiveStates(): List<State> = contextsToStates.values.toList() |  | ||||||
|  |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -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) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,35 +1,61 @@ | |||||||
| package dev.inmo.micro_utils.fsm.common.dsl | package dev.inmo.micro_utils.fsm.common.dsl | ||||||
|  |  | ||||||
| import dev.inmo.micro_utils.fsm.common.* | import dev.inmo.micro_utils.fsm.common.* | ||||||
|  | import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager | ||||||
|  | import dev.inmo.micro_utils.fsm.common.managers.InMemoryDefaultStatesManagerRepo | ||||||
| import kotlin.reflect.KClass | import kotlin.reflect.KClass | ||||||
|  |  | ||||||
| class FSMBuilder( | class FSMBuilder<T : State>( | ||||||
|     var statesManager: StatesManager = InMemoryStatesManager() |     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<StateHandlerHolder<*>>() |     private var states = mutableListOf<CheckableHandlerHolder<T, T>>() | ||||||
|  |  | ||||||
|     fun <I : State> add(kClass: KClass<I>, handler: StatesHandler<I>) { |     fun add(handler: CheckableHandlerHolder<T, T>) { | ||||||
|         states.add(StateHandlerHolder(kClass, false, handler)) |         states.add(handler) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun <I : State> addStrict(kClass: KClass<I>, handler: StatesHandler<I>) { |     fun <I : T> add(kClass: KClass<I>, handler: StatesHandler<I, T>) { | ||||||
|         states.add(StateHandlerHolder(kClass, true, handler)) |         add(CheckableHandlerHolder(kClass, false, handler)) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun build() = StatesMachine( |     fun <I : T> add(filter: suspend (state: State) -> Boolean, handler: StatesHandler<I, T>) { | ||||||
|  |         add(handler.holder(filter)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun <I : T> addStrict(kClass: KClass<I>, handler: StatesHandler<I, T>) { | ||||||
|  |         states.add(CheckableHandlerHolder(kClass, true, handler)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     inline fun <reified I : T> onStateOrSubstate(handler: StatesHandler<I, T>) { | ||||||
|  |         add(I::class, handler) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     inline fun <reified I : T> strictlyOn(handler: StatesHandler<I, T>) { | ||||||
|  |         addStrict(I::class, handler) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     inline fun <reified I : T> doWhen( | ||||||
|  |         noinline filter: suspend (state: State) -> Boolean, | ||||||
|  |         handler: StatesHandler<I, T> | ||||||
|  |     ) { | ||||||
|  |         add(filter, handler) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun build() = fsmBuilder( | ||||||
|         statesManager, |         statesManager, | ||||||
|         states.toList() |         states.toList().let { list -> | ||||||
|  |             defaultStateHandler ?.let { list + it.holder { true } } ?: list | ||||||
|  |         } | ||||||
|     ) |     ) | ||||||
| } | } | ||||||
|  |  | ||||||
| inline fun <reified I : State> FSMBuilder.onStateOrSubstate(handler: StatesHandler<I>) { | fun <T : State> buildFSM( | ||||||
|     add(I::class, handler) |     block: FSMBuilder<T>.() -> Unit | ||||||
| } | ): StatesMachine<T> = FSMBuilder<T>().apply(block).build() | ||||||
|  |  | ||||||
| inline fun <reified I : State> FSMBuilder.strictlyOn(handler: StatesHandler<I>) { |  | ||||||
|     addStrict(I::class, handler) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fun buildFSM( |  | ||||||
|     block: FSMBuilder.() -> Unit |  | ||||||
| ): StatesMachine = FSMBuilder().apply(block).build() |  | ||||||
|   | |||||||
| @@ -0,0 +1,101 @@ | |||||||
|  | package dev.inmo.micro_utils.fsm.common.managers | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.fsm.common.State | ||||||
|  | import dev.inmo.micro_utils.fsm.common.StatesManager | ||||||
|  | import kotlinx.coroutines.flow.* | ||||||
|  | import kotlinx.coroutines.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Implement this repo if you want to use some custom repo for [DefaultStatesManager] | ||||||
|  |  */ | ||||||
|  | interface DefaultStatesManagerRepo<T : State> { | ||||||
|  |     /** | ||||||
|  |      * Must save [state] as current state of chain with [State.context] of [state] | ||||||
|  |      */ | ||||||
|  |     suspend fun set(state: T) | ||||||
|  |     /** | ||||||
|  |      * Remove exactly [state]. In case if internally [State.context] is busy with different [State], that [State] should | ||||||
|  |      * NOT be removed | ||||||
|  |      */ | ||||||
|  |     suspend fun removeState(state: T) | ||||||
|  |     /** | ||||||
|  |      * @return Current list of available and saved states | ||||||
|  |      */ | ||||||
|  |     suspend fun getStates(): List<T> | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @return Current state by [context] | ||||||
|  |      */ | ||||||
|  |     suspend fun getContextState(context: Any): T? | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @return Current state by [context] | ||||||
|  |      */ | ||||||
|  |     suspend fun contains(context: Any): Boolean = getContextState(context) != null | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @param repo This repo will be used as repository for storing states. All operations with this repo will happen BEFORE | ||||||
|  |  * any event will be sent to [onChainStateUpdated], [onStartChain] or [onEndChain]. By default will be used | ||||||
|  |  * [InMemoryDefaultStatesManagerRepo] or you may create custom [DefaultStatesManagerRepo] and pass as [repo] parameter | ||||||
|  |  * @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context] | ||||||
|  |  * key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by | ||||||
|  |  * new state by using [endChain] with that state | ||||||
|  |  */ | ||||||
|  | class DefaultStatesManager<T : State>( | ||||||
|  |     private val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(), | ||||||
|  |     private val onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true } | ||||||
|  | ) : StatesManager<T> { | ||||||
|  |     private val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0) | ||||||
|  |     override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow() | ||||||
|  |     private val _onStartChain = MutableSharedFlow<T>(0) | ||||||
|  |     override val onStartChain: Flow<T> = _onStartChain.asSharedFlow() | ||||||
|  |     private val _onEndChain = MutableSharedFlow<T>(0) | ||||||
|  |     override val onEndChain: Flow<T> = _onEndChain.asSharedFlow() | ||||||
|  |  | ||||||
|  |     private val mapMutex = Mutex() | ||||||
|  |  | ||||||
|  |     override suspend fun update(old: T, new: T) = mapMutex.withLock { | ||||||
|  |         val stateByOldContext: T? = repo.getContextState(old.context) | ||||||
|  |         when { | ||||||
|  |             stateByOldContext != old -> return@withLock | ||||||
|  |             stateByOldContext == null || old.context == new.context -> { | ||||||
|  |                 repo.set(new) | ||||||
|  |                 _onChainStateUpdated.emit(old to new) | ||||||
|  |             } | ||||||
|  |             else -> { | ||||||
|  |                 val stateOnNewOneContext = repo.getContextState(new.context) | ||||||
|  |                 if (stateOnNewOneContext == null || onContextsConflictResolver(old, new, stateOnNewOneContext)) { | ||||||
|  |                     stateOnNewOneContext ?.let { endChainWithoutLock(it) } | ||||||
|  |                     repo.removeState(old) | ||||||
|  |                     repo.set(new) | ||||||
|  |                     _onChainStateUpdated.emit(old to new) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override suspend fun startChain(state: T) = mapMutex.withLock { | ||||||
|  |         if (!repo.contains(state.context)) { | ||||||
|  |             repo.set(state) | ||||||
|  |             _onStartChain.emit(state) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private suspend fun endChainWithoutLock(state: T) { | ||||||
|  |         if (repo.getContextState(state.context) == state) { | ||||||
|  |             repo.removeState(state) | ||||||
|  |             _onEndChain.emit(state) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override suspend fun endChain(state: T) { | ||||||
|  |         mapMutex.withLock { | ||||||
|  |             endChainWithoutLock(state) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override suspend fun getActiveStates(): List<T> = repo.getStates() | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,25 @@ | |||||||
|  | package dev.inmo.micro_utils.fsm.common.managers | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.fsm.common.State | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Simple [DefaultStatesManagerRepo] for [DefaultStatesManager] which will store data in [map] and use primitive | ||||||
|  |  * functionality | ||||||
|  |  */ | ||||||
|  | class InMemoryDefaultStatesManagerRepo<T : State>( | ||||||
|  |     private val map: MutableMap<Any, T> = mutableMapOf() | ||||||
|  | ) : DefaultStatesManagerRepo<T> { | ||||||
|  |     override suspend fun set(state: T) { | ||||||
|  |         map[state.context] = state | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override suspend fun removeState(state: T) { | ||||||
|  |         map.remove(state.context) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override suspend fun getStates(): List<T> = map.values.toList() | ||||||
|  |  | ||||||
|  |     override suspend fun getContextState(context: Any): T? = map[context] | ||||||
|  |  | ||||||
|  |     override suspend fun contains(context: Any): Boolean = map.contains(context) | ||||||
|  | } | ||||||
| @@ -0,0 +1,16 @@ | |||||||
|  | package dev.inmo.micro_utils.fsm.common.managers | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.fsm.common.State | ||||||
|  | import kotlinx.coroutines.flow.* | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Creates [DefaultStatesManager] with [InMemoryDefaultStatesManagerRepo] | ||||||
|  |  * | ||||||
|  |  * @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context] | ||||||
|  |  * key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by | ||||||
|  |  * new state by using [endChain] with that state | ||||||
|  |  */ | ||||||
|  | @Deprecated("Use DefaultStatesManager instead", ReplaceWith("DefaultStatesManager")) | ||||||
|  | fun <T: State> InMemoryStatesManager( | ||||||
|  |     onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true } | ||||||
|  | ) = DefaultStatesManager(onContextsConflictResolver = onContextsConflictResolver) | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| import dev.inmo.micro_utils.fsm.common.* | import dev.inmo.micro_utils.fsm.common.* | ||||||
| import dev.inmo.micro_utils.fsm.common.dsl.buildFSM | import dev.inmo.micro_utils.fsm.common.dsl.buildFSM | ||||||
| import dev.inmo.micro_utils.fsm.common.dsl.strictlyOn | import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager | ||||||
|  | import dev.inmo.micro_utils.fsm.common.managers.InMemoryStatesManager | ||||||
| import kotlinx.coroutines.* | import kotlinx.coroutines.* | ||||||
|  |  | ||||||
| sealed interface TrafficLightState : State { | sealed interface TrafficLightState : State { | ||||||
| @@ -25,9 +26,9 @@ class PlayableMain { | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             val statesManager = InMemoryStatesManager() |             val statesManager = DefaultStatesManager<TrafficLightState>() | ||||||
|  |  | ||||||
|             val machine = buildFSM { |             val machine = buildFSM<TrafficLightState> { | ||||||
|                 strictlyOn<GreenCommon> { |                 strictlyOn<GreenCommon> { | ||||||
|                     delay(1000L) |                     delay(1000L) | ||||||
|                     YellowCommon(it.context).also(::println) |                     YellowCommon(it.context).also(::println) | ||||||
|   | |||||||
| @@ -0,0 +1,25 @@ | |||||||
|  | package dev.inmo.micro_utils.fsm.repos.common | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.fsm.common.State | ||||||
|  | import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo | ||||||
|  | import dev.inmo.micro_utils.repos.* | ||||||
|  | import dev.inmo.micro_utils.repos.pagination.getAll | ||||||
|  |  | ||||||
|  | class KeyValueBasedDefaultStatesManagerRepo<T : State>( | ||||||
|  |     private val keyValueRepo: KeyValueRepo<Any, T> | ||||||
|  | ) : DefaultStatesManagerRepo<T> { | ||||||
|  |     override suspend fun set(state: T) { | ||||||
|  |         keyValueRepo.set(state.context, state) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override suspend fun removeState(state: T) { | ||||||
|  |         if (keyValueRepo.get(state.context) == state) { | ||||||
|  |             keyValueRepo.unset(state.context) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override suspend fun getStates(): List<T> = keyValueRepo.getAll { keys(it) }.map { it.second } | ||||||
|  |     override suspend fun getContextState(context: Any): T? = keyValueRepo.get(context) | ||||||
|  |  | ||||||
|  |     override suspend fun contains(context: Any): Boolean = keyValueRepo.contains(context) | ||||||
|  | } | ||||||
| @@ -1,83 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.fsm.repos.common |  | ||||||
|  |  | ||||||
| import dev.inmo.micro_utils.fsm.common.State |  | ||||||
| import dev.inmo.micro_utils.fsm.common.StatesManager |  | ||||||
| import dev.inmo.micro_utils.repos.* |  | ||||||
| import dev.inmo.micro_utils.repos.mappers.withMapper |  | ||||||
| import dev.inmo.micro_utils.repos.pagination.getAll |  | ||||||
| import kotlinx.coroutines.flow.* |  | ||||||
| import kotlinx.coroutines.sync.Mutex |  | ||||||
| import kotlinx.coroutines.sync.withLock |  | ||||||
|  |  | ||||||
| class KeyValueBasedStatesManager( |  | ||||||
|     private val keyValueRepo: KeyValueRepo<Any, State>, |  | ||||||
|     private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true } |  | ||||||
| ) : StatesManager { |  | ||||||
|     private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0) |  | ||||||
|     override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow() |  | ||||||
|     private val _onEndChain = MutableSharedFlow<State>(0) |  | ||||||
|     override val onEndChain: Flow<State> = _onEndChain.asSharedFlow() |  | ||||||
|  |  | ||||||
|     override val onStartChain: Flow<State> = keyValueRepo.onNewValue.map { it.second } |  | ||||||
|  |  | ||||||
|     private val mutex = Mutex() |  | ||||||
|  |  | ||||||
|     override suspend fun update(old: State, new: State) { |  | ||||||
|         mutex.withLock { |  | ||||||
|             when { |  | ||||||
|                 keyValueRepo.get(old.context) != old -> return@withLock |  | ||||||
|                 old.context == new.context || !keyValueRepo.contains(new.context) -> { |  | ||||||
|                     keyValueRepo.set(old.context, new) |  | ||||||
|                     _onChainStateUpdated.emit(old to new) |  | ||||||
|                 } |  | ||||||
|                 else -> { |  | ||||||
|                     val stateOnNewOneContext = keyValueRepo.get(new.context)!! |  | ||||||
|                     if (onContextsConflictResolver(old, new, stateOnNewOneContext)) { |  | ||||||
|                         endChainWithoutLock(stateOnNewOneContext) |  | ||||||
|                         keyValueRepo.unset(old.context) |  | ||||||
|                         keyValueRepo.set(new.context, new) |  | ||||||
|                         _onChainStateUpdated.emit(old to new) |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override suspend fun startChain(state: State) { |  | ||||||
|         if (!keyValueRepo.contains(state.context)) { |  | ||||||
|             keyValueRepo.set(state.context, state) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private suspend fun endChainWithoutLock(state: State) { |  | ||||||
|         if (keyValueRepo.get(state.context) == state) { |  | ||||||
|             keyValueRepo.unset(state.context) |  | ||||||
|             _onEndChain.emit(state) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override suspend fun endChain(state: State) { |  | ||||||
|         mutex.withLock { endChainWithoutLock(state) } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override suspend fun getActiveStates(): List<State> { |  | ||||||
|         return keyValueRepo.getAll { keys(it) }.map { it.second } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| inline fun <reified TargetContextType, reified TargetStateType> createStatesManager( |  | ||||||
|     targetKeyValueRepo: KeyValueRepo<TargetContextType, TargetStateType>, |  | ||||||
|     noinline contextToOutTransformer: suspend Any.() -> TargetContextType, |  | ||||||
|     noinline stateToOutTransformer: suspend State.() -> TargetStateType, |  | ||||||
|     noinline outToContextTransformer: suspend TargetContextType.() -> Any, |  | ||||||
|     noinline outToStateTransformer: suspend TargetStateType.() -> State, |  | ||||||
| ) = KeyValueBasedStatesManager( |  | ||||||
|     targetKeyValueRepo.withMapper<Any, State, TargetContextType, TargetStateType>( |  | ||||||
|         contextToOutTransformer, |  | ||||||
|         stateToOutTransformer, |  | ||||||
|         outToContextTransformer, |  | ||||||
|         outToStateTransformer |  | ||||||
|     ) |  | ||||||
| ) |  | ||||||
| @@ -9,12 +9,12 @@ org.gradle.jvmargs=-Xmx2g | |||||||
|  |  | ||||||
| kotlin_version=1.5.31 | kotlin_version=1.5.31 | ||||||
| kotlin_coroutines_version=1.5.2 | kotlin_coroutines_version=1.5.2 | ||||||
| kotlin_serialisation_core_version=1.2.2 | kotlin_serialisation_core_version=1.3.1 | ||||||
| kotlin_exposed_version=0.34.2 | kotlin_exposed_version=0.36.2 | ||||||
|  |  | ||||||
| ktor_version=1.6.3 | ktor_version=1.6.5 | ||||||
|  |  | ||||||
| klockVersion=2.4.2 | klockVersion=2.4.8 | ||||||
|  |  | ||||||
| github_release_plugin_version=2.2.12 | github_release_plugin_version=2.2.12 | ||||||
|  |  | ||||||
| @@ -22,14 +22,14 @@ uuidVersion=0.3.1 | |||||||
|  |  | ||||||
| # ANDROID | # ANDROID | ||||||
|  |  | ||||||
| core_ktx_version=1.6.0 | core_ktx_version=1.7.0 | ||||||
| androidx_recycler_version=1.2.1 | androidx_recycler_version=1.2.1 | ||||||
| appcompat_version=1.3.1 | appcompat_version=1.4.0 | ||||||
|  |  | ||||||
| android_minSdkVersion=19 | android_minSdkVersion=19 | ||||||
| android_compileSdkVersion=30 | android_compileSdkVersion=32 | ||||||
| android_buildToolsVersion=30.0.3 | android_buildToolsVersion=32.0.0 | ||||||
| dexcount_version=3.0.0 | dexcount_version=3.0.1 | ||||||
| junit_version=4.12 | junit_version=4.12 | ||||||
| test_ext_junit_version=1.1.2 | test_ext_junit_version=1.1.2 | ||||||
| espresso_core=3.3.0 | espresso_core=3.3.0 | ||||||
| @@ -40,10 +40,10 @@ crypto_js_version=4.1.1 | |||||||
|  |  | ||||||
| # Dokka | # Dokka | ||||||
|  |  | ||||||
| dokka_version=1.5.0 | dokka_version=1.5.31 | ||||||
|  |  | ||||||
| # Project data | # Project data | ||||||
|  |  | ||||||
| group=dev.inmo | group=dev.inmo | ||||||
| version=0.5.28 | version=0.8.9 | ||||||
| android_code_version=69 | android_code_version=89 | ||||||
|   | |||||||
| @@ -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 | 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 dev.inmo.micro_utils.ktor.common.* | ||||||
| import io.ktor.client.HttpClient | import io.ktor.client.HttpClient | ||||||
| import io.ktor.client.request.get | import io.ktor.client.request.* | ||||||
| import io.ktor.client.request.post | import io.ktor.client.request.forms.* | ||||||
|  | import io.ktor.http.* | ||||||
|  | import io.ktor.utils.io.core.ByteReadPacket | ||||||
| import kotlinx.serialization.* | import kotlinx.serialization.* | ||||||
|  |  | ||||||
| typealias BodyPair<T> = Pair<SerializationStrategy<T>, T> | typealias BodyPair<T> = Pair<SerializationStrategy<T>, T> | ||||||
|  |  | ||||||
| class UnifiedRequester( | class UnifiedRequester( | ||||||
|     private val client: HttpClient = HttpClient(), |     val client: HttpClient = HttpClient(), | ||||||
|     private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat |     val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat | ||||||
| ) { | ) { | ||||||
|     suspend fun <ResultType> uniget( |     suspend fun <ResultType> uniget( | ||||||
|         url: String, |         url: String, | ||||||
| @@ -31,6 +35,54 @@ class UnifiedRequester( | |||||||
|         resultDeserializer: DeserializationStrategy<ResultType> |         resultDeserializer: DeserializationStrategy<ResultType> | ||||||
|     ) = client.unipost(url, bodyInfo, resultDeserializer, serialFormat) |     ) = 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( |     fun <T> createStandardWebsocketFlow( | ||||||
|         url: String, |         url: String, | ||||||
|         checkReconnection: (Throwable?) -> Boolean = { true }, |         checkReconnection: (Throwable?) -> Boolean = { true }, | ||||||
| @@ -69,3 +121,124 @@ suspend fun <BodyType, ResultType> HttpClient.unipost( | |||||||
| }.let { | }.let { | ||||||
|     serialFormat.decodeDefault(resultDeserializer, it) |     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 { |     sourceSets { | ||||||
|         commonMain { |         commonMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|  |                 api internalProject("micro_utils.common") | ||||||
|                 api "org.jetbrains.kotlinx:kotlinx-serialization-cbor:$kotlin_serialisation_core_version" |                 api "org.jetbrains.kotlinx:kotlinx-serialization-cbor:$kotlin_serialisation_core_version" | ||||||
|                 api "com.soywiz.korlibs.klock:klock:$klockVersion" |                 api "com.soywiz.korlibs.klock:klock:$klockVersion" | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -1,22 +1,31 @@ | |||||||
| package dev.inmo.micro_utils.ktor.server | 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.coroutines.safely | ||||||
| import dev.inmo.micro_utils.ktor.common.* | import dev.inmo.micro_utils.ktor.common.* | ||||||
| import io.ktor.application.ApplicationCall | import io.ktor.application.ApplicationCall | ||||||
| import io.ktor.application.call | import io.ktor.application.call | ||||||
| import io.ktor.http.ContentType | import io.ktor.http.ContentType | ||||||
| import io.ktor.http.HttpStatusCode | 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.receive | ||||||
|  | import io.ktor.request.receiveMultipart | ||||||
| import io.ktor.response.respond | import io.ktor.response.respond | ||||||
| import io.ktor.response.respondBytes | import io.ktor.response.respondBytes | ||||||
| import io.ktor.routing.Route | 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.util.pipeline.PipelineContext | ||||||
|  | import io.ktor.utils.io.core.* | ||||||
| import kotlinx.coroutines.flow.Flow | import kotlinx.coroutines.flow.Flow | ||||||
| import kotlinx.serialization.* | import kotlinx.serialization.* | ||||||
|  | import java.io.File | ||||||
|  | import java.io.File.createTempFile | ||||||
|  |  | ||||||
| class UnifiedRouter( | class UnifiedRouter( | ||||||
|     private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat, |     val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat, | ||||||
|     private val serialFormatContentType: ContentType = standardKtorSerialFormatContentType |     val serialFormatContentType: ContentType = standardKtorSerialFormatContentType | ||||||
| ) { | ) { | ||||||
|     fun <T> Route.includeWebsocketHandling( |     fun <T> Route.includeWebsocketHandling( | ||||||
|         suburl: String, |         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( | suspend fun ApplicationCall.getParameterOrSendError( | ||||||
|     field: String |     field: String | ||||||
| ) = parameters[field].also { | ) = parameters[field].also { | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| package dev.inmo.micro_utils.ktor.server | package dev.inmo.micro_utils.ktor.server | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator | ||||||
| import io.ktor.application.Application | import io.ktor.application.Application | ||||||
| import io.ktor.server.cio.CIO | import io.ktor.server.cio.CIO | ||||||
| import io.ktor.server.engine.* | import io.ktor.server.engine.* | ||||||
| @@ -31,3 +32,27 @@ fun createKtorServer( | |||||||
|     port: Int = Random.nextInt(1024, 65535), |     port: Int = Random.nextInt(1024, 65535), | ||||||
|     block: Application.() -> Unit |     block: Application.() -> Unit | ||||||
| ): ApplicationEngine = createKtorServer(CIO, host, port, block) | ): ApplicationEngine = createKtorServer(CIO, host, port, block) | ||||||
|  |  | ||||||
|  | fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration> createKtorServer( | ||||||
|  |     engine: ApplicationEngineFactory<TEngine, TConfiguration>, | ||||||
|  |     host: String = "localhost", | ||||||
|  |     port: Int = Random.nextInt(1024, 65535), | ||||||
|  |     configurators: List<KtorApplicationConfigurator> | ||||||
|  | ): TEngine = createKtorServer( | ||||||
|  |     engine, | ||||||
|  |     host, | ||||||
|  |     port | ||||||
|  | ) { | ||||||
|  |     configurators.forEach { it.apply { configure() } } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Create server with [CIO] server engine without starting of it | ||||||
|  |  * | ||||||
|  |  * @see ApplicationEngine.start | ||||||
|  |  */ | ||||||
|  | fun createKtorServer( | ||||||
|  |     host: String = "localhost", | ||||||
|  |     port: Int = Random.nextInt(1024, 65535), | ||||||
|  |     configurators: List<KtorApplicationConfigurator> | ||||||
|  | ): ApplicationEngine = createKtorServer(CIO, host, port, configurators) | ||||||
|   | |||||||
| @@ -24,3 +24,8 @@ dependencies { | |||||||
| } | } | ||||||
|  |  | ||||||
| mainClassName="MainKt" | mainClassName="MainKt" | ||||||
|  |  | ||||||
|  | java { | ||||||
|  |     sourceCompatibility = JavaVersion.VERSION_1_8 | ||||||
|  |     targetCompatibility = JavaVersion.VERSION_1_8 | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | @file:Suppress("SERIALIZER_TYPE_INCOMPATIBLE") | ||||||
|  |  | ||||||
| package dev.inmo.micro_utils.language_codes | package dev.inmo.micro_utils.language_codes | ||||||
|  |  | ||||||
| import kotlinx.serialization.Serializable | import kotlinx.serialization.Serializable | ||||||
|   | |||||||
							
								
								
									
										52
									
								
								mime_types/mimes_generator/mime_generator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								mime_types/mimes_generator/mime_generator.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | import requests | ||||||
|  | from bs4 import BeautifulSoup | ||||||
|  | import pandas as pd | ||||||
|  | import itertools | ||||||
|  |  | ||||||
|  | def fix_name(category, raw_name): | ||||||
|  |     splitted = raw_name.replace('-', '+').replace('.', '+').replace(',', '+').split('+') | ||||||
|  |     out1 = "" | ||||||
|  |     for s in splitted: | ||||||
|  |         out1 += s.capitalize() | ||||||
|  |  | ||||||
|  |     result = "" | ||||||
|  |     if out1[0].isdigit(): | ||||||
|  |         result += category[0].capitalize() | ||||||
|  |         result += out1 | ||||||
|  |     else: | ||||||
|  |         result += out1 | ||||||
|  |     return result | ||||||
|  |  | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     df = pd.read_html(open('table.html', 'r')) | ||||||
|  |     mimes = [] | ||||||
|  |     for row in df[0].iterrows(): | ||||||
|  |         mime = row[1][1] | ||||||
|  |         mime_category = mime.split('/', 1)[0] | ||||||
|  |         mime_name = mime.split('/', 1)[1] | ||||||
|  |         mimes.append({ | ||||||
|  |             'mime_category': mime_category, | ||||||
|  |             'mime_name': mime_name, | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |     # codegen | ||||||
|  |  | ||||||
|  |     mimes.sort(key=lambda x: x['mime_category']) | ||||||
|  |     grouped = itertools.groupby(mimes, lambda x: x['mime_category']) | ||||||
|  |     code = '' | ||||||
|  |     code2 = 'internal val knownMimeTypes: Set<MimeType> = setOf(\n' | ||||||
|  |     code2 += '    KnownMimeTypes.Any,\n' | ||||||
|  |     for key, group in grouped: | ||||||
|  |         group_name = key.capitalize() | ||||||
|  |         code += '@Serializable(MimeTypeSerializer::class)\nsealed class %s(raw: String) : MimeType, KnownMimeTypes(raw) {\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']) | ||||||
|  |             code2 += '    KnownMimeTypes.%s.%s,\n' % (group_name, name) | ||||||
|  |         code += '}\n\n' | ||||||
|  |     code2 += ')\n' | ||||||
|  |     with open('out1.txt', 'w') as file: | ||||||
|  |         file.write(code) | ||||||
|  |     with open('out2.txt', 'w') as file: | ||||||
|  |         file.write(code2) | ||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | @file:Suppress("SERIALIZER_TYPE_INCOMPATIBLE") | ||||||
|  |  | ||||||
| package dev.inmo.micro_utils.mime_types | package dev.inmo.micro_utils.mime_types | ||||||
|  |  | ||||||
| import kotlinx.serialization.Serializable | import kotlinx.serialization.Serializable | ||||||
|   | |||||||
| @@ -26,7 +26,6 @@ kotlin { | |||||||
| apply from: "$defaultAndroidSettingsPresetPath" | apply from: "$defaultAndroidSettingsPresetPath" | ||||||
|  |  | ||||||
| java { | java { | ||||||
|     toolchain { |     sourceCompatibility = JavaVersion.VERSION_1_8 | ||||||
|         languageVersion = JavaLanguageVersion.of(8) |     targetCompatibility = JavaVersion.VERSION_1_8 | ||||||
|     } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,7 +4,13 @@ project.group = "$group" | |||||||
| apply from: "$publishGradlePath" | apply from: "$publishGradlePath" | ||||||
|  |  | ||||||
| kotlin { | kotlin { | ||||||
|     jvm() |     jvm { | ||||||
|  |         compilations.main { | ||||||
|  |             kotlinOptions { | ||||||
|  |                 jvmTarget = "1.8" | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     sourceSets { |     sourceSets { | ||||||
|         commonMain { |         commonMain { | ||||||
| @@ -28,7 +34,6 @@ kotlin { | |||||||
| } | } | ||||||
|  |  | ||||||
| java { | java { | ||||||
|     toolchain { |     sourceCompatibility = JavaVersion.VERSION_1_8 | ||||||
|         languageVersion = JavaLanguageVersion.of(8) |     targetCompatibility = JavaVersion.VERSION_1_8 | ||||||
|     } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,7 +4,13 @@ project.group = "$group" | |||||||
| apply from: "$publishGradlePath" | apply from: "$publishGradlePath" | ||||||
|  |  | ||||||
| kotlin { | kotlin { | ||||||
|     jvm() |     jvm { | ||||||
|  |         compilations.main { | ||||||
|  |             kotlinOptions { | ||||||
|  |                 jvmTarget = "1.8" | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|     js (IR) { |     js (IR) { | ||||||
|         browser() |         browser() | ||||||
|         nodejs() |         nodejs() | ||||||
| @@ -50,7 +56,6 @@ kotlin { | |||||||
| apply from: "$defaultAndroidSettingsPresetPath" | apply from: "$defaultAndroidSettingsPresetPath" | ||||||
|  |  | ||||||
| java { | java { | ||||||
|     toolchain { |     sourceCompatibility = JavaVersion.VERSION_1_8 | ||||||
|         languageVersion = JavaLanguageVersion.of(8) |     targetCompatibility = JavaVersion.VERSION_1_8 | ||||||
|     } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,3 +5,13 @@ plugins { | |||||||
| } | } | ||||||
|  |  | ||||||
| apply from: "$mppProjectWithSerializationPresetPath" | apply from: "$mppProjectWithSerializationPresetPath" | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     sourceSets { | ||||||
|  |         commonMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api project(":micro_utils.common") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| package dev.inmo.micro_utils.pagination | package dev.inmo.micro_utils.pagination | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.intersect | ||||||
| import kotlin.math.ceil | import kotlin.math.ceil | ||||||
| import kotlin.math.floor | import kotlin.math.floor | ||||||
|  |  | ||||||
| @@ -9,7 +10,7 @@ import kotlin.math.floor | |||||||
|  * If you want to request something, you should use [SimplePagination]. If you need to return some result including |  * If you want to request something, you should use [SimplePagination]. If you need to return some result including | ||||||
|  * pagination - [PaginationResult] |  * pagination - [PaginationResult] | ||||||
|  */ |  */ | ||||||
| interface Pagination { | interface Pagination : ClosedRange<Int> { | ||||||
|     /** |     /** | ||||||
|      * Started with 0. |      * Started with 0. | ||||||
|      * Number of page inside of pagination. Offset can be calculated as [page] * [size] |      * Number of page inside of pagination. Offset can be calculated as [page] * [size] | ||||||
| @@ -20,6 +21,17 @@ interface Pagination { | |||||||
|      * Size of current page. Offset can be calculated as [page] * [size] |      * Size of current page. Offset can be calculated as [page] * [size] | ||||||
|      */ |      */ | ||||||
|     val size: Int |     val size: Int | ||||||
|  |  | ||||||
|  |     override val start: Int | ||||||
|  |         get() = page * size | ||||||
|  |     override val endInclusive: Int | ||||||
|  |         get() = start + size - 1 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Pagination.intersect( | ||||||
|  |     other: Pagination | ||||||
|  | ): Pagination? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let { | ||||||
|  |     PaginationByIndexes(it.first, it.second) | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -32,7 +44,7 @@ inline val Pagination.isFirstPage | |||||||
|  * First number in index of objects. It can be used as offset for databases or other data sources |  * First number in index of objects. It can be used as offset for databases or other data sources | ||||||
|  */ |  */ | ||||||
| val Pagination.firstIndex: Int | val Pagination.firstIndex: Int | ||||||
|     get() = page * size |     get() = start | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Last number in index of objects. In fact, one [Pagination] object represent data in next range: |  * Last number in index of objects. In fact, one [Pagination] object represent data in next range: | ||||||
| @@ -41,7 +53,7 @@ val Pagination.firstIndex: Int | |||||||
|  * you will retrieve [Pagination.firstIndex] == 10 and [Pagination.lastIndex] == 19. Here [Pagination.lastIndexExclusive] == 20 |  * you will retrieve [Pagination.firstIndex] == 10 and [Pagination.lastIndex] == 19. Here [Pagination.lastIndexExclusive] == 20 | ||||||
|  */ |  */ | ||||||
| val Pagination.lastIndexExclusive: Int | val Pagination.lastIndexExclusive: Int | ||||||
|     get() = firstIndex + size |     get() = endInclusive + 1 | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Last number in index of objects. In fact, one [Pagination] object represent data in next range: |  * Last number in index of objects. In fact, one [Pagination] object represent data in next range: | ||||||
| @@ -50,7 +62,7 @@ val Pagination.lastIndexExclusive: Int | |||||||
|  * you will retrieve [Pagination.firstIndex] == 10 and [Pagination.lastIndex] == 19. |  * you will retrieve [Pagination.firstIndex] == 10 and [Pagination.lastIndex] == 19. | ||||||
|  */ |  */ | ||||||
| val Pagination.lastIndex: Int | val Pagination.lastIndex: Int | ||||||
|     get() = lastIndexExclusive - 1 |     get() = endInclusive | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Calculates pages count for given [datasetSize] |  * Calculates pages count for given [datasetSize] | ||||||
|   | |||||||
| @@ -16,6 +16,16 @@ suspend fun <T> getAll( | |||||||
|     return results.toList() |     return results.toList() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | suspend fun <T, R> R.getAllBy( | ||||||
|  |     initialPagination: Pagination = FirstPagePagination(), | ||||||
|  |     paginationMapper: R.(PaginationResult<T>) -> Pagination?, | ||||||
|  |     block: suspend R.(Pagination) -> PaginationResult<T> | ||||||
|  | ): List<T> = getAll( | ||||||
|  |     initialPagination, | ||||||
|  |     { paginationMapper(it) }, | ||||||
|  |     { block(it) } | ||||||
|  | ) | ||||||
|  |  | ||||||
| suspend fun <T> getAllWithNextPaging( | suspend fun <T> getAllWithNextPaging( | ||||||
|     initialPagination: Pagination = FirstPagePagination(), |     initialPagination: Pagination = FirstPagePagination(), | ||||||
|     block: suspend (Pagination) -> PaginationResult<T> |     block: suspend (Pagination) -> PaginationResult<T> | ||||||
| @@ -25,6 +35,14 @@ suspend fun <T> getAllWithNextPaging( | |||||||
|     block |     block | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | suspend fun <T, R> R.getAllByWithNextPaging( | ||||||
|  |     initialPagination: Pagination = FirstPagePagination(), | ||||||
|  |     block: suspend R.(Pagination) -> PaginationResult<T> | ||||||
|  | ): List<T> = getAllWithNextPaging( | ||||||
|  |     initialPagination, | ||||||
|  |     { block(it) } | ||||||
|  | ) | ||||||
|  |  | ||||||
| suspend fun <T> getAllWithCurrentPaging( | suspend fun <T> getAllWithCurrentPaging( | ||||||
|     initialPagination: Pagination = FirstPagePagination(), |     initialPagination: Pagination = FirstPagePagination(), | ||||||
|     block: suspend (Pagination) -> PaginationResult<T> |     block: suspend (Pagination) -> PaginationResult<T> | ||||||
| @@ -33,3 +51,11 @@ suspend fun <T> getAllWithCurrentPaging( | |||||||
|     { it.currentPageIfNotEmpty() }, |     { it.currentPageIfNotEmpty() }, | ||||||
|     block |     block | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | suspend fun <T, R> R.getAllByWithCurrentPaging( | ||||||
|  |     initialPagination: Pagination = FirstPagePagination(), | ||||||
|  |     block: suspend R.(Pagination) -> PaginationResult<T> | ||||||
|  | ): List<T> = getAllWithCurrentPaging( | ||||||
|  |     initialPagination, | ||||||
|  |     { block(it) } | ||||||
|  | ) | ||||||
|   | |||||||
| @@ -6,13 +6,15 @@ import dev.inmo.micro_utils.repos.* | |||||||
| import kotlinx.coroutines.flow.* | import kotlinx.coroutines.flow.* | ||||||
|  |  | ||||||
| abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType>( | abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType>( | ||||||
|     helper: StandardSQLHelper |     helper: StandardSQLHelper, | ||||||
|  |     replyInFlows: Int = 0, | ||||||
|  |     extraBufferCapacityInFlows: Int = 64 | ||||||
| ) : WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>, | ) : WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>, | ||||||
|     AbstractAndroidCRUDRepo<ObjectType, IdType>(helper), |     AbstractAndroidCRUDRepo<ObjectType, IdType>(helper), | ||||||
|     StandardCRUDRepo<ObjectType, IdType, InputValueType> { |     StandardCRUDRepo<ObjectType, IdType, InputValueType> { | ||||||
|     protected val newObjectsChannel = MutableSharedFlow<ObjectType>(64) |     protected val newObjectsChannel = MutableSharedFlow<ObjectType>(replyInFlows, extraBufferCapacityInFlows) | ||||||
|     protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(64) |     protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(replyInFlows, extraBufferCapacityInFlows) | ||||||
|     protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(64) |     protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(replyInFlows, extraBufferCapacityInFlows) | ||||||
|     override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asSharedFlow() |     override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asSharedFlow() | ||||||
|     override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow() |     override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow() | ||||||
|     override val deletedObjectsIdsFlow: Flow<IdType> = deleteObjectsIdsChannel.asSharedFlow() |     override val deletedObjectsIdsFlow: Flow<IdType> = deleteObjectsIdsChannel.asSharedFlow() | ||||||
|   | |||||||
| @@ -10,15 +10,16 @@ import org.jetbrains.exposed.sql.transactions.transaction | |||||||
|  |  | ||||||
| abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>( | abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>( | ||||||
|     flowsChannelsSize: Int = 0, |     flowsChannelsSize: Int = 0, | ||||||
|     tableName: String = "" |     tableName: String = "", | ||||||
|  |     replyCacheInFlows: Int = 0 | ||||||
| ) : | ) : | ||||||
|     AbstractExposedReadCRUDRepo<ObjectType, IdType>(tableName), |     AbstractExposedReadCRUDRepo<ObjectType, IdType>(tableName), | ||||||
|     ExposedCRUDRepo<ObjectType, IdType>, |     ExposedCRUDRepo<ObjectType, IdType>, | ||||||
|     WriteStandardCRUDRepo<ObjectType, IdType, InputValueType> |     WriteStandardCRUDRepo<ObjectType, IdType, InputValueType> | ||||||
| { | { | ||||||
|     protected val newObjectsChannel = MutableSharedFlow<ObjectType>(flowsChannelsSize) |     protected val newObjectsChannel = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize) | ||||||
|     protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(flowsChannelsSize) |     protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize) | ||||||
|     protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(flowsChannelsSize) |     protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(replyCacheInFlows, flowsChannelsSize) | ||||||
|  |  | ||||||
|     override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asSharedFlow() |     override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asSharedFlow() | ||||||
|     override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow() |     override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow() | ||||||
|   | |||||||
| @@ -19,8 +19,8 @@ open class ExposedKeyValueRepo<Key, Value>( | |||||||
|     valueColumnAllocator, |     valueColumnAllocator, | ||||||
|     tableName |     tableName | ||||||
| ) { | ) { | ||||||
|     private val _onNewValue = MutableSharedFlow<Pair<Key, Value>>() |     protected val _onNewValue = MutableSharedFlow<Pair<Key, Value>>() | ||||||
|     private val _onValueRemoved = MutableSharedFlow<Key>() |     protected val _onValueRemoved = MutableSharedFlow<Key>() | ||||||
|  |  | ||||||
|     override val onNewValue: Flow<Pair<Key, Value>> = _onNewValue.asSharedFlow() |     override val onNewValue: Flow<Pair<Key, Value>> = _onNewValue.asSharedFlow() | ||||||
|     override val onValueRemoved: Flow<Key> = _onValueRemoved.asSharedFlow() |     override val onValueRemoved: Flow<Key> = _onValueRemoved.asSharedFlow() | ||||||
|   | |||||||
| @@ -20,18 +20,6 @@ open class TypedSerializer<T : Any>( | |||||||
|         element("type", String.serializer().descriptor) |         element("type", String.serializer().descriptor) | ||||||
|         element("value", ContextualSerializer(kClass).descriptor) |         element("value", ContextualSerializer(kClass).descriptor) | ||||||
|     } |     } | ||||||
|     @InternalSerializationApi |  | ||||||
|     @Deprecated( |  | ||||||
|         "This descriptor was deprecated due to incorrect serial name. You may use it in case something require it, " + |  | ||||||
|             "but it is strongly recommended to migrate onto new descriptor" |  | ||||||
|     ) |  | ||||||
|     protected val oldDescriptor: SerialDescriptor = buildSerialDescriptor( |  | ||||||
|         "TextSourceSerializer", |  | ||||||
|         SerialKind.CONTEXTUAL |  | ||||||
|     ) { |  | ||||||
|         element("type", String.serializer().descriptor) |  | ||||||
|         element("value", ContextualSerializer(kClass).descriptor) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @ExperimentalSerializationApi |     @ExperimentalSerializationApi | ||||||
|     @InternalSerializationApi |     @InternalSerializationApi | ||||||
| @@ -98,3 +86,7 @@ operator fun <T : Any> TypedSerializer<T>.minusAssign(kClass: KClass<T>) { | |||||||
| inline fun <reified T : Any> TypedSerializer( | inline fun <reified T : Any> TypedSerializer( | ||||||
|     presetSerializers: Map<String, KSerializer<out T>> = emptyMap() |     presetSerializers: Map<String, KSerializer<out T>> = emptyMap() | ||||||
| ) = TypedSerializer(T::class, presetSerializers) | ) = TypedSerializer(T::class, presetSerializers) | ||||||
|  |  | ||||||
|  | inline fun <reified T : Any> TypedSerializer( | ||||||
|  |     vararg presetSerializers: Pair<String, KSerializer<out T>> | ||||||
|  | ) = TypedSerializer(presetSerializers.toMap()) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user