mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 14:29:24 +00:00
Compare commits
164 Commits
Author | SHA1 | Date | |
---|---|---|---|
1c0b8cf842 | |||
a1624ea2a9 | |||
23a050cf1e | |||
916f2f96f4 | |||
00cc214754 | |||
b2e38f72b9 | |||
e7107d238d | |||
ed9ebdbd1a | |||
e80676d3d2 | |||
02d02fa8f2 | |||
bd783fb74f | |||
50386adf70 | |||
f4ee6c2890 | |||
d45aef9fe5 | |||
a56cd3dddd | |||
419e7070ee | |||
612cf40b5f | |||
8b39882e83 | |||
e639ae172b | |||
d0446850ae | |||
c48465b90b | |||
f419fd03d2 | |||
494812a660 | |||
eb78f21eec | |||
4bda70268b | |||
f037ce4371 | |||
3d2196e35d | |||
a74f061b02 | |||
11ade14676 | |||
eb562d8784 | |||
1ee5b4bfd4 | |||
d97892080b | |||
6f37125724 | |||
ed1baaade7 | |||
bb9669f8fd | |||
bdac715d48 | |||
acf4971298 | |||
249bc83a8c | |||
0fbb92f03f | |||
ca27cb3f82 | |||
3a5771a0cc | |||
527a2a91ac | |||
6763e5c4c6 | |||
06918d8310 | |||
89ccaa1b57 | |||
5d0bdb9bcf | |||
31fdcf74a5 | |||
afca09cc1d | |||
531d89d9db | |||
6bbbea0bc3 | |||
e337cd98c8 | |||
bcbab3b380 | |||
fb63de7568 | |||
aa45a4ab13 | |||
2af7e2f681 | |||
34fd9edce0 | |||
2a4cb8c5f9 | |||
50ea40bc3a | |||
a77654052d | |||
88aafce552 | |||
4e95d6bfff | |||
38d0e34fb5 | |||
8fbc6b9041 | |||
e8219d6cf4 | |||
6c20fc4ca6 | |||
85cd975492 | |||
1171a717fe | |||
bbe5320312 | |||
00acb9fddd | |||
de3d14dc41 | |||
67ff9cc9b3 | |||
af132103a0 | |||
3b1124a804 | |||
f226c2dfd6 | |||
69d6e63846 | |||
02c3d397ad | |||
67a1050646 | |||
8cd0775a6c | |||
162294d6c6 | |||
c4dd19dd00 | |||
d2314422f1 | |||
6fedd6f859 | |||
e52b59665f | |||
cda9d09689 | |||
c9237b3f00 | |||
18bba66c4a | |||
63418c4a8a | |||
2e66c6f4e3 | |||
e9c5df4c13 | |||
bc7789ad2c | |||
e3da761249 | |||
4082f65afa | |||
5d1cab075d | |||
bcf67f7e59 | |||
7d3b1f8e75 | |||
119a0588cc | |||
fab789d9c0 | |||
ceba81c08f | |||
a061af0558 | |||
c7a53846ad | |||
a683cccf0c | |||
50d41e35c1 | |||
aa0e831cea | |||
44e26ccb4f | |||
2a783f6e2b | |||
6058d6a724 | |||
2e9c7eb5fa | |||
e75465ad10 | |||
de01ad54e9 | |||
eeea7ddbe3 | |||
e0b18bec05 | |||
410e89bba9 | |||
9ef19dc42b | |||
0337d1b82d | |||
f5bd4c5ccb | |||
630f9bc0d4 | |||
18b4ffece1 | |||
f64e1effa3 | |||
847fcbb488 | |||
88002ec8e7 | |||
7f8db6a29d | |||
b183b82443 | |||
5dad27de72 | |||
6b66084d0e | |||
50b56a7c39 | |||
7ab7d14471 | |||
bdcc179b7b | |||
55ffd4b46f | |||
7fc5ee70e1 | |||
a24a335743 | |||
ef9af71960 | |||
925702d315 | |||
d50dffec8c | |||
cef2081a13 | |||
06c8bde7c9 | |||
c9bbfa3820 | |||
eed7cfdc42 | |||
bd9b0d16ab | |||
ea6c33b497 | |||
dc80ade2fb | |||
f6a06ee8ea | |||
2644f27975 | |||
3dc68a7b8b | |||
97fc1d6239 | |||
662f4d22a3 | |||
b70aa12be9 | |||
71f12f5f19 | |||
e10504eeeb | |||
2dea9f3bc0 | |||
35c9dda5bc | |||
e831f3949a | |||
b0b39cc693 | |||
fc03be3f73 | |||
b61f6b81f1 | |||
f5bc1c1fce | |||
a729f9568c | |||
5749e00377 | |||
ef73c24a0c | |||
94717ee351 | |||
9a18ded65b | |||
b23220f491 | |||
6e6bb03246 | |||
1ae6bae3b8 | |||
57b7797ea4 |
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
@@ -1,12 +0,0 @@
|
||||
name: Regular build
|
||||
on: [push]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
- name: Build
|
||||
run: ./gradlew build
|
5
.github/workflows/dokka_push.yml
vendored
5
.github/workflows/dokka_push.yml
vendored
@@ -10,7 +10,10 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
java-version: 11
|
||||
- name: Fix android 32.0.0 dx
|
||||
continue-on-error: true
|
||||
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
|
||||
- name: Build
|
||||
run: ./gradlew dokkaHtml
|
||||
- name: Publish KDocs
|
||||
|
6
.github/workflows/packages_push.yml
vendored
6
.github/workflows/packages_push.yml
vendored
@@ -8,7 +8,10 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
java-version: 11
|
||||
- name: Fix android 32.0.0 dx
|
||||
continue-on-error: true
|
||||
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
|
||||
- name: Rewrite version
|
||||
run: |
|
||||
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
|
||||
@@ -18,6 +21,7 @@ jobs:
|
||||
- name: Build
|
||||
run: ./gradlew build
|
||||
- name: Publish
|
||||
continue-on-error: true
|
||||
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidDebugPublication -x signAndroidReleasePublication -x signKotlinMultiplatformPublication
|
||||
env:
|
||||
GITHUBPACKAGES_USER: ${{ github.actor }}
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,5 +11,6 @@ out/
|
||||
|
||||
secret.gradle
|
||||
local.properties
|
||||
kotlin-js-store
|
||||
|
||||
publishing.sh
|
||||
|
255
CHANGELOG.md
255
CHANGELOG.md
@@ -1,5 +1,260 @@
|
||||
# Changelog
|
||||
|
||||
## 0.9.2
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.4.10` -> `2.4.12`
|
||||
|
||||
## 0.9.1
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* Default realizations of standard interfaces for exposed DB are using public fields for now:
|
||||
* `ExposedReadKeyValueRepo`
|
||||
* `ExposedReadOneToManyKeyValueRepo`
|
||||
* `ExposedStandardVersionsRepoProxy`
|
||||
* New typealiases for one to many exposed realizations:
|
||||
* `ExposedReadKeyValuesRepo`
|
||||
* `ExposedKeyValuesRepo`
|
||||
|
||||
## 0.9.0
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.5.31` -> `1.6.10`
|
||||
* `Coroutines`: `1.5.2` -> `1.6.0`
|
||||
* `Serialization`: `1.3.1` -> `1.3.2`
|
||||
* `Exposed`: `0.36.2` -> `0.37.2`
|
||||
* `Ktor`: `1.6.5` -> `1.6.7`
|
||||
* `Klock`: `2.4.8` -> `2.4.10`
|
||||
|
||||
## 0.8.9
|
||||
|
||||
* `Ktor`:
|
||||
* `Server`:
|
||||
* Fixes in `uniloadMultipart`
|
||||
* `Client`:
|
||||
* Fixes in `unimultipart`
|
||||
* `FSM`:
|
||||
* Fixes in `DefaultUpdatableStatesMachine`
|
||||
|
||||
## 0.8.8
|
||||
|
||||
* `Versions`:
|
||||
* `AppCompat`: `1.3.1` -> `1.4.0`
|
||||
* Android Compile SDK: `31.0.0` -> `32.0.0`
|
||||
* `FSM`:
|
||||
* `DefaultStatesMachine` now is extendable
|
||||
* New type `UpdatableStatesMachine` with default realization`DefaultUpdatableStatesMachine`
|
||||
|
||||
## 0.8.7
|
||||
|
||||
* `Ktor`:
|
||||
* `Client`:
|
||||
* `UnifiedRequester` now have no private fields
|
||||
* Add preview work with multipart
|
||||
* `Server`
|
||||
* `UnifiedRouter` now have no private fields
|
||||
* Add preview work with multipart
|
||||
|
||||
## 0.8.6
|
||||
|
||||
* `Common`:
|
||||
* `Either` extensions `onFirst` and `onSecond` now accept not `crossinline` callbacks
|
||||
* All `joinTo` now accept not `crossinline` callbacks
|
||||
|
||||
## 0.8.5
|
||||
|
||||
* `Common`:
|
||||
* `repeatOnFailure`
|
||||
|
||||
## 0.8.4
|
||||
|
||||
* `Ktor`:
|
||||
* `Server`:
|
||||
* Several new `createKtorServer`
|
||||
|
||||
## 0.8.3
|
||||
|
||||
* `Common`:
|
||||
* Ranges intersection functionality
|
||||
* New type `Optional`
|
||||
* `Pagination`:
|
||||
* `Pagination` now extends `ClosedRange<Int>`
|
||||
* `Pagination` intersection functionality
|
||||
|
||||
## 0.8.2
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.4.7` -> `2.4.8`
|
||||
* `Serialization`: `1.3.0` -> `1.3.1`
|
||||
* `FSM`:
|
||||
* Now it is possible to pass any `CheckableHandlerHolder` in `FSMBuilder`
|
||||
* Now `StatesMachine` works with `CheckableHandlerHolder` instead of `CustomizableHandlerHolder`
|
||||
|
||||
## 0.8.1
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.36.1` -> `0.36.2`
|
||||
* `Core KTX`: `1.6.0` -> `1.7.0`
|
||||
|
||||
## 0.8.0
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.4.6` -> `2.4.7`
|
||||
* `Ktor`: `1.6.4` -> `1.6.5`
|
||||
* `Exposed`: `0.35.3` -> `0.36.1`
|
||||
* `Common`:
|
||||
* Type `Either` got its own serializer
|
||||
* `FSM`:
|
||||
* `Common`:
|
||||
* Full rework of FSM:
|
||||
* Now it is more flexible for checking of handler opportunity to handle state
|
||||
* Now machine and states managers are type-oriented
|
||||
* `StateHandlerHolder` has been renamed to `CheckableHandlerHolder`
|
||||
* Add opportunity for comfortable adding default state handler
|
||||
|
||||
## 0.7.4
|
||||
|
||||
* `Common`:
|
||||
* New type `Either`
|
||||
* `Serialization`:
|
||||
* `TypedSerializer`
|
||||
* New factory fun which accept vararg pairs of type and its serializer
|
||||
* `Repos`:
|
||||
* `Common` (`Android`):
|
||||
* `AbstractMutableAndroidCRUDRepo` flows now will have extra buffer capacity instead of reply. It means that
|
||||
android crud repo _WILL NOT_ send previous events to the
|
||||
* `Exposed`:
|
||||
* New parameter `AbstractExposedWriteCRUDRepo#replyCacheInFlows`
|
||||
* KeyValue realization `ExposedKeyValueRepo` properties `_onNewValue` and `_onValueRemoved` now are available in
|
||||
inheritors
|
||||
* `Pagination`:
|
||||
* `Common`:
|
||||
* New types `getAllBy*` for current, next and custom paging
|
||||
|
||||
## 0.7.3
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.35.2` -> `0.35.3`
|
||||
|
||||
## 0.7.2
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.4.5` -> `2.4.6`
|
||||
|
||||
## 0.7.1
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.4.3` -> `2.4.5`
|
||||
* `Exposed`: `0.35.1` -> `0.35.2`
|
||||
* `Coroutines`:
|
||||
* `Common`:
|
||||
* New `Flow` - `AccumulatorFlow`
|
||||
* `FSM`:
|
||||
* `Common`:
|
||||
* `InMemoryStatesManager` has been replaced
|
||||
* `StatesMachine` became an interface
|
||||
* New manager `DefaultStatesManager` with `DefaultStatesManagerRepo` for abstraction of manager and storing of
|
||||
data info
|
||||
|
||||
## 0.7.0
|
||||
|
||||
**THIS VERSION HAS MIGRATED FROM KOTLINX DATETIME TO KORLIBS KLOCK. CAREFUL**
|
||||
|
||||
* `Versions`
|
||||
* `kotlinx.datetime` -> `Klock`
|
||||
|
||||
## 0.6.0 DO NOT RECOMMENDED
|
||||
|
||||
**THIS VERSION HAS MIGRATED FROM KORLIBS KLOCK TO KOTLINX DATETIME. CAREFUL**
|
||||
**ALL DEPRECATION HAVE BEEN REMOVED**
|
||||
|
||||
* `Versions`
|
||||
* `Klock` -> `kotlinx.datetime`
|
||||
|
||||
## 0.5.31
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.4.2` -> `2.4.3`
|
||||
* `Ktor`: `1.6.3` -> `1.6.4`
|
||||
|
||||
## 0.5.30
|
||||
|
||||
* `Versions`:
|
||||
* `Serialization`: `1.2.2` -> `1.3.0`
|
||||
|
||||
## 0.5.29
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.34.2` -> `0.35.1`
|
||||
|
||||
## 0.5.28
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.5.30` -> `1.5.31`
|
||||
* `Klock`: `2.4.1` -> `2.4.2`
|
||||
|
||||
## 0.5.27
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.34.1` -> `0.34.2`
|
||||
|
||||
## 0.5.26
|
||||
|
||||
* `Repos`:
|
||||
* `InMemory`:
|
||||
* `MapCRUDRepo`s and `MapKeyValueRepo`s got `protected` methods and properties instead of private
|
||||
|
||||
## 0.5.25
|
||||
|
||||
* `Versions`:
|
||||
* `UUID`: `0.3.0` -> `0.3.1`
|
||||
* `Common`:
|
||||
* New property `MPPFile#withoutSlashAtTheEnd`
|
||||
* Extension `clamp` has been deprecated
|
||||
* New extension `Iterable#diff`
|
||||
* `Serialization`:
|
||||
* New operators `TypedSerializer#plusAssign` and `TypedSerializer#minusAssign`
|
||||
|
||||
## 0.5.24
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.5.1` -> `1.5.2`
|
||||
* `Klock`: `2.3.4` -> `2.4.1`
|
||||
* `Coroutines`:
|
||||
* New function `CoroutineScope` with safely exceptions handler as second parameter
|
||||
|
||||
## 0.5.23
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.33.1` -> `0.34.1`
|
||||
* `Common`:
|
||||
* New extensions `Iterable#joinTo` and `Array#joinTo`
|
||||
|
||||
## 0.5.22
|
||||
|
||||
* `Versions`
|
||||
* `Kotlin`: `1.5.21` -> `1.5.30`
|
||||
* `Klock`: `2.3.2` -> `2.3.4`
|
||||
* `AppCompat`: `1.3.0` -> `1.3.1`
|
||||
* `Ktor`: `1.6.2` -> `1.6.3`
|
||||
|
||||
## 0.5.21
|
||||
|
||||
* `Versions`
|
||||
* `Klock`: `2.3.1` -> `2.3.2`
|
||||
* `Serialization`
|
||||
* `Typed Serializer`:
|
||||
* `TypedSerializer` Descriptor serial name has been fixed
|
||||
|
||||
## 0.5.20
|
||||
|
||||
* `Repos`:
|
||||
* `Common`
|
||||
* `Android`:
|
||||
* `*OrNull` analogs of `Cursor.get*(String)` extensions have been added
|
||||
* Extensions `Cursor.getFloat` and `Cursor.getFloatOrNull` have been added
|
||||
|
||||
## 0.5.19
|
||||
|
||||
* `LanguageCode`:
|
||||
|
@@ -7,7 +7,7 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.3'
|
||||
classpath 'com.android.tools.build:gradle:7.0.4'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
|
||||
|
@@ -12,9 +12,7 @@ package dev.inmo.micro_utils.common
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
AnnotationTarget.PROPERTY_SETTER,
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.TYPE,
|
||||
AnnotationTarget.TYPEALIAS,
|
||||
AnnotationTarget.TYPE_PARAMETER
|
||||
AnnotationTarget.TYPEALIAS
|
||||
)
|
||||
annotation class PreviewFeature(val message: String = "It is possible, that behaviour of this thing will be changed or removed in future releases")
|
||||
|
||||
@@ -30,8 +28,6 @@ annotation class PreviewFeature(val message: String = "It is possible, that beha
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
AnnotationTarget.PROPERTY_SETTER,
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.TYPE,
|
||||
AnnotationTarget.TYPEALIAS,
|
||||
AnnotationTarget.TYPE_PARAMETER
|
||||
AnnotationTarget.TYPEALIAS
|
||||
)
|
||||
annotation class Warning(val message: String)
|
||||
|
@@ -1,10 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T : Comparable<T>> T.clamp(min: T, max: T): T {
|
||||
return when {
|
||||
this < min -> min
|
||||
this > max -> max
|
||||
else -> this
|
||||
}
|
||||
}
|
@@ -27,8 +27,8 @@ data class Diff<T> internal constructor(
|
||||
|
||||
private inline fun <T> performChanges(
|
||||
potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>,
|
||||
additionalsInOld: MutableList<T>,
|
||||
additionalsInNew: MutableList<T>,
|
||||
additionsInOld: MutableList<T>,
|
||||
additionsInNew: MutableList<T>,
|
||||
changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>,
|
||||
removedList: MutableList<IndexedValue<T>>,
|
||||
addedList: MutableList<IndexedValue<T>>,
|
||||
@@ -52,20 +52,20 @@ private inline fun <T> performChanges(
|
||||
newPotentials.first().second ?.let { addedList.add(it) }
|
||||
newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) ->
|
||||
addedList.add(newOne!!)
|
||||
oldOne ?.let { additionalsInOld.add(oldOne.value) }
|
||||
oldOne ?.let { additionsInOld.add(oldOne.value) }
|
||||
}
|
||||
if (newPotentials.size > 1) {
|
||||
newPotentials.last().first ?.value ?.let { additionalsInOld.add(it) }
|
||||
newPotentials.last().first ?.value ?.let { additionsInOld.add(it) }
|
||||
}
|
||||
}
|
||||
newOneEqualToOldObject -> {
|
||||
newPotentials.first().first ?.let { removedList.add(it) }
|
||||
newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) ->
|
||||
removedList.add(oldOne!!)
|
||||
newOne ?.let { additionalsInNew.add(newOne.value) }
|
||||
newOne ?.let { additionsInNew.add(newOne.value) }
|
||||
}
|
||||
if (newPotentials.size > 1) {
|
||||
newPotentials.last().second ?.value ?.let { additionalsInNew.add(it) }
|
||||
newPotentials.last().second ?.value ?.let { additionsInNew.add(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,6 +139,10 @@ fun <T> Iterable<T>.calculateDiff(
|
||||
|
||||
return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList())
|
||||
}
|
||||
inline fun <T> Iterable<T>.diff(
|
||||
other: Iterable<T>,
|
||||
strictComparison: Boolean = false
|
||||
): Diff<T> = calculateDiff(other, strictComparison)
|
||||
|
||||
inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new)
|
||||
inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true)
|
||||
|
@@ -0,0 +1,148 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.descriptors.*
|
||||
import kotlinx.serialization.encoding.*
|
||||
|
||||
/**
|
||||
* Realization of this interface will contains at least one not null - [t1] or [t2]
|
||||
*
|
||||
* @see EitherFirst
|
||||
* @see EitherSecond
|
||||
* @see Either.Companion.first
|
||||
* @see Either.Companion.second
|
||||
* @see Either.onFirst
|
||||
* @see Either.onSecond
|
||||
*/
|
||||
@Serializable(EitherSerializer::class)
|
||||
sealed interface Either<T1, T2> {
|
||||
val t1: T1?
|
||||
val t2: T2?
|
||||
|
||||
companion object {
|
||||
fun <T1, T2> serializer(
|
||||
t1Serializer: KSerializer<T1>,
|
||||
t2Serializer: KSerializer<T2>,
|
||||
): KSerializer<Either<T1, T2>> = EitherSerializer(t1Serializer, t2Serializer)
|
||||
}
|
||||
}
|
||||
|
||||
class EitherSerializer<T1, T2>(
|
||||
t1Serializer: KSerializer<T1>,
|
||||
t2Serializer: KSerializer<T2>,
|
||||
) : KSerializer<Either<T1, T2>> {
|
||||
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||
override val descriptor: SerialDescriptor = buildSerialDescriptor(
|
||||
"TypedSerializer",
|
||||
SerialKind.CONTEXTUAL
|
||||
) {
|
||||
element("type", String.serializer().descriptor)
|
||||
element("value", ContextualSerializer(Either::class).descriptor)
|
||||
}
|
||||
private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer)
|
||||
private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer)
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||
override fun deserialize(decoder: Decoder): Either<T1, T2> {
|
||||
return decoder.decodeStructure(descriptor) {
|
||||
var type: String? = null
|
||||
lateinit var result: Either<T1, T2>
|
||||
while (true) {
|
||||
when (val index = decodeElementIndex(descriptor)) {
|
||||
0 -> type = decodeStringElement(descriptor, 0)
|
||||
1 -> {
|
||||
result = when (type) {
|
||||
"t1" -> decodeSerializableElement(
|
||||
descriptor,
|
||||
1,
|
||||
t1EitherSerializer
|
||||
)
|
||||
"t2" -> decodeSerializableElement(
|
||||
descriptor,
|
||||
1,
|
||||
t2EitherSerializer
|
||||
)
|
||||
else -> error("Unknown type of either: $type")
|
||||
}
|
||||
}
|
||||
CompositeDecoder.DECODE_DONE -> break
|
||||
else -> error("Unexpected index: $index")
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||
override fun serialize(encoder: Encoder, value: Either<T1, T2>) {
|
||||
encoder.encodeStructure(descriptor) {
|
||||
when (value) {
|
||||
is EitherFirst -> {
|
||||
encodeStringElement(descriptor, 0, "t1")
|
||||
encodeSerializableElement(descriptor, 1, t1EitherSerializer, value)
|
||||
}
|
||||
is EitherSecond -> {
|
||||
encodeStringElement(descriptor, 0, "t2")
|
||||
encodeSerializableElement(descriptor, 1, t2EitherSerializer, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This type [Either] will always have not nullable [t1]
|
||||
*/
|
||||
@Serializable
|
||||
data class EitherFirst<T1, T2>(
|
||||
override val t1: T1
|
||||
) : Either<T1, T2> {
|
||||
override val t2: T2?
|
||||
get() = null
|
||||
}
|
||||
|
||||
/**
|
||||
* This type [Either] will always have not nullable [t2]
|
||||
*/
|
||||
@Serializable
|
||||
data class EitherSecond<T1, T2>(
|
||||
override val t2: T2
|
||||
) : Either<T1, T2> {
|
||||
override val t1: T1?
|
||||
get() = null
|
||||
}
|
||||
|
||||
/**
|
||||
* @return New instance of [EitherFirst]
|
||||
*/
|
||||
inline fun <T1, T2> Either.Companion.first(t1: T1): Either<T1, T2> = EitherFirst(t1)
|
||||
/**
|
||||
* @return New instance of [EitherSecond]
|
||||
*/
|
||||
inline fun <T1, T2> Either.Companion.second(t2: T2): Either<T1, T2> = EitherSecond(t2)
|
||||
|
||||
/**
|
||||
* Will call [block] in case when [Either.t1] of [this] is not null
|
||||
*/
|
||||
inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(block: (T1) -> Unit): E {
|
||||
val t1 = t1
|
||||
t1 ?.let(block)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [block] in case when [Either.t2] of [this] is not null
|
||||
*/
|
||||
inline fun <T1, T2, E : Either<T1, T2>> E.onSecond(block: (T2) -> Unit): E {
|
||||
val t2 = t2
|
||||
t2 ?.let(block)
|
||||
return this
|
||||
}
|
||||
|
||||
inline fun <reified T1, reified T2> Any.either() = when (this) {
|
||||
is T1 -> Either.first<T1, T2>(this)
|
||||
is T2 -> Either.second<T1, T2>(this)
|
||||
else -> error("Incorrect type of either argument $this")
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
inline fun <I, R> Iterable<I>.joinTo(
|
||||
separatorFun: (I) -> R?,
|
||||
prefix: R? = null,
|
||||
postfix: R? = null,
|
||||
transform: (I) -> R?
|
||||
): List<R> {
|
||||
val result = mutableListOf<R>()
|
||||
val iterator = iterator()
|
||||
|
||||
prefix ?.let(result::add)
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
val element = iterator.next()
|
||||
result.add(transform(element) ?: continue)
|
||||
|
||||
if (iterator.hasNext()) {
|
||||
result.add(separatorFun(element) ?: continue)
|
||||
}
|
||||
}
|
||||
|
||||
postfix ?.let(result::add)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
inline fun <I, R> Iterable<I>.joinTo(
|
||||
separator: R? = null,
|
||||
prefix: R? = null,
|
||||
postfix: R? = null,
|
||||
transform: (I) -> R?
|
||||
): List<R> = joinTo({ separator }, prefix, postfix, transform)
|
||||
|
||||
inline fun <I> Iterable<I>.joinTo(
|
||||
separatorFun: (I) -> I?,
|
||||
prefix: I? = null,
|
||||
postfix: I? = null
|
||||
): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it }
|
||||
|
||||
inline fun <I> Iterable<I>.joinTo(
|
||||
separator: I? = null,
|
||||
prefix: I? = null,
|
||||
postfix: I? = null
|
||||
): List<I> = joinTo<I>({ separator }, prefix, postfix)
|
||||
|
||||
inline fun <I, reified R> Array<I>.joinTo(
|
||||
separatorFun: (I) -> R?,
|
||||
prefix: R? = null,
|
||||
postfix: R? = null,
|
||||
transform: (I) -> R?
|
||||
): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray()
|
||||
|
||||
inline fun <I, reified R> Array<I>.joinTo(
|
||||
separator: R? = null,
|
||||
prefix: R? = null,
|
||||
postfix: R? = null,
|
||||
transform: (I) -> R?
|
||||
): Array<R> = asIterable().joinTo(separator, prefix, postfix, transform).toTypedArray()
|
@@ -7,7 +7,7 @@ import kotlin.jvm.JvmInline
|
||||
@JvmInline
|
||||
value class FileName(val string: String) {
|
||||
val name: String
|
||||
get() = string.takeLastWhile { it != '/' }
|
||||
get() = withoutSlashAtTheEnd.takeLastWhile { it != '/' }
|
||||
val extension: String
|
||||
get() = name.takeLastWhile { it != '.' }
|
||||
val nameWithoutExtension: String
|
||||
@@ -17,15 +17,18 @@ value class FileName(val string: String) {
|
||||
filename.substring(0, it)
|
||||
} ?: filename
|
||||
}
|
||||
val withoutSlashAtTheEnd: String
|
||||
get() = string.dropLastWhile { it == '/' }
|
||||
override fun toString(): String = string
|
||||
}
|
||||
|
||||
|
||||
@PreviewFeature
|
||||
expect class MPPFile
|
||||
|
||||
expect val MPPFile.filename: FileName
|
||||
expect val MPPFile.filesize: Long
|
||||
expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
||||
expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||
fun MPPFile.bytesSync() = bytesAllocatorSync()
|
||||
suspend fun MPPFile.bytes() = bytesAllocator()
|
||||
|
||||
|
@@ -0,0 +1,92 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* This type represents [T] as not only potentially nullable data, but also as a data which can not be presented. This
|
||||
* type will be useful in cases when [T] is nullable and null as valuable data too in time of data absence should be
|
||||
* presented by some third type.
|
||||
*
|
||||
* Let's imagine, you have nullable name in some database. In case when name is not nullable everything is clear - null
|
||||
* will represent absence of row in the database. In case when name is nullable null will be a little bit dual-meaning,
|
||||
* cause this null will say nothing about availability of the row (of course, it is exaggerated example)
|
||||
*
|
||||
* @see Optional.presented
|
||||
* @see Optional.absent
|
||||
* @see Optional.optional
|
||||
* @see Optional.onPresented
|
||||
* @see Optional.onAbsent
|
||||
*/
|
||||
@Serializable
|
||||
data class Optional<T> internal constructor(
|
||||
@Warning("It is unsafe to use this data directly")
|
||||
val data: T?,
|
||||
@Warning("It is unsafe to use this data directly")
|
||||
val dataPresented: Boolean
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Will create [Optional] with presented data
|
||||
*/
|
||||
fun <T> presented(data: T) = Optional(data, true)
|
||||
/**
|
||||
* Will create [Optional] without data
|
||||
*/
|
||||
fun <T> absent() = Optional<T>(null, false)
|
||||
}
|
||||
}
|
||||
|
||||
inline val <T> T.optional
|
||||
get() = Optional.presented(this)
|
||||
|
||||
/**
|
||||
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||
*/
|
||||
inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply {
|
||||
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||
*/
|
||||
inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run {
|
||||
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [block] when data absent ([Optional.dataPresented] == false)
|
||||
*/
|
||||
inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply {
|
||||
if (!dataPresented) { block() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||
*/
|
||||
inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run {
|
||||
if (!dataPresented) { @Suppress("UNCHECKED_CAST") block() } else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise
|
||||
*/
|
||||
fun <T> Optional<T>.dataOrNull() = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null
|
||||
|
||||
/**
|
||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or throw [throwable] otherwise
|
||||
*/
|
||||
fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable
|
||||
|
||||
|
||||
/**
|
||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
|
||||
*/
|
||||
inline fun <T> Optional<T>.dataOrElse(block: () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
|
||||
|
||||
/**
|
||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
|
||||
*/
|
||||
@Deprecated("dataOrElse now is inline", ReplaceWith("dataOrElse", "dev.inmo.micro_utils.common.dataOrElse"))
|
||||
suspend fun <T> Optional<T>.dataOrElseSuspendable(block: suspend () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
|
@@ -0,0 +1,19 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
fun <T : Comparable<T>> ClosedRange<T>.intersect(other: ClosedRange<T>): Pair<T, T>? = when {
|
||||
start == other.start && endInclusive == other.endInclusive -> start to endInclusive
|
||||
start > other.endInclusive || other.start > endInclusive -> null
|
||||
else -> maxOf(start, other.start) to minOf(endInclusive, other.endInclusive)
|
||||
}
|
||||
|
||||
fun IntRange.intersect(
|
||||
other: IntRange
|
||||
): IntRange? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let {
|
||||
it.first .. it.second
|
||||
}
|
||||
|
||||
fun LongRange.intersect(
|
||||
other: LongRange
|
||||
): LongRange? = (this as ClosedRange<Long>).intersect(other as ClosedRange<Long>) ?.let {
|
||||
it.first .. it.second
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
/**
|
||||
* Executes the given [action] until getting of successful result specified number of [times].
|
||||
*
|
||||
* A zero-based index of current iteration is passed as a parameter to [action].
|
||||
*/
|
||||
inline fun <R> repeatOnFailure(
|
||||
times: Int,
|
||||
onEachFailure: (Throwable) -> Unit = {},
|
||||
action: (Int) -> R
|
||||
): Optional<R> {
|
||||
repeat(times) {
|
||||
runCatching {
|
||||
action(it)
|
||||
}.onFailure(onEachFailure).onSuccess {
|
||||
return Optional.presented(it)
|
||||
}
|
||||
}
|
||||
return Optional.absent()
|
||||
}
|
@@ -11,7 +11,7 @@ class DiffUtilsTests {
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||
for ((i, v) in withIndex) {
|
||||
for ((i, _) in withIndex) {
|
||||
if (i + count > oldList.lastIndex) {
|
||||
continue
|
||||
}
|
||||
|
@@ -2,10 +2,12 @@ package dev.inmo.micro_utils.common
|
||||
|
||||
import org.khronos.webgl.ArrayBuffer
|
||||
import org.w3c.dom.ErrorEvent
|
||||
import org.w3c.files.File
|
||||
import org.w3c.files.FileReader
|
||||
import org.w3c.files.*
|
||||
import kotlin.js.Promise
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual typealias MPPFile = File
|
||||
|
||||
fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure ->
|
||||
@@ -21,12 +23,32 @@ fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure ->
|
||||
reader.readAsArrayBuffer(this)
|
||||
}
|
||||
|
||||
fun MPPFile.readBytes(): ByteArray {
|
||||
val reader = FileReaderSync()
|
||||
return reader.readAsArrayBuffer(this).toByteArray()
|
||||
}
|
||||
|
||||
private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await()
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filename: FileName
|
||||
get() = FileName(name)
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filesize: Long
|
||||
get() = size.toLong()
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
|
||||
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
||||
get() = ::readBytes
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
|
||||
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||
get() = ::dirtyReadBytes
|
||||
|
@@ -4,12 +4,29 @@ import dev.inmo.micro_utils.coroutines.doInIO
|
||||
import dev.inmo.micro_utils.coroutines.doOutsideOfCoroutine
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual typealias MPPFile = File
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filename: FileName
|
||||
get() = FileName(name)
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filesize: Long
|
||||
get() = length()
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
||||
get() = ::readBytes
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||
get() = {
|
||||
doInIO {
|
||||
|
@@ -0,0 +1,94 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
private sealed interface AccumulatorFlowStep
|
||||
private data class DataRetrievedAccumulatorFlowStep(val data: Any) : AccumulatorFlowStep
|
||||
private data class SubscribeAccumulatorFlowStep(val channel: Channel<Any>) : AccumulatorFlowStep
|
||||
private data class UnsubscribeAccumulatorFlowStep(val channel: Channel<Any>) : AccumulatorFlowStep
|
||||
|
||||
/**
|
||||
* This [Flow] will have behaviour very similar to [SharedFlow], but there are several differences:
|
||||
*
|
||||
* * All unhandled by [FlowCollector] data will not be removed from [AccumulatorFlow] and will be sent to new
|
||||
* [FlowCollector]s until anybody will handle it
|
||||
* * Here there are an [activeData] where data [T] will be stored until somebody will handle it
|
||||
*/
|
||||
class AccumulatorFlow<T>(
|
||||
sourceDataFlow: Flow<T>,
|
||||
scope: CoroutineScope
|
||||
) : AbstractFlow<T>() {
|
||||
private val subscope = scope.LinkedSupervisorScope()
|
||||
private val activeData = ArrayDeque<T>()
|
||||
private val dataMutex = Mutex()
|
||||
private val channelsForBroadcast = mutableListOf<Channel<Any>>()
|
||||
private val channelsMutex = Mutex()
|
||||
private val steps = subscope.actor<AccumulatorFlowStep> { step ->
|
||||
when (step) {
|
||||
is DataRetrievedAccumulatorFlowStep -> {
|
||||
if (activeData.first() === step.data) {
|
||||
dataMutex.withLock {
|
||||
activeData.removeFirst()
|
||||
}
|
||||
}
|
||||
}
|
||||
is SubscribeAccumulatorFlowStep -> channelsMutex.withLock {
|
||||
channelsForBroadcast.add(step.channel)
|
||||
dataMutex.withLock {
|
||||
val dataToSend = activeData.toList()
|
||||
safelyWithoutExceptions {
|
||||
dataToSend.forEach { step.channel.send(it as Any) }
|
||||
}
|
||||
}
|
||||
}
|
||||
is UnsubscribeAccumulatorFlowStep -> channelsMutex.withLock {
|
||||
channelsForBroadcast.remove(step.channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
private val subscriptionJob = sourceDataFlow.subscribeSafelyWithoutExceptions(subscope) {
|
||||
dataMutex.withLock {
|
||||
activeData.addLast(it)
|
||||
}
|
||||
channelsMutex.withLock {
|
||||
channelsForBroadcast.forEach { channel ->
|
||||
safelyWithResult {
|
||||
channel.send(it as Any)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun collectSafely(collector: FlowCollector<T>) {
|
||||
val channel = Channel<Any>(Channel.UNLIMITED, BufferOverflow.SUSPEND)
|
||||
steps.send(SubscribeAccumulatorFlowStep(channel))
|
||||
for (data in channel) {
|
||||
try {
|
||||
collector.emit(data as T)
|
||||
steps.send(DataRetrievedAccumulatorFlowStep(data))
|
||||
} finally {
|
||||
channel.cancel()
|
||||
steps.send(UnsubscribeAccumulatorFlowStep(channel))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates [AccumulatorFlow] using [this] as base [Flow]
|
||||
*/
|
||||
fun <T> Flow<T>.accumulatorFlow(scope: CoroutineScope): Flow<T> {
|
||||
return AccumulatorFlow(this, scope)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates [AccumulatorFlow] using [this] with [receiveAsFlow] to get
|
||||
*/
|
||||
fun <T> Channel<T>.accumulatorFlow(scope: CoroutineScope): Flow<T> {
|
||||
return receiveAsFlow().accumulatorFlow(scope)
|
||||
}
|
@@ -147,3 +147,10 @@ suspend inline fun <T> runCatchingSafelyWithoutExceptions(
|
||||
): Result<T?> = runCatching {
|
||||
safelyWithoutExceptions(onException, block)
|
||||
}
|
||||
|
||||
inline fun CoroutineScope(
|
||||
context: CoroutineContext,
|
||||
noinline defaultExceptionsHandler: ExceptionHandler<Unit>
|
||||
) = CoroutineScope(
|
||||
context + ContextSafelyExceptionHandler(defaultExceptionsHandler)
|
||||
)
|
||||
|
@@ -1,3 +1,6 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual fun SourceBytes.md5(): MD5 = CryptoJS.MD5(decodeToString())
|
||||
|
@@ -3,6 +3,9 @@ package dev.inmo.micro_utils.crypto
|
||||
import java.math.BigInteger
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual fun SourceBytes.md5(): MD5 = BigInteger(
|
||||
1,
|
||||
MessageDigest.getInstance("MD5").digest(this)
|
||||
|
@@ -13,10 +13,10 @@ repositories {
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
js(IR) {
|
||||
browser()
|
||||
nodejs()
|
||||
}
|
||||
// js(IR) {
|
||||
// browser()
|
||||
// nodejs()
|
||||
// }
|
||||
android {}
|
||||
|
||||
sourceSets {
|
||||
@@ -29,7 +29,7 @@ kotlin {
|
||||
it != project
|
||||
&& it.hasProperty("kotlin")
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") }
|
||||
// && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
|
||||
) {
|
||||
@@ -38,22 +38,22 @@ kotlin {
|
||||
}
|
||||
}
|
||||
}
|
||||
jsMain {
|
||||
dependencies {
|
||||
implementation kotlin('stdlib')
|
||||
// jsMain {
|
||||
// dependencies {
|
||||
// implementation kotlin('stdlib')
|
||||
|
||||
project.parent.subprojects.forEach {
|
||||
if (
|
||||
it != project
|
||||
&& it.hasProperty("kotlin")
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") }
|
||||
) {
|
||||
api it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// project.parent.subprojects.forEach {
|
||||
// if (
|
||||
// it != project
|
||||
// && it.hasProperty("kotlin")
|
||||
// && it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||
// && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
|
||||
// ) {
|
||||
// api it
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
jvmMain {
|
||||
dependencies {
|
||||
implementation kotlin('stdlib')
|
||||
@@ -116,9 +116,9 @@ tasks.dokkaHtml {
|
||||
sourceRoots.setFrom(findSourcesWithName("commonMain"))
|
||||
}
|
||||
|
||||
named("jsMain") {
|
||||
sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain"))
|
||||
}
|
||||
// named("jsMain") {
|
||||
// sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain"))
|
||||
// }
|
||||
|
||||
named("jvmMain") {
|
||||
sourceRoots.setFrom(findSourcesWithName("jvmMain", "commonMain"))
|
||||
|
@@ -10,6 +10,7 @@ kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.common")
|
||||
api project(":micro_utils.coroutines")
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,81 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Define checkable holder which can be used to precheck that this handler may handle incoming [State]
|
||||
*/
|
||||
interface CheckableHandlerHolder<I : State, O : State> : StatesHandler<I, O> {
|
||||
suspend fun checkHandleable(state: O): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Default realization of [StatesHandler]. It will incapsulate checking of [State] type in [checkHandleable] and class
|
||||
* casting in [handleState]
|
||||
*/
|
||||
class CustomizableHandlerHolder<I : O, O : State>(
|
||||
private val delegateTo: StatesHandler<I, O>,
|
||||
private val filter: suspend (state: O) -> Boolean
|
||||
) : CheckableHandlerHolder<I, O> {
|
||||
/**
|
||||
* Checks that [state] can be handled by [delegateTo]. Under the hood it will check exact equality of [state]
|
||||
* [KClass] and use [KClass.isInstance] of [inputKlass] if [strict] == false
|
||||
*/
|
||||
override suspend fun checkHandleable(state: O) = filter(state)
|
||||
|
||||
/**
|
||||
* Calls [delegateTo] method [StatesHandler.handleState] with [state] casted to [I]. Use [checkHandleable]
|
||||
* to be sure that this [StatesHandlerHolder] will be able to handle [state]
|
||||
*/
|
||||
override suspend fun StatesMachine<in O>.handleState(state: I): O? {
|
||||
return delegateTo.run { handleState(state) }
|
||||
}
|
||||
}
|
||||
|
||||
fun <I : O, O : State> CheckableHandlerHolder(
|
||||
inputKlass: KClass<I>,
|
||||
strict: Boolean = false,
|
||||
delegateTo: StatesHandler<I, O>
|
||||
) = CustomizableHandlerHolder(
|
||||
StatesHandler<O, O> {
|
||||
delegateTo.run { handleState(it as I) }
|
||||
},
|
||||
if (strict) {
|
||||
{ it::class == inputKlass }
|
||||
} else {
|
||||
{ inputKlass.isInstance(it) }
|
||||
}
|
||||
)
|
||||
|
||||
@Deprecated("Renamed", ReplaceWith("CheckableHandlerHolder"))
|
||||
fun <I : O, O : State> StateHandlerHolder(
|
||||
inputKlass: KClass<I>,
|
||||
strict: Boolean = false,
|
||||
delegateTo: StatesHandler<I, O>
|
||||
) = CheckableHandlerHolder(inputKlass, strict, delegateTo)
|
||||
|
||||
inline fun <reified I : O, O : State> CheckableHandlerHolder(
|
||||
strict: Boolean = false,
|
||||
delegateTo: StatesHandler<I, O>
|
||||
) = CheckableHandlerHolder(I::class, strict, delegateTo)
|
||||
|
||||
@Deprecated("Renamed", ReplaceWith("CheckableHandlerHolder"))
|
||||
inline fun <reified I : O, O : State> StateHandlerHolder(
|
||||
strict: Boolean = false,
|
||||
delegateTo: StatesHandler<I, O>
|
||||
) = CheckableHandlerHolder(strict, delegateTo)
|
||||
|
||||
inline fun <reified I : O, O: State> StatesHandler<I, O>.holder(
|
||||
strict: Boolean = true
|
||||
) = CheckableHandlerHolder<I, O>(
|
||||
I::class,
|
||||
strict,
|
||||
this
|
||||
)
|
||||
|
||||
inline fun <I : O, O: State> StatesHandler<I, O>.holder(
|
||||
noinline filter: suspend (state: State) -> Boolean
|
||||
) = CustomizableHandlerHolder<O, O>(
|
||||
{ this@holder.run { handleState(it as I) } },
|
||||
filter
|
||||
)
|
@@ -1,15 +0,0 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class StateHandlerHolder<I : State>(
|
||||
private val inputKlass: KClass<I>,
|
||||
private val strict: Boolean = false,
|
||||
private val delegateTo: StatesHandler<I>
|
||||
) : StatesHandler<State> {
|
||||
fun checkHandleable(state: State) = state::class == inputKlass || (!strict && inputKlass.isInstance(state))
|
||||
|
||||
override suspend fun StatesMachine.handleState(state: State): State? {
|
||||
return delegateTo.run { handleState(state as I) }
|
||||
}
|
||||
}
|
@@ -1,5 +1,12 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
fun interface StatesHandler<I : State> {
|
||||
suspend fun StatesMachine.handleState(state: I): State?
|
||||
/**
|
||||
* Default realization of states handler
|
||||
*/
|
||||
fun interface StatesHandler<I : State, O: State> {
|
||||
/**
|
||||
* Main handling of [state]. In case when this [state] leads to another [State] and [handleState] returns not null
|
||||
* [State] it is assumed that chain is not completed.
|
||||
*/
|
||||
suspend fun StatesMachine<in O>.handleState(state: I): O?
|
||||
}
|
||||
|
@@ -1,46 +1,120 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
import dev.inmo.micro_utils.common.Optional
|
||||
import dev.inmo.micro_utils.common.onPresented
|
||||
import dev.inmo.micro_utils.coroutines.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
private suspend fun <I : State> StatesMachine.launchStateHandling(
|
||||
state: State,
|
||||
handlers: List<StateHandlerHolder<out I>>
|
||||
): State? {
|
||||
return handlers.firstOrNull { it.checkHandleable(state) } ?.run {
|
||||
handleState(state)
|
||||
/**
|
||||
* Default [StatesMachine] may [startChain] and use inside logic for handling [State]s. By default you may use
|
||||
* [DefaultStatesMachine] or build it with [dev.inmo.micro_utils.fsm.common.dsl.buildFSM]. Implementers MUST NOT start
|
||||
* handling until [start] method will be called
|
||||
*/
|
||||
interface StatesMachine<T : State> : StatesHandler<T, T> {
|
||||
suspend fun launchStateHandling(
|
||||
state: T,
|
||||
handlers: List<CheckableHandlerHolder<in T, T>>
|
||||
): T? {
|
||||
return handlers.firstOrNull { it.checkHandleable(state) } ?.run {
|
||||
handleState(state)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts handling of [State]s
|
||||
*/
|
||||
fun start(scope: CoroutineScope): Job
|
||||
|
||||
/**
|
||||
* Start chain of [State]s witn [state]
|
||||
*/
|
||||
suspend fun startChain(state: T)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Creates [DefaultStatesMachine]
|
||||
*/
|
||||
operator fun <T: State> invoke(
|
||||
statesManager: StatesManager<T>,
|
||||
handlers: List<CheckableHandlerHolder<in T, T>>
|
||||
) = DefaultStatesMachine(statesManager, handlers)
|
||||
}
|
||||
}
|
||||
|
||||
class StatesMachine (
|
||||
private val statesManager: StatesManager,
|
||||
private val handlers: List<StateHandlerHolder<*>>
|
||||
) : StatesHandler<State> {
|
||||
override suspend fun StatesMachine.handleState(state: State): State? = launchStateHandling(state, handlers)
|
||||
/**
|
||||
* Default realization of [StatesMachine]. It uses [statesManager] for incapsulation of [State]s storing and contexts
|
||||
* resolving, and uses [launchStateHandling] for [State] handling.
|
||||
*
|
||||
* This class suppose to be extended in case you wish some custom behaviour inside of [launchStateHandling], for example
|
||||
*/
|
||||
open class DefaultStatesMachine <T: State>(
|
||||
protected val statesManager: StatesManager<T>,
|
||||
protected val handlers: List<CheckableHandlerHolder<in T, T>>,
|
||||
) : StatesMachine<T> {
|
||||
/**
|
||||
* Will call [launchStateHandling] for state handling
|
||||
*/
|
||||
override suspend fun StatesMachine<in T>.handleState(state: T): T? = launchStateHandling(state, handlers)
|
||||
|
||||
fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
|
||||
val statePerformer: suspend (State) -> Unit = { state: State ->
|
||||
val newState = launchStateHandling(state, handlers)
|
||||
if (newState != null) {
|
||||
statesManager.update(state, newState)
|
||||
} else {
|
||||
statesManager.endChain(state)
|
||||
/**
|
||||
* This
|
||||
*/
|
||||
protected val statesJobs = mutableMapOf<T, Job>()
|
||||
protected val statesJobsMutex = Mutex()
|
||||
|
||||
protected open suspend fun performUpdate(state: T) {
|
||||
val newState = launchStateHandling(state, handlers)
|
||||
if (newState != null) {
|
||||
statesManager.update(state, newState)
|
||||
} else {
|
||||
statesManager.endChain(state)
|
||||
}
|
||||
}
|
||||
|
||||
open suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
|
||||
statesJobsMutex.withLock {
|
||||
statesJobs[actualState] ?.cancel()
|
||||
statesJobs[actualState] = scope.launch {
|
||||
performUpdate(actualState)
|
||||
}.also { job ->
|
||||
job.invokeOnCompletion { _ ->
|
||||
scope.launch {
|
||||
statesJobsMutex.withLock {
|
||||
if (statesJobs[actualState] == job) {
|
||||
statesJobs.remove(actualState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch handling of states. On [statesManager] [StatesManager.onStartChain],
|
||||
* [statesManager] [StatesManager.onChainStateUpdated] will be called lambda with performing of state. If
|
||||
* [launchStateHandling] will returns some [State] then [statesManager] [StatesManager.update] will be used, otherwise
|
||||
* [StatesManager.endChain].
|
||||
*/
|
||||
override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
|
||||
statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) {
|
||||
launch { statePerformer(it) }
|
||||
launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
|
||||
}
|
||||
statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) {
|
||||
launch { statePerformer(it.second) }
|
||||
launch { performStateUpdate(Optional.presented(it.first), it.second, scope.LinkedSupervisorScope()) }
|
||||
}
|
||||
|
||||
statesManager.getActiveStates().forEach {
|
||||
launch { statePerformer(it) }
|
||||
launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startChain(state: State) {
|
||||
/**
|
||||
* Just calls [StatesManager.startChain] of [statesManager]
|
||||
*/
|
||||
override suspend fun startChain(state: T) {
|
||||
statesManager.startChain(state)
|
||||
}
|
||||
}
|
||||
|
@@ -1,92 +1,30 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface StatesManager {
|
||||
val onChainStateUpdated: Flow<Pair<State, State>>
|
||||
val onStartChain: Flow<State>
|
||||
val onEndChain: Flow<State>
|
||||
interface StatesManager<T : State> {
|
||||
val onChainStateUpdated: Flow<Pair<T, T>>
|
||||
val onStartChain: Flow<T>
|
||||
val onEndChain: Flow<T>
|
||||
|
||||
|
||||
/**
|
||||
* Must set current set using [State.context]
|
||||
*/
|
||||
suspend fun update(old: State, new: State)
|
||||
suspend fun update(old: T, new: T)
|
||||
|
||||
/**
|
||||
* Starts chain with [state] as first [State]. May returns false in case of [State.context] of [state] is already
|
||||
* busy by the other [State]
|
||||
*/
|
||||
suspend fun startChain(state: State)
|
||||
suspend fun startChain(state: T)
|
||||
|
||||
/**
|
||||
* Ends chain with context from [state]. In case when [State.context] of [state] is absent, [state] should be just
|
||||
* ignored
|
||||
*/
|
||||
suspend fun endChain(state: State)
|
||||
suspend fun endChain(state: T)
|
||||
|
||||
suspend fun getActiveStates(): List<State>
|
||||
suspend fun getActiveStates(): List<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
|
||||
* key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
|
||||
* new state by using [endChain] with that state
|
||||
*/
|
||||
class InMemoryStatesManager(
|
||||
private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true }
|
||||
) : StatesManager {
|
||||
private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0)
|
||||
override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow()
|
||||
private val _onStartChain = MutableSharedFlow<State>(0)
|
||||
override val onStartChain: Flow<State> = _onStartChain.asSharedFlow()
|
||||
private val _onEndChain = MutableSharedFlow<State>(0)
|
||||
override val onEndChain: Flow<State> = _onEndChain.asSharedFlow()
|
||||
|
||||
private val contextsToStates = mutableMapOf<Any, State>()
|
||||
private val mapMutex = Mutex()
|
||||
|
||||
override suspend fun update(old: State, new: State) = mapMutex.withLock {
|
||||
when {
|
||||
contextsToStates[old.context] != old -> return@withLock
|
||||
old.context == new.context || !contextsToStates.containsKey(new.context) -> {
|
||||
contextsToStates[old.context] = new
|
||||
_onChainStateUpdated.emit(old to new)
|
||||
}
|
||||
else -> {
|
||||
val stateOnNewOneContext = contextsToStates.getValue(new.context)
|
||||
if (onContextsConflictResolver(old, new, stateOnNewOneContext)) {
|
||||
endChainWithoutLock(stateOnNewOneContext)
|
||||
contextsToStates.remove(old.context)
|
||||
contextsToStates[new.context] = new
|
||||
_onChainStateUpdated.emit(old to new)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun startChain(state: State) = mapMutex.withLock {
|
||||
if (!contextsToStates.containsKey(state.context)) {
|
||||
contextsToStates[state.context] = state
|
||||
_onStartChain.emit(state)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun endChainWithoutLock(state: State) {
|
||||
if (contextsToStates[state.context] == state) {
|
||||
contextsToStates.remove(state.context)
|
||||
_onEndChain.emit(state)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun endChain(state: State) {
|
||||
mapMutex.withLock {
|
||||
endChainWithoutLock(state)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getActiveStates(): List<State> = contextsToStates.values.toList()
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,60 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
import dev.inmo.micro_utils.common.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/**
|
||||
* This extender of [StatesMachine] interface declare one new function [updateChain]. Realizations of this interface
|
||||
* must be able to perform update of chain in internal [StatesManager]
|
||||
*/
|
||||
interface UpdatableStatesMachine<T : State> : StatesMachine<T> {
|
||||
/**
|
||||
* Update chain with current state equal to [currentState] with [newState]. Behaviour of this update preforming
|
||||
* in cases when [currentState] does not exist in [StatesManager] must be declared inside of realization of
|
||||
* [StatesManager.update] function
|
||||
*/
|
||||
suspend fun updateChain(currentState: T, newState: T)
|
||||
}
|
||||
|
||||
open class DefaultUpdatableStatesMachine<T : State>(
|
||||
statesManager: StatesManager<T>,
|
||||
handlers: List<CheckableHandlerHolder<in T, T>>,
|
||||
) : DefaultStatesMachine<T>(
|
||||
statesManager,
|
||||
handlers
|
||||
), UpdatableStatesMachine<T> {
|
||||
protected val jobsStates = mutableMapOf<Job, T>()
|
||||
|
||||
override suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
|
||||
statesJobsMutex.withLock {
|
||||
if (compare(previousState, actualState)) {
|
||||
statesJobs[actualState] ?.cancel()
|
||||
}
|
||||
val job = previousState.mapOnPresented {
|
||||
statesJobs.remove(it)
|
||||
} ?.takeIf { it.isActive } ?: scope.launch {
|
||||
performUpdate(actualState)
|
||||
}.also { job ->
|
||||
job.invokeOnCompletion { _ ->
|
||||
scope.launch {
|
||||
statesJobsMutex.withLock {
|
||||
statesJobs.remove(
|
||||
jobsStates[job] ?: return@withLock
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
jobsStates.remove(job)
|
||||
statesJobs[actualState] = job
|
||||
jobsStates[job] = actualState
|
||||
}
|
||||
}
|
||||
|
||||
protected open suspend fun compare(previous: Optional<T>, new: T): Boolean = previous.dataOrNull() != new
|
||||
|
||||
override suspend fun updateChain(currentState: T, newState: T) {
|
||||
statesManager.update(currentState, newState)
|
||||
}
|
||||
}
|
@@ -1,35 +1,61 @@
|
||||
package dev.inmo.micro_utils.fsm.common.dsl
|
||||
|
||||
import dev.inmo.micro_utils.fsm.common.*
|
||||
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager
|
||||
import dev.inmo.micro_utils.fsm.common.managers.InMemoryDefaultStatesManagerRepo
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class FSMBuilder(
|
||||
var statesManager: StatesManager = InMemoryStatesManager()
|
||||
class FSMBuilder<T : State>(
|
||||
var statesManager: StatesManager<T> = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()),
|
||||
val fsmBuilder: (statesManager: StatesManager<T>, states: List<CheckableHandlerHolder<T, T>>) -> StatesMachine<T> = { statesManager, states ->
|
||||
StatesMachine(
|
||||
statesManager,
|
||||
states
|
||||
)
|
||||
},
|
||||
var defaultStateHandler: StatesHandler<T, T>? = StatesHandler { null }
|
||||
) {
|
||||
private var states = mutableListOf<StateHandlerHolder<*>>()
|
||||
private var states = mutableListOf<CheckableHandlerHolder<T, T>>()
|
||||
|
||||
fun <I : State> add(kClass: KClass<I>, handler: StatesHandler<I>) {
|
||||
states.add(StateHandlerHolder(kClass, false, handler))
|
||||
fun add(handler: CheckableHandlerHolder<T, T>) {
|
||||
states.add(handler)
|
||||
}
|
||||
|
||||
fun <I : State> addStrict(kClass: KClass<I>, handler: StatesHandler<I>) {
|
||||
states.add(StateHandlerHolder(kClass, true, handler))
|
||||
fun <I : T> add(kClass: KClass<I>, handler: StatesHandler<I, T>) {
|
||||
add(CheckableHandlerHolder(kClass, false, handler))
|
||||
}
|
||||
|
||||
fun build() = StatesMachine(
|
||||
fun <I : T> add(filter: suspend (state: State) -> Boolean, handler: StatesHandler<I, T>) {
|
||||
add(handler.holder(filter))
|
||||
}
|
||||
|
||||
fun <I : T> addStrict(kClass: KClass<I>, handler: StatesHandler<I, T>) {
|
||||
states.add(CheckableHandlerHolder(kClass, true, handler))
|
||||
}
|
||||
|
||||
inline fun <reified I : T> onStateOrSubstate(handler: StatesHandler<I, T>) {
|
||||
add(I::class, handler)
|
||||
}
|
||||
|
||||
inline fun <reified I : T> strictlyOn(handler: StatesHandler<I, T>) {
|
||||
addStrict(I::class, handler)
|
||||
}
|
||||
|
||||
inline fun <reified I : T> doWhen(
|
||||
noinline filter: suspend (state: State) -> Boolean,
|
||||
handler: StatesHandler<I, T>
|
||||
) {
|
||||
add(filter, handler)
|
||||
}
|
||||
|
||||
fun build() = fsmBuilder(
|
||||
statesManager,
|
||||
states.toList()
|
||||
states.toList().let { list ->
|
||||
defaultStateHandler ?.let { list + it.holder { true } } ?: list
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
inline fun <reified I : State> FSMBuilder.onStateOrSubstate(handler: StatesHandler<I>) {
|
||||
add(I::class, handler)
|
||||
}
|
||||
|
||||
inline fun <reified I : State> FSMBuilder.strictlyOn(handler: StatesHandler<I>) {
|
||||
addStrict(I::class, handler)
|
||||
}
|
||||
|
||||
fun buildFSM(
|
||||
block: FSMBuilder.() -> Unit
|
||||
): StatesMachine = FSMBuilder().apply(block).build()
|
||||
fun <T : State> buildFSM(
|
||||
block: FSMBuilder<T>.() -> Unit
|
||||
): StatesMachine<T> = FSMBuilder<T>().apply(block).build()
|
||||
|
@@ -0,0 +1,101 @@
|
||||
package dev.inmo.micro_utils.fsm.common.managers
|
||||
|
||||
import dev.inmo.micro_utils.fsm.common.State
|
||||
import dev.inmo.micro_utils.fsm.common.StatesManager
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/**
|
||||
* Implement this repo if you want to use some custom repo for [DefaultStatesManager]
|
||||
*/
|
||||
interface DefaultStatesManagerRepo<T : State> {
|
||||
/**
|
||||
* Must save [state] as current state of chain with [State.context] of [state]
|
||||
*/
|
||||
suspend fun set(state: T)
|
||||
/**
|
||||
* Remove exactly [state]. In case if internally [State.context] is busy with different [State], that [State] should
|
||||
* NOT be removed
|
||||
*/
|
||||
suspend fun removeState(state: T)
|
||||
/**
|
||||
* @return Current list of available and saved states
|
||||
*/
|
||||
suspend fun getStates(): List<T>
|
||||
|
||||
/**
|
||||
* @return Current state by [context]
|
||||
*/
|
||||
suspend fun getContextState(context: Any): T?
|
||||
|
||||
/**
|
||||
* @return Current state by [context]
|
||||
*/
|
||||
suspend fun contains(context: Any): Boolean = getContextState(context) != null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param repo This repo will be used as repository for storing states. All operations with this repo will happen BEFORE
|
||||
* any event will be sent to [onChainStateUpdated], [onStartChain] or [onEndChain]. By default will be used
|
||||
* [InMemoryDefaultStatesManagerRepo] or you may create custom [DefaultStatesManagerRepo] and pass as [repo] parameter
|
||||
* @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
|
||||
* key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
|
||||
* new state by using [endChain] with that state
|
||||
*/
|
||||
class DefaultStatesManager<T : State>(
|
||||
private val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
|
||||
private val onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
|
||||
) : StatesManager<T> {
|
||||
private val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
|
||||
override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow()
|
||||
private val _onStartChain = MutableSharedFlow<T>(0)
|
||||
override val onStartChain: Flow<T> = _onStartChain.asSharedFlow()
|
||||
private val _onEndChain = MutableSharedFlow<T>(0)
|
||||
override val onEndChain: Flow<T> = _onEndChain.asSharedFlow()
|
||||
|
||||
private val mapMutex = Mutex()
|
||||
|
||||
override suspend fun update(old: T, new: T) = mapMutex.withLock {
|
||||
val stateByOldContext: T? = repo.getContextState(old.context)
|
||||
when {
|
||||
stateByOldContext != old -> return@withLock
|
||||
stateByOldContext == null || old.context == new.context -> {
|
||||
repo.set(new)
|
||||
_onChainStateUpdated.emit(old to new)
|
||||
}
|
||||
else -> {
|
||||
val stateOnNewOneContext = repo.getContextState(new.context)
|
||||
if (stateOnNewOneContext == null || onContextsConflictResolver(old, new, stateOnNewOneContext)) {
|
||||
stateOnNewOneContext ?.let { endChainWithoutLock(it) }
|
||||
repo.removeState(old)
|
||||
repo.set(new)
|
||||
_onChainStateUpdated.emit(old to new)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun startChain(state: T) = mapMutex.withLock {
|
||||
if (!repo.contains(state.context)) {
|
||||
repo.set(state)
|
||||
_onStartChain.emit(state)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun endChainWithoutLock(state: T) {
|
||||
if (repo.getContextState(state.context) == state) {
|
||||
repo.removeState(state)
|
||||
_onEndChain.emit(state)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun endChain(state: T) {
|
||||
mapMutex.withLock {
|
||||
endChainWithoutLock(state)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getActiveStates(): List<T> = repo.getStates()
|
||||
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package dev.inmo.micro_utils.fsm.common.managers
|
||||
|
||||
import dev.inmo.micro_utils.fsm.common.State
|
||||
|
||||
/**
|
||||
* Simple [DefaultStatesManagerRepo] for [DefaultStatesManager] which will store data in [map] and use primitive
|
||||
* functionality
|
||||
*/
|
||||
class InMemoryDefaultStatesManagerRepo<T : State>(
|
||||
private val map: MutableMap<Any, T> = mutableMapOf()
|
||||
) : DefaultStatesManagerRepo<T> {
|
||||
override suspend fun set(state: T) {
|
||||
map[state.context] = state
|
||||
}
|
||||
|
||||
override suspend fun removeState(state: T) {
|
||||
map.remove(state.context)
|
||||
}
|
||||
|
||||
override suspend fun getStates(): List<T> = map.values.toList()
|
||||
|
||||
override suspend fun getContextState(context: Any): T? = map[context]
|
||||
|
||||
override suspend fun contains(context: Any): Boolean = map.contains(context)
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
package dev.inmo.micro_utils.fsm.common.managers
|
||||
|
||||
import dev.inmo.micro_utils.fsm.common.State
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/**
|
||||
* Creates [DefaultStatesManager] with [InMemoryDefaultStatesManagerRepo]
|
||||
*
|
||||
* @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
|
||||
* key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
|
||||
* new state by using [endChain] with that state
|
||||
*/
|
||||
@Deprecated("Use DefaultStatesManager instead", ReplaceWith("DefaultStatesManager"))
|
||||
fun <T: State> InMemoryStatesManager(
|
||||
onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
|
||||
) = DefaultStatesManager(onContextsConflictResolver = onContextsConflictResolver)
|
@@ -1,6 +1,7 @@
|
||||
import dev.inmo.micro_utils.fsm.common.*
|
||||
import dev.inmo.micro_utils.fsm.common.dsl.buildFSM
|
||||
import dev.inmo.micro_utils.fsm.common.dsl.strictlyOn
|
||||
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager
|
||||
import dev.inmo.micro_utils.fsm.common.managers.InMemoryStatesManager
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
sealed interface TrafficLightState : State {
|
||||
@@ -25,9 +26,9 @@ class PlayableMain {
|
||||
}
|
||||
}
|
||||
|
||||
val statesManager = InMemoryStatesManager()
|
||||
val statesManager = DefaultStatesManager<TrafficLightState>()
|
||||
|
||||
val machine = buildFSM {
|
||||
val machine = buildFSM<TrafficLightState> {
|
||||
strictlyOn<GreenCommon> {
|
||||
delay(1000L)
|
||||
YellowCommon(it.context).also(::println)
|
||||
|
@@ -0,0 +1,25 @@
|
||||
package dev.inmo.micro_utils.fsm.repos.common
|
||||
|
||||
import dev.inmo.micro_utils.fsm.common.State
|
||||
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import dev.inmo.micro_utils.repos.pagination.getAll
|
||||
|
||||
class KeyValueBasedDefaultStatesManagerRepo<T : State>(
|
||||
private val keyValueRepo: KeyValueRepo<Any, T>
|
||||
) : DefaultStatesManagerRepo<T> {
|
||||
override suspend fun set(state: T) {
|
||||
keyValueRepo.set(state.context, state)
|
||||
}
|
||||
|
||||
override suspend fun removeState(state: T) {
|
||||
if (keyValueRepo.get(state.context) == state) {
|
||||
keyValueRepo.unset(state.context)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getStates(): List<T> = keyValueRepo.getAll { keys(it) }.map { it.second }
|
||||
override suspend fun getContextState(context: Any): T? = keyValueRepo.get(context)
|
||||
|
||||
override suspend fun contains(context: Any): Boolean = keyValueRepo.contains(context)
|
||||
}
|
@@ -1,83 +0,0 @@
|
||||
package dev.inmo.micro_utils.fsm.repos.common
|
||||
|
||||
import dev.inmo.micro_utils.fsm.common.State
|
||||
import dev.inmo.micro_utils.fsm.common.StatesManager
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import dev.inmo.micro_utils.repos.mappers.withMapper
|
||||
import dev.inmo.micro_utils.repos.pagination.getAll
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class KeyValueBasedStatesManager(
|
||||
private val keyValueRepo: KeyValueRepo<Any, State>,
|
||||
private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true }
|
||||
) : StatesManager {
|
||||
private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0)
|
||||
override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow()
|
||||
private val _onEndChain = MutableSharedFlow<State>(0)
|
||||
override val onEndChain: Flow<State> = _onEndChain.asSharedFlow()
|
||||
|
||||
override val onStartChain: Flow<State> = keyValueRepo.onNewValue.map { it.second }
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
override suspend fun update(old: State, new: State) {
|
||||
mutex.withLock {
|
||||
when {
|
||||
keyValueRepo.get(old.context) != old -> return@withLock
|
||||
old.context == new.context || !keyValueRepo.contains(new.context) -> {
|
||||
keyValueRepo.set(old.context, new)
|
||||
_onChainStateUpdated.emit(old to new)
|
||||
}
|
||||
else -> {
|
||||
val stateOnNewOneContext = keyValueRepo.get(new.context)!!
|
||||
if (onContextsConflictResolver(old, new, stateOnNewOneContext)) {
|
||||
endChainWithoutLock(stateOnNewOneContext)
|
||||
keyValueRepo.unset(old.context)
|
||||
keyValueRepo.set(new.context, new)
|
||||
_onChainStateUpdated.emit(old to new)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun startChain(state: State) {
|
||||
if (!keyValueRepo.contains(state.context)) {
|
||||
keyValueRepo.set(state.context, state)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun endChainWithoutLock(state: State) {
|
||||
if (keyValueRepo.get(state.context) == state) {
|
||||
keyValueRepo.unset(state.context)
|
||||
_onEndChain.emit(state)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun endChain(state: State) {
|
||||
mutex.withLock { endChainWithoutLock(state) }
|
||||
}
|
||||
|
||||
override suspend fun getActiveStates(): List<State> {
|
||||
return keyValueRepo.getAll { keys(it) }.map { it.second }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
inline fun <reified TargetContextType, reified TargetStateType> createStatesManager(
|
||||
targetKeyValueRepo: KeyValueRepo<TargetContextType, TargetStateType>,
|
||||
noinline contextToOutTransformer: suspend Any.() -> TargetContextType,
|
||||
noinline stateToOutTransformer: suspend State.() -> TargetStateType,
|
||||
noinline outToContextTransformer: suspend TargetContextType.() -> Any,
|
||||
noinline outToStateTransformer: suspend TargetStateType.() -> State,
|
||||
) = KeyValueBasedStatesManager(
|
||||
targetKeyValueRepo.withMapper<Any, State, TargetContextType, TargetStateType>(
|
||||
contextToOutTransformer,
|
||||
stateToOutTransformer,
|
||||
outToContextTransformer,
|
||||
outToStateTransformer
|
||||
)
|
||||
)
|
@@ -7,29 +7,29 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
org.gradle.jvmargs=-Xmx2g
|
||||
|
||||
kotlin_version=1.5.21
|
||||
kotlin_coroutines_version=1.5.1
|
||||
kotlin_serialisation_core_version=1.2.2
|
||||
kotlin_exposed_version=0.33.1
|
||||
kotlin_version=1.6.10
|
||||
kotlin_coroutines_version=1.6.0
|
||||
kotlin_serialisation_core_version=1.3.2
|
||||
kotlin_exposed_version=0.37.2
|
||||
|
||||
ktor_version=1.6.2
|
||||
ktor_version=1.6.7
|
||||
|
||||
klockVersion=2.3.1
|
||||
klockVersion=2.4.12
|
||||
|
||||
github_release_plugin_version=2.2.12
|
||||
|
||||
uuidVersion=0.3.0
|
||||
uuidVersion=0.3.1
|
||||
|
||||
# ANDROID
|
||||
|
||||
core_ktx_version=1.6.0
|
||||
core_ktx_version=1.7.0
|
||||
androidx_recycler_version=1.2.1
|
||||
appcompat_version=1.3.0
|
||||
appcompat_version=1.4.0
|
||||
|
||||
android_minSdkVersion=19
|
||||
android_compileSdkVersion=30
|
||||
android_buildToolsVersion=30.0.3
|
||||
dexcount_version=2.1.0-RC01
|
||||
android_compileSdkVersion=32
|
||||
android_buildToolsVersion=32.0.0
|
||||
dexcount_version=3.0.1
|
||||
junit_version=4.12
|
||||
test_ext_junit_version=1.1.2
|
||||
espresso_core=3.3.0
|
||||
@@ -40,10 +40,10 @@ crypto_js_version=4.1.1
|
||||
|
||||
# Dokka
|
||||
|
||||
dokka_version=1.5.0
|
||||
dokka_version=1.6.0
|
||||
|
||||
# Project data
|
||||
|
||||
group=dev.inmo
|
||||
version=0.5.19
|
||||
android_code_version=60
|
||||
version=0.9.2
|
||||
android_code_version=92
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@@ -0,0 +1,6 @@
|
||||
package dev.inmo.micro_utils.ktor.client
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import io.ktor.client.request.forms.InputProvider
|
||||
|
||||
expect suspend fun MPPFile.inputProvider(): InputProvider
|
@@ -1,16 +1,20 @@
|
||||
package dev.inmo.micro_utils.ktor.client
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.common.filename
|
||||
import dev.inmo.micro_utils.ktor.common.*
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.request.forms.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.utils.io.core.ByteReadPacket
|
||||
import kotlinx.serialization.*
|
||||
|
||||
typealias BodyPair<T> = Pair<SerializationStrategy<T>, T>
|
||||
|
||||
class UnifiedRequester(
|
||||
private val client: HttpClient = HttpClient(),
|
||||
private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
val client: HttpClient = HttpClient(),
|
||||
val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
) {
|
||||
suspend fun <ResultType> uniget(
|
||||
url: String,
|
||||
@@ -31,6 +35,54 @@ class UnifiedRequester(
|
||||
resultDeserializer: DeserializationStrategy<ResultType>
|
||||
) = client.unipost(url, bodyInfo, resultDeserializer, serialFormat)
|
||||
|
||||
suspend fun <ResultType> unimultipart(
|
||||
url: String,
|
||||
filename: String,
|
||||
inputProvider: InputProvider,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
mimetype: String = "*/*",
|
||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
): ResultType = client.unimultipart(url, filename, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat)
|
||||
|
||||
suspend fun <BodyType, ResultType> unimultipart(
|
||||
url: String,
|
||||
filename: String,
|
||||
inputProvider: InputProvider,
|
||||
otherData: BodyPair<BodyType>,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
mimetype: String = "*/*",
|
||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
): ResultType = client.unimultipart(url, filename, otherData, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat)
|
||||
|
||||
suspend fun <ResultType> unimultipart(
|
||||
url: String,
|
||||
mppFile: MPPFile,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
mimetype: String = "*/*",
|
||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {}
|
||||
): ResultType = client.unimultipart(
|
||||
url, mppFile, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat
|
||||
)
|
||||
|
||||
suspend fun <BodyType, ResultType> unimultipart(
|
||||
url: String,
|
||||
mppFile: MPPFile,
|
||||
otherData: BodyPair<BodyType>,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
mimetype: String = "*/*",
|
||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {}
|
||||
): ResultType = client.unimultipart(
|
||||
url, mppFile, otherData, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat
|
||||
)
|
||||
|
||||
fun <T> createStandardWebsocketFlow(
|
||||
url: String,
|
||||
checkReconnection: (Throwable?) -> Boolean = { true },
|
||||
@@ -69,3 +121,124 @@ suspend fun <BodyType, ResultType> HttpClient.unipost(
|
||||
}.let {
|
||||
serialFormat.decodeDefault(resultDeserializer, it)
|
||||
}
|
||||
|
||||
suspend fun <ResultType> HttpClient.unimultipart(
|
||||
url: String,
|
||||
filename: String,
|
||||
inputProvider: InputProvider,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
mimetype: String = "*/*",
|
||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
): ResultType = submitFormWithBinaryData<StandardKtorSerialInputData>(
|
||||
url,
|
||||
formData = formData {
|
||||
append(
|
||||
"bytes",
|
||||
inputProvider,
|
||||
Headers.build {
|
||||
append(HttpHeaders.ContentType, mimetype)
|
||||
append(HttpHeaders.ContentDisposition, "filename=\"$filename\"")
|
||||
dataHeadersBuilder()
|
||||
}
|
||||
)
|
||||
additionalParametersBuilder()
|
||||
}
|
||||
) {
|
||||
requestBuilder()
|
||||
}.let { serialFormat.decodeDefault(resultDeserializer, it) }
|
||||
|
||||
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
|
||||
url: String,
|
||||
filename: String,
|
||||
otherData: BodyPair<BodyType>,
|
||||
inputProvider: InputProvider,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
mimetype: String = "*/*",
|
||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
): ResultType = unimultipart(
|
||||
url,
|
||||
filename,
|
||||
inputProvider,
|
||||
resultDeserializer,
|
||||
mimetype,
|
||||
additionalParametersBuilder = {
|
||||
val serialized = serialFormat.encodeDefault(otherData.first, otherData.second)
|
||||
append(
|
||||
"data",
|
||||
InputProvider(serialized.size.toLong()) {
|
||||
ByteReadPacket(serialized)
|
||||
},
|
||||
Headers.build {
|
||||
append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType)
|
||||
append(HttpHeaders.ContentDisposition, "filename=data.bytes")
|
||||
dataHeadersBuilder()
|
||||
}
|
||||
)
|
||||
additionalParametersBuilder()
|
||||
},
|
||||
dataHeadersBuilder,
|
||||
requestBuilder,
|
||||
serialFormat
|
||||
)
|
||||
|
||||
suspend fun <ResultType> HttpClient.unimultipart(
|
||||
url: String,
|
||||
mppFile: MPPFile,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
mimetype: String = "*/*",
|
||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
): ResultType = unimultipart(
|
||||
url,
|
||||
mppFile.filename.string,
|
||||
mppFile.inputProvider(),
|
||||
resultDeserializer,
|
||||
mimetype,
|
||||
additionalParametersBuilder,
|
||||
dataHeadersBuilder,
|
||||
requestBuilder,
|
||||
serialFormat
|
||||
)
|
||||
|
||||
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
|
||||
url: String,
|
||||
mppFile: MPPFile,
|
||||
otherData: BodyPair<BodyType>,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
mimetype: String = "*/*",
|
||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
): ResultType = unimultipart(
|
||||
url,
|
||||
mppFile,
|
||||
resultDeserializer,
|
||||
mimetype,
|
||||
additionalParametersBuilder = {
|
||||
val serialized = serialFormat.encodeDefault(otherData.first, otherData.second)
|
||||
append(
|
||||
"data",
|
||||
InputProvider(serialized.size.toLong()) {
|
||||
ByteReadPacket(serialized)
|
||||
},
|
||||
Headers.build {
|
||||
append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType)
|
||||
append(HttpHeaders.ContentDisposition, "filename=data.bytes")
|
||||
dataHeadersBuilder()
|
||||
}
|
||||
)
|
||||
additionalParametersBuilder()
|
||||
},
|
||||
dataHeadersBuilder,
|
||||
requestBuilder,
|
||||
serialFormat
|
||||
)
|
||||
|
@@ -0,0 +1,11 @@
|
||||
package dev.inmo.micro_utils.ktor.client
|
||||
|
||||
import dev.inmo.micro_utils.common.*
|
||||
import io.ktor.client.request.forms.InputProvider
|
||||
import io.ktor.utils.io.core.ByteReadPacket
|
||||
|
||||
actual suspend fun MPPFile.inputProvider(): InputProvider = bytes().let {
|
||||
InputProvider(it.size.toLong()) {
|
||||
ByteReadPacket(it)
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package dev.inmo.micro_utils.ktor.client
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import io.ktor.client.request.forms.InputProvider
|
||||
import io.ktor.utils.io.streams.asInput
|
||||
|
||||
actual suspend fun MPPFile.inputProvider(): InputProvider = InputProvider(length()) {
|
||||
inputStream().asInput()
|
||||
}
|
@@ -10,6 +10,7 @@ kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api internalProject("micro_utils.common")
|
||||
api "org.jetbrains.kotlinx:kotlinx-serialization-cbor:$kotlin_serialisation_core_version"
|
||||
api "com.soywiz.korlibs.klock:klock:$klockVersion"
|
||||
}
|
||||
|
@@ -1,22 +1,31 @@
|
||||
package dev.inmo.micro_utils.ktor.server
|
||||
|
||||
import dev.inmo.micro_utils.common.*
|
||||
import dev.inmo.micro_utils.coroutines.safely
|
||||
import dev.inmo.micro_utils.ktor.common.*
|
||||
import io.ktor.application.ApplicationCall
|
||||
import io.ktor.application.call
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.content.PartData
|
||||
import io.ktor.http.content.forEachPart
|
||||
import io.ktor.request.receive
|
||||
import io.ktor.request.receiveMultipart
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.response.respondBytes
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.util.asStream
|
||||
import io.ktor.util.cio.writeChannel
|
||||
import io.ktor.util.pipeline.PipelineContext
|
||||
import io.ktor.utils.io.core.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.serialization.*
|
||||
import java.io.File
|
||||
import java.io.File.createTempFile
|
||||
|
||||
class UnifiedRouter(
|
||||
private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
||||
private val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
|
||||
val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
||||
val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
|
||||
) {
|
||||
fun <T> Route.includeWebsocketHandling(
|
||||
suburl: String,
|
||||
@@ -104,6 +113,139 @@ suspend fun <T> ApplicationCall.uniload(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun ApplicationCall.uniloadMultipart(
|
||||
onFormItem: (PartData.FormItem) -> Unit = {},
|
||||
onCustomFileItem: (PartData.FileItem) -> Unit = {},
|
||||
onBinaryContent: (PartData.BinaryItem) -> Unit = {}
|
||||
) = safely {
|
||||
val multipartData = receiveMultipart()
|
||||
|
||||
var resultInput: Input? = null
|
||||
|
||||
multipartData.forEachPart {
|
||||
when (it) {
|
||||
is PartData.FormItem -> onFormItem(it)
|
||||
is PartData.FileItem -> {
|
||||
when (it.name) {
|
||||
"bytes" -> resultInput = it.provider()
|
||||
else -> onCustomFileItem(it)
|
||||
}
|
||||
}
|
||||
is PartData.BinaryItem -> onBinaryContent(it)
|
||||
}
|
||||
}
|
||||
|
||||
resultInput ?: error("Bytes has not been received")
|
||||
}
|
||||
|
||||
suspend fun <T> ApplicationCall.uniloadMultipart(
|
||||
deserializer: DeserializationStrategy<T>,
|
||||
onFormItem: (PartData.FormItem) -> Unit = {},
|
||||
onCustomFileItem: (PartData.FileItem) -> Unit = {},
|
||||
onBinaryContent: (PartData.BinaryItem) -> Unit = {}
|
||||
): Pair<Input, T> {
|
||||
var data: Optional<T>? = null
|
||||
val resultInput = uniloadMultipart(
|
||||
onFormItem,
|
||||
{
|
||||
if (it.name == "data") {
|
||||
data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional
|
||||
} else {
|
||||
onCustomFileItem(it)
|
||||
}
|
||||
},
|
||||
onBinaryContent
|
||||
)
|
||||
|
||||
val completeData = data ?: error("Data has not been received")
|
||||
return resultInput to (completeData.dataOrNull().let { it as T })
|
||||
}
|
||||
|
||||
suspend fun <T> ApplicationCall.uniloadMultipartFile(
|
||||
deserializer: DeserializationStrategy<T>,
|
||||
onFormItem: (PartData.FormItem) -> Unit = {},
|
||||
onCustomFileItem: (PartData.FileItem) -> Unit = {},
|
||||
onBinaryContent: (PartData.BinaryItem) -> Unit = {},
|
||||
) = safely {
|
||||
val multipartData = receiveMultipart()
|
||||
|
||||
var resultInput: MPPFile? = null
|
||||
var data: Optional<T>? = null
|
||||
|
||||
multipartData.forEachPart {
|
||||
when (it) {
|
||||
is PartData.FormItem -> onFormItem(it)
|
||||
is PartData.FileItem -> {
|
||||
when (it.name) {
|
||||
"bytes" -> {
|
||||
val name = FileName(it.originalFileName ?: error("File name is unknown for default part"))
|
||||
resultInput = MPPFile.createTempFile(
|
||||
name.nameWithoutExtension.let {
|
||||
var resultName = it
|
||||
while (resultName.length < 3) {
|
||||
resultName += "_"
|
||||
}
|
||||
resultName
|
||||
},
|
||||
".${name.extension}"
|
||||
).apply {
|
||||
outputStream().use { fileStream ->
|
||||
it.provider().asStream().copyTo(fileStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
"data" -> data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional
|
||||
else -> onCustomFileItem(it)
|
||||
}
|
||||
}
|
||||
is PartData.BinaryItem -> onBinaryContent(it)
|
||||
}
|
||||
}
|
||||
|
||||
val completeData = data ?: error("Data has not been received")
|
||||
(resultInput ?: error("Bytes has not been received")) to (completeData.dataOrNull().let { it as T })
|
||||
}
|
||||
|
||||
suspend fun ApplicationCall.uniloadMultipartFile(
|
||||
onFormItem: (PartData.FormItem) -> Unit = {},
|
||||
onCustomFileItem: (PartData.FileItem) -> Unit = {},
|
||||
onBinaryContent: (PartData.BinaryItem) -> Unit = {},
|
||||
) = safely {
|
||||
val multipartData = receiveMultipart()
|
||||
|
||||
var resultInput: MPPFile? = null
|
||||
|
||||
multipartData.forEachPart {
|
||||
when (it) {
|
||||
is PartData.FormItem -> onFormItem(it)
|
||||
is PartData.FileItem -> {
|
||||
if (it.name == "bytes") {
|
||||
val name = FileName(it.originalFileName ?: error("File name is unknown for default part"))
|
||||
resultInput = MPPFile.createTempFile(
|
||||
name.nameWithoutExtension.let {
|
||||
var resultName = it
|
||||
while (resultName.length < 3) {
|
||||
resultName += "_"
|
||||
}
|
||||
resultName
|
||||
},
|
||||
".${name.extension}"
|
||||
).apply {
|
||||
outputStream().use { fileStream ->
|
||||
it.provider().asStream().copyTo(fileStream)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onCustomFileItem(it)
|
||||
}
|
||||
}
|
||||
is PartData.BinaryItem -> onBinaryContent(it)
|
||||
}
|
||||
}
|
||||
|
||||
resultInput ?: error("Bytes has not been received")
|
||||
}
|
||||
|
||||
suspend fun ApplicationCall.getParameterOrSendError(
|
||||
field: String
|
||||
) = parameters[field].also {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package dev.inmo.micro_utils.ktor.server
|
||||
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator
|
||||
import io.ktor.application.Application
|
||||
import io.ktor.server.cio.CIO
|
||||
import io.ktor.server.engine.*
|
||||
@@ -31,3 +32,27 @@ fun createKtorServer(
|
||||
port: Int = Random.nextInt(1024, 65535),
|
||||
block: Application.() -> Unit
|
||||
): ApplicationEngine = createKtorServer(CIO, host, port, block)
|
||||
|
||||
fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration> createKtorServer(
|
||||
engine: ApplicationEngineFactory<TEngine, TConfiguration>,
|
||||
host: String = "localhost",
|
||||
port: Int = Random.nextInt(1024, 65535),
|
||||
configurators: List<KtorApplicationConfigurator>
|
||||
): TEngine = createKtorServer(
|
||||
engine,
|
||||
host,
|
||||
port
|
||||
) {
|
||||
configurators.forEach { it.apply { configure() } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create server with [CIO] server engine without starting of it
|
||||
*
|
||||
* @see ApplicationEngine.start
|
||||
*/
|
||||
fun createKtorServer(
|
||||
host: String = "localhost",
|
||||
port: Int = Random.nextInt(1024, 65535),
|
||||
configurators: List<KtorApplicationConfigurator>
|
||||
): ApplicationEngine = createKtorServer(CIO, host, port, configurators)
|
||||
|
@@ -24,3 +24,8 @@ dependencies {
|
||||
}
|
||||
|
||||
mainClassName="MainKt"
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
|
||||
|
||||
package dev.inmo.micro_utils.language_codes
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
52
mime_types/mimes_generator/mime_generator.py
Normal file
52
mime_types/mimes_generator/mime_generator.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
import pandas as pd
|
||||
import itertools
|
||||
|
||||
def fix_name(category, raw_name):
|
||||
splitted = raw_name.replace('-', '+').replace('.', '+').replace(',', '+').split('+')
|
||||
out1 = ""
|
||||
for s in splitted:
|
||||
out1 += s.capitalize()
|
||||
|
||||
result = ""
|
||||
if out1[0].isdigit():
|
||||
result += category[0].capitalize()
|
||||
result += out1
|
||||
else:
|
||||
result += out1
|
||||
return result
|
||||
|
||||
if __name__ == '__main__':
|
||||
df = pd.read_html(open('table.html', 'r'))
|
||||
mimes = []
|
||||
for row in df[0].iterrows():
|
||||
mime = row[1][1]
|
||||
mime_category = mime.split('/', 1)[0]
|
||||
mime_name = mime.split('/', 1)[1]
|
||||
mimes.append({
|
||||
'mime_category': mime_category,
|
||||
'mime_name': mime_name,
|
||||
})
|
||||
|
||||
# codegen
|
||||
|
||||
mimes.sort(key=lambda x: x['mime_category'])
|
||||
grouped = itertools.groupby(mimes, lambda x: x['mime_category'])
|
||||
code = ''
|
||||
code2 = 'internal val knownMimeTypes: Set<MimeType> = setOf(\n'
|
||||
code2 += ' KnownMimeTypes.Any,\n'
|
||||
for key, group in grouped:
|
||||
group_name = key.capitalize()
|
||||
code += '@Serializable(MimeTypeSerializer::class)\nsealed class %s(raw: String) : MimeType, KnownMimeTypes(raw) {\n' % group_name
|
||||
code += ' @Serializable(MimeTypeSerializer::class)\n object Any: %s ("%s/*")\n' % (group_name, key)
|
||||
for mime in group:
|
||||
name = fix_name(mime['mime_category'], mime['mime_name'])
|
||||
code += ' @Serializable(MimeTypeSerializer::class)\n object %s: %s ("%s/%s")\n' % (name, group_name, mime['mime_category'], mime['mime_name'])
|
||||
code2 += ' KnownMimeTypes.%s.%s,\n' % (group_name, name)
|
||||
code += '}\n\n'
|
||||
code2 += ')\n'
|
||||
with open('out1.txt', 'w') as file:
|
||||
file.write(code)
|
||||
with open('out2.txt', 'w') as file:
|
||||
file.write(code2)
|
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
|
||||
|
||||
package dev.inmo.micro_utils.mime_types
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
@@ -24,3 +24,8 @@ kotlin {
|
||||
}
|
||||
|
||||
apply from: "$defaultAndroidSettingsPresetPath"
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
@@ -4,7 +4,13 @@ project.group = "$group"
|
||||
apply from: "$publishGradlePath"
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
jvm {
|
||||
compilations.main {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
@@ -26,3 +32,8 @@ kotlin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
@@ -4,7 +4,13 @@ project.group = "$group"
|
||||
apply from: "$publishGradlePath"
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
jvm {
|
||||
compilations.main {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
js (IR) {
|
||||
browser()
|
||||
nodejs()
|
||||
@@ -48,3 +54,8 @@ kotlin {
|
||||
}
|
||||
|
||||
apply from: "$defaultAndroidSettingsPresetPath"
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
@@ -5,3 +5,13 @@ plugins {
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.common")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package dev.inmo.micro_utils.pagination
|
||||
|
||||
import dev.inmo.micro_utils.common.intersect
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
|
||||
@@ -9,7 +10,7 @@ import kotlin.math.floor
|
||||
* If you want to request something, you should use [SimplePagination]. If you need to return some result including
|
||||
* pagination - [PaginationResult]
|
||||
*/
|
||||
interface Pagination {
|
||||
interface Pagination : ClosedRange<Int> {
|
||||
/**
|
||||
* Started with 0.
|
||||
* Number of page inside of pagination. Offset can be calculated as [page] * [size]
|
||||
@@ -20,6 +21,17 @@ interface Pagination {
|
||||
* Size of current page. Offset can be calculated as [page] * [size]
|
||||
*/
|
||||
val size: Int
|
||||
|
||||
override val start: Int
|
||||
get() = page * size
|
||||
override val endInclusive: Int
|
||||
get() = start + size - 1
|
||||
}
|
||||
|
||||
fun Pagination.intersect(
|
||||
other: Pagination
|
||||
): Pagination? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let {
|
||||
PaginationByIndexes(it.first, it.second)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,7 +44,7 @@ inline val Pagination.isFirstPage
|
||||
* First number in index of objects. It can be used as offset for databases or other data sources
|
||||
*/
|
||||
val Pagination.firstIndex: Int
|
||||
get() = page * size
|
||||
get() = start
|
||||
|
||||
/**
|
||||
* Last number in index of objects. In fact, one [Pagination] object represent data in next range:
|
||||
@@ -41,7 +53,7 @@ val Pagination.firstIndex: Int
|
||||
* you will retrieve [Pagination.firstIndex] == 10 and [Pagination.lastIndex] == 19. Here [Pagination.lastIndexExclusive] == 20
|
||||
*/
|
||||
val Pagination.lastIndexExclusive: Int
|
||||
get() = firstIndex + size
|
||||
get() = endInclusive + 1
|
||||
|
||||
/**
|
||||
* Last number in index of objects. In fact, one [Pagination] object represent data in next range:
|
||||
@@ -50,7 +62,7 @@ val Pagination.lastIndexExclusive: Int
|
||||
* you will retrieve [Pagination.firstIndex] == 10 and [Pagination.lastIndex] == 19.
|
||||
*/
|
||||
val Pagination.lastIndex: Int
|
||||
get() = lastIndexExclusive - 1
|
||||
get() = endInclusive
|
||||
|
||||
/**
|
||||
* Calculates pages count for given [datasetSize]
|
||||
|
@@ -16,6 +16,16 @@ suspend fun <T> getAll(
|
||||
return results.toList()
|
||||
}
|
||||
|
||||
suspend fun <T, R> R.getAllBy(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
paginationMapper: R.(PaginationResult<T>) -> Pagination?,
|
||||
block: suspend R.(Pagination) -> PaginationResult<T>
|
||||
): List<T> = getAll(
|
||||
initialPagination,
|
||||
{ paginationMapper(it) },
|
||||
{ block(it) }
|
||||
)
|
||||
|
||||
suspend fun <T> getAllWithNextPaging(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
block: suspend (Pagination) -> PaginationResult<T>
|
||||
@@ -25,6 +35,14 @@ suspend fun <T> getAllWithNextPaging(
|
||||
block
|
||||
)
|
||||
|
||||
suspend fun <T, R> R.getAllByWithNextPaging(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
block: suspend R.(Pagination) -> PaginationResult<T>
|
||||
): List<T> = getAllWithNextPaging(
|
||||
initialPagination,
|
||||
{ block(it) }
|
||||
)
|
||||
|
||||
suspend fun <T> getAllWithCurrentPaging(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
block: suspend (Pagination) -> PaginationResult<T>
|
||||
@@ -33,3 +51,11 @@ suspend fun <T> getAllWithCurrentPaging(
|
||||
{ it.currentPageIfNotEmpty() },
|
||||
block
|
||||
)
|
||||
|
||||
suspend fun <T, R> R.getAllByWithCurrentPaging(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
block: suspend R.(Pagination) -> PaginationResult<T>
|
||||
): List<T> = getAllWithCurrentPaging(
|
||||
initialPagination,
|
||||
{ block(it) }
|
||||
)
|
||||
|
@@ -1,5 +1,4 @@
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'signing'
|
||||
|
||||
task javadocsJar(type: Jar) {
|
||||
classifier = 'javadoc'
|
||||
@@ -69,8 +68,19 @@ publishing {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signing {
|
||||
useGpgCmd()
|
||||
sign publishing.publications
|
||||
|
||||
if (project.hasProperty("signing.gnupg.keyName")) {
|
||||
apply plugin: 'signing'
|
||||
|
||||
signing {
|
||||
useGpgCmd()
|
||||
|
||||
sign publishing.publications
|
||||
}
|
||||
|
||||
task signAll {
|
||||
tasks.withType(Sign).forEach {
|
||||
dependsOn(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1 +1 @@
|
||||
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","includeGpgSigning":true,"developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}]}}
|
||||
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}}}
|
@@ -2,6 +2,8 @@ package dev.inmo.micro_utils.repos
|
||||
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.core.database.*
|
||||
import dev.inmo.micro_utils.repos.getLongOrNull
|
||||
|
||||
fun createTableQuery(
|
||||
tableName: String,
|
||||
@@ -32,6 +34,11 @@ fun SQLiteDatabase.createTable(
|
||||
fun Cursor.getString(columnName: String): String = getString(
|
||||
getColumnIndexOrThrow(columnName)
|
||||
)
|
||||
fun Cursor.getStringOrNull(columnName: String): String? {
|
||||
return getStringOrNull(
|
||||
getColumnIndex(columnName).takeIf { it != -1 } ?: return null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IllegalArgumentException
|
||||
@@ -39,6 +46,11 @@ fun Cursor.getString(columnName: String): String = getString(
|
||||
fun Cursor.getShort(columnName: String): Short = getShort(
|
||||
getColumnIndexOrThrow(columnName)
|
||||
)
|
||||
fun Cursor.getShortOrNull(columnName: String): Short? {
|
||||
return getShortOrNull(
|
||||
getColumnIndex(columnName).takeIf { it != -1 } ?: return null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IllegalArgumentException
|
||||
@@ -46,6 +58,11 @@ fun Cursor.getShort(columnName: String): Short = getShort(
|
||||
fun Cursor.getLong(columnName: String): Long = getLong(
|
||||
getColumnIndexOrThrow(columnName)
|
||||
)
|
||||
fun Cursor.getLongOrNull(columnName: String): Long? {
|
||||
return getLongOrNull(
|
||||
getColumnIndex(columnName).takeIf { it != -1 } ?: return null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IllegalArgumentException
|
||||
@@ -53,6 +70,23 @@ fun Cursor.getLong(columnName: String): Long = getLong(
|
||||
fun Cursor.getInt(columnName: String): Int = getInt(
|
||||
getColumnIndexOrThrow(columnName)
|
||||
)
|
||||
fun Cursor.getIntOrNull(columnName: String): Int? {
|
||||
return getIntOrNull(
|
||||
getColumnIndex(columnName).takeIf { it != -1 } ?: return null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
fun Cursor.getFloat(columnName: String): Float = getFloat(
|
||||
getColumnIndexOrThrow(columnName)
|
||||
)
|
||||
fun Cursor.getFloatOrNull(columnName: String): Float? {
|
||||
return getFloatOrNull(
|
||||
getColumnIndex(columnName).takeIf { it != -1 } ?: return null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IllegalArgumentException
|
||||
@@ -60,6 +94,11 @@ fun Cursor.getInt(columnName: String): Int = getInt(
|
||||
fun Cursor.getDouble(columnName: String): Double = getDouble(
|
||||
getColumnIndexOrThrow(columnName)
|
||||
)
|
||||
fun Cursor.getDoubleOrNull(columnName: String): Double? {
|
||||
return getDoubleOrNull(
|
||||
getColumnIndex(columnName).takeIf { it != -1 } ?: return null
|
||||
)
|
||||
}
|
||||
|
||||
fun SQLiteDatabase.select(
|
||||
table: String,
|
||||
|
@@ -6,13 +6,15 @@ import dev.inmo.micro_utils.repos.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType>(
|
||||
helper: StandardSQLHelper
|
||||
helper: StandardSQLHelper,
|
||||
replyInFlows: Int = 0,
|
||||
extraBufferCapacityInFlows: Int = 64
|
||||
) : WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>,
|
||||
AbstractAndroidCRUDRepo<ObjectType, IdType>(helper),
|
||||
StandardCRUDRepo<ObjectType, IdType, InputValueType> {
|
||||
protected val newObjectsChannel = MutableSharedFlow<ObjectType>(64)
|
||||
protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(64)
|
||||
protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(64)
|
||||
protected val newObjectsChannel = MutableSharedFlow<ObjectType>(replyInFlows, extraBufferCapacityInFlows)
|
||||
protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(replyInFlows, extraBufferCapacityInFlows)
|
||||
protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(replyInFlows, extraBufferCapacityInFlows)
|
||||
override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asSharedFlow()
|
||||
override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow()
|
||||
override val deletedObjectsIdsFlow: Flow<IdType> = deleteObjectsIdsChannel.asSharedFlow()
|
||||
@@ -102,4 +104,4 @@ abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType
|
||||
}
|
||||
|
||||
override suspend fun count(): Long = helper.blockingReadableTransaction { select(tableName).use { it.count.toLong() } }
|
||||
}
|
||||
}
|
||||
|
@@ -10,15 +10,16 @@ import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
|
||||
flowsChannelsSize: Int = 0,
|
||||
tableName: String = ""
|
||||
tableName: String = "",
|
||||
replyCacheInFlows: Int = 0
|
||||
) :
|
||||
AbstractExposedReadCRUDRepo<ObjectType, IdType>(tableName),
|
||||
ExposedCRUDRepo<ObjectType, IdType>,
|
||||
WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>
|
||||
{
|
||||
protected val newObjectsChannel = MutableSharedFlow<ObjectType>(flowsChannelsSize)
|
||||
protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(flowsChannelsSize)
|
||||
protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(flowsChannelsSize)
|
||||
protected val newObjectsChannel = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize)
|
||||
protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize)
|
||||
protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(replyCacheInFlows, flowsChannelsSize)
|
||||
|
||||
override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asSharedFlow()
|
||||
override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow()
|
||||
|
@@ -19,8 +19,8 @@ open class ExposedKeyValueRepo<Key, Value>(
|
||||
valueColumnAllocator,
|
||||
tableName
|
||||
) {
|
||||
private val _onNewValue = MutableSharedFlow<Pair<Key, Value>>()
|
||||
private val _onValueRemoved = MutableSharedFlow<Key>()
|
||||
protected val _onNewValue = MutableSharedFlow<Pair<Key, Value>>()
|
||||
protected val _onValueRemoved = MutableSharedFlow<Key>()
|
||||
|
||||
override val onNewValue: Flow<Pair<Key, Value>> = _onNewValue.asSharedFlow()
|
||||
override val onValueRemoved: Flow<Key> = _onValueRemoved.asSharedFlow()
|
||||
|
@@ -12,8 +12,8 @@ open class ExposedReadKeyValueRepo<Key, Value>(
|
||||
valueColumnAllocator: ColumnAllocator<Value>,
|
||||
tableName: String? = null
|
||||
) : ReadStandardKeyValueRepo<Key, Value>, ExposedRepo, Table(tableName ?: "") {
|
||||
protected val keyColumn: Column<Key> = keyColumnAllocator()
|
||||
protected val valueColumn: Column<Value> = valueColumnAllocator()
|
||||
val keyColumn: Column<Key> = keyColumnAllocator()
|
||||
val valueColumn: Column<Value> = valueColumnAllocator()
|
||||
override val primaryKey: PrimaryKey = PrimaryKey(keyColumn, valueColumn)
|
||||
|
||||
init { initTable() }
|
||||
|
@@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.*
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
typealias ExposedKeyValuesRepo<Key, Value> = ExposedOneToManyKeyValueRepo<Key, Value>
|
||||
open class ExposedOneToManyKeyValueRepo<Key, Value>(
|
||||
database: Database,
|
||||
keyColumnAllocator: ColumnAllocator<Key>,
|
||||
|
@@ -3,17 +3,20 @@ package dev.inmo.micro_utils.repos.exposed.onetomany
|
||||
import dev.inmo.micro_utils.pagination.*
|
||||
import dev.inmo.micro_utils.repos.ReadOneToManyKeyValueRepo
|
||||
import dev.inmo.micro_utils.repos.exposed.*
|
||||
import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedReadKeyValueRepo
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
typealias ExposedReadKeyValuesRepo<Key, Value> = ExposedReadOneToManyKeyValueRepo<Key, Value>
|
||||
|
||||
open class ExposedReadOneToManyKeyValueRepo<Key, Value>(
|
||||
override val database: Database,
|
||||
keyColumnAllocator: ColumnAllocator<Key>,
|
||||
valueColumnAllocator: ColumnAllocator<Value>,
|
||||
tableName: String? = null
|
||||
) : ReadOneToManyKeyValueRepo<Key, Value>, ExposedRepo, Table(tableName ?: "") {
|
||||
protected val keyColumn: Column<Key> = keyColumnAllocator()
|
||||
protected val valueColumn: Column<Value> = valueColumnAllocator()
|
||||
val keyColumn: Column<Key> = keyColumnAllocator()
|
||||
val valueColumn: Column<Value> = valueColumnAllocator()
|
||||
|
||||
init { initTable() }
|
||||
|
||||
|
@@ -18,8 +18,8 @@ inline fun versionsRepo(database: Database): VersionsRepo<Database> = StandardVe
|
||||
class ExposedStandardVersionsRepoProxy(
|
||||
override val database: Database
|
||||
) : StandardVersionsRepoProxy<Database>, Table("ExposedVersionsProxy"), ExposedRepo {
|
||||
private val tableNameColumn = text("tableName")
|
||||
private val tableVersionColumn = integer("tableName")
|
||||
val tableNameColumn = text("tableName")
|
||||
val tableVersionColumn = integer("tableName")
|
||||
|
||||
init {
|
||||
initTable()
|
||||
|
@@ -23,13 +23,13 @@ class ReadMapCRUDRepo<ObjectType, IdType>(
|
||||
}
|
||||
|
||||
abstract class WriteMapCRUDRepo<ObjectType, IdType, InputValueType>(
|
||||
private val map: MutableMap<IdType, ObjectType> = mutableMapOf()
|
||||
protected val map: MutableMap<IdType, ObjectType> = mutableMapOf()
|
||||
) : WriteStandardCRUDRepo<ObjectType, IdType, InputValueType> {
|
||||
private val _newObjectsFlow: MutableSharedFlow<ObjectType> = MutableSharedFlow()
|
||||
protected val _newObjectsFlow: MutableSharedFlow<ObjectType> = MutableSharedFlow()
|
||||
override val newObjectsFlow: Flow<ObjectType> = _newObjectsFlow.asSharedFlow()
|
||||
private val _updatedObjectsFlow: MutableSharedFlow<ObjectType> = MutableSharedFlow()
|
||||
protected val _updatedObjectsFlow: MutableSharedFlow<ObjectType> = MutableSharedFlow()
|
||||
override val updatedObjectsFlow: Flow<ObjectType> = _updatedObjectsFlow.asSharedFlow()
|
||||
private val _deletedObjectsIdsFlow: MutableSharedFlow<IdType> = MutableSharedFlow()
|
||||
protected val _deletedObjectsIdsFlow: MutableSharedFlow<IdType> = MutableSharedFlow()
|
||||
override val deletedObjectsIdsFlow: Flow<IdType> = _deletedObjectsIdsFlow.asSharedFlow()
|
||||
|
||||
protected abstract suspend fun updateObject(newValue: InputValueType, id: IdType, old: ObjectType): ObjectType
|
||||
|
@@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
||||
class ReadMapKeyValueRepo<Key, Value>(
|
||||
private val map: Map<Key, Value> = emptyMap()
|
||||
protected val map: Map<Key, Value> = emptyMap()
|
||||
) : ReadStandardKeyValueRepo<Key, Value> {
|
||||
override suspend fun get(k: Key): Value? = map[k]
|
||||
|
||||
|
@@ -8,20 +8,20 @@ import kotlin.reflect.KClass
|
||||
|
||||
open class TypedSerializer<T : Any>(
|
||||
kClass: KClass<T>,
|
||||
presetSerializers: Map<String, KSerializer<out T>> = emptyMap()
|
||||
presetSerializers: Map<String, KSerializer<out T>> = emptyMap(),
|
||||
) : KSerializer<T> {
|
||||
protected val serializers = presetSerializers.toMutableMap()
|
||||
@InternalSerializationApi
|
||||
open override val descriptor: SerialDescriptor = buildSerialDescriptor(
|
||||
"TextSourceSerializer",
|
||||
@OptIn(InternalSerializationApi::class)
|
||||
override val descriptor: SerialDescriptor = buildSerialDescriptor(
|
||||
"TypedSerializer",
|
||||
SerialKind.CONTEXTUAL
|
||||
) {
|
||||
element("type", String.serializer().descriptor)
|
||||
element("value", ContextualSerializer(kClass).descriptor)
|
||||
}
|
||||
|
||||
@InternalSerializationApi
|
||||
open override fun deserialize(decoder: Decoder): T {
|
||||
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||
override fun deserialize(decoder: Decoder): T {
|
||||
return decoder.decodeStructure(descriptor) {
|
||||
var type: String? = null
|
||||
lateinit var result: T
|
||||
@@ -44,13 +44,13 @@ open class TypedSerializer<T : Any>(
|
||||
}
|
||||
}
|
||||
|
||||
@InternalSerializationApi
|
||||
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||
protected open fun <O: T> CompositeEncoder.encode(value: O) {
|
||||
encodeSerializableElement(descriptor, 1, value::class.serializer() as KSerializer<O>, value)
|
||||
}
|
||||
|
||||
@InternalSerializationApi
|
||||
open override fun serialize(encoder: Encoder, value: T) {
|
||||
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||
override fun serialize(encoder: Encoder, value: T) {
|
||||
encoder.encodeStructure(descriptor) {
|
||||
val valueSerializer = value::class.serializer()
|
||||
val type = serializers.keys.first { serializers[it] == valueSerializer }
|
||||
@@ -69,6 +69,20 @@ open class TypedSerializer<T : Any>(
|
||||
}
|
||||
}
|
||||
|
||||
@InternalSerializationApi
|
||||
operator fun <T : Any> TypedSerializer<T>.plusAssign(kClass: KClass<T>) {
|
||||
include(kClass.simpleName!!, kClass.serializer())
|
||||
}
|
||||
|
||||
@InternalSerializationApi
|
||||
operator fun <T : Any> TypedSerializer<T>.minusAssign(kClass: KClass<T>) {
|
||||
exclude(kClass.simpleName!!)
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> TypedSerializer(
|
||||
presetSerializers: Map<String, KSerializer<out T>> = emptyMap()
|
||||
) = TypedSerializer(T::class, presetSerializers)
|
||||
|
||||
inline fun <reified T : Any> TypedSerializer(
|
||||
vararg presetSerializers: Pair<String, KSerializer<out T>>
|
||||
) = TypedSerializer(presetSerializers.toMap())
|
||||
|
Reference in New Issue
Block a user