mirror of
				https://github.com/InsanusMokrassar/MicroUtils.git
				synced 2025-10-30 19:50:31 +00:00 
			
		
		
		
	Compare commits
	
		
			161 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 216c03205c | |||
| ab112aa7a4 | |||
| d85b3d0da9 | |||
| 67b9a03366 | |||
| 3ac56dcfd3 | |||
| d1021d283a | |||
| 97ed973cb5 | |||
| 0b48afd251 | |||
| 19857930a4 | |||
| d0dbe3ed2f | |||
| 8b7e78b63a | |||
| 92a4ecb523 | |||
| 6a5ad4d728 | |||
| be4aa8daac | |||
| b5eac37782 | |||
| b1ad3c5a39 | |||
| ba16bad029 | |||
| ca8ae4cd72 | |||
| 53d35d74b3 | |||
| 49c139e235 | |||
| caf9c821f3 | |||
| ca4c6db96f | |||
| 6b2298c752 | |||
| a1bf43def9 | |||
| 15e9254e00 | |||
| afe5a72c6f | |||
| 750a8b9ecf | |||
| 27fc3f93e0 | |||
| 8166d4b99b | |||
| b61d2ae2eb | |||
| 4790fe0aea | |||
| bc37b11cee | |||
| 223fed910f | |||
| b85ab7b061 | |||
| 888dc299c9 | |||
| e113dc28ed | |||
| 31e55d2307 | |||
| e90645f248 | |||
| 4bb7ba2571 | |||
| 8d31c25bf8 | |||
| c7ee1c28b2 | |||
| 99b09c8b28 | |||
| a328c4425a | |||
| c0f61ca896 | |||
| 86e70c0961 | |||
| d87a3a039f | |||
| 6279a2c40a | |||
| f377ebea88 | |||
| 7373fef964 | |||
| 41ef86dbda | |||
| 7bc2b2336d | |||
| 0a615e6d78 | |||
| 8f928e16e1 | |||
| 72bc8da1e7 | |||
| 51e349a5db | |||
| 3cbb19ba2c | |||
| ae3a5bf45d | |||
| 9c667f4b78 | |||
| 21195e1bcb | |||
| 03117ac565 | |||
| d13fbdf176 | |||
| 7cecc0e0b6 | |||
| 203e781f5d | |||
| 3eb6cd77cd | |||
| 51855b2405 | |||
| 00cc26d874 | |||
| 02520636ad | |||
| 76813fae8e | |||
| 4693483c2b | |||
| 6dbd12df59 | |||
| 9e84dc5031 | |||
| 8f790360bc | |||
| 808375cea6 | |||
| d9c15db9de | |||
| cf2be8ed43 | |||
| 97339c9b1d | |||
| eb35903de9 | |||
| 43e88e00da | |||
| 1e51f9d77b | |||
| dad738357c | |||
| cb828ab3f2 | |||
| aefaf4a8cc | |||
| b29f37a251 | |||
| 88854020ac | |||
| 14ebe01fc6 | |||
| 771aed0f0f | |||
| 16c57fcd6a | |||
| 75397a7ccb | |||
| b05404f828 | |||
| edea942874 | |||
| 8a8f568b9a | |||
| 4dc8d30c52 | |||
| cb4e08e823 | |||
| c443bf4fa0 | |||
| a6982de822 | |||
| 4f1a663e75 | |||
| dab262d626 | |||
| 87a3f61ca6 | |||
| 506e937a68 | |||
| 5a037c76dd | |||
| 313f622f7e | |||
| 6cba1fe1a2 | |||
| fd2d0e80b7 | |||
| 96ab2e8aca | |||
| 0202988cae | |||
| d619d59947 | |||
| 85b3e48d18 | |||
| 7a9b7d98a1 | |||
| b212acfcaf | |||
| 3a45e5dc70 | |||
| 73190518d5 | |||
| 03f78180dc | |||
| 1c0b8cf842 | |||
| a1624ea2a9 | |||
| 23a050cf1e | |||
| 916f2f96f4 | |||
| 00cc214754 | |||
| b2e38f72b9 | |||
| e7107d238d | |||
| ed9ebdbd1a | |||
| e80676d3d2 | |||
| 02d02fa8f2 | |||
| bd783fb74f | |||
| 50386adf70 | |||
| f4ee6c2890 | |||
| d45aef9fe5 | |||
| a56cd3dddd | |||
| 419e7070ee | |||
| 612cf40b5f | |||
| 8b39882e83 | |||
| e639ae172b | |||
| d0446850ae | |||
| c48465b90b | |||
| f419fd03d2 | |||
| 494812a660 | |||
| eb78f21eec | |||
| 4bda70268b | |||
| f037ce4371 | |||
| 3d2196e35d | |||
| a74f061b02 | |||
| 11ade14676 | |||
| eb562d8784 | |||
| 1ee5b4bfd4 | |||
| d97892080b | |||
| 6f37125724 | |||
| ed1baaade7 | |||
| bb9669f8fd | |||
| bdac715d48 | |||
| acf4971298 | |||
| 249bc83a8c | |||
| 0fbb92f03f | |||
| ca27cb3f82 | |||
| 3a5771a0cc | |||
| 527a2a91ac | |||
| 6763e5c4c6 | |||
| 06918d8310 | |||
| 89ccaa1b57 | |||
| 5d0bdb9bcf | |||
| 31fdcf74a5 | |||
| afca09cc1d | |||
| 531d89d9db | 
							
								
								
									
										6
									
								
								.github/workflows/dokka_push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/dokka_push.yml
									
									
									
									
										vendored
									
									
								
							| @@ -10,10 +10,10 @@ jobs: | |||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v2 | ||||||
|       - uses: actions/setup-java@v1 |       - uses: actions/setup-java@v1 | ||||||
|         with: |         with: | ||||||
|           java-version: 1.8 |           java-version: 11 | ||||||
|       - name: Fix android 31.0.0 dx |       - name: Fix android 32.0.0 dx | ||||||
|         continue-on-error: true |         continue-on-error: true | ||||||
|         run: cd /usr/local/lib/android/sdk/build-tools/31.0.0/ && mv d8 dx && cd lib  && mv d8.jar dx.jar |         run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib  && mv d8.jar dx.jar | ||||||
|       - name: Build |       - name: Build | ||||||
|         run: ./gradlew dokkaHtml |         run: ./gradlew dokkaHtml | ||||||
|       - name: Publish KDocs |       - name: Publish KDocs | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								.github/workflows/packages_push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/packages_push.yml
									
									
									
									
										vendored
									
									
								
							| @@ -8,10 +8,10 @@ jobs: | |||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v2 | ||||||
|       - uses: actions/setup-java@v1 |       - uses: actions/setup-java@v1 | ||||||
|         with: |         with: | ||||||
|           java-version: 1.8 |           java-version: 11 | ||||||
|       - name: Fix android 31.0.0 dx |       - name: Fix android 32.0.0 dx | ||||||
|         continue-on-error: true |         continue-on-error: true | ||||||
|         run: cd /usr/local/lib/android/sdk/build-tools/31.0.0/ && mv d8 dx && cd lib  && mv d8.jar dx.jar |         run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib  && mv d8.jar dx.jar | ||||||
|       - name: Rewrite version |       - name: Rewrite version | ||||||
|         run: | |         run: | | ||||||
|           branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" |           branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" | ||||||
| @@ -22,7 +22,7 @@ jobs: | |||||||
|         run: ./gradlew build |         run: ./gradlew build | ||||||
|       - name: Publish |       - name: Publish | ||||||
|         continue-on-error: true |         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 | ||||||
|         env: |         env: | ||||||
|           GITHUBPACKAGES_USER: ${{ github.actor }} |           GITHUBPACKAGES_USER: ${{ github.actor }} | ||||||
|           GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} |           GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -11,5 +11,6 @@ out/ | |||||||
|  |  | ||||||
| secret.gradle | secret.gradle | ||||||
| local.properties | local.properties | ||||||
|  | kotlin-js-store | ||||||
|  |  | ||||||
| publishing.sh | publishing.sh | ||||||
|   | |||||||
							
								
								
									
										257
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										257
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,5 +1,262 @@ | |||||||
| # Changelog | # Changelog | ||||||
|  |  | ||||||
|  | ## 0.9.25 | ||||||
|  |  | ||||||
|  | ## 0.9.24 | ||||||
|  |  | ||||||
|  | * `Ktor`: | ||||||
|  |     * `Common`: | ||||||
|  |         * New extension fun `MPPFile#input` | ||||||
|  |  | ||||||
|  | ## 0.9.23 | ||||||
|  |  | ||||||
|  | * `Repos`: | ||||||
|  |     * `Exposed`: | ||||||
|  |         * New property `ExposedRepo#selectAll` to retrieve all the rows in the table | ||||||
|  |  | ||||||
|  | ## 0.9.22 | ||||||
|  |  | ||||||
|  | * `Ktor`: | ||||||
|  |     * `Server`: | ||||||
|  |         * Now `createKtorServer` fun is fully customizable | ||||||
|  |  | ||||||
|  | ## 0.9.21 | ||||||
|  |  | ||||||
|  | * `Repos`: | ||||||
|  |     * `Exposed`: | ||||||
|  |         * fixes in `AbstractExposedWriteCRUDRepo` | ||||||
|  |  | ||||||
|  | ## 0.9.20 | ||||||
|  |  | ||||||
|  | * `Repos`: | ||||||
|  |     * `Common`: | ||||||
|  |         * Fixes in `OneToManyAndroidRepo` | ||||||
|  |         * New `CursorIterator` | ||||||
|  |  | ||||||
|  | ## 0.9.19 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Coroutines`: `1.6.0` -> `1.6.1` | ||||||
|  | * `Repos`: | ||||||
|  |     * `Exposed`: | ||||||
|  |         * Fixes in `ExposedStandardVersionsRepoProxy` | ||||||
|  |  | ||||||
|  | ## 0.9.18 | ||||||
|  |  | ||||||
|  | * `Common` | ||||||
|  |     * New extensions for `Element`: `Element#onActionOutside` and `Element#onClickOutside` | ||||||
|  |  | ||||||
|  | ## 0.9.17 | ||||||
|  |  | ||||||
|  | * `Common`: | ||||||
|  |     * New extensions `Element#onVisibilityChanged`, `Element#onVisible` and `Element#onInvisible` | ||||||
|  | * `Coroutines`: | ||||||
|  |     * New extension `Element.visibilityFlow()` | ||||||
|  | * `FSM`: | ||||||
|  |     * Now it is possible to resolve conflicts on `startChain` | ||||||
|  |  | ||||||
|  | ## 0.9.16 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Klock`: `2.6.3` -> `2.7.0` | ||||||
|  | * `Common`: | ||||||
|  |     * New extension `Node#onRemoved` | ||||||
|  |     * `Compose`: | ||||||
|  |         * New extension `Composition#linkWithRoot` for removing of composition with root element | ||||||
|  | * `Coroutines`: | ||||||
|  |     * `Compose`: | ||||||
|  |         * New function `renderComposableAndLinkToContextAndRoot` with linking of composition to root element | ||||||
|  |  | ||||||
|  | ## 0.9.15 | ||||||
|  |  | ||||||
|  | * `FSM`: | ||||||
|  |     * Rename `DefaultUpdatableStatesMachine#compare` to `DefaultUpdatableStatesMachine#shouldReplaceJob` | ||||||
|  |     * `DefaultStatesManager` now is extendable | ||||||
|  |     * `DefaultStatesMachine` will stop all jobs of states which was removed from `statesManager` | ||||||
|  |  | ||||||
|  | ## 0.9.14 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Klock`: `2.6.2` -> `2.6.3` | ||||||
|  |     * `Ktor`: `1.6.7` -> `1.6.8` | ||||||
|  | * `Ktor`: | ||||||
|  |     * Add temporal files uploading functionality (for clients to upload and for server to receive) | ||||||
|  |  | ||||||
|  | ## 0.9.13 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Compose`: `1.1.0` -> `1.1.1` | ||||||
|  |  | ||||||
|  | ## 0.9.12 | ||||||
|  |  | ||||||
|  | * `Common`: | ||||||
|  |     * `JS`: | ||||||
|  |         * New function `openLink` | ||||||
|  |         * New function `selectFile` | ||||||
|  |         * New function `triggerDownloadFile` | ||||||
|  |     * `Compose`: | ||||||
|  |         * Created :) | ||||||
|  |         * `Common`: | ||||||
|  |             * `DefaultDisposableEffectResult` as a default realization of `DisposableEffectResult` | ||||||
|  |         * `JS`: | ||||||
|  |             * `openLink` on top of `openLink` with `String` target from common | ||||||
|  | * `Coroutines`: | ||||||
|  |     * `Compose`: | ||||||
|  |         * `Common`: | ||||||
|  |             * New extension `Flow.toMutableState` | ||||||
|  |             * New extension `StateFlow.toMutableState` | ||||||
|  |         * `JS`: | ||||||
|  |             * New function `selectFileOrThrow` on top of `selectFile` from `common` | ||||||
|  |             * New function `selectFileOrNull` on top of `selectFile` from `common` | ||||||
|  |  | ||||||
|  | ## 0.9.11 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Klock`: `2.6.1` -> `2.6.2` | ||||||
|  | * `Coroutines`: | ||||||
|  |     * `Compose`: | ||||||
|  |         * Created :) | ||||||
|  |         * New extensions and function: | ||||||
|  |             * `Composition#linkWithJob` | ||||||
|  |             * `Composition#linkWithContext` | ||||||
|  |             * `renderComposableAndLinkToContext` | ||||||
|  |  | ||||||
|  | ## 0.9.10 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Klock`: `2.5.2` -> `2.6.1` | ||||||
|  | * Ktor: | ||||||
|  |     * Client: | ||||||
|  |         * New function `UnifiedRequester#createStandardWebsocketFlow` without `checkReconnection` arg | ||||||
|  |     * Server: | ||||||
|  |         * Now it is possible to filter data in `Route#includeWebsocketHandling` | ||||||
|  |         * Callback in `Route#includeWebsocketHandling` and dependent methods is `suspend` since now | ||||||
|  |         * Add `URLProtocol` support in `Route#includeWebsocketHandling` and dependent methods | ||||||
|  |  | ||||||
|  | ## 0.9.9 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Klock`: `2.5.1` -> `2.5.2` | ||||||
|  | * `Common`: | ||||||
|  |     * Add new diff tool - `applyDiff` | ||||||
|  |     * Implementation of `IntersectionObserver` in JS part (copypaste of [this](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0) comment) | ||||||
|  |  | ||||||
|  | ## 0.9.8 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Exposed`: `0.37.2` -> `0.37.3` | ||||||
|  |     * `Klock`: `2.4.13` -> `2.5.1` | ||||||
|  |     * `AppCompat`: `1.4.0` -> `1.4.1` | ||||||
|  |  | ||||||
|  | ## 0.9.7 | ||||||
|  |  | ||||||
|  | * `Repos`: | ||||||
|  |     * `Exposed`: | ||||||
|  |         * Fix in `ExposedOneToManyKeyValueRepo` - now it will not use `insertIgnore` | ||||||
|  | * `Ktor`: | ||||||
|  |     * `Server`: | ||||||
|  |         * `Route#includeWebsocketHandling` now will check that `WebSockets` feature and install it if not | ||||||
|  |  | ||||||
|  | ## 0.9.6 | ||||||
|  |  | ||||||
|  | * `Repos`: | ||||||
|  |     * `Exposed`: | ||||||
|  |         * Fix in `ExposedOneToManyKeyValueRepo` - now it will not use `deleteIgnoreWhere` | ||||||
|  |  | ||||||
|  | ## 0.9.5 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Klock`: `2.4.12` -> `2.4.13` | ||||||
|  |  | ||||||
|  | ## 0.9.4 | ||||||
|  |  | ||||||
|  | * `Pagination`: | ||||||
|  |     * `Common`: | ||||||
|  |         * Add several `optionallyReverse` functions | ||||||
|  | * `Common`: | ||||||
|  |     * Changes in `Either`: | ||||||
|  |         * Now `Either` uses `optionalT1` and `optionalT2` as main properties | ||||||
|  |         * `Either#t1` and `Either#t2` are deprecated | ||||||
|  |         * New extensions `Either#mapOnFirst` and `Either#mapOnSecond` | ||||||
|  |  | ||||||
|  | ## 0.9.3 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `UUID`: `0.3.1` -> `0.4.0` | ||||||
|  |  | ||||||
|  | ## 0.9.2 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Klock`: `2.4.10` -> `2.4.12` | ||||||
|  |  | ||||||
|  | ## 0.9.1 | ||||||
|  |  | ||||||
|  | * `Repos`: | ||||||
|  |     * `Exposed`: | ||||||
|  |         * Default realizations of standard interfaces for exposed DB are using public fields for now: | ||||||
|  |             * `ExposedReadKeyValueRepo` | ||||||
|  |             * `ExposedReadOneToManyKeyValueRepo` | ||||||
|  |             * `ExposedStandardVersionsRepoProxy` | ||||||
|  |         * New typealiases for one to many exposed realizations: | ||||||
|  |             * `ExposedReadKeyValuesRepo` | ||||||
|  |             * `ExposedKeyValuesRepo` | ||||||
|  |  | ||||||
|  | ## 0.9.0 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `Kotlin`: `1.5.31` -> `1.6.10` | ||||||
|  |     * `Coroutines`: `1.5.2` -> `1.6.0` | ||||||
|  |     * `Serialization`: `1.3.1` -> `1.3.2` | ||||||
|  |     * `Exposed`: `0.36.2` -> `0.37.2` | ||||||
|  |     * `Ktor`: `1.6.5` -> `1.6.7` | ||||||
|  |     * `Klock`: `2.4.8` -> `2.4.10` | ||||||
|  |  | ||||||
|  | ## 0.8.9 | ||||||
|  |  | ||||||
|  | * `Ktor`: | ||||||
|  |     * `Server`: | ||||||
|  |         * Fixes in `uniloadMultipart` | ||||||
|  |     * `Client`: | ||||||
|  |         * Fixes in `unimultipart` | ||||||
|  | * `FSM`: | ||||||
|  |     * Fixes in `DefaultUpdatableStatesMachine` | ||||||
|  |  | ||||||
|  | ## 0.8.8 | ||||||
|  |  | ||||||
|  | * `Versions`: | ||||||
|  |     * `AppCompat`: `1.3.1` -> `1.4.0` | ||||||
|  |     * Android Compile SDK: `31.0.0` -> `32.0.0` | ||||||
|  | * `FSM`: | ||||||
|  |     * `DefaultStatesMachine` now is extendable | ||||||
|  |     * New type `UpdatableStatesMachine` with default realization`DefaultUpdatableStatesMachine` | ||||||
|  |  | ||||||
|  | ## 0.8.7 | ||||||
|  |  | ||||||
|  | * `Ktor`: | ||||||
|  |     * `Client`: | ||||||
|  |         * `UnifiedRequester` now have no private fields | ||||||
|  |         * Add preview work with multipart | ||||||
|  |     * `Server` | ||||||
|  |         * `UnifiedRouter` now have no private fields | ||||||
|  |         * Add preview work with multipart | ||||||
|  |  | ||||||
|  | ## 0.8.6 | ||||||
|  |  | ||||||
|  | * `Common`: | ||||||
|  |     * `Either` extensions `onFirst` and `onSecond` now accept not `crossinline` callbacks | ||||||
|  |     * All `joinTo` now accept not `crossinline` callbacks | ||||||
|  |  | ||||||
|  | ## 0.8.5 | ||||||
|  |  | ||||||
|  | * `Common`: | ||||||
|  |     * `repeatOnFailure` | ||||||
|  |  | ||||||
|  | ## 0.8.4 | ||||||
|  |  | ||||||
|  | * `Ktor`: | ||||||
|  |     * `Server`: | ||||||
|  |         * Several new `createKtorServer` | ||||||
|  |  | ||||||
| ## 0.8.3 | ## 0.8.3 | ||||||
|  |  | ||||||
| * `Common`: | * `Common`: | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ kotlin { | |||||||
|     sourceSets { |     sourceSets { | ||||||
|         androidMain { |         androidMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "androidx.appcompat:appcompat-resources:$appcompat_version" |                 api libs.android.appCompat.resources | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -10,13 +10,13 @@ kotlin { | |||||||
|     sourceSets { |     sourceSets { | ||||||
|         commonMain { |         commonMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" |                 api libs.kt.coroutines | ||||||
|                 api project(":micro_utils.common") |                 api project(":micro_utils.common") | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         androidMain { |         androidMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "androidx.recyclerview:recyclerview:$androidx_recycler_version" |                 api libs.android.recyclerView | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								build.gradle
									
									
									
									
									
								
							| @@ -7,12 +7,12 @@ buildscript { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     dependencies { |     dependencies { | ||||||
|         classpath 'com.android.tools.build:gradle:4.1.3' |         classpath libs.buildscript.kt.gradle | ||||||
|         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" |         classpath libs.buildscript.kt.serialization | ||||||
|         classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" |         classpath libs.buildscript.jb.dokka | ||||||
|         classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version" |         classpath libs.buildscript.gh.release | ||||||
|         classpath "com.github.breadmoirai:github-release:$github_release_plugin_version" |         classpath libs.buildscript.android.gradle | ||||||
|         classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" |         classpath libs.buildscript.android.dexcount | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								common/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								common/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | plugins { | ||||||
|  |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|  |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "com.android.library" | ||||||
|  |     alias(libs.plugins.jb.compose) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$mppProjectWithSerializationAndComposePresetPath" | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     sourceSets { | ||||||
|  |         commonMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api project(":micro_utils.common") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,16 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.DisposableEffectResult | ||||||
|  |  | ||||||
|  | class DefaultDisposableEffectResult( | ||||||
|  |     private val onDispose: () -> Unit | ||||||
|  | ) : DisposableEffectResult { | ||||||
|  |     override fun dispose() { | ||||||
|  |         onDispose() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         val DoNothing = DefaultDisposableEffectResult {} | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.Composition | ||||||
|  | import dev.inmo.micro_utils.common.onRemoved | ||||||
|  | import org.w3c.dom.Element | ||||||
|  |  | ||||||
|  | fun Composition.linkWithElement(element: Element) { | ||||||
|  |     element.onRemoved { dispose() } | ||||||
|  | } | ||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import org.jetbrains.compose.web.attributes.ATarget | ||||||
|  |  | ||||||
|  | fun openLink(link: String, mode: ATarget = ATarget.Blank, features: String = "") = dev.inmo.micro_utils.common.openLink( | ||||||
|  |     link, | ||||||
|  |     mode.targetStr, | ||||||
|  |     features | ||||||
|  | ) | ||||||
|  |  | ||||||
| @@ -0,0 +1,13 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import org.jetbrains.compose.web.dom.DOMScope | ||||||
|  | import org.w3c.dom.Element | ||||||
|  |  | ||||||
|  | fun <TElement : Element> renderComposableAndLinkToRoot( | ||||||
|  |     root: TElement, | ||||||
|  |     monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock, | ||||||
|  |     content: @Composable DOMScope<TElement>.() -> Unit | ||||||
|  | ): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply { | ||||||
|  |     linkWithElement(root) | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								common/compose/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								common/compose/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest package="dev.inmo.micro_utils.common.compose"/> | ||||||
| @@ -153,3 +153,22 @@ inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDif | |||||||
| inline fun <T> Iterable<T>.calculateStrictDiff( | inline fun <T> Iterable<T>.calculateStrictDiff( | ||||||
|     other: Iterable<T> |     other: Iterable<T> | ||||||
| ) = calculateDiff(other, strictComparison = true) | ) = calculateDiff(other, strictComparison = true) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this] | ||||||
|  |  * mutable list | ||||||
|  |  */ | ||||||
|  | fun <T> MutableList<T>.applyDiff( | ||||||
|  |     source: Iterable<T>, | ||||||
|  |     strictComparison: Boolean = false | ||||||
|  | ) = calculateDiff(source, strictComparison).let { | ||||||
|  |     for (i in it.removed.indices.sortedDescending()) { | ||||||
|  |         removeAt(it.removed[i].index) | ||||||
|  |     } | ||||||
|  |     it.added.forEach { (i, t) -> | ||||||
|  |         add(i, t) | ||||||
|  |     } | ||||||
|  |     it.replaced.forEach { (_, new) -> | ||||||
|  |         set(new.index, new.value) | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import kotlinx.serialization.descriptors.* | |||||||
| import kotlinx.serialization.encoding.* | import kotlinx.serialization.encoding.* | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Realization of this interface will contains at least one not null - [t1] or [t2] |  * Realization of this interface will contains at least one not null - [optionalT1] or [optionalT2] | ||||||
|  * |  * | ||||||
|  * @see EitherFirst |  * @see EitherFirst | ||||||
|  * @see EitherSecond |  * @see EitherSecond | ||||||
| @@ -14,11 +14,19 @@ import kotlinx.serialization.encoding.* | |||||||
|  * @see Either.Companion.second |  * @see Either.Companion.second | ||||||
|  * @see Either.onFirst |  * @see Either.onFirst | ||||||
|  * @see Either.onSecond |  * @see Either.onSecond | ||||||
|  |  * @see Either.mapOnFirst | ||||||
|  |  * @see Either.mapOnSecond | ||||||
|  */ |  */ | ||||||
| @Serializable(EitherSerializer::class) | @Serializable(EitherSerializer::class) | ||||||
| sealed interface Either<T1, T2> { | sealed interface Either<T1, T2> { | ||||||
|  |     val optionalT1: Optional<T1> | ||||||
|  |     val optionalT2: Optional<T2> | ||||||
|  |     @Deprecated("Use optionalT1 instead", ReplaceWith("optionalT1")) | ||||||
|     val t1: T1? |     val t1: T1? | ||||||
|  |         get() = optionalT1.dataOrNull() | ||||||
|  |     @Deprecated("Use optionalT2 instead", ReplaceWith("optionalT2")) | ||||||
|     val t2: T2? |     val t2: T2? | ||||||
|  |         get() = optionalT2.dataOrNull() | ||||||
|  |  | ||||||
|     companion object { |     companion object { | ||||||
|         fun <T1, T2> serializer( |         fun <T1, T2> serializer( | ||||||
| @@ -32,8 +40,7 @@ class EitherSerializer<T1, T2>( | |||||||
|     t1Serializer: KSerializer<T1>, |     t1Serializer: KSerializer<T1>, | ||||||
|     t2Serializer: KSerializer<T2>, |     t2Serializer: KSerializer<T2>, | ||||||
| ) : KSerializer<Either<T1, T2>> { | ) : KSerializer<Either<T1, T2>> { | ||||||
|     @ExperimentalSerializationApi |     @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) | ||||||
|     @InternalSerializationApi |  | ||||||
|     override val descriptor: SerialDescriptor = buildSerialDescriptor( |     override val descriptor: SerialDescriptor = buildSerialDescriptor( | ||||||
|         "TypedSerializer", |         "TypedSerializer", | ||||||
|         SerialKind.CONTEXTUAL |         SerialKind.CONTEXTUAL | ||||||
| @@ -44,8 +51,6 @@ class EitherSerializer<T1, T2>( | |||||||
|     private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer) |     private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer) | ||||||
|     private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer) |     private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer) | ||||||
|  |  | ||||||
|     @ExperimentalSerializationApi |  | ||||||
|     @InternalSerializationApi |  | ||||||
|     override fun deserialize(decoder: Decoder): Either<T1, T2> { |     override fun deserialize(decoder: Decoder): Either<T1, T2> { | ||||||
|         return decoder.decodeStructure(descriptor) { |         return decoder.decodeStructure(descriptor) { | ||||||
|             var type: String? = null |             var type: String? = null | ||||||
| @@ -77,8 +82,6 @@ class EitherSerializer<T1, T2>( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     @ExperimentalSerializationApi |  | ||||||
|     @InternalSerializationApi |  | ||||||
|     override fun serialize(encoder: Encoder, value: Either<T1, T2>) { |     override fun serialize(encoder: Encoder, value: Either<T1, T2>) { | ||||||
|         encoder.encodeStructure(descriptor) { |         encoder.encodeStructure(descriptor) { | ||||||
|             when (value) { |             when (value) { | ||||||
| @@ -96,25 +99,25 @@ class EitherSerializer<T1, T2>( | |||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * This type [Either] will always have not nullable [t1] |  * This type [Either] will always have not nullable [optionalT1] | ||||||
|  */ |  */ | ||||||
| @Serializable | @Serializable | ||||||
| data class EitherFirst<T1, T2>( | data class EitherFirst<T1, T2>( | ||||||
|     override val t1: T1 |     override val t1: T1 | ||||||
| ) : Either<T1, T2> { | ) : Either<T1, T2> { | ||||||
|     override val t2: T2? |     override val optionalT1: Optional<T1> = t1.optional | ||||||
|         get() = null |     override val optionalT2: Optional<T2> = Optional.absent() | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * This type [Either] will always have not nullable [t2] |  * This type [Either] will always have not nullable [optionalT2] | ||||||
|  */ |  */ | ||||||
| @Serializable | @Serializable | ||||||
| data class EitherSecond<T1, T2>( | data class EitherSecond<T1, T2>( | ||||||
|     override val t2: T2 |     override val t2: T2 | ||||||
| ) : Either<T1, T2> { | ) : Either<T1, T2> { | ||||||
|     override val t1: T1? |     override val optionalT1: Optional<T1> = Optional.absent() | ||||||
|         get() = null |     override val optionalT2: Optional<T2> = t2.optional | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -127,23 +130,35 @@ inline fun <T1, T2> Either.Companion.first(t1: T1): Either<T1, T2> = EitherFirst | |||||||
| inline fun <T1, T2> Either.Companion.second(t2: T2): Either<T1, T2> = EitherSecond(t2) | 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 |  * Will call [block] in case when [this] is [EitherFirst] | ||||||
|  */ |  */ | ||||||
| inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(crossinline block: (T1) -> Unit): E { | inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(block: (T1) -> Unit): E { | ||||||
|     val t1 = t1 |     optionalT1.onPresented(block) | ||||||
|     t1 ?.let(block) |  | ||||||
|     return this |     return this | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Will call [block] in case when [Either.t2] of [this] is not null |  * Will call [block] in case when [this] is [EitherSecond] | ||||||
|  */ |  */ | ||||||
| inline fun <T1, T2, E : Either<T1, T2>> E.onSecond(crossinline block: (T2) -> Unit): E { | inline fun <T1, T2, E : Either<T1, T2>> E.onSecond(block: (T2) -> Unit): E { | ||||||
|     val t2 = t2 |     optionalT2.onPresented(block) | ||||||
|     t2 ?.let(block) |  | ||||||
|     return this |     return this | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @return Result of [block] if [this] is [EitherFirst] | ||||||
|  |  */ | ||||||
|  | inline fun <T1, R> Either<T1, *>.mapOnFirst(block: (T1) -> R): R? { | ||||||
|  |     return optionalT1.mapOnPresented(block) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @return Result of [block] if [this] is [EitherSecond] | ||||||
|  |  */ | ||||||
|  | inline fun <T2, R> Either<*, T2>.mapOnSecond(block: (T2) -> R): R? { | ||||||
|  |     return optionalT2.mapOnPresented(block) | ||||||
|  | } | ||||||
|  |  | ||||||
| inline fun <reified T1, reified T2> Any.either() = when (this) { | inline fun <reified T1, reified T2> Any.either() = when (this) { | ||||||
|     is T1 -> Either.first<T1, T2>(this) |     is T1 -> Either.first<T1, T2>(this) | ||||||
|     is T2 -> Either.second<T1, T2>(this) |     is T2 -> Either.second<T1, T2>(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() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -21,8 +21,10 @@ import kotlinx.serialization.Serializable | |||||||
|  */ |  */ | ||||||
| @Serializable | @Serializable | ||||||
| data class Optional<T> internal constructor( | data class Optional<T> internal constructor( | ||||||
|     internal val data: T?, |     @Warning("It is unsafe to use this data directly") | ||||||
|     internal val dataPresented: Boolean |     val data: T?, | ||||||
|  |     @Warning("It is unsafe to use this data directly") | ||||||
|  |     val dataPresented: Boolean | ||||||
| ) { | ) { | ||||||
|     companion object { |     companion object { | ||||||
|         /** |         /** | ||||||
| @@ -42,17 +44,31 @@ inline val <T> T.optional | |||||||
| /** | /** | ||||||
|  * Will call [block] when data presented ([Optional.dataPresented] == true) |  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||||
|  */ |  */ | ||||||
| fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply { | inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply { | ||||||
|     if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } |     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) |  * Will call [block] when data absent ([Optional.dataPresented] == false) | ||||||
|  */ |  */ | ||||||
| fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply { | inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply { | ||||||
|     if (!dataPresented) { block() } |     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 |  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise | ||||||
|  */ |  */ | ||||||
| @@ -67,9 +83,10 @@ fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = if (dataPresented) @Supp | |||||||
| /** | /** | ||||||
|  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it |  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it | ||||||
|  */ |  */ | ||||||
| fun <T> Optional<T>.dataOrElse(block: () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block() | inline fun <T> Optional<T>.dataOrElse(block: () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block() | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it |  * 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() | suspend fun <T> Optional<T>.dataOrElseSuspendable(block: suspend () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block() | ||||||
|   | |||||||
| @@ -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() | ||||||
|  | } | ||||||
| @@ -54,7 +54,7 @@ class DiffUtilsTests { | |||||||
|         val oldList = (0 until 10).map { it.toString() } |         val oldList = (0 until 10).map { it.toString() } | ||||||
|         val withIndex = oldList.withIndex() |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|         for (step in 0 until oldList.size) { |         for (step in oldList.indices) { | ||||||
|             for ((i, v) in withIndex) { |             for ((i, v) in withIndex) { | ||||||
|                 val mutable = oldList.toMutableList() |                 val mutable = oldList.toMutableList() | ||||||
|                 val changes = ( |                 val changes = ( | ||||||
| @@ -73,4 +73,78 @@ class DiffUtilsTests { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun testThatSimpleRemoveApplyWorks() { | ||||||
|  |         val oldList = (0 until 10).toList() | ||||||
|  |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|  |         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||||
|  |             for ((i, _) in withIndex) { | ||||||
|  |                 if (i + count > oldList.lastIndex) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 val removedSublist = oldList.subList(i, i + count) | ||||||
|  |                 val mutableOldList = oldList.toMutableList() | ||||||
|  |                 val targetList = oldList - removedSublist | ||||||
|  |  | ||||||
|  |                 mutableOldList.applyDiff(targetList) | ||||||
|  |  | ||||||
|  |                 assertEquals( | ||||||
|  |                     targetList, | ||||||
|  |                     mutableOldList | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun testThatSimpleAddApplyWorks() { | ||||||
|  |         val oldList = (0 until 10).map { it.toString() } | ||||||
|  |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|  |         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||||
|  |             for ((i, v) in withIndex) { | ||||||
|  |                 if (i + count > oldList.lastIndex) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 val addedSublist = oldList.subList(i, i + count).map { "added$it" } | ||||||
|  |                 val mutable = oldList.toMutableList() | ||||||
|  |                 mutable.addAll(i, addedSublist) | ||||||
|  |                 val mutableOldList = oldList.toMutableList() | ||||||
|  |  | ||||||
|  |                 mutableOldList.applyDiff(mutable) | ||||||
|  |  | ||||||
|  |                 assertEquals( | ||||||
|  |                     mutable, | ||||||
|  |                     mutableOldList | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun testThatSimpleChangesApplyWorks() { | ||||||
|  |         val oldList = (0 until 10).map { it.toString() } | ||||||
|  |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|  |         for (step in oldList.indices) { | ||||||
|  |             for ((i, v) in withIndex) { | ||||||
|  |                 val mutable = oldList.toMutableList() | ||||||
|  |                 val changes = ( | ||||||
|  |                     if (step == 0) i until oldList.size else (i until oldList.size step step) | ||||||
|  |                 ).map { index -> | ||||||
|  |                     IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also { | ||||||
|  |                         mutable[index] = it.value | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 val mutableOldList = oldList.toMutableList() | ||||||
|  |                 mutableOldList.applyDiff(mutable) | ||||||
|  |                 assertEquals( | ||||||
|  |                     mutable, | ||||||
|  |                     mutableOldList | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,61 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.browser.document | ||||||
|  | import org.w3c.dom.* | ||||||
|  |  | ||||||
|  | fun Node.onRemoved(block: () -> Unit): MutationObserver { | ||||||
|  |     lateinit var observer: MutationObserver | ||||||
|  |  | ||||||
|  |     observer = MutationObserver { _, _ -> | ||||||
|  |         fun checkIfRemoved(node: Node): Boolean { | ||||||
|  |             return node.parentNode != document && (node.parentNode ?.let { checkIfRemoved(it) } ?: true) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (checkIfRemoved(this)) { | ||||||
|  |             observer.disconnect() | ||||||
|  |             block() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     observer.observe(document, MutationObserverInit(childList = true, subtree = true)) | ||||||
|  |     return observer | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Element.onVisibilityChanged(block: IntersectionObserverEntry.(Float, IntersectionObserver) -> Unit): IntersectionObserver { | ||||||
|  |     var previousIntersectionRatio = -1f | ||||||
|  |     val observer = IntersectionObserver { entries, observer -> | ||||||
|  |         entries.forEach { | ||||||
|  |             if (previousIntersectionRatio != it.intersectionRatio) { | ||||||
|  |                 previousIntersectionRatio = it.intersectionRatio.toFloat() | ||||||
|  |                 it.block(previousIntersectionRatio, observer) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     observer.observe(this) | ||||||
|  |     return observer | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Element.onVisible(block: Element.(IntersectionObserver) -> Unit) { | ||||||
|  |     var previous = -1f | ||||||
|  |     onVisibilityChanged { intersectionRatio, observer -> | ||||||
|  |         if (previous != intersectionRatio) { | ||||||
|  |             if (intersectionRatio > 0 && previous == 0f) { | ||||||
|  |                 block(observer) | ||||||
|  |             } | ||||||
|  |             previous = intersectionRatio | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Element.onInvisible(block: Element.(IntersectionObserver) -> Unit): IntersectionObserver { | ||||||
|  |     var previous = -1f | ||||||
|  |     return onVisibilityChanged { intersectionRatio, observer -> | ||||||
|  |         if (previous != intersectionRatio) { | ||||||
|  |             if (intersectionRatio == 0f && previous != 0f) { | ||||||
|  |                 block(observer) | ||||||
|  |             } | ||||||
|  |             previous = intersectionRatio | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,124 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import org.w3c.dom.DOMRectReadOnly | ||||||
|  | import org.w3c.dom.Element | ||||||
|  |  | ||||||
|  | external interface IntersectionObserverOptions { | ||||||
|  |     /** | ||||||
|  |      * An Element or Document object which is an ancestor of the intended target, whose bounding rectangle will be | ||||||
|  |      * considered the viewport. Any part of the target not visible in the visible area of the root is not considered | ||||||
|  |      * visible. | ||||||
|  |      */ | ||||||
|  |     var root: Element? | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * A string which specifies a set of offsets to add to the root's bounding_box when calculating intersections, | ||||||
|  |      * effectively shrinking or growing the root for calculation purposes. The syntax is approximately the same as that | ||||||
|  |      * for the CSS margin property; see The root element and root margin in Intersection Observer API for more | ||||||
|  |      * information on how the margin works and the syntax. The default is "0px 0px 0px 0px". | ||||||
|  |      */ | ||||||
|  |     var rootMargin: String? | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Either a single number or an array of numbers between 0.0 and 1.0, specifying a ratio of intersection area to | ||||||
|  |      * total bounding box area for the observed target. A value of 0.0 means that even a single visible pixel counts as | ||||||
|  |      * the target being visible. 1.0 means that the entire target element is visible. See Thresholds in Intersection | ||||||
|  |      * Observer API for a more in-depth description of how thresholds are used. The default is a threshold of 0.0. | ||||||
|  |      */ | ||||||
|  |     var threshold: Array<Number>? | ||||||
|  | } | ||||||
|  | fun IntersectionObserverOptions( | ||||||
|  |     block: IntersectionObserverOptions.() -> Unit = {} | ||||||
|  | ): IntersectionObserverOptions = js("{}").unsafeCast<IntersectionObserverOptions>().apply(block) | ||||||
|  |  | ||||||
|  | external interface IntersectionObserverEntry { | ||||||
|  |     /** | ||||||
|  |      * Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in | ||||||
|  |      * the documentation for Element.getBoundingClientRect(). | ||||||
|  |      */ | ||||||
|  |     val boundingClientRect: DOMRectReadOnly | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns the ratio of the intersectionRect to the boundingClientRect. | ||||||
|  |      */ | ||||||
|  |     val intersectionRatio: Number | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns a DOMRectReadOnly representing the target's visible area. | ||||||
|  |      */ | ||||||
|  |     val intersectionRect: DOMRectReadOnly | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * A Boolean value which is true if the target element intersects with the intersection observer's root. If this is | ||||||
|  |      * true, then, the IntersectionObserverEntry describes a transition into a state of intersection; if it's false, | ||||||
|  |      * then you know the transition is from intersecting to not-intersecting. | ||||||
|  |      */ | ||||||
|  |     val isIntersecting: Boolean | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns a DOMRectReadOnly for the intersection observer's root. | ||||||
|  |      */ | ||||||
|  |     val rootBounds: DOMRectReadOnly | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The Element whose intersection with the root changed. | ||||||
|  |      */ | ||||||
|  |     val target: Element | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * A DOMHighResTimeStamp indicating the time at which the intersection was recorded, relative to the | ||||||
|  |      * IntersectionObserver's time origin. | ||||||
|  |      */ | ||||||
|  |     val time: Double | ||||||
|  | } | ||||||
|  |  | ||||||
|  | typealias IntersectionObserverCallback = (entries: Array<IntersectionObserverEntry>, observer: IntersectionObserver) -> Unit | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This is just an implementation from [this commentary](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0) | ||||||
|  |  * of Kotlin JS issue related to the absence of [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) | ||||||
|  |  */ | ||||||
|  | external class IntersectionObserver(callback: IntersectionObserverCallback) { | ||||||
|  |     constructor(callback: IntersectionObserverCallback, options: IntersectionObserverOptions) | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The Element or Document whose bounds are used as the bounding box when testing for intersection. If no root value | ||||||
|  |      * was passed to the constructor or its value is null, the top-level document's viewport is used. | ||||||
|  |      */ | ||||||
|  |     val root: Element | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * An offset rectangle applied to the root's bounding box when calculating intersections, effectively shrinking or | ||||||
|  |      * growing the root for calculation purposes. The value returned by this property may not be the same as the one | ||||||
|  |      * specified when calling the constructor as it may be changed to match internal requirements. Each offset can be | ||||||
|  |      * expressed in pixels (px) or as a percentage (%). The default is "0px 0px 0px 0px". | ||||||
|  |      */ | ||||||
|  |     val rootMargin: String | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * A list of thresholds, sorted in increasing numeric order, where each threshold is a ratio of intersection area to | ||||||
|  |      * bounding box area of an observed target. Notifications for a target are generated when any of the thresholds are | ||||||
|  |      * crossed for that target. If no value was passed to the constructor, 0 is used. | ||||||
|  |      */ | ||||||
|  |     val thresholds: Array<Number> | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Stops the IntersectionObserver object from observing any target. | ||||||
|  |      */ | ||||||
|  |     fun disconnect() | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Tells the IntersectionObserver a target element to observe. | ||||||
|  |      */ | ||||||
|  |     fun observe(targetElement: Element) | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns an array of IntersectionObserverEntry objects for all observed targets. | ||||||
|  |      */ | ||||||
|  |     fun takeRecords(): Array<IntersectionObserverEntry> | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Tells the IntersectionObserver to stop observing a particular target element. | ||||||
|  |      */ | ||||||
|  |     fun unobserve(targetElement: Element) | ||||||
|  | } | ||||||
| @@ -2,8 +2,7 @@ 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 | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -24,6 +23,11 @@ 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() | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -40,5 +44,11 @@ actual val MPPFile.filesize: Long | |||||||
|  * @suppress |  * @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.bytesAllocatorSync: ByteArrayAllocator | ||||||
|  |     get() = ::readBytes | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | @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 | ||||||
|   | |||||||
| @@ -0,0 +1,38 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.browser.document | ||||||
|  | import org.w3c.dom.* | ||||||
|  | import org.w3c.dom.events.Event | ||||||
|  | import org.w3c.dom.events.EventListener | ||||||
|  |  | ||||||
|  | fun Element.onActionOutside(type: String, options: dynamic = null, callback: (Event) -> Unit): EventListener { | ||||||
|  |     lateinit var observer: MutationObserver | ||||||
|  |     val listener = EventListener { | ||||||
|  |         val elementsToCheck = mutableListOf<Element>(this@onActionOutside) | ||||||
|  |         while (it.target != this@onActionOutside && elementsToCheck.isNotEmpty()) { | ||||||
|  |             val childrenGettingElement = elementsToCheck.removeFirst() | ||||||
|  |             for (i in 0 until childrenGettingElement.childElementCount) { | ||||||
|  |                 elementsToCheck.add(childrenGettingElement.children[i] ?: continue) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (elementsToCheck.isEmpty()) { | ||||||
|  |             callback(it) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     if (options == null) { | ||||||
|  |         document.addEventListener(type, listener) | ||||||
|  |     } else { | ||||||
|  |         document.addEventListener(type, listener, options) | ||||||
|  |     } | ||||||
|  |     observer = onRemoved { | ||||||
|  |         if (options == null) { | ||||||
|  |             document.removeEventListener(type, listener) | ||||||
|  |         } else { | ||||||
|  |             document.removeEventListener(type, listener, options) | ||||||
|  |         } | ||||||
|  |         observer.disconnect() | ||||||
|  |     } | ||||||
|  |     return listener | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Element.onClickOutside(options: dynamic = null, callback: (Event) -> Unit) = onActionOutside("click", options, callback) | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.browser.window | ||||||
|  |  | ||||||
|  | fun openLink(link: String, target: String = "_blank", features: String = "") { | ||||||
|  |     window.open(link, target, features) ?.focus() | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -0,0 +1,30 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.browser.document | ||||||
|  | import kotlinx.dom.createElement | ||||||
|  | import org.w3c.dom.HTMLElement | ||||||
|  | import org.w3c.dom.HTMLInputElement | ||||||
|  | import org.w3c.files.get | ||||||
|  |  | ||||||
|  | fun selectFile( | ||||||
|  |     inputSetup: (HTMLInputElement) -> Unit = {}, | ||||||
|  |     onFailure: (Throwable) -> Unit = {}, | ||||||
|  |     onFile: (MPPFile) -> Unit | ||||||
|  | ) { | ||||||
|  |     (document.createElement("input") { | ||||||
|  |         (this as HTMLInputElement).apply { | ||||||
|  |             type = "file" | ||||||
|  |             onchange = { | ||||||
|  |                 runCatching { | ||||||
|  |                     files ?.get(0) ?: error("File must not be null") | ||||||
|  |                 }.onSuccess { | ||||||
|  |                     onFile(it) | ||||||
|  |                 }.onFailure { | ||||||
|  |                     onFailure(it) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             inputSetup(this) | ||||||
|  |         } | ||||||
|  |     } as HTMLElement).click() | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -0,0 +1,14 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.browser.document | ||||||
|  | import org.w3c.dom.HTMLAnchorElement | ||||||
|  |  | ||||||
|  | fun triggerDownloadFile(filename: String, fileLink: String) { | ||||||
|  |     val hiddenElement = document.createElement("a") as HTMLAnchorElement | ||||||
|  |  | ||||||
|  |     hiddenElement.href = fileLink | ||||||
|  |     hiddenElement.target = "_blank" | ||||||
|  |     hiddenElement.download = filename | ||||||
|  |     hiddenElement.click() | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -22,6 +22,11 @@ actual val MPPFile.filesize: Long | |||||||
| /** | /** | ||||||
|  * @suppress |  * @suppress | ||||||
|  */ |  */ | ||||||
|  | actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator | ||||||
|  |     get() = ::readBytes | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
| actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator | actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator | ||||||
|     get() = { |     get() = { | ||||||
|         doInIO { |         doInIO { | ||||||
|   | |||||||
| @@ -10,12 +10,17 @@ kotlin { | |||||||
|     sourceSets { |     sourceSets { | ||||||
|         commonMain { |         commonMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" |                 api libs.kt.coroutines | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         jsMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api project(":micro_utils.common") | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         androidMain { |         androidMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" |                 api libs.kt.coroutines.android | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								coroutines/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								coroutines/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | plugins { | ||||||
|  |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|  |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "com.android.library" | ||||||
|  |     alias(libs.plugins.jb.compose) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$mppProjectWithSerializationAndComposePresetPath" | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     sourceSets { | ||||||
|  |         commonMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.kt.coroutines | ||||||
|  |                 api project(":micro_utils.coroutines") | ||||||
|  |                 api project(":micro_utils.common.compose") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.MutableState | ||||||
|  | import androidx.compose.runtime.mutableStateOf | ||||||
|  | import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  | import kotlinx.coroutines.flow.StateFlow | ||||||
|  |  | ||||||
|  | fun <T> Flow<T>.toMutableState( | ||||||
|  |     initial: T, | ||||||
|  |     scope: CoroutineScope | ||||||
|  | ): MutableState<T> { | ||||||
|  |     val state = mutableStateOf(initial) | ||||||
|  |     subscribeSafelyWithoutExceptions(scope) { state.value = it } | ||||||
|  |     return state | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <T> StateFlow<T>.toMutableState( | ||||||
|  |     scope: CoroutineScope | ||||||
|  | ): MutableState<T> = toMutableState(value, scope) | ||||||
|  |  | ||||||
| @@ -0,0 +1,14 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import kotlinx.coroutines.Job | ||||||
|  | import kotlinx.coroutines.job | ||||||
|  | import kotlin.coroutines.CoroutineContext | ||||||
|  |  | ||||||
|  | fun Composition.linkWithJob(job: Job) { | ||||||
|  |     job.invokeOnCompletion { | ||||||
|  |         this@linkWithJob.dispose() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Composition.linkWithContext(coroutineContext: CoroutineContext) = linkWithJob(coroutineContext.job) | ||||||
| @@ -0,0 +1,26 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import dev.inmo.micro_utils.common.compose.linkWithElement | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import org.jetbrains.compose.web.dom.DOMScope | ||||||
|  | import org.w3c.dom.Element | ||||||
|  |  | ||||||
|  | suspend fun <TElement : Element> renderComposableAndLinkToContext( | ||||||
|  |     root: TElement, | ||||||
|  |     monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock, | ||||||
|  |     content: @Composable DOMScope<TElement>.() -> Unit | ||||||
|  | ): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply { | ||||||
|  |     linkWithContext( | ||||||
|  |         currentCoroutineContext() | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | suspend fun <TElement : Element> renderComposableAndLinkToContextAndRoot( | ||||||
|  |     root: TElement, | ||||||
|  |     monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock, | ||||||
|  |     content: @Composable DOMScope<TElement>.() -> Unit | ||||||
|  | ): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply { | ||||||
|  |     linkWithContext(currentCoroutineContext()) | ||||||
|  |     linkWithElement(root) | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								coroutines/compose/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								coroutines/compose/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest package="dev.inmo.micro_utils.coroutines.compose"/> | ||||||
| @@ -0,0 +1,28 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.onRemoved | ||||||
|  | import dev.inmo.micro_utils.common.onVisibilityChanged | ||||||
|  | import kotlinx.coroutines.flow.* | ||||||
|  | import org.w3c.dom.Element | ||||||
|  |  | ||||||
|  | fun Element.visibilityFlow(): Flow<Boolean> = channelFlow { | ||||||
|  |     var previousData: Boolean? = null | ||||||
|  |  | ||||||
|  |     val observer = onVisibilityChanged { intersectionRatio, _ -> | ||||||
|  |         val currentData = intersectionRatio > 0 | ||||||
|  |         if (currentData != previousData) { | ||||||
|  |             trySend(currentData) | ||||||
|  |         } | ||||||
|  |         previousData = currentData | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val removeObserver = onRemoved { | ||||||
|  |         observer.disconnect() | ||||||
|  |         close() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     invokeOnClose { | ||||||
|  |         observer.disconnect() | ||||||
|  |         removeObserver.disconnect() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,42 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.MPPFile | ||||||
|  | import dev.inmo.micro_utils.common.selectFile | ||||||
|  | import kotlinx.coroutines.CompletableDeferred | ||||||
|  | import org.w3c.dom.HTMLInputElement | ||||||
|  |  | ||||||
|  | suspend fun selectFileOrThrow( | ||||||
|  |     inputSetup: (HTMLInputElement) -> Unit = {} | ||||||
|  | ): MPPFile { | ||||||
|  |     val result = CompletableDeferred<MPPFile>() | ||||||
|  |  | ||||||
|  |     selectFile( | ||||||
|  |         inputSetup, | ||||||
|  |         { | ||||||
|  |             result.completeExceptionally(it) | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|  |         result.complete(it) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return result.await() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | suspend fun selectFileOrNull( | ||||||
|  |     inputSetup: (HTMLInputElement) -> Unit = {}, | ||||||
|  |     onFailure: (Throwable) -> Unit = {} | ||||||
|  | ): MPPFile? { | ||||||
|  |     val result = CompletableDeferred<MPPFile?>() | ||||||
|  |  | ||||||
|  |     selectFile( | ||||||
|  |         inputSetup, | ||||||
|  |         { | ||||||
|  |             result.complete(null) | ||||||
|  |             onFailure(it) | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|  |         result.complete(it) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return result.await() | ||||||
|  | } | ||||||
| @@ -26,12 +26,12 @@ ext { | |||||||
| } | } | ||||||
|  |  | ||||||
| android { | android { | ||||||
|     compileSdkVersion "$android_compileSdkVersion".toInteger() |     compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger() | ||||||
|     buildToolsVersion "$android_buildToolsVersion" |     buildToolsVersion libs.versions.android.props.buildTools.get() | ||||||
|  |  | ||||||
|     defaultConfig { |     defaultConfig { | ||||||
|         minSdkVersion "$android_minSdkVersion".toInteger() |         minSdkVersion libs.versions.android.props.minSdk.get().toInteger() | ||||||
|         targetSdkVersion "$android_compileSdkVersion".toInteger() |         targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger() | ||||||
|         versionCode "${android_code_version}".toInteger() |         versionCode "${android_code_version}".toInteger() | ||||||
|         versionName "$version" |         versionName "$version" | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ allprojects { | |||||||
|         releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true" |         releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true" | ||||||
|  |  | ||||||
|         mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle" |         mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle" | ||||||
|  |         mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle" | ||||||
|         mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle" |         mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle" | ||||||
|         mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle" |         mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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") | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -1,8 +1,11 @@ | |||||||
| package dev.inmo.micro_utils.fsm.common | package dev.inmo.micro_utils.fsm.common | ||||||
|  |  | ||||||
| import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions | import dev.inmo.micro_utils.common.Optional | ||||||
| import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions | import dev.inmo.micro_utils.common.onPresented | ||||||
|  | import dev.inmo.micro_utils.coroutines.* | ||||||
| import kotlinx.coroutines.* | import kotlinx.coroutines.* | ||||||
|  | import kotlinx.coroutines.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Default [StatesMachine] may [startChain] and use inside logic for handling [State]s. By default you may use |  * Default [StatesMachine] may [startChain] and use inside logic for handling [State]s. By default you may use | ||||||
| @@ -42,17 +45,53 @@ interface StatesMachine<T : State> : StatesHandler<T, T> { | |||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Default realization of [StatesMachine]. It uses [statesManager] for incapsulation of [State]s storing and contexts |  * Default realization of [StatesMachine]. It uses [statesManager] for incapsulation of [State]s storing and contexts | ||||||
|  * resolving, and uses [launchStateHandling] for [State] handling |  * resolving, and uses [launchStateHandling] for [State] handling. | ||||||
|  |  * | ||||||
|  |  * This class suppose to be extended in case you wish some custom behaviour inside of [launchStateHandling], for example | ||||||
|  */ |  */ | ||||||
| class DefaultStatesMachine <T: State>( | open class DefaultStatesMachine <T: State>( | ||||||
|     private val statesManager: StatesManager<T>, |     protected val statesManager: StatesManager<T>, | ||||||
|     private val handlers: List<CheckableHandlerHolder<in T, T>> |     protected val handlers: List<CheckableHandlerHolder<in T, T>>, | ||||||
| ) : StatesMachine<T> { | ) : StatesMachine<T> { | ||||||
|     /** |     /** | ||||||
|      * Will call [launchStateHandling] for state handling |      * Will call [launchStateHandling] for state handling | ||||||
|      */ |      */ | ||||||
|     override suspend fun StatesMachine<in T>.handleState(state: T): T? = launchStateHandling(state, handlers) |     override suspend fun StatesMachine<in T>.handleState(state: T): T? = launchStateHandling(state, handlers) | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * This | ||||||
|  |      */ | ||||||
|  |     protected val statesJobs = mutableMapOf<T, Job>() | ||||||
|  |     protected val statesJobsMutex = Mutex() | ||||||
|  |  | ||||||
|  |     protected open suspend fun performUpdate(state: T) { | ||||||
|  |         val newState = launchStateHandling(state, handlers) | ||||||
|  |         if (newState != null) { | ||||||
|  |             statesManager.update(state, newState) | ||||||
|  |         } else { | ||||||
|  |             statesManager.endChain(state) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     open suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) { | ||||||
|  |         statesJobsMutex.withLock { | ||||||
|  |             statesJobs[actualState] ?.cancel() | ||||||
|  |             statesJobs[actualState] = scope.launch { | ||||||
|  |                 performUpdate(actualState) | ||||||
|  |             }.also { job -> | ||||||
|  |                 job.invokeOnCompletion { _ -> | ||||||
|  |                     scope.launch { | ||||||
|  |                         statesJobsMutex.withLock { | ||||||
|  |                             if (statesJobs[actualState] == job) { | ||||||
|  |                                 statesJobs.remove(actualState) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Launch handling of states. On [statesManager] [StatesManager.onStartChain], |      * Launch handling of states. On [statesManager] [StatesManager.onStartChain], | ||||||
|      * [statesManager] [StatesManager.onChainStateUpdated] will be called lambda with performing of state. If |      * [statesManager] [StatesManager.onChainStateUpdated] will be called lambda with performing of state. If | ||||||
| @@ -60,23 +99,25 @@ class DefaultStatesMachine <T: State>( | |||||||
|      * [StatesManager.endChain]. |      * [StatesManager.endChain]. | ||||||
|      */ |      */ | ||||||
|     override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions { |     override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions { | ||||||
|         val statePerformer: suspend (T) -> Unit = { state: T -> |  | ||||||
|             val newState = launchStateHandling(state, handlers) |  | ||||||
|             if (newState != null) { |  | ||||||
|                 statesManager.update(state, newState) |  | ||||||
|             } else { |  | ||||||
|                 statesManager.endChain(state) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) { |         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.onEndChain.subscribeSafelyWithoutExceptions(this) { removedState -> | ||||||
|  |             launch { | ||||||
|  |                 statesJobsMutex.withLock { | ||||||
|  |                     val stateInMap = statesJobs.keys.firstOrNull { stateInMap -> stateInMap == removedState } | ||||||
|  |                     if (stateInMap === removedState) { | ||||||
|  |                         statesJobs[stateInMap] ?.cancel() | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         statesManager.getActiveStates().forEach { |         statesManager.getActiveStates().forEach { | ||||||
|             launch { statePerformer(it) } |             launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -0,0 +1,72 @@ | |||||||
|  | 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>() | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Realization of this update will use the [Job] of [previousState] in [statesJobs] and [jobsStates] if | ||||||
|  |      * [previousState] is [Optional.presented] and [shouldReplaceJob] has returned true for [previousState] and [actualState]. In | ||||||
|  |      * other words, [Job] of [previousState] WILL NOT be replaced with the new one if they are "equal". Equality of | ||||||
|  |      * states is solved in [shouldReplaceJob] and can be rewritten in subclasses | ||||||
|  |      */ | ||||||
|  |     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 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Compare if [previous] potentially lead to the same behaviour with [new] | ||||||
|  |      */ | ||||||
|  |     protected open suspend fun shouldReplaceJob(previous: Optional<T>, new: T): Boolean = previous.dataOrNull() != new | ||||||
|  |  | ||||||
|  |     @Deprecated("Overwrite shouldReplaceJob instead") | ||||||
|  |     protected open suspend fun compare(previous: Optional<T>, new: T): Boolean = shouldReplaceJob(previous, new) | ||||||
|  |  | ||||||
|  |     override suspend fun updateChain(currentState: T, newState: T) { | ||||||
|  |         statesManager.update(currentState, newState) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -7,6 +7,12 @@ import kotlin.reflect.KClass | |||||||
|  |  | ||||||
| class FSMBuilder<T : State>( | class FSMBuilder<T : State>( | ||||||
|     var statesManager: StatesManager<T> = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()), |     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 } |     var defaultStateHandler: StatesHandler<T, T>? = StatesHandler { null } | ||||||
| ) { | ) { | ||||||
|     private var states = mutableListOf<CheckableHandlerHolder<T, T>>() |     private var states = mutableListOf<CheckableHandlerHolder<T, T>>() | ||||||
| @@ -42,7 +48,7 @@ class FSMBuilder<T : State>( | |||||||
|         add(filter, handler) |         add(filter, handler) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun build() = StatesMachine( |     fun build() = fsmBuilder( | ||||||
|         statesManager, |         statesManager, | ||||||
|         states.toList().let { list -> |         states.toList().let { list -> | ||||||
|             defaultStateHandler ?.let { list + it.holder { true } } ?: list |             defaultStateHandler ?.let { list + it.holder { true } } ?: list | ||||||
|   | |||||||
| @@ -37,36 +37,49 @@ interface DefaultStatesManagerRepo<T : State> { | |||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @param repo This repo will be used as repository for storing states. All operations with this repo will happen BEFORE |  * @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 |  * 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 |  * [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] |  * @param onStartContextsConflictResolver Receive current [State] and the state passed with [startChain]. In case when | ||||||
|  |  * this callback will return true, currently placed on the [State.context] [State] will be replaced by new state | ||||||
|  |  * with [endChain] with current state | ||||||
|  |  * @param onUpdateContextsConflictResolver 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 |  * 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 |  * new state by using [endChain] with that state | ||||||
|  */ |  */ | ||||||
| class DefaultStatesManager<T : State>( | open class DefaultStatesManager<T : State>( | ||||||
|     private val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(), |     protected val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(), | ||||||
|     private val onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true } |     protected val onStartContextsConflictResolver: suspend (current: T, new: T) -> Boolean = { _, _ -> true }, | ||||||
|  |     protected val onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true } | ||||||
| ) : StatesManager<T> { | ) : StatesManager<T> { | ||||||
|     private val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0) |     protected val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0) | ||||||
|     override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow() |     override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow() | ||||||
|     private val _onStartChain = MutableSharedFlow<T>(0) |     protected val _onStartChain = MutableSharedFlow<T>(0) | ||||||
|     override val onStartChain: Flow<T> = _onStartChain.asSharedFlow() |     override val onStartChain: Flow<T> = _onStartChain.asSharedFlow() | ||||||
|     private val _onEndChain = MutableSharedFlow<T>(0) |     protected val _onEndChain = MutableSharedFlow<T>(0) | ||||||
|     override val onEndChain: Flow<T> = _onEndChain.asSharedFlow() |     override val onEndChain: Flow<T> = _onEndChain.asSharedFlow() | ||||||
|  |  | ||||||
|     private val mapMutex = Mutex() |     protected val mapMutex = Mutex() | ||||||
|  |  | ||||||
|  |     constructor( | ||||||
|  |         repo: DefaultStatesManagerRepo<T>, | ||||||
|  |         onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean | ||||||
|  |     ) : this ( | ||||||
|  |         repo, | ||||||
|  |         onUpdateContextsConflictResolver = onContextsConflictResolver | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     override suspend fun update(old: T, new: T) = mapMutex.withLock { |     override suspend fun update(old: T, new: T) = mapMutex.withLock { | ||||||
|         val stateByOldContext: T? = repo.getContextState(old.context) |         val stateByOldContext: T? = repo.getContextState(old.context) | ||||||
|         when { |         when { | ||||||
|             stateByOldContext != old -> return@withLock |             stateByOldContext != old -> return@withLock | ||||||
|             stateByOldContext == null || old.context == new.context -> { |             stateByOldContext == null || old.context == new.context -> { | ||||||
|  |                 repo.removeState(old) | ||||||
|                 repo.set(new) |                 repo.set(new) | ||||||
|                 _onChainStateUpdated.emit(old to new) |                 _onChainStateUpdated.emit(old to new) | ||||||
|             } |             } | ||||||
|             else -> { |             else -> { | ||||||
|                 val stateOnNewOneContext = repo.getContextState(new.context) |                 val stateOnNewOneContext = repo.getContextState(new.context) | ||||||
|                 if (stateOnNewOneContext == null || onContextsConflictResolver(old, new, stateOnNewOneContext)) { |                 if (stateOnNewOneContext == null || onUpdateContextsConflictResolver(old, new, stateOnNewOneContext)) { | ||||||
|                     stateOnNewOneContext ?.let { endChainWithoutLock(it) } |                     stateOnNewOneContext ?.let { endChainWithoutLock(it) } | ||||||
|                     repo.removeState(old) |                     repo.removeState(old) | ||||||
|                     repo.set(new) |                     repo.set(new) | ||||||
| @@ -77,13 +90,17 @@ class DefaultStatesManager<T : State>( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     override suspend fun startChain(state: T) = mapMutex.withLock { |     override suspend fun startChain(state: T) = mapMutex.withLock { | ||||||
|         if (!repo.contains(state.context)) { |         val stateOnContext = repo.getContextState(state.context) | ||||||
|  |         if (stateOnContext == null || onStartContextsConflictResolver(stateOnContext, state)) { | ||||||
|  |             stateOnContext ?.let { | ||||||
|  |                 endChainWithoutLock(it) | ||||||
|  |             } | ||||||
|             repo.set(state) |             repo.set(state) | ||||||
|             _onStartChain.emit(state) |             _onStartChain.emit(state) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private suspend fun endChainWithoutLock(state: T) { |     protected open suspend fun endChainWithoutLock(state: T) { | ||||||
|         if (repo.getContextState(state.context) == state) { |         if (repo.getContextState(state.context) == state) { | ||||||
|             repo.removeState(state) |             repo.removeState(state) | ||||||
|             _onEndChain.emit(state) |             _onEndChain.emit(state) | ||||||
|   | |||||||
| @@ -12,5 +12,6 @@ import kotlinx.coroutines.flow.* | |||||||
|  */ |  */ | ||||||
| @Deprecated("Use DefaultStatesManager instead", ReplaceWith("DefaultStatesManager")) | @Deprecated("Use DefaultStatesManager instead", ReplaceWith("DefaultStatesManager")) | ||||||
| fun <T: State> InMemoryStatesManager( | fun <T: State> InMemoryStatesManager( | ||||||
|     onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true } |     onStartContextsConflictResolver: suspend (old: T, new: T) -> Boolean = { _, _ -> true }, | ||||||
| ) = DefaultStatesManager(onContextsConflictResolver = onContextsConflictResolver) |     onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true } | ||||||
|  | ) = DefaultStatesManager(onStartContextsConflictResolver = onStartContextsConflictResolver, onUpdateContextsConflictResolver = onUpdateContextsConflictResolver) | ||||||
|   | |||||||
| @@ -7,43 +7,12 @@ android.useAndroidX=true | |||||||
| android.enableJetifier=true | android.enableJetifier=true | ||||||
| org.gradle.jvmargs=-Xmx2g | org.gradle.jvmargs=-Xmx2g | ||||||
|  |  | ||||||
| kotlin_version=1.5.31 |  | ||||||
| kotlin_coroutines_version=1.5.2 |  | ||||||
| kotlin_serialisation_core_version=1.3.1 |  | ||||||
| kotlin_exposed_version=0.36.2 |  | ||||||
|  |  | ||||||
| ktor_version=1.6.5 |  | ||||||
|  |  | ||||||
| klockVersion=2.4.8 |  | ||||||
|  |  | ||||||
| github_release_plugin_version=2.2.12 |  | ||||||
|  |  | ||||||
| uuidVersion=0.3.1 |  | ||||||
|  |  | ||||||
| # ANDROID |  | ||||||
|  |  | ||||||
| core_ktx_version=1.7.0 |  | ||||||
| androidx_recycler_version=1.2.1 |  | ||||||
| appcompat_version=1.3.1 |  | ||||||
|  |  | ||||||
| android_minSdkVersion=19 |  | ||||||
| android_compileSdkVersion=31 |  | ||||||
| android_buildToolsVersion=31.0.0 |  | ||||||
| dexcount_version=3.0.0 |  | ||||||
| junit_version=4.12 |  | ||||||
| test_ext_junit_version=1.1.2 |  | ||||||
| espresso_core=3.3.0 |  | ||||||
|  |  | ||||||
| # JS NPM | # JS NPM | ||||||
|  |  | ||||||
| crypto_js_version=4.1.1 | crypto_js_version=4.1.1 | ||||||
|  |  | ||||||
| # Dokka |  | ||||||
|  |  | ||||||
| dokka_version=1.5.31 |  | ||||||
|  |  | ||||||
| # Project data | # Project data | ||||||
|  |  | ||||||
| group=dev.inmo | group=dev.inmo | ||||||
| version=0.8.3 | version=0.9.25 | ||||||
| android_code_version=83 | android_code_version=115 | ||||||
|   | |||||||
							
								
								
									
										78
									
								
								gradle/libs.versions.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								gradle/libs.versions.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | |||||||
|  | [versions] | ||||||
|  |  | ||||||
|  | kt = "1.6.10" | ||||||
|  | kt-serialization = "1.3.2" | ||||||
|  | kt-coroutines = "1.6.1" | ||||||
|  |  | ||||||
|  | jb-compose = "1.1.1" | ||||||
|  | jb-exposed = "0.37.3" | ||||||
|  | jb-dokka = "1.6.10" | ||||||
|  |  | ||||||
|  | klock = "2.7.0" | ||||||
|  | uuid = "0.4.0" | ||||||
|  |  | ||||||
|  | ktor = "1.6.8" | ||||||
|  |  | ||||||
|  | gh-release = "2.2.12" | ||||||
|  |  | ||||||
|  | android-gradle = "7.0.4" | ||||||
|  | dexcount = "3.0.1" | ||||||
|  |  | ||||||
|  | android-coreKtx = "1.7.0" | ||||||
|  | android-recyclerView = "1.2.1" | ||||||
|  | android-appCompat = "1.4.1" | ||||||
|  | android-espresso = "3.3.0" | ||||||
|  | android-test = "1.1.2" | ||||||
|  |  | ||||||
|  | android-props-minSdk = "19" | ||||||
|  | android-props-compileSdk = "32" | ||||||
|  | android-props-buildTools = "32.0.0" | ||||||
|  |  | ||||||
|  | [libraries] | ||||||
|  |  | ||||||
|  | kt-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kt" } | ||||||
|  |  | ||||||
|  | kt-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kt-serialization" } | ||||||
|  | kt-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kt-serialization" } | ||||||
|  |  | ||||||
|  | kt-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kt-coroutines" } | ||||||
|  | kt-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kt-coroutines" } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ktor-io = { module = "io.ktor:ktor-io", version.ref = "ktor" } | ||||||
|  | ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } | ||||||
|  | ktor-client-java = { module = "io.ktor:ktor-client-java", version.ref = "ktor" } | ||||||
|  | ktor-server = { module = "io.ktor:ktor-server", version.ref = "ktor" } | ||||||
|  | ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" } | ||||||
|  | ktor-server-host-common = { module = "io.ktor:ktor-server-host-common", version.ref = "ktor" } | ||||||
|  | ktor-websockets = { module = "io.ktor:ktor-websockets", version.ref = "ktor" } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "klock" } | ||||||
|  | uuid = { module = "com.benasher44:uuid", version.ref = "uuid" } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-exposed" } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" } | ||||||
|  | android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" } | ||||||
|  | android-appCompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "android-appCompat" } | ||||||
|  | android-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" } | ||||||
|  | android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | kt-test-js = { module = "org.jetbrains.kotlin:kotlin-test-js", version.ref = "kt" } | ||||||
|  | kt-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kt" } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | buildscript-kt-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kt" } | ||||||
|  | buildscript-kt-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kt" } | ||||||
|  | buildscript-jb-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "jb-dokka" } | ||||||
|  | buildscript-gh-release = { module = "com.github.breadmoirai:github-release", version.ref = "gh-release" } | ||||||
|  | buildscript-android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle" } | ||||||
|  | buildscript-android-dexcount = { module = "com.getkeepsafe.dexcount:dexcount-gradle-plugin", version.ref = "dexcount" } | ||||||
|  |  | ||||||
|  | [plugins] | ||||||
|  |  | ||||||
|  | jb-compose = { id = "org.jetbrains.compose", version.ref = "jb-compose" } | ||||||
							
								
								
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +1,5 @@ | |||||||
| distributionBase=GRADLE_USER_HOME | distributionBase=GRADLE_USER_HOME | ||||||
| distributionPath=wrapper/dists | distributionPath=wrapper/dists | ||||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip | ||||||
| zipStoreBase=GRADLE_USER_HOME | zipStoreBase=GRADLE_USER_HOME | ||||||
| zipStorePath=wrapper/dists | zipStorePath=wrapper/dists | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ kotlin { | |||||||
|             dependencies { |             dependencies { | ||||||
|                 api internalProject("micro_utils.ktor.common") |                 api internalProject("micro_utils.ktor.common") | ||||||
|                 api internalProject("micro_utils.coroutines") |                 api internalProject("micro_utils.coroutines") | ||||||
|                 api "io.ktor:ktor-client-core:$ktor_version" |                 api libs.ktor.client | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import dev.inmo.micro_utils.coroutines.safely | |||||||
| 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.features.websocket.ws | import io.ktor.client.features.websocket.ws | ||||||
|  | import io.ktor.client.request.HttpRequestBuilder | ||||||
| import io.ktor.http.cio.websocket.Frame | import io.ktor.http.cio.websocket.Frame | ||||||
| import io.ktor.http.cio.websocket.readBytes | import io.ktor.http.cio.websocket.readBytes | ||||||
| import kotlinx.coroutines.flow.Flow | import kotlinx.coroutines.flow.Flow | ||||||
| @@ -17,6 +18,7 @@ import kotlinx.serialization.DeserializationStrategy | |||||||
| inline fun <T> HttpClient.createStandardWebsocketFlow( | inline fun <T> HttpClient.createStandardWebsocketFlow( | ||||||
|     url: String, |     url: String, | ||||||
|     crossinline checkReconnection: (Throwable?) -> Boolean = { true }, |     crossinline checkReconnection: (Throwable?) -> Boolean = { true }, | ||||||
|  |     noinline requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||||
|     crossinline conversation: suspend (StandardKtorSerialInputData) -> T |     crossinline conversation: suspend (StandardKtorSerialInputData) -> T | ||||||
| ): Flow<T> { | ): Flow<T> { | ||||||
|     val correctedUrl = url.asCorrectWebSocketUrl |     val correctedUrl = url.asCorrectWebSocketUrl | ||||||
| @@ -26,7 +28,7 @@ inline fun <T> HttpClient.createStandardWebsocketFlow( | |||||||
|         do { |         do { | ||||||
|             val reconnect = try { |             val reconnect = try { | ||||||
|                 safely { |                 safely { | ||||||
|                     ws(correctedUrl) { |                     ws(correctedUrl, requestBuilder) { | ||||||
|                         for (received in incoming) { |                         for (received in incoming) { | ||||||
|                             when (received) { |                             when (received) { | ||||||
|                                 is Frame.Binary -> producerScope.send(conversation(received.readBytes())) |                                 is Frame.Binary -> producerScope.send(conversation(received.readBytes())) | ||||||
| @@ -65,10 +67,12 @@ inline fun <T> HttpClient.createStandardWebsocketFlow( | |||||||
|     url: String, |     url: String, | ||||||
|     crossinline checkReconnection: (Throwable?) -> Boolean = { true }, |     crossinline checkReconnection: (Throwable?) -> Boolean = { true }, | ||||||
|     deserializer: DeserializationStrategy<T>, |     deserializer: DeserializationStrategy<T>, | ||||||
|     serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat |     serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat, | ||||||
|  |     noinline requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||||
| ) = createStandardWebsocketFlow( | ) = createStandardWebsocketFlow( | ||||||
|     url, |     url, | ||||||
|     checkReconnection |     checkReconnection, | ||||||
|  |     requestBuilder | ||||||
| ) { | ) { | ||||||
|     serialFormat.decodeDefault(deserializer, it) |     serialFormat.decodeDefault(deserializer, it) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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,11 +35,66 @@ 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, | ||||||
|         deserializer: DeserializationStrategy<T> |         deserializer: DeserializationStrategy<T>, | ||||||
|     ) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat) |         requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||||
|  |     ) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat, requestBuilder) | ||||||
|  |  | ||||||
|  |     fun <T> createStandardWebsocketFlow( | ||||||
|  |         url: String, | ||||||
|  |         deserializer: DeserializationStrategy<T>, | ||||||
|  |         requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||||
|  |     ) = createStandardWebsocketFlow(url, { true  }, deserializer, requestBuilder) | ||||||
| } | } | ||||||
|  |  | ||||||
| val defaultRequester = UnifiedRequester() | val defaultRequester = UnifiedRequester() | ||||||
| @@ -69,3 +128,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,19 @@ | |||||||
|  | package dev.inmo.micro_utils.ktor.client | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.MPPFile | ||||||
|  | import dev.inmo.micro_utils.ktor.common.* | ||||||
|  | import io.ktor.client.HttpClient | ||||||
|  |  | ||||||
|  | expect suspend fun HttpClient.tempUpload( | ||||||
|  |     fullTempUploadDraftPath: String, | ||||||
|  |     file: MPPFile, | ||||||
|  |     onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> } | ||||||
|  | ): TemporalFileId | ||||||
|  |  | ||||||
|  | suspend fun UnifiedRequester.tempUpload( | ||||||
|  |     fullTempUploadDraftPath: String, | ||||||
|  |     file: MPPFile, | ||||||
|  |     onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> } | ||||||
|  | ): TemporalFileId = client.tempUpload( | ||||||
|  |     fullTempUploadDraftPath, file, onUpload | ||||||
|  | ) | ||||||
| @@ -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,58 @@ | |||||||
|  | package dev.inmo.micro_utils.ktor.client | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.MPPFile | ||||||
|  | import dev.inmo.micro_utils.ktor.common.TemporalFileId | ||||||
|  | import io.ktor.client.HttpClient | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import org.w3c.xhr.* | ||||||
|  |  | ||||||
|  | suspend fun tempUpload( | ||||||
|  |     fullTempUploadDraftPath: String, | ||||||
|  |     file: MPPFile, | ||||||
|  |     onUpload: (Long, Long) -> Unit | ||||||
|  | ): TemporalFileId { | ||||||
|  |     val formData = FormData() | ||||||
|  |     val answer = CompletableDeferred<TemporalFileId>() | ||||||
|  |  | ||||||
|  |     formData.append( | ||||||
|  |         "data", | ||||||
|  |         file | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     val request = XMLHttpRequest() | ||||||
|  |     request.responseType = XMLHttpRequestResponseType.TEXT | ||||||
|  |     request.upload.onprogress = { | ||||||
|  |         onUpload(it.loaded.toLong(), it.total.toLong()) | ||||||
|  |     } | ||||||
|  |     request.onload = { | ||||||
|  |         if (request.status == 200.toShort()) { | ||||||
|  |             answer.complete(TemporalFileId(request.responseText)) | ||||||
|  |         } else { | ||||||
|  |             answer.completeExceptionally(Exception("Something went wrong: $it")) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     request.onerror = { | ||||||
|  |         answer.completeExceptionally(Exception("Something went wrong: $it")) | ||||||
|  |     } | ||||||
|  |     request.open("POST", fullTempUploadDraftPath, true) | ||||||
|  |     request.send(formData) | ||||||
|  |  | ||||||
|  |     val handle = currentCoroutineContext().job.invokeOnCompletion { | ||||||
|  |         runCatching { | ||||||
|  |             request.abort() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return runCatching { | ||||||
|  |         answer.await() | ||||||
|  |     }.also { | ||||||
|  |         handle.dispose() | ||||||
|  |     }.getOrThrow() | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | actual suspend fun HttpClient.tempUpload( | ||||||
|  |     fullTempUploadDraftPath: String, | ||||||
|  |     file: MPPFile, | ||||||
|  |     onUpload: (uploaded: Long, count: Long) -> Unit | ||||||
|  | ): TemporalFileId = dev.inmo.micro_utils.ktor.client.tempUpload(fullTempUploadDraftPath, file, onUpload) | ||||||
| @@ -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() | ||||||
|  | } | ||||||
| @@ -0,0 +1,39 @@ | |||||||
|  | package dev.inmo.micro_utils.ktor.client | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.MPPFile | ||||||
|  | import dev.inmo.micro_utils.common.filename | ||||||
|  | import dev.inmo.micro_utils.ktor.common.TemporalFileId | ||||||
|  | import io.ktor.client.HttpClient | ||||||
|  | import io.ktor.client.features.onUpload | ||||||
|  | import io.ktor.client.request.forms.formData | ||||||
|  | import io.ktor.client.request.forms.submitFormWithBinaryData | ||||||
|  | import io.ktor.http.Headers | ||||||
|  | import io.ktor.http.HttpHeaders | ||||||
|  | import java.net.URLConnection | ||||||
|  |  | ||||||
|  | internal val MPPFile.mimeType: String | ||||||
|  |     get() = URLConnection.getFileNameMap().getContentTypeFor(filename.name) ?: "*/*" | ||||||
|  |  | ||||||
|  | actual suspend fun HttpClient.tempUpload( | ||||||
|  |     fullTempUploadDraftPath: String, | ||||||
|  |     file: MPPFile, | ||||||
|  |     onUpload: (Long, Long) -> Unit | ||||||
|  | ): TemporalFileId { | ||||||
|  |     val inputProvider = file.inputProvider() | ||||||
|  |     val fileId = submitFormWithBinaryData<String>( | ||||||
|  |         fullTempUploadDraftPath, | ||||||
|  |         formData = formData { | ||||||
|  |             append( | ||||||
|  |                 "data", | ||||||
|  |                 inputProvider, | ||||||
|  |                 Headers.build { | ||||||
|  |                     append(HttpHeaders.ContentType, file.mimeType) | ||||||
|  |                     append(HttpHeaders.ContentDisposition, "filename=\"${file.filename.string}\"") | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|  |         onUpload(onUpload) | ||||||
|  |     } | ||||||
|  |     return TemporalFileId(fileId) | ||||||
|  | } | ||||||
| @@ -10,8 +10,11 @@ kotlin { | |||||||
|     sourceSets { |     sourceSets { | ||||||
|         commonMain { |         commonMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "org.jetbrains.kotlinx:kotlinx-serialization-cbor:$kotlin_serialisation_core_version" |                 api internalProject("micro_utils.common") | ||||||
|                 api "com.soywiz.korlibs.klock:klock:$klockVersion" |                 api libs.kt.serialization.cbor | ||||||
|  |                 api libs.klock | ||||||
|  |                 api libs.uuid | ||||||
|  |                 api libs.ktor.io | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | package dev.inmo.micro_utils.ktor.common | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.MPPFile | ||||||
|  | import io.ktor.utils.io.core.Input | ||||||
|  |  | ||||||
|  | expect fun MPPFile.input(): Input | ||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | package dev.inmo.micro_utils.ktor.common | ||||||
|  |  | ||||||
|  | import kotlin.jvm.JvmInline | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  |  | ||||||
|  | const val DefaultTemporalFilesSubPath = "temp_upload" | ||||||
|  |  | ||||||
|  | @Serializable | ||||||
|  | @JvmInline | ||||||
|  | value class TemporalFileId(val string: String) | ||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | package dev.inmo.micro_utils.ktor.common | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.* | ||||||
|  | import io.ktor.utils.io.core.ByteReadPacket | ||||||
|  | import io.ktor.utils.io.core.Input | ||||||
|  |  | ||||||
|  | actual fun MPPFile.input(): Input = ByteReadPacket(readBytes()) | ||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | package dev.inmo.micro_utils.ktor.common | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.MPPFile | ||||||
|  | import io.ktor.utils.io.core.Input | ||||||
|  | import io.ktor.utils.io.streams.asInput | ||||||
|  |  | ||||||
|  | actual fun MPPFile.input(): Input = inputStream().asInput() | ||||||
| @@ -16,10 +16,10 @@ kotlin { | |||||||
|  |  | ||||||
|         jvmMain { |         jvmMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "io.ktor:ktor-server:$ktor_version" |                 api libs.ktor.server | ||||||
|                 api "io.ktor:ktor-server-cio:$ktor_version" |                 api libs.ktor.server.cio | ||||||
|                 api "io.ktor:ktor-server-host-common:$ktor_version" |                 api libs.ktor.server.host.common | ||||||
|                 api "io.ktor:ktor-websockets:$ktor_version" |                 api libs.ktor.websockets | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -2,29 +2,31 @@ package dev.inmo.micro_utils.ktor.server | |||||||
|  |  | ||||||
| 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.featureOrNull | ||||||
|  | import io.ktor.application.install | ||||||
|  | import io.ktor.http.URLProtocol | ||||||
| import io.ktor.http.cio.websocket.* | import io.ktor.http.cio.websocket.* | ||||||
| import io.ktor.routing.Route | import io.ktor.routing.Route | ||||||
| import io.ktor.websocket.webSocket | import io.ktor.routing.application | ||||||
|  | import io.ktor.websocket.* | ||||||
| import kotlinx.coroutines.flow.Flow | import kotlinx.coroutines.flow.Flow | ||||||
| import kotlinx.coroutines.flow.collect |  | ||||||
| import kotlinx.serialization.SerializationStrategy | import kotlinx.serialization.SerializationStrategy | ||||||
|  |  | ||||||
| private suspend fun DefaultWebSocketSession.checkReceivedAndCloseIfExists() { |  | ||||||
|     if (incoming.tryReceive() != null) { |  | ||||||
|         close() |  | ||||||
|         throw CorrectCloseException |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fun <T> Route.includeWebsocketHandling( | fun <T> Route.includeWebsocketHandling( | ||||||
|     suburl: String, |     suburl: String, | ||||||
|     flow: Flow<T>, |     flow: Flow<T>, | ||||||
|     converter: (T) -> StandardKtorSerialInputData |     protocol: URLProtocol = URLProtocol.WS, | ||||||
|  |     converter: suspend WebSocketServerSession.(T) -> StandardKtorSerialInputData? | ||||||
| ) { | ) { | ||||||
|     webSocket(suburl) { |     application.apply { | ||||||
|  |         featureOrNull(io.ktor.websocket.WebSockets) ?: install(io.ktor.websocket.WebSockets) | ||||||
|  |     } | ||||||
|  |     webSocket(suburl, protocol.name) { | ||||||
|         safely { |         safely { | ||||||
|             flow.collect { |             flow.collect { | ||||||
|                 send(converter(it)) |                 converter(it) ?.let { data -> | ||||||
|  |                     send(data) | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -34,10 +36,24 @@ fun <T> Route.includeWebsocketHandling( | |||||||
|     suburl: String, |     suburl: String, | ||||||
|     flow: Flow<T>, |     flow: Flow<T>, | ||||||
|     serializer: SerializationStrategy<T>, |     serializer: SerializationStrategy<T>, | ||||||
|     serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat |     serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat, | ||||||
|  |     protocol: URLProtocol = URLProtocol.WS, | ||||||
|  |     filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null | ||||||
| ) = includeWebsocketHandling( | ) = includeWebsocketHandling( | ||||||
|     suburl, |     suburl, | ||||||
|     flow |     flow, | ||||||
| ) { |     protocol, | ||||||
|  |     converter = if (filter == null) { | ||||||
|  |         { | ||||||
|             serialFormat.encodeDefault(serializer, it) |             serialFormat.encodeDefault(serializer, it) | ||||||
| } |         } | ||||||
|  |     } else { | ||||||
|  |         { | ||||||
|  |             if (filter(it)) { | ||||||
|  |                 serialFormat.encodeDefault(serializer, it) | ||||||
|  |             } else { | ||||||
|  |                 null | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | ) | ||||||
|   | |||||||
| @@ -1,28 +1,39 @@ | |||||||
| 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.* | ||||||
| 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 io.ktor.websocket.WebSocketServerSession | ||||||
| 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, | ||||||
|         flow: Flow<T>, |         flow: Flow<T>, | ||||||
|         serializer: SerializationStrategy<T> |         serializer: SerializationStrategy<T>, | ||||||
|     ) = includeWebsocketHandling(suburl, flow, serializer, serialFormat) |         protocol: URLProtocol = URLProtocol.WS, | ||||||
|  |         filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null | ||||||
|  |     ) = includeWebsocketHandling(suburl, flow, serializer, serialFormat, protocol, filter) | ||||||
|  |  | ||||||
|     suspend fun <T> PipelineContext<*, ApplicationCall>.unianswer( |     suspend fun <T> PipelineContext<*, ApplicationCall>.unianswer( | ||||||
|         answerSerializer: SerializationStrategy<T>, |         answerSerializer: SerializationStrategy<T>, | ||||||
| @@ -81,6 +92,11 @@ class UnifiedRouter( | |||||||
|             call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field") |             call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field") | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         val default | ||||||
|  |             get() = defaultUnifiedRouter | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| val defaultUnifiedRouter = UnifiedRouter() | val defaultUnifiedRouter = UnifiedRouter() | ||||||
| @@ -104,6 +120,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,7 +1,9 @@ | |||||||
| 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.cio.CIOApplicationEngine | ||||||
| import io.ktor.server.engine.* | import io.ktor.server.engine.* | ||||||
| import kotlin.random.Random | import kotlin.random.Random | ||||||
|  |  | ||||||
| @@ -9,16 +11,57 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati | |||||||
|     engine: ApplicationEngineFactory<TEngine, TConfiguration>, |     engine: ApplicationEngineFactory<TEngine, TConfiguration>, | ||||||
|     host: String = "localhost", |     host: String = "localhost", | ||||||
|     port: Int = Random.nextInt(1024, 65535), |     port: Int = Random.nextInt(1024, 65535), | ||||||
|  |     additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {}, | ||||||
|  |     additionalConfigurationConfigurator: TConfiguration.() -> Unit = {}, | ||||||
|     block: Application.() -> Unit |     block: Application.() -> Unit | ||||||
| ): TEngine { | ): TEngine = embeddedServer( | ||||||
|     val env = applicationEngineEnvironment { |     engine, | ||||||
|  |     applicationEngineEnvironment { | ||||||
|         module(block) |         module(block) | ||||||
|         connector { |         connector { | ||||||
|             this@connector.host = host |             this.host = host | ||||||
|             this@connector.port = port |             this.port = port | ||||||
|         } |         } | ||||||
|     } |         additionalEngineEnvironmentConfigurator() | ||||||
|     return embeddedServer(engine, env) |     }, | ||||||
|  |     additionalConfigurationConfigurator | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Create server with [CIO] server engine without starting of it | ||||||
|  |  * | ||||||
|  |  * @see ApplicationEngine.start | ||||||
|  |  */ | ||||||
|  | fun createKtorServer( | ||||||
|  |     host: String = "localhost", | ||||||
|  |     port: Int = Random.nextInt(1024, 65535), | ||||||
|  |     additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {}, | ||||||
|  |     additionalConfigurationConfigurator: CIOApplicationEngine.Configuration.() -> Unit = {}, | ||||||
|  |     block: Application.() -> Unit | ||||||
|  | ): CIOApplicationEngine = createKtorServer( | ||||||
|  |     CIO, | ||||||
|  |     host, | ||||||
|  |     port, | ||||||
|  |     additionalEngineEnvironmentConfigurator, | ||||||
|  |     additionalConfigurationConfigurator, | ||||||
|  |     block | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration> createKtorServer( | ||||||
|  |     engine: ApplicationEngineFactory<TEngine, TConfiguration>, | ||||||
|  |     host: String = "localhost", | ||||||
|  |     port: Int = Random.nextInt(1024, 65535), | ||||||
|  |     additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {}, | ||||||
|  |     additionalConfigurationConfigurator: TConfiguration.() -> Unit = {}, | ||||||
|  |     configurators: List<KtorApplicationConfigurator> | ||||||
|  | ): TEngine = createKtorServer( | ||||||
|  |     engine, | ||||||
|  |     host, | ||||||
|  |     port, | ||||||
|  |     additionalEngineEnvironmentConfigurator, | ||||||
|  |     additionalConfigurationConfigurator | ||||||
|  | ) { | ||||||
|  |     configurators.forEach { it.apply { configure() } } | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -29,5 +72,7 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati | |||||||
| fun createKtorServer( | fun createKtorServer( | ||||||
|     host: String = "localhost", |     host: String = "localhost", | ||||||
|     port: Int = Random.nextInt(1024, 65535), |     port: Int = Random.nextInt(1024, 65535), | ||||||
|     block: Application.() -> Unit |     configurators: List<KtorApplicationConfigurator>, | ||||||
| ): ApplicationEngine = createKtorServer(CIO, host, port, block) |     additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {}, | ||||||
|  |     additionalConfigurationConfigurator: CIOApplicationEngine.Configuration.() -> Unit = {}, | ||||||
|  | ): ApplicationEngine = createKtorServer(CIO, host, port, additionalEngineEnvironmentConfigurator, additionalConfigurationConfigurator, configurators) | ||||||
|   | |||||||
| @@ -0,0 +1,132 @@ | |||||||
|  | package dev.inmo.micro_utils.ktor.server | ||||||
|  |  | ||||||
|  | import com.benasher44.uuid.uuid4 | ||||||
|  | import dev.inmo.micro_utils.common.FileName | ||||||
|  | import dev.inmo.micro_utils.common.MPPFile | ||||||
|  | import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions | ||||||
|  | import dev.inmo.micro_utils.ktor.common.DefaultTemporalFilesSubPath | ||||||
|  | import dev.inmo.micro_utils.ktor.common.TemporalFileId | ||||||
|  | import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator | ||||||
|  | import io.ktor.application.call | ||||||
|  | import io.ktor.http.HttpStatusCode | ||||||
|  | import io.ktor.http.content.PartData | ||||||
|  | import io.ktor.http.content.streamProvider | ||||||
|  | import io.ktor.request.receiveMultipart | ||||||
|  | import io.ktor.response.respond | ||||||
|  | import io.ktor.routing.Route | ||||||
|  | import io.ktor.routing.post | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import kotlinx.coroutines.flow.* | ||||||
|  | import kotlinx.coroutines.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  | import java.io.File | ||||||
|  | import java.nio.file.Files | ||||||
|  | import java.nio.file.attribute.FileTime | ||||||
|  |  | ||||||
|  | class TemporalFilesRoutingConfigurator( | ||||||
|  |     private val subpath: String = DefaultTemporalFilesSubPath, | ||||||
|  |     private val unifiedRouter: UnifiedRouter = UnifiedRouter.default, | ||||||
|  |     private val temporalFilesUtilizer: TemporalFilesUtilizer = TemporalFilesUtilizer | ||||||
|  | ) : ApplicationRoutingConfigurator.Element { | ||||||
|  |     interface TemporalFilesUtilizer { | ||||||
|  |         fun start(filesMap: MutableMap<TemporalFileId, MPPFile>, filesMutex: Mutex, onNewFileFlow: Flow<TemporalFileId>): Job | ||||||
|  |  | ||||||
|  |         companion object : TemporalFilesUtilizer { | ||||||
|  |             class ByTimerUtilizer( | ||||||
|  |                 private val removeMillis: Long, | ||||||
|  |                 private val scope: CoroutineScope | ||||||
|  |             ) : TemporalFilesUtilizer { | ||||||
|  |                 override fun start( | ||||||
|  |                     filesMap: MutableMap<TemporalFileId, MPPFile>, | ||||||
|  |                     filesMutex: Mutex, | ||||||
|  |                     onNewFileFlow: Flow<TemporalFileId> | ||||||
|  |                 ): Job = scope.launchSafelyWithoutExceptions { | ||||||
|  |                     while (isActive) { | ||||||
|  |                         val filesWithCreationInfo = filesMap.mapNotNull { (fileId, file) -> | ||||||
|  |                             fileId to ((Files.getAttribute(file.toPath(), "creationTime") as? FileTime) ?.toMillis() ?: return@mapNotNull null) | ||||||
|  |                         } | ||||||
|  |                         if (filesWithCreationInfo.isEmpty()) { | ||||||
|  |                             delay(removeMillis) | ||||||
|  |                             continue | ||||||
|  |                         } | ||||||
|  |                         var min = filesWithCreationInfo.first() | ||||||
|  |                         for (fileWithCreationInfo in filesWithCreationInfo) { | ||||||
|  |                             if (fileWithCreationInfo.second < min.second) { | ||||||
|  |                                 min = fileWithCreationInfo | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         delay(System.currentTimeMillis() - (min.second + removeMillis)) | ||||||
|  |                         filesMutex.withLock { | ||||||
|  |                             filesMap.remove(min.first) | ||||||
|  |                         } ?.delete() | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             override fun start( | ||||||
|  |                 filesMap: MutableMap<TemporalFileId, MPPFile>, | ||||||
|  |                 filesMutex: Mutex, | ||||||
|  |                 onNewFileFlow: Flow<TemporalFileId> | ||||||
|  |             ): Job = Job() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private val temporalFilesMap = mutableMapOf<TemporalFileId, MPPFile>() | ||||||
|  |     private val temporalFilesMutex = Mutex() | ||||||
|  |     private val filesFlow = MutableSharedFlow<TemporalFileId>() | ||||||
|  |     val utilizerJob = temporalFilesUtilizer.start(temporalFilesMap, temporalFilesMutex, filesFlow.asSharedFlow()) | ||||||
|  |  | ||||||
|  |     override fun Route.invoke() { | ||||||
|  |         post(subpath) { | ||||||
|  |             unifiedRouter.apply { | ||||||
|  |                 val multipart = call.receiveMultipart() | ||||||
|  |  | ||||||
|  |                 var fileInfo: Pair<TemporalFileId, MPPFile>? = null | ||||||
|  |                 var part = multipart.readPart() | ||||||
|  |  | ||||||
|  |                 while (part != null) { | ||||||
|  |                     if (part is PartData.FileItem) { | ||||||
|  |                         break | ||||||
|  |                     } | ||||||
|  |                     part = multipart.readPart() | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 part ?.let { | ||||||
|  |                     if (it is PartData.FileItem) { | ||||||
|  |                         val fileId = TemporalFileId(uuid4().toString()) | ||||||
|  |                         val fileName = it.originalFileName ?.let { FileName(it) } ?: return@let | ||||||
|  |                         fileInfo = fileId to File.createTempFile(fileId.string, ".${fileName.extension}").apply { | ||||||
|  |                             outputStream().use { outputStream -> | ||||||
|  |                                 it.streamProvider().use { | ||||||
|  |                                     it.copyTo(outputStream) | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                             deleteOnExit() | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 fileInfo ?.also { (fileId, file) -> | ||||||
|  |                     temporalFilesMutex.withLock { | ||||||
|  |                         temporalFilesMap[fileId] = file | ||||||
|  |                     } | ||||||
|  |                     call.respond(fileId.string) | ||||||
|  |                     launchSafelyWithoutExceptions { filesFlow.emit(fileId) } | ||||||
|  |                 } ?: call.respond(HttpStatusCode.BadRequest) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     suspend fun removeTemporalFile(temporalFileId: TemporalFileId) { | ||||||
|  |         temporalFilesMutex.withLock { | ||||||
|  |             temporalFilesMap.remove(temporalFileId) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun getTemporalFile(temporalFileId: TemporalFileId) = temporalFilesMap[temporalFileId] | ||||||
|  |  | ||||||
|  |     suspend fun getAndRemoveTemporalFile(temporalFileId: TemporalFileId) = temporalFilesMutex.withLock { | ||||||
|  |         temporalFilesMap.remove(temporalFileId) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -4,8 +4,8 @@ buildscript { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     dependencies { |     dependencies { | ||||||
|         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" |         classpath libs.buildscript.kt.gradle | ||||||
|         classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" |         classpath libs.buildscript.kt.serialization | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -16,11 +16,11 @@ plugins { | |||||||
| } | } | ||||||
|  |  | ||||||
| dependencies { | dependencies { | ||||||
|     implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" |     implementation libs.kt.stdlib | ||||||
|     implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_core_version" |     implementation libs.kt.serialization | ||||||
|  |  | ||||||
|     implementation "io.ktor:ktor-client-core:$ktor_version" |     implementation libs.ktor.client | ||||||
|     implementation "io.ktor:ktor-client-java:$ktor_version" |     implementation libs.ktor.client.java | ||||||
| } | } | ||||||
|  |  | ||||||
| mainClassName="MainKt" | mainClassName="MainKt" | ||||||
|   | |||||||
							
								
								
									
										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) | ||||||
| @@ -23,7 +23,7 @@ kotlin { | |||||||
|         commonMain { |         commonMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 implementation kotlin('stdlib') |                 implementation kotlin('stdlib') | ||||||
|                 api "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_core_version" |                 implementation libs.kt.serialization | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         commonTest { |         commonTest { | ||||||
| @@ -46,8 +46,8 @@ kotlin { | |||||||
|         androidTest { |         androidTest { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 implementation kotlin('test-junit') |                 implementation kotlin('test-junit') | ||||||
|                 implementation "androidx.test.ext:junit:$test_ext_junit_version" |                 implementation libs.android.test.junit | ||||||
|                 implementation "androidx.test.espresso:espresso-core:$espresso_core" |                 implementation libs.android.espresso | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										72
									
								
								mppProjectWithSerializationAndCompose.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								mppProjectWithSerializationAndCompose.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | |||||||
|  | project.version = "$version" | ||||||
|  | project.group = "$group" | ||||||
|  |  | ||||||
|  | apply from: "$publishGradlePath" | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     jvm { | ||||||
|  |         compilations.main { | ||||||
|  |             kotlinOptions { | ||||||
|  |                 jvmTarget = "1.8" | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     js (IR) { | ||||||
|  |         browser() | ||||||
|  |         nodejs() | ||||||
|  |     } | ||||||
|  |     android { | ||||||
|  |         publishAllLibraryVariants() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     sourceSets { | ||||||
|  |         commonMain { | ||||||
|  |             dependencies { | ||||||
|  |                 implementation kotlin('stdlib') | ||||||
|  |                 implementation libs.kt.serialization | ||||||
|  |                 implementation compose.runtime | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         commonTest { | ||||||
|  |             dependencies { | ||||||
|  |                 implementation kotlin('test-common') | ||||||
|  |                 implementation kotlin('test-annotations-common') | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         jvmMain { | ||||||
|  |             dependencies { | ||||||
|  |                 implementation compose.desktop.currentOs | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         jvmTest { | ||||||
|  |             dependencies { | ||||||
|  |                 implementation kotlin('test-junit') | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         jsMain { | ||||||
|  |             dependencies { | ||||||
|  |                 implementation compose.web.core | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         jsTest { | ||||||
|  |             dependencies { | ||||||
|  |                 implementation kotlin('test-js') | ||||||
|  |                 implementation kotlin('test-junit') | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         androidTest { | ||||||
|  |             dependencies { | ||||||
|  |                 implementation kotlin('test-junit') | ||||||
|  |                 implementation libs.android.test.junit | ||||||
|  |                 implementation libs.android.espresso | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$defaultAndroidSettingsPresetPath" | ||||||
|  |  | ||||||
|  | java { | ||||||
|  |     sourceCompatibility = JavaVersion.VERSION_1_8 | ||||||
|  |     targetCompatibility = JavaVersion.VERSION_1_8 | ||||||
|  | } | ||||||
| @@ -25,7 +25,7 @@ interface Pagination : ClosedRange<Int> { | |||||||
|     override val start: Int |     override val start: Int | ||||||
|         get() = page * size |         get() = page * size | ||||||
|     override val endInclusive: Int |     override val endInclusive: Int | ||||||
|         get() = lastIndex |         get() = start + size - 1 | ||||||
| } | } | ||||||
|  |  | ||||||
| fun Pagination.intersect( | fun Pagination.intersect( | ||||||
|   | |||||||
| @@ -38,3 +38,31 @@ fun <T> Set<T>.paginate(with: Pagination): PaginationResult<T> { | |||||||
|         size.toLong() |         size.toLong() | ||||||
|     ) |     ) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | fun <T> Iterable<T>.optionallyReverse(reverse: Boolean): Iterable<T> = when (this) { | ||||||
|  |     is List<T> -> optionallyReverse(reverse) | ||||||
|  |     is Set<T> -> optionallyReverse(reverse) | ||||||
|  |     else -> if (reverse) { | ||||||
|  |         reversed() | ||||||
|  |     } else { | ||||||
|  |         this | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | fun <T> List<T>.optionallyReverse(reverse: Boolean): List<T> = if (reverse) { | ||||||
|  |     reversed() | ||||||
|  | } else { | ||||||
|  |     this | ||||||
|  | } | ||||||
|  | fun <T> Set<T>.optionallyReverse(reverse: Boolean): Set<T> = if (reverse) { | ||||||
|  |     reversed().toSet() | ||||||
|  | } else { | ||||||
|  |     this | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <reified T> Array<T>.optionallyReverse(reverse: Boolean) = if (reverse) { | ||||||
|  |     Array(size) { | ||||||
|  |         get(lastIndex - it) | ||||||
|  |     } | ||||||
|  | } else { | ||||||
|  |     this | ||||||
|  | } | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import dev.inmo.micro_utils.pagination.* | |||||||
|  * Example: |  * Example: | ||||||
|  * |  * | ||||||
|  * * `|__f__l_______________________|` will be transformed to `|_______________________f__l__|` |  * * `|__f__l_______________________|` will be transformed to `|_______________________f__l__|` | ||||||
|  * * `|__f__l_|` will be transformed to `|__f__l_|` |  * * `|__f__l_|` will be transformed to `|_f__l__|` | ||||||
|  * |  * | ||||||
|  * @return Reversed version of this [Pagination] |  * @return Reversed version of this [Pagination] | ||||||
|  */ |  */ | ||||||
| @@ -26,3 +26,15 @@ fun Pagination.reverse(datasetSize: Long): SimplePagination { | |||||||
|  * Shortcut for [reverse] |  * Shortcut for [reverse] | ||||||
|  */ |  */ | ||||||
| fun Pagination.reverse(objectsCount: Int) = reverse(objectsCount.toLong()) | fun Pagination.reverse(objectsCount: Int) = reverse(objectsCount.toLong()) | ||||||
|  |  | ||||||
|  | fun Pagination.optionallyReverse(objectsCount: Int, reverse: Boolean) = if (reverse) { | ||||||
|  |     reverse(objectsCount) | ||||||
|  | } else { | ||||||
|  |     this | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Pagination.optionallyReverse(objectsCount: Long, reverse: Boolean) = if (reverse) { | ||||||
|  |     reverse(objectsCount) | ||||||
|  | } else { | ||||||
|  |     this | ||||||
|  | } | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ kotlin { | |||||||
|         } |         } | ||||||
|         jvmMain { |         jvmMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "org.jetbrains.exposed:exposed-core:$kotlin_exposed_version" |                 api libs.jb.exposed | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -15,8 +15,8 @@ kotlin { | |||||||
|  |  | ||||||
|         jvmMain { |         jvmMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "io.ktor:ktor-server:$ktor_version" |                 api libs.ktor.server | ||||||
|                 api "io.ktor:ktor-server-host-common:$ktor_version" |                 api libs.ktor.server.host.common | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| apply plugin: 'maven-publish' | apply plugin: 'maven-publish' | ||||||
| apply plugin: 'signing' |  | ||||||
|  |  | ||||||
| task javadocsJar(type: Jar) { | task javadocsJar(type: Jar) { | ||||||
|     classifier = 'javadoc' |     classifier = 'javadoc' | ||||||
| @@ -70,7 +69,18 @@ publishing { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|      |      | ||||||
| signing { | if (project.hasProperty("signing.gnupg.keyName")) { | ||||||
|  |     apply plugin: 'signing' | ||||||
|  |      | ||||||
|  |     signing { | ||||||
|         useGpgCmd() |         useGpgCmd() | ||||||
|  |      | ||||||
|         sign publishing.publications |         sign publishing.publications | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     task signAll { | ||||||
|  |         tasks.withType(Sign).forEach { | ||||||
|  |             dependsOn(it) | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| {"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","includeGpgSigning":true,"developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}]}} | {"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}}} | ||||||
| @@ -10,10 +10,10 @@ kotlin { | |||||||
|     sourceSets { |     sourceSets { | ||||||
|         commonMain { |         commonMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" |                 api libs.kt.coroutines | ||||||
|                 api internalProject("micro_utils.pagination.common") |                 api internalProject("micro_utils.pagination.common") | ||||||
|  |  | ||||||
|                 api "com.benasher44:uuid:$uuidVersion" |                 api libs.uuid | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -24,7 +24,7 @@ kotlin { | |||||||
|         } |         } | ||||||
|         androidMain { |         androidMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "androidx.core:core-ktx:$core_ktx_version" |                 api libs.android.coreKtx | ||||||
|                 api internalProject("micro_utils.common") |                 api internalProject("micro_utils.common") | ||||||
|                 api internalProject("micro_utils.coroutines") |                 api internalProject("micro_utils.coroutines") | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -13,12 +13,12 @@ interface VersionsRepo<T> : Repo { | |||||||
|      * By default, instance of this interface will check that version of table with name [tableName] is less than |      * By default, instance of this interface will check that version of table with name [tableName] is less than | ||||||
|      * [version] or is absent |      * [version] or is absent | ||||||
|      * |      * | ||||||
|      * * In case if [tableName] didn't found, will be called [onCreate] and version of table will be set up to [version] |      * In case if [tableName] didn't found, will be called [onCreate]. Then in case if [tableName] have version less | ||||||
|      * * In case if [tableName] have version less than parameter [version], it will increase version one-by-one |      * than parameter [version] or null, it will increase version one-by-one until database version will be equal to | ||||||
|      * until database version will be equal to [version] |      * [version] | ||||||
|      * |      * | ||||||
|      * @param version Current version of table |      * @param version Current version of table | ||||||
|      * @param onCreate This callback will be called in case when table have no information about table |      * @param onCreate This callback will be called in case when repo have no information about table | ||||||
|      * @param onUpdate This callback will be called after **iterative** changing of version. It is expected that parameter |      * @param onUpdate This callback will be called after **iterative** changing of version. It is expected that parameter | ||||||
|      * "to" will always be greater than "from" |      * "to" will always be greater than "from" | ||||||
|      */ |      */ | ||||||
|   | |||||||
| @@ -0,0 +1,27 @@ | |||||||
|  | package dev.inmo.micro_utils.repos | ||||||
|  |  | ||||||
|  | import android.database.Cursor | ||||||
|  |  | ||||||
|  | class CursorIterator( | ||||||
|  |     private val c: Cursor | ||||||
|  | ) : Iterator<Cursor> { | ||||||
|  |     private var i = 0 | ||||||
|  |  | ||||||
|  |     init { | ||||||
|  |         c.moveToFirst() | ||||||
|  |     } | ||||||
|  |     override fun hasNext(): Boolean { | ||||||
|  |         return i < c.count | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun next(): Cursor { | ||||||
|  |         i++ | ||||||
|  |         return if (c.moveToNext()) { | ||||||
|  |             c | ||||||
|  |         } else { | ||||||
|  |             throw NoSuchElementException() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | operator fun Cursor.iterator(): CursorIterator = CursorIterator(this) | ||||||
| @@ -143,7 +143,12 @@ class OneToManyAndroidRepo<Key, Value>( | |||||||
|     }.toLong() |     }.toLong() | ||||||
|  |  | ||||||
|     override suspend fun count(k: Key): Long = helper.blockingReadableTransaction { |     override suspend fun count(k: Key): Long = helper.blockingReadableTransaction { | ||||||
|         selectDistinct(tableName, columns = valueColumnArray, selection = "$idColumnName=?", selectionArgs = arrayOf(k.keyAsString()), limit = FirstPagePagination(1).limitClause()).use { |         selectDistinct( | ||||||
|  |             tableName, | ||||||
|  |             columns = valueColumnArray, | ||||||
|  |             selection = "$idColumnName=?", | ||||||
|  |             selectionArgs = arrayOf(k.keyAsString()) | ||||||
|  |         ).use { | ||||||
|             it.count |             it.count | ||||||
|         } |         } | ||||||
|     }.toLong() |     }.toLong() | ||||||
|   | |||||||
| @@ -17,13 +17,19 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>( | |||||||
|     ExposedCRUDRepo<ObjectType, IdType>, |     ExposedCRUDRepo<ObjectType, IdType>, | ||||||
|     WriteStandardCRUDRepo<ObjectType, IdType, InputValueType> |     WriteStandardCRUDRepo<ObjectType, IdType, InputValueType> | ||||||
| { | { | ||||||
|     protected val newObjectsChannel = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize) |     protected val _newObjectsFlow = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize) | ||||||
|     protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize) |     protected val _updatedObjectsFlow = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize) | ||||||
|     protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(replyCacheInFlows, flowsChannelsSize) |     protected val _deletedObjectsIdsFlow = MutableSharedFlow<IdType>(replyCacheInFlows, flowsChannelsSize) | ||||||
|  |     @Deprecated("Renamed", ReplaceWith("_newObjectsFlow")) | ||||||
|  |     protected val newObjectsChannel = _newObjectsFlow | ||||||
|  |     @Deprecated("Renamed", ReplaceWith("_updatedObjectsFlow")) | ||||||
|  |     protected val updateObjectsChannel = _updatedObjectsFlow | ||||||
|  |     @Deprecated("Renamed", ReplaceWith("_deletedObjectsIdsFlow")) | ||||||
|  |     protected val deleteObjectsIdsChannel = _deletedObjectsIdsFlow | ||||||
|  |  | ||||||
|     override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asSharedFlow() |     override val newObjectsFlow: Flow<ObjectType> = _newObjectsFlow.asSharedFlow() | ||||||
|     override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow() |     override val updatedObjectsFlow: Flow<ObjectType> = _updatedObjectsFlow.asSharedFlow() | ||||||
|     override val deletedObjectsIdsFlow: Flow<IdType> = deleteObjectsIdsChannel.asSharedFlow() |     override val deletedObjectsIdsFlow: Flow<IdType> = _deletedObjectsIdsFlow.asSharedFlow() | ||||||
|  |  | ||||||
|     protected abstract fun InsertStatement<Number>.asObject(value: InputValueType): ObjectType |     protected abstract fun InsertStatement<Number>.asObject(value: InputValueType): ObjectType | ||||||
|     abstract val selectByIds: SqlExpressionBuilder.(List<IdType>) -> Op<Boolean> |     abstract val selectByIds: SqlExpressionBuilder.(List<IdType>) -> Op<Boolean> | ||||||
| @@ -43,7 +49,7 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>( | |||||||
|         return transaction(db = database) { |         return transaction(db = database) { | ||||||
|             values.map { value -> createWithoutNotification(value) } |             values.map { value -> createWithoutNotification(value) } | ||||||
|         }.onEach { |         }.onEach { | ||||||
|             newObjectsChannel.emit(it) |             _newObjectsFlow.emit(it) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -74,7 +80,7 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>( | |||||||
|         onBeforeUpdate(listOf(id to value)) |         onBeforeUpdate(listOf(id to value)) | ||||||
|         return updateWithoutNotification(id, value).also { |         return updateWithoutNotification(id, value).also { | ||||||
|             if (it != null) { |             if (it != null) { | ||||||
|                 updateObjectsChannel.emit(it) |                 _updatedObjectsFlow.emit(it) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -85,16 +91,25 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>( | |||||||
|                 values.map { (id, value) -> updateWithoutNotification(id, value) } |                 values.map { (id, value) -> updateWithoutNotification(id, value) } | ||||||
|             }.filterNotNull() |             }.filterNotNull() | ||||||
|         ).onEach { |         ).onEach { | ||||||
|             updateObjectsChannel.emit(it) |             _updatedObjectsFlow.emit(it) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     protected open suspend fun onBeforeDelete(ids: List<IdType>) {} |     protected open suspend fun onBeforeDelete(ids: List<IdType>) {} | ||||||
|     override suspend fun deleteById(ids: List<IdType>) { |     override suspend fun deleteById(ids: List<IdType>) { | ||||||
|         onBeforeDelete(ids) |         onBeforeDelete(ids) | ||||||
|         transaction(db = database) { |         transaction(db = database) { | ||||||
|             deleteWhere(null, null) { |             val deleted = deleteWhere(null, null) { | ||||||
|                 selectByIds(ids) |                 selectByIds(ids) | ||||||
|             } |             } | ||||||
|  |             if (deleted == ids.size) { | ||||||
|  |                 ids | ||||||
|  |             } else { | ||||||
|  |                 ids.filter { | ||||||
|  |                     select { selectById(it) }.limit(1).none() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }.forEach { | ||||||
|  |             _deletedObjectsIdsFlow.emit(it) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,8 +1,10 @@ | |||||||
| package dev.inmo.micro_utils.repos.exposed | package dev.inmo.micro_utils.repos.exposed | ||||||
|  |  | ||||||
| import dev.inmo.micro_utils.repos.Repo | import dev.inmo.micro_utils.repos.Repo | ||||||
| import org.jetbrains.exposed.sql.Database | import org.jetbrains.exposed.sql.* | ||||||
|  |  | ||||||
| interface ExposedRepo : Repo { | interface ExposedRepo : Repo, FieldSet { | ||||||
|     val database: Database |     val database: Database | ||||||
|  |     val selectAll: Transaction.() -> Query | ||||||
|  |         get() = { (this@ExposedRepo as FieldSet).selectAll() } | ||||||
| } | } | ||||||
| @@ -12,8 +12,8 @@ open class ExposedReadKeyValueRepo<Key, Value>( | |||||||
|     valueColumnAllocator: ColumnAllocator<Value>, |     valueColumnAllocator: ColumnAllocator<Value>, | ||||||
|     tableName: String? = null |     tableName: String? = null | ||||||
| ) : ReadStandardKeyValueRepo<Key, Value>, ExposedRepo, Table(tableName ?: "") { | ) : ReadStandardKeyValueRepo<Key, Value>, ExposedRepo, Table(tableName ?: "") { | ||||||
|     protected val keyColumn: Column<Key> = keyColumnAllocator() |     val keyColumn: Column<Key> = keyColumnAllocator() | ||||||
|     protected val valueColumn: Column<Value> = valueColumnAllocator() |     val valueColumn: Column<Value> = valueColumnAllocator() | ||||||
|     override val primaryKey: PrimaryKey = PrimaryKey(keyColumn, valueColumn) |     override val primaryKey: PrimaryKey = PrimaryKey(keyColumn, valueColumn) | ||||||
|  |  | ||||||
|     init { initTable() } |     init { initTable() } | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.* | |||||||
| import org.jetbrains.exposed.sql.* | import org.jetbrains.exposed.sql.* | ||||||
| import org.jetbrains.exposed.sql.transactions.transaction | import org.jetbrains.exposed.sql.transactions.transaction | ||||||
|  |  | ||||||
|  | typealias ExposedKeyValuesRepo<Key, Value> = ExposedOneToManyKeyValueRepo<Key, Value> | ||||||
| open class ExposedOneToManyKeyValueRepo<Key, Value>( | open class ExposedOneToManyKeyValueRepo<Key, Value>( | ||||||
|     database: Database, |     database: Database, | ||||||
|     keyColumnAllocator: ColumnAllocator<Key>, |     keyColumnAllocator: ColumnAllocator<Key>, | ||||||
| @@ -34,10 +35,15 @@ open class ExposedOneToManyKeyValueRepo<Key, Value>( | |||||||
|                     if (select { keyColumn.eq(k).and(valueColumn.eq(v)) }.limit(1).count() > 0) { |                     if (select { keyColumn.eq(k).and(valueColumn.eq(v)) }.limit(1).count() > 0) { | ||||||
|                         return@mapNotNull null |                         return@mapNotNull null | ||||||
|                     } |                     } | ||||||
|                     insertIgnore { |                     val insertResult = insert { | ||||||
|                         it[keyColumn] = k |                         it[keyColumn] = k | ||||||
|                         it[valueColumn] = v |                         it[valueColumn] = v | ||||||
|                     }.getOrNull(keyColumn) ?.let { k to v } |                     } | ||||||
|  |                     if (insertResult.insertedCount > 0) { | ||||||
|  |                         k to v | ||||||
|  |                     } else { | ||||||
|  |                         null | ||||||
|  |                     } | ||||||
|                 } ?: emptyList() |                 } ?: emptyList() | ||||||
|             } |             } | ||||||
|         }.forEach { _onNewValue.emit(it) } |         }.forEach { _onNewValue.emit(it) } | ||||||
| @@ -47,7 +53,7 @@ open class ExposedOneToManyKeyValueRepo<Key, Value>( | |||||||
|         transaction(database) { |         transaction(database) { | ||||||
|             toRemove.keys.flatMap { k -> |             toRemove.keys.flatMap { k -> | ||||||
|                 toRemove[k] ?.mapNotNull { v -> |                 toRemove[k] ?.mapNotNull { v -> | ||||||
|                     if (deleteIgnoreWhere { keyColumn.eq(k).and(valueColumn.eq(v)) } > 0 ) { |                     if (deleteWhere { keyColumn.eq(k).and(valueColumn.eq(v)) } > 0 ) { | ||||||
|                         k to v |                         k to v | ||||||
|                     } else { |                     } else { | ||||||
|                         null |                         null | ||||||
|   | |||||||
| @@ -3,17 +3,20 @@ package dev.inmo.micro_utils.repos.exposed.onetomany | |||||||
| import dev.inmo.micro_utils.pagination.* | import dev.inmo.micro_utils.pagination.* | ||||||
| import dev.inmo.micro_utils.repos.ReadOneToManyKeyValueRepo | import dev.inmo.micro_utils.repos.ReadOneToManyKeyValueRepo | ||||||
| import dev.inmo.micro_utils.repos.exposed.* | import dev.inmo.micro_utils.repos.exposed.* | ||||||
|  | import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedReadKeyValueRepo | ||||||
| import org.jetbrains.exposed.sql.* | import org.jetbrains.exposed.sql.* | ||||||
| import org.jetbrains.exposed.sql.transactions.transaction | import org.jetbrains.exposed.sql.transactions.transaction | ||||||
|  |  | ||||||
|  | typealias ExposedReadKeyValuesRepo<Key, Value> = ExposedReadOneToManyKeyValueRepo<Key, Value> | ||||||
|  |  | ||||||
| open class ExposedReadOneToManyKeyValueRepo<Key, Value>( | open class ExposedReadOneToManyKeyValueRepo<Key, Value>( | ||||||
|     override val database: Database, |     override val database: Database, | ||||||
|     keyColumnAllocator: ColumnAllocator<Key>, |     keyColumnAllocator: ColumnAllocator<Key>, | ||||||
|     valueColumnAllocator: ColumnAllocator<Value>, |     valueColumnAllocator: ColumnAllocator<Value>, | ||||||
|     tableName: String? = null |     tableName: String? = null | ||||||
| ) : ReadOneToManyKeyValueRepo<Key, Value>, ExposedRepo, Table(tableName ?: "") { | ) : ReadOneToManyKeyValueRepo<Key, Value>, ExposedRepo, Table(tableName ?: "") { | ||||||
|     protected val keyColumn: Column<Key> = keyColumnAllocator() |     val keyColumn: Column<Key> = keyColumnAllocator() | ||||||
|     protected val valueColumn: Column<Value> = valueColumnAllocator() |     val valueColumn: Column<Value> = valueColumnAllocator() | ||||||
|  |  | ||||||
|     init { initTable() } |     init { initTable() } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -18,8 +18,8 @@ inline fun versionsRepo(database: Database): VersionsRepo<Database> = StandardVe | |||||||
| class ExposedStandardVersionsRepoProxy( | class ExposedStandardVersionsRepoProxy( | ||||||
|     override val database: Database |     override val database: Database | ||||||
| ) : StandardVersionsRepoProxy<Database>, Table("ExposedVersionsProxy"), ExposedRepo { | ) : StandardVersionsRepoProxy<Database>, Table("ExposedVersionsProxy"), ExposedRepo { | ||||||
|     private val tableNameColumn = text("tableName") |     val tableNameColumn = text("tableName") | ||||||
|     private val tableVersionColumn = integer("tableName") |     val tableVersionColumn = integer("tableVersion") | ||||||
|  |  | ||||||
|     init { |     init { | ||||||
|         initTable() |         initTable() | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ kotlin { | |||||||
|     sourceSets { |     sourceSets { | ||||||
|         commonMain { |         commonMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" |                 api libs.kt.coroutines | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -11,8 +11,7 @@ open class TypedSerializer<T : Any>( | |||||||
|     presetSerializers: Map<String, KSerializer<out T>> = emptyMap(), |     presetSerializers: Map<String, KSerializer<out T>> = emptyMap(), | ||||||
| ) : KSerializer<T> { | ) : KSerializer<T> { | ||||||
|     protected val serializers = presetSerializers.toMutableMap() |     protected val serializers = presetSerializers.toMutableMap() | ||||||
|     @ExperimentalSerializationApi |     @OptIn(InternalSerializationApi::class) | ||||||
|     @InternalSerializationApi |  | ||||||
|     override val descriptor: SerialDescriptor = buildSerialDescriptor( |     override val descriptor: SerialDescriptor = buildSerialDescriptor( | ||||||
|         "TypedSerializer", |         "TypedSerializer", | ||||||
|         SerialKind.CONTEXTUAL |         SerialKind.CONTEXTUAL | ||||||
| @@ -21,8 +20,7 @@ open class TypedSerializer<T : Any>( | |||||||
|         element("value", ContextualSerializer(kClass).descriptor) |         element("value", ContextualSerializer(kClass).descriptor) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @ExperimentalSerializationApi |     @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) | ||||||
|     @InternalSerializationApi |  | ||||||
|     override fun deserialize(decoder: Decoder): T { |     override fun deserialize(decoder: Decoder): T { | ||||||
|         return decoder.decodeStructure(descriptor) { |         return decoder.decodeStructure(descriptor) { | ||||||
|             var type: String? = null |             var type: String? = null | ||||||
| @@ -46,14 +44,12 @@ open class TypedSerializer<T : Any>( | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @ExperimentalSerializationApi |     @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) | ||||||
|     @InternalSerializationApi |  | ||||||
|     protected open fun <O: T> CompositeEncoder.encode(value: O) { |     protected open fun <O: T> CompositeEncoder.encode(value: O) { | ||||||
|         encodeSerializableElement(descriptor, 1, value::class.serializer() as KSerializer<O>, value) |         encodeSerializableElement(descriptor, 1, value::class.serializer() as KSerializer<O>, value) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @ExperimentalSerializationApi |     @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) | ||||||
|     @InternalSerializationApi |  | ||||||
|     override fun serialize(encoder: Encoder, value: T) { |     override fun serialize(encoder: Encoder, value: T) { | ||||||
|         encoder.encodeStructure(descriptor) { |         encoder.encodeStructure(descriptor) { | ||||||
|             val valueSerializer = value::class.serializer() |             val valueSerializer = value::class.serializer() | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ rootProject.name='micro_utils' | |||||||
|  |  | ||||||
| String[] includes = [ | String[] includes = [ | ||||||
|     ":common", |     ":common", | ||||||
|  |     ":common:compose", | ||||||
|     ":matrix", |     ":matrix", | ||||||
|     ":crypto", |     ":crypto", | ||||||
|     ":selector:common", |     ":selector:common", | ||||||
| @@ -23,6 +24,7 @@ String[] includes = [ | |||||||
|     ":ktor:common", |     ":ktor:common", | ||||||
|     ":ktor:client", |     ":ktor:client", | ||||||
|     ":coroutines", |     ":coroutines", | ||||||
|  |     ":coroutines:compose", | ||||||
|     ":android:recyclerview", |     ":android:recyclerview", | ||||||
|     ":android:alerts:common", |     ":android:alerts:common", | ||||||
|     ":android:alerts:recyclerview", |     ":android:alerts:recyclerview", | ||||||
| @@ -38,11 +40,13 @@ String[] includes = [ | |||||||
|  |  | ||||||
|  |  | ||||||
| includes.each { originalName -> | includes.each { originalName -> | ||||||
|     String projectDirectory = "${rootProject.projectDir.getAbsolutePath()}${originalName.replaceAll(":", File.separator)}" |     String projectDirectory = "${rootProject.projectDir.getAbsolutePath()}${originalName.replace(":", File.separator)}" | ||||||
|     String projectName = "${rootProject.name}${originalName.replaceAll(":", ".")}" |     String projectName = "${rootProject.name}${originalName.replace(":", ".")}" | ||||||
|     String projectIdentifier = ":${projectName}" |     String projectIdentifier = ":${projectName}" | ||||||
|     include projectIdentifier |     include projectIdentifier | ||||||
|     ProjectDescriptor project = project(projectIdentifier) |     ProjectDescriptor project = project(projectIdentifier) | ||||||
|     project.name = projectName |     project.name = projectName | ||||||
|     project.projectDir = new File(projectDirectory) |     project.projectDir = new File(projectDirectory) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | enableFeaturePreview("VERSION_CATALOGS") | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user