mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 22:39:25 +00:00
Compare commits
199 Commits
Author | SHA1 | Date | |
---|---|---|---|
d69fee1732 | |||
178518db5e | |||
6fb20fb973 | |||
831bf44e34 | |||
a4c6c367e3 | |||
bc98e59709 | |||
ef287bc331 | |||
3437f4c712 | |||
618f2dcd79 | |||
6df8ad3095 | |||
eda6221288 | |||
a9859f6a0d | |||
0db88bac25 | |||
daa3d9c0dd | |||
b343b33594 | |||
46e435a448 | |||
7fe62b4ffa | |||
9c94348a15 | |||
bac256e93e | |||
49f59aa129 | |||
800dab5be0 | |||
b9977527b2 | |||
c216dba69d | |||
d4148d52e3 | |||
2006a8cdd0 | |||
feb52ecbd1 | |||
42909c3b7a | |||
706a787163 | |||
1bc14bded6 | |||
f00cb81db1 | |||
2fbd14956d | |||
24657b43be | |||
659d3b6fa5 | |||
339483c8a3 | |||
51ec46bbd7 | |||
8a059cc26d | |||
ddb8e1efb4 | |||
3bf2ed5168 | |||
e70d34d91a | |||
7a650f5c2f | |||
fc6f5ae2ee | |||
3b7dde3cb1 | |||
0a5cfaba18 | |||
ea527b5e91 | |||
85f11439e8 | |||
98c7b48625 | |||
8b007bb3af | |||
89e2d88d1c | |||
260399e965 | |||
de72843b8e | |||
b589142d9f | |||
f479c85869 | |||
26992c039a | |||
ef50e1a24f | |||
2f201670d2 | |||
90c80573a0 | |||
059519cdca | |||
55647e2e2f | |||
1802be68ef | |||
aa79496a36 | |||
4bfa4c32aa | |||
d8ca29eab1 | |||
f7a9f2e13d | |||
af286d3d53 | |||
9a20e8155e | |||
b81af7e1c8 | |||
74563bbac9 | |||
07ad79f0b3 | |||
5c28255e06 | |||
99783f281d | |||
a5008c3d15 | |||
f807f2beeb | |||
db34b25ef2 | |||
96daf11fd4 | |||
f7e98dfd2d | |||
61277e92bd | |||
32ef9f399f | |||
54e6ca5dc3 | |||
a8e226786d | |||
ce717a4c9f | |||
fd41bf0ae7 | |||
b2b68bf29f | |||
b87c29c354 | |||
24977822c9 | |||
647daa8627 | |||
a372efacb1 | |||
f40d33db2a | |||
a5eb0fbd24 | |||
307c8030af | |||
a9016465fa | |||
358b70eb5f | |||
6fcbb80a71 | |||
|
643f6c420b | ||
ac68b0b941 | |||
d62f67bd88 | |||
8718c5e310 | |||
e8273ab80c | |||
2718605987 | |||
d99538d80b | |||
ce7a1e4e21 | |||
921734763d | |||
18c608f569 | |||
b915f6ece2 | |||
24347b422c | |||
e5f4ae647f | |||
72202b8a21 | |||
dbc14d41de | |||
1dca5ea00d | |||
f03d1d788c | |||
fcac6f9fa8 | |||
ca0cd433c9 | |||
39589fdbd0 | |||
605fc3cff9 | |||
12cd6f48f8 | |||
1f57478d10 | |||
7ef4c5d282 | |||
f09d92be32 | |||
6245b36bdb | |||
54cc353bcc | |||
b7abba099c | |||
c5dbd10335 | |||
a44e3e953d | |||
ee2521cb01 | |||
4625dfb857 | |||
b9a2653066 | |||
fcaa327660 | |||
496117d517 | |||
8ce7d37b72 | |||
25391609b9 | |||
f0b7b9c5e5 | |||
301cdaa2c2 | |||
fce7ec8912 | |||
24bd403549 | |||
46c89c48a9 | |||
bad9a53fdb | |||
0bce7bd60a | |||
2f70a1cfb4 | |||
bfb6e738ee | |||
c7ad9aae07 | |||
fecd719239 | |||
18d6ac31b5 | |||
d8dbebfc7e | |||
16463d0eb9 | |||
837cac644d | |||
e83e0a8535 | |||
2e309c31a6 | |||
625db02651 | |||
e3144c08c1 | |||
40e25970ad | |||
4ccb8a8d11 | |||
26d1c3d9ef | |||
cc8fe7b001 | |||
4601eab864 | |||
1ee9c88ffd | |||
7feac213f6 | |||
c75a9c0f61 | |||
afd0c9784f | |||
f1ddbea06e | |||
42bf3cd4e3 | |||
27f5549f56 | |||
b0569f8421 | |||
8d86f29325 | |||
aeca32498a | |||
e8232664f3 | |||
efa869f91a | |||
dbff3d7cf1 | |||
9e70f73684 | |||
9f2f0de0c4 | |||
bf293a8f8f | |||
5fbc1a132f | |||
36093b9741 | |||
8713fb04c5 | |||
ea82a59f31 | |||
18f67cd4f2 | |||
120e7228c7 | |||
892fa90c37 | |||
491dac5bf0 | |||
2ab06fbafd | |||
e68735d061 | |||
9ba4d98c30 | |||
19bbfd4916 | |||
d94cd7ea94 | |||
0dd8f41eb0 | |||
f1ab5ab51f | |||
a8056f3120 | |||
09c0cdebb5 | |||
629d7c7a82 | |||
a0dd1aec3d | |||
17d6377902 | |||
ad401105a1 | |||
4913e99c2e | |||
16b97029cf | |||
481130c9bb | |||
698ed6718d | |||
d164813bb4 | |||
52157ee0e7 | |||
877b62fe5d | |||
d823a02971 | |||
70014ba233 |
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
@@ -17,8 +17,9 @@ jobs:
|
|||||||
mv gradle.properties.tmp gradle.properties
|
mv gradle.properties.tmp gradle.properties
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./gradlew build
|
run: ./gradlew build
|
||||||
- name: Publish
|
- name: Publish to InmoNexus
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: ./gradlew publishAllPublicationsToGiteaRepository
|
run: ./gradlew publishAllPublicationsToInmoNexusRepository
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
INMONEXUS_USER: ${{ secrets.INMONEXUS_USER }}
|
||||||
|
INMONEXUS_PASSWORD: ${{ secrets.INMONEXUS_PASSWORD }}
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
.idea
|
.idea
|
||||||
|
.kotlin
|
||||||
out/*
|
out/*
|
||||||
*.iml
|
*.iml
|
||||||
target
|
target
|
||||||
@@ -17,5 +18,6 @@ publishing.sh
|
|||||||
|
|
||||||
local.*
|
local.*
|
||||||
local/
|
local/
|
||||||
|
**/*.local.*
|
||||||
|
|
||||||
.kotlin/
|
.kotlin/
|
||||||
|
270
CHANGELOG.md
270
CHANGELOG.md
@@ -1,5 +1,275 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.25.2
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Exposed`: `0.59.0` -> `0.60.0`
|
||||||
|
* `Repo`:
|
||||||
|
* `Cache`:
|
||||||
|
* Add extensions `alsoInvalidate`, `alsoInvalidateAsync`, `alsoInvalidateSync` and `alsoInvalidateSyncLogging`
|
||||||
|
* `Koin`:
|
||||||
|
* Add extensions `singleSuspend` and `factorySuspend` for defining of dependencies with suspendable blocks
|
||||||
|
|
||||||
|
## 0.25.1
|
||||||
|
|
||||||
|
* `Coroutines`:
|
||||||
|
* Add `SortedMapLikeBinaryTreeNode`
|
||||||
|
* `Pagination`:
|
||||||
|
* `Compose`:
|
||||||
|
* One more rework of `InfinityPagedComponent` and `PagedComponent`
|
||||||
|
|
||||||
|
## 0.25.0
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Cache`:
|
||||||
|
* All cache repos now do not have `open` vals - to avoid collisions in runtime
|
||||||
|
|
||||||
|
## 0.24.9
|
||||||
|
|
||||||
|
* `Pagination`:
|
||||||
|
* Make alternative constructor parameter `size` of `PaginationResult` with default value
|
||||||
|
* Add `Pagination.previousPage` extension
|
||||||
|
* `Compose`:
|
||||||
|
* Rework of `InfinityPagedComponentContext`
|
||||||
|
* Rework of `PagedComponent`
|
||||||
|
|
||||||
|
## 0.24.8
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Ktor`: `3.1.0` -> `3.1.1`
|
||||||
|
* `KSP`: `2.1.10-1.0.30` -> `2.1.10-1.0.31`
|
||||||
|
* `Common`:
|
||||||
|
* `Compose`:
|
||||||
|
* Add component `LoadableComponent`
|
||||||
|
* `Coroutines`:
|
||||||
|
* Add `SortedBinaryTreeNode`
|
||||||
|
* `Pagination`:
|
||||||
|
* `Compose`:
|
||||||
|
* Add components `PagedComponent` and `InfinityPagedComponent`
|
||||||
|
|
||||||
|
## 0.24.7
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `SQLite`: `3.49.0.0` -> `3.49.1.0`
|
||||||
|
* `Common`:
|
||||||
|
* Add `retryOnFailure` utility for simple retries code writing
|
||||||
|
* `Repos`:
|
||||||
|
* `Cache`:
|
||||||
|
* Fix of `FullKeyValueCacheRepo` fields usage
|
||||||
|
* `Exposed`:
|
||||||
|
* `AbstractExposedKeyValuesRepo` will produce `onValueRemoved` event on `set` if some data has been removed
|
||||||
|
|
||||||
|
## 0.24.6
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `KSLog`: `1.4.0` -> `1.4.1`
|
||||||
|
* `Exposed`: `0.58.0` -> `0.59.0`
|
||||||
|
* `SQLite`: `3.48.0.0` -> `3.49.0.0`
|
||||||
|
* `AndroidFragment`: `1.8.5` -> `1.8.6`
|
||||||
|
* `Coroutines`:
|
||||||
|
* Safely functions has been replaced with `Logging` variations (resolve of [#541](https://github.com/InsanusMokrassar/MicroUtils/issues/541))
|
||||||
|
* `KSP`:
|
||||||
|
* `Variations`:
|
||||||
|
* Module has been created
|
||||||
|
|
||||||
|
## 0.24.5
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `2.1.0` -> `2.1.10`
|
||||||
|
* `SQLite`: `3.47.2.0` -> `3.48.0.0`
|
||||||
|
* `Koin`: `4.0.1` -> `4.0.2`
|
||||||
|
* `Android RecyclerView`: `1.3.2` -> `1.4.0`
|
||||||
|
|
||||||
|
## 0.24.4
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Exposed`:
|
||||||
|
* Improve `CommonExposedRepo.selectByIds`
|
||||||
|
* `FSM`:
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
## 0.24.3
|
||||||
|
|
||||||
|
* `Ksp`:
|
||||||
|
* `Sealed`:
|
||||||
|
* Fixes in processing of `GenerateSealedTypesWorkaround` annotations
|
||||||
|
|
||||||
|
## 0.24.2
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Exposed`: `0.57.0` -> `0.58.0`
|
||||||
|
* `Ksp`:
|
||||||
|
* `Sealed`:
|
||||||
|
* Add annotation `GenerateSealedTypesWorkaround` which allow to generate `subtypes` lists
|
||||||
|
|
||||||
|
## 0.24.1
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Serialization`: `1.7.3` -> `1.8.0`
|
||||||
|
* `SQLite`: `3.47.1.0` -> `3.47.2.0`
|
||||||
|
* `Koin`: `4.0.0` -> `3.10.2`
|
||||||
|
* `OKio`: `3.9.1` -> `3.10.2`
|
||||||
|
|
||||||
|
## 0.24.0
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Coroutines`: `1.9.0` -> `1.10.1`
|
||||||
|
* `KSLog`: `1.3.6` -> `1.4.0`
|
||||||
|
* `Compose`: `1.7.1` -> `1.7.3`
|
||||||
|
* `Ktor`: `3.0.2` -> `3.0.3`
|
||||||
|
* `Common`:
|
||||||
|
* Rename `Progress` to more common `Percentage`. `Progress` now is typealias
|
||||||
|
* Fix of `Progress.compareTo` extension
|
||||||
|
|
||||||
|
## 0.23.2
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `2.0.21` -> `2.1.0`
|
||||||
|
* `Exposed`: `0.56.0` -> `0.57.0`
|
||||||
|
* `Xerial SQLite`: `3.47.0.0` -> `3.47.1.0`
|
||||||
|
* `Ktor`: `3.0.1` -> `3.0.2`
|
||||||
|
* `Coroutines`:
|
||||||
|
* Small refactor in `AccumulatorFlow` to use `runCatching` instead of `runCatchingSafely`
|
||||||
|
|
||||||
|
## 0.23.1
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Compose`: `1.7.0` -> `1.7.1`
|
||||||
|
* `Exposed`: `0.55.0` -> `0.56.0`
|
||||||
|
* `Xerial SQLite`: `3.46.1.3` -> `3.47.0.0`
|
||||||
|
* `Android CoreKTX`: `1.13.1` -> `1.15.0`
|
||||||
|
* `Android Fragment`: `1.8.4` -> `1.8.5`
|
||||||
|
* `Coroutines`:
|
||||||
|
* `Compose`:
|
||||||
|
* Add `StyleSheetsAggregator`
|
||||||
|
|
||||||
|
## 0.23.0
|
||||||
|
|
||||||
|
**THIS UPDATE MAY CONTAINS SOME BREAKING CHANGES (INCLUDING BREAKING CHANGES IN BYTECODE LAYER) RELATED TO UPDATE OF
|
||||||
|
KTOR DEPENDENCY**
|
||||||
|
|
||||||
|
**THIS UPDATE CONTAINS CHANGES ACCORDING TO MIGRATION [GUIDE FROM KTOR](https://ktor.io/docs/migrating-3.html)**
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Ktor`: `2.3.12` -> `3.0.1`
|
||||||
|
* `Ktor`:
|
||||||
|
* `Common`:
|
||||||
|
* Extension `Input.downloadToTempFile` has changed its receiver to `Source`. Its API can be broken
|
||||||
|
* `Client`:
|
||||||
|
* Extension `HttpClient.tempUpload` has changed type of `onUpload` argument from `OnUploadCallback` to `ProgressListener`
|
||||||
|
* All extensions `HttpClient.uniUpload` have changed type of `onUpload` argument from `OnUploadCallback` to `ProgressListener`
|
||||||
|
* `Server`:
|
||||||
|
* Remove redundant `ApplicationCall.respond` extension due to its presence in the ktor library
|
||||||
|
|
||||||
|
## 0.22.9
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Cache`:
|
||||||
|
* Add direct caching repos
|
||||||
|
|
||||||
|
## 0.22.8
|
||||||
|
|
||||||
|
* `Common`:
|
||||||
|
* Add `List.breakAsPairs` extension
|
||||||
|
* Add `Sequence.padWith`/`Sequence.padStart`/`Sequence.padEnd` and `List.padWith`/`List.padStart`/`List.padEnd` extensions
|
||||||
|
|
||||||
|
## 0.22.7
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `2.0.20` -> `2.0.21`
|
||||||
|
* `Compose`: `1.7.0-rc01` -> `1.7.0`
|
||||||
|
* `KSP`:
|
||||||
|
* `Sealed`:
|
||||||
|
* Change package of `GenerateSealedWorkaround`. Migration: replace `dev.inmo.microutils.kps.sealed.GenerateSealedWorkaround` -> `dev.inmo.micro_utils.ksp.sealed.GenerateSealedWorkaround`
|
||||||
|
|
||||||
|
## 0.22.6
|
||||||
|
|
||||||
|
* `KSP`:
|
||||||
|
* `Generator`:
|
||||||
|
* Add extension `KSClassDeclaration.buildSubFileName`
|
||||||
|
* Add extension `KSClassDeclaration.companion`
|
||||||
|
* Add extension `KSClassDeclaration.resolveSubclasses`
|
||||||
|
* `Sealed`:
|
||||||
|
* Improvements
|
||||||
|
|
||||||
|
## 0.22.5
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Compose`: `1.7.0-beta02` -> `1.7.0-rc01`
|
||||||
|
* `SQLite`: `3.46.1.2` -> `3.46.1.3`
|
||||||
|
* `AndroidXFragment`: `1.8.3` -> `1.8.4`
|
||||||
|
* `Common`:
|
||||||
|
* Add extension `withReplacedAt`/`withReplaced` ([#489](https://github.com/InsanusMokrassar/MicroUtils/issues/489))
|
||||||
|
* `Coroutines`:
|
||||||
|
* Add extension `Flow.debouncedBy`
|
||||||
|
* `Ktor`:
|
||||||
|
* `Server`:
|
||||||
|
* Add `KtorApplicationConfigurator.Routing.Static` as solution for [#488](https://github.com/InsanusMokrassar/MicroUtils/issues/488)
|
||||||
|
|
||||||
|
## 0.22.4
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Exposed`: `0.54.0` -> `0.55.0`
|
||||||
|
* `SQLite`: `3.46.1.0` -> `3.46.1.2`
|
||||||
|
|
||||||
|
## 0.22.3
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Serialization`: `1.7.2` -> `1.7.3`
|
||||||
|
* `Coroutines`: `1.8.1` -> `1.9.0`
|
||||||
|
* `Compose`: `1.7.0-alpha03` -> `1.7.0-beta02`
|
||||||
|
* `Koin`: `3.5.6` -> `4.0.0`
|
||||||
|
* `Okio`: `3.9.0` -> `3.9.1`
|
||||||
|
* `AndroidFragment`: `1.8.2` -> `1.8.3`
|
||||||
|
* `androidx.compose.material3:material3` has been replaced with `org.jetbrains.compose.material3:material3`
|
||||||
|
* `Common`:
|
||||||
|
* `JS`:
|
||||||
|
* Add several useful extensions
|
||||||
|
* `Compose`:
|
||||||
|
* `JS`:
|
||||||
|
* Add several useful extensions
|
||||||
|
|
||||||
|
## 0.22.2
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Exposed`: `0.53.0` -> `0.54.0`
|
||||||
|
* `SQLite`: `3.46.0.1` -> `3.46.1.0`
|
||||||
|
|
||||||
|
## 0.22.1
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `2.0.10` -> `2.0.20`
|
||||||
|
* `Serialization`: `1.7.1` -> `1.7.2`
|
||||||
|
* `KSLog`: `1.3.5` -> `1.3.6`
|
||||||
|
* `Compose`: `1.7.0-alpha02` -> `1.7.0-alpha03`
|
||||||
|
* `Ktor`: `2.3.11` -> `2.3.12`
|
||||||
|
|
||||||
|
## 0.22.0
|
||||||
|
|
||||||
|
**THIS UPDATE CONTAINS BREAKING CHANGES ACCORDING TO UPDATE UP TO KOTLIN 2.0**
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `1.9.23` -> `2.0.10`
|
||||||
|
* `Serialization`: `1.6.3` -> `1.7.1`
|
||||||
|
* `KSLog`: `1.3.4` -> `1.3.5`
|
||||||
|
* `Compose`: `1.6.2` -> `1.7.0-alpha02`
|
||||||
|
* `Exposed`: `0.50.1` -> `0.53.0`
|
||||||
|
* `AndroidAppCompat`: `1.6.1` -> `1.7.0`
|
||||||
|
* `AndroidFragment`: `1.7.1` -> `1.8.2`
|
||||||
|
|
||||||
|
## 0.21.6
|
||||||
|
|
||||||
|
* `KSP`:
|
||||||
|
* `Sealed`:
|
||||||
|
* Fixes in generation
|
||||||
|
|
||||||
|
## 0.21.5
|
||||||
|
|
||||||
|
* `KSP`:
|
||||||
|
* Add utility functions `KSClassDeclaration.findSubClasses`
|
||||||
|
* `Sealed`:
|
||||||
|
* Improve generation
|
||||||
|
|
||||||
## 0.21.4
|
## 0.21.4
|
||||||
|
|
||||||
* `Common`:
|
* `Common`:
|
||||||
|
@@ -4,7 +4,7 @@ plugins {
|
|||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppAndroidProjectPresetPath"
|
apply from: "$mppAndroidProject"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
@@ -4,7 +4,7 @@ plugins {
|
|||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppAndroidProjectPresetPath"
|
apply from: "$mppAndroidProject"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
@@ -3,9 +3,10 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
alias(libs.plugins.jb.compose)
|
alias(libs.plugins.jb.compose)
|
||||||
|
alias(libs.plugins.kt.jb.compose)
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
apply from: "$mppProjectWithSerializationAndCompose"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
@@ -4,7 +4,7 @@ plugins {
|
|||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppAndroidProjectPresetPath"
|
apply from: "$mppAndroidProject"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
@@ -3,15 +3,16 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
alias(libs.plugins.jb.compose)
|
alias(libs.plugins.jb.compose)
|
||||||
|
alias(libs.plugins.kt.jb.compose)
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
apply from: "$mppProjectWithSerializationAndCompose"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.android.compose.material3
|
api libs.jb.compose.material3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -29,15 +29,6 @@ allprojects {
|
|||||||
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
|
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
|
||||||
maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
|
maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
|
||||||
}
|
}
|
||||||
|
|
||||||
// temporal crutch until legacy tests will be stabled or legacy target will be removed
|
|
||||||
if (it != rootProject.findProject("docs")) {
|
|
||||||
tasks.whenTaskAdded { task ->
|
|
||||||
if(task.name == "jsLegacyBrowserTest" || task.name == "jsLegacyNodeTest") {
|
|
||||||
task.enabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "./extensions.gradle"
|
apply from: "./extensions.gradle"
|
||||||
|
@@ -4,7 +4,7 @@ plugins {
|
|||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
@@ -4,4 +4,4 @@ plugins {
|
|||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
|
||||||
|
@@ -4,7 +4,7 @@ plugins {
|
|||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
@@ -3,15 +3,17 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
alias(libs.plugins.jb.compose)
|
alias(libs.plugins.jb.compose)
|
||||||
|
alias(libs.plugins.kt.jb.compose)
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppComposeJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
apply from: "$mppComposeJvmJsAndroidLinuxMingwLinuxArm64Project"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api project(":micro_utils.common")
|
api project(":micro_utils.common")
|
||||||
|
api libs.kt.coroutines
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,74 @@
|
|||||||
|
package dev.inmo.micro_utils.common.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import dev.inmo.micro_utils.common.Optional
|
||||||
|
import dev.inmo.micro_utils.common.dataOrThrow
|
||||||
|
import dev.inmo.micro_utils.common.optional
|
||||||
|
|
||||||
|
class LoadableComponentContext<T> internal constructor(
|
||||||
|
presetOptional: Optional<T>,
|
||||||
|
) {
|
||||||
|
internal val iterationState: MutableState<Int> = mutableStateOf(0)
|
||||||
|
|
||||||
|
internal var dataOptional: Optional<T> = if (presetOptional.dataPresented) presetOptional else Optional.absent()
|
||||||
|
private set
|
||||||
|
internal val dataState: MutableState<Optional<T>> = mutableStateOf(dataOptional)
|
||||||
|
|
||||||
|
fun reload() {
|
||||||
|
iterationState.value++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Showing data with ability to reload data
|
||||||
|
*
|
||||||
|
* [block] will be shown when [loader] will complete loading. If you want to reload data, just call
|
||||||
|
* [LoadableComponentContext.reload]
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun <T> LoadableComponent(
|
||||||
|
preload: Optional<T>,
|
||||||
|
loader: suspend LoadableComponentContext<T>.() -> T,
|
||||||
|
block: @Composable LoadableComponentContext<T>.(T) -> Unit
|
||||||
|
) {
|
||||||
|
val context = remember { LoadableComponentContext(preload) }
|
||||||
|
|
||||||
|
LaunchedEffect(context.iterationState.value) {
|
||||||
|
context.dataState.value = loader(context).optional
|
||||||
|
}
|
||||||
|
|
||||||
|
context.dataState.let {
|
||||||
|
if (it.value.dataPresented) {
|
||||||
|
context.block(it.value.dataOrThrow(IllegalStateException("Data must be presented, but optional has been changed by some way")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Showing data with ability to reload data
|
||||||
|
*
|
||||||
|
* [block] will be shown when [loader] will complete loading. If you want to reload data, just call
|
||||||
|
* [LoadableComponentContext.reload]
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun <T> LoadableComponent(
|
||||||
|
preload: T,
|
||||||
|
loader: suspend LoadableComponentContext<T>.() -> T,
|
||||||
|
block: @Composable LoadableComponentContext<T>.(T) -> Unit
|
||||||
|
) {
|
||||||
|
LoadableComponent(preload.optional, loader, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Showing data with ability to reload data
|
||||||
|
*
|
||||||
|
* [block] will be shown when [loader] will complete loading. If you want to reload data, just call
|
||||||
|
* [LoadableComponentContext.reload]
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun <T> LoadableComponent(
|
||||||
|
loader: suspend LoadableComponentContext<T>.() -> T,
|
||||||
|
block: @Composable LoadableComponentContext<T>.(T) -> Unit
|
||||||
|
) {
|
||||||
|
LoadableComponent(Optional.absent(), loader, block)
|
||||||
|
}
|
@@ -0,0 +1,19 @@
|
|||||||
|
package dev.inmo.micro_utils.common.compose
|
||||||
|
|
||||||
|
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
|
||||||
|
operator fun <T : Element> AttrBuilderContext<T>?.plus(
|
||||||
|
other: AttrBuilderContext<T>?
|
||||||
|
) = when (this) {
|
||||||
|
null -> other ?: {}
|
||||||
|
else -> when (other) {
|
||||||
|
null -> this ?: {}
|
||||||
|
else -> {
|
||||||
|
{
|
||||||
|
invoke(this)
|
||||||
|
other(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,22 @@
|
|||||||
|
package dev.inmo.micro_utils.common.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.DisposableEffectResult
|
||||||
|
import androidx.compose.runtime.DisposableEffectScope
|
||||||
|
import org.jetbrains.compose.web.attributes.AttrsScope
|
||||||
|
import org.jetbrains.compose.web.dom.ElementScope
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function must be called in the context of your tag content. It works like default [AttrsScope.ref],
|
||||||
|
* but able to be used several times. Uses [DisposableEffect] under the hood
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun <T : Element> ElementScope<T>.ref(
|
||||||
|
block: DisposableEffectScope.(T) -> DisposableEffectResult
|
||||||
|
) {
|
||||||
|
DisposableEffect(0) {
|
||||||
|
block(scopeElement)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,11 @@
|
|||||||
|
package dev.inmo.micro_utils.common.compose
|
||||||
|
|
||||||
|
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||||
|
|
||||||
|
fun tagClasses(vararg classnames: String): AttrBuilderContext<*> = {
|
||||||
|
classes(*classnames)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun tagId(id: String): AttrBuilderContext<*> = {
|
||||||
|
id(id)
|
||||||
|
}
|
42
common/compose/src/jvmTest/kotlin/LoadableComponentTests.kt
Normal file
42
common/compose/src/jvmTest/kotlin/LoadableComponentTests.kt
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
|
import androidx.compose.ui.test.runComposeUiTest
|
||||||
|
import dev.inmo.micro_utils.common.compose.LoadableComponent
|
||||||
|
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import org.jetbrains.annotations.TestOnly
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class LoadableComponentTests {
|
||||||
|
@OptIn(ExperimentalTestApi::class)
|
||||||
|
@Test
|
||||||
|
@TestOnly
|
||||||
|
fun testSimpleLoad() = runComposeUiTest {
|
||||||
|
val loadingFlow = SpecialMutableStateFlow<Int>(0)
|
||||||
|
val loadedFlow = SpecialMutableStateFlow<Int>(0)
|
||||||
|
setContent {
|
||||||
|
LoadableComponent<Int>({
|
||||||
|
loadingFlow.filter { it == 1 }.first()
|
||||||
|
}) {
|
||||||
|
assert(dataState.value.data == 1)
|
||||||
|
remember {
|
||||||
|
loadedFlow.value = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForIdle()
|
||||||
|
|
||||||
|
assertTrue(loadedFlow.value == 0)
|
||||||
|
|
||||||
|
loadingFlow.value = 1
|
||||||
|
|
||||||
|
waitForIdle()
|
||||||
|
|
||||||
|
assertTrue(loadedFlow.value == 2)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
fun <T> List<T>.breakAsPairs(): List<Pair<T, T>> {
|
||||||
|
val result = mutableListOf<Pair<T, T>>()
|
||||||
|
|
||||||
|
for (i in 0 until size - 1) {
|
||||||
|
val first = get(i)
|
||||||
|
val second = get(i + 1)
|
||||||
|
result.add(first to second)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
@@ -0,0 +1,6 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates simple [Comparator] which will use [compareTo] of [T] for both objects
|
||||||
|
*/
|
||||||
|
fun <T : Comparable<C>, C : T> T.createComparator() = Comparator<C> { o1, o2 -> o1.compareTo(o2) }
|
@@ -8,7 +8,7 @@ private inline fun <T> getObject(
|
|||||||
additional: MutableList<T>,
|
additional: MutableList<T>,
|
||||||
iterator: Iterator<T>
|
iterator: Iterator<T>
|
||||||
): T? = when {
|
): T? = when {
|
||||||
additional.isNotEmpty() -> additional.removeFirst()
|
additional.isNotEmpty() -> additional.removeAt(0)
|
||||||
iterator.hasNext() -> iterator.next()
|
iterator.hasNext() -> iterator.next()
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,32 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
inline fun <T> Sequence<T>.padWith(size: Int, inserter: (Sequence<T>) -> Sequence<T>): Sequence<T> {
|
||||||
|
var result = this
|
||||||
|
while (result.count() < size) {
|
||||||
|
result = inserter(result)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> Sequence<T>.padEnd(size: Int, padBlock: (Int) -> T): Sequence<T> = padWith(size) { it + padBlock(it.count()) }
|
||||||
|
|
||||||
|
inline fun <T> Sequence<T>.padEnd(size: Int, o: T) = padEnd(size) { o }
|
||||||
|
|
||||||
|
inline fun <T> List<T>.padWith(size: Int, inserter: (List<T>) -> List<T>): List<T> {
|
||||||
|
var result = this
|
||||||
|
while (result.size < size) {
|
||||||
|
result = inserter(result)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
inline fun <T> List<T>.padEnd(size: Int, padBlock: (Int) -> T): List<T> = asSequence().padEnd(size, padBlock).toList()
|
||||||
|
|
||||||
|
inline fun <T> List<T>.padEnd(size: Int, o: T): List<T> = asSequence().padEnd(size, o).toList()
|
||||||
|
|
||||||
|
inline fun <T> Sequence<T>.padStart(size: Int, padBlock: (Int) -> T): Sequence<T> = padWith(size) { sequenceOf(padBlock(it.count())) + it }
|
||||||
|
|
||||||
|
inline fun <T> Sequence<T>.padStart(size: Int, o: T) = padStart(size) { o }
|
||||||
|
|
||||||
|
inline fun <T> List<T>.padStart(size: Int, padBlock: (Int) -> T): List<T> = asSequence().padStart(size, padBlock).toList()
|
||||||
|
|
||||||
|
inline fun <T> List<T>.padStart(size: Int, o: T): List<T> = asSequence().padStart(size, o).toList()
|
@@ -0,0 +1,76 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.jvm.JvmInline
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains [of1] as main value, where 100% of percentage is when of1 == 1
|
||||||
|
*
|
||||||
|
* @see invoke
|
||||||
|
* @see partOfTotal
|
||||||
|
* @see of100
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
@JvmInline
|
||||||
|
value class Percentage private constructor(
|
||||||
|
/**
|
||||||
|
* Value of percentage. When it equals to 1, means 100%
|
||||||
|
*/
|
||||||
|
val of1: Double
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Same as [of1], but float (using [Double.toFloat])
|
||||||
|
*/
|
||||||
|
val of1Float
|
||||||
|
get() = of1.toFloat()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represent this percentage as common percentage where 100% is 100%
|
||||||
|
*/
|
||||||
|
val of100
|
||||||
|
get() = of1 * 100
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as [of100], but float (using [Double.toFloat])
|
||||||
|
*/
|
||||||
|
val of100Float
|
||||||
|
get() = of100.toFloat()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as [of100], but int (using [Double.toInt])
|
||||||
|
*/
|
||||||
|
val of100Int
|
||||||
|
get() = of100.toInt()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val rangeOfValues = 0.0 .. 1.0
|
||||||
|
|
||||||
|
val START = Percentage(rangeOfValues.start)
|
||||||
|
val COMPLETED = Percentage(rangeOfValues.endInclusive)
|
||||||
|
|
||||||
|
operator fun invoke(of1: Double) = Percentage(of1.coerceIn(rangeOfValues))
|
||||||
|
operator fun invoke(part: Number, total: Number) = Percentage(
|
||||||
|
part.toDouble() / total.toDouble()
|
||||||
|
)
|
||||||
|
fun of1(of1: Double) = Percentage(of1 = of1)
|
||||||
|
fun of100(of100: Double) = Percentage(of1 = of100 / 100)
|
||||||
|
fun partOfTotal(part: Number, total: Number) = Percentage(part = part, total = total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias Progress = Percentage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will return [this] [Progress] if [Percentage.of1] in `0 .. 1` range
|
||||||
|
*/
|
||||||
|
fun Progress.ensureStrictOrNull(): Progress? = if (of1 in Percentage.rangeOfValues) this else null
|
||||||
|
/**
|
||||||
|
* Will return [this] [Progress] if [Percentage.of1] in `0 .. 1` range. Otherwise, will throw error
|
||||||
|
* [IllegalArgumentException] due to [require] failure
|
||||||
|
*/
|
||||||
|
fun Progress.ensureStrictOrThrow(): Progress {
|
||||||
|
require(of1 in Percentage.rangeOfValues) {
|
||||||
|
"For strict checks value of percentage must be in ${Percentage.rangeOfValues}, but actual value is $of1"
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
@@ -0,0 +1,80 @@
|
|||||||
|
@file:Suppress(
|
||||||
|
"RemoveRedundantCallsOfConversionMethods",
|
||||||
|
"RedundantVisibilityModifier",
|
||||||
|
)
|
||||||
|
|
||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlin.Byte
|
||||||
|
import kotlin.Double
|
||||||
|
import kotlin.Float
|
||||||
|
import kotlin.Int
|
||||||
|
import kotlin.Long
|
||||||
|
import kotlin.Short
|
||||||
|
import kotlin.Suppress
|
||||||
|
|
||||||
|
public operator fun Percentage.plus(other: Percentage): Percentage = Percentage(of1 + other.of1)
|
||||||
|
|
||||||
|
public operator fun Percentage.minus(other: Percentage): Percentage = Percentage(of1 - other.of1)
|
||||||
|
|
||||||
|
public operator fun Percentage.plus(i: Byte): Percentage = Percentage((of1 + i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.minus(i: Byte): Percentage = Percentage((of1 - i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.times(i: Byte): Percentage = Percentage((of1 * i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.div(i: Byte): Percentage = Percentage((of1 / i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.rem(i: Byte): Percentage = Percentage((of1 % i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.plus(i: Short): Percentage = Percentage((of1 + i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.minus(i: Short): Percentage = Percentage((of1 - i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.times(i: Short): Percentage = Percentage((of1 * i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.div(i: Short): Percentage = Percentage((of1 / i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.rem(i: Short): Percentage = Percentage((of1 % i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.plus(i: Int): Percentage = Percentage((of1 + i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.minus(i: Int): Percentage = Percentage((of1 - i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.times(i: Int): Percentage = Percentage((of1 * i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.div(i: Int): Percentage = Percentage((of1 / i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.rem(i: Int): Percentage = Percentage((of1 % i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.plus(i: Long): Percentage = Percentage((of1 + i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.minus(i: Long): Percentage = Percentage((of1 - i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.times(i: Long): Percentage = Percentage((of1 * i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.div(i: Long): Percentage = Percentage((of1 / i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.rem(i: Long): Percentage = Percentage((of1 % i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.plus(i: Float): Percentage = Percentage((of1 + i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.minus(i: Float): Percentage = Percentage((of1 - i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.times(i: Float): Percentage = Percentage((of1 * i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.div(i: Float): Percentage = Percentage((of1 / i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.rem(i: Float): Percentage = Percentage((of1 % i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.plus(i: Double): Percentage = Percentage((of1 + i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.minus(i: Double): Percentage = Percentage((of1 - i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.times(i: Double): Percentage = Percentage((of1 * i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.div(i: Double): Percentage = Percentage((of1 / i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.rem(i: Double): Percentage = Percentage((of1 % i).toDouble())
|
||||||
|
|
||||||
|
public operator fun Percentage.compareTo(other: Percentage): Int = (of1.compareTo(other.of1))
|
@@ -1,37 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlin.jvm.JvmInline
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@JvmInline
|
|
||||||
value class Progress private constructor(
|
|
||||||
val of1: Double
|
|
||||||
) {
|
|
||||||
val of1Float
|
|
||||||
get() = of1.toFloat()
|
|
||||||
val of100
|
|
||||||
get() = of1 * 100
|
|
||||||
val of100Float
|
|
||||||
get() = of100.toFloat()
|
|
||||||
val of100Int
|
|
||||||
get() = of100.toInt()
|
|
||||||
|
|
||||||
init {
|
|
||||||
require(of1 in rangeOfValues) {
|
|
||||||
"Progress main value should be in $rangeOfValues, but incoming value is $of1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val rangeOfValues = 0.0 .. 1.0
|
|
||||||
|
|
||||||
val START = Progress(rangeOfValues.start)
|
|
||||||
val COMPLETED = Progress(rangeOfValues.endInclusive)
|
|
||||||
|
|
||||||
operator fun invoke(of1: Double) = Progress(of1.coerceIn(rangeOfValues))
|
|
||||||
operator fun invoke(part: Number, total: Number) = Progress(
|
|
||||||
part.toDouble() / total.toDouble()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,80 +0,0 @@
|
|||||||
@file:Suppress(
|
|
||||||
"RemoveRedundantCallsOfConversionMethods",
|
|
||||||
"RedundantVisibilityModifier",
|
|
||||||
)
|
|
||||||
|
|
||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import kotlin.Byte
|
|
||||||
import kotlin.Double
|
|
||||||
import kotlin.Float
|
|
||||||
import kotlin.Int
|
|
||||||
import kotlin.Long
|
|
||||||
import kotlin.Short
|
|
||||||
import kotlin.Suppress
|
|
||||||
|
|
||||||
public operator fun Progress.plus(other: Progress): Progress = Progress(of1 + other.of1)
|
|
||||||
|
|
||||||
public operator fun Progress.minus(other: Progress): Progress = Progress(of1 - other.of1)
|
|
||||||
|
|
||||||
public operator fun Progress.plus(i: Byte): Progress = Progress((of1 + i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.minus(i: Byte): Progress = Progress((of1 - i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.times(i: Byte): Progress = Progress((of1 * i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.div(i: Byte): Progress = Progress((of1 / i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.rem(i: Byte): Progress = Progress((of1 % i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.plus(i: Short): Progress = Progress((of1 + i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.minus(i: Short): Progress = Progress((of1 - i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.times(i: Short): Progress = Progress((of1 * i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.div(i: Short): Progress = Progress((of1 / i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.rem(i: Short): Progress = Progress((of1 % i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.plus(i: Int): Progress = Progress((of1 + i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.minus(i: Int): Progress = Progress((of1 - i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.times(i: Int): Progress = Progress((of1 * i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.div(i: Int): Progress = Progress((of1 / i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.rem(i: Int): Progress = Progress((of1 % i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.plus(i: Long): Progress = Progress((of1 + i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.minus(i: Long): Progress = Progress((of1 - i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.times(i: Long): Progress = Progress((of1 * i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.div(i: Long): Progress = Progress((of1 / i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.rem(i: Long): Progress = Progress((of1 % i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.plus(i: Float): Progress = Progress((of1 + i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.minus(i: Float): Progress = Progress((of1 - i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.times(i: Float): Progress = Progress((of1 * i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.div(i: Float): Progress = Progress((of1 / i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.rem(i: Float): Progress = Progress((of1 % i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.plus(i: Double): Progress = Progress((of1 + i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.minus(i: Double): Progress = Progress((of1 - i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.times(i: Double): Progress = Progress((of1 * i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.div(i: Double): Progress = Progress((of1 / i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.rem(i: Double): Progress = Progress((of1 % i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.compareTo(other: Progress): Int = (of1 - other.of1).toInt()
|
|
@@ -0,0 +1,26 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will try to execute [action] and, if any exception will happen, execution will be retried.
|
||||||
|
* This process will happen at most [count] times. There is no any limits on [count] value, but [action] will run at
|
||||||
|
* least once and [retryOnFailure] will return its result if it is successful
|
||||||
|
*/
|
||||||
|
inline fun <T> retryOnFailure(count: Int, action: () -> T): T {
|
||||||
|
var triesCount = 0
|
||||||
|
while (true) {
|
||||||
|
val result = runCatching {
|
||||||
|
action()
|
||||||
|
}.onFailure {
|
||||||
|
triesCount++
|
||||||
|
|
||||||
|
if (triesCount >= count) {
|
||||||
|
throw it
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isSuccess) return result.getOrThrow()
|
||||||
|
}
|
||||||
|
error("Unreachable code: retry must throw latest exception if error happen or success value if not")
|
||||||
|
}
|
@@ -0,0 +1,5 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
fun <T> Iterable<T>.withReplacedAt(i: Int, block: (T) -> T): List<T> = take(i) + block(elementAt(i)) + drop(i + 1)
|
||||||
|
fun <T> Iterable<T>.withReplaced(t: T, block: (T) -> T): List<T> = withReplacedAt(indexOf(t), block)
|
||||||
|
|
@@ -0,0 +1,29 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class PercentageTests {
|
||||||
|
@Test
|
||||||
|
fun testCompareTo() {
|
||||||
|
val step = 0.01
|
||||||
|
|
||||||
|
var i = Percentage.START.of1
|
||||||
|
while (i <= Percentage.COMPLETED.of1) {
|
||||||
|
val percentageI = Percentage(i)
|
||||||
|
|
||||||
|
var j = Percentage.START.of1
|
||||||
|
while (j <= Percentage.COMPLETED.of1) {
|
||||||
|
val percentageJ = Percentage(j)
|
||||||
|
|
||||||
|
assertEquals(percentageI.of1.compareTo(percentageJ.of1), percentageI.compareTo(percentageJ))
|
||||||
|
assertEquals(percentageI.of1 > percentageJ.of1, percentageI > percentageJ)
|
||||||
|
assertEquals(percentageI.of1 < percentageJ.of1, percentageI < percentageJ)
|
||||||
|
|
||||||
|
j += step
|
||||||
|
}
|
||||||
|
|
||||||
|
i += step
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,21 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class WithReplacedTest {
|
||||||
|
@Test
|
||||||
|
fun testReplaced() {
|
||||||
|
val data = 0 until 10
|
||||||
|
val testData = Int.MAX_VALUE
|
||||||
|
|
||||||
|
for (i in 0 until data.last) {
|
||||||
|
val withReplaced = data.withReplacedAt(i) {
|
||||||
|
testData
|
||||||
|
}
|
||||||
|
val dataAsMutableList = data.toMutableList()
|
||||||
|
dataAsMutableList[i] = testData
|
||||||
|
assertEquals(withReplaced, dataAsMutableList.toList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlinx.browser.window
|
||||||
|
|
||||||
|
fun copyToClipboard(text: String): Boolean {
|
||||||
|
return runCatching {
|
||||||
|
window.navigator.clipboard.writeText(
|
||||||
|
text
|
||||||
|
)
|
||||||
|
}.onFailure {
|
||||||
|
it.printStackTrace()
|
||||||
|
}.isSuccess
|
||||||
|
}
|
@@ -0,0 +1,29 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlinx.browser.window
|
||||||
|
import org.w3c.files.Blob
|
||||||
|
import org.w3c.files.BlobPropertyBag
|
||||||
|
import kotlin.js.json
|
||||||
|
|
||||||
|
external class ClipboardItem(data: dynamic)
|
||||||
|
|
||||||
|
inline fun Blob.convertToClipboardItem(): ClipboardItem {
|
||||||
|
val itemData: dynamic = json(this.type to this)
|
||||||
|
return ClipboardItem(itemData)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun copyImageURLToClipboard(imageUrl: String): Boolean {
|
||||||
|
return runCatching {
|
||||||
|
val response = window.fetch(imageUrl).await()
|
||||||
|
val blob = response.blob().await()
|
||||||
|
val data = arrayOf(
|
||||||
|
Blob(
|
||||||
|
arrayOf(blob),
|
||||||
|
BlobPropertyBag("image/png")
|
||||||
|
).convertToClipboardItem()
|
||||||
|
).asDynamic()
|
||||||
|
window.navigator.clipboard.write(data)
|
||||||
|
}.onFailure {
|
||||||
|
it.printStackTrace()
|
||||||
|
}.isSuccess
|
||||||
|
}
|
@@ -4,13 +4,14 @@ plugins {
|
|||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.kt.coroutines
|
api libs.kt.coroutines
|
||||||
|
api libs.kslog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jsMain {
|
jsMain {
|
||||||
|
@@ -3,9 +3,10 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
alias(libs.plugins.jb.compose)
|
alias(libs.plugins.jb.compose)
|
||||||
|
alias(libs.plugins.kt.jb.compose)
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppComposeJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
apply from: "$mppComposeJvmJsAndroidLinuxMingwLinuxArm64Project"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
@@ -0,0 +1,66 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import org.jetbrains.compose.web.css.CSSRulesHolder
|
||||||
|
import org.jetbrains.compose.web.css.Style
|
||||||
|
import org.jetbrains.compose.web.css.StyleSheet
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregator of Compose CSS StyleSheet. Allowing to add [StyleSheet] in it and draw it in one place without requiring
|
||||||
|
* to add `Style(stylesheet)` on every compose function call
|
||||||
|
*/
|
||||||
|
object StyleSheetsAggregator {
|
||||||
|
private val _stylesFlow = SpecialMutableStateFlow<Set<CSSRulesHolder>>(emptySet())
|
||||||
|
val stylesFlow: StateFlow<Set<CSSRulesHolder>> = _stylesFlow.asStateFlow()
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun draw() {
|
||||||
|
_stylesFlow.debounce(13L).collectAsState(emptySet()).value.forEach {
|
||||||
|
Style(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adding [styleSheet] into the [Set] of included stylesheets. If you called [enableStyleSheetsAggregator],
|
||||||
|
* new styles will be enabled in the document
|
||||||
|
*/
|
||||||
|
fun addStyleSheet(styleSheet: CSSRulesHolder) {
|
||||||
|
_stylesFlow.value += styleSheet
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removed [styleSheet] into the [Set] of included stylesheets
|
||||||
|
*/
|
||||||
|
fun removeStyleSheet(styleSheet: CSSRulesHolder) {
|
||||||
|
_stylesFlow.value -= styleSheet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drawing [StyleSheetsAggregator] in place. You may pass [Set] of [CSSRulesHolder]/[StyleSheet]s as preset of styles
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun enableStyleSheetsAggregator(
|
||||||
|
stylesPreset: Set<CSSRulesHolder> = emptySet(),
|
||||||
|
) {
|
||||||
|
remember {
|
||||||
|
stylesPreset.forEach {
|
||||||
|
StyleSheetsAggregator.addStyleSheet(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StyleSheetsAggregator.draw()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will include [this] [CSSRulesHolder]/[StyleSheet] in the [StyleSheetsAggregator] using its
|
||||||
|
* [StyleSheetsAggregator.addStyleSheet]
|
||||||
|
*/
|
||||||
|
fun CSSRulesHolder.includeInStyleSheetsAggregator() {
|
||||||
|
StyleSheetsAggregator.addStyleSheet(this)
|
||||||
|
}
|
@@ -68,9 +68,9 @@ class AccumulatorFlow<T>(
|
|||||||
override suspend fun collectSafely(collector: FlowCollector<T>) {
|
override suspend fun collectSafely(collector: FlowCollector<T>) {
|
||||||
val channel = Channel<T>(Channel.UNLIMITED, BufferOverflow.SUSPEND)
|
val channel = Channel<T>(Channel.UNLIMITED, BufferOverflow.SUSPEND)
|
||||||
steps.send(SubscribeAccumulatorFlowStep(channel))
|
steps.send(SubscribeAccumulatorFlowStep(channel))
|
||||||
val result = runCatchingSafely {
|
val result = runCatching {
|
||||||
for (data in channel) {
|
for (data in channel) {
|
||||||
val emitResult = runCatchingSafely {
|
val emitResult = runCatching {
|
||||||
collector.emit(data)
|
collector.emit(data)
|
||||||
}
|
}
|
||||||
if (emitResult.isSuccess || emitResult.exceptionOrNull() is CancellationException) {
|
if (emitResult.isSuccess || emitResult.exceptionOrNull() is CancellationException) {
|
||||||
|
@@ -41,4 +41,4 @@ class ContextSafelyExceptionHandler(
|
|||||||
) : CoroutineContext.Element {
|
) : CoroutineContext.Element {
|
||||||
override val key: CoroutineContext.Key<*>
|
override val key: CoroutineContext.Key<*>
|
||||||
get() = ContextSafelyExceptionHandlerKey
|
get() = ContextSafelyExceptionHandlerKey
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,40 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlin.jvm.JvmInline
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
@JvmInline
|
||||||
|
private value class DebouncedByData<T>(
|
||||||
|
val millisToData: Pair<Long, T>
|
||||||
|
)
|
||||||
|
|
||||||
|
fun <T> Flow<T>.debouncedBy(timeout: (T) -> Long, markerFactory: (T) -> Any?): Flow<T> = channelFlow {
|
||||||
|
val jobs = mutableMapOf<Any?, Job>()
|
||||||
|
val mutex = Mutex()
|
||||||
|
subscribe(this) {
|
||||||
|
mutex.withLock {
|
||||||
|
val marker = markerFactory(it)
|
||||||
|
lateinit var job: Job
|
||||||
|
job = async {
|
||||||
|
delay(timeout(it))
|
||||||
|
mutex.withLock {
|
||||||
|
if (jobs[marker] === job) {
|
||||||
|
this@channelFlow.send(it)
|
||||||
|
jobs.remove(marker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jobs[marker] ?.cancel()
|
||||||
|
jobs[marker] = job
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Flow<T>.debouncedBy(timeout: Long, markerFactory: (T) -> Any?): Flow<T> = debouncedBy({ timeout }, markerFactory)
|
||||||
|
fun <T> Flow<T>.debouncedBy(timeout: Duration, markerFactory: (T) -> Any?): Flow<T> = debouncedBy({ timeout.inWholeMilliseconds }, markerFactory)
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
package dev.inmo.micro_utils.coroutines
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
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.Mutex
|
||||||
@@ -16,6 +17,45 @@ inline fun <T> Flow<T>.subscribe(scope: CoroutineScope, noinline block: suspend
|
|||||||
* Use [subscribe], but all [block]s will be called inside of [safely] function.
|
* Use [subscribe], but all [block]s will be called inside of [safely] function.
|
||||||
* Use [onException] to set up your reaction for [Throwable]s
|
* Use [onException] to set up your reaction for [Throwable]s
|
||||||
*/
|
*/
|
||||||
|
inline fun <T> Flow<T>.subscribeLogging(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
noinline errorMessageBuilder: T.(Throwable) -> Any = { "Something web wrong" },
|
||||||
|
logger: KSLog = KSLog,
|
||||||
|
noinline block: suspend (T) -> Unit
|
||||||
|
) = subscribe(scope) {
|
||||||
|
it.runCatchingLogging(
|
||||||
|
errorMessageBuilder,
|
||||||
|
logger
|
||||||
|
) {
|
||||||
|
block(it)
|
||||||
|
}.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use [subscribeSafelyWithoutExceptions], but all exceptions will be passed to [defaultSafelyExceptionHandler]
|
||||||
|
*/
|
||||||
|
inline fun <T> Flow<T>.subscribeLoggingDropExceptions(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
noinline errorMessageBuilder: T.(Throwable) -> Any = { "Something web wrong" },
|
||||||
|
logger: KSLog = KSLog,
|
||||||
|
noinline block: suspend (T) -> Unit
|
||||||
|
) = subscribe(scope) {
|
||||||
|
it.runCatchingLogging(
|
||||||
|
errorMessageBuilder,
|
||||||
|
logger
|
||||||
|
) {
|
||||||
|
block(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use [subscribe], but all [block]s will be called inside of [safely] function.
|
||||||
|
* Use [onException] to set up your reaction for [Throwable]s
|
||||||
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
"Will be removed soon due to replacement by subscribeLogging",
|
||||||
|
ReplaceWith("this.subscribeLogging(scope = scope, block = block)")
|
||||||
|
)
|
||||||
inline fun <T> Flow<T>.subscribeSafely(
|
inline fun <T> Flow<T>.subscribeSafely(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||||
@@ -29,6 +69,10 @@ inline fun <T> Flow<T>.subscribeSafely(
|
|||||||
/**
|
/**
|
||||||
* Use [subscribeSafelyWithoutExceptions], but all exceptions will be passed to [defaultSafelyExceptionHandler]
|
* Use [subscribeSafelyWithoutExceptions], but all exceptions will be passed to [defaultSafelyExceptionHandler]
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
"Will be removed soon due to replacement by subscribeLoggingDropExceptions",
|
||||||
|
ReplaceWith("this.subscribeLoggingDropExceptions(scope = scope, block = block)")
|
||||||
|
)
|
||||||
inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions(
|
inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||||
@@ -42,6 +86,10 @@ inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions(
|
|||||||
/**
|
/**
|
||||||
* Use [subscribeSafelyWithoutExceptions], but all exceptions inside of [safely] will be skipped
|
* Use [subscribeSafelyWithoutExceptions], but all exceptions inside of [safely] will be skipped
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
"Will be removed soon due to replacement by subscribeLoggingDropExceptions",
|
||||||
|
ReplaceWith("this.subscribeLoggingDropExceptions(scope = scope, block = block)")
|
||||||
|
)
|
||||||
inline fun <T> Flow<T>.subscribeSafelySkippingExceptions(
|
inline fun <T> Flow<T>.subscribeSafelySkippingExceptions(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
noinline block: suspend (T) -> Unit
|
noinline block: suspend (T) -> Unit
|
||||||
|
@@ -15,7 +15,7 @@ private class SubscribeAsyncReceiver<T>(
|
|||||||
get() = dataChannel
|
get() = dataChannel
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launchSafelyWithoutExceptions {
|
scope.launchLoggingDropExceptions {
|
||||||
for (data in dataChannel) {
|
for (data in dataChannel) {
|
||||||
output(data)
|
output(data)
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ fun <T, M> Flow<T>.subscribeAsync(
|
|||||||
it.invoke(markersMap)
|
it.invoke(markersMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
val job = subscribeSafelyWithoutExceptions(subscope) { data ->
|
val job = subscribeLoggingDropExceptions(subscope) { data ->
|
||||||
val dataCommand = AsyncSubscriptionCommandData(data, subscope, markerFactory, block) { marker ->
|
val dataCommand = AsyncSubscriptionCommandData(data, subscope, markerFactory, block) { marker ->
|
||||||
actor.send(
|
actor.send(
|
||||||
AsyncSubscriptionCommandClearReceiver(marker)
|
AsyncSubscriptionCommandClearReceiver(marker)
|
||||||
|
@@ -15,6 +15,10 @@ import kotlin.coroutines.coroutineContext
|
|||||||
*
|
*
|
||||||
* @return [Result] with result of [block] if no exceptions or [Result] from [onException] execution
|
* @return [Result] with result of [block] if no exceptions or [Result] from [onException] execution
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
"This function become redundant since coroutines correctly handling throwing exceptions",
|
||||||
|
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { onException(it) }")
|
||||||
|
)
|
||||||
suspend inline fun <T> runCatchingSafely(
|
suspend inline fun <T> runCatchingSafely(
|
||||||
onException: ExceptionHandler<T>,
|
onException: ExceptionHandler<T>,
|
||||||
block: suspend () -> T
|
block: suspend () -> T
|
||||||
@@ -29,6 +33,10 @@ suspend inline fun <T> runCatchingSafely(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
"This function become redundant since coroutines correctly handling throwing exceptions",
|
||||||
|
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { onException(it) }")
|
||||||
|
)
|
||||||
suspend inline fun <T, R> R.runCatchingSafely(
|
suspend inline fun <T, R> R.runCatchingSafely(
|
||||||
onException: ExceptionHandler<T>,
|
onException: ExceptionHandler<T>,
|
||||||
block: suspend R.() -> T
|
block: suspend R.() -> T
|
||||||
@@ -39,10 +47,18 @@ suspend inline fun <T, R> R.runCatchingSafely(
|
|||||||
/**
|
/**
|
||||||
* Launching [runCatchingSafely] with [defaultSafelyExceptionHandler] as `onException` parameter
|
* Launching [runCatchingSafely] with [defaultSafelyExceptionHandler] as `onException` parameter
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
"This function become redundant since coroutines correctly handling throwing exceptions",
|
||||||
|
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { defaultSafelyExceptionHandler(it) }")
|
||||||
|
)
|
||||||
suspend inline fun <T> runCatchingSafely(
|
suspend inline fun <T> runCatchingSafely(
|
||||||
block: suspend () -> T
|
block: suspend () -> T
|
||||||
): Result<T> = runCatchingSafely(defaultSafelyExceptionHandler, block)
|
): Result<T> = runCatchingSafely(defaultSafelyExceptionHandler, block)
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
"This function become redundant since coroutines correctly handling throwing exceptions",
|
||||||
|
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { defaultSafelyExceptionHandler(it) }")
|
||||||
|
)
|
||||||
suspend inline fun <T, R> R.runCatchingSafely(
|
suspend inline fun <T, R> R.runCatchingSafely(
|
||||||
block: suspend R.() -> T
|
block: suspend R.() -> T
|
||||||
): Result<T> = runCatchingSafely<T> {
|
): Result<T> = runCatchingSafely<T> {
|
||||||
@@ -73,6 +89,9 @@ suspend fun contextSafelyExceptionHandler() = coroutineContext[ContextSafelyExce
|
|||||||
* After all, will be called [withContext] method with created [ContextSafelyExceptionHandler] and block which will call
|
* After all, will be called [withContext] method with created [ContextSafelyExceptionHandler] and block which will call
|
||||||
* [safely] method with [safelyExceptionHandler] as onException parameter and [block] as execution block
|
* [safely] method with [safelyExceptionHandler] as onException parameter and [block] as execution block
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
"This function become redundant since coroutines correctly handling throwing exceptions",
|
||||||
|
)
|
||||||
suspend fun <T> safelyWithContextExceptionHandler(
|
suspend fun <T> safelyWithContextExceptionHandler(
|
||||||
contextExceptionHandler: ExceptionHandler<Unit>,
|
contextExceptionHandler: ExceptionHandler<Unit>,
|
||||||
safelyExceptionHandler: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
safelyExceptionHandler: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||||
@@ -94,6 +113,10 @@ suspend fun <T> safelyWithContextExceptionHandler(
|
|||||||
*
|
*
|
||||||
* @see runCatchingSafely
|
* @see runCatchingSafely
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
"This function become redundant since coroutines correctly handling throwing exceptions",
|
||||||
|
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { onException(it) }.getOrThrow()")
|
||||||
|
)
|
||||||
suspend inline fun <T> safely(
|
suspend inline fun <T> safely(
|
||||||
onException: ExceptionHandler<T>,
|
onException: ExceptionHandler<T>,
|
||||||
block: suspend () -> T
|
block: suspend () -> T
|
||||||
@@ -104,9 +127,17 @@ suspend inline fun <T> safely(
|
|||||||
*
|
*
|
||||||
* @see runCatchingSafely
|
* @see runCatchingSafely
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
"This function become redundant since coroutines correctly handling throwing exceptions",
|
||||||
|
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { defaultSafelyExceptionHandler(it) }.getOrThrow()")
|
||||||
|
)
|
||||||
suspend inline fun <T> safely(
|
suspend inline fun <T> safely(
|
||||||
block: suspend () -> T
|
block: suspend () -> T
|
||||||
): T = safely(defaultSafelyExceptionHandler, block)
|
): T = safely(defaultSafelyExceptionHandler, block)
|
||||||
|
@Deprecated(
|
||||||
|
"This function become redundant since coroutines correctly handling throwing exceptions",
|
||||||
|
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { defaultSafelyExceptionHandler(it) }.getOrThrow()")
|
||||||
|
)
|
||||||
suspend inline fun <T, R> R.safely(
|
suspend inline fun <T, R> R.safely(
|
||||||
block: suspend R.() -> T
|
block: suspend R.() -> T
|
||||||
): T = safely<T> { block() }
|
): T = safely<T> { block() }
|
||||||
@@ -137,11 +168,19 @@ val defaultSafelyWithoutExceptionHandlerWithNull: ExceptionHandler<Nothing?> = {
|
|||||||
* Shortcut for [safely] with exception handler, that as expected must return null in case of impossible creating of
|
* Shortcut for [safely] with exception handler, that as expected must return null in case of impossible creating of
|
||||||
* result from exception (instead of throwing it, by default always returns null)
|
* result from exception (instead of throwing it, by default always returns null)
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
"This function become redundant since coroutines correctly handling throwing exceptions",
|
||||||
|
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { onException(it) }.getOrNull()")
|
||||||
|
)
|
||||||
suspend fun <T> safelyWithoutExceptions(
|
suspend fun <T> safelyWithoutExceptions(
|
||||||
onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||||
block: suspend () -> T
|
block: suspend () -> T
|
||||||
): T? = runCatchingSafely(onException, block).getOrNull()
|
): T? = runCatchingSafely(onException, block).getOrNull()
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
"This function become redundant since coroutines correctly handling throwing exceptions",
|
||||||
|
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { onException(it) }.getOrNull()")
|
||||||
|
)
|
||||||
suspend fun <T> runCatchingSafelyWithoutExceptions(
|
suspend fun <T> runCatchingSafelyWithoutExceptions(
|
||||||
onException: ExceptionHandler<T?> = defaultSafelyExceptionHandler,
|
onException: ExceptionHandler<T?> = defaultSafelyExceptionHandler,
|
||||||
block: suspend () -> T
|
block: suspend () -> T
|
||||||
|
@@ -0,0 +1,55 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.e
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
|
fun CoroutineScope.launchLogging(
|
||||||
|
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
|
||||||
|
logger: KSLog = KSLog,
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
) = launch(context, start) {
|
||||||
|
runCatching { block() }.onFailure {
|
||||||
|
logger.e(it) { errorMessageBuilder(it) }
|
||||||
|
}.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CoroutineScope.launchLoggingDropExceptions(
|
||||||
|
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
|
||||||
|
logger: KSLog = KSLog,
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
) = launch(context, start) {
|
||||||
|
runCatching { block() }.onFailure {
|
||||||
|
logger.e(it) { errorMessageBuilder(it) }
|
||||||
|
} // just dropping exception
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> CoroutineScope.asyncLogging(
|
||||||
|
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
|
||||||
|
logger: KSLog = KSLog,
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> T
|
||||||
|
) = async(context, start) {
|
||||||
|
runCatching { block() }.onFailure {
|
||||||
|
logger.e(it) { errorMessageBuilder(it) }
|
||||||
|
}.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> CoroutineScope.asyncLoggingDropExceptions(
|
||||||
|
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
|
||||||
|
logger: KSLog = KSLog,
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> T
|
||||||
|
) = async(context, start) {
|
||||||
|
runCatching { block() }.onFailure {
|
||||||
|
logger.e(it) { errorMessageBuilder(it) }
|
||||||
|
}
|
||||||
|
}
|
@@ -4,6 +4,10 @@ import kotlinx.coroutines.*
|
|||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
"This method will be removed soon. Use launchLogging instead",
|
||||||
|
ReplaceWith("this.launchLogging(context = context, start = start, block = block)")
|
||||||
|
)
|
||||||
fun CoroutineScope.launchSafely(
|
fun CoroutineScope.launchSafely(
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
@@ -15,6 +19,10 @@ fun CoroutineScope.launchSafely(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
"This method will be removed soon. Use launchLoggingDropExceptions instead",
|
||||||
|
ReplaceWith("this.launchLoggingDropExceptions(context = context, start = start, block = block)")
|
||||||
|
)
|
||||||
fun CoroutineScope.launchSafelyWithoutExceptions(
|
fun CoroutineScope.launchSafelyWithoutExceptions(
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
@@ -26,6 +34,10 @@ fun CoroutineScope.launchSafelyWithoutExceptions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
"This method will be removed soon. Use asyncLogging instead",
|
||||||
|
ReplaceWith("this.asyncLogging(context = context, start = start, block = block)")
|
||||||
|
)
|
||||||
fun <T> CoroutineScope.asyncSafely(
|
fun <T> CoroutineScope.asyncSafely(
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
@@ -37,6 +49,10 @@ fun <T> CoroutineScope.asyncSafely(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
"This method will be removed soon. Use asyncLoggingDropExceptions instead",
|
||||||
|
ReplaceWith("this.asyncLoggingDropExceptions(context = context, start = start, block = block)")
|
||||||
|
)
|
||||||
fun <T> CoroutineScope.asyncSafelyWithoutExceptions(
|
fun <T> CoroutineScope.asyncSafelyWithoutExceptions(
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
inline fun <T> Result<T>.replaceIfFailure(onException: (Throwable) -> T) = if (isSuccess) { this } else { runCatching { onException(exceptionOrNull()!!) } }
|
@@ -0,0 +1,12 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.e
|
||||||
|
|
||||||
|
inline fun <T, R> R.runCatchingLogging(
|
||||||
|
noinline errorMessageBuilder: R.(Throwable) -> Any = { "Something web wrong" },
|
||||||
|
logger: KSLog = KSLog,
|
||||||
|
block: R.() -> T
|
||||||
|
) = runCatching(block).onFailure {
|
||||||
|
logger.e(it) { errorMessageBuilder(it) }
|
||||||
|
}
|
@@ -13,7 +13,7 @@ import kotlin.contracts.contract
|
|||||||
* * [unlockWrite] will just unlock [writeMutex]
|
* * [unlockWrite] will just unlock [writeMutex]
|
||||||
*/
|
*/
|
||||||
class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked: Boolean = false) {
|
class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked: Boolean = false) {
|
||||||
private val _readSemaphore = SmartSemaphore.Mutable(permits = readPermits, acquiredPermits = 0)
|
private val _readSemaphore = SmartSemaphore.Mutable(permits = readPermits, acquiredPermits = if (writeIsLocked) readPermits else 0)
|
||||||
private val _writeMutex = SmartMutex.Mutable(locked = writeIsLocked)
|
private val _writeMutex = SmartMutex.Mutable(locked = writeIsLocked)
|
||||||
|
|
||||||
val readSemaphore: SmartSemaphore.Immutable = _readSemaphore.immutable()
|
val readSemaphore: SmartSemaphore.Immutable = _readSemaphore.immutable()
|
||||||
|
@@ -0,0 +1,318 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines.collections
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.coroutines.SmartRWLocker
|
||||||
|
import dev.inmo.micro_utils.coroutines.withReadAcquire
|
||||||
|
import dev.inmo.micro_utils.coroutines.withWriteLock
|
||||||
|
import kotlinx.coroutines.job
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates simple [Comparator] which will use [compareTo] of [T] for both objects
|
||||||
|
*/
|
||||||
|
private fun <T : Comparable<C>, C : T> T.createComparator() = Comparator<C> { o1, o2 -> o1.compareTo(o2) }
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SortedBinaryTreeNode<T>(
|
||||||
|
val data: T,
|
||||||
|
internal val comparator: Comparator<T>,
|
||||||
|
) : Iterable<SortedBinaryTreeNode<T>> {
|
||||||
|
internal var leftNode: SortedBinaryTreeNode<T>? = null
|
||||||
|
internal var rightNode: SortedBinaryTreeNode<T>? = null
|
||||||
|
internal val locker: SmartRWLocker by lazy {
|
||||||
|
SmartRWLocker()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getLeftNode() = locker.withReadAcquire {
|
||||||
|
leftNode
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getRightNode() = locker.withReadAcquire {
|
||||||
|
rightNode
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getLeft() = getLeftNode() ?.data
|
||||||
|
|
||||||
|
suspend fun getRight() = getRightNode() ?.data
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return other === this || (other is SortedBinaryTreeNode<*> && other.data == data && other.rightNode == rightNode && other.leftNode == leftNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return data.hashCode() * 31 + rightNode.hashCode() + leftNode.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun size(): Int {
|
||||||
|
return locker.withReadAcquire {
|
||||||
|
1 + (leftNode ?.size() ?: 0) + (rightNode ?.size() ?: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This [Iterator] will run from less to greater values of nodes starting the
|
||||||
|
* [dev.inmo.micro_utils.coroutines.collections.SortedBinaryTreeNode]-receiver. Due to non-suspending
|
||||||
|
* nature of [iterator] builder, this [Iterator] **DO NOT** guarantee consistent content due to iterations. It
|
||||||
|
* means, that tree can be changed during to iteration process
|
||||||
|
*/
|
||||||
|
override fun iterator(): Iterator<SortedBinaryTreeNode<T>> = iterator {
|
||||||
|
leftNode ?.let {
|
||||||
|
it.iterator().forEach { yield(it) }
|
||||||
|
}
|
||||||
|
yield(this@SortedBinaryTreeNode)
|
||||||
|
rightNode ?.let {
|
||||||
|
it.iterator().forEach { yield(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$data($leftNode;$rightNode)"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
operator fun <T : Comparable<T>> invoke(
|
||||||
|
data: T,
|
||||||
|
) = SortedBinaryTreeNode(
|
||||||
|
data,
|
||||||
|
data.createComparator()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will add subnode in tree if there are no any node with [newData]
|
||||||
|
*
|
||||||
|
* * If [newData] is greater than [SortedBinaryTreeNode.data] of currently checking node,
|
||||||
|
* will be used [SortedBinaryTreeNode.rightNode]
|
||||||
|
* * If [newData] is equal to [SortedBinaryTreeNode.data] of currently
|
||||||
|
* checking node - will be returned currently checking node
|
||||||
|
* * If [newData] is less than [SortedBinaryTreeNode.data] of currently
|
||||||
|
* checking node - will be used [SortedBinaryTreeNode.leftNode]
|
||||||
|
*
|
||||||
|
* This process will continue until function will not find place to put [SortedBinaryTreeNode] with data or
|
||||||
|
* [SortedBinaryTreeNode] with [SortedBinaryTreeNode.data] same as [newData] will be found
|
||||||
|
*/
|
||||||
|
private suspend fun <T> SortedBinaryTreeNode<T>.upsertSubNode(
|
||||||
|
subNode: SortedBinaryTreeNode<T>,
|
||||||
|
skipLockers: Set<SmartRWLocker> = emptySet()
|
||||||
|
): SortedBinaryTreeNode<T> {
|
||||||
|
var currentlyChecking = this
|
||||||
|
val lockedLockers = mutableSetOf<SmartRWLocker>()
|
||||||
|
try {
|
||||||
|
while (coroutineContext.job.isActive) {
|
||||||
|
if (currentlyChecking.locker !in lockedLockers && currentlyChecking.locker !in skipLockers) {
|
||||||
|
currentlyChecking.locker.lockWrite()
|
||||||
|
lockedLockers.add(currentlyChecking.locker)
|
||||||
|
}
|
||||||
|
val left = currentlyChecking.leftNode
|
||||||
|
val right = currentlyChecking.rightNode
|
||||||
|
val comparingResult = currentlyChecking.comparator.compare(subNode.data, currentlyChecking.data)
|
||||||
|
val isGreater = comparingResult > 0
|
||||||
|
when {
|
||||||
|
comparingResult == 0 -> return currentlyChecking
|
||||||
|
isGreater && right == null -> {
|
||||||
|
currentlyChecking.rightNode = subNode
|
||||||
|
return subNode
|
||||||
|
}
|
||||||
|
isGreater && right != null -> {
|
||||||
|
currentlyChecking = right
|
||||||
|
}
|
||||||
|
left == null -> {
|
||||||
|
currentlyChecking.leftNode = subNode
|
||||||
|
return subNode
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
currentlyChecking = left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lockedLockers.forEach {
|
||||||
|
runCatching { it.unlockWrite() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error("Unable to add node")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will add subnode in tree if there are no any node with [newData]
|
||||||
|
*
|
||||||
|
* * If [newData] is greater than [SortedBinaryTreeNode.data] of currently checking node,
|
||||||
|
* will be used [SortedBinaryTreeNode.rightNode]
|
||||||
|
* * If [newData] is equal to [SortedBinaryTreeNode.data] of currently
|
||||||
|
* checking node - will be returned currently checking node
|
||||||
|
* * If [newData] is less than [SortedBinaryTreeNode.data] of currently
|
||||||
|
* checking node - will be used [SortedBinaryTreeNode.leftNode]
|
||||||
|
*
|
||||||
|
* This process will continue until function will not find place to put [SortedBinaryTreeNode] with data or
|
||||||
|
* [SortedBinaryTreeNode] with [SortedBinaryTreeNode.data] same as [newData] will be found
|
||||||
|
*/
|
||||||
|
suspend fun <T> SortedBinaryTreeNode<T>.addSubNode(newData: T): SortedBinaryTreeNode<T> {
|
||||||
|
return upsertSubNode(
|
||||||
|
SortedBinaryTreeNode(newData, comparator)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> SortedBinaryTreeNode<T>.findParentNode(data: T): SortedBinaryTreeNode<T>? {
|
||||||
|
var currentParent: SortedBinaryTreeNode<T>? = null
|
||||||
|
var currentlyChecking: SortedBinaryTreeNode<T>? = this
|
||||||
|
val lockedLockers = mutableSetOf<SmartRWLocker>()
|
||||||
|
try {
|
||||||
|
while (coroutineContext.job.isActive) {
|
||||||
|
if (currentlyChecking == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (currentlyChecking.locker !in lockedLockers) {
|
||||||
|
currentlyChecking.locker.acquireRead()
|
||||||
|
lockedLockers.add(currentlyChecking.locker)
|
||||||
|
}
|
||||||
|
val comparingResult = currentlyChecking.comparator.compare(data, currentlyChecking.data)
|
||||||
|
when {
|
||||||
|
comparingResult > 0 -> {
|
||||||
|
currentParent = currentlyChecking
|
||||||
|
currentlyChecking = currentlyChecking.rightNode
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
comparingResult < 0 -> {
|
||||||
|
currentParent = currentlyChecking
|
||||||
|
currentlyChecking = currentlyChecking.leftNode
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
else -> return currentParent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lockedLockers.forEach {
|
||||||
|
runCatching { it.releaseRead() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error("Unable to find node")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will remove (detach) node from tree starting with [this] [SortedBinaryTreeNode]
|
||||||
|
*
|
||||||
|
* @return If data were found, [Pair] where [Pair.first] is the parent node where from [Pair.second] has been detached;
|
||||||
|
* null otherwise
|
||||||
|
*/
|
||||||
|
suspend fun <T> SortedBinaryTreeNode<T>.removeSubNode(data: T): Pair<SortedBinaryTreeNode<T>, SortedBinaryTreeNode<T>>? {
|
||||||
|
val onFoundToRemoveCallback: suspend SortedBinaryTreeNode<T>.(left: SortedBinaryTreeNode<T>?, right: SortedBinaryTreeNode<T>?) -> Unit = { left, right ->
|
||||||
|
left ?.also { leftNode -> upsertSubNode(leftNode, setOf(locker)) }
|
||||||
|
right ?.also { rightNode -> upsertSubNode(rightNode, setOf(locker)) }
|
||||||
|
}
|
||||||
|
while (coroutineContext.job.isActive) {
|
||||||
|
val foundParentNode = findParentNode(data) ?: return null
|
||||||
|
foundParentNode.locker.withWriteLock {
|
||||||
|
val left = foundParentNode.leftNode
|
||||||
|
val right = foundParentNode.rightNode
|
||||||
|
when {
|
||||||
|
left != null && left.comparator.compare(data, left.data) == 0 -> {
|
||||||
|
foundParentNode.leftNode = null
|
||||||
|
foundParentNode.onFoundToRemoveCallback(left.leftNode, left.rightNode)
|
||||||
|
return foundParentNode to left
|
||||||
|
}
|
||||||
|
right != null && right.comparator.compare(data, right.data) == 0 -> {
|
||||||
|
foundParentNode.rightNode = null
|
||||||
|
foundParentNode.onFoundToRemoveCallback(right.leftNode, right.rightNode)
|
||||||
|
return foundParentNode to right
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
return@withWriteLock // data has been changed, new search required
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error("Unable to remove node")
|
||||||
|
}
|
||||||
|
suspend fun <T> SortedBinaryTreeNode<T>.findNode(data: T): SortedBinaryTreeNode<T>? {
|
||||||
|
var currentlyChecking: SortedBinaryTreeNode<T>? = this
|
||||||
|
val lockedLockers = mutableSetOf<SmartRWLocker>()
|
||||||
|
try {
|
||||||
|
while (coroutineContext.job.isActive) {
|
||||||
|
if (currentlyChecking == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (currentlyChecking.locker !in lockedLockers) {
|
||||||
|
currentlyChecking.locker.acquireRead()
|
||||||
|
lockedLockers.add(currentlyChecking.locker)
|
||||||
|
}
|
||||||
|
val comparingResult = currentlyChecking.comparator.compare(data, currentlyChecking.data)
|
||||||
|
when {
|
||||||
|
comparingResult > 0 -> {
|
||||||
|
currentlyChecking = currentlyChecking.rightNode
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
comparingResult < 0 -> {
|
||||||
|
currentlyChecking = currentlyChecking.leftNode
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
else -> return currentlyChecking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lockedLockers.forEach {
|
||||||
|
runCatching { it.releaseRead() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error("Unable to find node")
|
||||||
|
}
|
||||||
|
suspend fun <T> SortedBinaryTreeNode<T>.contains(data: T): Boolean = findNode(data) != null
|
||||||
|
|
||||||
|
suspend fun <T> SortedBinaryTreeNode<T>.findNodesInRange(from: T, to: T, fromInclusiveMode: Boolean, toInclusiveMode: Boolean): Set<SortedBinaryTreeNode<T>> {
|
||||||
|
val results = mutableSetOf<SortedBinaryTreeNode<T>>()
|
||||||
|
val leftToCheck = mutableSetOf(this)
|
||||||
|
val lockedLockers = mutableSetOf<SmartRWLocker>()
|
||||||
|
val fromComparingFun: (SortedBinaryTreeNode<T>) -> Boolean = if (fromInclusiveMode) {
|
||||||
|
{ it.comparator.compare(from, it.data) <= 0 }
|
||||||
|
} else {
|
||||||
|
{ it.comparator.compare(from, it.data) < 0 }
|
||||||
|
}
|
||||||
|
val toComparingFun: (SortedBinaryTreeNode<T>) -> Boolean = if (toInclusiveMode) {
|
||||||
|
{ it.comparator.compare(to, it.data) >= 0 }
|
||||||
|
} else {
|
||||||
|
{ it.comparator.compare(to, it.data) > 0 }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
while (coroutineContext.job.isActive && leftToCheck.isNotEmpty()) {
|
||||||
|
val currentlyChecking = leftToCheck.first()
|
||||||
|
leftToCheck.remove(currentlyChecking)
|
||||||
|
if (currentlyChecking in results) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
currentlyChecking.locker.acquireRead()
|
||||||
|
lockedLockers.add(currentlyChecking.locker)
|
||||||
|
if (fromComparingFun(currentlyChecking) && toComparingFun(currentlyChecking)) {
|
||||||
|
results.add(currentlyChecking)
|
||||||
|
currentlyChecking.leftNode ?.let { leftToCheck.add(it) }
|
||||||
|
currentlyChecking.rightNode ?.let { leftToCheck.add(it) }
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
when {
|
||||||
|
currentlyChecking.comparator.compare(to, currentlyChecking.data) < 0 -> currentlyChecking.leftNode ?.let { leftToCheck.add(it) }
|
||||||
|
currentlyChecking.comparator.compare(from, currentlyChecking.data) > 0 -> currentlyChecking.rightNode ?.let { leftToCheck.add(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results.toSet()
|
||||||
|
} finally {
|
||||||
|
lockedLockers.forEach {
|
||||||
|
runCatching { it.releaseRead() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error("Unable to find nodes range")
|
||||||
|
}
|
||||||
|
suspend fun <T> SortedBinaryTreeNode<T>.findNodesInRange(from: T, to: T): Set<SortedBinaryTreeNode<T>> = findNodesInRange(
|
||||||
|
from = from,
|
||||||
|
to = to,
|
||||||
|
fromInclusiveMode = true,
|
||||||
|
toInclusiveMode = true
|
||||||
|
)
|
||||||
|
suspend fun <T> SortedBinaryTreeNode<T>.findNodesInRangeExcluding(from: T, to: T): Set<SortedBinaryTreeNode<T>> = findNodesInRange(
|
||||||
|
from = from,
|
||||||
|
to = to,
|
||||||
|
fromInclusiveMode = false,
|
||||||
|
toInclusiveMode = false
|
||||||
|
)
|
||||||
|
suspend fun <T : Comparable<T>> SortedBinaryTreeNode<T>.findNodesInRange(range: ClosedRange<T>): Set<SortedBinaryTreeNode<T>> = findNodesInRange(
|
||||||
|
from = range.start,
|
||||||
|
to = range.endInclusive,
|
||||||
|
)
|
@@ -0,0 +1,401 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines.collections
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.coroutines.SmartRWLocker
|
||||||
|
import dev.inmo.micro_utils.coroutines.withReadAcquire
|
||||||
|
import dev.inmo.micro_utils.coroutines.withWriteLock
|
||||||
|
import kotlinx.coroutines.job
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates simple [Comparator] which will use [compareTo] of [T] for both objects
|
||||||
|
*/
|
||||||
|
private fun <T : Comparable<C>, C : T> T.createComparator() = Comparator<C> { o1, o2 -> o1.compareTo(o2) }
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SortedMapLikeBinaryTreeNode<K, V>(
|
||||||
|
val key: K,
|
||||||
|
val value: V,
|
||||||
|
internal val comparator: Comparator<K>,
|
||||||
|
) : Iterable<SortedMapLikeBinaryTreeNode<K, V>> {
|
||||||
|
internal var leftNode: SortedMapLikeBinaryTreeNode<K, V>? = null
|
||||||
|
internal var rightNode: SortedMapLikeBinaryTreeNode<K, V>? = null
|
||||||
|
internal val locker: SmartRWLocker by lazy {
|
||||||
|
SmartRWLocker()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getLeftNode() = locker.withReadAcquire {
|
||||||
|
leftNode
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getRightNode() = locker.withReadAcquire {
|
||||||
|
rightNode
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getLeftKey() = getLeftNode() ?.key
|
||||||
|
suspend fun getLeftValue() = getLeftNode() ?.value
|
||||||
|
|
||||||
|
suspend fun getRightKey() = getRightNode() ?.value
|
||||||
|
suspend fun getRightValue() = getRightNode() ?.value
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return other === this || (other is SortedMapLikeBinaryTreeNode<*, *> && other.key == key && other.rightNode == rightNode && other.leftNode == leftNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return key.hashCode() * 31 + rightNode.hashCode() + leftNode.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun size(): Int {
|
||||||
|
return locker.withReadAcquire {
|
||||||
|
1 + (leftNode ?.size() ?: 0) + (rightNode ?.size() ?: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This [Iterator] will run from less to greater values of nodes starting the
|
||||||
|
* [dev.inmo.micro_utils.coroutines.collections.SortedMapLikeBinaryTreeNode]-receiver. Due to non-suspending
|
||||||
|
* nature of [iterator] builder, this [Iterator] **DO NOT** guarantee consistent content due to iterations. It
|
||||||
|
* means, that tree can be changed during to iteration process
|
||||||
|
*/
|
||||||
|
override fun iterator(): Iterator<SortedMapLikeBinaryTreeNode<K, V>> = iterator {
|
||||||
|
leftNode ?.let {
|
||||||
|
it.iterator().forEach { yield(it) }
|
||||||
|
}
|
||||||
|
yield(this@SortedMapLikeBinaryTreeNode)
|
||||||
|
rightNode ?.let {
|
||||||
|
it.iterator().forEach { yield(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$key($leftNode;$rightNode)"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
operator fun <K : Comparable<K>, V> invoke(
|
||||||
|
key: K,
|
||||||
|
value: V
|
||||||
|
) = SortedMapLikeBinaryTreeNode(
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
key.createComparator()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will add subnode in tree if there are no any node with [newData]
|
||||||
|
*
|
||||||
|
* * If [newData] is greater than [SortedMapLikeBinaryTreeNode.key] of currently checking node,
|
||||||
|
* will be used [SortedMapLikeBinaryTreeNode.rightNode]
|
||||||
|
* * If [newData] is equal to [SortedMapLikeBinaryTreeNode.key] of currently
|
||||||
|
* checking node - will be returned currently checking node
|
||||||
|
* * If [newData] is less than [SortedMapLikeBinaryTreeNode.key] of currently
|
||||||
|
* checking node - will be used [SortedMapLikeBinaryTreeNode.leftNode]
|
||||||
|
*
|
||||||
|
* This process will continue until function will not find place to put [SortedMapLikeBinaryTreeNode] with data or
|
||||||
|
* [SortedMapLikeBinaryTreeNode] with [SortedMapLikeBinaryTreeNode.key] same as [newData] will be found
|
||||||
|
*
|
||||||
|
* @param replaceMode Will replace only value if node already exists
|
||||||
|
*/
|
||||||
|
private suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.upsertSubNode(
|
||||||
|
subNode: SortedMapLikeBinaryTreeNode<K, V>,
|
||||||
|
skipLockers: Set<SmartRWLocker> = emptySet(),
|
||||||
|
replaceMode: Boolean
|
||||||
|
): SortedMapLikeBinaryTreeNode<K, V> {
|
||||||
|
var currentlyChecking = this
|
||||||
|
var latestParent: SortedMapLikeBinaryTreeNode<K, V>? = null
|
||||||
|
val lockedLockers = mutableSetOf<SmartRWLocker>()
|
||||||
|
try {
|
||||||
|
while (coroutineContext.job.isActive) {
|
||||||
|
if (currentlyChecking.locker !in lockedLockers && currentlyChecking.locker !in skipLockers) {
|
||||||
|
currentlyChecking.locker.lockWrite()
|
||||||
|
lockedLockers.add(currentlyChecking.locker)
|
||||||
|
}
|
||||||
|
val left = currentlyChecking.leftNode
|
||||||
|
val right = currentlyChecking.rightNode
|
||||||
|
val comparingResult = currentlyChecking.comparator.compare(subNode.key, currentlyChecking.key)
|
||||||
|
val isGreater = comparingResult > 0
|
||||||
|
when {
|
||||||
|
comparingResult == 0 -> {
|
||||||
|
val resultNode = if (replaceMode) {
|
||||||
|
subNode
|
||||||
|
} else {
|
||||||
|
val newNode = SortedMapLikeBinaryTreeNode(
|
||||||
|
subNode.key,
|
||||||
|
subNode.value,
|
||||||
|
currentlyChecking.comparator,
|
||||||
|
)
|
||||||
|
newNode.leftNode = currentlyChecking.leftNode
|
||||||
|
newNode.rightNode = currentlyChecking.rightNode
|
||||||
|
newNode
|
||||||
|
}
|
||||||
|
|
||||||
|
latestParent ?.let {
|
||||||
|
when {
|
||||||
|
it.leftNode === currentlyChecking -> it.leftNode = resultNode
|
||||||
|
it.rightNode === currentlyChecking -> it.rightNode = resultNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultNode
|
||||||
|
}
|
||||||
|
isGreater && right == null -> {
|
||||||
|
currentlyChecking.rightNode = subNode
|
||||||
|
return subNode
|
||||||
|
}
|
||||||
|
isGreater && right != null -> {
|
||||||
|
latestParent = currentlyChecking
|
||||||
|
currentlyChecking = right
|
||||||
|
}
|
||||||
|
left == null -> {
|
||||||
|
currentlyChecking.leftNode = subNode
|
||||||
|
return subNode
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
latestParent = currentlyChecking
|
||||||
|
currentlyChecking = left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lockedLockers.forEach {
|
||||||
|
runCatching { it.unlockWrite() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error("Unable to add node")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will add subnode in tree if there are no any node with [key]
|
||||||
|
*
|
||||||
|
* * If [key] is greater than [SortedMapLikeBinaryTreeNode.key] of currently checking node,
|
||||||
|
* will be used [SortedMapLikeBinaryTreeNode.rightNode]
|
||||||
|
* * If [key] is equal to [SortedMapLikeBinaryTreeNode.key] of currently
|
||||||
|
* checking node - will be returned currently checking node
|
||||||
|
* * If [key] is less than [SortedMapLikeBinaryTreeNode.key] of currently
|
||||||
|
* checking node - will be used [SortedMapLikeBinaryTreeNode.leftNode]
|
||||||
|
*
|
||||||
|
* This process will continue until function will not find place to put [SortedMapLikeBinaryTreeNode] with data or
|
||||||
|
* [SortedMapLikeBinaryTreeNode] with [SortedMapLikeBinaryTreeNode.key] same as [key] will be found
|
||||||
|
*/
|
||||||
|
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.upsertSubNode(
|
||||||
|
key: K,
|
||||||
|
value: V
|
||||||
|
): SortedMapLikeBinaryTreeNode<K, V> {
|
||||||
|
return upsertSubNode(
|
||||||
|
SortedMapLikeBinaryTreeNode(key, value, comparator),
|
||||||
|
replaceMode = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.findParentNode(data: K): SortedMapLikeBinaryTreeNode<K, V>? {
|
||||||
|
var currentParent: SortedMapLikeBinaryTreeNode<K, V>? = null
|
||||||
|
var currentlyChecking: SortedMapLikeBinaryTreeNode<K, V>? = this
|
||||||
|
val lockedLockers = mutableSetOf<SmartRWLocker>()
|
||||||
|
try {
|
||||||
|
while (coroutineContext.job.isActive) {
|
||||||
|
if (currentlyChecking == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (currentlyChecking.locker !in lockedLockers) {
|
||||||
|
currentlyChecking.locker.acquireRead()
|
||||||
|
lockedLockers.add(currentlyChecking.locker)
|
||||||
|
}
|
||||||
|
val comparingResult = currentlyChecking.comparator.compare(data, currentlyChecking.key)
|
||||||
|
when {
|
||||||
|
comparingResult > 0 -> {
|
||||||
|
currentParent = currentlyChecking
|
||||||
|
currentlyChecking = currentlyChecking.rightNode
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
comparingResult < 0 -> {
|
||||||
|
currentParent = currentlyChecking
|
||||||
|
currentlyChecking = currentlyChecking.leftNode
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
else -> return currentParent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lockedLockers.forEach {
|
||||||
|
runCatching { it.releaseRead() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error("Unable to find node")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will remove (detach) node from tree starting with [this] [SortedMapLikeBinaryTreeNode]
|
||||||
|
*
|
||||||
|
* @return If data were found, [Pair] where [Pair.first] is the parent node where from [Pair.second] has been detached;
|
||||||
|
* null otherwise
|
||||||
|
*/
|
||||||
|
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.removeSubNode(data: K): Pair<SortedMapLikeBinaryTreeNode<K, V>, SortedMapLikeBinaryTreeNode<K, V>>? {
|
||||||
|
val onFoundToRemoveCallback: suspend SortedMapLikeBinaryTreeNode<K, V>.(left: SortedMapLikeBinaryTreeNode<K, V>?, right: SortedMapLikeBinaryTreeNode<K, V>?) -> Unit = { left, right ->
|
||||||
|
left ?.also { leftNode -> upsertSubNode(leftNode, setOf(locker), replaceMode = true) }
|
||||||
|
right ?.also { rightNode -> upsertSubNode(rightNode, setOf(locker), replaceMode = true) }
|
||||||
|
}
|
||||||
|
while (coroutineContext.job.isActive) {
|
||||||
|
val foundParentNode = findParentNode(data) ?: return null
|
||||||
|
foundParentNode.locker.withWriteLock {
|
||||||
|
val left = foundParentNode.leftNode
|
||||||
|
val right = foundParentNode.rightNode
|
||||||
|
when {
|
||||||
|
left != null && left.comparator.compare(data, left.key) == 0 -> {
|
||||||
|
foundParentNode.leftNode = null
|
||||||
|
foundParentNode.onFoundToRemoveCallback(left.leftNode, left.rightNode)
|
||||||
|
return foundParentNode to left
|
||||||
|
}
|
||||||
|
right != null && right.comparator.compare(data, right.key) == 0 -> {
|
||||||
|
foundParentNode.rightNode = null
|
||||||
|
foundParentNode.onFoundToRemoveCallback(right.leftNode, right.rightNode)
|
||||||
|
return foundParentNode to right
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
return@withWriteLock // data has been changed, new search required
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error("Unable to remove node")
|
||||||
|
}
|
||||||
|
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.findNode(key: K): SortedMapLikeBinaryTreeNode<K, V>? {
|
||||||
|
var currentlyChecking: SortedMapLikeBinaryTreeNode<K, V>? = this
|
||||||
|
val lockedLockers = mutableSetOf<SmartRWLocker>()
|
||||||
|
try {
|
||||||
|
while (coroutineContext.job.isActive) {
|
||||||
|
if (currentlyChecking == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (currentlyChecking.locker !in lockedLockers) {
|
||||||
|
currentlyChecking.locker.acquireRead()
|
||||||
|
lockedLockers.add(currentlyChecking.locker)
|
||||||
|
}
|
||||||
|
val comparingResult = currentlyChecking.comparator.compare(key, currentlyChecking.key)
|
||||||
|
when {
|
||||||
|
comparingResult > 0 -> {
|
||||||
|
currentlyChecking = currentlyChecking.rightNode
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
comparingResult < 0 -> {
|
||||||
|
currentlyChecking = currentlyChecking.leftNode
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
else -> return currentlyChecking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lockedLockers.forEach {
|
||||||
|
runCatching { it.releaseRead() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error("Unable to find node")
|
||||||
|
}
|
||||||
|
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.contains(data: K): Boolean = findNode(data) != null
|
||||||
|
|
||||||
|
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.findNodesInRange(from: K, to: K, fromInclusiveMode: Boolean, toInclusiveMode: Boolean): Set<SortedMapLikeBinaryTreeNode<K, V>> {
|
||||||
|
val results = mutableSetOf<SortedMapLikeBinaryTreeNode<K, V>>()
|
||||||
|
val leftToCheck = mutableSetOf(this)
|
||||||
|
val lockedLockers = mutableSetOf<SmartRWLocker>()
|
||||||
|
val fromComparingFun: (SortedMapLikeBinaryTreeNode<K, V>) -> Boolean = if (fromInclusiveMode) {
|
||||||
|
{ it.comparator.compare(from, it.key) <= 0 }
|
||||||
|
} else {
|
||||||
|
{ it.comparator.compare(from, it.key) < 0 }
|
||||||
|
}
|
||||||
|
val toComparingFun: (SortedMapLikeBinaryTreeNode<K, V>) -> Boolean = if (toInclusiveMode) {
|
||||||
|
{ it.comparator.compare(to, it.key) >= 0 }
|
||||||
|
} else {
|
||||||
|
{ it.comparator.compare(to, it.key) > 0 }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
while (coroutineContext.job.isActive && leftToCheck.isNotEmpty()) {
|
||||||
|
val currentlyChecking = leftToCheck.first()
|
||||||
|
leftToCheck.remove(currentlyChecking)
|
||||||
|
if (currentlyChecking in results) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
currentlyChecking.locker.acquireRead()
|
||||||
|
lockedLockers.add(currentlyChecking.locker)
|
||||||
|
if (fromComparingFun(currentlyChecking) && toComparingFun(currentlyChecking)) {
|
||||||
|
results.add(currentlyChecking)
|
||||||
|
currentlyChecking.leftNode ?.let { leftToCheck.add(it) }
|
||||||
|
currentlyChecking.rightNode ?.let { leftToCheck.add(it) }
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
when {
|
||||||
|
currentlyChecking.comparator.compare(to, currentlyChecking.key) < 0 -> currentlyChecking.leftNode ?.let { leftToCheck.add(it) }
|
||||||
|
currentlyChecking.comparator.compare(from, currentlyChecking.key) > 0 -> currentlyChecking.rightNode ?.let { leftToCheck.add(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results.toSet()
|
||||||
|
} finally {
|
||||||
|
lockedLockers.forEach {
|
||||||
|
runCatching { it.releaseRead() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error("Unable to find nodes range")
|
||||||
|
}
|
||||||
|
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.deepEquals(other: SortedMapLikeBinaryTreeNode<K, V>): Boolean {
|
||||||
|
val leftToCheck = mutableSetOf(this)
|
||||||
|
val othersToCheck = mutableSetOf(other)
|
||||||
|
val lockedLockers = mutableSetOf<SmartRWLocker>()
|
||||||
|
try {
|
||||||
|
while (leftToCheck.isNotEmpty() && othersToCheck.isNotEmpty()) {
|
||||||
|
val thisToCheck = leftToCheck.first()
|
||||||
|
leftToCheck.remove(thisToCheck)
|
||||||
|
|
||||||
|
val otherToCheck = othersToCheck.first()
|
||||||
|
othersToCheck.remove(otherToCheck)
|
||||||
|
|
||||||
|
if (thisToCheck.locker !in lockedLockers) {
|
||||||
|
thisToCheck.locker.acquireRead()
|
||||||
|
lockedLockers.add(thisToCheck.locker)
|
||||||
|
}
|
||||||
|
if (otherToCheck.locker !in lockedLockers) {
|
||||||
|
otherToCheck.locker.acquireRead()
|
||||||
|
lockedLockers.add(otherToCheck.locker)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thisToCheck.key != otherToCheck.key || thisToCheck.value != otherToCheck.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((thisToCheck.leftNode == null).xor(otherToCheck.leftNode == null)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if ((thisToCheck.rightNode == null).xor(otherToCheck.rightNode == null)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
thisToCheck.leftNode?.let { leftToCheck.add(it) }
|
||||||
|
thisToCheck.rightNode?.let { leftToCheck.add(it) }
|
||||||
|
|
||||||
|
otherToCheck.leftNode?.let { othersToCheck.add(it) }
|
||||||
|
otherToCheck.rightNode?.let { othersToCheck.add(it) }
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lockedLockers.forEach {
|
||||||
|
runCatching { it.releaseRead() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return leftToCheck.isEmpty() && othersToCheck.isEmpty()
|
||||||
|
}
|
||||||
|
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.findNodesInRange(from: K, to: K): Set<SortedMapLikeBinaryTreeNode<K, V>> = findNodesInRange(
|
||||||
|
from = from,
|
||||||
|
to = to,
|
||||||
|
fromInclusiveMode = true,
|
||||||
|
toInclusiveMode = true
|
||||||
|
)
|
||||||
|
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.findNodesInRangeExcluding(from: K, to: K): Set<SortedMapLikeBinaryTreeNode<K, V>> = findNodesInRange(
|
||||||
|
from = from,
|
||||||
|
to = to,
|
||||||
|
fromInclusiveMode = false,
|
||||||
|
toInclusiveMode = false
|
||||||
|
)
|
||||||
|
suspend fun <K : Comparable<K>, V> SortedMapLikeBinaryTreeNode<K, V>.findNodesInRange(range: ClosedRange<K>): Set<SortedMapLikeBinaryTreeNode<K, V>> = findNodesInRange(
|
||||||
|
from = range.start,
|
||||||
|
to = range.endInclusive,
|
||||||
|
)
|
42
coroutines/src/commonTest/kotlin/DebouncedByTests.kt
Normal file
42
coroutines/src/commonTest/kotlin/DebouncedByTests.kt
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import dev.inmo.micro_utils.coroutines.debouncedBy
|
||||||
|
import kotlinx.coroutines.flow.asFlow
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class DebouncedByTests {
|
||||||
|
@Test
|
||||||
|
fun testThatParallelDebouncingWorksCorrectly() = runTest {
|
||||||
|
val dataToMarkerFactories = listOf(
|
||||||
|
1 to 0,
|
||||||
|
2 to 1,
|
||||||
|
3 to 2,
|
||||||
|
4 to 0,
|
||||||
|
5 to 1,
|
||||||
|
6 to 2,
|
||||||
|
7 to 0,
|
||||||
|
8 to 1,
|
||||||
|
9 to 2,
|
||||||
|
)
|
||||||
|
|
||||||
|
val collected = mutableListOf<Int>()
|
||||||
|
|
||||||
|
dataToMarkerFactories.asFlow().debouncedBy(10L) {
|
||||||
|
it.second
|
||||||
|
}.collect {
|
||||||
|
when (it.second) {
|
||||||
|
0 -> assertEquals(7, it.first)
|
||||||
|
1 -> assertEquals(8, it.first)
|
||||||
|
2 -> assertEquals(9, it.first)
|
||||||
|
else -> error("wtf")
|
||||||
|
}
|
||||||
|
collected.add(it.first)
|
||||||
|
}
|
||||||
|
|
||||||
|
val expectedList = listOf(7, 8, 9)
|
||||||
|
assertEquals(expectedList, collected)
|
||||||
|
assertTrue { collected.containsAll(expectedList) }
|
||||||
|
assertTrue { expectedList.containsAll(collected) }
|
||||||
|
}
|
||||||
|
}
|
@@ -7,7 +7,9 @@ fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T
|
|||||||
val objectToSynchronize = Object()
|
val objectToSynchronize = Object()
|
||||||
synchronized(objectToSynchronize) {
|
synchronized(objectToSynchronize) {
|
||||||
launch(start = CoroutineStart.UNDISPATCHED) {
|
launch(start = CoroutineStart.UNDISPATCHED) {
|
||||||
result = safelyWithResult(block)
|
result = runCatching {
|
||||||
|
block()
|
||||||
|
}
|
||||||
}.invokeOnCompletion {
|
}.invokeOnCompletion {
|
||||||
synchronized(objectToSynchronize) {
|
synchronized(objectToSynchronize) {
|
||||||
objectToSynchronize.notifyAll()
|
objectToSynchronize.notifyAll()
|
||||||
|
@@ -1,25 +1,20 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|
||||||
class HandleSafelyCoroutineContextTest {
|
class HandleSafelyCoroutineContextTest {
|
||||||
@Test
|
@Test
|
||||||
fun testHandleSafelyCoroutineContext() {
|
fun testHandleSafelyCoroutineContext() = runTest {
|
||||||
val scope = CoroutineScope(Dispatchers.Default)
|
val scope = this
|
||||||
var contextHandlerHappen = false
|
var contextHandlerHappen = false
|
||||||
var localHandlerHappen = false
|
var localHandlerHappen = false
|
||||||
var defaultHandlerHappen = false
|
|
||||||
defaultSafelyExceptionHandler = {
|
|
||||||
defaultHandlerHappen = true
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
val contextHandler: ExceptionHandler<Unit> = {
|
|
||||||
contextHandlerHappen = true
|
|
||||||
}
|
|
||||||
val checkJob = scope.launch {
|
val checkJob = scope.launch {
|
||||||
safelyWithContextExceptionHandler(contextHandler) {
|
runCatchingLogging ({
|
||||||
safely(
|
contextHandlerHappen = true
|
||||||
|
}) {
|
||||||
|
runCatchingLogging (
|
||||||
{
|
{
|
||||||
localHandlerHappen = true
|
localHandlerHappen = true
|
||||||
}
|
}
|
||||||
@@ -29,10 +24,8 @@ class HandleSafelyCoroutineContextTest {
|
|||||||
println(coroutineContext)
|
println(coroutineContext)
|
||||||
error("That must happen too:)")
|
error("That must happen too:)")
|
||||||
}
|
}
|
||||||
}
|
}.join()
|
||||||
launchSynchronously { checkJob.join() }
|
|
||||||
assert(contextHandlerHappen)
|
assert(contextHandlerHappen)
|
||||||
assert(localHandlerHappen)
|
assert(localHandlerHappen)
|
||||||
assert(defaultHandlerHappen)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,176 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.coroutines.collections.SortedBinaryTreeNode
|
||||||
|
import dev.inmo.micro_utils.coroutines.collections.addSubNode
|
||||||
|
import dev.inmo.micro_utils.coroutines.collections.findNode
|
||||||
|
import dev.inmo.micro_utils.coroutines.collections.findNodesInRange
|
||||||
|
import dev.inmo.micro_utils.coroutines.collections.findParentNode
|
||||||
|
import dev.inmo.micro_utils.coroutines.collections.removeSubNode
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
class SortedBinaryTreeNodeTests {
|
||||||
|
@Test
|
||||||
|
fun insertOnZeroLevelWorks() = runTest {
|
||||||
|
val zeroNode = SortedBinaryTreeNode(0)
|
||||||
|
zeroNode.addSubNode(1)
|
||||||
|
zeroNode.addSubNode(-1)
|
||||||
|
|
||||||
|
assertEquals(0, zeroNode.data)
|
||||||
|
assertEquals(1, zeroNode.getRightNode() ?.data)
|
||||||
|
assertEquals(-1, zeroNode.getLeftNode() ?.data)
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun searchOnZeroLevelWorks() = runTest {
|
||||||
|
val zeroNode = SortedBinaryTreeNode(0)
|
||||||
|
val oneNode = zeroNode.addSubNode(1)
|
||||||
|
val minusOneNode = zeroNode.addSubNode(-1)
|
||||||
|
|
||||||
|
val assertingNodesToSearchQuery = mapOf(
|
||||||
|
setOf(oneNode) to (1 .. 1),
|
||||||
|
setOf(zeroNode, oneNode) to (0 .. 1),
|
||||||
|
setOf(minusOneNode, zeroNode, oneNode) to (-1 .. 1),
|
||||||
|
setOf(minusOneNode, zeroNode) to (-1 .. 0),
|
||||||
|
setOf(minusOneNode) to (-1 .. -1),
|
||||||
|
setOf(zeroNode) to (0 .. 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertingNodesToSearchQuery.forEach {
|
||||||
|
val foundData = zeroNode.findNodesInRange(it.value)
|
||||||
|
assertTrue(foundData.containsAll(it.key))
|
||||||
|
assertTrue(it.key.containsAll(foundData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun deepReInsertOnWorks() = runTest(timeout = 300.seconds) {
|
||||||
|
val zeroNode = SortedBinaryTreeNode(0)
|
||||||
|
val rangeRadius = 500
|
||||||
|
val nodes = mutableMapOf<Int, SortedBinaryTreeNode<Int>>()
|
||||||
|
for (i in -rangeRadius .. rangeRadius) {
|
||||||
|
nodes[i] = zeroNode.addSubNode(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in -rangeRadius .. rangeRadius) {
|
||||||
|
val expectedNode = nodes.getValue(i)
|
||||||
|
val foundNode = zeroNode.findNode(i)
|
||||||
|
|
||||||
|
assertTrue(expectedNode === foundNode)
|
||||||
|
|
||||||
|
if (expectedNode === zeroNode) continue
|
||||||
|
|
||||||
|
val parentNode = zeroNode.findParentNode(i)
|
||||||
|
assertTrue(
|
||||||
|
parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode,
|
||||||
|
"It is expected, that parent node with data ${parentNode ?.data} will be parent of ${expectedNode.data}, but its left subnode is ${parentNode ?.getLeftNode() ?.data} and right one is ${parentNode ?.getRightNode() ?.data}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val sourceTreeSize = zeroNode.size()
|
||||||
|
assertTrue(sourceTreeSize == nodes.size)
|
||||||
|
assertTrue(sourceTreeSize == (rangeRadius * 2 + 1))
|
||||||
|
|
||||||
|
for (i in -rangeRadius .. rangeRadius) {
|
||||||
|
val expectedNode = nodes.getValue(i)
|
||||||
|
val parentNode = zeroNode.findParentNode(i)
|
||||||
|
|
||||||
|
if (parentNode == null && i == zeroNode.data && expectedNode === zeroNode) continue
|
||||||
|
|
||||||
|
assertTrue(parentNode != null, "It is expected, that parent node of ${expectedNode.data} will not be null")
|
||||||
|
|
||||||
|
assertTrue(
|
||||||
|
parentNode.getLeftNode() === expectedNode || parentNode.getRightNode() === expectedNode,
|
||||||
|
"It is expected, that parent node with data ${parentNode ?.data} will be parent of ${expectedNode.data}, but its left subnode is ${parentNode ?.getLeftNode() ?.data} and right one is ${parentNode ?.getRightNode() ?.data}"
|
||||||
|
)
|
||||||
|
|
||||||
|
val removeResult = zeroNode.removeSubNode(i)
|
||||||
|
assertTrue(removeResult ?.first === parentNode)
|
||||||
|
assertTrue(removeResult.second === expectedNode)
|
||||||
|
|
||||||
|
nodes[i] = zeroNode.addSubNode(i)
|
||||||
|
assertTrue(nodes[i] != null)
|
||||||
|
assertTrue(nodes[i] != expectedNode)
|
||||||
|
assertTrue(nodes[i] ?.data == i)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(sourceTreeSize == zeroNode.size())
|
||||||
|
|
||||||
|
for (i in -rangeRadius .. rangeRadius) {
|
||||||
|
val expectedNode = nodes.getValue(i)
|
||||||
|
val foundNode = zeroNode.findNode(i)
|
||||||
|
|
||||||
|
assertTrue(expectedNode === foundNode)
|
||||||
|
|
||||||
|
if (expectedNode === zeroNode) continue
|
||||||
|
|
||||||
|
val parentNode = zeroNode.findParentNode(i)
|
||||||
|
assertTrue(
|
||||||
|
parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode,
|
||||||
|
"It is expected, that parent node with data ${parentNode ?.data} will be parent of ${expectedNode.data}, but its left subnode is ${parentNode ?.getLeftNode() ?.data} and right one is ${parentNode ?.getRightNode() ?.data}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var previousData = -rangeRadius - 1
|
||||||
|
for (node in zeroNode) {
|
||||||
|
assertTrue(nodes[node.data] === node)
|
||||||
|
assertTrue(previousData == node.data - 1)
|
||||||
|
previousData = node.data
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(sourceTreeSize == zeroNode.size())
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun deepInsertOnWorks() = runTest(timeout = 240.seconds) {
|
||||||
|
val zeroNode = SortedBinaryTreeNode(0)
|
||||||
|
val rangeRadius = 500
|
||||||
|
val nodes = mutableMapOf<Int, SortedBinaryTreeNode<Int>>()
|
||||||
|
for (i in -rangeRadius .. rangeRadius) {
|
||||||
|
nodes[i] = zeroNode.addSubNode(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in -rangeRadius .. rangeRadius) {
|
||||||
|
val expectedNode = nodes.getValue(i)
|
||||||
|
val foundNode = zeroNode.findNode(i)
|
||||||
|
|
||||||
|
assertTrue(expectedNode === foundNode)
|
||||||
|
|
||||||
|
if (expectedNode === zeroNode) continue
|
||||||
|
|
||||||
|
val parentNode = zeroNode.findParentNode(i)
|
||||||
|
assertTrue(
|
||||||
|
parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode,
|
||||||
|
"It is expected, that parent node with data ${parentNode ?.data} will be parent of ${expectedNode.data}, but its left subnode is ${parentNode ?.getLeftNode() ?.data} and right one is ${parentNode ?.getRightNode() ?.data}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val sourceTreeSize = zeroNode.size()
|
||||||
|
|
||||||
|
var previousData = -rangeRadius - 1
|
||||||
|
for (node in zeroNode) {
|
||||||
|
assertTrue(nodes[node.data] === node)
|
||||||
|
assertTrue(previousData == node.data - 1)
|
||||||
|
previousData = node.data
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(sourceTreeSize == zeroNode.size())
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun deepInsertIteratorWorking() = runTest {
|
||||||
|
val zeroNode = SortedBinaryTreeNode(0)
|
||||||
|
val rangeRadius = 500
|
||||||
|
val nodes = mutableMapOf<Int, SortedBinaryTreeNode<Int>>()
|
||||||
|
for (i in -rangeRadius .. rangeRadius) {
|
||||||
|
nodes[i] = zeroNode.addSubNode(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
var previousData = -rangeRadius - 1
|
||||||
|
for (node in zeroNode) {
|
||||||
|
assertTrue(nodes[node.data] === node)
|
||||||
|
assertTrue(previousData == node.data - 1)
|
||||||
|
previousData = node.data
|
||||||
|
}
|
||||||
|
assertTrue(previousData == rangeRadius)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,118 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.coroutines.collections.*
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
class SortedMapLikeBinaryTreeNodeTests {
|
||||||
|
@Test
|
||||||
|
fun insertOnZeroLevelWorks() = runTest {
|
||||||
|
val zeroNode = SortedMapLikeBinaryTreeNode(0, 0)
|
||||||
|
zeroNode.upsertSubNode(1, 1)
|
||||||
|
zeroNode.upsertSubNode(-1, -1)
|
||||||
|
|
||||||
|
assertEquals(0, zeroNode.key)
|
||||||
|
assertEquals(1, zeroNode.getRightNode() ?.key)
|
||||||
|
assertEquals(-1, zeroNode.getLeftNode() ?.key)
|
||||||
|
|
||||||
|
assertEquals(0, zeroNode.findNode(0) ?.value)
|
||||||
|
assertEquals(1, zeroNode.findNode(1) ?.value)
|
||||||
|
assertEquals(-1, zeroNode.findNode(-1) ?.value)
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun searchOnZeroLevelWorks() = runTest {
|
||||||
|
val zeroNode = SortedMapLikeBinaryTreeNode(0, 0)
|
||||||
|
val oneNode = zeroNode.upsertSubNode(1, 1)
|
||||||
|
val minusOneNode = zeroNode.upsertSubNode(-1, -1)
|
||||||
|
|
||||||
|
val assertingNodesToSearchQuery = mapOf(
|
||||||
|
setOf(oneNode) to (1 .. 1),
|
||||||
|
setOf(zeroNode, oneNode) to (0 .. 1),
|
||||||
|
setOf(minusOneNode, zeroNode, oneNode) to (-1 .. 1),
|
||||||
|
setOf(minusOneNode, zeroNode) to (-1 .. 0),
|
||||||
|
setOf(minusOneNode) to (-1 .. -1),
|
||||||
|
setOf(zeroNode) to (0 .. 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertingNodesToSearchQuery.forEach {
|
||||||
|
val foundData = zeroNode.findNodesInRange(it.value)
|
||||||
|
assertTrue(foundData.containsAll(it.key))
|
||||||
|
assertTrue(it.key.containsAll(foundData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun deepReInsertOnWorks() = runTest(timeout = 300.seconds) {
|
||||||
|
var zeroNode = SortedMapLikeBinaryTreeNode(0, 0)
|
||||||
|
val rangeRadius = 500
|
||||||
|
val nodes = mutableMapOf<Int, SortedMapLikeBinaryTreeNode<Int, Int>>()
|
||||||
|
for (i in -rangeRadius .. rangeRadius) {
|
||||||
|
nodes[i] = zeroNode.upsertSubNode(i, i)
|
||||||
|
if (i == zeroNode.key) {
|
||||||
|
zeroNode = nodes.getValue(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in -rangeRadius .. rangeRadius) {
|
||||||
|
val expectedNode = nodes.getValue(i)
|
||||||
|
val foundNode = zeroNode.findNode(i)
|
||||||
|
|
||||||
|
assertEquals(expectedNode, foundNode)
|
||||||
|
|
||||||
|
if (expectedNode === zeroNode) continue
|
||||||
|
|
||||||
|
val parentNode = zeroNode.findParentNode(i)
|
||||||
|
assertTrue(
|
||||||
|
parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode,
|
||||||
|
"It is expected, that parent node with data ${parentNode ?.key} will be parent of ${expectedNode.key}, but its left subnode is ${parentNode ?.getLeftNode() ?.key} and right one is ${parentNode ?.getRightNode() ?.key}"
|
||||||
|
)
|
||||||
|
assertTrue(
|
||||||
|
foundNode != null && expectedNode.deepEquals(foundNode)
|
||||||
|
)
|
||||||
|
|
||||||
|
zeroNode.upsertSubNode(i, -i)
|
||||||
|
val foundModifiedNode = zeroNode.findNode(i)
|
||||||
|
assertEquals(foundNode ?.value, foundModifiedNode ?.value ?.times(-1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun deepInsertOnWorks() = runTest(timeout = 240.seconds) {
|
||||||
|
val zeroNode = SortedMapLikeBinaryTreeNode(0, 0)
|
||||||
|
val rangeRadius = 500
|
||||||
|
val nodes = mutableMapOf<Int, SortedMapLikeBinaryTreeNode<Int, Int>>()
|
||||||
|
for (i in -rangeRadius .. rangeRadius) {
|
||||||
|
if (zeroNode.key != i) {
|
||||||
|
nodes[i] = zeroNode.upsertSubNode(i, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes[zeroNode.key] = zeroNode
|
||||||
|
|
||||||
|
for (i in -rangeRadius .. rangeRadius) {
|
||||||
|
val expectedNode = nodes.getValue(i)
|
||||||
|
val foundNode = zeroNode.findNode(i)
|
||||||
|
|
||||||
|
assertTrue(expectedNode === foundNode)
|
||||||
|
|
||||||
|
if (expectedNode === zeroNode) continue
|
||||||
|
|
||||||
|
val parentNode = zeroNode.findParentNode(i)
|
||||||
|
assertTrue(
|
||||||
|
parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode,
|
||||||
|
"It is expected, that parent node with data ${parentNode ?.key} will be parent of ${expectedNode.key}, but its left subnode is ${parentNode ?.getLeftNode() ?.key} and right one is ${parentNode ?.getRightNode() ?.key}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val sourceTreeSize = zeroNode.size()
|
||||||
|
|
||||||
|
var previousData = -rangeRadius - 1
|
||||||
|
for (node in zeroNode) {
|
||||||
|
assertTrue(nodes[node.key] === node)
|
||||||
|
assertTrue(previousData == node.key - 1)
|
||||||
|
previousData = node.key
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(sourceTreeSize == zeroNode.size())
|
||||||
|
}
|
||||||
|
}
|
@@ -4,7 +4,7 @@ plugins {
|
|||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
@@ -130,4 +130,4 @@ tasks.dokkaHtml {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$defaultAndroidSettingsPresetPath"
|
apply from: "$defaultAndroidSettings"
|
||||||
|
@@ -1,43 +1,32 @@
|
|||||||
allprojects {
|
File templatesFolder = new File("$rootProject.projectDir.absolutePath${File.separatorChar}gradle${File.separatorChar}templates")
|
||||||
ext {
|
|
||||||
projectByName = { name ->
|
|
||||||
for (subproject in rootProject.subprojects) {
|
|
||||||
if (subproject.name == name) {
|
|
||||||
return subproject
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
internalProject = { name ->
|
Map properties = new HashMap<String, String>()
|
||||||
// if (releaseMode) {
|
|
||||||
// "$group:$name:$version"
|
|
||||||
// } else {
|
|
||||||
// projectByName("$name")
|
|
||||||
// }
|
|
||||||
projectByName("$name")
|
|
||||||
}
|
|
||||||
|
|
||||||
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
|
if (templatesFolder.exists() && templatesFolder.isDirectory()) {
|
||||||
// String compilerPluginVersionFromProperties = (String) project.properties["compose.kotlinCompilerPluginVersion"]
|
templatesFolder.listFiles().each {
|
||||||
// String compilerPluginVersionFromLibrariesVersions = libs.versions.compose.kotlin.get()
|
properties[it.name - ".gradle"] = it.absolutePath
|
||||||
// composePluginKotlinVersion = compilerPluginVersionFromProperties
|
}
|
||||||
// if (compilerPluginVersionFromProperties == null) {
|
}
|
||||||
// composePluginKotlinVersion = compilerPluginVersionFromLibrariesVersions
|
|
||||||
// }
|
properties["projectByName"] = { name ->
|
||||||
|
for (subproject in rootProject.subprojects) {
|
||||||
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsAndroidProject.gradle"
|
if (subproject.name == name) {
|
||||||
mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle"
|
return subproject
|
||||||
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
|
}
|
||||||
mppJvmJsLinuxMingwProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsLinuxMingwProject.gradle"
|
}
|
||||||
mppJvmJsLinuxMingwLinuxArm64ProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsLinuxMingwLinuxArm64Project.gradle"
|
return null
|
||||||
mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsAndroidLinuxMingwLinuxArm64Project.gradle"
|
}
|
||||||
mppComposeJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppComposeJvmJsAndroidLinuxMingwLinuxArm64Project.gradle"
|
|
||||||
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
|
properties["internalProject"] = { name ->
|
||||||
|
projectByName("$name")
|
||||||
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle"
|
}
|
||||||
|
|
||||||
publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle"
|
allprojects {
|
||||||
publishJvmOnlyPath = "${rootProject.projectDir.absolutePath}/jvm.publish.gradle"
|
ext {
|
||||||
|
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
|
||||||
|
|
||||||
|
properties.forEach { k, v ->
|
||||||
|
it[k] = v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ plugins {
|
|||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@@ -12,6 +12,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
api project(":micro_utils.common")
|
api project(":micro_utils.common")
|
||||||
api project(":micro_utils.coroutines")
|
api project(":micro_utils.coroutines")
|
||||||
|
api libs.kslog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
package dev.inmo.micro_utils.fsm.common
|
package dev.inmo.micro_utils.fsm.common
|
||||||
|
|
||||||
|
import dev.inmo.kslog.common.TagLogger
|
||||||
|
import dev.inmo.kslog.common.e
|
||||||
import dev.inmo.micro_utils.common.Optional
|
import dev.inmo.micro_utils.common.Optional
|
||||||
import dev.inmo.micro_utils.coroutines.*
|
import dev.inmo.micro_utils.coroutines.*
|
||||||
import dev.inmo.micro_utils.fsm.common.utils.StateHandlingErrorHandler
|
import dev.inmo.micro_utils.fsm.common.utils.StateHandlingErrorHandler
|
||||||
@@ -68,6 +70,7 @@ open class DefaultStatesMachine <T: State>(
|
|||||||
protected val handlers: List<CheckableHandlerHolder<in T, T>>,
|
protected val handlers: List<CheckableHandlerHolder<in T, T>>,
|
||||||
protected val onStateHandlingErrorHandler: StateHandlingErrorHandler<T> = defaultStateHandlingErrorHandler()
|
protected val onStateHandlingErrorHandler: StateHandlingErrorHandler<T> = defaultStateHandlingErrorHandler()
|
||||||
) : StatesMachine<T> {
|
) : StatesMachine<T> {
|
||||||
|
protected val logger = TagLogger(this::class.simpleName!!)
|
||||||
/**
|
/**
|
||||||
* Will call [launchStateHandling] for state handling
|
* Will call [launchStateHandling] for state handling
|
||||||
*/
|
*/
|
||||||
@@ -96,7 +99,13 @@ open class DefaultStatesMachine <T: State>(
|
|||||||
statesJobsMutex.withLock {
|
statesJobsMutex.withLock {
|
||||||
statesJobs[actualState] ?.cancel()
|
statesJobs[actualState] ?.cancel()
|
||||||
statesJobs[actualState] = scope.launch {
|
statesJobs[actualState] = scope.launch {
|
||||||
performUpdate(actualState)
|
runCatching {
|
||||||
|
performUpdate(actualState)
|
||||||
|
}.onFailure {
|
||||||
|
logger.e(it) {
|
||||||
|
"Unable to perform update of state from $actualState"
|
||||||
|
}
|
||||||
|
}.getOrThrow()
|
||||||
}.also { job ->
|
}.also { job ->
|
||||||
job.invokeOnCompletion { _ ->
|
job.invokeOnCompletion { _ ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -119,8 +128,10 @@ open class DefaultStatesMachine <T: State>(
|
|||||||
*/
|
*/
|
||||||
override fun start(scope: CoroutineScope): Job {
|
override fun start(scope: CoroutineScope): Job {
|
||||||
val supervisorScope = scope.LinkedSupervisorScope()
|
val supervisorScope = scope.LinkedSupervisorScope()
|
||||||
supervisorScope.launchSafelyWithoutExceptions {
|
supervisorScope.launchLoggingDropExceptions {
|
||||||
(statesManager.getActiveStates().asFlow() + statesManager.onStartChain).subscribeSafelyWithoutExceptions(supervisorScope) {
|
(statesManager.getActiveStates().asFlow() + statesManager.onStartChain).subscribeSafelyWithoutExceptions(
|
||||||
|
supervisorScope
|
||||||
|
) {
|
||||||
supervisorScope.launch { performStateUpdate(Optional.absent(), it, supervisorScope) }
|
supervisorScope.launch { performStateUpdate(Optional.absent(), it, supervisorScope) }
|
||||||
}
|
}
|
||||||
statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(supervisorScope) {
|
statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(supervisorScope) {
|
||||||
@@ -131,7 +142,7 @@ open class DefaultStatesMachine <T: State>(
|
|||||||
statesJobsMutex.withLock {
|
statesJobsMutex.withLock {
|
||||||
val stateInMap = statesJobs.keys.firstOrNull { stateInMap -> stateInMap == removedState }
|
val stateInMap = statesJobs.keys.firstOrNull { stateInMap -> stateInMap == removedState }
|
||||||
if (stateInMap === removedState) {
|
if (stateInMap === removedState) {
|
||||||
statesJobs[stateInMap] ?.cancel()
|
statesJobs[stateInMap]?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,12 @@ interface StatesManager<T : State> {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Must set current set using [State.context]
|
* It is expected, that [new] state will be saved in manager.
|
||||||
|
*
|
||||||
|
* If [new] context will not be equal to [old] one, it must do some check of availability for replacement
|
||||||
|
* of potentially exists state on [new] context. If this state can't be replaced, it will throw [IllegalStateException]
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException - in case when [new] [State] can't be set
|
||||||
*/
|
*/
|
||||||
suspend fun update(old: T, new: T)
|
suspend fun update(old: T, new: T)
|
||||||
|
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
package dev.inmo.micro_utils.fsm.common
|
package dev.inmo.micro_utils.fsm.common
|
||||||
|
|
||||||
|
import dev.inmo.kslog.common.e
|
||||||
import dev.inmo.micro_utils.common.*
|
import dev.inmo.micro_utils.common.*
|
||||||
import dev.inmo.micro_utils.fsm.common.utils.StateHandlingErrorHandler
|
import dev.inmo.micro_utils.fsm.common.utils.StateHandlingErrorHandler
|
||||||
import dev.inmo.micro_utils.fsm.common.utils.defaultStateHandlingErrorHandler
|
import dev.inmo.micro_utils.fsm.common.utils.defaultStateHandlingErrorHandler
|
||||||
@@ -44,7 +45,13 @@ open class DefaultUpdatableStatesMachine<T : State>(
|
|||||||
val job = previousState.mapOnPresented {
|
val job = previousState.mapOnPresented {
|
||||||
statesJobs.remove(it)
|
statesJobs.remove(it)
|
||||||
} ?.takeIf { it.isActive } ?: scope.launch {
|
} ?.takeIf { it.isActive } ?: scope.launch {
|
||||||
performUpdate(actualState)
|
runCatching {
|
||||||
|
performUpdate(actualState)
|
||||||
|
}.onFailure {
|
||||||
|
logger.e(it) {
|
||||||
|
"Unable to perform update of state up to $actualState"
|
||||||
|
}
|
||||||
|
}.getOrThrow()
|
||||||
}.also { job ->
|
}.also { job ->
|
||||||
job.invokeOnCompletion { _ ->
|
job.invokeOnCompletion { _ ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
package dev.inmo.micro_utils.fsm.common.managers
|
package dev.inmo.micro_utils.fsm.common.managers
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.coroutines.SmartRWLocker
|
||||||
|
import dev.inmo.micro_utils.coroutines.withReadAcquire
|
||||||
|
import dev.inmo.micro_utils.coroutines.withWriteLock
|
||||||
import dev.inmo.micro_utils.fsm.common.State
|
import dev.inmo.micro_utils.fsm.common.State
|
||||||
import dev.inmo.micro_utils.fsm.common.StatesManager
|
import dev.inmo.micro_utils.fsm.common.StatesManager
|
||||||
import kotlinx.coroutines.flow.*
|
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]
|
* Implement this repo if you want to use some custom repo for [DefaultStatesManager]
|
||||||
@@ -19,6 +20,14 @@ interface DefaultStatesManagerRepo<T : State> {
|
|||||||
* NOT be removed
|
* NOT be removed
|
||||||
*/
|
*/
|
||||||
suspend fun removeState(state: T)
|
suspend fun removeState(state: T)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Semantically, calls [removeState] and then [set]
|
||||||
|
*/
|
||||||
|
suspend fun removeAndSet(toRemove: T, toSet: T) {
|
||||||
|
removeState(toRemove)
|
||||||
|
set(toSet)
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* @return Current list of available and saved states
|
* @return Current list of available and saved states
|
||||||
*/
|
*/
|
||||||
@@ -58,7 +67,7 @@ open class DefaultStatesManager<T : State>(
|
|||||||
protected val _onEndChain = MutableSharedFlow<T>(0)
|
protected val _onEndChain = MutableSharedFlow<T>(0)
|
||||||
override val onEndChain: Flow<T> = _onEndChain.asSharedFlow()
|
override val onEndChain: Flow<T> = _onEndChain.asSharedFlow()
|
||||||
|
|
||||||
protected val mapMutex = Mutex()
|
protected val internalLocker = SmartRWLocker()
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
repo: DefaultStatesManagerRepo<T>,
|
repo: DefaultStatesManagerRepo<T>,
|
||||||
@@ -68,28 +77,30 @@ open class DefaultStatesManager<T : State>(
|
|||||||
onUpdateContextsConflictResolver = onContextsConflictResolver
|
onUpdateContextsConflictResolver = onContextsConflictResolver
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun update(old: T, new: T) = mapMutex.withLock {
|
override suspend fun update(old: T, new: T) = internalLocker.withWriteLock {
|
||||||
val stateByOldContext: T? = repo.getContextState(old.context)
|
val stateByOldContext: T? = repo.getContextState(old.context)
|
||||||
when {
|
when {
|
||||||
stateByOldContext != old -> return@withLock
|
stateByOldContext != old -> return@withWriteLock
|
||||||
stateByOldContext == null || old.context == new.context -> {
|
old.context == new.context -> {
|
||||||
repo.removeState(old)
|
repo.removeAndSet(old, new)
|
||||||
repo.set(new)
|
|
||||||
_onChainStateUpdated.emit(old to new)
|
_onChainStateUpdated.emit(old to new)
|
||||||
}
|
}
|
||||||
else -> {
|
old.context != new.context -> {
|
||||||
val stateOnNewOneContext = repo.getContextState(new.context)
|
val stateOnNewOneContext = repo.getContextState(new.context)
|
||||||
if (stateOnNewOneContext == null || onUpdateContextsConflictResolver(old, new, stateOnNewOneContext)) {
|
if (stateOnNewOneContext == null || onUpdateContextsConflictResolver(old, new, stateOnNewOneContext)) {
|
||||||
stateOnNewOneContext ?.let { endChainWithoutLock(it) }
|
stateOnNewOneContext ?.let { endChainWithoutLock(it) }
|
||||||
repo.removeState(old)
|
repo.removeAndSet(old, new)
|
||||||
repo.set(new)
|
|
||||||
_onChainStateUpdated.emit(old to new)
|
_onChainStateUpdated.emit(old to new)
|
||||||
|
} else {
|
||||||
|
error(
|
||||||
|
"Unable to update state from $old to $new due to false answer from $onUpdateContextsConflictResolver and state on old context $stateOnNewOneContext"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun startChain(state: T) = mapMutex.withLock {
|
override suspend fun startChain(state: T) = internalLocker.withWriteLock {
|
||||||
val stateOnContext = repo.getContextState(state.context)
|
val stateOnContext = repo.getContextState(state.context)
|
||||||
if (stateOnContext == null || onStartContextsConflictResolver(stateOnContext, state)) {
|
if (stateOnContext == null || onStartContextsConflictResolver(stateOnContext, state)) {
|
||||||
stateOnContext ?.let {
|
stateOnContext ?.let {
|
||||||
@@ -108,11 +119,13 @@ open class DefaultStatesManager<T : State>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun endChain(state: T) {
|
override suspend fun endChain(state: T) {
|
||||||
mapMutex.withLock {
|
internalLocker.withWriteLock {
|
||||||
endChainWithoutLock(state)
|
endChainWithoutLock(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getActiveStates(): List<T> = repo.getStates()
|
override suspend fun getActiveStates(): List<T> = internalLocker.withReadAcquire {
|
||||||
|
repo.getStates()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ plugins {
|
|||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
@@ -1,25 +1,59 @@
|
|||||||
package dev.inmo.micro_utils.fsm.repos.common
|
package dev.inmo.micro_utils.fsm.repos.common
|
||||||
|
|
||||||
|
import dev.inmo.kslog.common.TagLogger
|
||||||
|
import dev.inmo.kslog.common.i
|
||||||
|
import dev.inmo.micro_utils.coroutines.SmartRWLocker
|
||||||
|
import dev.inmo.micro_utils.coroutines.withReadAcquire
|
||||||
|
import dev.inmo.micro_utils.coroutines.withWriteLock
|
||||||
import dev.inmo.micro_utils.fsm.common.State
|
import dev.inmo.micro_utils.fsm.common.State
|
||||||
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
|
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
|
||||||
import dev.inmo.micro_utils.repos.*
|
import dev.inmo.micro_utils.repos.*
|
||||||
import dev.inmo.micro_utils.repos.pagination.getAll
|
import dev.inmo.micro_utils.repos.pagination.getAll
|
||||||
|
import dev.inmo.micro_utils.repos.unset
|
||||||
|
|
||||||
class KeyValueBasedDefaultStatesManagerRepo<T : State>(
|
class KeyValueBasedDefaultStatesManagerRepo<T : State>(
|
||||||
private val keyValueRepo: KeyValueRepo<Any, T>
|
private val keyValueRepo: KeyValueRepo<Any, T>
|
||||||
) : DefaultStatesManagerRepo<T> {
|
) : DefaultStatesManagerRepo<T> {
|
||||||
|
private val locker = SmartRWLocker()
|
||||||
|
private val logger = TagLogger("KeyValueBasedDefaultStatesManagerRepo")
|
||||||
override suspend fun set(state: T) {
|
override suspend fun set(state: T) {
|
||||||
keyValueRepo.set(state.context, state)
|
locker.withWriteLock {
|
||||||
}
|
keyValueRepo.set(state.context, state)
|
||||||
|
logger.i { "Set ${state.context} value to $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 removeState(state: T) {
|
||||||
override suspend fun getContextState(context: Any): T? = keyValueRepo.get(context)
|
locker.withWriteLock {
|
||||||
|
if (keyValueRepo.get(state.context) == state) {
|
||||||
|
keyValueRepo.unset(state.context)
|
||||||
|
logger.i { "Unset $state" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun contains(context: Any): Boolean = keyValueRepo.contains(context)
|
override suspend fun removeAndSet(toRemove: T, toSet: T) {
|
||||||
|
locker.withWriteLock {
|
||||||
|
when {
|
||||||
|
toRemove.context == toSet.context -> {
|
||||||
|
keyValueRepo.set(toSet.context, toSet)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
keyValueRepo.set(toSet.context, toSet)
|
||||||
|
keyValueRepo.unset(toRemove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getStates(): List<T> = locker.withReadAcquire {
|
||||||
|
keyValueRepo.getAll { keys(it) }.map { it.second }
|
||||||
|
}
|
||||||
|
override suspend fun getContextState(context: Any): T? = locker.withReadAcquire {
|
||||||
|
keyValueRepo.get(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun contains(context: Any): Boolean = locker.withReadAcquire {
|
||||||
|
keyValueRepo.contains(context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,5 +15,5 @@ crypto_js_version=4.1.1
|
|||||||
# Project data
|
# Project data
|
||||||
|
|
||||||
group=dev.inmo
|
group=dev.inmo
|
||||||
version=0.21.4
|
version=0.25.2
|
||||||
android_code_version=263
|
android_code_version=292
|
||||||
|
@@ -1,45 +1,46 @@
|
|||||||
[versions]
|
[versions]
|
||||||
|
|
||||||
kt = "1.9.23"
|
kt = "2.1.10"
|
||||||
kt-serialization = "1.6.3"
|
kt-serialization = "1.8.0"
|
||||||
kt-coroutines = "1.8.1"
|
kt-coroutines = "1.10.1"
|
||||||
|
|
||||||
kslog = "1.3.4"
|
kslog = "1.4.1"
|
||||||
|
|
||||||
jb-compose = "1.6.2"
|
jb-compose = "1.7.3"
|
||||||
jb-exposed = "0.50.1"
|
jb-exposed = "0.60.0"
|
||||||
jb-dokka = "1.9.20"
|
jb-dokka = "2.0.0"
|
||||||
|
|
||||||
|
sqlite = "3.49.1.0"
|
||||||
|
|
||||||
korlibs = "5.4.0"
|
korlibs = "5.4.0"
|
||||||
uuid = "0.8.4"
|
uuid = "0.8.4"
|
||||||
|
|
||||||
ktor = "2.3.11"
|
ktor = "3.1.1"
|
||||||
|
|
||||||
gh-release = "2.5.2"
|
gh-release = "2.5.2"
|
||||||
|
|
||||||
koin = "3.5.6"
|
koin = "4.0.2"
|
||||||
|
|
||||||
okio = "3.9.0"
|
okio = "3.10.2"
|
||||||
|
|
||||||
ksp = "1.9.23-1.0.20"
|
ksp = "2.1.10-1.0.31"
|
||||||
kotlin-poet = "1.16.0"
|
kotlin-poet = "1.18.1"
|
||||||
|
|
||||||
versions = "0.51.0"
|
versions = "0.51.0"
|
||||||
|
|
||||||
android-gradle = "8.2.0"
|
android-gradle = "8.7.+"
|
||||||
dexcount = "4.0.0"
|
dexcount = "4.0.0"
|
||||||
|
|
||||||
android-coreKtx = "1.13.1"
|
android-coreKtx = "1.15.0"
|
||||||
android-recyclerView = "1.3.2"
|
android-recyclerView = "1.4.0"
|
||||||
android-appCompat = "1.6.1"
|
android-appCompat = "1.7.0"
|
||||||
android-fragment = "1.7.1"
|
android-fragment = "1.8.6"
|
||||||
android-espresso = "3.5.1"
|
android-espresso = "3.6.1"
|
||||||
android-test = "1.1.5"
|
android-test = "1.2.1"
|
||||||
android-compose-material3 = "1.2.1"
|
|
||||||
|
|
||||||
android-props-minSdk = "21"
|
android-props-minSdk = "21"
|
||||||
android-props-compileSdk = "34"
|
android-props-compileSdk = "35"
|
||||||
android-props-buildTools = "34.0.0"
|
android-props-buildTools = "35.0.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ kt-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-
|
|||||||
kt-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kt-coroutines" }
|
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" }
|
kt-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kt-coroutines" }
|
||||||
kt-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kt-coroutines" }
|
kt-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kt-coroutines" }
|
||||||
|
kt-coroutines-debug = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-debug", version.ref = "kt-coroutines" }
|
||||||
|
|
||||||
|
|
||||||
ktor-io = { module = "io.ktor:ktor-io", version.ref = "ktor" }
|
ktor-io = { module = "io.ktor:ktor-io", version.ref = "ktor" }
|
||||||
@@ -79,12 +81,14 @@ koin = { module = "io.insert-koin:koin-core", version.ref = "koin" }
|
|||||||
|
|
||||||
|
|
||||||
jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-exposed" }
|
jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-exposed" }
|
||||||
|
jb-exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "jb-exposed" }
|
||||||
|
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
|
||||||
|
|
||||||
|
jb-compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "jb-compose" }
|
||||||
|
|
||||||
android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
|
android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
|
||||||
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }
|
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }
|
||||||
android-appCompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "android-appCompat" }
|
android-appCompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "android-appCompat" }
|
||||||
android-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "android-compose-material3" }
|
|
||||||
android-fragment = { module = "androidx.fragment:fragment", version.ref = "android-fragment" }
|
android-fragment = { module = "androidx.fragment:fragment", version.ref = "android-fragment" }
|
||||||
android-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" }
|
android-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" }
|
||||||
android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" }
|
android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" }
|
||||||
@@ -113,5 +117,6 @@ buildscript-android-dexcount = { module = "com.getkeepsafe.dexcount:dexcount-gra
|
|||||||
[plugins]
|
[plugins]
|
||||||
|
|
||||||
jb-compose = { id = "org.jetbrains.compose", version.ref = "jb-compose" }
|
jb-compose = { id = "org.jetbrains.compose", version.ref = "jb-compose" }
|
||||||
|
kt-jb-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kt" }
|
||||||
|
|
||||||
versions = { id = "com.github.ben-manes.versions", version.ref = "versions" }
|
versions = { id = "com.github.ben-manes.versions", version.ref = "versions" }
|
||||||
|
@@ -6,6 +6,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion libs.versions.android.props.minSdk.get().toInteger()
|
minSdkVersion libs.versions.android.props.minSdk.get().toInteger()
|
||||||
|
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||||
targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||||
versionCode "${android_code_version}".toInteger()
|
versionCode "${android_code_version}".toInteger()
|
||||||
versionName "$version"
|
versionName "$version"
|
@@ -1,7 +1,7 @@
|
|||||||
project.version = "$version"
|
project.version = "$version"
|
||||||
project.group = "$group"
|
project.group = "$group"
|
||||||
|
|
||||||
apply from: "$publishGradlePath"
|
apply from: "$publish"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
androidTarget {
|
androidTarget {
|
||||||
@@ -29,7 +29,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$defaultAndroidSettingsPresetPath"
|
apply from: "$defaultAndroidSettings"
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
@@ -1,7 +1,7 @@
|
|||||||
project.version = "$version"
|
project.version = "$version"
|
||||||
project.group = "$group"
|
project.group = "$group"
|
||||||
|
|
||||||
apply from: "$publishGradlePath"
|
apply from: "$publish"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm {
|
jvm {
|
||||||
@@ -12,8 +12,20 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
js (IR) {
|
js (IR) {
|
||||||
browser()
|
browser {
|
||||||
nodejs()
|
testTask {
|
||||||
|
useMocha {
|
||||||
|
timeout = "240000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodejs {
|
||||||
|
testTask {
|
||||||
|
useMocha {
|
||||||
|
timeout = "240000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
androidTarget {
|
androidTarget {
|
||||||
publishAllLibraryVariants()
|
publishAllLibraryVariants()
|
||||||
@@ -77,7 +89,6 @@ kotlin {
|
|||||||
jsTest {
|
jsTest {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin('test-js')
|
implementation kotlin('test-js')
|
||||||
implementation kotlin('test-junit')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nativeMain.dependsOn commonMain
|
nativeMain.dependsOn commonMain
|
||||||
@@ -89,7 +100,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$defaultAndroidSettingsPresetPath"
|
apply from: "$defaultAndroidSettings"
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
@@ -1,7 +1,7 @@
|
|||||||
project.version = "$version"
|
project.version = "$version"
|
||||||
project.group = "$group"
|
project.group = "$group"
|
||||||
|
|
||||||
apply from: "$publishGradlePath"
|
apply from: "$publish"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm {
|
jvm {
|
@@ -1,7 +1,7 @@
|
|||||||
project.version = "$version"
|
project.version = "$version"
|
||||||
project.group = "$group"
|
project.group = "$group"
|
||||||
|
|
||||||
apply from: "$publishGradlePath"
|
apply from: "$publish"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm {
|
jvm {
|
||||||
@@ -12,8 +12,20 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
js (IR) {
|
js (IR) {
|
||||||
browser()
|
browser {
|
||||||
nodejs()
|
testTask {
|
||||||
|
useMocha {
|
||||||
|
timeout = "240000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodejs {
|
||||||
|
testTask {
|
||||||
|
useMocha {
|
||||||
|
timeout = "240000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
androidTarget {
|
androidTarget {
|
||||||
publishAllLibraryVariants()
|
publishAllLibraryVariants()
|
||||||
@@ -36,7 +48,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
commonTest {
|
commonTest {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin('test-common')
|
implementation kotlin('test')
|
||||||
implementation kotlin('test-annotations-common')
|
implementation kotlin('test-annotations-common')
|
||||||
implementation libs.kt.coroutines.test
|
implementation libs.kt.coroutines.test
|
||||||
}
|
}
|
||||||
@@ -57,7 +69,6 @@ kotlin {
|
|||||||
jsTest {
|
jsTest {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin('test-js')
|
implementation kotlin('test-js')
|
||||||
implementation kotlin('test-junit')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nativeMain.dependsOn commonMain
|
nativeMain.dependsOn commonMain
|
||||||
@@ -69,7 +80,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$defaultAndroidSettingsPresetPath"
|
apply from: "$defaultAndroidSettings"
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
@@ -1,7 +1,7 @@
|
|||||||
project.version = "$version"
|
project.version = "$version"
|
||||||
project.group = "$group"
|
project.group = "$group"
|
||||||
|
|
||||||
apply from: "$publishGradlePath"
|
apply from: "$publish"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm {
|
jvm {
|
||||||
@@ -12,8 +12,20 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
js (IR) {
|
js (IR) {
|
||||||
browser()
|
browser {
|
||||||
nodejs()
|
testTask {
|
||||||
|
useMocha {
|
||||||
|
timeout = "240000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodejs {
|
||||||
|
testTask {
|
||||||
|
useMocha {
|
||||||
|
timeout = "240000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
linuxX64()
|
linuxX64()
|
||||||
mingwX64()
|
mingwX64()
|
||||||
@@ -41,17 +53,6 @@ kotlin {
|
|||||||
jsTest {
|
jsTest {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin('test-js')
|
implementation kotlin('test-js')
|
||||||
implementation kotlin('test-junit')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mingwX64Test {
|
|
||||||
dependencies {
|
|
||||||
implementation kotlin('test-junit')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
linuxX64Test {
|
|
||||||
dependencies {
|
|
||||||
implementation kotlin('test-junit')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@@ -1,7 +1,7 @@
|
|||||||
project.version = "$version"
|
project.version = "$version"
|
||||||
project.group = "$group"
|
project.group = "$group"
|
||||||
|
|
||||||
apply from: "$publishGradlePath"
|
apply from: "$publish"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm {
|
jvm {
|
||||||
@@ -12,8 +12,20 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
js (IR) {
|
js (IR) {
|
||||||
browser()
|
browser {
|
||||||
nodejs()
|
testTask {
|
||||||
|
useMocha {
|
||||||
|
timeout = "240000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodejs {
|
||||||
|
testTask {
|
||||||
|
useMocha {
|
||||||
|
timeout = "240000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
androidTarget {
|
androidTarget {
|
||||||
publishAllLibraryVariants()
|
publishAllLibraryVariants()
|
||||||
@@ -58,7 +70,6 @@ kotlin {
|
|||||||
jsTest {
|
jsTest {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin('test-js')
|
implementation kotlin('test-js')
|
||||||
implementation kotlin('test-junit')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidUnitTest {
|
androidUnitTest {
|
||||||
@@ -69,17 +80,10 @@ kotlin {
|
|||||||
implementation compose.uiTest
|
implementation compose.uiTest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidInstrumentedTest {
|
|
||||||
dependencies {
|
|
||||||
implementation kotlin('test-junit')
|
|
||||||
implementation libs.android.test.junit
|
|
||||||
implementation libs.android.espresso
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$defaultAndroidSettingsPresetPath"
|
apply from: "$defaultAndroidSettings"
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
@@ -1,3 +1,7 @@
|
|||||||
|
if (ext.getProperties()["do_publish"] == false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
apply plugin: 'maven-publish'
|
apply plugin: 'maven-publish'
|
||||||
|
|
||||||
task javadocsJar(type: Jar) {
|
task javadocsJar(type: Jar) {
|
||||||
@@ -19,29 +23,29 @@ publishing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
developers {
|
developers {
|
||||||
|
|
||||||
developer {
|
developer {
|
||||||
id = "InsanusMokrassar"
|
id = "InsanusMokrassar"
|
||||||
name = "Aleksei Ovsiannikov"
|
name = "Aleksei Ovsiannikov"
|
||||||
email = "ovsyannikov.alexey95@gmail.com"
|
email = "ovsyannikov.alexey95@gmail.com"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
developer {
|
developer {
|
||||||
id = "000Sanya"
|
id = "000Sanya"
|
||||||
name = "Syrov Aleksandr"
|
name = "Syrov Aleksandr"
|
||||||
email = "000sanya.000sanya@gmail.com"
|
email = "000sanya.000sanya@gmail.com"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
licenses {
|
licenses {
|
||||||
|
|
||||||
license {
|
license {
|
||||||
name = "Apache Software License 2.0"
|
name = "Apache Software License 2.0"
|
||||||
url = "https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"
|
url = "https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
repositories {
|
repositories {
|
||||||
@@ -49,51 +53,51 @@ publishing {
|
|||||||
maven {
|
maven {
|
||||||
name = "GithubPackages"
|
name = "GithubPackages"
|
||||||
url = uri("https://maven.pkg.github.com/InsanusMokrassar/MicroUtils")
|
url = uri("https://maven.pkg.github.com/InsanusMokrassar/MicroUtils")
|
||||||
|
|
||||||
credentials {
|
credentials {
|
||||||
username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER')
|
username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER')
|
||||||
password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD')
|
password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD')
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((project.hasProperty('INMONEXUS_USER') || System.getenv('INMONEXUS_USER') != null) && (project.hasProperty('INMONEXUS_PASSWORD') || System.getenv('INMONEXUS_PASSWORD') != null)) {
|
if ((project.hasProperty('INMONEXUS_USER') || System.getenv('INMONEXUS_USER') != null) && (project.hasProperty('INMONEXUS_PASSWORD') || System.getenv('INMONEXUS_PASSWORD') != null)) {
|
||||||
maven {
|
maven {
|
||||||
name = "InmoNexus"
|
name = "InmoNexus"
|
||||||
url = uri("https://nexus.inmo.dev/repository/maven-releases/")
|
url = uri("https://nexus.inmo.dev/repository/maven-releases/")
|
||||||
|
|
||||||
credentials {
|
credentials {
|
||||||
username = project.hasProperty('INMONEXUS_USER') ? project.property('INMONEXUS_USER') : System.getenv('INMONEXUS_USER')
|
username = project.hasProperty('INMONEXUS_USER') ? project.property('INMONEXUS_USER') : System.getenv('INMONEXUS_USER')
|
||||||
password = project.hasProperty('INMONEXUS_PASSWORD') ? project.property('INMONEXUS_PASSWORD') : System.getenv('INMONEXUS_PASSWORD')
|
password = project.hasProperty('INMONEXUS_PASSWORD') ? project.property('INMONEXUS_PASSWORD') : System.getenv('INMONEXUS_PASSWORD')
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
|
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
|
||||||
maven {
|
maven {
|
||||||
name = "sonatype"
|
name = "sonatype"
|
||||||
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
|
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
|
||||||
|
|
||||||
credentials {
|
credentials {
|
||||||
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
|
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
|
||||||
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
|
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (project.hasProperty("signing.gnupg.keyName")) {
|
if (project.hasProperty("signing.gnupg.keyName")) {
|
||||||
apply plugin: 'signing'
|
apply plugin: 'signing'
|
||||||
|
|
||||||
signing {
|
signing {
|
||||||
useGpgCmd()
|
useGpgCmd()
|
||||||
|
|
||||||
sign publishing.publications
|
sign publishing.publications
|
||||||
}
|
}
|
||||||
|
|
||||||
task signAll {
|
task signAll {
|
||||||
tasks.withType(Sign).forEach {
|
tasks.withType(Sign).forEach {
|
||||||
dependsOn(it)
|
dependsOn(it)
|
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-8.7-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@@ -4,7 +4,7 @@ plugins {
|
|||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@@ -17,11 +17,13 @@ kotlin {
|
|||||||
jvmMain {
|
jvmMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.kt.reflect
|
api libs.kt.reflect
|
||||||
|
api project(":micro_utils.coroutines")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.kt.reflect
|
api libs.kt.reflect
|
||||||
|
api project(":micro_utils.coroutines")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.jvm"
|
id "org.jetbrains.kotlin.jvm"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$publishJvmOnlyPath"
|
apply from: "$publish_jvm"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
@@ -5,7 +5,7 @@ plugins {
|
|||||||
id "com.google.devtools.ksp"
|
id "com.google.devtools.ksp"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
|
||||||
|
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
32
koin/src/jvmMain/kotlin/FactorySuspend.kt
Normal file
32
koin/src/jvmMain/kotlin/FactorySuspend.kt
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package dev.inmo.micro_utils.koin
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.coroutines.doSynchronously
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
import org.koin.core.parameter.ParametersHolder
|
||||||
|
import org.koin.core.qualifier.Qualifier
|
||||||
|
import org.koin.core.qualifier.StringQualifier
|
||||||
|
import org.koin.core.scope.Scope
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
inline fun <reified T : Any> Module.factorySuspend(
|
||||||
|
qualifier: Qualifier? = null,
|
||||||
|
coroutineScope: CoroutineScope? = null,
|
||||||
|
noinline definition: suspend Scope.(ParametersHolder) -> T
|
||||||
|
) = factory(
|
||||||
|
qualifier,
|
||||||
|
if (coroutineScope == null) {
|
||||||
|
{
|
||||||
|
doSynchronously {
|
||||||
|
definition(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
{
|
||||||
|
coroutineScope.doSynchronously {
|
||||||
|
definition(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
32
koin/src/jvmMain/kotlin/SingleSuspend.kt
Normal file
32
koin/src/jvmMain/kotlin/SingleSuspend.kt
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package dev.inmo.micro_utils.koin
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.coroutines.doSynchronously
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
import org.koin.core.parameter.ParametersHolder
|
||||||
|
import org.koin.core.qualifier.StringQualifier
|
||||||
|
import org.koin.core.scope.Scope
|
||||||
|
|
||||||
|
inline fun <reified T : Any> Module.singleSuspend(
|
||||||
|
qualifier: StringQualifier,
|
||||||
|
createdAtStart: Boolean = false,
|
||||||
|
coroutineScope: CoroutineScope? = null,
|
||||||
|
noinline definition: suspend Scope.(ParametersHolder) -> T
|
||||||
|
) = single(
|
||||||
|
qualifier,
|
||||||
|
createdAtStart,
|
||||||
|
if (coroutineScope == null) {
|
||||||
|
{
|
||||||
|
doSynchronously {
|
||||||
|
definition(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
{
|
||||||
|
coroutineScope.doSynchronously {
|
||||||
|
definition(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
@@ -4,4 +4,4 @@ plugins {
|
|||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
|
||||||
|
@@ -2,7 +2,7 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.jvm"
|
id "org.jetbrains.kotlin.jvm"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$publishJvmOnlyPath"
|
apply from: "$publish_jvm"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
@@ -5,7 +5,9 @@ plugins {
|
|||||||
id "com.google.devtools.ksp"
|
id "com.google.devtools.ksp"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
ext.do_publish = false
|
||||||
|
|
||||||
|
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
|
||||||
|
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
@@ -2,7 +2,7 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.jvm"
|
id "org.jetbrains.kotlin.jvm"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$publishJvmOnlyPath"
|
apply from: "$publish_jvm"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
@@ -1,28 +1,38 @@
|
|||||||
package dev.inmo.micro_ksp.generator
|
package dev.inmo.micro_ksp.generator
|
||||||
|
|
||||||
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||||
|
import com.google.devtools.ksp.symbol.KSDeclaration
|
||||||
import com.google.devtools.ksp.symbol.KSFile
|
import com.google.devtools.ksp.symbol.KSFile
|
||||||
|
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
|
||||||
import com.squareup.kotlinpoet.FileSpec
|
import com.squareup.kotlinpoet.FileSpec
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
fun KSClassDeclaration.writeFile(
|
fun KSDeclaration.writeFile(
|
||||||
prefix: String = "",
|
prefix: String = "",
|
||||||
suffix: String = "",
|
suffix: String = "",
|
||||||
relatedPath: String = "",
|
relatedPath: String = "",
|
||||||
force: Boolean = false,
|
force: Boolean = false,
|
||||||
|
forceUppercase: Boolean = true,
|
||||||
fileSpecBuilder: () -> FileSpec
|
fileSpecBuilder: () -> FileSpec
|
||||||
) {
|
) {
|
||||||
val containingFile = containingFile!!
|
val containingFile = containingFile!!
|
||||||
|
val simpleName = if (forceUppercase) {
|
||||||
|
val rawSimpleName = simpleName.asString()
|
||||||
|
rawSimpleName.replaceFirst(rawSimpleName.first().toString(), rawSimpleName.first().uppercase())
|
||||||
|
} else {
|
||||||
|
simpleName.asString()
|
||||||
|
}
|
||||||
File(
|
File(
|
||||||
File(
|
File(
|
||||||
File(containingFile.filePath).parent,
|
File(containingFile.filePath).parent,
|
||||||
relatedPath
|
relatedPath
|
||||||
),
|
),
|
||||||
"$prefix${simpleName.asString()}$suffix.kt"
|
"$prefix${simpleName}$suffix.kt"
|
||||||
).takeIf { force || !it.exists() } ?.apply {
|
).takeIf { force || !it.exists() } ?.apply {
|
||||||
parentFile.mkdirs()
|
parentFile.mkdirs()
|
||||||
|
val fileSpec = fileSpecBuilder()
|
||||||
writer().use { writer ->
|
writer().use { writer ->
|
||||||
fileSpecBuilder().writeTo(writer)
|
fileSpec.writeTo(writer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,8 +52,9 @@ fun KSFile.writeFile(
|
|||||||
"$prefix${fileName.dropLastWhile { it != '.' }.removeSuffix(".")}$suffix.kt"
|
"$prefix${fileName.dropLastWhile { it != '.' }.removeSuffix(".")}$suffix.kt"
|
||||||
).takeIf { force || !it.exists() } ?.apply {
|
).takeIf { force || !it.exists() } ?.apply {
|
||||||
parentFile.mkdirs()
|
parentFile.mkdirs()
|
||||||
|
val fileSpec = fileSpecBuilder()
|
||||||
writer().use { writer ->
|
writer().use { writer ->
|
||||||
fileSpecBuilder().writeTo(writer)
|
fileSpec.writeTo(writer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
25
ksp/generator/src/main/kotlin/KClassWorkarounds.kt
Normal file
25
ksp/generator/src/main/kotlin/KClassWorkarounds.kt
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package dev.inmo.micro_ksp.generator
|
||||||
|
|
||||||
|
import com.google.devtools.ksp.KSTypeNotPresentException
|
||||||
|
import com.google.devtools.ksp.KSTypesNotPresentException
|
||||||
|
import com.google.devtools.ksp.KspExperimental
|
||||||
|
import com.google.devtools.ksp.symbol.KSType
|
||||||
|
import com.squareup.kotlinpoet.asClassName
|
||||||
|
import com.squareup.kotlinpoet.ksp.toClassName
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
@OptIn(KspExperimental::class)
|
||||||
|
inline fun convertToClassName(getter: () -> KClass<*>) = try {
|
||||||
|
getter().asClassName()
|
||||||
|
} catch (e: KSTypeNotPresentException) {
|
||||||
|
e.ksType.toClassName()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(KspExperimental::class)
|
||||||
|
inline fun convertToClassNames(getter: () -> List<KClass<*>>) = try {
|
||||||
|
getter().map { it.asClassName() }
|
||||||
|
} catch (e: KSTypesNotPresentException) {
|
||||||
|
e.ksTypes.map {
|
||||||
|
it.toClassName()
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
package dev.inmo.micro_ksp.generator
|
||||||
|
|
||||||
|
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||||
|
|
||||||
|
val KSClassDeclaration.buildSubFileName: String
|
||||||
|
get() {
|
||||||
|
val parentDeclarationCaptured = parentDeclaration
|
||||||
|
val simpleNameString = simpleName.asString()
|
||||||
|
return when (parentDeclarationCaptured) {
|
||||||
|
is KSClassDeclaration -> parentDeclarationCaptured.buildSubFileName
|
||||||
|
else -> ""
|
||||||
|
} + simpleNameString
|
||||||
|
}
|
@@ -0,0 +1,8 @@
|
|||||||
|
package dev.inmo.micro_ksp.generator
|
||||||
|
|
||||||
|
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||||
|
|
||||||
|
val KSClassDeclaration.companion
|
||||||
|
get() = declarations.firstNotNullOfOrNull {
|
||||||
|
(it as? KSClassDeclaration)?.takeIf { it.isCompanionObject }
|
||||||
|
}
|
11
ksp/generator/src/main/kotlin/ResolveSubclasses.kt
Normal file
11
ksp/generator/src/main/kotlin/ResolveSubclasses.kt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package dev.inmo.micro_ksp.generator
|
||||||
|
|
||||||
|
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||||
|
|
||||||
|
fun KSClassDeclaration.resolveSubclasses(): List<KSClassDeclaration> {
|
||||||
|
return (getSealedSubclasses().flatMap {
|
||||||
|
it.resolveSubclasses()
|
||||||
|
}.ifEmpty {
|
||||||
|
sequenceOf(this)
|
||||||
|
}).toList()
|
||||||
|
}
|
30
ksp/generator/src/main/kotlin/WalkOnKSFiles.kt
Normal file
30
ksp/generator/src/main/kotlin/WalkOnKSFiles.kt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package dev.inmo.micro_ksp.generator
|
||||||
|
|
||||||
|
import com.google.devtools.ksp.getAllSuperTypes
|
||||||
|
import com.google.devtools.ksp.symbol.KSAnnotated
|
||||||
|
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||||
|
import com.google.devtools.ksp.symbol.KSDeclarationContainer
|
||||||
|
import com.google.devtools.ksp.symbol.KSFile
|
||||||
|
|
||||||
|
fun KSClassDeclaration.findSubClasses(subSymbol: KSAnnotated): Sequence<KSClassDeclaration> {
|
||||||
|
return when (subSymbol) {
|
||||||
|
is KSClassDeclaration -> if (subSymbol.getAllSuperTypes().map { it.declaration }.contains(this)) {
|
||||||
|
sequenceOf(subSymbol)
|
||||||
|
} else {
|
||||||
|
sequenceOf()
|
||||||
|
}
|
||||||
|
else -> sequenceOf()
|
||||||
|
} + if (subSymbol is KSDeclarationContainer) {
|
||||||
|
subSymbol.declarations.flatMap {
|
||||||
|
findSubClasses(it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sequenceOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun KSClassDeclaration.findSubClasses(files: Sequence<KSAnnotated>): Sequence<KSClassDeclaration> {
|
||||||
|
return files.flatMap {
|
||||||
|
findSubClasses(it)
|
||||||
|
}
|
||||||
|
}
|
@@ -4,4 +4,4 @@ plugins {
|
|||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
|
||||||
|
@@ -2,7 +2,7 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.jvm"
|
id "org.jetbrains.kotlin.jvm"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$publishJvmOnlyPath"
|
apply from: "$publish_jvm"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
@@ -0,0 +1,17 @@
|
|||||||
|
package dev.inmo.micro_utils.ksp.sealed.generator
|
||||||
|
|
||||||
|
import com.google.devtools.ksp.KspExperimental
|
||||||
|
import com.google.devtools.ksp.getAnnotationsByType
|
||||||
|
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||||
|
import dev.inmo.micro_utils.ksp.sealed.GenerateSealedTypesWorkaround
|
||||||
|
import dev.inmo.micro_utils.ksp.sealed.GenerateSealedWorkaround
|
||||||
|
import dev.inmo.microutils.kps.sealed.GenerateSealedWorkaround as OldGenerateSealedWorkaround
|
||||||
|
|
||||||
|
@OptIn(KspExperimental::class)
|
||||||
|
val KSClassDeclaration.getGenerateSealedWorkaroundAnnotation
|
||||||
|
get() = (getAnnotationsByType(GenerateSealedWorkaround::class).firstOrNull() ?: getAnnotationsByType(OldGenerateSealedWorkaround::class).firstOrNull())
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(KspExperimental::class)
|
||||||
|
val KSClassDeclaration.getGenerateSealedTypesWorkaroundAnnotation
|
||||||
|
get() = getAnnotationsByType(GenerateSealedTypesWorkaround::class).firstOrNull()
|
@@ -6,28 +6,42 @@ import com.google.devtools.ksp.processing.CodeGenerator
|
|||||||
import com.google.devtools.ksp.processing.Resolver
|
import com.google.devtools.ksp.processing.Resolver
|
||||||
import com.google.devtools.ksp.processing.SymbolProcessor
|
import com.google.devtools.ksp.processing.SymbolProcessor
|
||||||
import com.google.devtools.ksp.symbol.*
|
import com.google.devtools.ksp.symbol.*
|
||||||
import com.squareup.kotlinpoet.AnnotationSpec
|
import com.squareup.kotlinpoet.*
|
||||||
import com.squareup.kotlinpoet.ClassName
|
|
||||||
import com.squareup.kotlinpoet.CodeBlock
|
|
||||||
import com.squareup.kotlinpoet.FileSpec
|
|
||||||
import com.squareup.kotlinpoet.FunSpec
|
|
||||||
import com.squareup.kotlinpoet.KModifier
|
|
||||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||||
import com.squareup.kotlinpoet.PropertySpec
|
|
||||||
import com.squareup.kotlinpoet.asTypeName
|
|
||||||
import com.squareup.kotlinpoet.ksp.toClassName
|
import com.squareup.kotlinpoet.ksp.toClassName
|
||||||
|
import dev.inmo.micro_ksp.generator.buildSubFileName
|
||||||
|
import dev.inmo.micro_ksp.generator.companion
|
||||||
|
import dev.inmo.micro_ksp.generator.findSubClasses
|
||||||
import dev.inmo.micro_ksp.generator.writeFile
|
import dev.inmo.micro_ksp.generator.writeFile
|
||||||
import dev.inmo.microutils.kps.sealed.GenerateSealedWorkaround
|
import dev.inmo.micro_utils.ksp.sealed.GenerateSealedTypesWorkaround
|
||||||
|
import dev.inmo.micro_utils.ksp.sealed.GenerateSealedWorkaround
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
class Processor(
|
class Processor(
|
||||||
private val codeGenerator: CodeGenerator
|
private val codeGenerator: CodeGenerator
|
||||||
) : SymbolProcessor {
|
) : SymbolProcessor {
|
||||||
private fun KSClassDeclaration.resolveSubclasses(): List<KSClassDeclaration> {
|
private fun KSClassDeclaration.findSealedConnection(potentialSealedParent: KSClassDeclaration): Boolean {
|
||||||
return (getSealedSubclasses().flatMap {
|
val targetClassname = potentialSealedParent.qualifiedName ?.asString()
|
||||||
it.resolveSubclasses()
|
return superTypes.any {
|
||||||
}.ifEmpty {
|
val itAsDeclaration = it.resolve().declaration as? KSClassDeclaration ?: return@any false
|
||||||
sequenceOf(this)
|
targetClassname == (itAsDeclaration.qualifiedName ?.asString()) || (itAsDeclaration.getSealedSubclasses().any() && itAsDeclaration.findSealedConnection(potentialSealedParent))
|
||||||
}).toList()
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun KSClassDeclaration.resolveSubclasses(
|
||||||
|
searchIn: Sequence<KSAnnotated>,
|
||||||
|
allowNonSealed: Boolean
|
||||||
|
): Sequence<KSClassDeclaration> {
|
||||||
|
return findSubClasses(searchIn).let {
|
||||||
|
if (allowNonSealed) {
|
||||||
|
it
|
||||||
|
} else {
|
||||||
|
it.filter {
|
||||||
|
it.findSealedConnection(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(KspExperimental::class)
|
@OptIn(KspExperimental::class)
|
||||||
@@ -35,7 +49,11 @@ class Processor(
|
|||||||
ksClassDeclaration: KSClassDeclaration,
|
ksClassDeclaration: KSClassDeclaration,
|
||||||
resolver: Resolver
|
resolver: Resolver
|
||||||
) {
|
) {
|
||||||
val subClasses = ksClassDeclaration.resolveSubclasses().distinct()
|
val annotation = ksClassDeclaration.getGenerateSealedWorkaroundAnnotation
|
||||||
|
val subClasses = ksClassDeclaration.resolveSubclasses(
|
||||||
|
searchIn = resolver.getAllFiles(),
|
||||||
|
allowNonSealed = annotation ?.includeNonSealedSubTypes ?: false
|
||||||
|
).distinct()
|
||||||
val subClassesNames = subClasses.filter {
|
val subClassesNames = subClasses.filter {
|
||||||
when (it.classKind) {
|
when (it.classKind) {
|
||||||
ClassKind.ENUM_ENTRY,
|
ClassKind.ENUM_ENTRY,
|
||||||
@@ -51,7 +69,7 @@ class Processor(
|
|||||||
(it.getAnnotationsByType(GenerateSealedWorkaround.Order::class).firstOrNull()) ?.order ?: 0
|
(it.getAnnotationsByType(GenerateSealedWorkaround.Order::class).firstOrNull()) ?.order ?: 0
|
||||||
}.map {
|
}.map {
|
||||||
it.toClassName()
|
it.toClassName()
|
||||||
}
|
}.toList()
|
||||||
val className = ksClassDeclaration.toClassName()
|
val className = ksClassDeclaration.toClassName()
|
||||||
val setType = Set::class.asTypeName().parameterizedBy(
|
val setType = Set::class.asTypeName().parameterizedBy(
|
||||||
ksClassDeclaration.toClassName()
|
ksClassDeclaration.toClassName()
|
||||||
@@ -73,7 +91,10 @@ class Processor(
|
|||||||
)
|
)
|
||||||
addFunction(
|
addFunction(
|
||||||
FunSpec.builder("values").apply {
|
FunSpec.builder("values").apply {
|
||||||
receiver(ClassName(className.packageName, *className.simpleNames.toTypedArray(), "Companion"))
|
val companion = ksClassDeclaration.takeIf { it.isCompanionObject } ?.toClassName()
|
||||||
|
?: ksClassDeclaration.companion ?.toClassName()
|
||||||
|
?: ClassName(className.packageName, *className.simpleNames.toTypedArray(), "Companion")
|
||||||
|
receiver(companion)
|
||||||
returns(setType)
|
returns(setType)
|
||||||
addCode(
|
addCode(
|
||||||
CodeBlock.of(
|
CodeBlock.of(
|
||||||
@@ -84,10 +105,68 @@ class Processor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(KspExperimental::class)
|
||||||
|
private fun FileSpec.Builder.generateSealedTypesWorkaround(
|
||||||
|
ksClassDeclaration: KSClassDeclaration,
|
||||||
|
resolver: Resolver
|
||||||
|
) {
|
||||||
|
val annotation = ksClassDeclaration.getGenerateSealedTypesWorkaroundAnnotation
|
||||||
|
val subClasses = ksClassDeclaration.resolveSubclasses(
|
||||||
|
searchIn = resolver.getAllFiles(),
|
||||||
|
allowNonSealed = annotation ?.includeNonSealedSubTypes ?: false
|
||||||
|
).distinct()
|
||||||
|
val subClassesNames = subClasses.filter {
|
||||||
|
it.getAnnotationsByType(GenerateSealedTypesWorkaround.Exclude::class).count() == 0
|
||||||
|
}.sortedBy {
|
||||||
|
(it.getAnnotationsByType(GenerateSealedTypesWorkaround.Order::class).firstOrNull()) ?.order ?: 0
|
||||||
|
}.map {
|
||||||
|
it.toClassName()
|
||||||
|
}.toList()
|
||||||
|
val className = ksClassDeclaration.toClassName()
|
||||||
|
val setType = Set::class.asTypeName().parameterizedBy(
|
||||||
|
KClass::class.asTypeName().parameterizedBy(
|
||||||
|
TypeVariableName(
|
||||||
|
"out ${ksClassDeclaration.asStarProjectedType().toClassName().simpleNames.joinToString(".")}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
addProperty(
|
||||||
|
PropertySpec.builder(
|
||||||
|
"subtypes",
|
||||||
|
setType
|
||||||
|
).apply {
|
||||||
|
modifiers.add(
|
||||||
|
KModifier.PRIVATE
|
||||||
|
)
|
||||||
|
initializer(
|
||||||
|
CodeBlock.of(
|
||||||
|
"""setOf(${subClassesNames.joinToString(",\n") { it.simpleNames.joinToString(".") + "::class" }})"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}.build()
|
||||||
|
)
|
||||||
|
addFunction(
|
||||||
|
FunSpec.builder("subtypes").apply {
|
||||||
|
val companion = ksClassDeclaration.takeIf { it.isCompanionObject } ?.toClassName()
|
||||||
|
?: ksClassDeclaration.companion ?.toClassName()
|
||||||
|
?: ClassName(className.packageName, *className.simpleNames.toTypedArray(), "Companion")
|
||||||
|
receiver(companion)
|
||||||
|
returns(setType)
|
||||||
|
addCode(
|
||||||
|
CodeBlock.of(
|
||||||
|
"""return subtypes"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(KspExperimental::class)
|
@OptIn(KspExperimental::class)
|
||||||
override fun process(resolver: Resolver): List<KSAnnotated> {
|
override fun process(resolver: Resolver): List<KSAnnotated> {
|
||||||
(resolver.getSymbolsWithAnnotation(GenerateSealedWorkaround::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach {
|
(resolver.getSymbolsWithAnnotation(GenerateSealedWorkaround::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach {
|
||||||
val prefix = it.getAnnotationsByType(GenerateSealedWorkaround::class).first().prefix
|
val prefix = (it.getGenerateSealedWorkaroundAnnotation) ?.prefix ?.takeIf {
|
||||||
|
it.isNotEmpty()
|
||||||
|
} ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "")
|
||||||
it.writeFile(prefix = prefix, suffix = "SealedWorkaround") {
|
it.writeFile(prefix = prefix, suffix = "SealedWorkaround") {
|
||||||
FileSpec.builder(
|
FileSpec.builder(
|
||||||
it.packageName.asString(),
|
it.packageName.asString(),
|
||||||
@@ -104,6 +183,26 @@ class Processor(
|
|||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
(resolver.getSymbolsWithAnnotation(GenerateSealedTypesWorkaround::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach {
|
||||||
|
val prefix = (it.getGenerateSealedTypesWorkaroundAnnotation) ?.prefix ?.takeIf {
|
||||||
|
it.isNotEmpty()
|
||||||
|
} ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "")
|
||||||
|
it.writeFile(prefix = prefix, suffix = "SealedTypesWorkaround") {
|
||||||
|
FileSpec.builder(
|
||||||
|
it.packageName.asString(),
|
||||||
|
"${it.simpleName.getShortName()}SealedTypesWorkaround"
|
||||||
|
).apply {
|
||||||
|
addFileComment(
|
||||||
|
"""
|
||||||
|
THIS CODE HAVE BEEN GENERATED AUTOMATICALLY
|
||||||
|
TO REGENERATE IT JUST DELETE FILE
|
||||||
|
ORIGINAL FILE: ${it.containingFile ?.fileName}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
generateSealedTypesWorkaround(it, resolver)
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,9 @@ plugins {
|
|||||||
id "com.google.devtools.ksp"
|
id "com.google.devtools.ksp"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
ext.do_publish = false
|
||||||
|
|
||||||
|
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
|
||||||
|
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
@@ -1,14 +1,19 @@
|
|||||||
package dev.inmo.micro_utils.ksp.sealed.generator.test
|
package dev.inmo.micro_utils.ksp.sealed.generator.test
|
||||||
|
|
||||||
import dev.inmo.microutils.kps.sealed.GenerateSealedWorkaround
|
import dev.inmo.micro_utils.ksp.sealed.GenerateSealedTypesWorkaround
|
||||||
|
import dev.inmo.micro_utils.ksp.sealed.GenerateSealedWorkaround
|
||||||
|
|
||||||
@GenerateSealedWorkaround
|
@GenerateSealedWorkaround
|
||||||
|
@GenerateSealedTypesWorkaround
|
||||||
sealed interface Test {
|
sealed interface Test {
|
||||||
@GenerateSealedWorkaround.Order(2)
|
@GenerateSealedWorkaround.Order(2)
|
||||||
|
@GenerateSealedTypesWorkaround.Exclude
|
||||||
object A : Test
|
object A : Test
|
||||||
@GenerateSealedWorkaround.Exclude
|
@GenerateSealedWorkaround.Exclude
|
||||||
|
@GenerateSealedTypesWorkaround.Order(2)
|
||||||
object B : Test
|
object B : Test
|
||||||
@GenerateSealedWorkaround.Order(0)
|
@GenerateSealedWorkaround.Order(0)
|
||||||
|
@GenerateSealedTypesWorkaround.Order(0)
|
||||||
object C : Test
|
object C : Test
|
||||||
|
|
||||||
// Required for successful sealed workaround generation
|
// Required for successful sealed workaround generation
|
||||||
|
@@ -0,0 +1,12 @@
|
|||||||
|
// THIS CODE HAVE BEEN GENERATED AUTOMATICALLY
|
||||||
|
// TO REGENERATE IT JUST DELETE FILE
|
||||||
|
// ORIGINAL FILE: Test.kt
|
||||||
|
package dev.inmo.micro_utils.ksp.`sealed`.generator.test
|
||||||
|
|
||||||
|
import kotlin.collections.Set
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
private val subtypes: Set<KClass<out Test>> = setOf(Test.C::class,
|
||||||
|
Test.B::class)
|
||||||
|
|
||||||
|
public fun Test.Companion.subtypes(): Set<KClass<out Test>> = subtypes
|
40
ksp/sealed/generator/test/src/jvmTest/kotlin/TestTests.kt
Normal file
40
ksp/sealed/generator/test/src/jvmTest/kotlin/TestTests.kt
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import dev.inmo.micro_utils.ksp.sealed.generator.test.subtypes
|
||||||
|
import dev.inmo.micro_utils.ksp.sealed.generator.test.values
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class TestTests {
|
||||||
|
@Test
|
||||||
|
fun testThatAfterCompilationTestWorkaroundsHaveCorrectValues() {
|
||||||
|
val correctValues = arrayOf(
|
||||||
|
dev.inmo.micro_utils.ksp.sealed.generator.test.Test.C,
|
||||||
|
dev.inmo.micro_utils.ksp.sealed.generator.test.Test.A,
|
||||||
|
)
|
||||||
|
val correctSubtypes = arrayOf(
|
||||||
|
dev.inmo.micro_utils.ksp.sealed.generator.test.Test.C::class,
|
||||||
|
dev.inmo.micro_utils.ksp.sealed.generator.test.Test.B::class,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
correctValues.size, dev.inmo.micro_utils.ksp.sealed.generator.test.Test.values().size
|
||||||
|
)
|
||||||
|
correctValues.forEachIndexed { index, value ->
|
||||||
|
assertTrue(
|
||||||
|
value === dev.inmo.micro_utils.ksp.sealed.generator.test.Test.values().elementAt(index)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
correctSubtypes.size, dev.inmo.micro_utils.ksp.sealed.generator.test.Test.subtypes().size
|
||||||
|
)
|
||||||
|
correctSubtypes.forEachIndexed { index, value ->
|
||||||
|
assertTrue(
|
||||||
|
value.qualifiedName != null
|
||||||
|
)
|
||||||
|
assertTrue(
|
||||||
|
value.qualifiedName === dev.inmo.micro_utils.ksp.sealed.generator.test.Test.subtypes().elementAt(index).qualifiedName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user