mirror of
				https://github.com/InsanusMokrassar/MicroUtils.git
				synced 2025-10-26 17:50:41 +00:00 
			
		
		
		
	Compare commits
	
		
			276 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 9d7f99f286 | |||
| 6ef403853c | |||
| 6ae7ccb9a1 | |||
| dafc50c463 | |||
| e89e2c931d | |||
| 43a67b99e4 | |||
| 46c48f4f31 | |||
| bf0fe85aa6 | |||
| 42c5bd3a7f | |||
| d170e86c8a | |||
| e3078169b1 | |||
| a33ad123f6 | |||
| 7e14fa2f5c | |||
| ba698b41e1 | |||
| e76215987e | |||
| d1a247af8c | |||
| 2b7e9534f3 | |||
| 38521558a1 | |||
| 100f3d214b | |||
| 1309867611 | |||
| 611f64f2e1 | |||
| f118ebce6e | |||
| 59fc90e556 | |||
| fb9e4d57fb | |||
| 960c38b696 | |||
| 39895e58a6 | |||
| b420d85be5 | |||
| 19ea2f340a | |||
| 11b0d059bf | |||
| c8a25ce544 | |||
| 509583ea2e | |||
| 1c86f3f4bf | |||
| 6d999be590 | |||
| e715772dbf | |||
| 63eb7b7ea8 | |||
| b07683b815 | |||
| 96e97d1691 | |||
| 261d8827e3 | |||
| c3156f2e41 | |||
| 8c08801460 | |||
| aaf1299da7 | |||
| a411355b4f | |||
| eba41066b4 | |||
| f295dff8a2 | |||
| a16815143c | |||
| 6ff3f6ae42 | |||
| 84071881af | 
							
								
								
									
										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/checkout@v2 | ||||||
|       - uses: actions/setup-java@v1 |       - uses: actions/setup-java@v1 | ||||||
|         with: |         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 |       - name: Build | ||||||
|         run: ./gradlew dokkaHtml |         run: ./gradlew dokkaHtml | ||||||
|       - name: Publish KDocs |       - name: Publish KDocs | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								.github/workflows/packages_push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/packages_push.yml
									
									
									
									
										vendored
									
									
								
							| @@ -8,7 +8,10 @@ jobs: | |||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v2 | ||||||
|       - uses: actions/setup-java@v1 |       - uses: actions/setup-java@v1 | ||||||
|         with: |         with: | ||||||
|           java-version: 1.8 |           java-version: 11 | ||||||
|  |       - name: Fix android 32.0.0 dx | ||||||
|  |         continue-on-error: true | ||||||
|  |         run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib  && mv d8.jar dx.jar | ||||||
|       - name: Rewrite version |       - name: Rewrite version | ||||||
|         run: | |         run: | | ||||||
|           branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" |           branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" | ||||||
| @@ -18,7 +21,8 @@ jobs: | |||||||
|       - name: Build |       - name: Build | ||||||
|         run: ./gradlew build |         run: ./gradlew build | ||||||
|       - name: Publish |       - 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: |         env: | ||||||
|           GITHUBPACKAGES_USER: ${{ github.actor }} |           GITHUBPACKAGES_USER: ${{ github.actor }} | ||||||
|           GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} |           GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -11,5 +11,6 @@ out/ | |||||||
|  |  | ||||||
| secret.gradle | secret.gradle | ||||||
| local.properties | local.properties | ||||||
|  | kotlin-js-store | ||||||
|  |  | ||||||
| publishing.sh | publishing.sh | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								.space.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.space.kts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | job("Build and run tests") { | ||||||
|  |     container(displayName = "Run gradle build", image = "openjdk:11") { | ||||||
|  |         kotlinScript { api -> | ||||||
|  |             // here can be your complex logic | ||||||
|  |             api.gradlew("build") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								.travis.yml
									
									
									
									
									
								
							| @@ -1,27 +0,0 @@ | |||||||
| language: android |  | ||||||
| install: true |  | ||||||
|  |  | ||||||
| os: linux |  | ||||||
| dist: trusty |  | ||||||
| jdk: oraclejdk8 |  | ||||||
|  |  | ||||||
| android: |  | ||||||
|   components: |  | ||||||
|     - tools |  | ||||||
|     - platform-tools |  | ||||||
|     - build-tools-30.0.2 |  | ||||||
|     - android-30 |  | ||||||
|     - add-on |  | ||||||
|     - extra |  | ||||||
|  |  | ||||||
| before_script: |  | ||||||
|   - yes | /usr/local/android-sdk/tools/bin/sdkmanager "build-tools;30.0.2" |  | ||||||
|   - yes | /usr/local/android-sdk/tools/bin/sdkmanager "platforms;android-30" |  | ||||||
|  |  | ||||||
| jobs: |  | ||||||
|   include: |  | ||||||
|     - stage: build |  | ||||||
|       script: ./gradlew build -s -x jvmTest -x jsIrTest -x jsIrBrowserTest -x jsIrNodeTest -x jsLegacyTest -x jsLegacyBrowserTest -x jsLegacyNodeTest |  | ||||||
| #    Tests are temporarily disabled on public travis due to the problems of launching |  | ||||||
| #    - state: test |  | ||||||
| #      script: ./gradlew allTests |  | ||||||
							
								
								
									
										468
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										468
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,5 +1,473 @@ | |||||||
| # Changelog | # Changelog | ||||||
|  |  | ||||||
|  | ## 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 | ||||||
|  |  | ||||||
|  | ## 0.5.17 | ||||||
|  |  | ||||||
|  | **SINCE THIS UPDATE JS PARTS WILL BE COMPILED WITH IR COMPILER ONLY** | ||||||
|  |  | ||||||
|  | * `Versions` | ||||||
|  |     * `Kotlin`: `1.5.20` -> `1.5.21` | ||||||
|  |     * `Ktor`: `1.6.1` -> `1.6.2` | ||||||
|  |     * `Klock`: `2.2.0` -> `2.3.1` | ||||||
|  |     * `CryptoJS`: `4.0.0` -> `4.1.1` | ||||||
|  |  | ||||||
|  | ## 0.5.16 | ||||||
|  |  | ||||||
|  | * `Versions` | ||||||
|  |   * `Coroutines`: `1.5.0` -> `1.5.1` | ||||||
|  |   * `Serialization`: `1.2.1` -> `1.2.2` | ||||||
|  |   * `Ktor`: `1.6.0` -> `1.6.1` | ||||||
|  |   * `Klock`: `2.1.2` -> `2.2.0` | ||||||
|  |   * `Core KTX`: `1.5.0` -> `1.6.0` | ||||||
|  |  | ||||||
|  | ## 0.5.15 HOTFIX FOR 0.5.14 | ||||||
|  |  | ||||||
|  | * `Coroutines` | ||||||
|  |     * Fixes in `subscribeAsync` | ||||||
|  |  | ||||||
|  | ## 0.5.14 NOT RECOMMENDED | ||||||
|  |  | ||||||
|  | * `Versions` | ||||||
|  |     * `Kotlin`: `1.5.10` -> `1.5.20` | ||||||
|  | * `Coroutines` | ||||||
|  |     * `subscribeSafelyWithoutExceptions` got new parameter `onException` by analogue with `safelyWithoutExceptions` | ||||||
|  |     * New extensions `Flow#subscribeAsync` and subsequent analogs of `subscribe` with opportunity to set up custom marker | ||||||
|  |  | ||||||
|  | ## 0.5.13 | ||||||
|  |  | ||||||
|  | * `Common`: | ||||||
|  |     * Add functionality for multiplatform working with files: | ||||||
|  |         * Main class for files `MPPFile` | ||||||
|  |         * Inline class for filenames work encapsulation `FileName` | ||||||
|  | * `FSM` | ||||||
|  |     * Module inited and in preview state | ||||||
|  |  | ||||||
|  | ## 0.5.12 | ||||||
|  |  | ||||||
|  | * `Common`: | ||||||
|  |     * `Android` | ||||||
|  |         * Extension `View#changeVisibility` has been fixed | ||||||
|  | * `Android` | ||||||
|  |     * `RecyclerView` | ||||||
|  |         * Default adapter got `dataCountFlow` property | ||||||
|  |         * New subtype of adapter based on `StateFlow`: `StateFlowBasedRecyclerViewAdapter` | ||||||
|  |  | ||||||
|  | ## 0.5.11 | ||||||
|  |  | ||||||
|  | * `Repos`: | ||||||
|  |     * `Common`: | ||||||
|  |         * Fixes in `WriteOneToManyRepo#add` | ||||||
|  |     * `Exposed`: | ||||||
|  |         * Fixes in `ExposedOneToManyKeyValueRepo#add` | ||||||
|  |  | ||||||
|  | ## 0.5.10 | ||||||
|  |  | ||||||
|  | * `Versions` | ||||||
|  |     * `Core KTX`: `1.3.2` -> `1.5.0` | ||||||
|  |     * `AndroidX Recycler`: `1.2.0` -> `1.2.1` | ||||||
|  |     * `AppCompat`: `1.2.0` -> `1.3.0` | ||||||
|  | * `Android` | ||||||
|  |     * `RecyclerView`: | ||||||
|  |         * `data` of `RecyclerViewAdapter` became an abstract field | ||||||
|  |             * New function `RecyclerViewAdapter` | ||||||
|  |     * `Common`: | ||||||
|  |         * New extension `View#changeVisibility` | ||||||
|  |     * `Repos`: | ||||||
|  |         * `Common`: | ||||||
|  |             * `WriteOneToManyRepo` got new function `clearWithValue` | ||||||
|  |             * `Android`: | ||||||
|  |                 * New extension `SQLiteDatabase#selectDistinct` | ||||||
|  |                 * Fixes in `OneToManyAndroidRepo` | ||||||
|  | * `Ktor` | ||||||
|  |     * `Server` | ||||||
|  |         * All elements in configurators became a `fun interface` | ||||||
|  | * `Pagination` | ||||||
|  |     * New function `doForAllWithCurrentPaging` | ||||||
|  |  | ||||||
| ## 0.5.9 | ## 0.5.9 | ||||||
|  |  | ||||||
| * `Repos` | * `Repos` | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ kotlin { | |||||||
|     sourceSets { |     sourceSets { | ||||||
|         androidMain { |         androidMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "androidx.appcompat:appcompat-resources:$appcompat_version" |                 api libs.android.appCompat.resources | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -35,9 +35,9 @@ class ActionViewHolder( | |||||||
| } | } | ||||||
|  |  | ||||||
| class ActionsRecyclerViewAdapter( | class ActionsRecyclerViewAdapter( | ||||||
|     data: List<AlertAction>, |     override val data: List<AlertAction>, | ||||||
|     private val dialogInterfaceGetter: () -> DialogInterface |     private val dialogInterfaceGetter: () -> DialogInterface | ||||||
| ) : RecyclerViewAdapter<AlertAction>(data) { | ) : RecyclerViewAdapter<AlertAction>() { | ||||||
|     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<AlertAction> = ActionViewHolder( |     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<AlertAction> = ActionViewHolder( | ||||||
|         parent, dialogInterfaceGetter |         parent, dialogInterfaceGetter | ||||||
|     ) |     ) | ||||||
|   | |||||||
| @@ -10,12 +10,13 @@ kotlin { | |||||||
|     sourceSets { |     sourceSets { | ||||||
|         commonMain { |         commonMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" |                 api libs.kt.coroutines | ||||||
|  |                 api project(":micro_utils.common") | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         androidMain { |         androidMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "androidx.recyclerview:recyclerview:$androidx_recycler_version" |                 api libs.android.recyclerView | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,12 +1,21 @@ | |||||||
| package dev.inmo.micro_utils.android.recyclerview | package dev.inmo.micro_utils.android.recyclerview | ||||||
|  |  | ||||||
| import android.view.View | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
| import androidx.recyclerview.widget.RecyclerView | import androidx.recyclerview.widget.RecyclerView | ||||||
|  | import kotlinx.coroutines.flow.* | ||||||
|  |  | ||||||
|  |  | ||||||
| abstract class RecyclerViewAdapter<T>( | abstract class RecyclerViewAdapter<T>: RecyclerView.Adapter<AbstractViewHolder<T>>() { | ||||||
|     val data: List<T> |     protected abstract val data: List<T> | ||||||
| ): RecyclerView.Adapter<AbstractViewHolder<T>>() { |  | ||||||
|  |     private val _dataCountState by lazy { | ||||||
|  |         MutableStateFlow<Int>(data.size) | ||||||
|  |     } | ||||||
|  |     val dataCountState: StateFlow<Int> by lazy { | ||||||
|  |         _dataCountState.asStateFlow() | ||||||
|  |     } | ||||||
|  |  | ||||||
|     var emptyView: View? = null |     var emptyView: View? = null | ||||||
|         set(value) { |         set(value) { | ||||||
|             field = value |             field = value | ||||||
| @@ -18,31 +27,37 @@ abstract class RecyclerViewAdapter<T>( | |||||||
|             object : RecyclerView.AdapterDataObserver() { |             object : RecyclerView.AdapterDataObserver() { | ||||||
|                 override fun onItemRangeChanged(positionStart: Int, itemCount: Int) { |                 override fun onItemRangeChanged(positionStart: Int, itemCount: Int) { | ||||||
|                     super.onItemRangeChanged(positionStart, itemCount) |                     super.onItemRangeChanged(positionStart, itemCount) | ||||||
|  |                     _dataCountState.value = data.size | ||||||
|                     checkEmpty() |                     checkEmpty() | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) { |                 override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) { | ||||||
|                     super.onItemRangeChanged(positionStart, itemCount, payload) |                     super.onItemRangeChanged(positionStart, itemCount, payload) | ||||||
|  |                     _dataCountState.value = data.size | ||||||
|                     checkEmpty() |                     checkEmpty() | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 override fun onChanged() { |                 override fun onChanged() { | ||||||
|                     super.onChanged() |                     super.onChanged() | ||||||
|  |                     _dataCountState.value = data.size | ||||||
|                     checkEmpty() |                     checkEmpty() | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { |                 override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { | ||||||
|                     super.onItemRangeRemoved(positionStart, itemCount) |                     super.onItemRangeRemoved(positionStart, itemCount) | ||||||
|  |                     _dataCountState.value = data.size | ||||||
|                     checkEmpty() |                     checkEmpty() | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { |                 override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { | ||||||
|                     super.onItemRangeMoved(fromPosition, toPosition, itemCount) |                     super.onItemRangeMoved(fromPosition, toPosition, itemCount) | ||||||
|  |                     _dataCountState.value = data.size | ||||||
|                     checkEmpty() |                     checkEmpty() | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { |                 override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { | ||||||
|                     super.onItemRangeInserted(positionStart, itemCount) |                     super.onItemRangeInserted(positionStart, itemCount) | ||||||
|  |                     _dataCountState.value = data.size | ||||||
|                     checkEmpty() |                     checkEmpty() | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @@ -58,7 +73,7 @@ abstract class RecyclerViewAdapter<T>( | |||||||
|  |  | ||||||
|     private fun checkEmpty() { |     private fun checkEmpty() { | ||||||
|         emptyView ?. let { |         emptyView ?. let { | ||||||
|             if (data.isEmpty()) { |             if (dataCountState.value == 0) { | ||||||
|                 it.visibility = View.VISIBLE |                 it.visibility = View.VISIBLE | ||||||
|             } else { |             } else { | ||||||
|                 it.visibility = View.GONE |                 it.visibility = View.GONE | ||||||
| @@ -66,3 +81,11 @@ abstract class RecyclerViewAdapter<T>( | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | fun <T> RecyclerViewAdapter( | ||||||
|  |     data: List<T>, | ||||||
|  |     onCreateViewHolder: (parent: ViewGroup, viewType: Int) -> AbstractViewHolder<T> | ||||||
|  | ) = object : RecyclerViewAdapter<T>() { | ||||||
|  |     override val data: List<T> = data | ||||||
|  |     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<T> = onCreateViewHolder(parent, viewType) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -0,0 +1,50 @@ | |||||||
|  | package dev.inmo.micro_utils.android.recyclerview | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.Diff | ||||||
|  | import dev.inmo.micro_utils.common.PreviewFeature | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import kotlinx.coroutines.flow.* | ||||||
|  |  | ||||||
|  | @PreviewFeature("This feature in preview state and may contains different bugs. " + | ||||||
|  |                 "Besides, this feature can be changed in future in non-compatible way") | ||||||
|  | abstract class StateFlowBasedRecyclerViewAdapter<T>( | ||||||
|  |     listeningScope: CoroutineScope, | ||||||
|  |     dataState: StateFlow<List<T>> | ||||||
|  | ) : RecyclerViewAdapter<T>() { | ||||||
|  |     override var data: List<T> = emptyList() | ||||||
|  |  | ||||||
|  |     init { | ||||||
|  |         dataState.onEach { | ||||||
|  |             try { | ||||||
|  |                 val diffForRemoves = Diff(data, it) | ||||||
|  |                 val removedIndexes = diffForRemoves.removed.map { it.index } | ||||||
|  |                 val leftRemove = removedIndexes.toMutableList() | ||||||
|  |                 data = data.filterIndexed { i, _ -> | ||||||
|  |                     if (i in leftRemove) { | ||||||
|  |                         leftRemove.remove(i) | ||||||
|  |                         true | ||||||
|  |                     } else { | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 withContext(Dispatchers.Main) { | ||||||
|  |                     removedIndexes.sortedDescending().forEach { | ||||||
|  |                         notifyItemRemoved(it) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 val diffAddsAndReplaces = Diff(data, it) | ||||||
|  |                 data = it | ||||||
|  |                 withContext(Dispatchers.Main) { | ||||||
|  |                     diffAddsAndReplaces.replaced.forEach { (from, to) -> | ||||||
|  |                         notifyItemMoved(from.index, to.index) | ||||||
|  |                     } | ||||||
|  |                     diffAddsAndReplaces.added.forEach { | ||||||
|  |                         notifyItemInserted(it.index) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } catch (e: Throwable) { | ||||||
|  |                 // currently do nothing | ||||||
|  |             } | ||||||
|  |         }.launchIn(listeningScope) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								build.gradle
									
									
									
									
									
								
							| @@ -1,6 +1,5 @@ | |||||||
| buildscript { | buildscript { | ||||||
|     repositories { |     repositories { | ||||||
|         jcenter() |  | ||||||
|         google() |         google() | ||||||
|         mavenCentral() |         mavenCentral() | ||||||
|         mavenLocal() |         mavenLocal() | ||||||
| @@ -8,22 +7,20 @@ buildscript { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     dependencies { |     dependencies { | ||||||
|         classpath 'com.android.tools.build:gradle:4.1.3' |         classpath libs.buildscript.kt.gradle | ||||||
|         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" |         classpath libs.buildscript.kt.serialization | ||||||
|         classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" |         classpath libs.buildscript.jb.dokka | ||||||
|         classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version" |         classpath libs.buildscript.gh.release | ||||||
|         classpath "com.github.breadmoirai:github-release:$github_release_plugin_version" |         classpath libs.buildscript.android.gradle | ||||||
|         classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" |         classpath libs.buildscript.android.dexcount | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| allprojects { | allprojects { | ||||||
|     repositories { |     repositories { | ||||||
|         mavenLocal() |         mavenLocal() | ||||||
|         jcenter() |  | ||||||
|         mavenCentral() |         mavenCentral() | ||||||
|         google() |         google() | ||||||
|         maven { url "https://kotlin.bintray.com/kotlinx" } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // temporal crutch until legacy tests will be stabled or legacy target will be removed |     // temporal crutch until legacy tests will be stabled or legacy target will be removed | ||||||
|   | |||||||
| @@ -5,3 +5,18 @@ plugins { | |||||||
| } | } | ||||||
|  |  | ||||||
| apply from: "$mppProjectWithSerializationPresetPath" | apply from: "$mppProjectWithSerializationPresetPath" | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     sourceSets { | ||||||
|  |         jvmMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api project(":micro_utils.coroutines") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         androidMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api project(":micro_utils.coroutines") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										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,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 | ||||||
|  | ) | ||||||
|  |  | ||||||
							
								
								
									
										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,11 +12,9 @@ package dev.inmo.micro_utils.common | |||||||
|     AnnotationTarget.PROPERTY_GETTER, |     AnnotationTarget.PROPERTY_GETTER, | ||||||
|     AnnotationTarget.PROPERTY_SETTER, |     AnnotationTarget.PROPERTY_SETTER, | ||||||
|     AnnotationTarget.FUNCTION, |     AnnotationTarget.FUNCTION, | ||||||
|     AnnotationTarget.TYPE, |     AnnotationTarget.TYPEALIAS | ||||||
|     AnnotationTarget.TYPEALIAS, |  | ||||||
|     AnnotationTarget.TYPE_PARAMETER |  | ||||||
| ) | ) | ||||||
| annotation class PreviewFeature | annotation class PreviewFeature(val message: String = "It is possible, that behaviour of this thing will be changed or removed in future releases") | ||||||
|  |  | ||||||
| @RequiresOptIn( | @RequiresOptIn( | ||||||
|     "This thing is marked as warned. See message of warn to get more info", |     "This thing is marked as warned. See message of warn to get more info", | ||||||
| @@ -30,8 +28,6 @@ annotation class PreviewFeature | |||||||
|     AnnotationTarget.PROPERTY_GETTER, |     AnnotationTarget.PROPERTY_GETTER, | ||||||
|     AnnotationTarget.PROPERTY_SETTER, |     AnnotationTarget.PROPERTY_SETTER, | ||||||
|     AnnotationTarget.FUNCTION, |     AnnotationTarget.FUNCTION, | ||||||
|     AnnotationTarget.TYPE, |     AnnotationTarget.TYPEALIAS | ||||||
|     AnnotationTarget.TYPEALIAS, |  | ||||||
|     AnnotationTarget.TYPE_PARAMETER |  | ||||||
| ) | ) | ||||||
| annotation class Warning(val message: String) | 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( | private inline fun <T> performChanges( | ||||||
|     potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>, |     potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>, | ||||||
|     additionalsInOld: MutableList<T>, |     additionsInOld: MutableList<T>, | ||||||
|     additionalsInNew: MutableList<T>, |     additionsInNew: MutableList<T>, | ||||||
|     changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>, |     changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>, | ||||||
|     removedList: MutableList<IndexedValue<T>>, |     removedList: MutableList<IndexedValue<T>>, | ||||||
|     addedList: MutableList<IndexedValue<T>>, |     addedList: MutableList<IndexedValue<T>>, | ||||||
| @@ -52,20 +52,20 @@ private inline fun <T> performChanges( | |||||||
|                     newPotentials.first().second ?.let { addedList.add(it) } |                     newPotentials.first().second ?.let { addedList.add(it) } | ||||||
|                     newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) -> |                     newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) -> | ||||||
|                         addedList.add(newOne!!) |                         addedList.add(newOne!!) | ||||||
|                         oldOne ?.let { additionalsInOld.add(oldOne.value) } |                         oldOne ?.let { additionsInOld.add(oldOne.value) } | ||||||
|                     } |                     } | ||||||
|                     if (newPotentials.size > 1) { |                     if (newPotentials.size > 1) { | ||||||
|                         newPotentials.last().first ?.value ?.let { additionalsInOld.add(it) } |                         newPotentials.last().first ?.value ?.let { additionsInOld.add(it) } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 newOneEqualToOldObject -> { |                 newOneEqualToOldObject -> { | ||||||
|                     newPotentials.first().first ?.let { removedList.add(it) } |                     newPotentials.first().first ?.let { removedList.add(it) } | ||||||
|                     newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) -> |                     newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) -> | ||||||
|                         removedList.add(oldOne!!) |                         removedList.add(oldOne!!) | ||||||
|                         newOne ?.let { additionalsInNew.add(newOne.value) } |                         newOne ?.let { additionsInNew.add(newOne.value) } | ||||||
|                     } |                     } | ||||||
|                     if (newPotentials.size > 1) { |                     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()) |     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> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new) | ||||||
| inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true) | 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( | inline fun <T> Iterable<T>.calculateStrictDiff( | ||||||
|     other: Iterable<T> |     other: Iterable<T> | ||||||
| ) = calculateDiff(other, strictComparison = true) | ) = calculateDiff(other, strictComparison = true) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this] | ||||||
|  |  * mutable list | ||||||
|  |  */ | ||||||
|  | fun <T> MutableList<T>.applyDiff( | ||||||
|  |     source: Iterable<T>, | ||||||
|  |     strictComparison: Boolean = false | ||||||
|  | ) = calculateDiff(source, strictComparison).let { | ||||||
|  |     for (i in it.removed.indices.sortedDescending()) { | ||||||
|  |         removeAt(it.removed[i].index) | ||||||
|  |     } | ||||||
|  |     it.added.forEach { (i, t) -> | ||||||
|  |         add(i, t) | ||||||
|  |     } | ||||||
|  |     it.replaced.forEach { (_, new) -> | ||||||
|  |         set(new.index, new.value) | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -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") | ||||||
|  | } | ||||||
| @@ -7,9 +7,17 @@ import kotlinx.serialization.encoding.Decoder | |||||||
| import kotlinx.serialization.encoding.Encoder | import kotlinx.serialization.encoding.Encoder | ||||||
|  |  | ||||||
| typealias ByteArrayAllocator = () -> ByteArray | typealias ByteArrayAllocator = () -> ByteArray | ||||||
|  | typealias SuspendByteArrayAllocator = suspend () -> ByteArray | ||||||
|  |  | ||||||
| val ByteArray.asAllocator: ByteArrayAllocator | val ByteArray.asAllocator: ByteArrayAllocator | ||||||
|     get() = { this } |     get() = { this } | ||||||
|  | val ByteArray.asSuspendAllocator: SuspendByteArrayAllocator | ||||||
|  |     get() = { this } | ||||||
|  | val ByteArrayAllocator.asSuspendAllocator: SuspendByteArrayAllocator | ||||||
|  |     get() = { this() } | ||||||
|  | suspend fun SuspendByteArrayAllocator.asAllocator(): ByteArrayAllocator { | ||||||
|  |     return invoke().asAllocator | ||||||
|  | } | ||||||
|  |  | ||||||
| object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> { | object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> { | ||||||
|     private val realSerializer = ByteArraySerializer() |     private val realSerializer = ByteArraySerializer() | ||||||
| @@ -17,7 +25,7 @@ object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> { | |||||||
|  |  | ||||||
|     override fun deserialize(decoder: Decoder): ByteArrayAllocator { |     override fun deserialize(decoder: Decoder): ByteArrayAllocator { | ||||||
|         val bytes = realSerializer.deserialize(decoder) |         val bytes = realSerializer.deserialize(decoder) | ||||||
|         return { bytes } |         return bytes.asAllocator | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override fun serialize(encoder: Encoder, value: ByteArrayAllocator) { |     override fun serialize(encoder: Encoder, value: ByteArrayAllocator) { | ||||||
|   | |||||||
| @@ -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() | ||||||
| @@ -0,0 +1,34 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  | import kotlin.jvm.JvmInline | ||||||
|  |  | ||||||
|  | @Serializable | ||||||
|  | @JvmInline | ||||||
|  | value class FileName(val string: String) { | ||||||
|  |     val name: String | ||||||
|  |         get() = withoutSlashAtTheEnd.takeLastWhile { it != '/' } | ||||||
|  |     val extension: String | ||||||
|  |         get() = name.takeLastWhile { it != '.' } | ||||||
|  |     val nameWithoutExtension: String | ||||||
|  |         get() { | ||||||
|  |             val filename = name | ||||||
|  |             return filename.indexOfLast { it == '.' }.takeIf { it > -1 } ?.let { | ||||||
|  |                 filename.substring(0, it) | ||||||
|  |             } ?: filename | ||||||
|  |         } | ||||||
|  |     val withoutSlashAtTheEnd: String | ||||||
|  |         get() = string.dropLastWhile { it == '/' } | ||||||
|  |     override fun toString(): String = string | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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() |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { |         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||||
|             for ((i, v) in withIndex) { |             for ((i, _) in withIndex) { | ||||||
|                 if (i + count > oldList.lastIndex) { |                 if (i + count > oldList.lastIndex) { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
| @@ -54,7 +54,7 @@ class DiffUtilsTests { | |||||||
|         val oldList = (0 until 10).map { it.toString() } |         val oldList = (0 until 10).map { it.toString() } | ||||||
|         val withIndex = oldList.withIndex() |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|         for (step in 0 until oldList.size) { |         for (step in oldList.indices) { | ||||||
|             for ((i, v) in withIndex) { |             for ((i, v) in withIndex) { | ||||||
|                 val mutable = oldList.toMutableList() |                 val mutable = oldList.toMutableList() | ||||||
|                 val changes = ( |                 val changes = ( | ||||||
| @@ -73,4 +73,78 @@ class DiffUtilsTests { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun testThatSimpleRemoveApplyWorks() { | ||||||
|  |         val oldList = (0 until 10).toList() | ||||||
|  |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|  |         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||||
|  |             for ((i, _) in withIndex) { | ||||||
|  |                 if (i + count > oldList.lastIndex) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 val removedSublist = oldList.subList(i, i + count) | ||||||
|  |                 val mutableOldList = oldList.toMutableList() | ||||||
|  |                 val targetList = oldList - removedSublist | ||||||
|  |  | ||||||
|  |                 mutableOldList.applyDiff(targetList) | ||||||
|  |  | ||||||
|  |                 assertEquals( | ||||||
|  |                     targetList, | ||||||
|  |                     mutableOldList | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun testThatSimpleAddApplyWorks() { | ||||||
|  |         val oldList = (0 until 10).map { it.toString() } | ||||||
|  |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|  |         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||||
|  |             for ((i, v) in withIndex) { | ||||||
|  |                 if (i + count > oldList.lastIndex) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 val addedSublist = oldList.subList(i, i + count).map { "added$it" } | ||||||
|  |                 val mutable = oldList.toMutableList() | ||||||
|  |                 mutable.addAll(i, addedSublist) | ||||||
|  |                 val mutableOldList = oldList.toMutableList() | ||||||
|  |  | ||||||
|  |                 mutableOldList.applyDiff(mutable) | ||||||
|  |  | ||||||
|  |                 assertEquals( | ||||||
|  |                     mutable, | ||||||
|  |                     mutableOldList | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun testThatSimpleChangesApplyWorks() { | ||||||
|  |         val oldList = (0 until 10).map { it.toString() } | ||||||
|  |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|  |         for (step in oldList.indices) { | ||||||
|  |             for ((i, v) in withIndex) { | ||||||
|  |                 val mutable = oldList.toMutableList() | ||||||
|  |                 val changes = ( | ||||||
|  |                     if (step == 0) i until oldList.size else (i until oldList.size step step) | ||||||
|  |                 ).map { index -> | ||||||
|  |                     IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also { | ||||||
|  |                         mutable[index] = it.value | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 val mutableOldList = oldList.toMutableList() | ||||||
|  |                 mutableOldList.applyDiff(mutable) | ||||||
|  |                 assertEquals( | ||||||
|  |                     mutable, | ||||||
|  |                     mutableOldList | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,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) | ||||||
|  | } | ||||||
| @@ -0,0 +1,54 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import org.khronos.webgl.ArrayBuffer | ||||||
|  | import org.w3c.dom.ErrorEvent | ||||||
|  | import org.w3c.files.* | ||||||
|  | import kotlin.js.Promise | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual typealias MPPFile = File | ||||||
|  |  | ||||||
|  | fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure -> | ||||||
|  |     val reader = FileReader() | ||||||
|  |     reader.onload = { | ||||||
|  |         success((reader.result as ArrayBuffer).toByteArray()) | ||||||
|  |         Unit | ||||||
|  |     } | ||||||
|  |     reader.onerror = { | ||||||
|  |         failure(Exception((it as ErrorEvent).message)) | ||||||
|  |         Unit | ||||||
|  |     } | ||||||
|  |     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,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,8 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlin.coroutines.* | ||||||
|  | import kotlin.js.Promise | ||||||
|  |  | ||||||
|  | suspend fun <T> Promise<T>.await(): T = suspendCoroutine { cont -> | ||||||
|  |     then({ cont.resume(it) }, { cont.resumeWithException(it) }) | ||||||
|  | } | ||||||
| @@ -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() | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -0,0 +1,37 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | 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 { | ||||||
|  |             doOutsideOfCoroutine { | ||||||
|  |                 readBytes() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
| @@ -33,3 +33,15 @@ fun View.toggleVisibility(goneOnHide: Boolean = true) { | |||||||
|         show() |         show() | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | fun View.changeVisibility(show: Boolean = !isShown, goneOnHide: Boolean = true) { | ||||||
|  |     if (show) { | ||||||
|  |         show() | ||||||
|  |     } else { | ||||||
|  |         if (goneOnHide) { | ||||||
|  |             gone() | ||||||
|  |         } else { | ||||||
|  |             hide() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -10,12 +10,17 @@ kotlin { | |||||||
|     sourceSets { |     sourceSets { | ||||||
|         commonMain { |         commonMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" |                 api libs.kt.coroutines | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         jsMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api project(":micro_utils.common") | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         androidMain { |         androidMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" |                 api libs.kt.coroutines.android | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								coroutines/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								coroutines/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | 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") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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,16 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | 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() | ||||||
|  |     ) | ||||||
|  | } | ||||||
							
								
								
									
										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) | ||||||
|  | } | ||||||
| @@ -4,6 +4,8 @@ package dev.inmo.micro_utils.coroutines | |||||||
|  |  | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
| import kotlinx.coroutines.flow.* | import kotlinx.coroutines.flow.* | ||||||
|  | import kotlinx.coroutines.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Shortcut for chain if [Flow.onEach] and [Flow.launchIn] |  * Shortcut for chain if [Flow.onEach] and [Flow.launchIn] | ||||||
| @@ -29,9 +31,10 @@ inline fun <T> Flow<T>.subscribeSafely( | |||||||
|  */ |  */ | ||||||
| inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions( | inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions( | ||||||
|     scope: CoroutineScope, |     scope: CoroutineScope, | ||||||
|  |     noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
|     noinline block: suspend (T) -> Unit |     noinline block: suspend (T) -> Unit | ||||||
| ) = subscribe(scope) { | ) = subscribe(scope) { | ||||||
|     safelyWithoutExceptions { |     safelyWithoutExceptions(onException) { | ||||||
|         block(it) |         block(it) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,118 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import kotlinx.coroutines.channels.* | ||||||
|  | import kotlinx.coroutines.flow.* | ||||||
|  | import kotlinx.coroutines.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  |  | ||||||
|  | private class SubscribeAsyncReceiver<T>( | ||||||
|  |     val scope: CoroutineScope, | ||||||
|  |     output: suspend SubscribeAsyncReceiver<T>.(T) -> Unit | ||||||
|  | ) { | ||||||
|  |     private val dataChannel: Channel<T> = Channel(Channel.UNLIMITED) | ||||||
|  |     val channel: SendChannel<T> | ||||||
|  |         get() = dataChannel | ||||||
|  |  | ||||||
|  |     init { | ||||||
|  |         scope.launchSafelyWithoutExceptions { | ||||||
|  |             for (data in dataChannel) { | ||||||
|  |                 output(data) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun isEmpty(): Boolean = dataChannel.isEmpty | ||||||
|  | } | ||||||
|  |  | ||||||
|  | private sealed interface AsyncSubscriptionCommand<T, M> { | ||||||
|  |     suspend operator fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) | ||||||
|  | } | ||||||
|  | private data class AsyncSubscriptionCommandData<T, M>( | ||||||
|  |     val data: T, | ||||||
|  |     val scope: CoroutineScope, | ||||||
|  |     val markerFactory: suspend (T) -> M, | ||||||
|  |     val block: suspend (T) -> Unit, | ||||||
|  |     val onEmpty: suspend (M) -> Unit | ||||||
|  | ) : AsyncSubscriptionCommand<T, M> { | ||||||
|  |     override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) { | ||||||
|  |         val marker = markerFactory(data) | ||||||
|  |         markersMap.getOrPut(marker) { | ||||||
|  |             SubscribeAsyncReceiver(scope.LinkedSupervisorScope()) { | ||||||
|  |                 safelyWithoutExceptions { block(it) } | ||||||
|  |                 if (isEmpty()) { | ||||||
|  |                     onEmpty(marker) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }.channel.send(data) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | private data class AsyncSubscriptionCommandClearReceiver<T, M>( | ||||||
|  |     val marker: M | ||||||
|  | ) : AsyncSubscriptionCommand<T, M> { | ||||||
|  |     override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) { | ||||||
|  |         val receiver = markersMap[marker] | ||||||
|  |         if (receiver ?.isEmpty() == true) { | ||||||
|  |             markersMap.remove(marker) | ||||||
|  |             receiver.scope.cancel() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun <T, M> Flow<T>.subscribeAsync( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     markerFactory: suspend (T) -> M, | ||||||
|  |     block: suspend (T) -> Unit | ||||||
|  | ): Job { | ||||||
|  |     val subscope = scope.LinkedSupervisorScope() | ||||||
|  |     val markersMap = mutableMapOf<M, SubscribeAsyncReceiver<T>>() | ||||||
|  |     val actor = subscope.actor<AsyncSubscriptionCommand<T, M>>(Channel.UNLIMITED) { | ||||||
|  |         it.invoke(markersMap) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val job = subscribeSafelyWithoutExceptions(subscope) { data -> | ||||||
|  |         val dataCommand = AsyncSubscriptionCommandData(data, subscope, markerFactory, block) { marker -> | ||||||
|  |             actor.send( | ||||||
|  |                 AsyncSubscriptionCommandClearReceiver(marker) | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         actor.send(dataCommand) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     job.invokeOnCompletion { if (subscope.isActive) subscope.cancel() } | ||||||
|  |  | ||||||
|  |     return job | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <T, M> Flow<T>.subscribeSafelyAsync( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     noinline markerFactory: suspend (T) -> M, | ||||||
|  |     noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler, | ||||||
|  |     noinline block: suspend (T) -> Unit | ||||||
|  | ) = subscribeAsync(scope, markerFactory) { | ||||||
|  |     safely(onException) { | ||||||
|  |         block(it) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     noinline markerFactory: suspend (T) -> M, | ||||||
|  |     noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
|  |     noinline block: suspend (T) -> Unit | ||||||
|  | ) = subscribeAsync(scope, markerFactory) { | ||||||
|  |     safelyWithoutExceptions(onException) { | ||||||
|  |         block(it) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <T, M> Flow<T>.subscribeSafelySkippingExceptionsAsync( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     noinline markerFactory: suspend (T) -> M, | ||||||
|  |     noinline block: suspend (T) -> Unit | ||||||
|  | ) = subscribeAsync(scope, markerFactory) { | ||||||
|  |     safelyWithoutExceptions({ /* do nothing */}) { | ||||||
|  |         block(it) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -147,3 +147,10 @@ suspend inline fun <T> runCatchingSafelyWithoutExceptions( | |||||||
| ): Result<T?> = runCatching { | ): Result<T?> = runCatching { | ||||||
|     safelyWithoutExceptions(onException, block) |     safelyWithoutExceptions(onException, block) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | inline fun CoroutineScope( | ||||||
|  |     context: CoroutineContext, | ||||||
|  |     noinline defaultExceptionsHandler: ExceptionHandler<Unit> | ||||||
|  | ) = CoroutineScope( | ||||||
|  |     context + ContextSafelyExceptionHandler(defaultExceptionsHandler) | ||||||
|  | ) | ||||||
|   | |||||||
| @@ -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 | package dev.inmo.micro_utils.crypto | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
| actual fun SourceBytes.md5(): MD5 = CryptoJS.MD5(decodeToString()) | actual fun SourceBytes.md5(): MD5 = CryptoJS.MD5(decodeToString()) | ||||||
|   | |||||||
| @@ -3,6 +3,9 @@ package dev.inmo.micro_utils.crypto | |||||||
| import java.math.BigInteger | import java.math.BigInteger | ||||||
| import java.security.MessageDigest | import java.security.MessageDigest | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
| actual fun SourceBytes.md5(): MD5 = BigInteger( | actual fun SourceBytes.md5(): MD5 = BigInteger( | ||||||
|     1, |     1, | ||||||
|     MessageDigest.getInstance("MD5").digest(this) |     MessageDigest.getInstance("MD5").digest(this) | ||||||
|   | |||||||
| @@ -26,12 +26,12 @@ ext { | |||||||
| } | } | ||||||
|  |  | ||||||
| android { | android { | ||||||
|     compileSdkVersion "$android_compileSdkVersion".toInteger() |     compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger() | ||||||
|     buildToolsVersion "$android_buildToolsVersion" |     buildToolsVersion libs.versions.android.props.buildTools.get() | ||||||
|  |  | ||||||
|     defaultConfig { |     defaultConfig { | ||||||
|         minSdkVersion "$android_minSdkVersion".toInteger() |         minSdkVersion libs.versions.android.props.minSdk.get().toInteger() | ||||||
|         targetSdkVersion "$android_compileSdkVersion".toInteger() |         targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger() | ||||||
|         versionCode "${android_code_version}".toInteger() |         versionCode "${android_code_version}".toInteger() | ||||||
|         versionName "$version" |         versionName "$version" | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -7,17 +7,16 @@ plugins { | |||||||
|  |  | ||||||
| repositories { | repositories { | ||||||
|     mavenLocal() |     mavenLocal() | ||||||
|     jcenter() |  | ||||||
|     google() |     google() | ||||||
|     mavenCentral() |     mavenCentral() | ||||||
| } | } | ||||||
|  |  | ||||||
| kotlin { | kotlin { | ||||||
|     jvm() |     jvm() | ||||||
|     js(BOTH) { | //    js(IR) { | ||||||
|         browser() | //        browser() | ||||||
|         nodejs() | //        nodejs() | ||||||
|     } | //    } | ||||||
|     android {} |     android {} | ||||||
|  |  | ||||||
|     sourceSets { |     sourceSets { | ||||||
| @@ -30,7 +29,7 @@ kotlin { | |||||||
|                         it != project |                         it != project | ||||||
|                         && it.hasProperty("kotlin") |                         && it.hasProperty("kotlin") | ||||||
|                         && it.kotlin.sourceSets.any { it.name.contains("commonMain") } |                         && it.kotlin.sourceSets.any { it.name.contains("commonMain") } | ||||||
|                         && it.kotlin.sourceSets.any { it.name.contains("jsMain") } | //                        && it.kotlin.sourceSets.any { it.name.contains("jsMain") } | ||||||
|                         && it.kotlin.sourceSets.any { it.name.contains("jvmMain") } |                         && it.kotlin.sourceSets.any { it.name.contains("jvmMain") } | ||||||
|                         && it.kotlin.sourceSets.any { it.name.contains("androidMain") } |                         && it.kotlin.sourceSets.any { it.name.contains("androidMain") } | ||||||
|                     ) { |                     ) { | ||||||
| @@ -39,22 +38,22 @@ kotlin { | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         jsMain { | //        jsMain { | ||||||
|             dependencies { | //            dependencies { | ||||||
|                 implementation kotlin('stdlib') | //                implementation kotlin('stdlib') | ||||||
|  |  | ||||||
|                 project.parent.subprojects.forEach { | //                project.parent.subprojects.forEach { | ||||||
|                     if ( | //                    if ( | ||||||
|                         it != project | //                        it != project | ||||||
|                         && it.hasProperty("kotlin") | //                        && it.hasProperty("kotlin") | ||||||
|                         && it.kotlin.sourceSets.any { it.name.contains("commonMain") } | //                        && it.kotlin.sourceSets.any { it.name.contains("commonMain") } | ||||||
|                         && it.kotlin.sourceSets.any { it.name.contains("jsMain") } | //                        && it.kotlin.sourceSets.any { it.name.contains("jsMain") } | ||||||
|                     ) { | //                    ) { | ||||||
|                         api it | //                        api it | ||||||
|                     } | //                    } | ||||||
|                 } | //                } | ||||||
|             } | //            } | ||||||
|         } | //        } | ||||||
|         jvmMain { |         jvmMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 implementation kotlin('stdlib') |                 implementation kotlin('stdlib') | ||||||
| @@ -117,9 +116,9 @@ tasks.dokkaHtml { | |||||||
|             sourceRoots.setFrom(findSourcesWithName("commonMain")) |             sourceRoots.setFrom(findSourcesWithName("commonMain")) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         named("jsMain") { | //        named("jsMain") { | ||||||
|             sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain")) | //            sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain")) | ||||||
|         } | //        } | ||||||
|  |  | ||||||
|         named("jvmMain") { |         named("jvmMain") { | ||||||
|             sourceRoots.setFrom(findSourcesWithName("jvmMain", "commonMain")) |             sourceRoots.setFrom(findSourcesWithName("jvmMain", "commonMain")) | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ allprojects { | |||||||
|         releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true" |         releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true" | ||||||
|  |  | ||||||
|         mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle" |         mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle" | ||||||
|  |         mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle" | ||||||
|         mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle" |         mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle" | ||||||
|         mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle" |         mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle" | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								fsm/common/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								fsm/common/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | plugins { | ||||||
|  |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|  |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "com.android.library" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$mppProjectWithSerializationPresetPath" | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  | ) | ||||||
| @@ -0,0 +1,5 @@ | |||||||
|  | package dev.inmo.micro_utils.fsm.common | ||||||
|  |  | ||||||
|  | interface State { | ||||||
|  |     val context: Any | ||||||
|  | } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | package dev.inmo.micro_utils.fsm.common | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 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? | ||||||
|  | } | ||||||
| @@ -0,0 +1,130 @@ | |||||||
|  | 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.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 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) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 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) | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * This | ||||||
|  |      */ | ||||||
|  |     protected val statesJobs = mutableMapOf<T, Job>() | ||||||
|  |     protected val statesJobsMutex = Mutex() | ||||||
|  |  | ||||||
|  |     protected open suspend fun performUpdate(state: T) { | ||||||
|  |         val newState = launchStateHandling(state, handlers) | ||||||
|  |         if (newState != null) { | ||||||
|  |             statesManager.update(state, newState) | ||||||
|  |         } else { | ||||||
|  |             statesManager.endChain(state) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     open suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) { | ||||||
|  |         statesJobsMutex.withLock { | ||||||
|  |             statesJobs[actualState] ?.cancel() | ||||||
|  |             statesJobs[actualState] = scope.launch { | ||||||
|  |                 performUpdate(actualState) | ||||||
|  |             }.also { job -> | ||||||
|  |                 job.invokeOnCompletion { _ -> | ||||||
|  |                     scope.launch { | ||||||
|  |                         statesJobsMutex.withLock { | ||||||
|  |                             if (statesJobs[actualState] == job) { | ||||||
|  |                                 statesJobs.remove(actualState) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Launch handling of states. On [statesManager] [StatesManager.onStartChain], | ||||||
|  |      * [statesManager] [StatesManager.onChainStateUpdated] will be called lambda with performing of state. If | ||||||
|  |      * [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 { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) } | ||||||
|  |         } | ||||||
|  |         statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) { | ||||||
|  |             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 { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Just calls [StatesManager.startChain] of [statesManager] | ||||||
|  |      */ | ||||||
|  |     override suspend fun startChain(state: T) { | ||||||
|  |         statesManager.startChain(state) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,30 @@ | |||||||
|  | package dev.inmo.micro_utils.fsm.common | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  |  | ||||||
|  | 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: 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: 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: T) | ||||||
|  |  | ||||||
|  |     suspend fun getActiveStates(): List<T> | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -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) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +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<T : State>( | ||||||
|  |     var statesManager: StatesManager<T> = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()), | ||||||
|  |     val fsmBuilder: (statesManager: StatesManager<T>, states: List<CheckableHandlerHolder<T, T>>) -> StatesMachine<T> = { statesManager, states -> | ||||||
|  |         StatesMachine( | ||||||
|  |             statesManager, | ||||||
|  |             states | ||||||
|  |         ) | ||||||
|  |     }, | ||||||
|  |     var defaultStateHandler: StatesHandler<T, T>? = StatesHandler { null } | ||||||
|  | ) { | ||||||
|  |     private var states = mutableListOf<CheckableHandlerHolder<T, T>>() | ||||||
|  |  | ||||||
|  |     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 | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun <T : State> buildFSM( | ||||||
|  |     block: FSMBuilder<T>.() -> Unit | ||||||
|  | ): StatesMachine<T> = FSMBuilder<T>().apply(block).build() | ||||||
| @@ -0,0 +1,102 @@ | |||||||
|  | package dev.inmo.micro_utils.fsm.common.managers | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.fsm.common.State | ||||||
|  | import dev.inmo.micro_utils.fsm.common.StatesManager | ||||||
|  | import kotlinx.coroutines.flow.* | ||||||
|  | import kotlinx.coroutines.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Implement this repo if you want to use some custom repo for [DefaultStatesManager] | ||||||
|  |  */ | ||||||
|  | interface DefaultStatesManagerRepo<T : State> { | ||||||
|  |     /** | ||||||
|  |      * Must save [state] as current state of chain with [State.context] of [state] | ||||||
|  |      */ | ||||||
|  |     suspend fun set(state: T) | ||||||
|  |     /** | ||||||
|  |      * Remove exactly [state]. In case if internally [State.context] is busy with different [State], that [State] should | ||||||
|  |      * NOT be removed | ||||||
|  |      */ | ||||||
|  |     suspend fun removeState(state: T) | ||||||
|  |     /** | ||||||
|  |      * @return Current list of available and saved states | ||||||
|  |      */ | ||||||
|  |     suspend fun getStates(): List<T> | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @return Current state by [context] | ||||||
|  |      */ | ||||||
|  |     suspend fun getContextState(context: Any): T? | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @return Current state by [context] | ||||||
|  |      */ | ||||||
|  |     suspend fun contains(context: Any): Boolean = getContextState(context) != null | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @param repo This repo will be used as repository for storing states. All operations with this repo will happen BEFORE | ||||||
|  |  * any event will be sent to [onChainStateUpdated], [onStartChain] or [onEndChain]. By default, will be used | ||||||
|  |  * [InMemoryDefaultStatesManagerRepo] or you may create custom [DefaultStatesManagerRepo] and pass as [repo] parameter | ||||||
|  |  * @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context] | ||||||
|  |  * key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by | ||||||
|  |  * new state by using [endChain] with that state | ||||||
|  |  */ | ||||||
|  | open class DefaultStatesManager<T : State>( | ||||||
|  |     protected val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(), | ||||||
|  |     protected val onContextsConflictResolver: 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() | ||||||
|  |  | ||||||
|  |     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 || onContextsConflictResolver(old, new, stateOnNewOneContext)) { | ||||||
|  |                     stateOnNewOneContext ?.let { endChainWithoutLock(it) } | ||||||
|  |                     repo.removeState(old) | ||||||
|  |                     repo.set(new) | ||||||
|  |                     _onChainStateUpdated.emit(old to new) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override suspend fun startChain(state: T) = mapMutex.withLock { | ||||||
|  |         if (!repo.contains(state.context)) { | ||||||
|  |             repo.set(state) | ||||||
|  |             _onStartChain.emit(state) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     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,16 @@ | |||||||
|  | package dev.inmo.micro_utils.fsm.common.managers | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.fsm.common.State | ||||||
|  | import kotlinx.coroutines.flow.* | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Creates [DefaultStatesManager] with [InMemoryDefaultStatesManagerRepo] | ||||||
|  |  * | ||||||
|  |  * @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context] | ||||||
|  |  * key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by | ||||||
|  |  * new state by using [endChain] with that state | ||||||
|  |  */ | ||||||
|  | @Deprecated("Use DefaultStatesManager instead", ReplaceWith("DefaultStatesManager")) | ||||||
|  | fun <T: State> InMemoryStatesManager( | ||||||
|  |     onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true } | ||||||
|  | ) = DefaultStatesManager(onContextsConflictResolver = onContextsConflictResolver) | ||||||
							
								
								
									
										54
									
								
								fsm/common/src/jvmTest/kotlin/PlayableMain.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								fsm/common/src/jvmTest/kotlin/PlayableMain.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | import dev.inmo.micro_utils.fsm.common.* | ||||||
|  | import dev.inmo.micro_utils.fsm.common.dsl.buildFSM | ||||||
|  | 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 { | ||||||
|  |     val trafficLightNumber: Int | ||||||
|  |     override val context: Int | ||||||
|  |         get() = trafficLightNumber | ||||||
|  | } | ||||||
|  | data class GreenCommon(override val trafficLightNumber: Int) : TrafficLightState | ||||||
|  | data class YellowCommon(override val trafficLightNumber: Int) : TrafficLightState | ||||||
|  | data class RedCommon(override val trafficLightNumber: Int) : TrafficLightState | ||||||
|  |  | ||||||
|  | class PlayableMain { | ||||||
|  | //    @Test | ||||||
|  |     fun test() { | ||||||
|  |         runBlocking { | ||||||
|  |             val countOfTrafficLights = 10 | ||||||
|  |             val initialStates = (0 until countOfTrafficLights).map { | ||||||
|  |                 when (0/*Random.nextInt(3)*/) { | ||||||
|  |                     0 -> GreenCommon(it) | ||||||
|  |                     1 -> YellowCommon(it) | ||||||
|  |                     else -> RedCommon(it) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             val statesManager = DefaultStatesManager<TrafficLightState>() | ||||||
|  |  | ||||||
|  |             val machine = buildFSM<TrafficLightState> { | ||||||
|  |                 strictlyOn<GreenCommon> { | ||||||
|  |                     delay(1000L) | ||||||
|  |                     YellowCommon(it.context).also(::println) | ||||||
|  |                 } | ||||||
|  |                 strictlyOn<YellowCommon> { | ||||||
|  |                     delay(1000L) | ||||||
|  |                     RedCommon(it.context).also(::println) | ||||||
|  |                 } | ||||||
|  |                 strictlyOn<RedCommon> { | ||||||
|  |                     delay(1000L) | ||||||
|  |                     GreenCommon(it.context).also(::println) | ||||||
|  |                 } | ||||||
|  |                 this.statesManager = statesManager | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             initialStates.forEach { machine.startChain(it) } | ||||||
|  |  | ||||||
|  |             val scope = CoroutineScope(Dispatchers.Default) | ||||||
|  |             machine.start(scope).join() | ||||||
|  |  | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								fsm/common/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								fsm/common/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest package="dev.inmo.micro_utils.fsm.common"/> | ||||||
							
								
								
									
										18
									
								
								fsm/repos/common/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								fsm/repos/common/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | plugins { | ||||||
|  |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|  |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "com.android.library" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$mppProjectWithSerializationPresetPath" | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     sourceSets { | ||||||
|  |         commonMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api project(":micro_utils.fsm.common") | ||||||
|  |                 api project(":micro_utils.repos.common") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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
									
								
								fsm/repos/common/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								fsm/repos/common/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest package="dev.inmo.micro_utils.fsm.repos.common"/> | ||||||
| @@ -7,43 +7,12 @@ android.useAndroidX=true | |||||||
| android.enableJetifier=true | android.enableJetifier=true | ||||||
| org.gradle.jvmargs=-Xmx2g | org.gradle.jvmargs=-Xmx2g | ||||||
|  |  | ||||||
| kotlin_version=1.5.10 |  | ||||||
| kotlin_coroutines_version=1.5.0 |  | ||||||
| kotlin_serialisation_core_version=1.2.1 |  | ||||||
| kotlin_exposed_version=0.32.1 |  | ||||||
|  |  | ||||||
| ktor_version=1.6.0 |  | ||||||
|  |  | ||||||
| klockVersion=2.1.2 |  | ||||||
|  |  | ||||||
| github_release_plugin_version=2.2.12 |  | ||||||
|  |  | ||||||
| uuidVersion=0.3.0 |  | ||||||
|  |  | ||||||
| # ANDROID |  | ||||||
|  |  | ||||||
| core_ktx_version=1.3.2 |  | ||||||
| androidx_recycler_version=1.2.0 |  | ||||||
| appcompat_version=1.2.0 |  | ||||||
|  |  | ||||||
| android_minSdkVersion=19 |  | ||||||
| android_compileSdkVersion=30 |  | ||||||
| android_buildToolsVersion=30.0.3 |  | ||||||
| dexcount_version=2.0.0 |  | ||||||
| junit_version=4.12 |  | ||||||
| test_ext_junit_version=1.1.2 |  | ||||||
| espresso_core=3.3.0 |  | ||||||
|  |  | ||||||
| # JS NPM | # JS NPM | ||||||
|  |  | ||||||
| crypto_js_version=4.0.0 | crypto_js_version=4.1.1 | ||||||
|  |  | ||||||
| # Dokka |  | ||||||
|  |  | ||||||
| dokka_version=1.4.32 |  | ||||||
|  |  | ||||||
| # Project data | # Project data | ||||||
|  |  | ||||||
| group=dev.inmo | group=dev.inmo | ||||||
| version=0.5.9 | version=0.9.15 | ||||||
| android_code_version=50 | android_code_version=105 | ||||||
|   | |||||||
							
								
								
									
										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.6.3" | ||||||
|  | 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 | distributionBase=GRADLE_USER_HOME | ||||||
| distributionPath=wrapper/dists | distributionPath=wrapper/dists | ||||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip | ||||||
| zipStoreBase=GRADLE_USER_HOME | zipStoreBase=GRADLE_USER_HOME | ||||||
| zipStorePath=wrapper/dists | zipStorePath=wrapper/dists | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ kotlin { | |||||||
|             dependencies { |             dependencies { | ||||||
|                 api internalProject("micro_utils.ktor.common") |                 api internalProject("micro_utils.ktor.common") | ||||||
|                 api internalProject("micro_utils.coroutines") |                 api internalProject("micro_utils.coroutines") | ||||||
|                 api "io.ktor:ktor-client-core:$ktor_version" |                 api libs.ktor.client | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import dev.inmo.micro_utils.coroutines.safely | |||||||
| import dev.inmo.micro_utils.ktor.common.* | import dev.inmo.micro_utils.ktor.common.* | ||||||
| import io.ktor.client.HttpClient | import io.ktor.client.HttpClient | ||||||
| import io.ktor.client.features.websocket.ws | import io.ktor.client.features.websocket.ws | ||||||
|  | import io.ktor.client.request.HttpRequestBuilder | ||||||
| import io.ktor.http.cio.websocket.Frame | import io.ktor.http.cio.websocket.Frame | ||||||
| import io.ktor.http.cio.websocket.readBytes | import io.ktor.http.cio.websocket.readBytes | ||||||
| import kotlinx.coroutines.flow.Flow | import kotlinx.coroutines.flow.Flow | ||||||
| @@ -17,6 +18,7 @@ import kotlinx.serialization.DeserializationStrategy | |||||||
| inline fun <T> HttpClient.createStandardWebsocketFlow( | inline fun <T> HttpClient.createStandardWebsocketFlow( | ||||||
|     url: String, |     url: String, | ||||||
|     crossinline checkReconnection: (Throwable?) -> Boolean = { true }, |     crossinline checkReconnection: (Throwable?) -> Boolean = { true }, | ||||||
|  |     noinline requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||||
|     crossinline conversation: suspend (StandardKtorSerialInputData) -> T |     crossinline conversation: suspend (StandardKtorSerialInputData) -> T | ||||||
| ): Flow<T> { | ): Flow<T> { | ||||||
|     val correctedUrl = url.asCorrectWebSocketUrl |     val correctedUrl = url.asCorrectWebSocketUrl | ||||||
| @@ -26,7 +28,7 @@ inline fun <T> HttpClient.createStandardWebsocketFlow( | |||||||
|         do { |         do { | ||||||
|             val reconnect = try { |             val reconnect = try { | ||||||
|                 safely { |                 safely { | ||||||
|                     ws(correctedUrl) { |                     ws(correctedUrl, requestBuilder) { | ||||||
|                         for (received in incoming) { |                         for (received in incoming) { | ||||||
|                             when (received) { |                             when (received) { | ||||||
|                                 is Frame.Binary -> producerScope.send(conversation(received.readBytes())) |                                 is Frame.Binary -> producerScope.send(conversation(received.readBytes())) | ||||||
| @@ -65,10 +67,12 @@ inline fun <T> HttpClient.createStandardWebsocketFlow( | |||||||
|     url: String, |     url: String, | ||||||
|     crossinline checkReconnection: (Throwable?) -> Boolean = { true }, |     crossinline checkReconnection: (Throwable?) -> Boolean = { true }, | ||||||
|     deserializer: DeserializationStrategy<T>, |     deserializer: DeserializationStrategy<T>, | ||||||
|     serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat |     serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat, | ||||||
|  |     noinline requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||||
| ) = createStandardWebsocketFlow( | ) = createStandardWebsocketFlow( | ||||||
|     url, |     url, | ||||||
|     checkReconnection |     checkReconnection, | ||||||
|  |     requestBuilder | ||||||
| ) { | ) { | ||||||
|     serialFormat.decodeDefault(deserializer, it) |     serialFormat.decodeDefault(deserializer, it) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | package dev.inmo.micro_utils.ktor.client | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.MPPFile | ||||||
|  | import io.ktor.client.request.forms.InputProvider | ||||||
|  |  | ||||||
|  | expect suspend fun MPPFile.inputProvider(): InputProvider | ||||||
| @@ -1,16 +1,20 @@ | |||||||
| package dev.inmo.micro_utils.ktor.client | package dev.inmo.micro_utils.ktor.client | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.MPPFile | ||||||
|  | import dev.inmo.micro_utils.common.filename | ||||||
| import dev.inmo.micro_utils.ktor.common.* | import dev.inmo.micro_utils.ktor.common.* | ||||||
| import io.ktor.client.HttpClient | import io.ktor.client.HttpClient | ||||||
| import io.ktor.client.request.get | import io.ktor.client.request.* | ||||||
| import io.ktor.client.request.post | import io.ktor.client.request.forms.* | ||||||
|  | import io.ktor.http.* | ||||||
|  | import io.ktor.utils.io.core.ByteReadPacket | ||||||
| import kotlinx.serialization.* | import kotlinx.serialization.* | ||||||
|  |  | ||||||
| typealias BodyPair<T> = Pair<SerializationStrategy<T>, T> | typealias BodyPair<T> = Pair<SerializationStrategy<T>, T> | ||||||
|  |  | ||||||
| class UnifiedRequester( | class UnifiedRequester( | ||||||
|     private val client: HttpClient = HttpClient(), |     val client: HttpClient = HttpClient(), | ||||||
|     private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat |     val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat | ||||||
| ) { | ) { | ||||||
|     suspend fun <ResultType> uniget( |     suspend fun <ResultType> uniget( | ||||||
|         url: String, |         url: String, | ||||||
| @@ -31,11 +35,66 @@ class UnifiedRequester( | |||||||
|         resultDeserializer: DeserializationStrategy<ResultType> |         resultDeserializer: DeserializationStrategy<ResultType> | ||||||
|     ) = client.unipost(url, bodyInfo, resultDeserializer, serialFormat) |     ) = client.unipost(url, bodyInfo, resultDeserializer, serialFormat) | ||||||
|  |  | ||||||
|  |     suspend fun <ResultType> unimultipart( | ||||||
|  |         url: String, | ||||||
|  |         filename: String, | ||||||
|  |         inputProvider: InputProvider, | ||||||
|  |         resultDeserializer: DeserializationStrategy<ResultType>, | ||||||
|  |         mimetype: String = "*/*", | ||||||
|  |         additionalParametersBuilder: FormBuilder.() -> Unit = {}, | ||||||
|  |         dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, | ||||||
|  |         requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||||
|  |     ): ResultType = client.unimultipart(url, filename, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat) | ||||||
|  |  | ||||||
|  |     suspend fun <BodyType, ResultType> unimultipart( | ||||||
|  |         url: String, | ||||||
|  |         filename: String, | ||||||
|  |         inputProvider: InputProvider, | ||||||
|  |         otherData: BodyPair<BodyType>, | ||||||
|  |         resultDeserializer: DeserializationStrategy<ResultType>, | ||||||
|  |         mimetype: String = "*/*", | ||||||
|  |         additionalParametersBuilder: FormBuilder.() -> Unit = {}, | ||||||
|  |         dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, | ||||||
|  |         requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||||
|  |     ): ResultType = client.unimultipart(url, filename, otherData, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat) | ||||||
|  |  | ||||||
|  |     suspend fun <ResultType> unimultipart( | ||||||
|  |         url: String, | ||||||
|  |         mppFile: MPPFile, | ||||||
|  |         resultDeserializer: DeserializationStrategy<ResultType>, | ||||||
|  |         mimetype: String = "*/*", | ||||||
|  |         additionalParametersBuilder: FormBuilder.() -> Unit = {}, | ||||||
|  |         dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, | ||||||
|  |         requestBuilder: HttpRequestBuilder.() -> Unit = {} | ||||||
|  |     ): ResultType = client.unimultipart( | ||||||
|  |         url, mppFile, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     suspend fun <BodyType, ResultType> unimultipart( | ||||||
|  |         url: String, | ||||||
|  |         mppFile: MPPFile, | ||||||
|  |         otherData: BodyPair<BodyType>, | ||||||
|  |         resultDeserializer: DeserializationStrategy<ResultType>, | ||||||
|  |         mimetype: String = "*/*", | ||||||
|  |         additionalParametersBuilder: FormBuilder.() -> Unit = {}, | ||||||
|  |         dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, | ||||||
|  |         requestBuilder: HttpRequestBuilder.() -> Unit = {} | ||||||
|  |     ): ResultType = client.unimultipart( | ||||||
|  |         url, mppFile, otherData, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     fun <T> createStandardWebsocketFlow( |     fun <T> createStandardWebsocketFlow( | ||||||
|         url: String, |         url: String, | ||||||
|         checkReconnection: (Throwable?) -> Boolean = { true }, |         checkReconnection: (Throwable?) -> Boolean, | ||||||
|         deserializer: DeserializationStrategy<T> |         deserializer: DeserializationStrategy<T>, | ||||||
|     ) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat) |         requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||||
|  |     ) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat, requestBuilder) | ||||||
|  |  | ||||||
|  |     fun <T> createStandardWebsocketFlow( | ||||||
|  |         url: String, | ||||||
|  |         deserializer: DeserializationStrategy<T>, | ||||||
|  |         requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||||
|  |     ) = createStandardWebsocketFlow(url, { true  }, deserializer, requestBuilder) | ||||||
| } | } | ||||||
|  |  | ||||||
| val defaultRequester = UnifiedRequester() | val defaultRequester = UnifiedRequester() | ||||||
| @@ -69,3 +128,124 @@ suspend fun <BodyType, ResultType> HttpClient.unipost( | |||||||
| }.let { | }.let { | ||||||
|     serialFormat.decodeDefault(resultDeserializer, it) |     serialFormat.decodeDefault(resultDeserializer, it) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | suspend fun <ResultType> HttpClient.unimultipart( | ||||||
|  |     url: String, | ||||||
|  |     filename: String, | ||||||
|  |     inputProvider: InputProvider, | ||||||
|  |     resultDeserializer: DeserializationStrategy<ResultType>, | ||||||
|  |     mimetype: String = "*/*", | ||||||
|  |     additionalParametersBuilder: FormBuilder.() -> Unit = {}, | ||||||
|  |     dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, | ||||||
|  |     requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||||
|  |     serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat | ||||||
|  | ): ResultType = submitFormWithBinaryData<StandardKtorSerialInputData>( | ||||||
|  |     url, | ||||||
|  |     formData = formData { | ||||||
|  |         append( | ||||||
|  |             "bytes", | ||||||
|  |             inputProvider, | ||||||
|  |             Headers.build { | ||||||
|  |                 append(HttpHeaders.ContentType, mimetype) | ||||||
|  |                 append(HttpHeaders.ContentDisposition, "filename=\"$filename\"") | ||||||
|  |                 dataHeadersBuilder() | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |         additionalParametersBuilder() | ||||||
|  |     } | ||||||
|  | ) { | ||||||
|  |     requestBuilder() | ||||||
|  | }.let { serialFormat.decodeDefault(resultDeserializer, it) } | ||||||
|  |  | ||||||
|  | suspend fun <BodyType, ResultType> HttpClient.unimultipart( | ||||||
|  |     url: String, | ||||||
|  |     filename: String, | ||||||
|  |     otherData: BodyPair<BodyType>, | ||||||
|  |     inputProvider: InputProvider, | ||||||
|  |     resultDeserializer: DeserializationStrategy<ResultType>, | ||||||
|  |     mimetype: String = "*/*", | ||||||
|  |     additionalParametersBuilder: FormBuilder.() -> Unit = {}, | ||||||
|  |     dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, | ||||||
|  |     requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||||
|  |     serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat | ||||||
|  | ): ResultType = unimultipart( | ||||||
|  |     url, | ||||||
|  |     filename, | ||||||
|  |     inputProvider, | ||||||
|  |     resultDeserializer, | ||||||
|  |     mimetype, | ||||||
|  |     additionalParametersBuilder = { | ||||||
|  |         val serialized = serialFormat.encodeDefault(otherData.first, otherData.second) | ||||||
|  |         append( | ||||||
|  |             "data", | ||||||
|  |             InputProvider(serialized.size.toLong()) { | ||||||
|  |                 ByteReadPacket(serialized) | ||||||
|  |             }, | ||||||
|  |             Headers.build { | ||||||
|  |                 append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType) | ||||||
|  |                 append(HttpHeaders.ContentDisposition, "filename=data.bytes") | ||||||
|  |                 dataHeadersBuilder() | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |         additionalParametersBuilder() | ||||||
|  |     }, | ||||||
|  |     dataHeadersBuilder, | ||||||
|  |     requestBuilder, | ||||||
|  |     serialFormat | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | suspend fun <ResultType> HttpClient.unimultipart( | ||||||
|  |     url: String, | ||||||
|  |     mppFile: MPPFile, | ||||||
|  |     resultDeserializer: DeserializationStrategy<ResultType>, | ||||||
|  |     mimetype: String = "*/*", | ||||||
|  |     additionalParametersBuilder: FormBuilder.() -> Unit = {}, | ||||||
|  |     dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, | ||||||
|  |     requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||||
|  |     serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat | ||||||
|  | ): ResultType = unimultipart( | ||||||
|  |     url, | ||||||
|  |     mppFile.filename.string, | ||||||
|  |     mppFile.inputProvider(), | ||||||
|  |     resultDeserializer, | ||||||
|  |     mimetype, | ||||||
|  |     additionalParametersBuilder, | ||||||
|  |     dataHeadersBuilder, | ||||||
|  |     requestBuilder, | ||||||
|  |     serialFormat | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | suspend fun <BodyType, ResultType> HttpClient.unimultipart( | ||||||
|  |     url: String, | ||||||
|  |     mppFile: MPPFile, | ||||||
|  |     otherData: BodyPair<BodyType>, | ||||||
|  |     resultDeserializer: DeserializationStrategy<ResultType>, | ||||||
|  |     mimetype: String = "*/*", | ||||||
|  |     additionalParametersBuilder: FormBuilder.() -> Unit = {}, | ||||||
|  |     dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, | ||||||
|  |     requestBuilder: HttpRequestBuilder.() -> Unit = {}, | ||||||
|  |     serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat | ||||||
|  | ): ResultType = unimultipart( | ||||||
|  |     url, | ||||||
|  |     mppFile, | ||||||
|  |     resultDeserializer, | ||||||
|  |     mimetype, | ||||||
|  |     additionalParametersBuilder = { | ||||||
|  |         val serialized = serialFormat.encodeDefault(otherData.first, otherData.second) | ||||||
|  |         append( | ||||||
|  |             "data", | ||||||
|  |             InputProvider(serialized.size.toLong()) { | ||||||
|  |                 ByteReadPacket(serialized) | ||||||
|  |             }, | ||||||
|  |             Headers.build { | ||||||
|  |                 append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType) | ||||||
|  |                 append(HttpHeaders.ContentDisposition, "filename=data.bytes") | ||||||
|  |                 dataHeadersBuilder() | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |         additionalParametersBuilder() | ||||||
|  |     }, | ||||||
|  |     dataHeadersBuilder, | ||||||
|  |     requestBuilder, | ||||||
|  |     serialFormat | ||||||
|  | ) | ||||||
|   | |||||||
| @@ -0,0 +1,19 @@ | |||||||
|  | package dev.inmo.micro_utils.ktor.client | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.MPPFile | ||||||
|  | import dev.inmo.micro_utils.ktor.common.* | ||||||
|  | import io.ktor.client.HttpClient | ||||||
|  |  | ||||||
|  | expect suspend fun HttpClient.tempUpload( | ||||||
|  |     fullTempUploadDraftPath: String, | ||||||
|  |     file: MPPFile, | ||||||
|  |     onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> } | ||||||
|  | ): TemporalFileId | ||||||
|  |  | ||||||
|  | suspend fun UnifiedRequester.tempUpload( | ||||||
|  |     fullTempUploadDraftPath: String, | ||||||
|  |     file: MPPFile, | ||||||
|  |     onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> } | ||||||
|  | ): TemporalFileId = client.tempUpload( | ||||||
|  |     fullTempUploadDraftPath, file, onUpload | ||||||
|  | ) | ||||||
| @@ -0,0 +1,11 @@ | |||||||
|  | package dev.inmo.micro_utils.ktor.client | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.* | ||||||
|  | import io.ktor.client.request.forms.InputProvider | ||||||
|  | import io.ktor.utils.io.core.ByteReadPacket | ||||||
|  |  | ||||||
|  | actual suspend fun MPPFile.inputProvider(): InputProvider = bytes().let { | ||||||
|  |     InputProvider(it.size.toLong()) { | ||||||
|  |         ByteReadPacket(it) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,58 @@ | |||||||
|  | package dev.inmo.micro_utils.ktor.client | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.MPPFile | ||||||
|  | import dev.inmo.micro_utils.ktor.common.TemporalFileId | ||||||
|  | import io.ktor.client.HttpClient | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import org.w3c.xhr.* | ||||||
|  |  | ||||||
|  | suspend fun tempUpload( | ||||||
|  |     fullTempUploadDraftPath: String, | ||||||
|  |     file: MPPFile, | ||||||
|  |     onUpload: (Long, Long) -> Unit | ||||||
|  | ): TemporalFileId { | ||||||
|  |     val formData = FormData() | ||||||
|  |     val answer = CompletableDeferred<TemporalFileId>() | ||||||
|  |  | ||||||
|  |     formData.append( | ||||||
|  |         "data", | ||||||
|  |         file | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     val request = XMLHttpRequest() | ||||||
|  |     request.responseType = XMLHttpRequestResponseType.TEXT | ||||||
|  |     request.upload.onprogress = { | ||||||
|  |         onUpload(it.loaded.toLong(), it.total.toLong()) | ||||||
|  |     } | ||||||
|  |     request.onload = { | ||||||
|  |         if (request.status == 200.toShort()) { | ||||||
|  |             answer.complete(TemporalFileId(request.responseText)) | ||||||
|  |         } else { | ||||||
|  |             answer.completeExceptionally(Exception("Something went wrong: $it")) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     request.onerror = { | ||||||
|  |         answer.completeExceptionally(Exception("Something went wrong: $it")) | ||||||
|  |     } | ||||||
|  |     request.open("POST", fullTempUploadDraftPath, true) | ||||||
|  |     request.send(formData) | ||||||
|  |  | ||||||
|  |     val handle = currentCoroutineContext().job.invokeOnCompletion { | ||||||
|  |         runCatching { | ||||||
|  |             request.abort() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return runCatching { | ||||||
|  |         answer.await() | ||||||
|  |     }.also { | ||||||
|  |         handle.dispose() | ||||||
|  |     }.getOrThrow() | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | actual suspend fun HttpClient.tempUpload( | ||||||
|  |     fullTempUploadDraftPath: String, | ||||||
|  |     file: MPPFile, | ||||||
|  |     onUpload: (uploaded: Long, count: Long) -> Unit | ||||||
|  | ): TemporalFileId = dev.inmo.micro_utils.ktor.client.tempUpload(fullTempUploadDraftPath, file, onUpload) | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | package dev.inmo.micro_utils.ktor.client | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.MPPFile | ||||||
|  | import io.ktor.client.request.forms.InputProvider | ||||||
|  | import io.ktor.utils.io.streams.asInput | ||||||
|  |  | ||||||
|  | actual suspend fun MPPFile.inputProvider(): InputProvider = InputProvider(length()) { | ||||||
|  |     inputStream().asInput() | ||||||
|  | } | ||||||
| @@ -0,0 +1,39 @@ | |||||||
|  | package dev.inmo.micro_utils.ktor.client | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.MPPFile | ||||||
|  | import dev.inmo.micro_utils.common.filename | ||||||
|  | import dev.inmo.micro_utils.ktor.common.TemporalFileId | ||||||
|  | import io.ktor.client.HttpClient | ||||||
|  | import io.ktor.client.features.onUpload | ||||||
|  | import io.ktor.client.request.forms.formData | ||||||
|  | import io.ktor.client.request.forms.submitFormWithBinaryData | ||||||
|  | import io.ktor.http.Headers | ||||||
|  | import io.ktor.http.HttpHeaders | ||||||
|  | import java.net.URLConnection | ||||||
|  |  | ||||||
|  | internal val MPPFile.mimeType: String | ||||||
|  |     get() = URLConnection.getFileNameMap().getContentTypeFor(filename.name) ?: "*/*" | ||||||
|  |  | ||||||
|  | actual suspend fun HttpClient.tempUpload( | ||||||
|  |     fullTempUploadDraftPath: String, | ||||||
|  |     file: MPPFile, | ||||||
|  |     onUpload: (Long, Long) -> Unit | ||||||
|  | ): TemporalFileId { | ||||||
|  |     val inputProvider = file.inputProvider() | ||||||
|  |     val fileId = submitFormWithBinaryData<String>( | ||||||
|  |         fullTempUploadDraftPath, | ||||||
|  |         formData = formData { | ||||||
|  |             append( | ||||||
|  |                 "data", | ||||||
|  |                 inputProvider, | ||||||
|  |                 Headers.build { | ||||||
|  |                     append(HttpHeaders.ContentType, file.mimeType) | ||||||
|  |                     append(HttpHeaders.ContentDisposition, "filename=\"${file.filename.string}\"") | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|  |         onUpload(onUpload) | ||||||
|  |     } | ||||||
|  |     return TemporalFileId(fileId) | ||||||
|  | } | ||||||
| @@ -10,8 +10,10 @@ kotlin { | |||||||
|     sourceSets { |     sourceSets { | ||||||
|         commonMain { |         commonMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "org.jetbrains.kotlinx:kotlinx-serialization-cbor:$kotlin_serialisation_core_version" |                 api internalProject("micro_utils.common") | ||||||
|                 api "com.soywiz.korlibs.klock:klock:$klockVersion" |                 api libs.kt.serialization.cbor | ||||||
|  |                 api libs.klock | ||||||
|  |                 api libs.uuid | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -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 { |         jvmMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "io.ktor:ktor-server:$ktor_version" |                 api libs.ktor.server | ||||||
|                 api "io.ktor:ktor-server-cio:$ktor_version" |                 api libs.ktor.server.cio | ||||||
|                 api "io.ktor:ktor-server-host-common:$ktor_version" |                 api libs.ktor.server.host.common | ||||||
|                 api "io.ktor:ktor-websockets:$ktor_version" |                 api libs.ktor.websockets | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -2,29 +2,31 @@ package dev.inmo.micro_utils.ktor.server | |||||||
|  |  | ||||||
| import dev.inmo.micro_utils.coroutines.safely | import dev.inmo.micro_utils.coroutines.safely | ||||||
| import dev.inmo.micro_utils.ktor.common.* | import dev.inmo.micro_utils.ktor.common.* | ||||||
|  | import io.ktor.application.featureOrNull | ||||||
|  | import io.ktor.application.install | ||||||
|  | import io.ktor.http.URLProtocol | ||||||
| import io.ktor.http.cio.websocket.* | import io.ktor.http.cio.websocket.* | ||||||
| import io.ktor.routing.Route | import io.ktor.routing.Route | ||||||
| import io.ktor.websocket.webSocket | import io.ktor.routing.application | ||||||
|  | import io.ktor.websocket.* | ||||||
| import kotlinx.coroutines.flow.Flow | import kotlinx.coroutines.flow.Flow | ||||||
| import kotlinx.coroutines.flow.collect |  | ||||||
| import kotlinx.serialization.SerializationStrategy | import kotlinx.serialization.SerializationStrategy | ||||||
|  |  | ||||||
| private suspend fun DefaultWebSocketSession.checkReceivedAndCloseIfExists() { |  | ||||||
|     if (incoming.tryReceive() != null) { |  | ||||||
|         close() |  | ||||||
|         throw CorrectCloseException |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fun <T> Route.includeWebsocketHandling( | fun <T> Route.includeWebsocketHandling( | ||||||
|     suburl: String, |     suburl: String, | ||||||
|     flow: Flow<T>, |     flow: Flow<T>, | ||||||
|     converter: (T) -> StandardKtorSerialInputData |     protocol: URLProtocol = URLProtocol.WS, | ||||||
|  |     converter: suspend WebSocketServerSession.(T) -> StandardKtorSerialInputData? | ||||||
| ) { | ) { | ||||||
|     webSocket(suburl) { |     application.apply { | ||||||
|  |         featureOrNull(io.ktor.websocket.WebSockets) ?: install(io.ktor.websocket.WebSockets) | ||||||
|  |     } | ||||||
|  |     webSocket(suburl, protocol.name) { | ||||||
|         safely { |         safely { | ||||||
|             flow.collect { |             flow.collect { | ||||||
|                 send(converter(it)) |                 converter(it) ?.let { data -> | ||||||
|  |                     send(data) | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -34,10 +36,24 @@ fun <T> Route.includeWebsocketHandling( | |||||||
|     suburl: String, |     suburl: String, | ||||||
|     flow: Flow<T>, |     flow: Flow<T>, | ||||||
|     serializer: SerializationStrategy<T>, |     serializer: SerializationStrategy<T>, | ||||||
|     serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat |     serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat, | ||||||
|  |     protocol: URLProtocol = URLProtocol.WS, | ||||||
|  |     filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null | ||||||
| ) = includeWebsocketHandling( | ) = includeWebsocketHandling( | ||||||
|     suburl, |     suburl, | ||||||
|     flow |     flow, | ||||||
| ) { |     protocol, | ||||||
|     serialFormat.encodeDefault(serializer, it) |     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 | package dev.inmo.micro_utils.ktor.server | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.* | ||||||
| import dev.inmo.micro_utils.coroutines.safely | import dev.inmo.micro_utils.coroutines.safely | ||||||
| import dev.inmo.micro_utils.ktor.common.* | import dev.inmo.micro_utils.ktor.common.* | ||||||
| import io.ktor.application.ApplicationCall | import io.ktor.application.ApplicationCall | ||||||
| import io.ktor.application.call | import io.ktor.application.call | ||||||
| import io.ktor.http.ContentType | import io.ktor.http.* | ||||||
| import io.ktor.http.HttpStatusCode | import io.ktor.http.content.PartData | ||||||
|  | import io.ktor.http.content.forEachPart | ||||||
| import io.ktor.request.receive | import io.ktor.request.receive | ||||||
|  | import io.ktor.request.receiveMultipart | ||||||
| import io.ktor.response.respond | import io.ktor.response.respond | ||||||
| import io.ktor.response.respondBytes | import io.ktor.response.respondBytes | ||||||
| import io.ktor.routing.Route | import io.ktor.routing.Route | ||||||
|  | import io.ktor.util.asStream | ||||||
|  | import io.ktor.util.cio.writeChannel | ||||||
| import io.ktor.util.pipeline.PipelineContext | import io.ktor.util.pipeline.PipelineContext | ||||||
|  | import io.ktor.utils.io.core.* | ||||||
|  | import io.ktor.websocket.WebSocketServerSession | ||||||
| import kotlinx.coroutines.flow.Flow | import kotlinx.coroutines.flow.Flow | ||||||
| import kotlinx.serialization.* | import kotlinx.serialization.* | ||||||
|  | import java.io.File | ||||||
|  | import java.io.File.createTempFile | ||||||
|  |  | ||||||
| class UnifiedRouter( | class UnifiedRouter( | ||||||
|     private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat, |     val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat, | ||||||
|     private val serialFormatContentType: ContentType = standardKtorSerialFormatContentType |     val serialFormatContentType: ContentType = standardKtorSerialFormatContentType | ||||||
| ) { | ) { | ||||||
|     fun <T> Route.includeWebsocketHandling( |     fun <T> Route.includeWebsocketHandling( | ||||||
|         suburl: String, |         suburl: String, | ||||||
|         flow: Flow<T>, |         flow: Flow<T>, | ||||||
|         serializer: SerializationStrategy<T> |         serializer: SerializationStrategy<T>, | ||||||
|     ) = includeWebsocketHandling(suburl, flow, serializer, serialFormat) |         protocol: URLProtocol = URLProtocol.WS, | ||||||
|  |         filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null | ||||||
|  |     ) = includeWebsocketHandling(suburl, flow, serializer, serialFormat, protocol, filter) | ||||||
|  |  | ||||||
|     suspend fun <T> PipelineContext<*, ApplicationCall>.unianswer( |     suspend fun <T> PipelineContext<*, ApplicationCall>.unianswer( | ||||||
|         answerSerializer: SerializationStrategy<T>, |         answerSerializer: SerializationStrategy<T>, | ||||||
| @@ -81,6 +92,11 @@ class UnifiedRouter( | |||||||
|             call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field") |             call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field") | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         val default | ||||||
|  |             get() = defaultUnifiedRouter | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| val defaultUnifiedRouter = UnifiedRouter() | val defaultUnifiedRouter = UnifiedRouter() | ||||||
| @@ -104,6 +120,139 @@ suspend fun <T> ApplicationCall.uniload( | |||||||
|     ) |     ) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | suspend fun ApplicationCall.uniloadMultipart( | ||||||
|  |     onFormItem: (PartData.FormItem) -> Unit = {}, | ||||||
|  |     onCustomFileItem: (PartData.FileItem) -> Unit = {}, | ||||||
|  |     onBinaryContent: (PartData.BinaryItem) -> Unit = {} | ||||||
|  | ) = safely { | ||||||
|  |     val multipartData = receiveMultipart() | ||||||
|  |  | ||||||
|  |     var resultInput: Input? = null | ||||||
|  |  | ||||||
|  |     multipartData.forEachPart { | ||||||
|  |         when (it) { | ||||||
|  |             is PartData.FormItem -> onFormItem(it) | ||||||
|  |             is PartData.FileItem -> { | ||||||
|  |                 when (it.name) { | ||||||
|  |                     "bytes" -> resultInput = it.provider() | ||||||
|  |                     else -> onCustomFileItem(it) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             is PartData.BinaryItem -> onBinaryContent(it) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     resultInput ?: error("Bytes has not been received") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | suspend fun <T> ApplicationCall.uniloadMultipart( | ||||||
|  |     deserializer: DeserializationStrategy<T>, | ||||||
|  |     onFormItem: (PartData.FormItem) -> Unit = {}, | ||||||
|  |     onCustomFileItem: (PartData.FileItem) -> Unit = {}, | ||||||
|  |     onBinaryContent: (PartData.BinaryItem) -> Unit = {} | ||||||
|  | ): Pair<Input, T> { | ||||||
|  |     var data: Optional<T>? = null | ||||||
|  |     val resultInput = uniloadMultipart( | ||||||
|  |         onFormItem, | ||||||
|  |         { | ||||||
|  |             if (it.name == "data") { | ||||||
|  |                 data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional | ||||||
|  |             } else { | ||||||
|  |                 onCustomFileItem(it) | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         onBinaryContent | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     val completeData = data ?: error("Data has not been received") | ||||||
|  |     return resultInput to (completeData.dataOrNull().let { it as T }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | suspend fun <T> ApplicationCall.uniloadMultipartFile( | ||||||
|  |     deserializer: DeserializationStrategy<T>, | ||||||
|  |     onFormItem: (PartData.FormItem) -> Unit = {}, | ||||||
|  |     onCustomFileItem: (PartData.FileItem) -> Unit = {}, | ||||||
|  |     onBinaryContent: (PartData.BinaryItem) -> Unit = {}, | ||||||
|  | ) = safely { | ||||||
|  |     val multipartData = receiveMultipart() | ||||||
|  |  | ||||||
|  |     var resultInput: MPPFile? = null | ||||||
|  |     var data: Optional<T>? = null | ||||||
|  |  | ||||||
|  |     multipartData.forEachPart { | ||||||
|  |         when (it) { | ||||||
|  |             is PartData.FormItem -> onFormItem(it) | ||||||
|  |             is PartData.FileItem -> { | ||||||
|  |                 when (it.name) { | ||||||
|  |                     "bytes" -> { | ||||||
|  |                         val name = FileName(it.originalFileName ?: error("File name is unknown for default part")) | ||||||
|  |                         resultInput = MPPFile.createTempFile( | ||||||
|  |                             name.nameWithoutExtension.let { | ||||||
|  |                                 var resultName = it | ||||||
|  |                                 while (resultName.length < 3) { | ||||||
|  |                                     resultName += "_" | ||||||
|  |                                 } | ||||||
|  |                                 resultName | ||||||
|  |                             }, | ||||||
|  |                             ".${name.extension}" | ||||||
|  |                         ).apply { | ||||||
|  |                             outputStream().use { fileStream -> | ||||||
|  |                                 it.provider().asStream().copyTo(fileStream) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     "data" -> data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional | ||||||
|  |                     else -> onCustomFileItem(it) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             is PartData.BinaryItem -> onBinaryContent(it) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val completeData = data ?: error("Data has not been received") | ||||||
|  |     (resultInput ?: error("Bytes has not been received")) to (completeData.dataOrNull().let { it as T }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | suspend fun ApplicationCall.uniloadMultipartFile( | ||||||
|  |     onFormItem: (PartData.FormItem) -> Unit = {}, | ||||||
|  |     onCustomFileItem: (PartData.FileItem) -> Unit = {}, | ||||||
|  |     onBinaryContent: (PartData.BinaryItem) -> Unit = {}, | ||||||
|  | ) = safely { | ||||||
|  |     val multipartData = receiveMultipart() | ||||||
|  |  | ||||||
|  |     var resultInput: MPPFile? = null | ||||||
|  |  | ||||||
|  |     multipartData.forEachPart { | ||||||
|  |         when (it) { | ||||||
|  |             is PartData.FormItem -> onFormItem(it) | ||||||
|  |             is PartData.FileItem -> { | ||||||
|  |                 if (it.name == "bytes") { | ||||||
|  |                     val name = FileName(it.originalFileName ?: error("File name is unknown for default part")) | ||||||
|  |                     resultInput = MPPFile.createTempFile( | ||||||
|  |                         name.nameWithoutExtension.let { | ||||||
|  |                             var resultName = it | ||||||
|  |                             while (resultName.length < 3) { | ||||||
|  |                                 resultName += "_" | ||||||
|  |                             } | ||||||
|  |                             resultName | ||||||
|  |                         }, | ||||||
|  |                         ".${name.extension}" | ||||||
|  |                     ).apply { | ||||||
|  |                         outputStream().use { fileStream -> | ||||||
|  |                             it.provider().asStream().copyTo(fileStream) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     onCustomFileItem(it) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             is PartData.BinaryItem -> onBinaryContent(it) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     resultInput ?: error("Bytes has not been received") | ||||||
|  | } | ||||||
|  |  | ||||||
| suspend fun ApplicationCall.getParameterOrSendError( | suspend fun ApplicationCall.getParameterOrSendError( | ||||||
|     field: String |     field: String | ||||||
| ) = parameters[field].also { | ) = parameters[field].also { | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| package dev.inmo.micro_utils.ktor.server | package dev.inmo.micro_utils.ktor.server | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator | ||||||
| import io.ktor.application.Application | import io.ktor.application.Application | ||||||
| import io.ktor.server.cio.CIO | import io.ktor.server.cio.CIO | ||||||
| import io.ktor.server.engine.* | import io.ktor.server.engine.* | ||||||
| @@ -31,3 +32,27 @@ fun createKtorServer( | |||||||
|     port: Int = Random.nextInt(1024, 65535), |     port: Int = Random.nextInt(1024, 65535), | ||||||
|     block: Application.() -> Unit |     block: Application.() -> Unit | ||||||
| ): ApplicationEngine = createKtorServer(CIO, host, port, block) | ): ApplicationEngine = createKtorServer(CIO, host, port, block) | ||||||
|  |  | ||||||
|  | fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration> createKtorServer( | ||||||
|  |     engine: ApplicationEngineFactory<TEngine, TConfiguration>, | ||||||
|  |     host: String = "localhost", | ||||||
|  |     port: Int = Random.nextInt(1024, 65535), | ||||||
|  |     configurators: List<KtorApplicationConfigurator> | ||||||
|  | ): TEngine = createKtorServer( | ||||||
|  |     engine, | ||||||
|  |     host, | ||||||
|  |     port | ||||||
|  | ) { | ||||||
|  |     configurators.forEach { it.apply { configure() } } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Create server with [CIO] server engine without starting of it | ||||||
|  |  * | ||||||
|  |  * @see ApplicationEngine.start | ||||||
|  |  */ | ||||||
|  | fun createKtorServer( | ||||||
|  |     host: String = "localhost", | ||||||
|  |     port: Int = Random.nextInt(1024, 65535), | ||||||
|  |     configurators: List<KtorApplicationConfigurator> | ||||||
|  | ): ApplicationEngine = createKtorServer(CIO, host, port, configurators) | ||||||
|   | |||||||
| @@ -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) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -8,7 +8,7 @@ import kotlinx.serialization.Contextual | |||||||
| data class ApplicationCachingHeadersConfigurator( | data class ApplicationCachingHeadersConfigurator( | ||||||
|     private val elements: List<@Contextual Element> |     private val elements: List<@Contextual Element> | ||||||
| ) : KtorApplicationConfigurator { | ) : KtorApplicationConfigurator { | ||||||
|     interface Element { operator fun CachingHeaders.Configuration.invoke() } |     fun interface Element { operator fun CachingHeaders.Configuration.invoke() } | ||||||
|  |  | ||||||
|     override fun Application.configure() { |     override fun Application.configure() { | ||||||
|         install(CachingHeaders) { |         install(CachingHeaders) { | ||||||
|   | |||||||
| @@ -10,17 +10,18 @@ import kotlinx.serialization.Serializable | |||||||
| class ApplicationRoutingConfigurator( | class ApplicationRoutingConfigurator( | ||||||
|     private val elements: List<@Contextual Element> |     private val elements: List<@Contextual Element> | ||||||
| ) : KtorApplicationConfigurator { | ) : KtorApplicationConfigurator { | ||||||
|     interface Element { operator fun Route.invoke() } |     fun interface Element { operator fun Route.invoke() } | ||||||
|  |     private val rootInstaller = Element { | ||||||
|  |         elements.forEach { | ||||||
|  |             it.apply { invoke() } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     override fun Application.configure() { |     override fun Application.configure() { | ||||||
|         try { |         featureOrNull(Routing) ?.apply { | ||||||
|             feature(Routing) |             rootInstaller.apply { invoke() } | ||||||
|         } catch (e: IllegalStateException) { |         } ?: install(Routing) { | ||||||
|             install(Routing) { |             rootInstaller.apply { invoke() } | ||||||
|                 elements.forEach { |  | ||||||
|                     it.apply { invoke() } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ import kotlinx.serialization.Contextual | |||||||
| class ApplicationSessionsConfigurator( | class ApplicationSessionsConfigurator( | ||||||
|     private val elements: List<@Contextual Element> |     private val elements: List<@Contextual Element> | ||||||
| ) : KtorApplicationConfigurator { | ) : KtorApplicationConfigurator { | ||||||
|     interface Element { operator fun Sessions.Configuration.invoke() } |     fun interface Element { operator fun Sessions.Configuration.invoke() } | ||||||
|  |  | ||||||
|     override fun Application.configure() { |     override fun Application.configure() { | ||||||
|         install(Sessions) { |         install(Sessions) { | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ import kotlinx.serialization.Contextual | |||||||
| class StatusPagesConfigurator( | class StatusPagesConfigurator( | ||||||
|     private val elements: List<@Contextual Element> |     private val elements: List<@Contextual Element> | ||||||
| ) : KtorApplicationConfigurator { | ) : KtorApplicationConfigurator { | ||||||
|     interface Element { operator fun StatusPages.Configuration.invoke() } |     fun interface Element { operator fun StatusPages.Configuration.invoke() } | ||||||
|  |  | ||||||
|     override fun Application.configure() { |     override fun Application.configure() { | ||||||
|         install(StatusPages) { |         install(StatusPages) { | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								language_codes/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								language_codes/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | plugins { | ||||||
|  |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|  |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "com.android.library" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$mppProjectWithSerializationPresetPath" | ||||||
							
								
								
									
										31
									
								
								language_codes/generator/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								language_codes/generator/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | buildscript { | ||||||
|  |     repositories { | ||||||
|  |         mavenCentral() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     dependencies { | ||||||
|  |         classpath libs.buildscript.kt.gradle | ||||||
|  |         classpath libs.buildscript.kt.serialization | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | plugins { | ||||||
|  |     id 'org.jetbrains.kotlin.jvm' | ||||||
|  |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "application" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | dependencies { | ||||||
|  |     implementation libs.kt.stdlib | ||||||
|  |     implementation libs.kt.serialization | ||||||
|  |  | ||||||
|  |     implementation libs.ktor.client | ||||||
|  |     implementation libs.ktor.client.java | ||||||
|  | } | ||||||
|  |  | ||||||
|  | mainClassName="MainKt" | ||||||
|  |  | ||||||
|  | java { | ||||||
|  |     sourceCompatibility = JavaVersion.VERSION_1_8 | ||||||
|  |     targetCompatibility = JavaVersion.VERSION_1_8 | ||||||
|  | } | ||||||
							
								
								
									
										214
									
								
								language_codes/generator/src/main/kotlin/Main.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								language_codes/generator/src/main/kotlin/Main.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,214 @@ | |||||||
|  | import io.ktor.client.HttpClient | ||||||
|  | import io.ktor.client.request.get | ||||||
|  | import kotlinx.serialization.SerialName | ||||||
|  | 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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | private const val baseClassName = "IetfLanguageCode" | ||||||
|  | private const val unknownBaseClassName = "Unknown$baseClassName" | ||||||
|  | private const val baseClassSerializerName = "IetfLanguageCodeSerializer" | ||||||
|  | private const val baseClassSerializerAnnotationName = "@Serializable(${baseClassSerializerName}::class)" | ||||||
|  |  | ||||||
|  | @Serializable | ||||||
|  | private data class LanguageCode( | ||||||
|  |     @SerialName("alpha2") | ||||||
|  |     val tag: String, | ||||||
|  |     @SerialName("English") | ||||||
|  |     val title: String | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | fun String.adaptAsTitle() = if (first().isDigit()) { | ||||||
|  |     "L$this" | ||||||
|  | } else { | ||||||
|  |     this | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun String.normalized() = Normalizer.normalize(this, Normalizer.Form.NFD).replace(Regex("[^\\p{ASCII}]"), "") | ||||||
|  |  | ||||||
|  | @Serializable | ||||||
|  | private data class LanguageCodeWithTag( | ||||||
|  |     @SerialName("langType") | ||||||
|  |     val tag: String, | ||||||
|  |     @SerialName("lang") | ||||||
|  |     val withSubtag: String | ||||||
|  | ) { | ||||||
|  |     val partWithoutTag: String | ||||||
|  |         get() { | ||||||
|  |             return withSubtag.substring( | ||||||
|  |                 withSubtag.indexOf("-") + 1, withSubtag.length | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     val middleTag | ||||||
|  |         get() = if (partWithoutTag.contains("-")) { | ||||||
|  |             partWithoutTag.substring(0, partWithoutTag.indexOf("-")) | ||||||
|  |         } else { | ||||||
|  |             null | ||||||
|  |         } | ||||||
|  |     val middleTagTitle | ||||||
|  |         get() = middleTag ?.adaptAsTitle() ?: partWithoutTag.adaptAsTitle() | ||||||
|  |     val subtag: String | ||||||
|  |         get() = middleTag ?: partWithoutTag | ||||||
|  |     val endTag | ||||||
|  |         get() = if (partWithoutTag.contains("-")) { | ||||||
|  |             partWithoutTag.substring(partWithoutTag.indexOf("-") + 1, partWithoutTag.length) | ||||||
|  |         } else { | ||||||
|  |             null | ||||||
|  |         } | ||||||
|  |     val endTagAsTitle | ||||||
|  |         get() = endTag ?.adaptAsTitle() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | data class Tag( | ||||||
|  |     val title: String, | ||||||
|  |     val tag: String, | ||||||
|  |     val subtags: List<Tag> | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | private fun printLanguageCodeAndTags( | ||||||
|  |     tag: Tag, | ||||||
|  |     parent: Tag? = null, | ||||||
|  |     indents: String = "    " | ||||||
|  | ): String = if (tag.subtags.isEmpty()) { | ||||||
|  | """${indents}${baseClassSerializerAnnotationName} | ||||||
|  | ${indents}object ${tag.title} : ${parent ?.title ?: baseClassName}() { override val code: String = "${tag.tag}" }""" | ||||||
|  | } else { | ||||||
|  | """ | ||||||
|  | ${indents}${baseClassSerializerAnnotationName} | ||||||
|  | ${indents}sealed class ${tag.title} : ${parent ?.title ?: baseClassName}() { | ||||||
|  | ${indents}    override val code: String = "${tag.tag}" | ||||||
|  |  | ||||||
|  | ${tag.subtags.joinToString("\n") { printLanguageCodeAndTags(it, tag, "${indents}    ") }} | ||||||
|  |  | ||||||
|  | ${indents}    ${baseClassSerializerAnnotationName} | ||||||
|  | ${indents}    companion object : ${tag.title}() | ||||||
|  | ${indents}} | ||||||
|  | """ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun buildKtFileContent(tags: List<Tag>): String = """ | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This class has been automatically generated using | ||||||
|  |  * https://github.com/InsanusMokrassar/MicroUtils/tree/master/language_codes/generator . This generator uses | ||||||
|  |  * https://datahub.io/core/language-codes/ files (base and tags) and create the whole hierarchy using it. | ||||||
|  |  */ | ||||||
|  | ${baseClassSerializerAnnotationName} | ||||||
|  | sealed class $baseClassName { | ||||||
|  |     abstract val code: String | ||||||
|  |  | ||||||
|  | ${tags.joinToString("\n") { printLanguageCodeAndTags(it, indents = "    ") } } | ||||||
|  |  | ||||||
|  |     $baseClassSerializerAnnotationName | ||||||
|  |     data class $unknownBaseClassName (override val code: String) : $baseClassName() | ||||||
|  |  | ||||||
|  |     override fun toString() = code | ||||||
|  | } | ||||||
|  | """.trimIndent() | ||||||
|  |  | ||||||
|  | fun createStringConverterCode(tags: List<Tag>): String { | ||||||
|  |     fun createDeserializeVariantForTag( | ||||||
|  |         tag: Tag, | ||||||
|  |         pretitle: String = baseClassName, | ||||||
|  |         indents: String = "        " | ||||||
|  |     ): String { | ||||||
|  |         val currentTitle = "$pretitle.${tag.title}" | ||||||
|  |         return """${indents}$currentTitle.code -> $currentTitle${if (tag.subtags.isNotEmpty()) tag.subtags.joinToString("\n", "\n") { createDeserializeVariantForTag(it, currentTitle, indents) } else ""}""" | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return """fun String.as$baseClassName(): $baseClassName { | ||||||
|  |     return when (this) { | ||||||
|  | ${tags.joinToString("\n") { createDeserializeVariantForTag(it) }} | ||||||
|  |         else -> $baseClassName.${unknownBaseClassName}(this) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | fun convertTo$baseClassName(code: String) = code.as$baseClassName() | ||||||
|  | fun $baseClassName(code: String) = code.as$baseClassName() | ||||||
|  | """ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun createSerializerCode(tags: List<Tag>): String { | ||||||
|  |     return """import kotlinx.serialization.KSerializer | ||||||
|  | import kotlinx.serialization.builtins.serializer | ||||||
|  | import kotlinx.serialization.encoding.Decoder | ||||||
|  | import kotlinx.serialization.encoding.Encoder | ||||||
|  |  | ||||||
|  | object $baseClassSerializerName : KSerializer<$baseClassName> { | ||||||
|  |     override val descriptor = String.serializer().descriptor | ||||||
|  |  | ||||||
|  |     override fun deserialize(decoder: Decoder): $baseClassName { | ||||||
|  |         return $baseClassName(decoder.decodeString()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun serialize(encoder: Encoder, value: IetfLanguageCode) { | ||||||
|  |         encoder.encodeString(value.code) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | """ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | suspend fun main(vararg args: String) { | ||||||
|  |     val outputFolder = args.firstOrNull() ?.let { File(it) } | ||||||
|  |     outputFolder ?.mkdirs() | ||||||
|  |     val ietfLanguageCodesLink = "https://datahub.io/core/language-codes/r/language-codes.json" | ||||||
|  |     val ietfLanguageCodesAdditionalTagsLink = "https://datahub.io/core/language-codes/r/ietf-language-tags.json" | ||||||
|  |  | ||||||
|  |     val client = HttpClient() | ||||||
|  |  | ||||||
|  |     val ietfLanguageCodes = json.decodeFromString( | ||||||
|  |         ListSerializer(LanguageCode.serializer()), | ||||||
|  |         client.get(ietfLanguageCodesLink) | ||||||
|  |     ).map { | ||||||
|  |         it.copy( | ||||||
|  |             title = it.title | ||||||
|  |                 .replace(Regex("[;,()-]"), "") | ||||||
|  |                 .split(" ") | ||||||
|  |                 .joinToString("") { "${it.first().uppercase()}${it.substring(1)}" } | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |     val ietfLanguageCodesWithTagsMap = json.decodeFromString( | ||||||
|  |         ListSerializer(LanguageCodeWithTag.serializer()), | ||||||
|  |         client.get(ietfLanguageCodesAdditionalTagsLink) | ||||||
|  |     ).filter { it.withSubtag != it.tag }.groupBy { it.tag } | ||||||
|  |  | ||||||
|  |     val tags = ietfLanguageCodes.map { | ||||||
|  |         val unformattedSubtags = ietfLanguageCodesWithTagsMap[it.tag] ?: emptyList() | ||||||
|  |         val threeLevelTags = unformattedSubtags.filter { it.endTag != null }.groupBy { it.middleTag } | ||||||
|  |         val subtags = unformattedSubtags.mapNotNull { | ||||||
|  |             if (it.endTag == null) { | ||||||
|  |                 val currentSubtags = (threeLevelTags[it.subtag] ?: emptyList()).map { | ||||||
|  |                     Tag(it.endTagAsTitle!!.normalized(), it.withSubtag, emptyList()) | ||||||
|  |                 } | ||||||
|  |                 Tag(it.middleTagTitle.normalized(), it.withSubtag, currentSubtags) | ||||||
|  |             } else { | ||||||
|  |                 null | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         Tag(it.title.normalized(), it.tag, subtags) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     File(outputFolder, "LanguageCodes.kt").apply { | ||||||
|  |         delete() | ||||||
|  |         createNewFile() | ||||||
|  |         writeText(buildKtFileContent(tags)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     File(outputFolder, "StringToLanguageCodes.kt").apply { | ||||||
|  |         delete() | ||||||
|  |         createNewFile() | ||||||
|  |         writeText(createStringConverterCode(tags)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     File(outputFolder, "$baseClassSerializerName.kt").apply { | ||||||
|  |         delete() | ||||||
|  |         createNewFile() | ||||||
|  |         writeText(createSerializerCode(tags)) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | package dev.inmo.micro_utils.language_codes | ||||||
|  |  | ||||||
|  | import kotlinx.serialization.KSerializer | ||||||
|  | import kotlinx.serialization.builtins.serializer | ||||||
|  | import kotlinx.serialization.encoding.Decoder | ||||||
|  | import kotlinx.serialization.encoding.Encoder | ||||||
|  |  | ||||||
|  | object IetfLanguageCodeSerializer : KSerializer<IetfLanguageCode> { | ||||||
|  |     override val descriptor = String.serializer().descriptor | ||||||
|  |  | ||||||
|  |     override fun deserialize(decoder: Decoder): IetfLanguageCode { | ||||||
|  |         return IetfLanguageCode(decoder.decodeString()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun serialize(encoder: Encoder, value: IetfLanguageCode) { | ||||||
|  |         encoder.encodeString(value.code) | ||||||
|  |     } | ||||||
|  | } | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -0,0 +1,671 @@ | |||||||
|  | package dev.inmo.micro_utils.language_codes | ||||||
|  |  | ||||||
|  | fun String.asIetfLanguageCode(): IetfLanguageCode { | ||||||
|  |     return when (this) { | ||||||
|  |         IetfLanguageCode.Afar.code -> IetfLanguageCode.Afar | ||||||
|  |         IetfLanguageCode.Abkhazian.code -> IetfLanguageCode.Abkhazian | ||||||
|  |         IetfLanguageCode.Avestan.code -> IetfLanguageCode.Avestan | ||||||
|  |         IetfLanguageCode.Afrikaans.code -> IetfLanguageCode.Afrikaans | ||||||
|  |         IetfLanguageCode.Afrikaans.NA.code -> IetfLanguageCode.Afrikaans.NA | ||||||
|  |         IetfLanguageCode.Afrikaans.ZA.code -> IetfLanguageCode.Afrikaans.ZA | ||||||
|  |         IetfLanguageCode.Akan.code -> IetfLanguageCode.Akan | ||||||
|  |         IetfLanguageCode.Akan.GH.code -> IetfLanguageCode.Akan.GH | ||||||
|  |         IetfLanguageCode.Amharic.code -> IetfLanguageCode.Amharic | ||||||
|  |         IetfLanguageCode.Amharic.ET.code -> IetfLanguageCode.Amharic.ET | ||||||
|  |         IetfLanguageCode.Aragonese.code -> IetfLanguageCode.Aragonese | ||||||
|  |         IetfLanguageCode.Arabic.code -> IetfLanguageCode.Arabic | ||||||
|  |         IetfLanguageCode.Arabic.L001.code -> IetfLanguageCode.Arabic.L001 | ||||||
|  |         IetfLanguageCode.Arabic.AE.code -> IetfLanguageCode.Arabic.AE | ||||||
|  |         IetfLanguageCode.Arabic.BH.code -> IetfLanguageCode.Arabic.BH | ||||||
|  |         IetfLanguageCode.Arabic.DJ.code -> IetfLanguageCode.Arabic.DJ | ||||||
|  |         IetfLanguageCode.Arabic.DZ.code -> IetfLanguageCode.Arabic.DZ | ||||||
|  |         IetfLanguageCode.Arabic.EG.code -> IetfLanguageCode.Arabic.EG | ||||||
|  |         IetfLanguageCode.Arabic.EH.code -> IetfLanguageCode.Arabic.EH | ||||||
|  |         IetfLanguageCode.Arabic.ER.code -> IetfLanguageCode.Arabic.ER | ||||||
|  |         IetfLanguageCode.Arabic.IL.code -> IetfLanguageCode.Arabic.IL | ||||||
|  |         IetfLanguageCode.Arabic.IQ.code -> IetfLanguageCode.Arabic.IQ | ||||||
|  |         IetfLanguageCode.Arabic.JO.code -> IetfLanguageCode.Arabic.JO | ||||||
|  |         IetfLanguageCode.Arabic.KM.code -> IetfLanguageCode.Arabic.KM | ||||||
|  |         IetfLanguageCode.Arabic.KW.code -> IetfLanguageCode.Arabic.KW | ||||||
|  |         IetfLanguageCode.Arabic.LB.code -> IetfLanguageCode.Arabic.LB | ||||||
|  |         IetfLanguageCode.Arabic.LY.code -> IetfLanguageCode.Arabic.LY | ||||||
|  |         IetfLanguageCode.Arabic.MA.code -> IetfLanguageCode.Arabic.MA | ||||||
|  |         IetfLanguageCode.Arabic.MR.code -> IetfLanguageCode.Arabic.MR | ||||||
|  |         IetfLanguageCode.Arabic.OM.code -> IetfLanguageCode.Arabic.OM | ||||||
|  |         IetfLanguageCode.Arabic.PS.code -> IetfLanguageCode.Arabic.PS | ||||||
|  |         IetfLanguageCode.Arabic.QA.code -> IetfLanguageCode.Arabic.QA | ||||||
|  |         IetfLanguageCode.Arabic.SA.code -> IetfLanguageCode.Arabic.SA | ||||||
|  |         IetfLanguageCode.Arabic.SD.code -> IetfLanguageCode.Arabic.SD | ||||||
|  |         IetfLanguageCode.Arabic.SO.code -> IetfLanguageCode.Arabic.SO | ||||||
|  |         IetfLanguageCode.Arabic.SS.code -> IetfLanguageCode.Arabic.SS | ||||||
|  |         IetfLanguageCode.Arabic.SY.code -> IetfLanguageCode.Arabic.SY | ||||||
|  |         IetfLanguageCode.Arabic.TD.code -> IetfLanguageCode.Arabic.TD | ||||||
|  |         IetfLanguageCode.Arabic.TN.code -> IetfLanguageCode.Arabic.TN | ||||||
|  |         IetfLanguageCode.Arabic.YE.code -> IetfLanguageCode.Arabic.YE | ||||||
|  |         IetfLanguageCode.Assamese.code -> IetfLanguageCode.Assamese | ||||||
|  |         IetfLanguageCode.Assamese.IN.code -> IetfLanguageCode.Assamese.IN | ||||||
|  |         IetfLanguageCode.Avaric.code -> IetfLanguageCode.Avaric | ||||||
|  |         IetfLanguageCode.Aymara.code -> IetfLanguageCode.Aymara | ||||||
|  |         IetfLanguageCode.Azerbaijani.code -> IetfLanguageCode.Azerbaijani | ||||||
|  |         IetfLanguageCode.Azerbaijani.Cyrl.code -> IetfLanguageCode.Azerbaijani.Cyrl | ||||||
|  |         IetfLanguageCode.Azerbaijani.Cyrl.AZ.code -> IetfLanguageCode.Azerbaijani.Cyrl.AZ | ||||||
|  |         IetfLanguageCode.Azerbaijani.Latn.code -> IetfLanguageCode.Azerbaijani.Latn | ||||||
|  |         IetfLanguageCode.Azerbaijani.Latn.AZ.code -> IetfLanguageCode.Azerbaijani.Latn.AZ | ||||||
|  |         IetfLanguageCode.Bashkir.code -> IetfLanguageCode.Bashkir | ||||||
|  |         IetfLanguageCode.Belarusian.code -> IetfLanguageCode.Belarusian | ||||||
|  |         IetfLanguageCode.Belarusian.BY.code -> IetfLanguageCode.Belarusian.BY | ||||||
|  |         IetfLanguageCode.Bulgarian.code -> IetfLanguageCode.Bulgarian | ||||||
|  |         IetfLanguageCode.Bulgarian.BG.code -> IetfLanguageCode.Bulgarian.BG | ||||||
|  |         IetfLanguageCode.BihariLanguages.code -> IetfLanguageCode.BihariLanguages | ||||||
|  |         IetfLanguageCode.Bislama.code -> IetfLanguageCode.Bislama | ||||||
|  |         IetfLanguageCode.Bambara.code -> IetfLanguageCode.Bambara | ||||||
|  |         IetfLanguageCode.Bambara.ML.code -> IetfLanguageCode.Bambara.ML | ||||||
|  |         IetfLanguageCode.Bengali.code -> IetfLanguageCode.Bengali | ||||||
|  |         IetfLanguageCode.Bengali.BD.code -> IetfLanguageCode.Bengali.BD | ||||||
|  |         IetfLanguageCode.Bengali.IN.code -> IetfLanguageCode.Bengali.IN | ||||||
|  |         IetfLanguageCode.Tibetan.code -> IetfLanguageCode.Tibetan | ||||||
|  |         IetfLanguageCode.Tibetan.CN.code -> IetfLanguageCode.Tibetan.CN | ||||||
|  |         IetfLanguageCode.Tibetan.IN.code -> IetfLanguageCode.Tibetan.IN | ||||||
|  |         IetfLanguageCode.Breton.code -> IetfLanguageCode.Breton | ||||||
|  |         IetfLanguageCode.Breton.FR.code -> IetfLanguageCode.Breton.FR | ||||||
|  |         IetfLanguageCode.Bosnian.code -> IetfLanguageCode.Bosnian | ||||||
|  |         IetfLanguageCode.Bosnian.Cyrl.code -> IetfLanguageCode.Bosnian.Cyrl | ||||||
|  |         IetfLanguageCode.Bosnian.Cyrl.BA.code -> IetfLanguageCode.Bosnian.Cyrl.BA | ||||||
|  |         IetfLanguageCode.Bosnian.Latn.code -> IetfLanguageCode.Bosnian.Latn | ||||||
|  |         IetfLanguageCode.Bosnian.Latn.BA.code -> IetfLanguageCode.Bosnian.Latn.BA | ||||||
|  |         IetfLanguageCode.CatalanValencian.code -> IetfLanguageCode.CatalanValencian | ||||||
|  |         IetfLanguageCode.CatalanValencian.AD.code -> IetfLanguageCode.CatalanValencian.AD | ||||||
|  |         IetfLanguageCode.CatalanValencian.ES.code -> IetfLanguageCode.CatalanValencian.ES | ||||||
|  |         IetfLanguageCode.CatalanValencian.ES.VALENCIA.code -> IetfLanguageCode.CatalanValencian.ES.VALENCIA | ||||||
|  |         IetfLanguageCode.CatalanValencian.FR.code -> IetfLanguageCode.CatalanValencian.FR | ||||||
|  |         IetfLanguageCode.CatalanValencian.IT.code -> IetfLanguageCode.CatalanValencian.IT | ||||||
|  |         IetfLanguageCode.Chechen.code -> IetfLanguageCode.Chechen | ||||||
|  |         IetfLanguageCode.Chechen.RU.code -> IetfLanguageCode.Chechen.RU | ||||||
|  |         IetfLanguageCode.Chamorro.code -> IetfLanguageCode.Chamorro | ||||||
|  |         IetfLanguageCode.Corsican.code -> IetfLanguageCode.Corsican | ||||||
|  |         IetfLanguageCode.Cree.code -> IetfLanguageCode.Cree | ||||||
|  |         IetfLanguageCode.Czech.code -> IetfLanguageCode.Czech | ||||||
|  |         IetfLanguageCode.Czech.CZ.code -> IetfLanguageCode.Czech.CZ | ||||||
|  |         IetfLanguageCode.ChurchSlavicOldSlavonicChurchSlavonicOldBulgarianOldChurchSlavonic.code -> IetfLanguageCode.ChurchSlavicOldSlavonicChurchSlavonicOldBulgarianOldChurchSlavonic | ||||||
|  |         IetfLanguageCode.ChurchSlavicOldSlavonicChurchSlavonicOldBulgarianOldChurchSlavonic.RU.code -> IetfLanguageCode.ChurchSlavicOldSlavonicChurchSlavonicOldBulgarianOldChurchSlavonic.RU | ||||||
|  |         IetfLanguageCode.Chuvash.code -> IetfLanguageCode.Chuvash | ||||||
|  |         IetfLanguageCode.Welsh.code -> IetfLanguageCode.Welsh | ||||||
|  |         IetfLanguageCode.Welsh.GB.code -> IetfLanguageCode.Welsh.GB | ||||||
|  |         IetfLanguageCode.Danish.code -> IetfLanguageCode.Danish | ||||||
|  |         IetfLanguageCode.Danish.DK.code -> IetfLanguageCode.Danish.DK | ||||||
|  |         IetfLanguageCode.Danish.GL.code -> IetfLanguageCode.Danish.GL | ||||||
|  |         IetfLanguageCode.German.code -> IetfLanguageCode.German | ||||||
|  |         IetfLanguageCode.German.AT.code -> IetfLanguageCode.German.AT | ||||||
|  |         IetfLanguageCode.German.BE.code -> IetfLanguageCode.German.BE | ||||||
|  |         IetfLanguageCode.German.CH.code -> IetfLanguageCode.German.CH | ||||||
|  |         IetfLanguageCode.German.DE.code -> IetfLanguageCode.German.DE | ||||||
|  |         IetfLanguageCode.German.IT.code -> IetfLanguageCode.German.IT | ||||||
|  |         IetfLanguageCode.German.LI.code -> IetfLanguageCode.German.LI | ||||||
|  |         IetfLanguageCode.German.LU.code -> IetfLanguageCode.German.LU | ||||||
|  |         IetfLanguageCode.DivehiDhivehiMaldivian.code -> IetfLanguageCode.DivehiDhivehiMaldivian | ||||||
|  |         IetfLanguageCode.Dzongkha.code -> IetfLanguageCode.Dzongkha | ||||||
|  |         IetfLanguageCode.Dzongkha.BT.code -> IetfLanguageCode.Dzongkha.BT | ||||||
|  |         IetfLanguageCode.Ewe.code -> IetfLanguageCode.Ewe | ||||||
|  |         IetfLanguageCode.Ewe.GH.code -> IetfLanguageCode.Ewe.GH | ||||||
|  |         IetfLanguageCode.Ewe.TG.code -> IetfLanguageCode.Ewe.TG | ||||||
|  |         IetfLanguageCode.GreekModern1453.code -> IetfLanguageCode.GreekModern1453 | ||||||
|  |         IetfLanguageCode.GreekModern1453.CY.code -> IetfLanguageCode.GreekModern1453.CY | ||||||
|  |         IetfLanguageCode.GreekModern1453.GR.code -> IetfLanguageCode.GreekModern1453.GR | ||||||
|  |         IetfLanguageCode.English.code -> IetfLanguageCode.English | ||||||
|  |         IetfLanguageCode.English.L001.code -> IetfLanguageCode.English.L001 | ||||||
|  |         IetfLanguageCode.English.L150.code -> IetfLanguageCode.English.L150 | ||||||
|  |         IetfLanguageCode.English.AE.code -> IetfLanguageCode.English.AE | ||||||
|  |         IetfLanguageCode.English.AG.code -> IetfLanguageCode.English.AG | ||||||
|  |         IetfLanguageCode.English.AI.code -> IetfLanguageCode.English.AI | ||||||
|  |         IetfLanguageCode.English.AS.code -> IetfLanguageCode.English.AS | ||||||
|  |         IetfLanguageCode.English.AT.code -> IetfLanguageCode.English.AT | ||||||
|  |         IetfLanguageCode.English.AU.code -> IetfLanguageCode.English.AU | ||||||
|  |         IetfLanguageCode.English.BB.code -> IetfLanguageCode.English.BB | ||||||
|  |         IetfLanguageCode.English.BE.code -> IetfLanguageCode.English.BE | ||||||
|  |         IetfLanguageCode.English.BI.code -> IetfLanguageCode.English.BI | ||||||
|  |         IetfLanguageCode.English.BM.code -> IetfLanguageCode.English.BM | ||||||
|  |         IetfLanguageCode.English.BS.code -> IetfLanguageCode.English.BS | ||||||
|  |         IetfLanguageCode.English.BW.code -> IetfLanguageCode.English.BW | ||||||
|  |         IetfLanguageCode.English.BZ.code -> IetfLanguageCode.English.BZ | ||||||
|  |         IetfLanguageCode.English.CA.code -> IetfLanguageCode.English.CA | ||||||
|  |         IetfLanguageCode.English.CC.code -> IetfLanguageCode.English.CC | ||||||
|  |         IetfLanguageCode.English.CH.code -> IetfLanguageCode.English.CH | ||||||
|  |         IetfLanguageCode.English.CK.code -> IetfLanguageCode.English.CK | ||||||
|  |         IetfLanguageCode.English.CM.code -> IetfLanguageCode.English.CM | ||||||
|  |         IetfLanguageCode.English.CX.code -> IetfLanguageCode.English.CX | ||||||
|  |         IetfLanguageCode.English.CY.code -> IetfLanguageCode.English.CY | ||||||
|  |         IetfLanguageCode.English.DE.code -> IetfLanguageCode.English.DE | ||||||
|  |         IetfLanguageCode.English.DG.code -> IetfLanguageCode.English.DG | ||||||
|  |         IetfLanguageCode.English.DK.code -> IetfLanguageCode.English.DK | ||||||
|  |         IetfLanguageCode.English.DM.code -> IetfLanguageCode.English.DM | ||||||
|  |         IetfLanguageCode.English.ER.code -> IetfLanguageCode.English.ER | ||||||
|  |         IetfLanguageCode.English.FI.code -> IetfLanguageCode.English.FI | ||||||
|  |         IetfLanguageCode.English.FJ.code -> IetfLanguageCode.English.FJ | ||||||
|  |         IetfLanguageCode.English.FK.code -> IetfLanguageCode.English.FK | ||||||
|  |         IetfLanguageCode.English.FM.code -> IetfLanguageCode.English.FM | ||||||
|  |         IetfLanguageCode.English.GB.code -> IetfLanguageCode.English.GB | ||||||
|  |         IetfLanguageCode.English.GD.code -> IetfLanguageCode.English.GD | ||||||
|  |         IetfLanguageCode.English.GG.code -> IetfLanguageCode.English.GG | ||||||
|  |         IetfLanguageCode.English.GH.code -> IetfLanguageCode.English.GH | ||||||
|  |         IetfLanguageCode.English.GI.code -> IetfLanguageCode.English.GI | ||||||
|  |         IetfLanguageCode.English.GM.code -> IetfLanguageCode.English.GM | ||||||
|  |         IetfLanguageCode.English.GU.code -> IetfLanguageCode.English.GU | ||||||
|  |         IetfLanguageCode.English.GY.code -> IetfLanguageCode.English.GY | ||||||
|  |         IetfLanguageCode.English.HK.code -> IetfLanguageCode.English.HK | ||||||
|  |         IetfLanguageCode.English.IE.code -> IetfLanguageCode.English.IE | ||||||
|  |         IetfLanguageCode.English.IL.code -> IetfLanguageCode.English.IL | ||||||
|  |         IetfLanguageCode.English.IM.code -> IetfLanguageCode.English.IM | ||||||
|  |         IetfLanguageCode.English.IN.code -> IetfLanguageCode.English.IN | ||||||
|  |         IetfLanguageCode.English.IO.code -> IetfLanguageCode.English.IO | ||||||
|  |         IetfLanguageCode.English.JE.code -> IetfLanguageCode.English.JE | ||||||
|  |         IetfLanguageCode.English.JM.code -> IetfLanguageCode.English.JM | ||||||
|  |         IetfLanguageCode.English.KE.code -> IetfLanguageCode.English.KE | ||||||
|  |         IetfLanguageCode.English.KI.code -> IetfLanguageCode.English.KI | ||||||
|  |         IetfLanguageCode.English.KN.code -> IetfLanguageCode.English.KN | ||||||
|  |         IetfLanguageCode.English.KY.code -> IetfLanguageCode.English.KY | ||||||
|  |         IetfLanguageCode.English.LC.code -> IetfLanguageCode.English.LC | ||||||
|  |         IetfLanguageCode.English.LR.code -> IetfLanguageCode.English.LR | ||||||
|  |         IetfLanguageCode.English.LS.code -> IetfLanguageCode.English.LS | ||||||
|  |         IetfLanguageCode.English.MG.code -> IetfLanguageCode.English.MG | ||||||
|  |         IetfLanguageCode.English.MH.code -> IetfLanguageCode.English.MH | ||||||
|  |         IetfLanguageCode.English.MO.code -> IetfLanguageCode.English.MO | ||||||
|  |         IetfLanguageCode.English.MP.code -> IetfLanguageCode.English.MP | ||||||
|  |         IetfLanguageCode.English.MS.code -> IetfLanguageCode.English.MS | ||||||
|  |         IetfLanguageCode.English.MT.code -> IetfLanguageCode.English.MT | ||||||
|  |         IetfLanguageCode.English.MU.code -> IetfLanguageCode.English.MU | ||||||
|  |         IetfLanguageCode.English.MW.code -> IetfLanguageCode.English.MW | ||||||
|  |         IetfLanguageCode.English.MY.code -> IetfLanguageCode.English.MY | ||||||
|  |         IetfLanguageCode.English.NA.code -> IetfLanguageCode.English.NA | ||||||
|  |         IetfLanguageCode.English.NF.code -> IetfLanguageCode.English.NF | ||||||
|  |         IetfLanguageCode.English.NG.code -> IetfLanguageCode.English.NG | ||||||
|  |         IetfLanguageCode.English.NL.code -> IetfLanguageCode.English.NL | ||||||
|  |         IetfLanguageCode.English.NR.code -> IetfLanguageCode.English.NR | ||||||
|  |         IetfLanguageCode.English.NU.code -> IetfLanguageCode.English.NU | ||||||
|  |         IetfLanguageCode.English.NZ.code -> IetfLanguageCode.English.NZ | ||||||
|  |         IetfLanguageCode.English.PG.code -> IetfLanguageCode.English.PG | ||||||
|  |         IetfLanguageCode.English.PH.code -> IetfLanguageCode.English.PH | ||||||
|  |         IetfLanguageCode.English.PK.code -> IetfLanguageCode.English.PK | ||||||
|  |         IetfLanguageCode.English.PN.code -> IetfLanguageCode.English.PN | ||||||
|  |         IetfLanguageCode.English.PR.code -> IetfLanguageCode.English.PR | ||||||
|  |         IetfLanguageCode.English.PW.code -> IetfLanguageCode.English.PW | ||||||
|  |         IetfLanguageCode.English.RW.code -> IetfLanguageCode.English.RW | ||||||
|  |         IetfLanguageCode.English.SB.code -> IetfLanguageCode.English.SB | ||||||
|  |         IetfLanguageCode.English.SC.code -> IetfLanguageCode.English.SC | ||||||
|  |         IetfLanguageCode.English.SD.code -> IetfLanguageCode.English.SD | ||||||
|  |         IetfLanguageCode.English.SE.code -> IetfLanguageCode.English.SE | ||||||
|  |         IetfLanguageCode.English.SG.code -> IetfLanguageCode.English.SG | ||||||
|  |         IetfLanguageCode.English.SH.code -> IetfLanguageCode.English.SH | ||||||
|  |         IetfLanguageCode.English.SI.code -> IetfLanguageCode.English.SI | ||||||
|  |         IetfLanguageCode.English.SL.code -> IetfLanguageCode.English.SL | ||||||
|  |         IetfLanguageCode.English.SS.code -> IetfLanguageCode.English.SS | ||||||
|  |         IetfLanguageCode.English.SX.code -> IetfLanguageCode.English.SX | ||||||
|  |         IetfLanguageCode.English.SZ.code -> IetfLanguageCode.English.SZ | ||||||
|  |         IetfLanguageCode.English.TC.code -> IetfLanguageCode.English.TC | ||||||
|  |         IetfLanguageCode.English.TK.code -> IetfLanguageCode.English.TK | ||||||
|  |         IetfLanguageCode.English.TO.code -> IetfLanguageCode.English.TO | ||||||
|  |         IetfLanguageCode.English.TT.code -> IetfLanguageCode.English.TT | ||||||
|  |         IetfLanguageCode.English.TV.code -> IetfLanguageCode.English.TV | ||||||
|  |         IetfLanguageCode.English.TZ.code -> IetfLanguageCode.English.TZ | ||||||
|  |         IetfLanguageCode.English.UG.code -> IetfLanguageCode.English.UG | ||||||
|  |         IetfLanguageCode.English.UM.code -> IetfLanguageCode.English.UM | ||||||
|  |         IetfLanguageCode.English.US.code -> IetfLanguageCode.English.US | ||||||
|  |         IetfLanguageCode.English.US.POSIX.code -> IetfLanguageCode.English.US.POSIX | ||||||
|  |         IetfLanguageCode.English.VC.code -> IetfLanguageCode.English.VC | ||||||
|  |         IetfLanguageCode.English.VG.code -> IetfLanguageCode.English.VG | ||||||
|  |         IetfLanguageCode.English.VI.code -> IetfLanguageCode.English.VI | ||||||
|  |         IetfLanguageCode.English.VU.code -> IetfLanguageCode.English.VU | ||||||
|  |         IetfLanguageCode.English.WS.code -> IetfLanguageCode.English.WS | ||||||
|  |         IetfLanguageCode.English.ZA.code -> IetfLanguageCode.English.ZA | ||||||
|  |         IetfLanguageCode.English.ZM.code -> IetfLanguageCode.English.ZM | ||||||
|  |         IetfLanguageCode.English.ZW.code -> IetfLanguageCode.English.ZW | ||||||
|  |         IetfLanguageCode.Esperanto.code -> IetfLanguageCode.Esperanto | ||||||
|  |         IetfLanguageCode.Esperanto.L001.code -> IetfLanguageCode.Esperanto.L001 | ||||||
|  |         IetfLanguageCode.SpanishCastilian.code -> IetfLanguageCode.SpanishCastilian | ||||||
|  |         IetfLanguageCode.SpanishCastilian.L419.code -> IetfLanguageCode.SpanishCastilian.L419 | ||||||
|  |         IetfLanguageCode.SpanishCastilian.AR.code -> IetfLanguageCode.SpanishCastilian.AR | ||||||
|  |         IetfLanguageCode.SpanishCastilian.BO.code -> IetfLanguageCode.SpanishCastilian.BO | ||||||
|  |         IetfLanguageCode.SpanishCastilian.BR.code -> IetfLanguageCode.SpanishCastilian.BR | ||||||
|  |         IetfLanguageCode.SpanishCastilian.BZ.code -> IetfLanguageCode.SpanishCastilian.BZ | ||||||
|  |         IetfLanguageCode.SpanishCastilian.CL.code -> IetfLanguageCode.SpanishCastilian.CL | ||||||
|  |         IetfLanguageCode.SpanishCastilian.CO.code -> IetfLanguageCode.SpanishCastilian.CO | ||||||
|  |         IetfLanguageCode.SpanishCastilian.CR.code -> IetfLanguageCode.SpanishCastilian.CR | ||||||
|  |         IetfLanguageCode.SpanishCastilian.CU.code -> IetfLanguageCode.SpanishCastilian.CU | ||||||
|  |         IetfLanguageCode.SpanishCastilian.DO.code -> IetfLanguageCode.SpanishCastilian.DO | ||||||
|  |         IetfLanguageCode.SpanishCastilian.EA.code -> IetfLanguageCode.SpanishCastilian.EA | ||||||
|  |         IetfLanguageCode.SpanishCastilian.EC.code -> IetfLanguageCode.SpanishCastilian.EC | ||||||
|  |         IetfLanguageCode.SpanishCastilian.ES.code -> IetfLanguageCode.SpanishCastilian.ES | ||||||
|  |         IetfLanguageCode.SpanishCastilian.GQ.code -> IetfLanguageCode.SpanishCastilian.GQ | ||||||
|  |         IetfLanguageCode.SpanishCastilian.GT.code -> IetfLanguageCode.SpanishCastilian.GT | ||||||
|  |         IetfLanguageCode.SpanishCastilian.HN.code -> IetfLanguageCode.SpanishCastilian.HN | ||||||
|  |         IetfLanguageCode.SpanishCastilian.IC.code -> IetfLanguageCode.SpanishCastilian.IC | ||||||
|  |         IetfLanguageCode.SpanishCastilian.MX.code -> IetfLanguageCode.SpanishCastilian.MX | ||||||
|  |         IetfLanguageCode.SpanishCastilian.NI.code -> IetfLanguageCode.SpanishCastilian.NI | ||||||
|  |         IetfLanguageCode.SpanishCastilian.PA.code -> IetfLanguageCode.SpanishCastilian.PA | ||||||
|  |         IetfLanguageCode.SpanishCastilian.PE.code -> IetfLanguageCode.SpanishCastilian.PE | ||||||
|  |         IetfLanguageCode.SpanishCastilian.PH.code -> IetfLanguageCode.SpanishCastilian.PH | ||||||
|  |         IetfLanguageCode.SpanishCastilian.PR.code -> IetfLanguageCode.SpanishCastilian.PR | ||||||
|  |         IetfLanguageCode.SpanishCastilian.PY.code -> IetfLanguageCode.SpanishCastilian.PY | ||||||
|  |         IetfLanguageCode.SpanishCastilian.SV.code -> IetfLanguageCode.SpanishCastilian.SV | ||||||
|  |         IetfLanguageCode.SpanishCastilian.US.code -> IetfLanguageCode.SpanishCastilian.US | ||||||
|  |         IetfLanguageCode.SpanishCastilian.UY.code -> IetfLanguageCode.SpanishCastilian.UY | ||||||
|  |         IetfLanguageCode.SpanishCastilian.VE.code -> IetfLanguageCode.SpanishCastilian.VE | ||||||
|  |         IetfLanguageCode.Estonian.code -> IetfLanguageCode.Estonian | ||||||
|  |         IetfLanguageCode.Estonian.EE.code -> IetfLanguageCode.Estonian.EE | ||||||
|  |         IetfLanguageCode.Basque.code -> IetfLanguageCode.Basque | ||||||
|  |         IetfLanguageCode.Basque.ES.code -> IetfLanguageCode.Basque.ES | ||||||
|  |         IetfLanguageCode.Persian.code -> IetfLanguageCode.Persian | ||||||
|  |         IetfLanguageCode.Persian.AF.code -> IetfLanguageCode.Persian.AF | ||||||
|  |         IetfLanguageCode.Persian.IR.code -> IetfLanguageCode.Persian.IR | ||||||
|  |         IetfLanguageCode.Fulah.code -> IetfLanguageCode.Fulah | ||||||
|  |         IetfLanguageCode.Fulah.Adlm.code -> IetfLanguageCode.Fulah.Adlm | ||||||
|  |         IetfLanguageCode.Fulah.Adlm.BF.code -> IetfLanguageCode.Fulah.Adlm.BF | ||||||
|  |         IetfLanguageCode.Fulah.Adlm.CM.code -> IetfLanguageCode.Fulah.Adlm.CM | ||||||
|  |         IetfLanguageCode.Fulah.Adlm.GH.code -> IetfLanguageCode.Fulah.Adlm.GH | ||||||
|  |         IetfLanguageCode.Fulah.Adlm.GM.code -> IetfLanguageCode.Fulah.Adlm.GM | ||||||
|  |         IetfLanguageCode.Fulah.Adlm.GN.code -> IetfLanguageCode.Fulah.Adlm.GN | ||||||
|  |         IetfLanguageCode.Fulah.Adlm.GW.code -> IetfLanguageCode.Fulah.Adlm.GW | ||||||
|  |         IetfLanguageCode.Fulah.Adlm.LR.code -> IetfLanguageCode.Fulah.Adlm.LR | ||||||
|  |         IetfLanguageCode.Fulah.Adlm.MR.code -> IetfLanguageCode.Fulah.Adlm.MR | ||||||
|  |         IetfLanguageCode.Fulah.Adlm.NE.code -> IetfLanguageCode.Fulah.Adlm.NE | ||||||
|  |         IetfLanguageCode.Fulah.Adlm.NG.code -> IetfLanguageCode.Fulah.Adlm.NG | ||||||
|  |         IetfLanguageCode.Fulah.Adlm.SL.code -> IetfLanguageCode.Fulah.Adlm.SL | ||||||
|  |         IetfLanguageCode.Fulah.Adlm.SN.code -> IetfLanguageCode.Fulah.Adlm.SN | ||||||
|  |         IetfLanguageCode.Fulah.Latn.code -> IetfLanguageCode.Fulah.Latn | ||||||
|  |         IetfLanguageCode.Fulah.Latn.BF.code -> IetfLanguageCode.Fulah.Latn.BF | ||||||
|  |         IetfLanguageCode.Fulah.Latn.CM.code -> IetfLanguageCode.Fulah.Latn.CM | ||||||
|  |         IetfLanguageCode.Fulah.Latn.GH.code -> IetfLanguageCode.Fulah.Latn.GH | ||||||
|  |         IetfLanguageCode.Fulah.Latn.GM.code -> IetfLanguageCode.Fulah.Latn.GM | ||||||
|  |         IetfLanguageCode.Fulah.Latn.GN.code -> IetfLanguageCode.Fulah.Latn.GN | ||||||
|  |         IetfLanguageCode.Fulah.Latn.GW.code -> IetfLanguageCode.Fulah.Latn.GW | ||||||
|  |         IetfLanguageCode.Fulah.Latn.LR.code -> IetfLanguageCode.Fulah.Latn.LR | ||||||
|  |         IetfLanguageCode.Fulah.Latn.MR.code -> IetfLanguageCode.Fulah.Latn.MR | ||||||
|  |         IetfLanguageCode.Fulah.Latn.NE.code -> IetfLanguageCode.Fulah.Latn.NE | ||||||
|  |         IetfLanguageCode.Fulah.Latn.NG.code -> IetfLanguageCode.Fulah.Latn.NG | ||||||
|  |         IetfLanguageCode.Fulah.Latn.SL.code -> IetfLanguageCode.Fulah.Latn.SL | ||||||
|  |         IetfLanguageCode.Fulah.Latn.SN.code -> IetfLanguageCode.Fulah.Latn.SN | ||||||
|  |         IetfLanguageCode.Finnish.code -> IetfLanguageCode.Finnish | ||||||
|  |         IetfLanguageCode.Finnish.FI.code -> IetfLanguageCode.Finnish.FI | ||||||
|  |         IetfLanguageCode.Fijian.code -> IetfLanguageCode.Fijian | ||||||
|  |         IetfLanguageCode.Faroese.code -> IetfLanguageCode.Faroese | ||||||
|  |         IetfLanguageCode.Faroese.DK.code -> IetfLanguageCode.Faroese.DK | ||||||
|  |         IetfLanguageCode.Faroese.FO.code -> IetfLanguageCode.Faroese.FO | ||||||
|  |         IetfLanguageCode.French.code -> IetfLanguageCode.French | ||||||
|  |         IetfLanguageCode.French.BE.code -> IetfLanguageCode.French.BE | ||||||
|  |         IetfLanguageCode.French.BF.code -> IetfLanguageCode.French.BF | ||||||
|  |         IetfLanguageCode.French.BI.code -> IetfLanguageCode.French.BI | ||||||
|  |         IetfLanguageCode.French.BJ.code -> IetfLanguageCode.French.BJ | ||||||
|  |         IetfLanguageCode.French.BL.code -> IetfLanguageCode.French.BL | ||||||
|  |         IetfLanguageCode.French.CA.code -> IetfLanguageCode.French.CA | ||||||
|  |         IetfLanguageCode.French.CD.code -> IetfLanguageCode.French.CD | ||||||
|  |         IetfLanguageCode.French.CF.code -> IetfLanguageCode.French.CF | ||||||
|  |         IetfLanguageCode.French.CG.code -> IetfLanguageCode.French.CG | ||||||
|  |         IetfLanguageCode.French.CH.code -> IetfLanguageCode.French.CH | ||||||
|  |         IetfLanguageCode.French.CI.code -> IetfLanguageCode.French.CI | ||||||
|  |         IetfLanguageCode.French.CM.code -> IetfLanguageCode.French.CM | ||||||
|  |         IetfLanguageCode.French.DJ.code -> IetfLanguageCode.French.DJ | ||||||
|  |         IetfLanguageCode.French.DZ.code -> IetfLanguageCode.French.DZ | ||||||
|  |         IetfLanguageCode.French.FR.code -> IetfLanguageCode.French.FR | ||||||
|  |         IetfLanguageCode.French.GA.code -> IetfLanguageCode.French.GA | ||||||
|  |         IetfLanguageCode.French.GF.code -> IetfLanguageCode.French.GF | ||||||
|  |         IetfLanguageCode.French.GN.code -> IetfLanguageCode.French.GN | ||||||
|  |         IetfLanguageCode.French.GP.code -> IetfLanguageCode.French.GP | ||||||
|  |         IetfLanguageCode.French.GQ.code -> IetfLanguageCode.French.GQ | ||||||
|  |         IetfLanguageCode.French.HT.code -> IetfLanguageCode.French.HT | ||||||
|  |         IetfLanguageCode.French.KM.code -> IetfLanguageCode.French.KM | ||||||
|  |         IetfLanguageCode.French.LU.code -> IetfLanguageCode.French.LU | ||||||
|  |         IetfLanguageCode.French.MA.code -> IetfLanguageCode.French.MA | ||||||
|  |         IetfLanguageCode.French.MC.code -> IetfLanguageCode.French.MC | ||||||
|  |         IetfLanguageCode.French.MF.code -> IetfLanguageCode.French.MF | ||||||
|  |         IetfLanguageCode.French.MG.code -> IetfLanguageCode.French.MG | ||||||
|  |         IetfLanguageCode.French.ML.code -> IetfLanguageCode.French.ML | ||||||
|  |         IetfLanguageCode.French.MQ.code -> IetfLanguageCode.French.MQ | ||||||
|  |         IetfLanguageCode.French.MR.code -> IetfLanguageCode.French.MR | ||||||
|  |         IetfLanguageCode.French.MU.code -> IetfLanguageCode.French.MU | ||||||
|  |         IetfLanguageCode.French.NC.code -> IetfLanguageCode.French.NC | ||||||
|  |         IetfLanguageCode.French.NE.code -> IetfLanguageCode.French.NE | ||||||
|  |         IetfLanguageCode.French.PF.code -> IetfLanguageCode.French.PF | ||||||
|  |         IetfLanguageCode.French.PM.code -> IetfLanguageCode.French.PM | ||||||
|  |         IetfLanguageCode.French.RE.code -> IetfLanguageCode.French.RE | ||||||
|  |         IetfLanguageCode.French.RW.code -> IetfLanguageCode.French.RW | ||||||
|  |         IetfLanguageCode.French.SC.code -> IetfLanguageCode.French.SC | ||||||
|  |         IetfLanguageCode.French.SN.code -> IetfLanguageCode.French.SN | ||||||
|  |         IetfLanguageCode.French.SY.code -> IetfLanguageCode.French.SY | ||||||
|  |         IetfLanguageCode.French.TD.code -> IetfLanguageCode.French.TD | ||||||
|  |         IetfLanguageCode.French.TG.code -> IetfLanguageCode.French.TG | ||||||
|  |         IetfLanguageCode.French.TN.code -> IetfLanguageCode.French.TN | ||||||
|  |         IetfLanguageCode.French.VU.code -> IetfLanguageCode.French.VU | ||||||
|  |         IetfLanguageCode.French.WF.code -> IetfLanguageCode.French.WF | ||||||
|  |         IetfLanguageCode.French.YT.code -> IetfLanguageCode.French.YT | ||||||
|  |         IetfLanguageCode.WesternFrisian.code -> IetfLanguageCode.WesternFrisian | ||||||
|  |         IetfLanguageCode.WesternFrisian.NL.code -> IetfLanguageCode.WesternFrisian.NL | ||||||
|  |         IetfLanguageCode.Irish.code -> IetfLanguageCode.Irish | ||||||
|  |         IetfLanguageCode.Irish.GB.code -> IetfLanguageCode.Irish.GB | ||||||
|  |         IetfLanguageCode.Irish.IE.code -> IetfLanguageCode.Irish.IE | ||||||
|  |         IetfLanguageCode.GaelicScottishGaelic.code -> IetfLanguageCode.GaelicScottishGaelic | ||||||
|  |         IetfLanguageCode.GaelicScottishGaelic.GB.code -> IetfLanguageCode.GaelicScottishGaelic.GB | ||||||
|  |         IetfLanguageCode.Galician.code -> IetfLanguageCode.Galician | ||||||
|  |         IetfLanguageCode.Galician.ES.code -> IetfLanguageCode.Galician.ES | ||||||
|  |         IetfLanguageCode.Guarani.code -> IetfLanguageCode.Guarani | ||||||
|  |         IetfLanguageCode.Gujarati.code -> IetfLanguageCode.Gujarati | ||||||
|  |         IetfLanguageCode.Gujarati.IN.code -> IetfLanguageCode.Gujarati.IN | ||||||
|  |         IetfLanguageCode.Manx.code -> IetfLanguageCode.Manx | ||||||
|  |         IetfLanguageCode.Manx.IM.code -> IetfLanguageCode.Manx.IM | ||||||
|  |         IetfLanguageCode.Hausa.code -> IetfLanguageCode.Hausa | ||||||
|  |         IetfLanguageCode.Hausa.GH.code -> IetfLanguageCode.Hausa.GH | ||||||
|  |         IetfLanguageCode.Hausa.NE.code -> IetfLanguageCode.Hausa.NE | ||||||
|  |         IetfLanguageCode.Hausa.NG.code -> IetfLanguageCode.Hausa.NG | ||||||
|  |         IetfLanguageCode.Hebrew.code -> IetfLanguageCode.Hebrew | ||||||
|  |         IetfLanguageCode.Hebrew.IL.code -> IetfLanguageCode.Hebrew.IL | ||||||
|  |         IetfLanguageCode.Hindi.code -> IetfLanguageCode.Hindi | ||||||
|  |         IetfLanguageCode.Hindi.IN.code -> IetfLanguageCode.Hindi.IN | ||||||
|  |         IetfLanguageCode.HiriMotu.code -> IetfLanguageCode.HiriMotu | ||||||
|  |         IetfLanguageCode.Croatian.code -> IetfLanguageCode.Croatian | ||||||
|  |         IetfLanguageCode.Croatian.BA.code -> IetfLanguageCode.Croatian.BA | ||||||
|  |         IetfLanguageCode.Croatian.HR.code -> IetfLanguageCode.Croatian.HR | ||||||
|  |         IetfLanguageCode.HaitianHaitianCreole.code -> IetfLanguageCode.HaitianHaitianCreole | ||||||
|  |         IetfLanguageCode.Hungarian.code -> IetfLanguageCode.Hungarian | ||||||
|  |         IetfLanguageCode.Hungarian.HU.code -> IetfLanguageCode.Hungarian.HU | ||||||
|  |         IetfLanguageCode.Armenian.code -> IetfLanguageCode.Armenian | ||||||
|  |         IetfLanguageCode.Armenian.AM.code -> IetfLanguageCode.Armenian.AM | ||||||
|  |         IetfLanguageCode.Herero.code -> IetfLanguageCode.Herero | ||||||
|  |         IetfLanguageCode.InterlinguaInternationalAuxiliaryLanguageAssociation.code -> IetfLanguageCode.InterlinguaInternationalAuxiliaryLanguageAssociation | ||||||
|  |         IetfLanguageCode.InterlinguaInternationalAuxiliaryLanguageAssociation.L001.code -> IetfLanguageCode.InterlinguaInternationalAuxiliaryLanguageAssociation.L001 | ||||||
|  |         IetfLanguageCode.Indonesian.code -> IetfLanguageCode.Indonesian | ||||||
|  |         IetfLanguageCode.Indonesian.ID.code -> IetfLanguageCode.Indonesian.ID | ||||||
|  |         IetfLanguageCode.InterlingueOccidental.code -> IetfLanguageCode.InterlingueOccidental | ||||||
|  |         IetfLanguageCode.Igbo.code -> IetfLanguageCode.Igbo | ||||||
|  |         IetfLanguageCode.Igbo.NG.code -> IetfLanguageCode.Igbo.NG | ||||||
|  |         IetfLanguageCode.SichuanYiNuosu.code -> IetfLanguageCode.SichuanYiNuosu | ||||||
|  |         IetfLanguageCode.SichuanYiNuosu.CN.code -> IetfLanguageCode.SichuanYiNuosu.CN | ||||||
|  |         IetfLanguageCode.Inupiaq.code -> IetfLanguageCode.Inupiaq | ||||||
|  |         IetfLanguageCode.Ido.code -> IetfLanguageCode.Ido | ||||||
|  |         IetfLanguageCode.Icelandic.code -> IetfLanguageCode.Icelandic | ||||||
|  |         IetfLanguageCode.Icelandic.IS.code -> IetfLanguageCode.Icelandic.IS | ||||||
|  |         IetfLanguageCode.Italian.code -> IetfLanguageCode.Italian | ||||||
|  |         IetfLanguageCode.Italian.CH.code -> IetfLanguageCode.Italian.CH | ||||||
|  |         IetfLanguageCode.Italian.IT.code -> IetfLanguageCode.Italian.IT | ||||||
|  |         IetfLanguageCode.Italian.SM.code -> IetfLanguageCode.Italian.SM | ||||||
|  |         IetfLanguageCode.Italian.VA.code -> IetfLanguageCode.Italian.VA | ||||||
|  |         IetfLanguageCode.Inuktitut.code -> IetfLanguageCode.Inuktitut | ||||||
|  |         IetfLanguageCode.Japanese.code -> IetfLanguageCode.Japanese | ||||||
|  |         IetfLanguageCode.Japanese.JP.code -> IetfLanguageCode.Japanese.JP | ||||||
|  |         IetfLanguageCode.Javanese.code -> IetfLanguageCode.Javanese | ||||||
|  |         IetfLanguageCode.Javanese.ID.code -> IetfLanguageCode.Javanese.ID | ||||||
|  |         IetfLanguageCode.Georgian.code -> IetfLanguageCode.Georgian | ||||||
|  |         IetfLanguageCode.Georgian.GE.code -> IetfLanguageCode.Georgian.GE | ||||||
|  |         IetfLanguageCode.Kongo.code -> IetfLanguageCode.Kongo | ||||||
|  |         IetfLanguageCode.KikuyuGikuyu.code -> IetfLanguageCode.KikuyuGikuyu | ||||||
|  |         IetfLanguageCode.KikuyuGikuyu.KE.code -> IetfLanguageCode.KikuyuGikuyu.KE | ||||||
|  |         IetfLanguageCode.KuanyamaKwanyama.code -> IetfLanguageCode.KuanyamaKwanyama | ||||||
|  |         IetfLanguageCode.Kazakh.code -> IetfLanguageCode.Kazakh | ||||||
|  |         IetfLanguageCode.Kazakh.KZ.code -> IetfLanguageCode.Kazakh.KZ | ||||||
|  |         IetfLanguageCode.KalaallisutGreenlandic.code -> IetfLanguageCode.KalaallisutGreenlandic | ||||||
|  |         IetfLanguageCode.KalaallisutGreenlandic.GL.code -> IetfLanguageCode.KalaallisutGreenlandic.GL | ||||||
|  |         IetfLanguageCode.CentralKhmer.code -> IetfLanguageCode.CentralKhmer | ||||||
|  |         IetfLanguageCode.CentralKhmer.KH.code -> IetfLanguageCode.CentralKhmer.KH | ||||||
|  |         IetfLanguageCode.Kannada.code -> IetfLanguageCode.Kannada | ||||||
|  |         IetfLanguageCode.Kannada.IN.code -> IetfLanguageCode.Kannada.IN | ||||||
|  |         IetfLanguageCode.Korean.code -> IetfLanguageCode.Korean | ||||||
|  |         IetfLanguageCode.Korean.KP.code -> IetfLanguageCode.Korean.KP | ||||||
|  |         IetfLanguageCode.Korean.KR.code -> IetfLanguageCode.Korean.KR | ||||||
|  |         IetfLanguageCode.Kanuri.code -> IetfLanguageCode.Kanuri | ||||||
|  |         IetfLanguageCode.Kashmiri.code -> IetfLanguageCode.Kashmiri | ||||||
|  |         IetfLanguageCode.Kashmiri.Arab.code -> IetfLanguageCode.Kashmiri.Arab | ||||||
|  |         IetfLanguageCode.Kashmiri.Arab.IN.code -> IetfLanguageCode.Kashmiri.Arab.IN | ||||||
|  |         IetfLanguageCode.Kurdish.code -> IetfLanguageCode.Kurdish | ||||||
|  |         IetfLanguageCode.Kurdish.TR.code -> IetfLanguageCode.Kurdish.TR | ||||||
|  |         IetfLanguageCode.Komi.code -> IetfLanguageCode.Komi | ||||||
|  |         IetfLanguageCode.Cornish.code -> IetfLanguageCode.Cornish | ||||||
|  |         IetfLanguageCode.Cornish.GB.code -> IetfLanguageCode.Cornish.GB | ||||||
|  |         IetfLanguageCode.KirghizKyrgyz.code -> IetfLanguageCode.KirghizKyrgyz | ||||||
|  |         IetfLanguageCode.KirghizKyrgyz.KG.code -> IetfLanguageCode.KirghizKyrgyz.KG | ||||||
|  |         IetfLanguageCode.Latin.code -> IetfLanguageCode.Latin | ||||||
|  |         IetfLanguageCode.LuxembourgishLetzeburgesch.code -> IetfLanguageCode.LuxembourgishLetzeburgesch | ||||||
|  |         IetfLanguageCode.LuxembourgishLetzeburgesch.LU.code -> IetfLanguageCode.LuxembourgishLetzeburgesch.LU | ||||||
|  |         IetfLanguageCode.Ganda.code -> IetfLanguageCode.Ganda | ||||||
|  |         IetfLanguageCode.Ganda.UG.code -> IetfLanguageCode.Ganda.UG | ||||||
|  |         IetfLanguageCode.LimburganLimburgerLimburgish.code -> IetfLanguageCode.LimburganLimburgerLimburgish | ||||||
|  |         IetfLanguageCode.Lingala.code -> IetfLanguageCode.Lingala | ||||||
|  |         IetfLanguageCode.Lingala.AO.code -> IetfLanguageCode.Lingala.AO | ||||||
|  |         IetfLanguageCode.Lingala.CD.code -> IetfLanguageCode.Lingala.CD | ||||||
|  |         IetfLanguageCode.Lingala.CF.code -> IetfLanguageCode.Lingala.CF | ||||||
|  |         IetfLanguageCode.Lingala.CG.code -> IetfLanguageCode.Lingala.CG | ||||||
|  |         IetfLanguageCode.Lao.code -> IetfLanguageCode.Lao | ||||||
|  |         IetfLanguageCode.Lao.LA.code -> IetfLanguageCode.Lao.LA | ||||||
|  |         IetfLanguageCode.Lithuanian.code -> IetfLanguageCode.Lithuanian | ||||||
|  |         IetfLanguageCode.Lithuanian.LT.code -> IetfLanguageCode.Lithuanian.LT | ||||||
|  |         IetfLanguageCode.LubaKatanga.code -> IetfLanguageCode.LubaKatanga | ||||||
|  |         IetfLanguageCode.LubaKatanga.CD.code -> IetfLanguageCode.LubaKatanga.CD | ||||||
|  |         IetfLanguageCode.Latvian.code -> IetfLanguageCode.Latvian | ||||||
|  |         IetfLanguageCode.Latvian.LV.code -> IetfLanguageCode.Latvian.LV | ||||||
|  |         IetfLanguageCode.Malagasy.code -> IetfLanguageCode.Malagasy | ||||||
|  |         IetfLanguageCode.Malagasy.MG.code -> IetfLanguageCode.Malagasy.MG | ||||||
|  |         IetfLanguageCode.Marshallese.code -> IetfLanguageCode.Marshallese | ||||||
|  |         IetfLanguageCode.Maori.code -> IetfLanguageCode.Maori | ||||||
|  |         IetfLanguageCode.Maori.NZ.code -> IetfLanguageCode.Maori.NZ | ||||||
|  |         IetfLanguageCode.Macedonian.code -> IetfLanguageCode.Macedonian | ||||||
|  |         IetfLanguageCode.Macedonian.MK.code -> IetfLanguageCode.Macedonian.MK | ||||||
|  |         IetfLanguageCode.Malayalam.code -> IetfLanguageCode.Malayalam | ||||||
|  |         IetfLanguageCode.Malayalam.IN.code -> IetfLanguageCode.Malayalam.IN | ||||||
|  |         IetfLanguageCode.Mongolian.code -> IetfLanguageCode.Mongolian | ||||||
|  |         IetfLanguageCode.Mongolian.MN.code -> IetfLanguageCode.Mongolian.MN | ||||||
|  |         IetfLanguageCode.Marathi.code -> IetfLanguageCode.Marathi | ||||||
|  |         IetfLanguageCode.Marathi.IN.code -> IetfLanguageCode.Marathi.IN | ||||||
|  |         IetfLanguageCode.Malay.code -> IetfLanguageCode.Malay | ||||||
|  |         IetfLanguageCode.Malay.BN.code -> IetfLanguageCode.Malay.BN | ||||||
|  |         IetfLanguageCode.Malay.ID.code -> IetfLanguageCode.Malay.ID | ||||||
|  |         IetfLanguageCode.Malay.MY.code -> IetfLanguageCode.Malay.MY | ||||||
|  |         IetfLanguageCode.Malay.SG.code -> IetfLanguageCode.Malay.SG | ||||||
|  |         IetfLanguageCode.Maltese.code -> IetfLanguageCode.Maltese | ||||||
|  |         IetfLanguageCode.Maltese.MT.code -> IetfLanguageCode.Maltese.MT | ||||||
|  |         IetfLanguageCode.Burmese.code -> IetfLanguageCode.Burmese | ||||||
|  |         IetfLanguageCode.Burmese.MM.code -> IetfLanguageCode.Burmese.MM | ||||||
|  |         IetfLanguageCode.Nauru.code -> IetfLanguageCode.Nauru | ||||||
|  |         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 | ||||||
|  |         IetfLanguageCode.Nepali.IN.code -> IetfLanguageCode.Nepali.IN | ||||||
|  |         IetfLanguageCode.Nepali.NP.code -> IetfLanguageCode.Nepali.NP | ||||||
|  |         IetfLanguageCode.Ndonga.code -> IetfLanguageCode.Ndonga | ||||||
|  |         IetfLanguageCode.DutchFlemish.code -> IetfLanguageCode.DutchFlemish | ||||||
|  |         IetfLanguageCode.DutchFlemish.AW.code -> IetfLanguageCode.DutchFlemish.AW | ||||||
|  |         IetfLanguageCode.DutchFlemish.BE.code -> IetfLanguageCode.DutchFlemish.BE | ||||||
|  |         IetfLanguageCode.DutchFlemish.BQ.code -> IetfLanguageCode.DutchFlemish.BQ | ||||||
|  |         IetfLanguageCode.DutchFlemish.CW.code -> IetfLanguageCode.DutchFlemish.CW | ||||||
|  |         IetfLanguageCode.DutchFlemish.NL.code -> IetfLanguageCode.DutchFlemish.NL | ||||||
|  |         IetfLanguageCode.DutchFlemish.SR.code -> IetfLanguageCode.DutchFlemish.SR | ||||||
|  |         IetfLanguageCode.DutchFlemish.SX.code -> IetfLanguageCode.DutchFlemish.SX | ||||||
|  |         IetfLanguageCode.NorwegianNynorskNynorskNorwegian.code -> IetfLanguageCode.NorwegianNynorskNynorskNorwegian | ||||||
|  |         IetfLanguageCode.NorwegianNynorskNynorskNorwegian.NO.code -> IetfLanguageCode.NorwegianNynorskNynorskNorwegian.NO | ||||||
|  |         IetfLanguageCode.Norwegian.code -> IetfLanguageCode.Norwegian | ||||||
|  |         IetfLanguageCode.NdebeleSouthSouthNdebele.code -> IetfLanguageCode.NdebeleSouthSouthNdebele | ||||||
|  |         IetfLanguageCode.NavajoNavaho.code -> IetfLanguageCode.NavajoNavaho | ||||||
|  |         IetfLanguageCode.ChichewaChewaNyanja.code -> IetfLanguageCode.ChichewaChewaNyanja | ||||||
|  |         IetfLanguageCode.OccitanPost1500.code -> IetfLanguageCode.OccitanPost1500 | ||||||
|  |         IetfLanguageCode.Ojibwa.code -> IetfLanguageCode.Ojibwa | ||||||
|  |         IetfLanguageCode.Oromo.code -> IetfLanguageCode.Oromo | ||||||
|  |         IetfLanguageCode.Oromo.ET.code -> IetfLanguageCode.Oromo.ET | ||||||
|  |         IetfLanguageCode.Oromo.KE.code -> IetfLanguageCode.Oromo.KE | ||||||
|  |         IetfLanguageCode.Oriya.code -> IetfLanguageCode.Oriya | ||||||
|  |         IetfLanguageCode.Oriya.IN.code -> IetfLanguageCode.Oriya.IN | ||||||
|  |         IetfLanguageCode.OssetianOssetic.code -> IetfLanguageCode.OssetianOssetic | ||||||
|  |         IetfLanguageCode.OssetianOssetic.GE.code -> IetfLanguageCode.OssetianOssetic.GE | ||||||
|  |         IetfLanguageCode.OssetianOssetic.RU.code -> IetfLanguageCode.OssetianOssetic.RU | ||||||
|  |         IetfLanguageCode.PanjabiPunjabi.code -> IetfLanguageCode.PanjabiPunjabi | ||||||
|  |         IetfLanguageCode.PanjabiPunjabi.Arab.code -> IetfLanguageCode.PanjabiPunjabi.Arab | ||||||
|  |         IetfLanguageCode.PanjabiPunjabi.Arab.PK.code -> IetfLanguageCode.PanjabiPunjabi.Arab.PK | ||||||
|  |         IetfLanguageCode.PanjabiPunjabi.Guru.code -> IetfLanguageCode.PanjabiPunjabi.Guru | ||||||
|  |         IetfLanguageCode.PanjabiPunjabi.Guru.IN.code -> IetfLanguageCode.PanjabiPunjabi.Guru.IN | ||||||
|  |         IetfLanguageCode.Pali.code -> IetfLanguageCode.Pali | ||||||
|  |         IetfLanguageCode.Polish.code -> IetfLanguageCode.Polish | ||||||
|  |         IetfLanguageCode.Polish.PL.code -> IetfLanguageCode.Polish.PL | ||||||
|  |         IetfLanguageCode.PushtoPashto.code -> IetfLanguageCode.PushtoPashto | ||||||
|  |         IetfLanguageCode.PushtoPashto.AF.code -> IetfLanguageCode.PushtoPashto.AF | ||||||
|  |         IetfLanguageCode.PushtoPashto.PK.code -> IetfLanguageCode.PushtoPashto.PK | ||||||
|  |         IetfLanguageCode.Portuguese.code -> IetfLanguageCode.Portuguese | ||||||
|  |         IetfLanguageCode.Portuguese.AO.code -> IetfLanguageCode.Portuguese.AO | ||||||
|  |         IetfLanguageCode.Portuguese.BR.code -> IetfLanguageCode.Portuguese.BR | ||||||
|  |         IetfLanguageCode.Portuguese.CH.code -> IetfLanguageCode.Portuguese.CH | ||||||
|  |         IetfLanguageCode.Portuguese.CV.code -> IetfLanguageCode.Portuguese.CV | ||||||
|  |         IetfLanguageCode.Portuguese.GQ.code -> IetfLanguageCode.Portuguese.GQ | ||||||
|  |         IetfLanguageCode.Portuguese.GW.code -> IetfLanguageCode.Portuguese.GW | ||||||
|  |         IetfLanguageCode.Portuguese.LU.code -> IetfLanguageCode.Portuguese.LU | ||||||
|  |         IetfLanguageCode.Portuguese.MO.code -> IetfLanguageCode.Portuguese.MO | ||||||
|  |         IetfLanguageCode.Portuguese.MZ.code -> IetfLanguageCode.Portuguese.MZ | ||||||
|  |         IetfLanguageCode.Portuguese.PT.code -> IetfLanguageCode.Portuguese.PT | ||||||
|  |         IetfLanguageCode.Portuguese.ST.code -> IetfLanguageCode.Portuguese.ST | ||||||
|  |         IetfLanguageCode.Portuguese.TL.code -> IetfLanguageCode.Portuguese.TL | ||||||
|  |         IetfLanguageCode.Quechua.code -> IetfLanguageCode.Quechua | ||||||
|  |         IetfLanguageCode.Quechua.BO.code -> IetfLanguageCode.Quechua.BO | ||||||
|  |         IetfLanguageCode.Quechua.EC.code -> IetfLanguageCode.Quechua.EC | ||||||
|  |         IetfLanguageCode.Quechua.PE.code -> IetfLanguageCode.Quechua.PE | ||||||
|  |         IetfLanguageCode.Romansh.code -> IetfLanguageCode.Romansh | ||||||
|  |         IetfLanguageCode.Romansh.CH.code -> IetfLanguageCode.Romansh.CH | ||||||
|  |         IetfLanguageCode.Rundi.code -> IetfLanguageCode.Rundi | ||||||
|  |         IetfLanguageCode.Rundi.BI.code -> IetfLanguageCode.Rundi.BI | ||||||
|  |         IetfLanguageCode.RomanianMoldavianMoldovan.code -> IetfLanguageCode.RomanianMoldavianMoldovan | ||||||
|  |         IetfLanguageCode.RomanianMoldavianMoldovan.MD.code -> IetfLanguageCode.RomanianMoldavianMoldovan.MD | ||||||
|  |         IetfLanguageCode.RomanianMoldavianMoldovan.RO.code -> IetfLanguageCode.RomanianMoldavianMoldovan.RO | ||||||
|  |         IetfLanguageCode.Russian.code -> IetfLanguageCode.Russian | ||||||
|  |         IetfLanguageCode.Russian.BY.code -> IetfLanguageCode.Russian.BY | ||||||
|  |         IetfLanguageCode.Russian.KG.code -> IetfLanguageCode.Russian.KG | ||||||
|  |         IetfLanguageCode.Russian.KZ.code -> IetfLanguageCode.Russian.KZ | ||||||
|  |         IetfLanguageCode.Russian.MD.code -> IetfLanguageCode.Russian.MD | ||||||
|  |         IetfLanguageCode.Russian.RU.code -> IetfLanguageCode.Russian.RU | ||||||
|  |         IetfLanguageCode.Russian.UA.code -> IetfLanguageCode.Russian.UA | ||||||
|  |         IetfLanguageCode.Kinyarwanda.code -> IetfLanguageCode.Kinyarwanda | ||||||
|  |         IetfLanguageCode.Kinyarwanda.RW.code -> IetfLanguageCode.Kinyarwanda.RW | ||||||
|  |         IetfLanguageCode.Sanskrit.code -> IetfLanguageCode.Sanskrit | ||||||
|  |         IetfLanguageCode.Sardinian.code -> IetfLanguageCode.Sardinian | ||||||
|  |         IetfLanguageCode.Sindhi.code -> IetfLanguageCode.Sindhi | ||||||
|  |         IetfLanguageCode.Sindhi.Arab.code -> IetfLanguageCode.Sindhi.Arab | ||||||
|  |         IetfLanguageCode.Sindhi.Arab.PK.code -> IetfLanguageCode.Sindhi.Arab.PK | ||||||
|  |         IetfLanguageCode.Sindhi.Deva.code -> IetfLanguageCode.Sindhi.Deva | ||||||
|  |         IetfLanguageCode.Sindhi.Deva.IN.code -> IetfLanguageCode.Sindhi.Deva.IN | ||||||
|  |         IetfLanguageCode.NorthernSami.code -> IetfLanguageCode.NorthernSami | ||||||
|  |         IetfLanguageCode.NorthernSami.FI.code -> IetfLanguageCode.NorthernSami.FI | ||||||
|  |         IetfLanguageCode.NorthernSami.NO.code -> IetfLanguageCode.NorthernSami.NO | ||||||
|  |         IetfLanguageCode.NorthernSami.SE.code -> IetfLanguageCode.NorthernSami.SE | ||||||
|  |         IetfLanguageCode.Sango.code -> IetfLanguageCode.Sango | ||||||
|  |         IetfLanguageCode.Sango.CF.code -> IetfLanguageCode.Sango.CF | ||||||
|  |         IetfLanguageCode.SinhalaSinhalese.code -> IetfLanguageCode.SinhalaSinhalese | ||||||
|  |         IetfLanguageCode.SinhalaSinhalese.LK.code -> IetfLanguageCode.SinhalaSinhalese.LK | ||||||
|  |         IetfLanguageCode.Slovak.code -> IetfLanguageCode.Slovak | ||||||
|  |         IetfLanguageCode.Slovak.SK.code -> IetfLanguageCode.Slovak.SK | ||||||
|  |         IetfLanguageCode.Slovenian.code -> IetfLanguageCode.Slovenian | ||||||
|  |         IetfLanguageCode.Slovenian.SI.code -> IetfLanguageCode.Slovenian.SI | ||||||
|  |         IetfLanguageCode.Samoan.code -> IetfLanguageCode.Samoan | ||||||
|  |         IetfLanguageCode.Shona.code -> IetfLanguageCode.Shona | ||||||
|  |         IetfLanguageCode.Shona.ZW.code -> IetfLanguageCode.Shona.ZW | ||||||
|  |         IetfLanguageCode.Somali.code -> IetfLanguageCode.Somali | ||||||
|  |         IetfLanguageCode.Somali.DJ.code -> IetfLanguageCode.Somali.DJ | ||||||
|  |         IetfLanguageCode.Somali.ET.code -> IetfLanguageCode.Somali.ET | ||||||
|  |         IetfLanguageCode.Somali.KE.code -> IetfLanguageCode.Somali.KE | ||||||
|  |         IetfLanguageCode.Somali.SO.code -> IetfLanguageCode.Somali.SO | ||||||
|  |         IetfLanguageCode.Albanian.code -> IetfLanguageCode.Albanian | ||||||
|  |         IetfLanguageCode.Albanian.AL.code -> IetfLanguageCode.Albanian.AL | ||||||
|  |         IetfLanguageCode.Albanian.MK.code -> IetfLanguageCode.Albanian.MK | ||||||
|  |         IetfLanguageCode.Albanian.XK.code -> IetfLanguageCode.Albanian.XK | ||||||
|  |         IetfLanguageCode.Serbian.code -> IetfLanguageCode.Serbian | ||||||
|  |         IetfLanguageCode.Serbian.Cyrl.code -> IetfLanguageCode.Serbian.Cyrl | ||||||
|  |         IetfLanguageCode.Serbian.Cyrl.BA.code -> IetfLanguageCode.Serbian.Cyrl.BA | ||||||
|  |         IetfLanguageCode.Serbian.Cyrl.ME.code -> IetfLanguageCode.Serbian.Cyrl.ME | ||||||
|  |         IetfLanguageCode.Serbian.Cyrl.RS.code -> IetfLanguageCode.Serbian.Cyrl.RS | ||||||
|  |         IetfLanguageCode.Serbian.Cyrl.XK.code -> IetfLanguageCode.Serbian.Cyrl.XK | ||||||
|  |         IetfLanguageCode.Serbian.Latn.code -> IetfLanguageCode.Serbian.Latn | ||||||
|  |         IetfLanguageCode.Serbian.Latn.BA.code -> IetfLanguageCode.Serbian.Latn.BA | ||||||
|  |         IetfLanguageCode.Serbian.Latn.ME.code -> IetfLanguageCode.Serbian.Latn.ME | ||||||
|  |         IetfLanguageCode.Serbian.Latn.RS.code -> IetfLanguageCode.Serbian.Latn.RS | ||||||
|  |         IetfLanguageCode.Serbian.Latn.XK.code -> IetfLanguageCode.Serbian.Latn.XK | ||||||
|  |         IetfLanguageCode.Swati.code -> IetfLanguageCode.Swati | ||||||
|  |         IetfLanguageCode.SothoSouthern.code -> IetfLanguageCode.SothoSouthern | ||||||
|  |         IetfLanguageCode.Sundanese.code -> IetfLanguageCode.Sundanese | ||||||
|  |         IetfLanguageCode.Sundanese.Latn.code -> IetfLanguageCode.Sundanese.Latn | ||||||
|  |         IetfLanguageCode.Sundanese.Latn.ID.code -> IetfLanguageCode.Sundanese.Latn.ID | ||||||
|  |         IetfLanguageCode.Swedish.code -> IetfLanguageCode.Swedish | ||||||
|  |         IetfLanguageCode.Swedish.AX.code -> IetfLanguageCode.Swedish.AX | ||||||
|  |         IetfLanguageCode.Swedish.FI.code -> IetfLanguageCode.Swedish.FI | ||||||
|  |         IetfLanguageCode.Swedish.SE.code -> IetfLanguageCode.Swedish.SE | ||||||
|  |         IetfLanguageCode.Swahili.code -> IetfLanguageCode.Swahili | ||||||
|  |         IetfLanguageCode.Swahili.CD.code -> IetfLanguageCode.Swahili.CD | ||||||
|  |         IetfLanguageCode.Swahili.KE.code -> IetfLanguageCode.Swahili.KE | ||||||
|  |         IetfLanguageCode.Swahili.TZ.code -> IetfLanguageCode.Swahili.TZ | ||||||
|  |         IetfLanguageCode.Swahili.UG.code -> IetfLanguageCode.Swahili.UG | ||||||
|  |         IetfLanguageCode.Tamil.code -> IetfLanguageCode.Tamil | ||||||
|  |         IetfLanguageCode.Tamil.IN.code -> IetfLanguageCode.Tamil.IN | ||||||
|  |         IetfLanguageCode.Tamil.LK.code -> IetfLanguageCode.Tamil.LK | ||||||
|  |         IetfLanguageCode.Tamil.MY.code -> IetfLanguageCode.Tamil.MY | ||||||
|  |         IetfLanguageCode.Tamil.SG.code -> IetfLanguageCode.Tamil.SG | ||||||
|  |         IetfLanguageCode.Telugu.code -> IetfLanguageCode.Telugu | ||||||
|  |         IetfLanguageCode.Telugu.IN.code -> IetfLanguageCode.Telugu.IN | ||||||
|  |         IetfLanguageCode.Tajik.code -> IetfLanguageCode.Tajik | ||||||
|  |         IetfLanguageCode.Tajik.TJ.code -> IetfLanguageCode.Tajik.TJ | ||||||
|  |         IetfLanguageCode.Thai.code -> IetfLanguageCode.Thai | ||||||
|  |         IetfLanguageCode.Thai.TH.code -> IetfLanguageCode.Thai.TH | ||||||
|  |         IetfLanguageCode.Tigrinya.code -> IetfLanguageCode.Tigrinya | ||||||
|  |         IetfLanguageCode.Tigrinya.ER.code -> IetfLanguageCode.Tigrinya.ER | ||||||
|  |         IetfLanguageCode.Tigrinya.ET.code -> IetfLanguageCode.Tigrinya.ET | ||||||
|  |         IetfLanguageCode.Turkmen.code -> IetfLanguageCode.Turkmen | ||||||
|  |         IetfLanguageCode.Turkmen.TM.code -> IetfLanguageCode.Turkmen.TM | ||||||
|  |         IetfLanguageCode.Tagalog.code -> IetfLanguageCode.Tagalog | ||||||
|  |         IetfLanguageCode.Tswana.code -> IetfLanguageCode.Tswana | ||||||
|  |         IetfLanguageCode.TongaTongaIslands.code -> IetfLanguageCode.TongaTongaIslands | ||||||
|  |         IetfLanguageCode.TongaTongaIslands.TO.code -> IetfLanguageCode.TongaTongaIslands.TO | ||||||
|  |         IetfLanguageCode.Turkish.code -> IetfLanguageCode.Turkish | ||||||
|  |         IetfLanguageCode.Turkish.CY.code -> IetfLanguageCode.Turkish.CY | ||||||
|  |         IetfLanguageCode.Turkish.TR.code -> IetfLanguageCode.Turkish.TR | ||||||
|  |         IetfLanguageCode.Tsonga.code -> IetfLanguageCode.Tsonga | ||||||
|  |         IetfLanguageCode.Tatar.code -> IetfLanguageCode.Tatar | ||||||
|  |         IetfLanguageCode.Tatar.RU.code -> IetfLanguageCode.Tatar.RU | ||||||
|  |         IetfLanguageCode.Twi.code -> IetfLanguageCode.Twi | ||||||
|  |         IetfLanguageCode.Tahitian.code -> IetfLanguageCode.Tahitian | ||||||
|  |         IetfLanguageCode.UighurUyghur.code -> IetfLanguageCode.UighurUyghur | ||||||
|  |         IetfLanguageCode.UighurUyghur.CN.code -> IetfLanguageCode.UighurUyghur.CN | ||||||
|  |         IetfLanguageCode.Ukrainian.code -> IetfLanguageCode.Ukrainian | ||||||
|  |         IetfLanguageCode.Ukrainian.UA.code -> IetfLanguageCode.Ukrainian.UA | ||||||
|  |         IetfLanguageCode.Urdu.code -> IetfLanguageCode.Urdu | ||||||
|  |         IetfLanguageCode.Urdu.IN.code -> IetfLanguageCode.Urdu.IN | ||||||
|  |         IetfLanguageCode.Urdu.PK.code -> IetfLanguageCode.Urdu.PK | ||||||
|  |         IetfLanguageCode.Uzbek.code -> IetfLanguageCode.Uzbek | ||||||
|  |         IetfLanguageCode.Uzbek.Arab.code -> IetfLanguageCode.Uzbek.Arab | ||||||
|  |         IetfLanguageCode.Uzbek.Arab.AF.code -> IetfLanguageCode.Uzbek.Arab.AF | ||||||
|  |         IetfLanguageCode.Uzbek.Cyrl.code -> IetfLanguageCode.Uzbek.Cyrl | ||||||
|  |         IetfLanguageCode.Uzbek.Cyrl.UZ.code -> IetfLanguageCode.Uzbek.Cyrl.UZ | ||||||
|  |         IetfLanguageCode.Uzbek.Latn.code -> IetfLanguageCode.Uzbek.Latn | ||||||
|  |         IetfLanguageCode.Uzbek.Latn.UZ.code -> IetfLanguageCode.Uzbek.Latn.UZ | ||||||
|  |         IetfLanguageCode.Venda.code -> IetfLanguageCode.Venda | ||||||
|  |         IetfLanguageCode.Vietnamese.code -> IetfLanguageCode.Vietnamese | ||||||
|  |         IetfLanguageCode.Vietnamese.VN.code -> IetfLanguageCode.Vietnamese.VN | ||||||
|  |         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 | ||||||
|  |         IetfLanguageCode.Xhosa.code -> IetfLanguageCode.Xhosa | ||||||
|  |         IetfLanguageCode.Xhosa.ZA.code -> IetfLanguageCode.Xhosa.ZA | ||||||
|  |         IetfLanguageCode.Yiddish.code -> IetfLanguageCode.Yiddish | ||||||
|  |         IetfLanguageCode.Yiddish.L001.code -> IetfLanguageCode.Yiddish.L001 | ||||||
|  |         IetfLanguageCode.Yoruba.code -> IetfLanguageCode.Yoruba | ||||||
|  |         IetfLanguageCode.Yoruba.BJ.code -> IetfLanguageCode.Yoruba.BJ | ||||||
|  |         IetfLanguageCode.Yoruba.NG.code -> IetfLanguageCode.Yoruba.NG | ||||||
|  |         IetfLanguageCode.ZhuangChuang.code -> IetfLanguageCode.ZhuangChuang | ||||||
|  |         IetfLanguageCode.Chinese.code -> IetfLanguageCode.Chinese | ||||||
|  |         IetfLanguageCode.Chinese.Hans.code -> IetfLanguageCode.Chinese.Hans | ||||||
|  |         IetfLanguageCode.Chinese.Hans.CN.code -> IetfLanguageCode.Chinese.Hans.CN | ||||||
|  |         IetfLanguageCode.Chinese.Hans.HK.code -> IetfLanguageCode.Chinese.Hans.HK | ||||||
|  |         IetfLanguageCode.Chinese.Hans.MO.code -> IetfLanguageCode.Chinese.Hans.MO | ||||||
|  |         IetfLanguageCode.Chinese.Hans.SG.code -> IetfLanguageCode.Chinese.Hans.SG | ||||||
|  |         IetfLanguageCode.Chinese.Hant.code -> IetfLanguageCode.Chinese.Hant | ||||||
|  |         IetfLanguageCode.Chinese.Hant.HK.code -> IetfLanguageCode.Chinese.Hant.HK | ||||||
|  |         IetfLanguageCode.Chinese.Hant.MO.code -> IetfLanguageCode.Chinese.Hant.MO | ||||||
|  |         IetfLanguageCode.Chinese.Hant.TW.code -> IetfLanguageCode.Chinese.Hant.TW | ||||||
|  |         IetfLanguageCode.Zulu.code -> IetfLanguageCode.Zulu | ||||||
|  |         IetfLanguageCode.Zulu.ZA.code -> IetfLanguageCode.Zulu.ZA | ||||||
|  |         else -> IetfLanguageCode.UnknownIetfLanguageCode(this) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | fun convertToIetfLanguageCode(code: String) = code.asIetfLanguageCode() | ||||||
|  | fun IetfLanguageCode(code: String) = code.asIetfLanguageCode() | ||||||
							
								
								
									
										1
									
								
								language_codes/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								language_codes/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest package="dev.inmo.micro_utils.language_codes"/> | ||||||
							
								
								
									
										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) | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user