mirror of
				https://github.com/InsanusMokrassar/MicroUtils.git
				synced 2025-10-29 03:00:31 +00:00 
			
		
		
		
	Compare commits
	
		
			244 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 6bbbea0bc3 | |||
| e337cd98c8 | |||
| bcbab3b380 | |||
| fb63de7568 | |||
| aa45a4ab13 | |||
| 2af7e2f681 | |||
| 34fd9edce0 | |||
| 2a4cb8c5f9 | |||
| 50ea40bc3a | |||
| a77654052d | |||
| 88aafce552 | |||
| 4e95d6bfff | |||
| 38d0e34fb5 | |||
| 8fbc6b9041 | |||
| e8219d6cf4 | |||
| 6c20fc4ca6 | |||
| 85cd975492 | |||
| 1171a717fe | |||
| bbe5320312 | |||
| 00acb9fddd | |||
| de3d14dc41 | |||
| 67ff9cc9b3 | |||
| af132103a0 | |||
| 3b1124a804 | |||
| f226c2dfd6 | |||
| 69d6e63846 | |||
| 02c3d397ad | |||
| 67a1050646 | |||
| 8cd0775a6c | |||
| 162294d6c6 | |||
| c4dd19dd00 | |||
| d2314422f1 | |||
| 6fedd6f859 | |||
| e52b59665f | |||
| cda9d09689 | |||
| c9237b3f00 | |||
| 18bba66c4a | |||
| 63418c4a8a | |||
| 2e66c6f4e3 | |||
| e9c5df4c13 | |||
| bc7789ad2c | |||
| e3da761249 | |||
| 4082f65afa | |||
| 5d1cab075d | |||
| bcf67f7e59 | |||
| 7d3b1f8e75 | |||
| 119a0588cc | |||
| fab789d9c0 | |||
| ceba81c08f | |||
| a061af0558 | |||
| c7a53846ad | |||
| a683cccf0c | |||
| 50d41e35c1 | |||
| aa0e831cea | |||
| 44e26ccb4f | |||
| 2a783f6e2b | |||
| 6058d6a724 | |||
| 2e9c7eb5fa | |||
| e75465ad10 | |||
| de01ad54e9 | |||
| eeea7ddbe3 | |||
| e0b18bec05 | |||
| 410e89bba9 | |||
| 9ef19dc42b | |||
| 0337d1b82d | |||
| f5bd4c5ccb | |||
| 630f9bc0d4 | |||
| 18b4ffece1 | |||
| f64e1effa3 | |||
| 847fcbb488 | |||
| 88002ec8e7 | |||
| 7f8db6a29d | |||
| b183b82443 | |||
| 5dad27de72 | |||
| 6b66084d0e | |||
| 50b56a7c39 | |||
| 7ab7d14471 | |||
| bdcc179b7b | |||
| 55ffd4b46f | |||
| 7fc5ee70e1 | |||
| a24a335743 | |||
| ef9af71960 | |||
| 925702d315 | |||
| d50dffec8c | |||
| cef2081a13 | |||
| 06c8bde7c9 | |||
| c9bbfa3820 | |||
| eed7cfdc42 | |||
| bd9b0d16ab | |||
| ea6c33b497 | |||
| dc80ade2fb | |||
| f6a06ee8ea | |||
| 2644f27975 | |||
| 3dc68a7b8b | |||
| 97fc1d6239 | |||
| 662f4d22a3 | |||
| b70aa12be9 | |||
| 71f12f5f19 | |||
| e10504eeeb | |||
| 2dea9f3bc0 | |||
| 35c9dda5bc | |||
| e831f3949a | |||
| b0b39cc693 | |||
| fc03be3f73 | |||
| b61f6b81f1 | |||
| f5bc1c1fce | |||
| a729f9568c | |||
| 5749e00377 | |||
| ef73c24a0c | |||
| 94717ee351 | |||
| 9a18ded65b | |||
| b23220f491 | |||
| 6e6bb03246 | |||
| 1ae6bae3b8 | |||
| 1239ca3256 | |||
| 57b7797ea4 | |||
| 5ee5bfd1d5 | |||
| 7229a3e198 | |||
| bee083582f | |||
| 6ef403853c | 
							
								
								
									
										12
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,12 +0,0 @@ | ||||
| name: Regular build | ||||
| on: [push] | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: actions/setup-java@v1 | ||||
|         with: | ||||
|           java-version: 1.8 | ||||
|       - name: Build | ||||
|         run: ./gradlew build | ||||
							
								
								
									
										5
									
								
								.github/workflows/dokka_push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.github/workflows/dokka_push.yml
									
									
									
									
										vendored
									
									
								
							| @@ -10,7 +10,10 @@ jobs: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: actions/setup-java@v1 | ||||
|         with: | ||||
|           java-version: 1.8 | ||||
|           java-version: 11 | ||||
|       - name: Fix android 32.0.0 dx | ||||
|         continue-on-error: true | ||||
|         run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib  && mv d8.jar dx.jar | ||||
|       - name: Build | ||||
|         run: ./gradlew dokkaHtml | ||||
|       - name: Publish KDocs | ||||
|   | ||||
							
								
								
									
										8
									
								
								.github/workflows/packages_push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/packages_push.yml
									
									
									
									
										vendored
									
									
								
							| @@ -8,7 +8,10 @@ jobs: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: actions/setup-java@v1 | ||||
|         with: | ||||
|           java-version: 1.8 | ||||
|           java-version: 11 | ||||
|       - name: Fix android 32.0.0 dx | ||||
|         continue-on-error: true | ||||
|         run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib  && mv d8.jar dx.jar | ||||
|       - name: Rewrite version | ||||
|         run: | | ||||
|           branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" | ||||
| @@ -18,7 +21,8 @@ jobs: | ||||
|       - name: Build | ||||
|         run: ./gradlew build | ||||
|       - name: Publish | ||||
|         run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidDebugPublication -x signAndroidReleasePublication -x signKotlinMultiplatformPublication | ||||
|         continue-on-error: true | ||||
|         run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository | ||||
|         env: | ||||
|           GITHUBPACKAGES_USER: ${{ github.actor }} | ||||
|           GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} | ||||
|   | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -11,5 +11,6 @@ out/ | ||||
|  | ||||
| secret.gradle | ||||
| local.properties | ||||
| kotlin-js-store | ||||
|  | ||||
| publishing.sh | ||||
|   | ||||
							
								
								
									
										406
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										406
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,7 +1,413 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## 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 | ||||
|  | ||||
| * `Common`: | ||||
|     * Ranges intersection functionality | ||||
|     * New type `Optional` | ||||
| * `Pagination`: | ||||
|     * `Pagination` now extends `ClosedRange<Int>` | ||||
|     * `Pagination` intersection functionality | ||||
|  | ||||
| ## 0.8.2 | ||||
|  | ||||
| * `Versions`: | ||||
|     * `Klock`: `2.4.7` -> `2.4.8` | ||||
|     * `Serialization`: `1.3.0` -> `1.3.1` | ||||
| * `FSM`: | ||||
|     * Now it is possible to pass any `CheckableHandlerHolder` in `FSMBuilder` | ||||
|     * Now `StatesMachine` works with `CheckableHandlerHolder` instead of `CustomizableHandlerHolder` | ||||
|  | ||||
| ## 0.8.1 | ||||
|  | ||||
| * `Versions`: | ||||
|     * `Exposed`: `0.36.1` -> `0.36.2` | ||||
|     * `Core KTX`: `1.6.0` -> `1.7.0` | ||||
|  | ||||
| ## 0.8.0 | ||||
|  | ||||
| * `Versions`: | ||||
|     * `Klock`: `2.4.6` -> `2.4.7` | ||||
|     * `Ktor`: `1.6.4` -> `1.6.5` | ||||
|     * `Exposed`: `0.35.3` -> `0.36.1` | ||||
| * `Common`: | ||||
|     * Type `Either` got its own serializer | ||||
| * `FSM`: | ||||
|     * `Common`: | ||||
|         * Full rework of FSM: | ||||
|             * Now it is more flexible for checking of handler opportunity to handle state | ||||
|             * Now machine and states managers are type-oriented | ||||
|             * `StateHandlerHolder` has been renamed to `CheckableHandlerHolder` | ||||
|         * Add opportunity for comfortable adding default state handler | ||||
|  | ||||
| ## 0.7.4 | ||||
|  | ||||
| * `Common`: | ||||
|     * New type `Either` | ||||
| * `Serialization`: | ||||
|     * `TypedSerializer` | ||||
|         * New factory fun which accept vararg pairs of type and its serializer | ||||
| * `Repos`: | ||||
|     * `Common` (`Android`): | ||||
|         * `AbstractMutableAndroidCRUDRepo` flows now will have extra buffer capacity instead of reply. It means that | ||||
|           android crud repo _WILL NOT_ send previous events to the  | ||||
|     * `Exposed`: | ||||
|         * New parameter `AbstractExposedWriteCRUDRepo#replyCacheInFlows` | ||||
|         * KeyValue realization `ExposedKeyValueRepo` properties `_onNewValue` and `_onValueRemoved` now are available in | ||||
|           inheritors | ||||
| * `Pagination`: | ||||
|     * `Common`: | ||||
|         * New types `getAllBy*` for current, next and custom paging | ||||
|  | ||||
| ## 0.7.3 | ||||
|  | ||||
| * `Versions`: | ||||
|     * `Exposed`: `0.35.2` -> `0.35.3` | ||||
|  | ||||
| ## 0.7.2 | ||||
|  | ||||
| * `Versions`: | ||||
|     * `Klock`: `2.4.5` -> `2.4.6` | ||||
|  | ||||
| ## 0.7.1 | ||||
|  | ||||
| * `Versions`: | ||||
|     * `Klock`: `2.4.3` -> `2.4.5` | ||||
|     * `Exposed`: `0.35.1` -> `0.35.2` | ||||
| * `Coroutines`: | ||||
|     * `Common`: | ||||
|         * New `Flow` - `AccumulatorFlow` | ||||
| * `FSM`: | ||||
|     * `Common`: | ||||
|         * `InMemoryStatesManager` has been replaced | ||||
|         * `StatesMachine` became an interface | ||||
|         * New manager `DefaultStatesManager` with `DefaultStatesManagerRepo` for abstraction of manager and storing of | ||||
|           data info | ||||
|  | ||||
| ## 0.7.0 | ||||
|  | ||||
| **THIS VERSION HAS MIGRATED FROM KOTLINX DATETIME TO KORLIBS KLOCK. CAREFUL** | ||||
|  | ||||
| * `Versions` | ||||
|     * `kotlinx.datetime` -> `Klock` | ||||
|  | ||||
| ## 0.6.0 DO NOT RECOMMENDED | ||||
|  | ||||
| **THIS VERSION HAS MIGRATED FROM KORLIBS KLOCK TO KOTLINX DATETIME. CAREFUL** | ||||
| **ALL DEPRECATION HAVE BEEN REMOVED** | ||||
|  | ||||
| * `Versions` | ||||
|     * `Klock` -> `kotlinx.datetime` | ||||
|  | ||||
| ## 0.5.31 | ||||
|  | ||||
| * `Versions`: | ||||
|     * `Klock`: `2.4.2` -> `2.4.3` | ||||
|     * `Ktor`: `1.6.3` -> `1.6.4` | ||||
|  | ||||
| ## 0.5.30 | ||||
|  | ||||
| * `Versions`: | ||||
|     * `Serialization`: `1.2.2` -> `1.3.0` | ||||
|  | ||||
| ## 0.5.29 | ||||
|  | ||||
| * `Versions`: | ||||
|     * `Exposed`: `0.34.2` -> `0.35.1` | ||||
|  | ||||
| ## 0.5.28 | ||||
|  | ||||
| * `Versions`: | ||||
|     * `Kotlin`: `1.5.30` -> `1.5.31` | ||||
|     * `Klock`: `2.4.1` -> `2.4.2` | ||||
|  | ||||
| ## 0.5.27 | ||||
|  | ||||
| * `Versions`: | ||||
|     * `Exposed`: `0.34.1` -> `0.34.2` | ||||
|  | ||||
| ## 0.5.26 | ||||
|  | ||||
| * `Repos`: | ||||
|     * `InMemory`: | ||||
|         * `MapCRUDRepo`s and `MapKeyValueRepo`s got `protected` methods and properties instead of private | ||||
|  | ||||
| ## 0.5.25 | ||||
|  | ||||
| * `Versions`: | ||||
|     * `UUID`: `0.3.0` -> `0.3.1` | ||||
| * `Common`: | ||||
|     * New property `MPPFile#withoutSlashAtTheEnd` | ||||
|     * Extension `clamp` has been deprecated | ||||
|     * New extension `Iterable#diff` | ||||
| * `Serialization`: | ||||
|     * New operators `TypedSerializer#plusAssign` and `TypedSerializer#minusAssign` | ||||
|  | ||||
| ## 0.5.24 | ||||
|  | ||||
| * `Versions`: | ||||
|     * `Coroutines`: `1.5.1` -> `1.5.2` | ||||
|     * `Klock`: `2.3.4` -> `2.4.1` | ||||
| * `Coroutines`: | ||||
|     * New function `CoroutineScope` with safely exceptions handler as second parameter | ||||
|  | ||||
| ## 0.5.23 | ||||
|  | ||||
| * `Versions`: | ||||
|     * `Exposed`: `0.33.1` -> `0.34.1` | ||||
| * `Common`: | ||||
|     * New extensions `Iterable#joinTo` and `Array#joinTo` | ||||
|  | ||||
| ## 0.5.22 | ||||
|  | ||||
| * `Versions` | ||||
|     * `Kotlin`: `1.5.21` -> `1.5.30` | ||||
|     * `Klock`: `2.3.2` -> `2.3.4` | ||||
|     * `AppCompat`: `1.3.0` -> `1.3.1` | ||||
|     * `Ktor`: `1.6.2` -> `1.6.3` | ||||
|  | ||||
| ## 0.5.21 | ||||
|  | ||||
| * `Versions` | ||||
|     * `Klock`: `2.3.1` -> `2.3.2` | ||||
| * `Serialization` | ||||
|     * `Typed Serializer`: | ||||
|         * `TypedSerializer` Descriptor serial name has been fixed | ||||
|  | ||||
| ## 0.5.20 | ||||
|  | ||||
| * `Repos`: | ||||
|     * `Common` | ||||
|         * `Android`: | ||||
|             * `*OrNull` analogs of `Cursor.get*(String)` extensions have been added | ||||
|             * Extensions `Cursor.getFloat` and `Cursor.getFloatOrNull` have been added | ||||
|  | ||||
| ## 0.5.19 | ||||
|  | ||||
| * `LanguageCode`: | ||||
|     * `IetfLanguageCode` became as sealed class | ||||
|     * `IetfLanguageCode` now override `toString` and returns its code | ||||
|  | ||||
| ## 0.5.18 | ||||
|  | ||||
| * `Versions` | ||||
|     * `Kotlin Exposed`: `0.32.1` -> `0.33.1` | ||||
| * `LanguageCode`: | ||||
|     * Module has been created | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,7 @@ kotlin { | ||||
|     sourceSets { | ||||
|         androidMain { | ||||
|             dependencies { | ||||
|                 api "androidx.appcompat:appcompat-resources:$appcompat_version" | ||||
|                 api libs.android.appCompat.resources | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -10,13 +10,13 @@ kotlin { | ||||
|     sourceSets { | ||||
|         commonMain { | ||||
|             dependencies { | ||||
|                 api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" | ||||
|                 api libs.kt.coroutines | ||||
|                 api project(":micro_utils.common") | ||||
|             } | ||||
|         } | ||||
|         androidMain { | ||||
|             dependencies { | ||||
|                 api "androidx.recyclerview:recyclerview:$androidx_recycler_version" | ||||
|                 api libs.android.recyclerView | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
							
								
								
									
										12
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								build.gradle
									
									
									
									
									
								
							| @@ -7,12 +7,12 @@ buildscript { | ||||
|     } | ||||
|  | ||||
|     dependencies { | ||||
|         classpath 'com.android.tools.build:gradle:4.1.3' | ||||
|         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" | ||||
|         classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" | ||||
|         classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version" | ||||
|         classpath "com.github.breadmoirai:github-release:$github_release_plugin_version" | ||||
|         classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" | ||||
|         classpath libs.buildscript.kt.gradle | ||||
|         classpath libs.buildscript.kt.serialization | ||||
|         classpath libs.buildscript.jb.dokka | ||||
|         classpath libs.buildscript.gh.release | ||||
|         classpath libs.buildscript.android.gradle | ||||
|         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"/> | ||||
| @@ -12,9 +12,7 @@ package dev.inmo.micro_utils.common | ||||
|     AnnotationTarget.PROPERTY_GETTER, | ||||
|     AnnotationTarget.PROPERTY_SETTER, | ||||
|     AnnotationTarget.FUNCTION, | ||||
|     AnnotationTarget.TYPE, | ||||
|     AnnotationTarget.TYPEALIAS, | ||||
|     AnnotationTarget.TYPE_PARAMETER | ||||
|     AnnotationTarget.TYPEALIAS | ||||
| ) | ||||
| annotation class PreviewFeature(val message: String = "It is possible, that behaviour of this thing will be changed or removed in future releases") | ||||
|  | ||||
| @@ -30,8 +28,6 @@ annotation class PreviewFeature(val message: String = "It is possible, that beha | ||||
|     AnnotationTarget.PROPERTY_GETTER, | ||||
|     AnnotationTarget.PROPERTY_SETTER, | ||||
|     AnnotationTarget.FUNCTION, | ||||
|     AnnotationTarget.TYPE, | ||||
|     AnnotationTarget.TYPEALIAS, | ||||
|     AnnotationTarget.TYPE_PARAMETER | ||||
|     AnnotationTarget.TYPEALIAS | ||||
| ) | ||||
| annotation class Warning(val message: String) | ||||
|   | ||||
| @@ -1,10 +0,0 @@ | ||||
| package dev.inmo.micro_utils.common | ||||
|  | ||||
| @Suppress("NOTHING_TO_INLINE") | ||||
| inline fun <T : Comparable<T>> T.clamp(min: T, max: T): T { | ||||
|     return when { | ||||
|         this < min -> min | ||||
|         this > max -> max | ||||
|         else -> this | ||||
|     } | ||||
| } | ||||
| @@ -27,8 +27,8 @@ data class Diff<T> internal constructor( | ||||
|  | ||||
| private inline fun <T> performChanges( | ||||
|     potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>, | ||||
|     additionalsInOld: MutableList<T>, | ||||
|     additionalsInNew: MutableList<T>, | ||||
|     additionsInOld: MutableList<T>, | ||||
|     additionsInNew: MutableList<T>, | ||||
|     changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>, | ||||
|     removedList: MutableList<IndexedValue<T>>, | ||||
|     addedList: MutableList<IndexedValue<T>>, | ||||
| @@ -52,20 +52,20 @@ private inline fun <T> performChanges( | ||||
|                     newPotentials.first().second ?.let { addedList.add(it) } | ||||
|                     newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) -> | ||||
|                         addedList.add(newOne!!) | ||||
|                         oldOne ?.let { additionalsInOld.add(oldOne.value) } | ||||
|                         oldOne ?.let { additionsInOld.add(oldOne.value) } | ||||
|                     } | ||||
|                     if (newPotentials.size > 1) { | ||||
|                         newPotentials.last().first ?.value ?.let { additionalsInOld.add(it) } | ||||
|                         newPotentials.last().first ?.value ?.let { additionsInOld.add(it) } | ||||
|                     } | ||||
|                 } | ||||
|                 newOneEqualToOldObject -> { | ||||
|                     newPotentials.first().first ?.let { removedList.add(it) } | ||||
|                     newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) -> | ||||
|                         removedList.add(oldOne!!) | ||||
|                         newOne ?.let { additionalsInNew.add(newOne.value) } | ||||
|                         newOne ?.let { additionsInNew.add(newOne.value) } | ||||
|                     } | ||||
|                     if (newPotentials.size > 1) { | ||||
|                         newPotentials.last().second ?.value ?.let { additionalsInNew.add(it) } | ||||
|                         newPotentials.last().second ?.value ?.let { additionsInNew.add(it) } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| @@ -139,6 +139,10 @@ fun <T> Iterable<T>.calculateDiff( | ||||
|  | ||||
|     return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList()) | ||||
| } | ||||
| inline fun <T> Iterable<T>.diff( | ||||
|     other: Iterable<T>, | ||||
|     strictComparison: Boolean = false | ||||
| ): Diff<T> = calculateDiff(other, strictComparison) | ||||
|  | ||||
| inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new) | ||||
| inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true) | ||||
| @@ -149,3 +153,22 @@ inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDif | ||||
| inline fun <T> Iterable<T>.calculateStrictDiff( | ||||
|     other: Iterable<T> | ||||
| ) = 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) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,168 @@ | ||||
| package dev.inmo.micro_utils.common | ||||
|  | ||||
| import kotlinx.serialization.* | ||||
| import kotlinx.serialization.builtins.serializer | ||||
| import kotlinx.serialization.descriptors.* | ||||
| import kotlinx.serialization.encoding.* | ||||
|  | ||||
| /** | ||||
|  * Realization of this interface will contains at least one not null - [optionalT1] or [optionalT2] | ||||
|  * | ||||
|  * @see EitherFirst | ||||
|  * @see EitherSecond | ||||
|  * @see Either.Companion.first | ||||
|  * @see Either.Companion.second | ||||
|  * @see Either.onFirst | ||||
|  * @see Either.onSecond | ||||
|  * @see Either.mapOnFirst | ||||
|  * @see Either.mapOnSecond | ||||
|  */ | ||||
| @Serializable(EitherSerializer::class) | ||||
| sealed interface Either<T1, T2> { | ||||
|     val optionalT1: Optional<T1> | ||||
|     val optionalT2: Optional<T2> | ||||
|     @Deprecated("Use optionalT1 instead", ReplaceWith("optionalT1")) | ||||
|     val t1: T1? | ||||
|         get() = optionalT1.dataOrNull() | ||||
|     @Deprecated("Use optionalT2 instead", ReplaceWith("optionalT2")) | ||||
|     val t2: T2? | ||||
|         get() = optionalT2.dataOrNull() | ||||
|  | ||||
|     companion object { | ||||
|         fun <T1, T2> serializer( | ||||
|             t1Serializer: KSerializer<T1>, | ||||
|             t2Serializer: KSerializer<T2>, | ||||
|         ): KSerializer<Either<T1, T2>> = EitherSerializer(t1Serializer, t2Serializer) | ||||
|     } | ||||
| } | ||||
|  | ||||
| class EitherSerializer<T1, T2>( | ||||
|     t1Serializer: KSerializer<T1>, | ||||
|     t2Serializer: KSerializer<T2>, | ||||
| ) : KSerializer<Either<T1, T2>> { | ||||
|     @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) | ||||
|     override val descriptor: SerialDescriptor = buildSerialDescriptor( | ||||
|         "TypedSerializer", | ||||
|         SerialKind.CONTEXTUAL | ||||
|     ) { | ||||
|         element("type", String.serializer().descriptor) | ||||
|         element("value", ContextualSerializer(Either::class).descriptor) | ||||
|     } | ||||
|     private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer) | ||||
|     private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer) | ||||
|  | ||||
|     @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) | ||||
|     override fun deserialize(decoder: Decoder): Either<T1, T2> { | ||||
|         return decoder.decodeStructure(descriptor) { | ||||
|             var type: String? = null | ||||
|             lateinit var result: Either<T1, T2> | ||||
|             while (true) { | ||||
|                 when (val index = decodeElementIndex(descriptor)) { | ||||
|                     0 -> type = decodeStringElement(descriptor, 0) | ||||
|                     1 -> { | ||||
|                         result = when (type) { | ||||
|                             "t1" -> decodeSerializableElement( | ||||
|                                 descriptor, | ||||
|                                 1, | ||||
|                                 t1EitherSerializer | ||||
|                             ) | ||||
|                             "t2" -> decodeSerializableElement( | ||||
|                                 descriptor, | ||||
|                                 1, | ||||
|                                 t2EitherSerializer | ||||
|                             ) | ||||
|                             else -> error("Unknown type of either: $type") | ||||
|                         } | ||||
|                     } | ||||
|                     CompositeDecoder.DECODE_DONE -> break | ||||
|                     else -> error("Unexpected index: $index") | ||||
|                 } | ||||
|             } | ||||
|             result | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) | ||||
|     override fun serialize(encoder: Encoder, value: Either<T1, T2>) { | ||||
|         encoder.encodeStructure(descriptor) { | ||||
|             when (value) { | ||||
|                 is EitherFirst -> { | ||||
|                     encodeStringElement(descriptor, 0, "t1") | ||||
|                     encodeSerializableElement(descriptor, 1, t1EitherSerializer, value) | ||||
|                 } | ||||
|                 is EitherSecond -> { | ||||
|                     encodeStringElement(descriptor, 0, "t2") | ||||
|                     encodeSerializableElement(descriptor, 1, t2EitherSerializer, value) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * This type [Either] will always have not nullable [optionalT1] | ||||
|  */ | ||||
| @Serializable | ||||
| data class EitherFirst<T1, T2>( | ||||
|     override val t1: T1 | ||||
| ) : Either<T1, T2> { | ||||
|     override val optionalT1: Optional<T1> = t1.optional | ||||
|     override val optionalT2: Optional<T2> = Optional.absent() | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * This type [Either] will always have not nullable [optionalT2] | ||||
|  */ | ||||
| @Serializable | ||||
| data class EitherSecond<T1, T2>( | ||||
|     override val t2: T2 | ||||
| ) : Either<T1, T2> { | ||||
|     override val optionalT1: Optional<T1> = Optional.absent() | ||||
|     override val optionalT2: Optional<T2> = t2.optional | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @return New instance of [EitherFirst] | ||||
|  */ | ||||
| inline fun <T1, T2> Either.Companion.first(t1: T1): Either<T1, T2> = EitherFirst(t1) | ||||
| /** | ||||
|  * @return New instance of [EitherSecond] | ||||
|  */ | ||||
| inline fun <T1, T2> Either.Companion.second(t2: T2): Either<T1, T2> = EitherSecond(t2) | ||||
|  | ||||
| /** | ||||
|  * Will call [block] in case when [this] is [EitherFirst] | ||||
|  */ | ||||
| inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(block: (T1) -> Unit): E { | ||||
|     optionalT1.onPresented(block) | ||||
|     return this | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Will call [block] in case when [this] is [EitherSecond] | ||||
|  */ | ||||
| inline fun <T1, T2, E : Either<T1, T2>> E.onSecond(block: (T2) -> Unit): E { | ||||
|     optionalT2.onPresented(block) | ||||
|     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) { | ||||
|     is T1 -> Either.first<T1, T2>(this) | ||||
|     is T2 -> Either.second<T1, T2>(this) | ||||
|     else -> error("Incorrect type of either argument $this") | ||||
| } | ||||
| @@ -0,0 +1,59 @@ | ||||
| package dev.inmo.micro_utils.common | ||||
|  | ||||
| inline fun <I, R> Iterable<I>.joinTo( | ||||
|     separatorFun: (I) -> R?, | ||||
|     prefix: R? = null, | ||||
|     postfix: R? = null, | ||||
|     transform: (I) -> R? | ||||
| ): List<R> { | ||||
|     val result = mutableListOf<R>() | ||||
|     val iterator = iterator() | ||||
|  | ||||
|     prefix ?.let(result::add) | ||||
|  | ||||
|     while (iterator.hasNext()) { | ||||
|         val element = iterator.next() | ||||
|         result.add(transform(element) ?: continue) | ||||
|  | ||||
|         if (iterator.hasNext()) { | ||||
|             result.add(separatorFun(element) ?: continue) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     postfix ?.let(result::add) | ||||
|  | ||||
|     return result | ||||
| } | ||||
|  | ||||
| inline fun <I, R> Iterable<I>.joinTo( | ||||
|     separator: R? = null, | ||||
|     prefix: R? = null, | ||||
|     postfix: R? = null, | ||||
|     transform: (I) -> R? | ||||
| ): List<R> = joinTo({ separator }, prefix, postfix, transform) | ||||
|  | ||||
| inline fun <I> Iterable<I>.joinTo( | ||||
|     separatorFun: (I) -> I?, | ||||
|     prefix: I? = null, | ||||
|     postfix: I? = null | ||||
| ): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it } | ||||
|  | ||||
| inline fun <I> Iterable<I>.joinTo( | ||||
|     separator: I? = null, | ||||
|     prefix: I? = null, | ||||
|     postfix: I? = null | ||||
| ): List<I> = joinTo<I>({ separator }, prefix, postfix) | ||||
|  | ||||
| inline fun <I, reified R> Array<I>.joinTo( | ||||
|     separatorFun: (I) -> R?, | ||||
|     prefix: R? = null, | ||||
|     postfix: R? = null, | ||||
|     transform: (I) -> R? | ||||
| ): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray() | ||||
|  | ||||
| inline fun <I, reified R> Array<I>.joinTo( | ||||
|     separator: R? = null, | ||||
|     prefix: R? = null, | ||||
|     postfix: R? = null, | ||||
|     transform: (I) -> R? | ||||
| ): Array<R> = asIterable().joinTo(separator, prefix, postfix, transform).toTypedArray() | ||||
| @@ -7,7 +7,7 @@ import kotlin.jvm.JvmInline | ||||
| @JvmInline | ||||
| value class FileName(val string: String) { | ||||
|     val name: String | ||||
|         get() = string.takeLastWhile { it != '/' } | ||||
|         get() = withoutSlashAtTheEnd.takeLastWhile { it != '/' } | ||||
|     val extension: String | ||||
|         get() = name.takeLastWhile { it != '.' } | ||||
|     val nameWithoutExtension: String | ||||
| @@ -17,15 +17,18 @@ value class FileName(val string: String) { | ||||
|                 filename.substring(0, it) | ||||
|             } ?: filename | ||||
|         } | ||||
|     val withoutSlashAtTheEnd: String | ||||
|         get() = string.dropLastWhile { it == '/' } | ||||
|     override fun toString(): String = string | ||||
| } | ||||
|  | ||||
|  | ||||
| @PreviewFeature | ||||
| expect class MPPFile | ||||
|  | ||||
| expect val MPPFile.filename: FileName | ||||
| expect val MPPFile.filesize: Long | ||||
| expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator | ||||
| expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator | ||||
| fun MPPFile.bytesSync() = bytesAllocatorSync() | ||||
| suspend fun MPPFile.bytes() = bytesAllocator() | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,92 @@ | ||||
| @file:Suppress("unused") | ||||
|  | ||||
| package dev.inmo.micro_utils.common | ||||
|  | ||||
| import kotlinx.serialization.Serializable | ||||
|  | ||||
| /** | ||||
|  * This type represents [T] as not only potentially nullable data, but also as a data which can not be presented. This | ||||
|  * type will be useful in cases when [T] is nullable and null as valuable data too in time of data absence should be | ||||
|  * presented by some third type. | ||||
|  * | ||||
|  * Let's imagine, you have nullable name in some database. In case when name is not nullable everything is clear - null | ||||
|  * will represent absence of row in the database. In case when name is nullable null will be a little bit dual-meaning, | ||||
|  * cause this null will say nothing about availability of the row (of course, it is exaggerated example) | ||||
|  * | ||||
|  * @see Optional.presented | ||||
|  * @see Optional.absent | ||||
|  * @see Optional.optional | ||||
|  * @see Optional.onPresented | ||||
|  * @see Optional.onAbsent | ||||
|  */ | ||||
| @Serializable | ||||
| data class Optional<T> internal constructor( | ||||
|     @Warning("It is unsafe to use this data directly") | ||||
|     val data: T?, | ||||
|     @Warning("It is unsafe to use this data directly") | ||||
|     val dataPresented: Boolean | ||||
| ) { | ||||
|     companion object { | ||||
|         /** | ||||
|          * Will create [Optional] with presented data | ||||
|          */ | ||||
|         fun <T> presented(data: T) = Optional(data, true) | ||||
|         /** | ||||
|          * Will create [Optional] without data | ||||
|          */ | ||||
|         fun <T> absent() = Optional<T>(null, false) | ||||
|     } | ||||
| } | ||||
|  | ||||
| inline val <T> T.optional | ||||
|     get() = Optional.presented(this) | ||||
|  | ||||
| /** | ||||
|  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||
|  */ | ||||
| inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply { | ||||
|     if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||
|  */ | ||||
| inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run { | ||||
|     if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Will call [block] when data absent ([Optional.dataPresented] == false) | ||||
|  */ | ||||
| inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply { | ||||
|     if (!dataPresented) { block() } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||
|  */ | ||||
| inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run { | ||||
|     if (!dataPresented) { @Suppress("UNCHECKED_CAST") block() } else null | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise | ||||
|  */ | ||||
| fun <T> Optional<T>.dataOrNull() = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null | ||||
|  | ||||
| /** | ||||
|  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or throw [throwable] otherwise | ||||
|  */ | ||||
| fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it | ||||
|  */ | ||||
| inline fun <T> Optional<T>.dataOrElse(block: () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block() | ||||
|  | ||||
| /** | ||||
|  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it | ||||
|  */ | ||||
| @Deprecated("dataOrElse now is inline", ReplaceWith("dataOrElse", "dev.inmo.micro_utils.common.dataOrElse")) | ||||
| suspend fun <T> Optional<T>.dataOrElseSuspendable(block: suspend () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block() | ||||
| @@ -0,0 +1,19 @@ | ||||
| package dev.inmo.micro_utils.common | ||||
|  | ||||
| fun <T : Comparable<T>> ClosedRange<T>.intersect(other: ClosedRange<T>): Pair<T, T>? = when { | ||||
|     start == other.start && endInclusive == other.endInclusive -> start to endInclusive | ||||
|     start > other.endInclusive || other.start > endInclusive -> null | ||||
|     else -> maxOf(start, other.start) to minOf(endInclusive, other.endInclusive) | ||||
| } | ||||
|  | ||||
| fun IntRange.intersect( | ||||
|     other: IntRange | ||||
| ): IntRange? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let { | ||||
|     it.first .. it.second | ||||
| } | ||||
|  | ||||
| fun LongRange.intersect( | ||||
|     other: LongRange | ||||
| ): LongRange? = (this as ClosedRange<Long>).intersect(other as ClosedRange<Long>) ?.let { | ||||
|     it.first .. it.second | ||||
| } | ||||
| @@ -0,0 +1,21 @@ | ||||
| package dev.inmo.micro_utils.common | ||||
|  | ||||
| /** | ||||
|  * Executes the given [action] until getting of successful result specified number of [times]. | ||||
|  * | ||||
|  * A zero-based index of current iteration is passed as a parameter to [action]. | ||||
|  */ | ||||
| inline fun <R> repeatOnFailure( | ||||
|     times: Int, | ||||
|     onEachFailure: (Throwable) -> Unit = {}, | ||||
|     action: (Int) -> R | ||||
| ): Optional<R> { | ||||
|     repeat(times) { | ||||
|         runCatching { | ||||
|             action(it) | ||||
|         }.onFailure(onEachFailure).onSuccess { | ||||
|             return Optional.presented(it) | ||||
|         } | ||||
|     } | ||||
|     return Optional.absent() | ||||
| } | ||||
| @@ -11,7 +11,7 @@ class DiffUtilsTests { | ||||
|         val withIndex = oldList.withIndex() | ||||
|  | ||||
|         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||
|             for ((i, v) in withIndex) { | ||||
|             for ((i, _) in withIndex) { | ||||
|                 if (i + count > oldList.lastIndex) { | ||||
|                     continue | ||||
|                 } | ||||
| @@ -54,7 +54,7 @@ class DiffUtilsTests { | ||||
|         val oldList = (0 until 10).map { it.toString() } | ||||
|         val withIndex = oldList.withIndex() | ||||
|  | ||||
|         for (step in 0 until oldList.size) { | ||||
|         for (step in oldList.indices) { | ||||
|             for ((i, v) in withIndex) { | ||||
|                 val mutable = oldList.toMutableList() | ||||
|                 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,10 +2,12 @@ package dev.inmo.micro_utils.common | ||||
|  | ||||
| import org.khronos.webgl.ArrayBuffer | ||||
| import org.w3c.dom.ErrorEvent | ||||
| import org.w3c.files.File | ||||
| import org.w3c.files.FileReader | ||||
| import org.w3c.files.* | ||||
| import kotlin.js.Promise | ||||
|  | ||||
| /** | ||||
|  * @suppress | ||||
|  */ | ||||
| actual typealias MPPFile = File | ||||
|  | ||||
| fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure -> | ||||
| @@ -21,12 +23,32 @@ fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure -> | ||||
|     reader.readAsArrayBuffer(this) | ||||
| } | ||||
|  | ||||
| fun MPPFile.readBytes(): ByteArray { | ||||
|     val reader = FileReaderSync() | ||||
|     return reader.readAsArrayBuffer(this).toByteArray() | ||||
| } | ||||
|  | ||||
| private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await() | ||||
|  | ||||
| /** | ||||
|  * @suppress | ||||
|  */ | ||||
| actual val MPPFile.filename: FileName | ||||
|     get() = FileName(name) | ||||
| /** | ||||
|  * @suppress | ||||
|  */ | ||||
| actual val MPPFile.filesize: Long | ||||
|     get() = size.toLong() | ||||
| /** | ||||
|  * @suppress | ||||
|  */ | ||||
| @Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can") | ||||
| actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator | ||||
|     get() = ::readBytes | ||||
| /** | ||||
|  * @suppress | ||||
|  */ | ||||
| @Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can") | ||||
| actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator | ||||
|     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() | ||||
| } | ||||
|  | ||||
| @@ -4,12 +4,29 @@ import dev.inmo.micro_utils.coroutines.doInIO | ||||
| import dev.inmo.micro_utils.coroutines.doOutsideOfCoroutine | ||||
| import java.io.File | ||||
|  | ||||
| /** | ||||
|  * @suppress | ||||
|  */ | ||||
| actual typealias MPPFile = File | ||||
|  | ||||
| /** | ||||
|  * @suppress | ||||
|  */ | ||||
| actual val MPPFile.filename: FileName | ||||
|     get() = FileName(name) | ||||
| /** | ||||
|  * @suppress | ||||
|  */ | ||||
| actual val MPPFile.filesize: Long | ||||
|     get() = length() | ||||
| /** | ||||
|  * @suppress | ||||
|  */ | ||||
| actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator | ||||
|     get() = ::readBytes | ||||
| /** | ||||
|  * @suppress | ||||
|  */ | ||||
| actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator | ||||
|     get() = { | ||||
|         doInIO { | ||||
|   | ||||
| @@ -10,12 +10,17 @@ kotlin { | ||||
|     sourceSets { | ||||
|         commonMain { | ||||
|             dependencies { | ||||
|                 api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" | ||||
|                 api libs.kt.coroutines | ||||
|             } | ||||
|         } | ||||
|         jsMain { | ||||
|             dependencies { | ||||
|                 api project(":micro_utils.common") | ||||
|             } | ||||
|         } | ||||
|         androidMain { | ||||
|             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,94 @@ | ||||
| package dev.inmo.micro_utils.coroutines | ||||
|  | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.channels.BufferOverflow | ||||
| import kotlinx.coroutines.channels.Channel | ||||
| import kotlinx.coroutines.flow.* | ||||
| import kotlinx.coroutines.sync.Mutex | ||||
| import kotlinx.coroutines.sync.withLock | ||||
|  | ||||
| private sealed interface AccumulatorFlowStep | ||||
| private data class DataRetrievedAccumulatorFlowStep(val data: Any) : AccumulatorFlowStep | ||||
| private data class SubscribeAccumulatorFlowStep(val channel: Channel<Any>) : AccumulatorFlowStep | ||||
| private data class UnsubscribeAccumulatorFlowStep(val channel: Channel<Any>) : AccumulatorFlowStep | ||||
|  | ||||
| /** | ||||
|  * This [Flow] will have behaviour very similar to [SharedFlow], but there are several differences: | ||||
|  * | ||||
|  * * All unhandled by [FlowCollector] data will not be removed from [AccumulatorFlow] and will be sent to new | ||||
|  * [FlowCollector]s until anybody will handle it | ||||
|  * * Here there are an [activeData] where data [T] will be stored until somebody will handle it | ||||
|  */ | ||||
| class AccumulatorFlow<T>( | ||||
|     sourceDataFlow: Flow<T>, | ||||
|     scope: CoroutineScope | ||||
| ) : AbstractFlow<T>() { | ||||
|     private val subscope = scope.LinkedSupervisorScope() | ||||
|     private val activeData = ArrayDeque<T>() | ||||
|     private val dataMutex = Mutex() | ||||
|     private val channelsForBroadcast = mutableListOf<Channel<Any>>() | ||||
|     private val channelsMutex = Mutex() | ||||
|     private val steps = subscope.actor<AccumulatorFlowStep> { step -> | ||||
|         when (step) { | ||||
|             is DataRetrievedAccumulatorFlowStep -> { | ||||
|                 if (activeData.first() === step.data) { | ||||
|                     dataMutex.withLock { | ||||
|                         activeData.removeFirst() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             is SubscribeAccumulatorFlowStep -> channelsMutex.withLock { | ||||
|                 channelsForBroadcast.add(step.channel) | ||||
|                 dataMutex.withLock { | ||||
|                     val dataToSend = activeData.toList() | ||||
|                     safelyWithoutExceptions { | ||||
|                         dataToSend.forEach { step.channel.send(it as Any) } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             is UnsubscribeAccumulatorFlowStep -> channelsMutex.withLock { | ||||
|                 channelsForBroadcast.remove(step.channel) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     private val subscriptionJob = sourceDataFlow.subscribeSafelyWithoutExceptions(subscope) { | ||||
|         dataMutex.withLock { | ||||
|             activeData.addLast(it) | ||||
|         } | ||||
|         channelsMutex.withLock { | ||||
|             channelsForBroadcast.forEach { channel -> | ||||
|                 safelyWithResult { | ||||
|                     channel.send(it as Any) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override suspend fun collectSafely(collector: FlowCollector<T>) { | ||||
|         val channel = Channel<Any>(Channel.UNLIMITED, BufferOverflow.SUSPEND) | ||||
|         steps.send(SubscribeAccumulatorFlowStep(channel)) | ||||
|         for (data in channel) { | ||||
|             try { | ||||
|                 collector.emit(data as T) | ||||
|                 steps.send(DataRetrievedAccumulatorFlowStep(data)) | ||||
|             } finally { | ||||
|                 channel.cancel() | ||||
|                 steps.send(UnsubscribeAccumulatorFlowStep(channel)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Creates [AccumulatorFlow] using [this] as base [Flow] | ||||
|  */ | ||||
| fun <T> Flow<T>.accumulatorFlow(scope: CoroutineScope): Flow<T> { | ||||
|     return AccumulatorFlow(this, scope) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Creates [AccumulatorFlow] using [this] with [receiveAsFlow] to get | ||||
|  */ | ||||
| fun <T> Channel<T>.accumulatorFlow(scope: CoroutineScope): Flow<T> { | ||||
|     return receiveAsFlow().accumulatorFlow(scope) | ||||
| } | ||||
| @@ -147,3 +147,10 @@ suspend inline fun <T> runCatchingSafelyWithoutExceptions( | ||||
| ): Result<T?> = runCatching { | ||||
|     safelyWithoutExceptions(onException, block) | ||||
| } | ||||
|  | ||||
| inline fun CoroutineScope( | ||||
|     context: CoroutineContext, | ||||
|     noinline defaultExceptionsHandler: ExceptionHandler<Unit> | ||||
| ) = CoroutineScope( | ||||
|     context + ContextSafelyExceptionHandler(defaultExceptionsHandler) | ||||
| ) | ||||
|   | ||||
| @@ -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() | ||||
| } | ||||
| @@ -1,3 +1,6 @@ | ||||
| package dev.inmo.micro_utils.crypto | ||||
|  | ||||
| /** | ||||
|  * @suppress | ||||
|  */ | ||||
| actual fun SourceBytes.md5(): MD5 = CryptoJS.MD5(decodeToString()) | ||||
|   | ||||
| @@ -3,6 +3,9 @@ package dev.inmo.micro_utils.crypto | ||||
| import java.math.BigInteger | ||||
| import java.security.MessageDigest | ||||
|  | ||||
| /** | ||||
|  * @suppress | ||||
|  */ | ||||
| actual fun SourceBytes.md5(): MD5 = BigInteger( | ||||
|     1, | ||||
|     MessageDigest.getInstance("MD5").digest(this) | ||||
|   | ||||
| @@ -26,12 +26,12 @@ ext { | ||||
| } | ||||
|  | ||||
| android { | ||||
|     compileSdkVersion "$android_compileSdkVersion".toInteger() | ||||
|     buildToolsVersion "$android_buildToolsVersion" | ||||
|     compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger() | ||||
|     buildToolsVersion libs.versions.android.props.buildTools.get() | ||||
|  | ||||
|     defaultConfig { | ||||
|         minSdkVersion "$android_minSdkVersion".toInteger() | ||||
|         targetSdkVersion "$android_compileSdkVersion".toInteger() | ||||
|         minSdkVersion libs.versions.android.props.minSdk.get().toInteger() | ||||
|         targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger() | ||||
|         versionCode "${android_code_version}".toInteger() | ||||
|         versionName "$version" | ||||
|     } | ||||
|   | ||||
| @@ -13,10 +13,10 @@ repositories { | ||||
|  | ||||
| kotlin { | ||||
|     jvm() | ||||
|     js(IR) { | ||||
|         browser() | ||||
|         nodejs() | ||||
|     } | ||||
| //    js(IR) { | ||||
| //        browser() | ||||
| //        nodejs() | ||||
| //    } | ||||
|     android {} | ||||
|  | ||||
|     sourceSets { | ||||
| @@ -29,7 +29,7 @@ kotlin { | ||||
|                         it != project | ||||
|                         && it.hasProperty("kotlin") | ||||
|                         && it.kotlin.sourceSets.any { it.name.contains("commonMain") } | ||||
|                         && it.kotlin.sourceSets.any { it.name.contains("jsMain") } | ||||
| //                        && it.kotlin.sourceSets.any { it.name.contains("jsMain") } | ||||
|                         && it.kotlin.sourceSets.any { it.name.contains("jvmMain") } | ||||
|                         && it.kotlin.sourceSets.any { it.name.contains("androidMain") } | ||||
|                     ) { | ||||
| @@ -38,22 +38,22 @@ kotlin { | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         jsMain { | ||||
|             dependencies { | ||||
|                 implementation kotlin('stdlib') | ||||
| //        jsMain { | ||||
| //            dependencies { | ||||
| //                implementation kotlin('stdlib') | ||||
|  | ||||
|                 project.parent.subprojects.forEach { | ||||
|                     if ( | ||||
|                         it != project | ||||
|                         && it.hasProperty("kotlin") | ||||
|                         && it.kotlin.sourceSets.any { it.name.contains("commonMain") } | ||||
|                         && it.kotlin.sourceSets.any { it.name.contains("jsMain") } | ||||
|                     ) { | ||||
|                         api it | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| //                project.parent.subprojects.forEach { | ||||
| //                    if ( | ||||
| //                        it != project | ||||
| //                        && it.hasProperty("kotlin") | ||||
| //                        && it.kotlin.sourceSets.any { it.name.contains("commonMain") } | ||||
| //                        && it.kotlin.sourceSets.any { it.name.contains("jsMain") } | ||||
| //                    ) { | ||||
| //                        api it | ||||
| //                    } | ||||
| //                } | ||||
| //            } | ||||
| //        } | ||||
|         jvmMain { | ||||
|             dependencies { | ||||
|                 implementation kotlin('stdlib') | ||||
| @@ -116,9 +116,9 @@ tasks.dokkaHtml { | ||||
|             sourceRoots.setFrom(findSourcesWithName("commonMain")) | ||||
|         } | ||||
|  | ||||
|         named("jsMain") { | ||||
|             sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain")) | ||||
|         } | ||||
| //        named("jsMain") { | ||||
| //            sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain")) | ||||
| //        } | ||||
|  | ||||
|         named("jvmMain") { | ||||
|             sourceRoots.setFrom(findSourcesWithName("jvmMain", "commonMain")) | ||||
|   | ||||
| @@ -21,6 +21,7 @@ allprojects { | ||||
|         releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true" | ||||
|  | ||||
|         mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle" | ||||
|         mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle" | ||||
|         mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle" | ||||
|         mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle" | ||||
|  | ||||
|   | ||||
| @@ -10,6 +10,7 @@ kotlin { | ||||
|     sourceSets { | ||||
|         commonMain { | ||||
|             dependencies { | ||||
|                 api project(":micro_utils.common") | ||||
|                 api project(":micro_utils.coroutines") | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -0,0 +1,81 @@ | ||||
| package dev.inmo.micro_utils.fsm.common | ||||
|  | ||||
| import kotlin.reflect.KClass | ||||
|  | ||||
| /** | ||||
|  * Define checkable holder which can be used to precheck that this handler may handle incoming [State] | ||||
|  */ | ||||
| interface CheckableHandlerHolder<I : State, O : State> : StatesHandler<I, O> { | ||||
|     suspend fun checkHandleable(state: O): Boolean | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Default realization of [StatesHandler]. It will incapsulate checking of [State] type in [checkHandleable] and class | ||||
|  * casting in [handleState] | ||||
|  */ | ||||
| class CustomizableHandlerHolder<I : O, O : State>( | ||||
|     private val delegateTo: StatesHandler<I, O>, | ||||
|     private val filter: suspend (state: O) -> Boolean | ||||
| ) : CheckableHandlerHolder<I, O> { | ||||
|     /** | ||||
|      * Checks that [state] can be handled by [delegateTo]. Under the hood it will check exact equality of [state] | ||||
|      * [KClass] and use [KClass.isInstance] of [inputKlass] if [strict] == false | ||||
|      */ | ||||
|     override suspend fun checkHandleable(state: O) = filter(state) | ||||
|  | ||||
|     /** | ||||
|      * Calls [delegateTo] method [StatesHandler.handleState] with [state] casted to [I]. Use [checkHandleable] | ||||
|      * to be sure that this [StatesHandlerHolder] will be able to handle [state] | ||||
|      */ | ||||
|     override suspend fun StatesMachine<in O>.handleState(state: I): O? { | ||||
|         return delegateTo.run { handleState(state) } | ||||
|     } | ||||
| } | ||||
|  | ||||
| fun <I : O, O : State> CheckableHandlerHolder( | ||||
|     inputKlass: KClass<I>, | ||||
|     strict: Boolean = false, | ||||
|     delegateTo: StatesHandler<I, O> | ||||
| ) = CustomizableHandlerHolder( | ||||
|     StatesHandler<O, O> { | ||||
|         delegateTo.run { handleState(it as I) } | ||||
|     }, | ||||
|     if (strict) { | ||||
|         { it::class == inputKlass } | ||||
|     } else { | ||||
|         { inputKlass.isInstance(it) } | ||||
|     } | ||||
| ) | ||||
|  | ||||
| @Deprecated("Renamed", ReplaceWith("CheckableHandlerHolder")) | ||||
| fun <I : O, O : State> StateHandlerHolder( | ||||
|     inputKlass: KClass<I>, | ||||
|     strict: Boolean = false, | ||||
|     delegateTo: StatesHandler<I, O> | ||||
| ) = CheckableHandlerHolder(inputKlass, strict, delegateTo) | ||||
|  | ||||
| inline fun <reified I : O, O : State> CheckableHandlerHolder( | ||||
|     strict: Boolean = false, | ||||
|     delegateTo: StatesHandler<I, O> | ||||
| ) = CheckableHandlerHolder(I::class, strict, delegateTo) | ||||
|  | ||||
| @Deprecated("Renamed", ReplaceWith("CheckableHandlerHolder")) | ||||
| inline fun <reified I : O, O : State> StateHandlerHolder( | ||||
|     strict: Boolean = false, | ||||
|     delegateTo: StatesHandler<I, O> | ||||
| ) = CheckableHandlerHolder(strict, delegateTo) | ||||
|  | ||||
| inline fun <reified I : O, O: State> StatesHandler<I, O>.holder( | ||||
|     strict: Boolean = true | ||||
| ) = CheckableHandlerHolder<I, O>( | ||||
|     I::class, | ||||
|     strict, | ||||
|     this | ||||
| ) | ||||
|  | ||||
| inline fun <I : O, O: State> StatesHandler<I, O>.holder( | ||||
|     noinline filter: suspend (state: State) -> Boolean | ||||
| ) = CustomizableHandlerHolder<O, O>( | ||||
|     { this@holder.run { handleState(it as I) } }, | ||||
|     filter | ||||
| ) | ||||
| @@ -1,15 +0,0 @@ | ||||
| package dev.inmo.micro_utils.fsm.common | ||||
|  | ||||
| import kotlin.reflect.KClass | ||||
|  | ||||
| class StateHandlerHolder<I : State>( | ||||
|     private val inputKlass: KClass<I>, | ||||
|     private val strict: Boolean = false, | ||||
|     private val delegateTo: StatesHandler<I> | ||||
| ) : StatesHandler<State> { | ||||
|     fun checkHandleable(state: State) = state::class == inputKlass || (!strict && inputKlass.isInstance(state)) | ||||
|  | ||||
|     override suspend fun StatesMachine.handleState(state: State): State? { | ||||
|         return delegateTo.run { handleState(state as I) } | ||||
|     } | ||||
| } | ||||
| @@ -1,5 +1,12 @@ | ||||
| package dev.inmo.micro_utils.fsm.common | ||||
|  | ||||
| fun interface StatesHandler<I : State> { | ||||
|     suspend fun StatesMachine.handleState(state: I): State? | ||||
| /** | ||||
|  * Default realization of states handler | ||||
|  */ | ||||
| fun interface StatesHandler<I : State, O: State> { | ||||
|     /** | ||||
|      * Main handling of [state]. In case when this [state] leads to another [State] and [handleState] returns not null | ||||
|      * [State] it is assumed that chain is not completed. | ||||
|      */ | ||||
|     suspend fun StatesMachine<in O>.handleState(state: I): O? | ||||
| } | ||||
|   | ||||
| @@ -1,26 +1,70 @@ | ||||
| package dev.inmo.micro_utils.fsm.common | ||||
|  | ||||
| import dev.inmo.micro_utils.common.Optional | ||||
| import dev.inmo.micro_utils.common.onPresented | ||||
| import dev.inmo.micro_utils.coroutines.* | ||||
| import kotlinx.coroutines.* | ||||
| import kotlinx.coroutines.flow.asFlow | ||||
| import kotlinx.coroutines.sync.Mutex | ||||
| import kotlinx.coroutines.sync.withLock | ||||
|  | ||||
| private suspend fun <I : State> StatesMachine.launchStateHandling( | ||||
|     state: State, | ||||
|     handlers: List<StateHandlerHolder<out I>> | ||||
| ): State? { | ||||
| /** | ||||
|  * Default [StatesMachine] may [startChain] and use inside logic for handling [State]s. By default you may use | ||||
|  * [DefaultStatesMachine] or build it with [dev.inmo.micro_utils.fsm.common.dsl.buildFSM]. Implementers MUST NOT start | ||||
|  * handling until [start] method will be called | ||||
|  */ | ||||
| interface StatesMachine<T : State> : StatesHandler<T, T> { | ||||
|     suspend fun launchStateHandling( | ||||
|         state: T, | ||||
|         handlers: List<CheckableHandlerHolder<in T, T>> | ||||
|     ): T? { | ||||
|         return handlers.firstOrNull { it.checkHandleable(state) } ?.run { | ||||
|             handleState(state) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Starts handling of [State]s | ||||
|      */ | ||||
|     fun start(scope: CoroutineScope): Job | ||||
|  | ||||
|     /** | ||||
|      * Start chain of [State]s witn [state] | ||||
|      */ | ||||
|     suspend fun startChain(state: T) | ||||
|  | ||||
|     companion object { | ||||
|         /** | ||||
|          * Creates [DefaultStatesMachine] | ||||
|          */ | ||||
|         operator fun <T: State> invoke( | ||||
|             statesManager: StatesManager<T>, | ||||
|             handlers: List<CheckableHandlerHolder<in T, T>> | ||||
|         ) = DefaultStatesMachine(statesManager, handlers) | ||||
|     } | ||||
| } | ||||
|  | ||||
| class StatesMachine ( | ||||
|     private val statesManager: StatesManager, | ||||
|     private val handlers: List<StateHandlerHolder<*>> | ||||
| ) : StatesHandler<State> { | ||||
|     override suspend fun StatesMachine.handleState(state: State): State? = launchStateHandling(state, handlers) | ||||
| /** | ||||
|  * Default realization of [StatesMachine]. It uses [statesManager] for incapsulation of [State]s storing and contexts | ||||
|  * 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 | ||||
|  */ | ||||
| open class DefaultStatesMachine <T: State>( | ||||
|     protected val statesManager: StatesManager<T>, | ||||
|     protected val handlers: List<CheckableHandlerHolder<in T, T>>, | ||||
| ) : StatesMachine<T> { | ||||
|     /** | ||||
|      * Will call [launchStateHandling] for state handling | ||||
|      */ | ||||
|     override suspend fun StatesMachine<in T>.handleState(state: T): T? = launchStateHandling(state, handlers) | ||||
|  | ||||
|     fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions { | ||||
|         val statePerformer: suspend (State) -> Unit = { state: State -> | ||||
|     /** | ||||
|      * This | ||||
|      */ | ||||
|     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) | ||||
| @@ -28,19 +72,59 @@ class StatesMachine ( | ||||
|             statesManager.endChain(state) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     open suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) { | ||||
|         statesJobsMutex.withLock { | ||||
|             statesJobs[actualState] ?.cancel() | ||||
|             statesJobs[actualState] = scope.launch { | ||||
|                 performUpdate(actualState) | ||||
|             }.also { job -> | ||||
|                 job.invokeOnCompletion { _ -> | ||||
|                     scope.launch { | ||||
|                         statesJobsMutex.withLock { | ||||
|                             if (statesJobs[actualState] == job) { | ||||
|                                 statesJobs.remove(actualState) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Launch handling of states. On [statesManager] [StatesManager.onStartChain], | ||||
|      * [statesManager] [StatesManager.onChainStateUpdated] will be called lambda with performing of state. If | ||||
|      * [launchStateHandling] will returns some [State] then [statesManager] [StatesManager.update] will be used, otherwise | ||||
|      * [StatesManager.endChain]. | ||||
|      */ | ||||
|     override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions { | ||||
|         statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) { | ||||
|             launch { statePerformer(it) } | ||||
|             launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) } | ||||
|         } | ||||
|         statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) { | ||||
|             launch { statePerformer(it.second) } | ||||
|             launch { performStateUpdate(Optional.presented(it.first), it.second, scope.LinkedSupervisorScope()) } | ||||
|         } | ||||
|         statesManager.onEndChain.subscribeSafelyWithoutExceptions(this) { removedState -> | ||||
|             launch { | ||||
|                 statesJobsMutex.withLock { | ||||
|                     val stateInMap = statesJobs.keys.firstOrNull { stateInMap -> stateInMap == removedState } | ||||
|                     if (stateInMap === removedState) { | ||||
|                         statesJobs[stateInMap] ?.cancel() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         statesManager.getActiveStates().forEach { | ||||
|             launch { statePerformer(it) } | ||||
|             launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun startChain(state: State) { | ||||
|     /** | ||||
|      * Just calls [StatesManager.startChain] of [statesManager] | ||||
|      */ | ||||
|     override suspend fun startChain(state: T) { | ||||
|         statesManager.startChain(state) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,92 +1,30 @@ | ||||
| package dev.inmo.micro_utils.fsm.common | ||||
|  | ||||
| import kotlinx.coroutines.flow.* | ||||
| import kotlinx.coroutines.sync.Mutex | ||||
| import kotlinx.coroutines.sync.withLock | ||||
| import kotlinx.coroutines.flow.Flow | ||||
|  | ||||
| interface StatesManager { | ||||
|     val onChainStateUpdated: Flow<Pair<State, State>> | ||||
|     val onStartChain: Flow<State> | ||||
|     val onEndChain: Flow<State> | ||||
| interface StatesManager<T : State> { | ||||
|     val onChainStateUpdated: Flow<Pair<T, T>> | ||||
|     val onStartChain: Flow<T> | ||||
|     val onEndChain: Flow<T> | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Must set current set using [State.context] | ||||
|      */ | ||||
|     suspend fun update(old: State, new: State) | ||||
|     suspend fun update(old: T, new: T) | ||||
|  | ||||
|     /** | ||||
|      * Starts chain with [state] as first [State]. May returns false in case of [State.context] of [state] is already | ||||
|      * busy by the other [State] | ||||
|      */ | ||||
|     suspend fun startChain(state: State) | ||||
|     suspend fun startChain(state: T) | ||||
|  | ||||
|     /** | ||||
|      * Ends chain with context from [state]. In case when [State.context] of [state] is absent, [state] should be just | ||||
|      * ignored | ||||
|      */ | ||||
|     suspend fun endChain(state: State) | ||||
|     suspend fun endChain(state: T) | ||||
|  | ||||
|     suspend fun getActiveStates(): List<State> | ||||
|     suspend fun getActiveStates(): List<T> | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context] | ||||
|  * key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by | ||||
|  * new state by using [endChain] with that state | ||||
|  */ | ||||
| class InMemoryStatesManager( | ||||
|     private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true } | ||||
| ) : StatesManager { | ||||
|     private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0) | ||||
|     override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow() | ||||
|     private val _onStartChain = MutableSharedFlow<State>(0) | ||||
|     override val onStartChain: Flow<State> = _onStartChain.asSharedFlow() | ||||
|     private val _onEndChain = MutableSharedFlow<State>(0) | ||||
|     override val onEndChain: Flow<State> = _onEndChain.asSharedFlow() | ||||
|  | ||||
|     private val contextsToStates = mutableMapOf<Any, State>() | ||||
|     private val mapMutex = Mutex() | ||||
|  | ||||
|     override suspend fun update(old: State, new: State) = mapMutex.withLock { | ||||
|         when { | ||||
|             contextsToStates[old.context] != old -> return@withLock | ||||
|             old.context == new.context || !contextsToStates.containsKey(new.context) -> { | ||||
|                 contextsToStates[old.context] = new | ||||
|                 _onChainStateUpdated.emit(old to new) | ||||
|             } | ||||
|             else -> { | ||||
|                 val stateOnNewOneContext = contextsToStates.getValue(new.context) | ||||
|                 if (onContextsConflictResolver(old, new, stateOnNewOneContext)) { | ||||
|                     endChainWithoutLock(stateOnNewOneContext) | ||||
|                     contextsToStates.remove(old.context) | ||||
|                     contextsToStates[new.context] = new | ||||
|                     _onChainStateUpdated.emit(old to new) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override suspend fun startChain(state: State) = mapMutex.withLock { | ||||
|         if (!contextsToStates.containsKey(state.context)) { | ||||
|             contextsToStates[state.context] = state | ||||
|             _onStartChain.emit(state) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private suspend fun endChainWithoutLock(state: State) { | ||||
|         if (contextsToStates[state.context] == state) { | ||||
|             contextsToStates.remove(state.context) | ||||
|             _onEndChain.emit(state) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override suspend fun endChain(state: State) { | ||||
|         mapMutex.withLock { | ||||
|             endChainWithoutLock(state) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override suspend fun getActiveStates(): List<State> = contextsToStates.values.toList() | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,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) | ||||
|     } | ||||
| } | ||||
| @@ -1,35 +1,61 @@ | ||||
| package dev.inmo.micro_utils.fsm.common.dsl | ||||
|  | ||||
| import dev.inmo.micro_utils.fsm.common.* | ||||
| import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager | ||||
| import dev.inmo.micro_utils.fsm.common.managers.InMemoryDefaultStatesManagerRepo | ||||
| import kotlin.reflect.KClass | ||||
|  | ||||
| class FSMBuilder( | ||||
|     var statesManager: StatesManager = InMemoryStatesManager() | ||||
| ) { | ||||
|     private var states = mutableListOf<StateHandlerHolder<*>>() | ||||
|  | ||||
|     fun <I : State> add(kClass: KClass<I>, handler: StatesHandler<I>) { | ||||
|         states.add(StateHandlerHolder(kClass, false, handler)) | ||||
|     } | ||||
|  | ||||
|     fun <I : State> addStrict(kClass: KClass<I>, handler: StatesHandler<I>) { | ||||
|         states.add(StateHandlerHolder(kClass, true, handler)) | ||||
|     } | ||||
|  | ||||
|     fun build() = StatesMachine( | ||||
| class FSMBuilder<T : State>( | ||||
|     var statesManager: StatesManager<T> = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()), | ||||
|     val fsmBuilder: (statesManager: StatesManager<T>, states: List<CheckableHandlerHolder<T, T>>) -> StatesMachine<T> = { statesManager, states -> | ||||
|         StatesMachine( | ||||
|             statesManager, | ||||
|         states.toList() | ||||
|             states | ||||
|         ) | ||||
|     }, | ||||
|     var defaultStateHandler: StatesHandler<T, T>? = StatesHandler { null } | ||||
| ) { | ||||
|     private var states = mutableListOf<CheckableHandlerHolder<T, T>>() | ||||
|  | ||||
|     fun add(handler: CheckableHandlerHolder<T, T>) { | ||||
|         states.add(handler) | ||||
|     } | ||||
|  | ||||
|     fun <I : T> add(kClass: KClass<I>, handler: StatesHandler<I, T>) { | ||||
|         add(CheckableHandlerHolder(kClass, false, handler)) | ||||
|     } | ||||
|  | ||||
|     fun <I : T> add(filter: suspend (state: State) -> Boolean, handler: StatesHandler<I, T>) { | ||||
|         add(handler.holder(filter)) | ||||
|     } | ||||
|  | ||||
|     fun <I : T> addStrict(kClass: KClass<I>, handler: StatesHandler<I, T>) { | ||||
|         states.add(CheckableHandlerHolder(kClass, true, handler)) | ||||
|     } | ||||
|  | ||||
|     inline fun <reified I : T> onStateOrSubstate(handler: StatesHandler<I, T>) { | ||||
|         add(I::class, handler) | ||||
|     } | ||||
|  | ||||
|     inline fun <reified I : T> strictlyOn(handler: StatesHandler<I, T>) { | ||||
|         addStrict(I::class, handler) | ||||
|     } | ||||
|  | ||||
|     inline fun <reified I : T> doWhen( | ||||
|         noinline filter: suspend (state: State) -> Boolean, | ||||
|         handler: StatesHandler<I, T> | ||||
|     ) { | ||||
|         add(filter, handler) | ||||
|     } | ||||
|  | ||||
|     fun build() = fsmBuilder( | ||||
|         statesManager, | ||||
|         states.toList().let { list -> | ||||
|             defaultStateHandler ?.let { list + it.holder { true } } ?: list | ||||
|         } | ||||
|     ) | ||||
| } | ||||
|  | ||||
| inline fun <reified I : State> FSMBuilder.onStateOrSubstate(handler: StatesHandler<I>) { | ||||
|     add(I::class, handler) | ||||
| } | ||||
|  | ||||
| inline fun <reified I : State> FSMBuilder.strictlyOn(handler: StatesHandler<I>) { | ||||
|     addStrict(I::class, handler) | ||||
| } | ||||
|  | ||||
| fun buildFSM( | ||||
|     block: FSMBuilder.() -> Unit | ||||
| ): StatesMachine = FSMBuilder().apply(block).build() | ||||
| fun <T : State> buildFSM( | ||||
|     block: FSMBuilder<T>.() -> Unit | ||||
| ): StatesMachine<T> = FSMBuilder<T>().apply(block).build() | ||||
|   | ||||
| @@ -0,0 +1,118 @@ | ||||
| package dev.inmo.micro_utils.fsm.common.managers | ||||
|  | ||||
| import dev.inmo.micro_utils.fsm.common.State | ||||
| import dev.inmo.micro_utils.fsm.common.StatesManager | ||||
| import kotlinx.coroutines.flow.* | ||||
| import kotlinx.coroutines.sync.Mutex | ||||
| import kotlinx.coroutines.sync.withLock | ||||
|  | ||||
| /** | ||||
|  * Implement this repo if you want to use some custom repo for [DefaultStatesManager] | ||||
|  */ | ||||
| interface DefaultStatesManagerRepo<T : State> { | ||||
|     /** | ||||
|      * Must save [state] as current state of chain with [State.context] of [state] | ||||
|      */ | ||||
|     suspend fun set(state: T) | ||||
|     /** | ||||
|      * Remove exactly [state]. In case if internally [State.context] is busy with different [State], that [State] should | ||||
|      * NOT be removed | ||||
|      */ | ||||
|     suspend fun removeState(state: T) | ||||
|     /** | ||||
|      * @return Current list of available and saved states | ||||
|      */ | ||||
|     suspend fun getStates(): List<T> | ||||
|  | ||||
|     /** | ||||
|      * @return Current state by [context] | ||||
|      */ | ||||
|     suspend fun getContextState(context: Any): T? | ||||
|  | ||||
|     /** | ||||
|      * @return Current state by [context] | ||||
|      */ | ||||
|     suspend fun contains(context: Any): Boolean = getContextState(context) != null | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param repo This repo will be used as repository for storing states. All operations with this repo will happen BEFORE | ||||
|  * any event will be sent to [onChainStateUpdated], [onStartChain] or [onEndChain]. By default, will be used | ||||
|  * [InMemoryDefaultStatesManagerRepo] or you may create custom [DefaultStatesManagerRepo] and pass as [repo] parameter | ||||
|  * @param 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 | ||||
|  * new state by using [endChain] with that state | ||||
|  */ | ||||
| open class DefaultStatesManager<T : State>( | ||||
|     protected val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(), | ||||
|     protected val onStartContextsConflictResolver: suspend (current: T, new: T) -> Boolean = { _, _ -> true }, | ||||
|     protected val onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true } | ||||
| ) : StatesManager<T> { | ||||
|     protected val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0) | ||||
|     override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow() | ||||
|     protected val _onStartChain = MutableSharedFlow<T>(0) | ||||
|     override val onStartChain: Flow<T> = _onStartChain.asSharedFlow() | ||||
|     protected val _onEndChain = MutableSharedFlow<T>(0) | ||||
|     override val onEndChain: Flow<T> = _onEndChain.asSharedFlow() | ||||
|  | ||||
|     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 { | ||||
|         val stateByOldContext: T? = repo.getContextState(old.context) | ||||
|         when { | ||||
|             stateByOldContext != old -> return@withLock | ||||
|             stateByOldContext == null || old.context == new.context -> { | ||||
|                 repo.removeState(old) | ||||
|                 repo.set(new) | ||||
|                 _onChainStateUpdated.emit(old to new) | ||||
|             } | ||||
|             else -> { | ||||
|                 val stateOnNewOneContext = repo.getContextState(new.context) | ||||
|                 if (stateOnNewOneContext == null || onUpdateContextsConflictResolver(old, new, stateOnNewOneContext)) { | ||||
|                     stateOnNewOneContext ?.let { endChainWithoutLock(it) } | ||||
|                     repo.removeState(old) | ||||
|                     repo.set(new) | ||||
|                     _onChainStateUpdated.emit(old to new) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override suspend fun startChain(state: T) = mapMutex.withLock { | ||||
|         val stateOnContext = repo.getContextState(state.context) | ||||
|         if (stateOnContext == null || onStartContextsConflictResolver(stateOnContext, state)) { | ||||
|             stateOnContext ?.let { | ||||
|                 endChainWithoutLock(it) | ||||
|             } | ||||
|             repo.set(state) | ||||
|             _onStartChain.emit(state) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected open suspend fun endChainWithoutLock(state: T) { | ||||
|         if (repo.getContextState(state.context) == state) { | ||||
|             repo.removeState(state) | ||||
|             _onEndChain.emit(state) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override suspend fun endChain(state: T) { | ||||
|         mapMutex.withLock { | ||||
|             endChainWithoutLock(state) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override suspend fun getActiveStates(): List<T> = repo.getStates() | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,25 @@ | ||||
| package dev.inmo.micro_utils.fsm.common.managers | ||||
|  | ||||
| import dev.inmo.micro_utils.fsm.common.State | ||||
|  | ||||
| /** | ||||
|  * Simple [DefaultStatesManagerRepo] for [DefaultStatesManager] which will store data in [map] and use primitive | ||||
|  * functionality | ||||
|  */ | ||||
| class InMemoryDefaultStatesManagerRepo<T : State>( | ||||
|     private val map: MutableMap<Any, T> = mutableMapOf() | ||||
| ) : DefaultStatesManagerRepo<T> { | ||||
|     override suspend fun set(state: T) { | ||||
|         map[state.context] = state | ||||
|     } | ||||
|  | ||||
|     override suspend fun removeState(state: T) { | ||||
|         map.remove(state.context) | ||||
|     } | ||||
|  | ||||
|     override suspend fun getStates(): List<T> = map.values.toList() | ||||
|  | ||||
|     override suspend fun getContextState(context: Any): T? = map[context] | ||||
|  | ||||
|     override suspend fun contains(context: Any): Boolean = map.contains(context) | ||||
| } | ||||
| @@ -0,0 +1,17 @@ | ||||
| package dev.inmo.micro_utils.fsm.common.managers | ||||
|  | ||||
| import dev.inmo.micro_utils.fsm.common.State | ||||
| import kotlinx.coroutines.flow.* | ||||
|  | ||||
| /** | ||||
|  * Creates [DefaultStatesManager] with [InMemoryDefaultStatesManagerRepo] | ||||
|  * | ||||
|  * @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context] | ||||
|  * key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by | ||||
|  * new state by using [endChain] with that state | ||||
|  */ | ||||
| @Deprecated("Use DefaultStatesManager instead", ReplaceWith("DefaultStatesManager")) | ||||
| fun <T: State> InMemoryStatesManager( | ||||
|     onStartContextsConflictResolver: suspend (old: T, new: T) -> Boolean = { _, _ -> true }, | ||||
|     onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true } | ||||
| ) = DefaultStatesManager(onStartContextsConflictResolver = onStartContextsConflictResolver, onUpdateContextsConflictResolver = onUpdateContextsConflictResolver) | ||||
| @@ -1,6 +1,7 @@ | ||||
| import dev.inmo.micro_utils.fsm.common.* | ||||
| import dev.inmo.micro_utils.fsm.common.dsl.buildFSM | ||||
| import dev.inmo.micro_utils.fsm.common.dsl.strictlyOn | ||||
| import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager | ||||
| import dev.inmo.micro_utils.fsm.common.managers.InMemoryStatesManager | ||||
| import kotlinx.coroutines.* | ||||
|  | ||||
| sealed interface TrafficLightState : State { | ||||
| @@ -25,9 +26,9 @@ class PlayableMain { | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             val statesManager = InMemoryStatesManager() | ||||
|             val statesManager = DefaultStatesManager<TrafficLightState>() | ||||
|  | ||||
|             val machine = buildFSM { | ||||
|             val machine = buildFSM<TrafficLightState> { | ||||
|                 strictlyOn<GreenCommon> { | ||||
|                     delay(1000L) | ||||
|                     YellowCommon(it.context).also(::println) | ||||
|   | ||||
| @@ -0,0 +1,25 @@ | ||||
| package dev.inmo.micro_utils.fsm.repos.common | ||||
|  | ||||
| import dev.inmo.micro_utils.fsm.common.State | ||||
| import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo | ||||
| import dev.inmo.micro_utils.repos.* | ||||
| import dev.inmo.micro_utils.repos.pagination.getAll | ||||
|  | ||||
| class KeyValueBasedDefaultStatesManagerRepo<T : State>( | ||||
|     private val keyValueRepo: KeyValueRepo<Any, T> | ||||
| ) : DefaultStatesManagerRepo<T> { | ||||
|     override suspend fun set(state: T) { | ||||
|         keyValueRepo.set(state.context, state) | ||||
|     } | ||||
|  | ||||
|     override suspend fun removeState(state: T) { | ||||
|         if (keyValueRepo.get(state.context) == state) { | ||||
|             keyValueRepo.unset(state.context) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override suspend fun getStates(): List<T> = keyValueRepo.getAll { keys(it) }.map { it.second } | ||||
|     override suspend fun getContextState(context: Any): T? = keyValueRepo.get(context) | ||||
|  | ||||
|     override suspend fun contains(context: Any): Boolean = keyValueRepo.contains(context) | ||||
| } | ||||
| @@ -1,83 +0,0 @@ | ||||
| package dev.inmo.micro_utils.fsm.repos.common | ||||
|  | ||||
| import dev.inmo.micro_utils.fsm.common.State | ||||
| import dev.inmo.micro_utils.fsm.common.StatesManager | ||||
| import dev.inmo.micro_utils.repos.* | ||||
| import dev.inmo.micro_utils.repos.mappers.withMapper | ||||
| import dev.inmo.micro_utils.repos.pagination.getAll | ||||
| import kotlinx.coroutines.flow.* | ||||
| import kotlinx.coroutines.sync.Mutex | ||||
| import kotlinx.coroutines.sync.withLock | ||||
|  | ||||
| class KeyValueBasedStatesManager( | ||||
|     private val keyValueRepo: KeyValueRepo<Any, State>, | ||||
|     private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true } | ||||
| ) : StatesManager { | ||||
|     private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0) | ||||
|     override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow() | ||||
|     private val _onEndChain = MutableSharedFlow<State>(0) | ||||
|     override val onEndChain: Flow<State> = _onEndChain.asSharedFlow() | ||||
|  | ||||
|     override val onStartChain: Flow<State> = keyValueRepo.onNewValue.map { it.second } | ||||
|  | ||||
|     private val mutex = Mutex() | ||||
|  | ||||
|     override suspend fun update(old: State, new: State) { | ||||
|         mutex.withLock { | ||||
|             when { | ||||
|                 keyValueRepo.get(old.context) != old -> return@withLock | ||||
|                 old.context == new.context || !keyValueRepo.contains(new.context) -> { | ||||
|                     keyValueRepo.set(old.context, new) | ||||
|                     _onChainStateUpdated.emit(old to new) | ||||
|                 } | ||||
|                 else -> { | ||||
|                     val stateOnNewOneContext = keyValueRepo.get(new.context)!! | ||||
|                     if (onContextsConflictResolver(old, new, stateOnNewOneContext)) { | ||||
|                         endChainWithoutLock(stateOnNewOneContext) | ||||
|                         keyValueRepo.unset(old.context) | ||||
|                         keyValueRepo.set(new.context, new) | ||||
|                         _onChainStateUpdated.emit(old to new) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override suspend fun startChain(state: State) { | ||||
|         if (!keyValueRepo.contains(state.context)) { | ||||
|             keyValueRepo.set(state.context, state) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private suspend fun endChainWithoutLock(state: State) { | ||||
|         if (keyValueRepo.get(state.context) == state) { | ||||
|             keyValueRepo.unset(state.context) | ||||
|             _onEndChain.emit(state) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override suspend fun endChain(state: State) { | ||||
|         mutex.withLock { endChainWithoutLock(state) } | ||||
|     } | ||||
|  | ||||
|     override suspend fun getActiveStates(): List<State> { | ||||
|         return keyValueRepo.getAll { keys(it) }.map { it.second } | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| inline fun <reified TargetContextType, reified TargetStateType> createStatesManager( | ||||
|     targetKeyValueRepo: KeyValueRepo<TargetContextType, TargetStateType>, | ||||
|     noinline contextToOutTransformer: suspend Any.() -> TargetContextType, | ||||
|     noinline stateToOutTransformer: suspend State.() -> TargetStateType, | ||||
|     noinline outToContextTransformer: suspend TargetContextType.() -> Any, | ||||
|     noinline outToStateTransformer: suspend TargetStateType.() -> State, | ||||
| ) = KeyValueBasedStatesManager( | ||||
|     targetKeyValueRepo.withMapper<Any, State, TargetContextType, TargetStateType>( | ||||
|         contextToOutTransformer, | ||||
|         stateToOutTransformer, | ||||
|         outToContextTransformer, | ||||
|         outToStateTransformer | ||||
|     ) | ||||
| ) | ||||
| @@ -7,43 +7,12 @@ android.useAndroidX=true | ||||
| android.enableJetifier=true | ||||
| org.gradle.jvmargs=-Xmx2g | ||||
|  | ||||
| kotlin_version=1.5.21 | ||||
| kotlin_coroutines_version=1.5.1 | ||||
| kotlin_serialisation_core_version=1.2.2 | ||||
| kotlin_exposed_version=0.32.1 | ||||
|  | ||||
| ktor_version=1.6.2 | ||||
|  | ||||
| klockVersion=2.3.1 | ||||
|  | ||||
| github_release_plugin_version=2.2.12 | ||||
|  | ||||
| uuidVersion=0.3.0 | ||||
|  | ||||
| # ANDROID | ||||
|  | ||||
| core_ktx_version=1.6.0 | ||||
| androidx_recycler_version=1.2.1 | ||||
| appcompat_version=1.3.0 | ||||
|  | ||||
| android_minSdkVersion=19 | ||||
| android_compileSdkVersion=30 | ||||
| android_buildToolsVersion=30.0.3 | ||||
| dexcount_version=2.1.0-RC01 | ||||
| junit_version=4.12 | ||||
| test_ext_junit_version=1.1.2 | ||||
| espresso_core=3.3.0 | ||||
|  | ||||
| # JS NPM | ||||
|  | ||||
| crypto_js_version=4.1.1 | ||||
|  | ||||
| # Dokka | ||||
|  | ||||
| dokka_version=1.4.32 | ||||
|  | ||||
| # Project data | ||||
|  | ||||
| group=dev.inmo | ||||
| version=0.5.18 | ||||
| android_code_version=59 | ||||
| version=0.9.18 | ||||
| android_code_version=108 | ||||
|   | ||||
							
								
								
									
										77
									
								
								gradle/libs.versions.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								gradle/libs.versions.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| [versions] | ||||
|  | ||||
| kt = "1.6.10" | ||||
| kt-serialization = "1.3.2" | ||||
| kt-coroutines = "1.6.0" | ||||
|  | ||||
| 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-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 | ||||
| distributionPath=wrapper/dists | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip | ||||
| zipStoreBase=GRADLE_USER_HOME | ||||
| zipStorePath=wrapper/dists | ||||
|   | ||||
| @@ -12,7 +12,7 @@ kotlin { | ||||
|             dependencies { | ||||
|                 api internalProject("micro_utils.ktor.common") | ||||
|                 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 io.ktor.client.HttpClient | ||||
| 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.readBytes | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| @@ -17,6 +18,7 @@ import kotlinx.serialization.DeserializationStrategy | ||||
| inline fun <T> HttpClient.createStandardWebsocketFlow( | ||||
|     url: String, | ||||
|     crossinline checkReconnection: (Throwable?) -> Boolean = { true }, | ||||
|     noinline requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||
|     crossinline conversation: suspend (StandardKtorSerialInputData) -> T | ||||
| ): Flow<T> { | ||||
|     val correctedUrl = url.asCorrectWebSocketUrl | ||||
| @@ -26,7 +28,7 @@ inline fun <T> HttpClient.createStandardWebsocketFlow( | ||||
|         do { | ||||
|             val reconnect = try { | ||||
|                 safely { | ||||
|                     ws(correctedUrl) { | ||||
|                     ws(correctedUrl, requestBuilder) { | ||||
|                         for (received in incoming) { | ||||
|                             when (received) { | ||||
|                                 is Frame.Binary -> producerScope.send(conversation(received.readBytes())) | ||||
| @@ -65,10 +67,12 @@ inline fun <T> HttpClient.createStandardWebsocketFlow( | ||||
|     url: String, | ||||
|     crossinline checkReconnection: (Throwable?) -> Boolean = { true }, | ||||
|     deserializer: DeserializationStrategy<T>, | ||||
|     serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat | ||||
|     serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat, | ||||
|     noinline requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||
| ) = createStandardWebsocketFlow( | ||||
|     url, | ||||
|     checkReconnection | ||||
|     checkReconnection, | ||||
|     requestBuilder | ||||
| ) { | ||||
|     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 | ||||
|  | ||||
| import dev.inmo.micro_utils.common.MPPFile | ||||
| import dev.inmo.micro_utils.common.filename | ||||
| import dev.inmo.micro_utils.ktor.common.* | ||||
| import io.ktor.client.HttpClient | ||||
| import io.ktor.client.request.get | ||||
| import io.ktor.client.request.post | ||||
| import io.ktor.client.request.* | ||||
| import io.ktor.client.request.forms.* | ||||
| import io.ktor.http.* | ||||
| import io.ktor.utils.io.core.ByteReadPacket | ||||
| import kotlinx.serialization.* | ||||
|  | ||||
| typealias BodyPair<T> = Pair<SerializationStrategy<T>, T> | ||||
|  | ||||
| class UnifiedRequester( | ||||
|     private val client: HttpClient = HttpClient(), | ||||
|     private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat | ||||
|     val client: HttpClient = HttpClient(), | ||||
|     val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat | ||||
| ) { | ||||
|     suspend fun <ResultType> uniget( | ||||
|         url: String, | ||||
| @@ -31,11 +35,66 @@ class UnifiedRequester( | ||||
|         resultDeserializer: DeserializationStrategy<ResultType> | ||||
|     ) = client.unipost(url, bodyInfo, resultDeserializer, serialFormat) | ||||
|  | ||||
|     suspend fun <ResultType> unimultipart( | ||||
|         url: String, | ||||
|         filename: String, | ||||
|         inputProvider: InputProvider, | ||||
|         resultDeserializer: DeserializationStrategy<ResultType>, | ||||
|         mimetype: String = "*/*", | ||||
|         additionalParametersBuilder: FormBuilder.() -> Unit = {}, | ||||
|         dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, | ||||
|         requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||
|     ): ResultType = client.unimultipart(url, filename, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat) | ||||
|  | ||||
|     suspend fun <BodyType, ResultType> unimultipart( | ||||
|         url: String, | ||||
|         filename: String, | ||||
|         inputProvider: InputProvider, | ||||
|         otherData: BodyPair<BodyType>, | ||||
|         resultDeserializer: DeserializationStrategy<ResultType>, | ||||
|         mimetype: String = "*/*", | ||||
|         additionalParametersBuilder: FormBuilder.() -> Unit = {}, | ||||
|         dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, | ||||
|         requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||
|     ): ResultType = client.unimultipart(url, filename, otherData, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat) | ||||
|  | ||||
|     suspend fun <ResultType> unimultipart( | ||||
|         url: String, | ||||
|         mppFile: MPPFile, | ||||
|         resultDeserializer: DeserializationStrategy<ResultType>, | ||||
|         mimetype: String = "*/*", | ||||
|         additionalParametersBuilder: FormBuilder.() -> Unit = {}, | ||||
|         dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, | ||||
|         requestBuilder: HttpRequestBuilder.() -> Unit = {} | ||||
|     ): ResultType = client.unimultipart( | ||||
|         url, mppFile, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat | ||||
|     ) | ||||
|  | ||||
|     suspend fun <BodyType, ResultType> unimultipart( | ||||
|         url: String, | ||||
|         mppFile: MPPFile, | ||||
|         otherData: BodyPair<BodyType>, | ||||
|         resultDeserializer: DeserializationStrategy<ResultType>, | ||||
|         mimetype: String = "*/*", | ||||
|         additionalParametersBuilder: FormBuilder.() -> Unit = {}, | ||||
|         dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, | ||||
|         requestBuilder: HttpRequestBuilder.() -> Unit = {} | ||||
|     ): ResultType = client.unimultipart( | ||||
|         url, mppFile, otherData, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat | ||||
|     ) | ||||
|  | ||||
|     fun <T> createStandardWebsocketFlow( | ||||
|         url: String, | ||||
|         checkReconnection: (Throwable?) -> Boolean = { true }, | ||||
|         deserializer: DeserializationStrategy<T> | ||||
|     ) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat) | ||||
|         checkReconnection: (Throwable?) -> Boolean, | ||||
|         deserializer: DeserializationStrategy<T>, | ||||
|         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() | ||||
| @@ -69,3 +128,124 @@ suspend fun <BodyType, ResultType> HttpClient.unipost( | ||||
| }.let { | ||||
|     serialFormat.decodeDefault(resultDeserializer, it) | ||||
| } | ||||
|  | ||||
| suspend fun <ResultType> HttpClient.unimultipart( | ||||
|     url: String, | ||||
|     filename: String, | ||||
|     inputProvider: InputProvider, | ||||
|     resultDeserializer: DeserializationStrategy<ResultType>, | ||||
|     mimetype: String = "*/*", | ||||
|     additionalParametersBuilder: FormBuilder.() -> Unit = {}, | ||||
|     dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, | ||||
|     requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||
|     serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat | ||||
| ): ResultType = submitFormWithBinaryData<StandardKtorSerialInputData>( | ||||
|     url, | ||||
|     formData = formData { | ||||
|         append( | ||||
|             "bytes", | ||||
|             inputProvider, | ||||
|             Headers.build { | ||||
|                 append(HttpHeaders.ContentType, mimetype) | ||||
|                 append(HttpHeaders.ContentDisposition, "filename=\"$filename\"") | ||||
|                 dataHeadersBuilder() | ||||
|             } | ||||
|         ) | ||||
|         additionalParametersBuilder() | ||||
|     } | ||||
| ) { | ||||
|     requestBuilder() | ||||
| }.let { serialFormat.decodeDefault(resultDeserializer, it) } | ||||
|  | ||||
| suspend fun <BodyType, ResultType> HttpClient.unimultipart( | ||||
|     url: String, | ||||
|     filename: String, | ||||
|     otherData: BodyPair<BodyType>, | ||||
|     inputProvider: InputProvider, | ||||
|     resultDeserializer: DeserializationStrategy<ResultType>, | ||||
|     mimetype: String = "*/*", | ||||
|     additionalParametersBuilder: FormBuilder.() -> Unit = {}, | ||||
|     dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, | ||||
|     requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||
|     serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat | ||||
| ): ResultType = unimultipart( | ||||
|     url, | ||||
|     filename, | ||||
|     inputProvider, | ||||
|     resultDeserializer, | ||||
|     mimetype, | ||||
|     additionalParametersBuilder = { | ||||
|         val serialized = serialFormat.encodeDefault(otherData.first, otherData.second) | ||||
|         append( | ||||
|             "data", | ||||
|             InputProvider(serialized.size.toLong()) { | ||||
|                 ByteReadPacket(serialized) | ||||
|             }, | ||||
|             Headers.build { | ||||
|                 append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType) | ||||
|                 append(HttpHeaders.ContentDisposition, "filename=data.bytes") | ||||
|                 dataHeadersBuilder() | ||||
|             } | ||||
|         ) | ||||
|         additionalParametersBuilder() | ||||
|     }, | ||||
|     dataHeadersBuilder, | ||||
|     requestBuilder, | ||||
|     serialFormat | ||||
| ) | ||||
|  | ||||
| suspend fun <ResultType> HttpClient.unimultipart( | ||||
|     url: String, | ||||
|     mppFile: MPPFile, | ||||
|     resultDeserializer: DeserializationStrategy<ResultType>, | ||||
|     mimetype: String = "*/*", | ||||
|     additionalParametersBuilder: FormBuilder.() -> Unit = {}, | ||||
|     dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, | ||||
|     requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||
|     serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat | ||||
| ): ResultType = unimultipart( | ||||
|     url, | ||||
|     mppFile.filename.string, | ||||
|     mppFile.inputProvider(), | ||||
|     resultDeserializer, | ||||
|     mimetype, | ||||
|     additionalParametersBuilder, | ||||
|     dataHeadersBuilder, | ||||
|     requestBuilder, | ||||
|     serialFormat | ||||
| ) | ||||
|  | ||||
| suspend fun <BodyType, ResultType> HttpClient.unimultipart( | ||||
|     url: String, | ||||
|     mppFile: MPPFile, | ||||
|     otherData: BodyPair<BodyType>, | ||||
|     resultDeserializer: DeserializationStrategy<ResultType>, | ||||
|     mimetype: String = "*/*", | ||||
|     additionalParametersBuilder: FormBuilder.() -> Unit = {}, | ||||
|     dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, | ||||
|     requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||
|     serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat | ||||
| ): ResultType = unimultipart( | ||||
|     url, | ||||
|     mppFile, | ||||
|     resultDeserializer, | ||||
|     mimetype, | ||||
|     additionalParametersBuilder = { | ||||
|         val serialized = serialFormat.encodeDefault(otherData.first, otherData.second) | ||||
|         append( | ||||
|             "data", | ||||
|             InputProvider(serialized.size.toLong()) { | ||||
|                 ByteReadPacket(serialized) | ||||
|             }, | ||||
|             Headers.build { | ||||
|                 append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType) | ||||
|                 append(HttpHeaders.ContentDisposition, "filename=data.bytes") | ||||
|                 dataHeadersBuilder() | ||||
|             } | ||||
|         ) | ||||
|         additionalParametersBuilder() | ||||
|     }, | ||||
|     dataHeadersBuilder, | ||||
|     requestBuilder, | ||||
|     serialFormat | ||||
| ) | ||||
|   | ||||
| @@ -0,0 +1,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,10 @@ kotlin { | ||||
|     sourceSets { | ||||
|         commonMain { | ||||
|             dependencies { | ||||
|                 api "org.jetbrains.kotlinx:kotlinx-serialization-cbor:$kotlin_serialisation_core_version" | ||||
|                 api "com.soywiz.korlibs.klock:klock:$klockVersion" | ||||
|                 api internalProject("micro_utils.common") | ||||
|                 api libs.kt.serialization.cbor | ||||
|                 api libs.klock | ||||
|                 api libs.uuid | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -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) | ||||
| @@ -16,10 +16,10 @@ kotlin { | ||||
|  | ||||
|         jvmMain { | ||||
|             dependencies { | ||||
|                 api "io.ktor:ktor-server:$ktor_version" | ||||
|                 api "io.ktor:ktor-server-cio:$ktor_version" | ||||
|                 api "io.ktor:ktor-server-host-common:$ktor_version" | ||||
|                 api "io.ktor:ktor-websockets:$ktor_version" | ||||
|                 api libs.ktor.server | ||||
|                 api libs.ktor.server.cio | ||||
|                 api libs.ktor.server.host.common | ||||
|                 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.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.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.collect | ||||
| import kotlinx.serialization.SerializationStrategy | ||||
|  | ||||
| private suspend fun DefaultWebSocketSession.checkReceivedAndCloseIfExists() { | ||||
|     if (incoming.tryReceive() != null) { | ||||
|         close() | ||||
|         throw CorrectCloseException | ||||
|     } | ||||
| } | ||||
|  | ||||
| fun <T> Route.includeWebsocketHandling( | ||||
|     suburl: String, | ||||
|     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 { | ||||
|             flow.collect { | ||||
|                 send(converter(it)) | ||||
|                 converter(it) ?.let { data -> | ||||
|                     send(data) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -34,10 +36,24 @@ fun <T> Route.includeWebsocketHandling( | ||||
|     suburl: String, | ||||
|     flow: Flow<T>, | ||||
|     serializer: SerializationStrategy<T>, | ||||
|     serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat | ||||
|     serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat, | ||||
|     protocol: URLProtocol = URLProtocol.WS, | ||||
|     filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null | ||||
| ) = includeWebsocketHandling( | ||||
|     suburl, | ||||
|     flow | ||||
| ) { | ||||
|     flow, | ||||
|     protocol, | ||||
|     converter = if (filter == null) { | ||||
|         { | ||||
|             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 | ||||
|  | ||||
| import dev.inmo.micro_utils.common.* | ||||
| import dev.inmo.micro_utils.coroutines.safely | ||||
| import dev.inmo.micro_utils.ktor.common.* | ||||
| import io.ktor.application.ApplicationCall | ||||
| import io.ktor.application.call | ||||
| import io.ktor.http.ContentType | ||||
| import io.ktor.http.HttpStatusCode | ||||
| import io.ktor.http.* | ||||
| import io.ktor.http.content.PartData | ||||
| import io.ktor.http.content.forEachPart | ||||
| import io.ktor.request.receive | ||||
| import io.ktor.request.receiveMultipart | ||||
| import io.ktor.response.respond | ||||
| import io.ktor.response.respondBytes | ||||
| import io.ktor.routing.Route | ||||
| import io.ktor.util.asStream | ||||
| import io.ktor.util.cio.writeChannel | ||||
| import io.ktor.util.pipeline.PipelineContext | ||||
| import io.ktor.utils.io.core.* | ||||
| import io.ktor.websocket.WebSocketServerSession | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.serialization.* | ||||
| import java.io.File | ||||
| import java.io.File.createTempFile | ||||
|  | ||||
| class UnifiedRouter( | ||||
|     private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat, | ||||
|     private val serialFormatContentType: ContentType = standardKtorSerialFormatContentType | ||||
|     val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat, | ||||
|     val serialFormatContentType: ContentType = standardKtorSerialFormatContentType | ||||
| ) { | ||||
|     fun <T> Route.includeWebsocketHandling( | ||||
|         suburl: String, | ||||
|         flow: Flow<T>, | ||||
|         serializer: SerializationStrategy<T> | ||||
|     ) = includeWebsocketHandling(suburl, flow, serializer, serialFormat) | ||||
|         serializer: SerializationStrategy<T>, | ||||
|         protocol: URLProtocol = URLProtocol.WS, | ||||
|         filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null | ||||
|     ) = includeWebsocketHandling(suburl, flow, serializer, serialFormat, protocol, filter) | ||||
|  | ||||
|     suspend fun <T> PipelineContext<*, ApplicationCall>.unianswer( | ||||
|         answerSerializer: SerializationStrategy<T>, | ||||
| @@ -81,6 +92,11 @@ class UnifiedRouter( | ||||
|             call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field") | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         val default | ||||
|             get() = defaultUnifiedRouter | ||||
|     } | ||||
| } | ||||
|  | ||||
| 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( | ||||
|     field: String | ||||
| ) = parameters[field].also { | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| package dev.inmo.micro_utils.ktor.server | ||||
|  | ||||
| import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator | ||||
| import io.ktor.application.Application | ||||
| import io.ktor.server.cio.CIO | ||||
| import io.ktor.server.engine.* | ||||
| @@ -31,3 +32,27 @@ fun createKtorServer( | ||||
|     port: Int = Random.nextInt(1024, 65535), | ||||
|     block: Application.() -> Unit | ||||
| ): ApplicationEngine = createKtorServer(CIO, host, port, block) | ||||
|  | ||||
| fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration> createKtorServer( | ||||
|     engine: ApplicationEngineFactory<TEngine, TConfiguration>, | ||||
|     host: String = "localhost", | ||||
|     port: Int = Random.nextInt(1024, 65535), | ||||
|     configurators: List<KtorApplicationConfigurator> | ||||
| ): TEngine = createKtorServer( | ||||
|     engine, | ||||
|     host, | ||||
|     port | ||||
| ) { | ||||
|     configurators.forEach { it.apply { configure() } } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create server with [CIO] server engine without starting of it | ||||
|  * | ||||
|  * @see ApplicationEngine.start | ||||
|  */ | ||||
| fun createKtorServer( | ||||
|     host: String = "localhost", | ||||
|     port: Int = Random.nextInt(1024, 65535), | ||||
|     configurators: List<KtorApplicationConfigurator> | ||||
| ): ApplicationEngine = createKtorServer(CIO, host, port, configurators) | ||||
|   | ||||
| @@ -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 { | ||||
|         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" | ||||
|         classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" | ||||
|         classpath libs.buildscript.kt.gradle | ||||
|         classpath libs.buildscript.kt.serialization | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -16,11 +16,16 @@ plugins { | ||||
| } | ||||
|  | ||||
| dependencies { | ||||
|     implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" | ||||
|     implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_core_version" | ||||
|     implementation libs.kt.stdlib | ||||
|     implementation libs.kt.serialization | ||||
|  | ||||
|     implementation "io.ktor:ktor-client-core:$ktor_version" | ||||
|     implementation "io.ktor:ktor-client-java:$ktor_version" | ||||
|     implementation libs.ktor.client | ||||
|     implementation libs.ktor.client.java | ||||
| } | ||||
|  | ||||
| mainClassName="MainKt" | ||||
|  | ||||
| java { | ||||
|     sourceCompatibility = JavaVersion.VERSION_1_8 | ||||
|     targetCompatibility = JavaVersion.VERSION_1_8 | ||||
| } | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import kotlinx.serialization.Serializable | ||||
| import kotlinx.serialization.builtins.ListSerializer | ||||
| import kotlinx.serialization.json.Json | ||||
| import java.io.File | ||||
| import java.text.Normalizer | ||||
|  | ||||
| private val json = Json { | ||||
|     ignoreUnknownKeys = true | ||||
| @@ -29,6 +30,8 @@ fun String.adaptAsTitle() = if (first().isDigit()) { | ||||
|     this | ||||
| } | ||||
|  | ||||
| fun String.normalized() = Normalizer.normalize(this, Normalizer.Form.NFD).replace(Regex("[^\\p{ASCII}]"), "") | ||||
|  | ||||
| @Serializable | ||||
| private data class LanguageCodeWithTag( | ||||
|     @SerialName("langType") | ||||
| @@ -74,11 +77,11 @@ private fun printLanguageCodeAndTags( | ||||
|     indents: String = "    " | ||||
| ): String = if (tag.subtags.isEmpty()) { | ||||
| """${indents}${baseClassSerializerAnnotationName} | ||||
| ${indents}object ${tag.title} : ${parent ?.title ?.let { "$it()" } ?: baseClassName} { override val code: String = "${tag.tag}" }""" | ||||
| ${indents}object ${tag.title} : ${parent ?.title ?: baseClassName}() { override val code: String = "${tag.tag}" }""" | ||||
| } else { | ||||
| """ | ||||
| ${indents}${baseClassSerializerAnnotationName} | ||||
| ${indents}sealed class ${tag.title} : ${parent ?.title ?.let { "$it()" } ?: baseClassName} { | ||||
| ${indents}sealed class ${tag.title} : ${parent ?.title ?: baseClassName}() { | ||||
| ${indents}    override val code: String = "${tag.tag}" | ||||
|  | ||||
| ${tag.subtags.joinToString("\n") { printLanguageCodeAndTags(it, tag, "${indents}    ") }} | ||||
| @@ -98,13 +101,15 @@ import kotlinx.serialization.Serializable | ||||
|  * https://datahub.io/core/language-codes/ files (base and tags) and create the whole hierarchy using it. | ||||
|  */ | ||||
| ${baseClassSerializerAnnotationName} | ||||
| sealed interface $baseClassName { | ||||
|     val code: String | ||||
| sealed class $baseClassName { | ||||
|     abstract val code: String | ||||
|  | ||||
| ${tags.joinToString("\n") { printLanguageCodeAndTags(it, indents = "    ") } } | ||||
|  | ||||
|     $baseClassSerializerAnnotationName | ||||
|     data class $unknownBaseClassName (override val code: String) : $baseClassName | ||||
|     data class $unknownBaseClassName (override val code: String) : $baseClassName() | ||||
|  | ||||
|     override fun toString() = code | ||||
| } | ||||
| """.trimIndent() | ||||
|  | ||||
| @@ -179,18 +184,14 @@ suspend fun main(vararg args: String) { | ||||
|         val subtags = unformattedSubtags.mapNotNull { | ||||
|             if (it.endTag == null) { | ||||
|                 val currentSubtags = (threeLevelTags[it.subtag] ?: emptyList()).map { | ||||
|                     Tag(it.endTagAsTitle!!, it.withSubtag, emptyList()) | ||||
|                     Tag(it.endTagAsTitle!!.normalized(), it.withSubtag, emptyList()) | ||||
|                 } | ||||
|                 Tag(it.middleTagTitle, it.withSubtag, currentSubtags) | ||||
|                 Tag(it.middleTagTitle.normalized(), it.withSubtag, currentSubtags) | ||||
|             } else { | ||||
|                 null | ||||
|             } | ||||
|         } | ||||
|         Tag( | ||||
|             it.title, | ||||
|             it.tag, | ||||
|             subtags | ||||
|         ) | ||||
|         Tag(it.title.normalized(), it.tag, subtags) | ||||
|     } | ||||
|  | ||||
|     File(outputFolder, "LanguageCodes.kt").apply { | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -461,9 +461,9 @@ fun String.asIetfLanguageCode(): IetfLanguageCode { | ||||
|         IetfLanguageCode.Burmese.code -> IetfLanguageCode.Burmese | ||||
|         IetfLanguageCode.Burmese.MM.code -> IetfLanguageCode.Burmese.MM | ||||
|         IetfLanguageCode.Nauru.code -> IetfLanguageCode.Nauru | ||||
|         IetfLanguageCode.BokmålNorwegianNorwegianBokmål.code -> IetfLanguageCode.BokmålNorwegianNorwegianBokmål | ||||
|         IetfLanguageCode.BokmålNorwegianNorwegianBokmål.NO.code -> IetfLanguageCode.BokmålNorwegianNorwegianBokmål.NO | ||||
|         IetfLanguageCode.BokmålNorwegianNorwegianBokmål.SJ.code -> IetfLanguageCode.BokmålNorwegianNorwegianBokmål.SJ | ||||
|         IetfLanguageCode.BokmalNorwegianNorwegianBokmal.code -> IetfLanguageCode.BokmalNorwegianNorwegianBokmal | ||||
|         IetfLanguageCode.BokmalNorwegianNorwegianBokmal.NO.code -> IetfLanguageCode.BokmalNorwegianNorwegianBokmal.NO | ||||
|         IetfLanguageCode.BokmalNorwegianNorwegianBokmal.SJ.code -> IetfLanguageCode.BokmalNorwegianNorwegianBokmal.SJ | ||||
|         IetfLanguageCode.NdebeleNorthNorthNdebele.code -> IetfLanguageCode.NdebeleNorthNorthNdebele | ||||
|         IetfLanguageCode.NdebeleNorthNorthNdebele.ZW.code -> IetfLanguageCode.NdebeleNorthNorthNdebele.ZW | ||||
|         IetfLanguageCode.Nepali.code -> IetfLanguageCode.Nepali | ||||
| @@ -639,8 +639,8 @@ fun String.asIetfLanguageCode(): IetfLanguageCode { | ||||
|         IetfLanguageCode.Venda.code -> IetfLanguageCode.Venda | ||||
|         IetfLanguageCode.Vietnamese.code -> IetfLanguageCode.Vietnamese | ||||
|         IetfLanguageCode.Vietnamese.VN.code -> IetfLanguageCode.Vietnamese.VN | ||||
|         IetfLanguageCode.Volapük.code -> IetfLanguageCode.Volapük | ||||
|         IetfLanguageCode.Volapük.L001.code -> IetfLanguageCode.Volapük.L001 | ||||
|         IetfLanguageCode.Volapuk.code -> IetfLanguageCode.Volapuk | ||||
|         IetfLanguageCode.Volapuk.L001.code -> IetfLanguageCode.Volapuk.L001 | ||||
|         IetfLanguageCode.Walloon.code -> IetfLanguageCode.Walloon | ||||
|         IetfLanguageCode.Wolof.code -> IetfLanguageCode.Wolof | ||||
|         IetfLanguageCode.Wolof.SN.code -> IetfLanguageCode.Wolof.SN | ||||
|   | ||||
							
								
								
									
										52
									
								
								mime_types/mimes_generator/mime_generator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								mime_types/mimes_generator/mime_generator.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| import requests | ||||
| from bs4 import BeautifulSoup | ||||
| import pandas as pd | ||||
| import itertools | ||||
|  | ||||
| def fix_name(category, raw_name): | ||||
|     splitted = raw_name.replace('-', '+').replace('.', '+').replace(',', '+').split('+') | ||||
|     out1 = "" | ||||
|     for s in splitted: | ||||
|         out1 += s.capitalize() | ||||
|  | ||||
|     result = "" | ||||
|     if out1[0].isdigit(): | ||||
|         result += category[0].capitalize() | ||||
|         result += out1 | ||||
|     else: | ||||
|         result += out1 | ||||
|     return result | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     df = pd.read_html(open('table.html', 'r')) | ||||
|     mimes = [] | ||||
|     for row in df[0].iterrows(): | ||||
|         mime = row[1][1] | ||||
|         mime_category = mime.split('/', 1)[0] | ||||
|         mime_name = mime.split('/', 1)[1] | ||||
|         mimes.append({ | ||||
|             'mime_category': mime_category, | ||||
|             'mime_name': mime_name, | ||||
|         }) | ||||
|  | ||||
|     # codegen | ||||
|  | ||||
|     mimes.sort(key=lambda x: x['mime_category']) | ||||
|     grouped = itertools.groupby(mimes, lambda x: x['mime_category']) | ||||
|     code = '' | ||||
|     code2 = 'internal val knownMimeTypes: Set<MimeType> = setOf(\n' | ||||
|     code2 += '    KnownMimeTypes.Any,\n' | ||||
|     for key, group in grouped: | ||||
|         group_name = key.capitalize() | ||||
|         code += '@Serializable(MimeTypeSerializer::class)\nsealed class %s(raw: String) : MimeType, KnownMimeTypes(raw) {\n' % group_name | ||||
|         code += '    @Serializable(MimeTypeSerializer::class)\n    object Any: %s ("%s/*")\n' % (group_name, key) | ||||
|         for mime in group: | ||||
|             name = fix_name(mime['mime_category'], mime['mime_name']) | ||||
|             code += '    @Serializable(MimeTypeSerializer::class)\n    object %s: %s ("%s/%s")\n' % (name, group_name, mime['mime_category'], mime['mime_name']) | ||||
|             code2 += '    KnownMimeTypes.%s.%s,\n' % (group_name, name) | ||||
|         code += '}\n\n' | ||||
|     code2 += ')\n' | ||||
|     with open('out1.txt', 'w') as file: | ||||
|         file.write(code) | ||||
|     with open('out2.txt', 'w') as file: | ||||
|         file.write(code2) | ||||
| @@ -1,3 +1,5 @@ | ||||
| @file:Suppress("SERIALIZER_TYPE_INCOMPATIBLE") | ||||
|  | ||||
| package dev.inmo.micro_utils.mime_types | ||||
|  | ||||
| import kotlinx.serialization.Serializable | ||||
|   | ||||
| @@ -24,3 +24,8 @@ kotlin { | ||||
| } | ||||
|  | ||||
| apply from: "$defaultAndroidSettingsPresetPath" | ||||
|  | ||||
| java { | ||||
|     sourceCompatibility = JavaVersion.VERSION_1_8 | ||||
|     targetCompatibility = JavaVersion.VERSION_1_8 | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,13 @@ project.group = "$group" | ||||
| apply from: "$publishGradlePath" | ||||
|  | ||||
| kotlin { | ||||
|     jvm() | ||||
|     jvm { | ||||
|         compilations.main { | ||||
|             kotlinOptions { | ||||
|                 jvmTarget = "1.8" | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     sourceSets { | ||||
|         commonMain { | ||||
| @@ -26,3 +32,8 @@ kotlin { | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| java { | ||||
|     sourceCompatibility = JavaVersion.VERSION_1_8 | ||||
|     targetCompatibility = JavaVersion.VERSION_1_8 | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,13 @@ project.group = "$group" | ||||
| apply from: "$publishGradlePath" | ||||
|  | ||||
| kotlin { | ||||
|     jvm() | ||||
|     jvm { | ||||
|         compilations.main { | ||||
|             kotlinOptions { | ||||
|                 jvmTarget = "1.8" | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     js (IR) { | ||||
|         browser() | ||||
|         nodejs() | ||||
| @@ -17,7 +23,7 @@ kotlin { | ||||
|         commonMain { | ||||
|             dependencies { | ||||
|                 implementation kotlin('stdlib') | ||||
|                 api "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_core_version" | ||||
|                 implementation libs.kt.serialization | ||||
|             } | ||||
|         } | ||||
|         commonTest { | ||||
| @@ -40,11 +46,16 @@ kotlin { | ||||
|         androidTest { | ||||
|             dependencies { | ||||
|                 implementation kotlin('test-junit') | ||||
|                 implementation "androidx.test.ext:junit:$test_ext_junit_version" | ||||
|                 implementation "androidx.test.espresso:espresso-core:$espresso_core" | ||||
|                 implementation libs.android.test.junit | ||||
|                 implementation libs.android.espresso | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| apply from: "$defaultAndroidSettingsPresetPath" | ||||
|  | ||||
| java { | ||||
|     sourceCompatibility = JavaVersion.VERSION_1_8 | ||||
|     targetCompatibility = JavaVersion.VERSION_1_8 | ||||
| } | ||||
|   | ||||
							
								
								
									
										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 | ||||
| } | ||||
| @@ -5,3 +5,13 @@ plugins { | ||||
| } | ||||
|  | ||||
| apply from: "$mppProjectWithSerializationPresetPath" | ||||
|  | ||||
| kotlin { | ||||
|     sourceSets { | ||||
|         commonMain { | ||||
|             dependencies { | ||||
|                 api project(":micro_utils.common") | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| package dev.inmo.micro_utils.pagination | ||||
|  | ||||
| import dev.inmo.micro_utils.common.intersect | ||||
| import kotlin.math.ceil | ||||
| import kotlin.math.floor | ||||
|  | ||||
| @@ -9,7 +10,7 @@ import kotlin.math.floor | ||||
|  * If you want to request something, you should use [SimplePagination]. If you need to return some result including | ||||
|  * pagination - [PaginationResult] | ||||
|  */ | ||||
| interface Pagination { | ||||
| interface Pagination : ClosedRange<Int> { | ||||
|     /** | ||||
|      * Started with 0. | ||||
|      * Number of page inside of pagination. Offset can be calculated as [page] * [size] | ||||
| @@ -20,6 +21,17 @@ interface Pagination { | ||||
|      * Size of current page. Offset can be calculated as [page] * [size] | ||||
|      */ | ||||
|     val size: Int | ||||
|  | ||||
|     override val start: Int | ||||
|         get() = page * size | ||||
|     override val endInclusive: Int | ||||
|         get() = start + size - 1 | ||||
| } | ||||
|  | ||||
| fun Pagination.intersect( | ||||
|     other: Pagination | ||||
| ): Pagination? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let { | ||||
|     PaginationByIndexes(it.first, it.second) | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -32,7 +44,7 @@ inline val Pagination.isFirstPage | ||||
|  * First number in index of objects. It can be used as offset for databases or other data sources | ||||
|  */ | ||||
| val Pagination.firstIndex: Int | ||||
|     get() = page * size | ||||
|     get() = start | ||||
|  | ||||
| /** | ||||
|  * Last number in index of objects. In fact, one [Pagination] object represent data in next range: | ||||
| @@ -41,7 +53,7 @@ val Pagination.firstIndex: Int | ||||
|  * you will retrieve [Pagination.firstIndex] == 10 and [Pagination.lastIndex] == 19. Here [Pagination.lastIndexExclusive] == 20 | ||||
|  */ | ||||
| val Pagination.lastIndexExclusive: Int | ||||
|     get() = firstIndex + size | ||||
|     get() = endInclusive + 1 | ||||
|  | ||||
| /** | ||||
|  * Last number in index of objects. In fact, one [Pagination] object represent data in next range: | ||||
| @@ -50,7 +62,7 @@ val Pagination.lastIndexExclusive: Int | ||||
|  * you will retrieve [Pagination.firstIndex] == 10 and [Pagination.lastIndex] == 19. | ||||
|  */ | ||||
| val Pagination.lastIndex: Int | ||||
|     get() = lastIndexExclusive - 1 | ||||
|     get() = endInclusive | ||||
|  | ||||
| /** | ||||
|  * Calculates pages count for given [datasetSize] | ||||
|   | ||||
| @@ -16,6 +16,16 @@ suspend fun <T> getAll( | ||||
|     return results.toList() | ||||
| } | ||||
|  | ||||
| suspend fun <T, R> R.getAllBy( | ||||
|     initialPagination: Pagination = FirstPagePagination(), | ||||
|     paginationMapper: R.(PaginationResult<T>) -> Pagination?, | ||||
|     block: suspend R.(Pagination) -> PaginationResult<T> | ||||
| ): List<T> = getAll( | ||||
|     initialPagination, | ||||
|     { paginationMapper(it) }, | ||||
|     { block(it) } | ||||
| ) | ||||
|  | ||||
| suspend fun <T> getAllWithNextPaging( | ||||
|     initialPagination: Pagination = FirstPagePagination(), | ||||
|     block: suspend (Pagination) -> PaginationResult<T> | ||||
| @@ -25,6 +35,14 @@ suspend fun <T> getAllWithNextPaging( | ||||
|     block | ||||
| ) | ||||
|  | ||||
| suspend fun <T, R> R.getAllByWithNextPaging( | ||||
|     initialPagination: Pagination = FirstPagePagination(), | ||||
|     block: suspend R.(Pagination) -> PaginationResult<T> | ||||
| ): List<T> = getAllWithNextPaging( | ||||
|     initialPagination, | ||||
|     { block(it) } | ||||
| ) | ||||
|  | ||||
| suspend fun <T> getAllWithCurrentPaging( | ||||
|     initialPagination: Pagination = FirstPagePagination(), | ||||
|     block: suspend (Pagination) -> PaginationResult<T> | ||||
| @@ -33,3 +51,11 @@ suspend fun <T> getAllWithCurrentPaging( | ||||
|     { it.currentPageIfNotEmpty() }, | ||||
|     block | ||||
| ) | ||||
|  | ||||
| suspend fun <T, R> R.getAllByWithCurrentPaging( | ||||
|     initialPagination: Pagination = FirstPagePagination(), | ||||
|     block: suspend R.(Pagination) -> PaginationResult<T> | ||||
| ): List<T> = getAllWithCurrentPaging( | ||||
|     initialPagination, | ||||
|     { block(it) } | ||||
| ) | ||||
|   | ||||
| @@ -38,3 +38,31 @@ fun <T> Set<T>.paginate(with: Pagination): PaginationResult<T> { | ||||
|         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 | ||||
| } | ||||
|   | ||||
| @@ -26,3 +26,15 @@ fun Pagination.reverse(datasetSize: Long): SimplePagination { | ||||
|  * Shortcut for [reverse] | ||||
|  */ | ||||
| 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 { | ||||
|             dependencies { | ||||
|                 api "org.jetbrains.exposed:exposed-core:$kotlin_exposed_version" | ||||
|                 api libs.jb.exposed | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -15,8 +15,8 @@ kotlin { | ||||
|  | ||||
|         jvmMain { | ||||
|             dependencies { | ||||
|                 api "io.ktor:ktor-server:$ktor_version" | ||||
|                 api "io.ktor:ktor-server-host-common:$ktor_version" | ||||
|                 api libs.ktor.server | ||||
|                 api libs.ktor.server.host.common | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| apply plugin: 'maven-publish' | ||||
| apply plugin: 'signing' | ||||
|  | ||||
| task javadocsJar(type: Jar) { | ||||
|     classifier = 'javadoc' | ||||
| @@ -70,7 +69,18 @@ publishing { | ||||
|     } | ||||
| } | ||||
|      | ||||
| signing { | ||||
| if (project.hasProperty("signing.gnupg.keyName")) { | ||||
|     apply plugin: 'signing' | ||||
|      | ||||
|     signing { | ||||
|         useGpgCmd() | ||||
|      | ||||
|         sign publishing.publications | ||||
|     } | ||||
|      | ||||
|     task signAll { | ||||
|         tasks.withType(Sign).forEach { | ||||
|             dependsOn(it) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| {"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","includeGpgSigning":true,"developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}]}} | ||||
| {"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}}} | ||||
| @@ -10,10 +10,10 @@ kotlin { | ||||
|     sourceSets { | ||||
|         commonMain { | ||||
|             dependencies { | ||||
|                 api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" | ||||
|                 api libs.kt.coroutines | ||||
|                 api internalProject("micro_utils.pagination.common") | ||||
|  | ||||
|                 api "com.benasher44:uuid:$uuidVersion" | ||||
|                 api libs.uuid | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -24,7 +24,7 @@ kotlin { | ||||
|         } | ||||
|         androidMain { | ||||
|             dependencies { | ||||
|                 api "androidx.core:core-ktx:$core_ktx_version" | ||||
|                 api libs.android.coreKtx | ||||
|                 api internalProject("micro_utils.common") | ||||
|                 api internalProject("micro_utils.coroutines") | ||||
|             } | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user