Compare commits

...

131 Commits

Author SHA1 Message Date
313f622f7e Update CHANGELOG.md 2022-02-01 14:27:07 +06:00
6cba1fe1a2 Update klock 2022-02-01 14:25:24 +06:00
fd2d0e80b7 start 0.9.5 2022-02-01 14:24:12 +06:00
96ab2e8aca Merge pull request #125 from InsanusMokrassar/0.9.4
0.9.4
2022-01-18 18:58:49 +06:00
0202988cae several fixes in optionallyReverse 2022-01-17 16:11:55 +06:00
d619d59947 Either updates 2022-01-17 15:56:48 +06:00
85b3e48d18 add optionallyReverse extensions 2022-01-17 15:38:23 +06:00
7a9b7d98a1 start 0.9.4 2022-01-14 13:22:04 +06:00
b212acfcaf Merge pull request #124 from InsanusMokrassar/0.9.3
0.9.3
2022-01-14 00:46:20 +06:00
3a45e5dc70 Update CHANGELOG.md 2022-01-14 00:37:03 +06:00
73190518d5 Update gradle.properties 2022-01-14 00:35:51 +06:00
03f78180dc Merge pull request #123 from InsanusMokrassar/0.9.2
0.9.2
2022-01-13 11:22:54 +06:00
1c0b8cf842 Update CHANGELOG.md 2022-01-13 10:20:35 +06:00
a1624ea2a9 Update gradle.properties 2022-01-13 10:19:08 +06:00
23a050cf1e Merge pull request #122 from InsanusMokrassar/0.9.1
0.9.1
2022-01-08 16:12:04 +06:00
916f2f96f4 typealiases for exposed one to many 2022-01-08 14:35:28 +06:00
00cc214754 repo exposed updates 2022-01-08 14:14:44 +06:00
b2e38f72b9 start 0.9.1 2022-01-08 14:12:57 +06:00
e7107d238d Update dokka_push.yml 2021-12-30 02:55:19 +06:00
ed9ebdbd1a Merge pull request #121 from InsanusMokrassar/0.9.0
0.9.0
2021-12-30 02:52:34 +06:00
e80676d3d2 Update packages_push.yml 2021-12-29 19:43:19 +06:00
02d02fa8f2 update android tools build 2021-12-29 19:24:25 +06:00
bd783fb74f update dokka 2021-12-29 17:44:45 +06:00
50386adf70 ignore kotlin-js-store folder 2021-12-28 21:19:47 +06:00
f4ee6c2890 update exposed and adapt to new version of kotlin serialization 2021-12-28 21:17:17 +06:00
d45aef9fe5 start 0.9.0 2021-12-28 09:58:59 +06:00
a56cd3dddd Merge pull request #120 from InsanusMokrassar/0.8.9
0.8.9
2021-12-27 17:02:16 +06:00
419e7070ee more fixes to god of fixes 2021-12-27 17:02:00 +06:00
612cf40b5f small hotfix 2021-12-27 16:00:07 +06:00
8b39882e83 fixes in DefaultUpdatableStatesMachine 2021-12-27 15:57:55 +06:00
e639ae172b Fixes in uniloadMultipart 2021-12-27 15:55:05 +06:00
d0446850ae start 0.8.9 2021-12-27 15:45:44 +06:00
c48465b90b Merge pull request #119 from InsanusMokrassar/0.8.8
0.8.8
2021-12-26 22:11:04 +06:00
f419fd03d2 small fix of performing state update for UpdatableStatesMachine 2021-12-26 22:00:26 +06:00
494812a660 one more fix 2021-12-26 21:25:12 +06:00
eb78f21eec fix FSMBuilder 2021-12-26 21:21:40 +06:00
4bda70268b small fix of style in DefaultUpdatableStatesMachine 2021-12-26 21:07:40 +06:00
f037ce4371 updates in FSM 2021-12-26 21:06:26 +06:00
3d2196e35d update dependencies (which possible) 2021-12-26 20:07:13 +06:00
a74f061b02 start 0.8.8 2021-12-26 20:02:18 +06:00
11ade14676 Merge pull request #118 from InsanusMokrassar/0.8.7
0.8.7
2021-12-15 01:22:42 +06:00
eb562d8784 add stream using for multipart 2021-12-14 22:03:29 +06:00
1ee5b4bfd4 hotfix for multipart 2021-12-14 21:13:11 +06:00
d97892080b add preview work with multipart 2021-12-14 18:01:41 +06:00
6f37125724 start 0.8.7 and make UnifiedRequester/UnifiedRouter fields are public 2021-12-14 13:25:58 +06:00
ed1baaade7 Merge pull request #117 from InsanusMokrassar/0.8.6
0.8.6
2021-12-08 15:57:43 +06:00
bb9669f8fd fill changelog 2021-12-08 11:45:01 +06:00
bdac715d48 remove crossinline where possible 2021-12-08 11:41:49 +06:00
acf4971298 downgrade ktor 2021-12-08 11:36:24 +06:00
249bc83a8c update ktor 2021-11-27 16:22:13 +06:00
0fbb92f03f start 0.8.6 2021-11-27 16:20:56 +06:00
ca27cb3f82 Merge pull request #116 from InsanusMokrassar/0.8.5
0.8.5
2021-11-27 01:00:18 +06:00
3a5771a0cc Update RepeatOnFailure.kt 2021-11-26 23:07:00 +06:00
527a2a91ac repeatOnFailure 2021-11-26 19:45:59 +06:00
6763e5c4c6 start 0.8.5 2021-11-26 19:32:52 +06:00
06918d8310 add mime types generator python script 2021-11-24 14:31:24 +06:00
89ccaa1b57 Merge pull request #115 from InsanusMokrassar/0.8.4
0.8.4
2021-11-19 14:02:38 +06:00
5d0bdb9bcf fix pagination 2021-11-19 13:58:59 +06:00
31fdcf74a5 ktor server createKtorServer extensions 2021-11-19 13:24:45 +06:00
afca09cc1d start 0.8.4 2021-11-19 13:18:37 +06:00
531d89d9db Merge pull request #114 from InsanusMokrassar/0.8.3
0.8.3
2021-11-19 09:49:35 +06:00
6bbbea0bc3 suppressions in Optional.kt 2021-11-18 23:03:24 +06:00
e337cd98c8 optional workaround 2021-11-17 21:31:35 +06:00
bcbab3b380 add Optional type 2021-11-17 17:38:41 +06:00
fb63de7568 intersect 2021-11-17 14:22:45 +06:00
aa45a4ab13 now Pagination is an ClosedRange 2021-11-17 14:07:11 +06:00
2af7e2f681 start 0.8.3 2021-11-17 14:01:44 +06:00
34fd9edce0 Merge pull request #113 from InsanusMokrassar/0.8.2
0.8.2
2021-11-12 13:23:39 +06:00
2a4cb8c5f9 improvements in FSM 2021-11-12 13:19:15 +06:00
50ea40bc3a add changelog for dependencies update 2021-11-12 13:14:57 +06:00
a77654052d Update gradle.properties 2021-11-11 21:20:17 +06:00
88aafce552 start 0.8.2 2021-11-11 20:31:04 +06:00
4e95d6bfff Merge pull request #112 from InsanusMokrassar/0.8.1
0.8.1
2021-11-09 14:04:11 +06:00
38d0e34fb5 updates in scripts and update core ktx 2021-11-09 14:03:51 +06:00
8fbc6b9041 update changelog 2021-11-09 13:43:46 +06:00
e8219d6cf4 start 0.8.1 2021-11-09 10:21:23 +06:00
6c20fc4ca6 Merge pull request #111 from InsanusMokrassar/0.8.0
0.8.0
2021-11-05 21:56:00 +06:00
85cd975492 preparations for release 2021-11-05 21:45:01 +06:00
1171a717fe optimize imports 2021-11-05 15:49:39 +06:00
bbe5320312 rework of FSM + 0.8.0 2021-11-05 15:43:05 +06:00
00acb9fddd add suppresses to the generated classes 2021-11-03 17:22:32 +06:00
de3d14dc41 solution of #109 2021-11-03 15:36:25 +06:00
67ff9cc9b3 update dependencies 2021-11-03 15:18:41 +06:00
af132103a0 start 0.7.5 and add either serializer 2021-11-02 12:43:59 +06:00
3b1124a804 fixes 2021-10-30 12:17:18 +06:00
f226c2dfd6 Merge pull request #106 from InsanusMokrassar/0.7.4
0.7.4
2021-10-29 13:57:58 +06:00
69d6e63846 getAllBy* 2021-10-29 13:51:59 +06:00
02c3d397ad solution of #104 2021-10-29 13:40:53 +06:00
67a1050646 solution for #105 2021-10-28 18:46:53 +06:00
8cd0775a6c update kdocs of Either 2021-10-28 18:42:05 +06:00
162294d6c6 either 2021-10-28 18:02:02 +06:00
c4dd19dd00 start 0.7.4 2021-10-28 17:50:28 +06:00
d2314422f1 Merge pull request #103 from InsanusMokrassar/0.7.3
0.7.3
2021-10-23 14:26:42 +06:00
6fedd6f859 update dependencies 2021-10-23 14:24:31 +06:00
e52b59665f start 0.7.3 2021-10-23 14:19:30 +06:00
cda9d09689 fixes for kdocs 2021-10-18 22:57:25 +06:00
c9237b3f00 Merge pull request #102 from InsanusMokrassar/0.7.2
0.7.2
2021-10-16 09:51:24 +06:00
18bba66c4a Update CHANGELOG.md 2021-10-16 08:56:38 +06:00
63418c4a8a Update gradle.properties 2021-10-16 08:55:44 +06:00
2e66c6f4e3 Merge pull request #101 from InsanusMokrassar/0.7.1
0.7.1
2021-10-13 15:11:26 +06:00
e9c5df4c13 upfill kdocs 2021-10-13 15:09:05 +06:00
bc7789ad2c add kdocs 2021-10-13 15:06:58 +06:00
e3da761249 Fill changelog 2021-10-13 14:38:50 +06:00
4082f65afa AccumulatorFlow 2021-10-13 13:26:39 +06:00
5d1cab075d Update gradle.properties 2021-10-12 21:03:47 +06:00
bcf67f7e59 Update gradle.properties 2021-10-12 20:56:00 +06:00
7d3b1f8e75 StatesMachine is interface 2021-10-06 13:56:58 +06:00
119a0588cc DefaultStatesManager 2021-10-06 13:30:25 +06:00
fab789d9c0 start rework of FSM states manager 2021-10-06 12:14:02 +06:00
ceba81c08f start 0.7.1 2021-10-06 11:51:55 +06:00
a061af0558 actualize changelog 2021-10-05 13:52:28 +06:00
c7a53846ad Merge pull request #100 from InsanusMokrassar/0.7.0
0.7.0 + migration back to klock
2021-10-05 13:51:41 +06:00
a683cccf0c 0.7.0 + migration back to klock 2021-10-05 13:46:23 +06:00
50d41e35c1 remove deprecations 2021-10-04 16:09:01 +06:00
aa0e831cea Merge pull request #99 from InsanusMokrassar/0.6.0
0.6.0
2021-10-04 15:57:02 +06:00
44e26ccb4f migration onto datetime 2021-10-04 15:54:43 +06:00
2a783f6e2b start 0.6.0 2021-10-03 18:58:15 +06:00
6058d6a724 update ktor 2021-10-01 16:12:26 +06:00
2e9c7eb5fa Merge pull request #98 from InsanusMokrassar/0.5.31
0.5.31
2021-10-01 15:42:24 +06:00
e75465ad10 update dependencies 2021-09-30 11:59:44 +06:00
de01ad54e9 start 0.5.31 2021-09-30 11:57:29 +06:00
eeea7ddbe3 Merge pull request #97 from InsanusMokrassar/0.5.30
0.5.30
2021-09-25 16:22:34 +06:00
e0b18bec05 update dependencies 2021-09-25 14:55:56 +06:00
410e89bba9 start 0.5.30 2021-09-25 14:45:56 +06:00
9ef19dc42b Merge pull request #96 from InsanusMokrassar/0.5.29
0.5.29
2021-09-23 13:45:55 +06:00
0337d1b82d add fix of 31.0.0 for kdocs workflow 2021-09-23 13:23:02 +06:00
f5bd4c5ccb update dependencies 2021-09-23 13:12:03 +06:00
630f9bc0d4 start 0.5.29 2021-09-23 13:10:44 +06:00
18b4ffece1 Update packages_push.yml 2021-09-22 20:18:14 +06:00
f64e1effa3 Delete build.yml 2021-09-22 20:17:04 +06:00
847fcbb488 Merge pull request #95 from InsanusMokrassar/0.5.28
0.5.28
2021-09-19 21:59:25 +06:00
64 changed files with 1776 additions and 351 deletions

View File

@@ -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

View File

@@ -10,7 +10,10 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-java@v1 - uses: actions/setup-java@v1
with: with:
java-version: 1.8 java-version: 11
- name: Fix android 32.0.0 dx
continue-on-error: true
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
- name: Build - name: Build
run: ./gradlew dokkaHtml run: ./gradlew dokkaHtml
- name: Publish KDocs - name: Publish KDocs

View File

@@ -8,7 +8,10 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-java@v1 - uses: actions/setup-java@v1
with: with:
java-version: 1.8 java-version: 11
- name: Fix android 32.0.0 dx
continue-on-error: true
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
- name: Rewrite version - name: Rewrite version
run: | run: |
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
@@ -18,6 +21,7 @@ jobs:
- name: Build - name: Build
run: ./gradlew build run: ./gradlew build
- name: Publish - name: Publish
continue-on-error: true
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidDebugPublication -x signAndroidReleasePublication -x signKotlinMultiplatformPublication run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidDebugPublication -x signAndroidReleasePublication -x signKotlinMultiplatformPublication
env: env:
GITHUBPACKAGES_USER: ${{ github.actor }} GITHUBPACKAGES_USER: ${{ github.actor }}

1
.gitignore vendored
View File

@@ -11,5 +11,6 @@ out/
secret.gradle secret.gradle
local.properties local.properties
kotlin-js-store
publishing.sh publishing.sh

View File

@@ -1,5 +1,214 @@
# Changelog # Changelog
## 0.9.5
* `Versions`:
* `Klock`: `2.4.12` -> `2.4.13`
## 0.9.4
* `Pagination`:
* `Common`:
* Add several `optionallyReverse` functions
* `Common`:
* Changes in `Either`:
* Now `Either` uses `optionalT1` and `optionalT2` as main properties
* `Either#t1` and `Either#t2` are deprecated
* New extensions `Either#mapOnFirst` and `Either#mapOnSecond`
## 0.9.3
* `Versions`:
* `UUID`: `0.3.1` -> `0.4.0`
## 0.9.2
* `Versions`:
* `Klock`: `2.4.10` -> `2.4.12`
## 0.9.1
* `Repos`:
* `Exposed`:
* Default realizations of standard interfaces for exposed DB are using public fields for now:
* `ExposedReadKeyValueRepo`
* `ExposedReadOneToManyKeyValueRepo`
* `ExposedStandardVersionsRepoProxy`
* New typealiases for one to many exposed realizations:
* `ExposedReadKeyValuesRepo`
* `ExposedKeyValuesRepo`
## 0.9.0
* `Versions`:
* `Kotlin`: `1.5.31` -> `1.6.10`
* `Coroutines`: `1.5.2` -> `1.6.0`
* `Serialization`: `1.3.1` -> `1.3.2`
* `Exposed`: `0.36.2` -> `0.37.2`
* `Ktor`: `1.6.5` -> `1.6.7`
* `Klock`: `2.4.8` -> `2.4.10`
## 0.8.9
* `Ktor`:
* `Server`:
* Fixes in `uniloadMultipart`
* `Client`:
* Fixes in `unimultipart`
* `FSM`:
* Fixes in `DefaultUpdatableStatesMachine`
## 0.8.8
* `Versions`:
* `AppCompat`: `1.3.1` -> `1.4.0`
* Android Compile SDK: `31.0.0` -> `32.0.0`
* `FSM`:
* `DefaultStatesMachine` now is extendable
* New type `UpdatableStatesMachine` with default realization`DefaultUpdatableStatesMachine`
## 0.8.7
* `Ktor`:
* `Client`:
* `UnifiedRequester` now have no private fields
* Add preview work with multipart
* `Server`
* `UnifiedRouter` now have no private fields
* Add preview work with multipart
## 0.8.6
* `Common`:
* `Either` extensions `onFirst` and `onSecond` now accept not `crossinline` callbacks
* All `joinTo` now accept not `crossinline` callbacks
## 0.8.5
* `Common`:
* `repeatOnFailure`
## 0.8.4
* `Ktor`:
* `Server`:
* Several new `createKtorServer`
## 0.8.3
* `Common`:
* Ranges intersection functionality
* New type `Optional`
* `Pagination`:
* `Pagination` now extends `ClosedRange<Int>`
* `Pagination` intersection functionality
## 0.8.2
* `Versions`:
* `Klock`: `2.4.7` -> `2.4.8`
* `Serialization`: `1.3.0` -> `1.3.1`
* `FSM`:
* Now it is possible to pass any `CheckableHandlerHolder` in `FSMBuilder`
* Now `StatesMachine` works with `CheckableHandlerHolder` instead of `CustomizableHandlerHolder`
## 0.8.1
* `Versions`:
* `Exposed`: `0.36.1` -> `0.36.2`
* `Core KTX`: `1.6.0` -> `1.7.0`
## 0.8.0
* `Versions`:
* `Klock`: `2.4.6` -> `2.4.7`
* `Ktor`: `1.6.4` -> `1.6.5`
* `Exposed`: `0.35.3` -> `0.36.1`
* `Common`:
* Type `Either` got its own serializer
* `FSM`:
* `Common`:
* Full rework of FSM:
* Now it is more flexible for checking of handler opportunity to handle state
* Now machine and states managers are type-oriented
* `StateHandlerHolder` has been renamed to `CheckableHandlerHolder`
* Add opportunity for comfortable adding default state handler
## 0.7.4
* `Common`:
* New type `Either`
* `Serialization`:
* `TypedSerializer`
* New factory fun which accept vararg pairs of type and its serializer
* `Repos`:
* `Common` (`Android`):
* `AbstractMutableAndroidCRUDRepo` flows now will have extra buffer capacity instead of reply. It means that
android crud repo _WILL NOT_ send previous events to the
* `Exposed`:
* New parameter `AbstractExposedWriteCRUDRepo#replyCacheInFlows`
* KeyValue realization `ExposedKeyValueRepo` properties `_onNewValue` and `_onValueRemoved` now are available in
inheritors
* `Pagination`:
* `Common`:
* New types `getAllBy*` for current, next and custom paging
## 0.7.3
* `Versions`:
* `Exposed`: `0.35.2` -> `0.35.3`
## 0.7.2
* `Versions`:
* `Klock`: `2.4.5` -> `2.4.6`
## 0.7.1
* `Versions`:
* `Klock`: `2.4.3` -> `2.4.5`
* `Exposed`: `0.35.1` -> `0.35.2`
* `Coroutines`:
* `Common`:
* New `Flow` - `AccumulatorFlow`
* `FSM`:
* `Common`:
* `InMemoryStatesManager` has been replaced
* `StatesMachine` became an interface
* New manager `DefaultStatesManager` with `DefaultStatesManagerRepo` for abstraction of manager and storing of
data info
## 0.7.0
**THIS VERSION HAS MIGRATED FROM KOTLINX DATETIME TO KORLIBS KLOCK. CAREFUL**
* `Versions`
* `kotlinx.datetime` -> `Klock`
## 0.6.0 DO NOT RECOMMENDED
**THIS VERSION HAS MIGRATED FROM KORLIBS KLOCK TO KOTLINX DATETIME. CAREFUL**
**ALL DEPRECATION HAVE BEEN REMOVED**
* `Versions`
* `Klock` -> `kotlinx.datetime`
## 0.5.31
* `Versions`:
* `Klock`: `2.4.2` -> `2.4.3`
* `Ktor`: `1.6.3` -> `1.6.4`
## 0.5.30
* `Versions`:
* `Serialization`: `1.2.2` -> `1.3.0`
## 0.5.29
* `Versions`:
* `Exposed`: `0.34.2` -> `0.35.1`
## 0.5.28 ## 0.5.28
* `Versions`: * `Versions`:

View File

@@ -7,7 +7,7 @@ buildscript {
} }
dependencies { 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-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version" classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"

View File

@@ -1,5 +0,0 @@
package dev.inmo.micro_utils.common
@Deprecated("Redundant", ReplaceWith("coerceIn(min, max)"))
@Suppress("NOTHING_TO_INLINE")
inline fun <T : Comparable<T>> T.clamp(min: T, max: T): T = coerceIn(min, max)

View File

@@ -2,8 +2,6 @@
package dev.inmo.micro_utils.common package dev.inmo.micro_utils.common
import kotlin.jvm.JvmInline
private inline fun <T> getObject( private inline fun <T> getObject(
additional: MutableList<T>, additional: MutableList<T>,
iterator: Iterator<T> iterator: Iterator<T>

View File

@@ -0,0 +1,168 @@
package dev.inmo.micro_utils.common
import kotlinx.serialization.*
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
/**
* Realization of this interface will contains at least one not null - [optionalT1] or [optionalT2]
*
* @see EitherFirst
* @see EitherSecond
* @see Either.Companion.first
* @see Either.Companion.second
* @see Either.onFirst
* @see Either.onSecond
* @see Either.mapOnFirst
* @see Either.mapOnSecond
*/
@Serializable(EitherSerializer::class)
sealed interface Either<T1, T2> {
val optionalT1: Optional<T1>
val optionalT2: Optional<T2>
@Deprecated("Use optionalT1 instead", ReplaceWith("optionalT1"))
val t1: T1?
get() = optionalT1.dataOrNull()
@Deprecated("Use optionalT2 instead", ReplaceWith("optionalT2"))
val t2: T2?
get() = optionalT2.dataOrNull()
companion object {
fun <T1, T2> serializer(
t1Serializer: KSerializer<T1>,
t2Serializer: KSerializer<T2>,
): KSerializer<Either<T1, T2>> = EitherSerializer(t1Serializer, t2Serializer)
}
}
class EitherSerializer<T1, T2>(
t1Serializer: KSerializer<T1>,
t2Serializer: KSerializer<T2>,
) : KSerializer<Either<T1, T2>> {
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
override val descriptor: SerialDescriptor = buildSerialDescriptor(
"TypedSerializer",
SerialKind.CONTEXTUAL
) {
element("type", String.serializer().descriptor)
element("value", ContextualSerializer(Either::class).descriptor)
}
private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer)
private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer)
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
override fun deserialize(decoder: Decoder): Either<T1, T2> {
return decoder.decodeStructure(descriptor) {
var type: String? = null
lateinit var result: Either<T1, T2>
while (true) {
when (val index = decodeElementIndex(descriptor)) {
0 -> type = decodeStringElement(descriptor, 0)
1 -> {
result = when (type) {
"t1" -> decodeSerializableElement(
descriptor,
1,
t1EitherSerializer
)
"t2" -> decodeSerializableElement(
descriptor,
1,
t2EitherSerializer
)
else -> error("Unknown type of either: $type")
}
}
CompositeDecoder.DECODE_DONE -> break
else -> error("Unexpected index: $index")
}
}
result
}
}
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
override fun serialize(encoder: Encoder, value: Either<T1, T2>) {
encoder.encodeStructure(descriptor) {
when (value) {
is EitherFirst -> {
encodeStringElement(descriptor, 0, "t1")
encodeSerializableElement(descriptor, 1, t1EitherSerializer, value)
}
is EitherSecond -> {
encodeStringElement(descriptor, 0, "t2")
encodeSerializableElement(descriptor, 1, t2EitherSerializer, value)
}
}
}
}
}
/**
* This type [Either] will always have not nullable [optionalT1]
*/
@Serializable
data class EitherFirst<T1, T2>(
override val t1: T1
) : Either<T1, T2> {
override val optionalT1: Optional<T1> = t1.optional
override val optionalT2: Optional<T2> = Optional.absent()
}
/**
* This type [Either] will always have not nullable [optionalT2]
*/
@Serializable
data class EitherSecond<T1, T2>(
override val t2: T2
) : Either<T1, T2> {
override val optionalT1: Optional<T1> = Optional.absent()
override val optionalT2: Optional<T2> = t2.optional
}
/**
* @return New instance of [EitherFirst]
*/
inline fun <T1, T2> Either.Companion.first(t1: T1): Either<T1, T2> = EitherFirst(t1)
/**
* @return New instance of [EitherSecond]
*/
inline fun <T1, T2> Either.Companion.second(t2: T2): Either<T1, T2> = EitherSecond(t2)
/**
* Will call [block] in case when [this] is [EitherFirst]
*/
inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(block: (T1) -> Unit): E {
optionalT1.onPresented(block)
return this
}
/**
* Will call [block] in case when [this] is [EitherSecond]
*/
inline fun <T1, T2, E : Either<T1, T2>> E.onSecond(block: (T2) -> Unit): E {
optionalT2.onPresented(block)
return this
}
/**
* @return Result of [block] if [this] is [EitherFirst]
*/
inline fun <T1, R> Either<T1, *>.mapOnFirst(block: (T1) -> R): R? {
return optionalT1.mapOnPresented(block)
}
/**
* @return Result of [block] if [this] is [EitherSecond]
*/
inline fun <T2, R> Either<*, T2>.mapOnSecond(block: (T2) -> R): R? {
return optionalT2.mapOnPresented(block)
}
inline fun <reified T1, reified T2> Any.either() = when (this) {
is T1 -> Either.first<T1, T2>(this)
is T2 -> Either.second<T1, T2>(this)
else -> error("Incorrect type of either argument $this")
}

View File

@@ -1,10 +1,10 @@
package dev.inmo.micro_utils.common package dev.inmo.micro_utils.common
inline fun <I, R> Iterable<I>.joinTo( inline fun <I, R> Iterable<I>.joinTo(
crossinline separatorFun: (I) -> R?, separatorFun: (I) -> R?,
prefix: R? = null, prefix: R? = null,
postfix: R? = null, postfix: R? = null,
crossinline transform: (I) -> R? transform: (I) -> R?
): List<R> { ): List<R> {
val result = mutableListOf<R>() val result = mutableListOf<R>()
val iterator = iterator() val iterator = iterator()
@@ -29,11 +29,11 @@ inline fun <I, R> Iterable<I>.joinTo(
separator: R? = null, separator: R? = null,
prefix: R? = null, prefix: R? = null,
postfix: R? = null, postfix: R? = null,
crossinline transform: (I) -> R? transform: (I) -> R?
): List<R> = joinTo({ separator }, prefix, postfix, transform) ): List<R> = joinTo({ separator }, prefix, postfix, transform)
inline fun <I> Iterable<I>.joinTo( inline fun <I> Iterable<I>.joinTo(
crossinline separatorFun: (I) -> I?, separatorFun: (I) -> I?,
prefix: I? = null, prefix: I? = null,
postfix: I? = null postfix: I? = null
): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it } ): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it }
@@ -45,15 +45,15 @@ inline fun <I> Iterable<I>.joinTo(
): List<I> = joinTo<I>({ separator }, prefix, postfix) ): List<I> = joinTo<I>({ separator }, prefix, postfix)
inline fun <I, reified R> Array<I>.joinTo( inline fun <I, reified R> Array<I>.joinTo(
crossinline separatorFun: (I) -> R?, separatorFun: (I) -> R?,
prefix: R? = null, prefix: R? = null,
postfix: R? = null, postfix: R? = null,
crossinline transform: (I) -> R? transform: (I) -> R?
): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray() ): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray()
inline fun <I, reified R> Array<I>.joinTo( inline fun <I, reified R> Array<I>.joinTo(
separator: R? = null, separator: R? = null,
prefix: R? = null, prefix: R? = null,
postfix: R? = null, postfix: R? = null,
crossinline transform: (I) -> R? transform: (I) -> R?
): Array<R> = asIterable().joinTo(separator, prefix, postfix, transform).toTypedArray() ): Array<R> = asIterable().joinTo(separator, prefix, postfix, transform).toTypedArray()

View File

@@ -23,11 +23,12 @@ value class FileName(val string: String) {
} }
@PreviewFeature
expect class MPPFile expect class MPPFile
expect val MPPFile.filename: FileName expect val MPPFile.filename: FileName
expect val MPPFile.filesize: Long expect val MPPFile.filesize: Long
expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator
expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator
fun MPPFile.bytesSync() = bytesAllocatorSync()
suspend fun MPPFile.bytes() = bytesAllocator() suspend fun MPPFile.bytes() = bytesAllocator()

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -2,10 +2,12 @@ package dev.inmo.micro_utils.common
import org.khronos.webgl.ArrayBuffer import org.khronos.webgl.ArrayBuffer
import org.w3c.dom.ErrorEvent import org.w3c.dom.ErrorEvent
import org.w3c.files.File import org.w3c.files.*
import org.w3c.files.FileReader
import kotlin.js.Promise import kotlin.js.Promise
/**
* @suppress
*/
actual typealias MPPFile = File actual typealias MPPFile = File
fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure -> fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure ->
@@ -21,12 +23,32 @@ fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure ->
reader.readAsArrayBuffer(this) reader.readAsArrayBuffer(this)
} }
fun MPPFile.readBytes(): ByteArray {
val reader = FileReaderSync()
return reader.readAsArrayBuffer(this).toByteArray()
}
private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await() private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await()
/**
* @suppress
*/
actual val MPPFile.filename: FileName actual val MPPFile.filename: FileName
get() = FileName(name) get() = FileName(name)
/**
* @suppress
*/
actual val MPPFile.filesize: Long actual val MPPFile.filesize: Long
get() = size.toLong() 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") @Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
get() = ::dirtyReadBytes get() = ::dirtyReadBytes

View File

@@ -4,12 +4,29 @@ import dev.inmo.micro_utils.coroutines.doInIO
import dev.inmo.micro_utils.coroutines.doOutsideOfCoroutine import dev.inmo.micro_utils.coroutines.doOutsideOfCoroutine
import java.io.File import java.io.File
/**
* @suppress
*/
actual typealias MPPFile = File actual typealias MPPFile = File
/**
* @suppress
*/
actual val MPPFile.filename: FileName actual val MPPFile.filename: FileName
get() = FileName(name) get() = FileName(name)
/**
* @suppress
*/
actual val MPPFile.filesize: Long actual val MPPFile.filesize: Long
get() = length() get() = length()
/**
* @suppress
*/
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
get() = ::readBytes
/**
* @suppress
*/
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
get() = { get() = {
doInIO { doInIO {

View File

@@ -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)
}

View File

@@ -1,3 +1,6 @@
package dev.inmo.micro_utils.crypto package dev.inmo.micro_utils.crypto
/**
* @suppress
*/
actual fun SourceBytes.md5(): MD5 = CryptoJS.MD5(decodeToString()) actual fun SourceBytes.md5(): MD5 = CryptoJS.MD5(decodeToString())

View File

@@ -3,6 +3,9 @@ package dev.inmo.micro_utils.crypto
import java.math.BigInteger import java.math.BigInteger
import java.security.MessageDigest import java.security.MessageDigest
/**
* @suppress
*/
actual fun SourceBytes.md5(): MD5 = BigInteger( actual fun SourceBytes.md5(): MD5 = BigInteger(
1, 1,
MessageDigest.getInstance("MD5").digest(this) MessageDigest.getInstance("MD5").digest(this)

View File

@@ -13,10 +13,10 @@ repositories {
kotlin { kotlin {
jvm() jvm()
js(IR) { // js(IR) {
browser() // browser()
nodejs() // nodejs()
} // }
android {} android {}
sourceSets { sourceSets {
@@ -29,7 +29,7 @@ kotlin {
it != project it != project
&& it.hasProperty("kotlin") && it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") } && it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") } // && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") } && it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") } && it.kotlin.sourceSets.any { it.name.contains("androidMain") }
) { ) {
@@ -38,22 +38,22 @@ kotlin {
} }
} }
} }
jsMain { // jsMain {
dependencies { // dependencies {
implementation kotlin('stdlib') // implementation kotlin('stdlib')
project.parent.subprojects.forEach { // project.parent.subprojects.forEach {
if ( // if (
it != project // it != project
&& it.hasProperty("kotlin") // && it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") } // && it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") } // && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
) { // ) {
api it // api it
} // }
} // }
} // }
} // }
jvmMain { jvmMain {
dependencies { dependencies {
implementation kotlin('stdlib') implementation kotlin('stdlib')
@@ -116,9 +116,9 @@ tasks.dokkaHtml {
sourceRoots.setFrom(findSourcesWithName("commonMain")) sourceRoots.setFrom(findSourcesWithName("commonMain"))
} }
named("jsMain") { // named("jsMain") {
sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain")) // sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain"))
} // }
named("jvmMain") { named("jvmMain") {
sourceRoots.setFrom(findSourcesWithName("jvmMain", "commonMain")) sourceRoots.setFrom(findSourcesWithName("jvmMain", "commonMain"))

View File

@@ -10,6 +10,7 @@ kotlin {
sourceSets { sourceSets {
commonMain { commonMain {
dependencies { dependencies {
api project(":micro_utils.common")
api project(":micro_utils.coroutines") api project(":micro_utils.coroutines")
} }
} }

View File

@@ -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
)

View File

@@ -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) }
}
}

View File

@@ -1,5 +1,12 @@
package dev.inmo.micro_utils.fsm.common 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?
} }

View File

@@ -1,46 +1,120 @@
package dev.inmo.micro_utils.fsm.common 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 dev.inmo.micro_utils.coroutines.*
import kotlinx.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, * Default [StatesMachine] may [startChain] and use inside logic for handling [State]s. By default you may use
handlers: List<StateHandlerHolder<out I>> * [DefaultStatesMachine] or build it with [dev.inmo.micro_utils.fsm.common.dsl.buildFSM]. Implementers MUST NOT start
): State? { * handling until [start] method will be called
return handlers.firstOrNull { it.checkHandleable(state) } ?.run { */
handleState(state) 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, * Default realization of [StatesMachine]. It uses [statesManager] for incapsulation of [State]s storing and contexts
private val handlers: List<StateHandlerHolder<*>> * resolving, and uses [launchStateHandling] for [State] handling.
) : StatesHandler<State> { *
override suspend fun StatesMachine.handleState(state: State): State? = launchStateHandling(state, handlers) * This class suppose to be extended in case you wish some custom behaviour inside of [launchStateHandling], for example
*/
open class DefaultStatesMachine <T: State>(
protected val statesManager: StatesManager<T>,
protected val handlers: List<CheckableHandlerHolder<in T, T>>,
) : StatesMachine<T> {
/**
* Will call [launchStateHandling] for state handling
*/
override suspend fun StatesMachine<in T>.handleState(state: T): T? = launchStateHandling(state, handlers)
fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions { /**
val statePerformer: suspend (State) -> Unit = { state: State -> * This
val newState = launchStateHandling(state, handlers) */
if (newState != null) { protected val statesJobs = mutableMapOf<T, Job>()
statesManager.update(state, newState) protected val statesJobsMutex = Mutex()
} else {
statesManager.endChain(state) 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) { statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) {
launch { statePerformer(it) } launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
} }
statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) { statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) {
launch { statePerformer(it.second) } launch { performStateUpdate(Optional.presented(it.first), it.second, scope.LinkedSupervisorScope()) }
} }
statesManager.getActiveStates().forEach { 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) statesManager.startChain(state)
} }
} }

View File

@@ -1,92 +1,30 @@
package dev.inmo.micro_utils.fsm.common package dev.inmo.micro_utils.fsm.common
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
interface StatesManager { interface StatesManager<T : State> {
val onChainStateUpdated: Flow<Pair<State, State>> val onChainStateUpdated: Flow<Pair<T, T>>
val onStartChain: Flow<State> val onStartChain: Flow<T>
val onEndChain: Flow<State> val onEndChain: Flow<T>
/** /**
* Must set current set using [State.context] * 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 * Starts chain with [state] as first [State]. May returns false in case of [State.context] of [state] is already
* busy by the other [State] * 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 * Ends chain with context from [state]. In case when [State.context] of [state] is absent, [state] should be just
* ignored * 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()
}

View File

@@ -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)
}
}

View File

@@ -1,35 +1,61 @@
package dev.inmo.micro_utils.fsm.common.dsl package dev.inmo.micro_utils.fsm.common.dsl
import dev.inmo.micro_utils.fsm.common.* 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 import kotlin.reflect.KClass
class FSMBuilder( class FSMBuilder<T : State>(
var statesManager: StatesManager = InMemoryStatesManager() 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>) { fun add(handler: CheckableHandlerHolder<T, T>) {
states.add(StateHandlerHolder(kClass, false, handler)) states.add(handler)
} }
fun <I : State> addStrict(kClass: KClass<I>, handler: StatesHandler<I>) { fun <I : T> add(kClass: KClass<I>, handler: StatesHandler<I, T>) {
states.add(StateHandlerHolder(kClass, true, handler)) 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, statesManager,
states.toList() states.toList().let { list ->
defaultStateHandler ?.let { list + it.holder { true } } ?: list
}
) )
} }
inline fun <reified I : State> FSMBuilder.onStateOrSubstate(handler: StatesHandler<I>) { fun <T : State> buildFSM(
add(I::class, handler) block: FSMBuilder<T>.() -> Unit
} ): StatesMachine<T> = FSMBuilder<T>().apply(block).build()
inline fun <reified I : State> FSMBuilder.strictlyOn(handler: StatesHandler<I>) {
addStrict(I::class, handler)
}
fun buildFSM(
block: FSMBuilder.() -> Unit
): StatesMachine = FSMBuilder().apply(block).build()

View File

@@ -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()
}

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -1,6 +1,7 @@
import dev.inmo.micro_utils.fsm.common.* import dev.inmo.micro_utils.fsm.common.*
import dev.inmo.micro_utils.fsm.common.dsl.buildFSM 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.* import kotlinx.coroutines.*
sealed interface TrafficLightState : State { 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> { strictlyOn<GreenCommon> {
delay(1000L) delay(1000L)
YellowCommon(it.context).also(::println) YellowCommon(it.context).also(::println)

View File

@@ -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)
}

View File

@@ -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
)
)

View File

@@ -7,29 +7,29 @@ android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
org.gradle.jvmargs=-Xmx2g org.gradle.jvmargs=-Xmx2g
kotlin_version=1.5.31 kotlin_version=1.6.10
kotlin_coroutines_version=1.5.2 kotlin_coroutines_version=1.6.0
kotlin_serialisation_core_version=1.2.2 kotlin_serialisation_core_version=1.3.2
kotlin_exposed_version=0.34.2 kotlin_exposed_version=0.37.2
ktor_version=1.6.3 ktor_version=1.6.7
klockVersion=2.4.2 klockVersion=2.4.13
github_release_plugin_version=2.2.12 github_release_plugin_version=2.2.12
uuidVersion=0.3.1 uuidVersion=0.4.0
# ANDROID # ANDROID
core_ktx_version=1.6.0 core_ktx_version=1.7.0
androidx_recycler_version=1.2.1 androidx_recycler_version=1.2.1
appcompat_version=1.3.1 appcompat_version=1.4.0
android_minSdkVersion=19 android_minSdkVersion=19
android_compileSdkVersion=30 android_compileSdkVersion=32
android_buildToolsVersion=30.0.3 android_buildToolsVersion=32.0.0
dexcount_version=3.0.0 dexcount_version=3.0.1
junit_version=4.12 junit_version=4.12
test_ext_junit_version=1.1.2 test_ext_junit_version=1.1.2
espresso_core=3.3.0 espresso_core=3.3.0
@@ -40,10 +40,10 @@ crypto_js_version=4.1.1
# Dokka # Dokka
dokka_version=1.5.0 dokka_version=1.6.10
# Project data # Project data
group=dev.inmo group=dev.inmo
version=0.5.28 version=0.9.5
android_code_version=69 android_code_version=95

View File

@@ -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

View File

@@ -1,16 +1,20 @@
package dev.inmo.micro_utils.ktor.client package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.common.filename
import dev.inmo.micro_utils.ktor.common.* import dev.inmo.micro_utils.ktor.common.*
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.request.get import io.ktor.client.request.*
import io.ktor.client.request.post import io.ktor.client.request.forms.*
import io.ktor.http.*
import io.ktor.utils.io.core.ByteReadPacket
import kotlinx.serialization.* import kotlinx.serialization.*
typealias BodyPair<T> = Pair<SerializationStrategy<T>, T> typealias BodyPair<T> = Pair<SerializationStrategy<T>, T>
class UnifiedRequester( class UnifiedRequester(
private val client: HttpClient = HttpClient(), val client: HttpClient = HttpClient(),
private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
) { ) {
suspend fun <ResultType> uniget( suspend fun <ResultType> uniget(
url: String, url: String,
@@ -31,6 +35,54 @@ class UnifiedRequester(
resultDeserializer: DeserializationStrategy<ResultType> resultDeserializer: DeserializationStrategy<ResultType>
) = client.unipost(url, bodyInfo, resultDeserializer, serialFormat) ) = client.unipost(url, bodyInfo, resultDeserializer, serialFormat)
suspend fun <ResultType> unimultipart(
url: String,
filename: String,
inputProvider: InputProvider,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
): ResultType = client.unimultipart(url, filename, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat)
suspend fun <BodyType, ResultType> unimultipart(
url: String,
filename: String,
inputProvider: InputProvider,
otherData: BodyPair<BodyType>,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
): ResultType = client.unimultipart(url, filename, otherData, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat)
suspend fun <ResultType> unimultipart(
url: String,
mppFile: MPPFile,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {}
): ResultType = client.unimultipart(
url, mppFile, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat
)
suspend fun <BodyType, ResultType> unimultipart(
url: String,
mppFile: MPPFile,
otherData: BodyPair<BodyType>,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {}
): ResultType = client.unimultipart(
url, mppFile, otherData, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat
)
fun <T> createStandardWebsocketFlow( fun <T> createStandardWebsocketFlow(
url: String, url: String,
checkReconnection: (Throwable?) -> Boolean = { true }, checkReconnection: (Throwable?) -> Boolean = { true },
@@ -69,3 +121,124 @@ suspend fun <BodyType, ResultType> HttpClient.unipost(
}.let { }.let {
serialFormat.decodeDefault(resultDeserializer, it) serialFormat.decodeDefault(resultDeserializer, it)
} }
suspend fun <ResultType> HttpClient.unimultipart(
url: String,
filename: String,
inputProvider: InputProvider,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
): ResultType = submitFormWithBinaryData<StandardKtorSerialInputData>(
url,
formData = formData {
append(
"bytes",
inputProvider,
Headers.build {
append(HttpHeaders.ContentType, mimetype)
append(HttpHeaders.ContentDisposition, "filename=\"$filename\"")
dataHeadersBuilder()
}
)
additionalParametersBuilder()
}
) {
requestBuilder()
}.let { serialFormat.decodeDefault(resultDeserializer, it) }
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
url: String,
filename: String,
otherData: BodyPair<BodyType>,
inputProvider: InputProvider,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
): ResultType = unimultipart(
url,
filename,
inputProvider,
resultDeserializer,
mimetype,
additionalParametersBuilder = {
val serialized = serialFormat.encodeDefault(otherData.first, otherData.second)
append(
"data",
InputProvider(serialized.size.toLong()) {
ByteReadPacket(serialized)
},
Headers.build {
append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType)
append(HttpHeaders.ContentDisposition, "filename=data.bytes")
dataHeadersBuilder()
}
)
additionalParametersBuilder()
},
dataHeadersBuilder,
requestBuilder,
serialFormat
)
suspend fun <ResultType> HttpClient.unimultipart(
url: String,
mppFile: MPPFile,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
): ResultType = unimultipart(
url,
mppFile.filename.string,
mppFile.inputProvider(),
resultDeserializer,
mimetype,
additionalParametersBuilder,
dataHeadersBuilder,
requestBuilder,
serialFormat
)
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
url: String,
mppFile: MPPFile,
otherData: BodyPair<BodyType>,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
): ResultType = unimultipart(
url,
mppFile,
resultDeserializer,
mimetype,
additionalParametersBuilder = {
val serialized = serialFormat.encodeDefault(otherData.first, otherData.second)
append(
"data",
InputProvider(serialized.size.toLong()) {
ByteReadPacket(serialized)
},
Headers.build {
append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType)
append(HttpHeaders.ContentDisposition, "filename=data.bytes")
dataHeadersBuilder()
}
)
additionalParametersBuilder()
},
dataHeadersBuilder,
requestBuilder,
serialFormat
)

View File

@@ -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)
}
}

View File

@@ -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()
}

View File

@@ -10,6 +10,7 @@ kotlin {
sourceSets { sourceSets {
commonMain { commonMain {
dependencies { dependencies {
api internalProject("micro_utils.common")
api "org.jetbrains.kotlinx:kotlinx-serialization-cbor:$kotlin_serialisation_core_version" api "org.jetbrains.kotlinx:kotlinx-serialization-cbor:$kotlin_serialisation_core_version"
api "com.soywiz.korlibs.klock:klock:$klockVersion" api "com.soywiz.korlibs.klock:klock:$klockVersion"
} }

View File

@@ -1,22 +1,31 @@
package dev.inmo.micro_utils.ktor.server package dev.inmo.micro_utils.ktor.server
import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.coroutines.safely import dev.inmo.micro_utils.coroutines.safely
import dev.inmo.micro_utils.ktor.common.* import dev.inmo.micro_utils.ktor.common.*
import io.ktor.application.ApplicationCall import io.ktor.application.ApplicationCall
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.http.content.PartData
import io.ktor.http.content.forEachPart
import io.ktor.request.receive import io.ktor.request.receive
import io.ktor.request.receiveMultipart
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.response.respondBytes import io.ktor.response.respondBytes
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.util.asStream
import io.ktor.util.cio.writeChannel
import io.ktor.util.pipeline.PipelineContext import io.ktor.util.pipeline.PipelineContext
import io.ktor.utils.io.core.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.* import kotlinx.serialization.*
import java.io.File
import java.io.File.createTempFile
class UnifiedRouter( class UnifiedRouter(
private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat, val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
private val serialFormatContentType: ContentType = standardKtorSerialFormatContentType val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
) { ) {
fun <T> Route.includeWebsocketHandling( fun <T> Route.includeWebsocketHandling(
suburl: String, suburl: String,
@@ -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( suspend fun ApplicationCall.getParameterOrSendError(
field: String field: String
) = parameters[field].also { ) = parameters[field].also {

View File

@@ -1,5 +1,6 @@
package dev.inmo.micro_utils.ktor.server package dev.inmo.micro_utils.ktor.server
import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.server.cio.CIO import io.ktor.server.cio.CIO
import io.ktor.server.engine.* import io.ktor.server.engine.*
@@ -31,3 +32,27 @@ fun createKtorServer(
port: Int = Random.nextInt(1024, 65535), port: Int = Random.nextInt(1024, 65535),
block: Application.() -> Unit block: Application.() -> Unit
): ApplicationEngine = createKtorServer(CIO, host, port, block) ): ApplicationEngine = createKtorServer(CIO, host, port, block)
fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration> createKtorServer(
engine: ApplicationEngineFactory<TEngine, TConfiguration>,
host: String = "localhost",
port: Int = Random.nextInt(1024, 65535),
configurators: List<KtorApplicationConfigurator>
): TEngine = createKtorServer(
engine,
host,
port
) {
configurators.forEach { it.apply { configure() } }
}
/**
* Create server with [CIO] server engine without starting of it
*
* @see ApplicationEngine.start
*/
fun createKtorServer(
host: String = "localhost",
port: Int = Random.nextInt(1024, 65535),
configurators: List<KtorApplicationConfigurator>
): ApplicationEngine = createKtorServer(CIO, host, port, configurators)

View File

@@ -24,3 +24,8 @@ dependencies {
} }
mainClassName="MainKt" mainClassName="MainKt"
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

View File

@@ -1,3 +1,5 @@
@file:Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
package dev.inmo.micro_utils.language_codes package dev.inmo.micro_utils.language_codes
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View 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)

View File

@@ -1,3 +1,5 @@
@file:Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
package dev.inmo.micro_utils.mime_types package dev.inmo.micro_utils.mime_types
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View File

@@ -26,7 +26,6 @@ kotlin {
apply from: "$defaultAndroidSettingsPresetPath" apply from: "$defaultAndroidSettingsPresetPath"
java { java {
toolchain { sourceCompatibility = JavaVersion.VERSION_1_8
languageVersion = JavaLanguageVersion.of(8) targetCompatibility = JavaVersion.VERSION_1_8
}
} }

View File

@@ -4,7 +4,13 @@ project.group = "$group"
apply from: "$publishGradlePath" apply from: "$publishGradlePath"
kotlin { kotlin {
jvm() jvm {
compilations.main {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
sourceSets { sourceSets {
commonMain { commonMain {
@@ -28,7 +34,6 @@ kotlin {
} }
java { java {
toolchain { sourceCompatibility = JavaVersion.VERSION_1_8
languageVersion = JavaLanguageVersion.of(8) targetCompatibility = JavaVersion.VERSION_1_8
}
} }

View File

@@ -4,7 +4,13 @@ project.group = "$group"
apply from: "$publishGradlePath" apply from: "$publishGradlePath"
kotlin { kotlin {
jvm() jvm {
compilations.main {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
js (IR) { js (IR) {
browser() browser()
nodejs() nodejs()
@@ -50,7 +56,6 @@ kotlin {
apply from: "$defaultAndroidSettingsPresetPath" apply from: "$defaultAndroidSettingsPresetPath"
java { java {
toolchain { sourceCompatibility = JavaVersion.VERSION_1_8
languageVersion = JavaLanguageVersion.of(8) targetCompatibility = JavaVersion.VERSION_1_8
}
} }

View File

@@ -5,3 +5,13 @@ plugins {
} }
apply from: "$mppProjectWithSerializationPresetPath" apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":micro_utils.common")
}
}
}
}

View File

@@ -1,5 +1,6 @@
package dev.inmo.micro_utils.pagination package dev.inmo.micro_utils.pagination
import dev.inmo.micro_utils.common.intersect
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.floor 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 * If you want to request something, you should use [SimplePagination]. If you need to return some result including
* pagination - [PaginationResult] * pagination - [PaginationResult]
*/ */
interface Pagination { interface Pagination : ClosedRange<Int> {
/** /**
* Started with 0. * Started with 0.
* Number of page inside of pagination. Offset can be calculated as [page] * [size] * 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] * Size of current page. Offset can be calculated as [page] * [size]
*/ */
val size: Int 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 * First number in index of objects. It can be used as offset for databases or other data sources
*/ */
val Pagination.firstIndex: Int 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: * 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 * you will retrieve [Pagination.firstIndex] == 10 and [Pagination.lastIndex] == 19. Here [Pagination.lastIndexExclusive] == 20
*/ */
val Pagination.lastIndexExclusive: Int 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: * 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. * you will retrieve [Pagination.firstIndex] == 10 and [Pagination.lastIndex] == 19.
*/ */
val Pagination.lastIndex: Int val Pagination.lastIndex: Int
get() = lastIndexExclusive - 1 get() = endInclusive
/** /**
* Calculates pages count for given [datasetSize] * Calculates pages count for given [datasetSize]

View File

@@ -16,6 +16,16 @@ suspend fun <T> getAll(
return results.toList() 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( suspend fun <T> getAllWithNextPaging(
initialPagination: Pagination = FirstPagePagination(), initialPagination: Pagination = FirstPagePagination(),
block: suspend (Pagination) -> PaginationResult<T> block: suspend (Pagination) -> PaginationResult<T>
@@ -25,6 +35,14 @@ suspend fun <T> getAllWithNextPaging(
block 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( suspend fun <T> getAllWithCurrentPaging(
initialPagination: Pagination = FirstPagePagination(), initialPagination: Pagination = FirstPagePagination(),
block: suspend (Pagination) -> PaginationResult<T> block: suspend (Pagination) -> PaginationResult<T>
@@ -33,3 +51,11 @@ suspend fun <T> getAllWithCurrentPaging(
{ it.currentPageIfNotEmpty() }, { it.currentPageIfNotEmpty() },
block block
) )
suspend fun <T, R> R.getAllByWithCurrentPaging(
initialPagination: Pagination = FirstPagePagination(),
block: suspend R.(Pagination) -> PaginationResult<T>
): List<T> = getAllWithCurrentPaging(
initialPagination,
{ block(it) }
)

View File

@@ -38,3 +38,31 @@ fun <T> Set<T>.paginate(with: Pagination): PaginationResult<T> {
size.toLong() size.toLong()
) )
} }
fun <T> Iterable<T>.optionallyReverse(reverse: Boolean): Iterable<T> = when (this) {
is List<T> -> optionallyReverse(reverse)
is Set<T> -> optionallyReverse(reverse)
else -> if (reverse) {
reversed()
} else {
this
}
}
fun <T> List<T>.optionallyReverse(reverse: Boolean): List<T> = if (reverse) {
reversed()
} else {
this
}
fun <T> Set<T>.optionallyReverse(reverse: Boolean): Set<T> = if (reverse) {
reversed().toSet()
} else {
this
}
inline fun <reified T> Array<T>.optionallyReverse(reverse: Boolean) = if (reverse) {
Array(size) {
get(lastIndex - it)
}
} else {
this
}

View File

@@ -26,3 +26,15 @@ fun Pagination.reverse(datasetSize: Long): SimplePagination {
* Shortcut for [reverse] * Shortcut for [reverse]
*/ */
fun Pagination.reverse(objectsCount: Int) = reverse(objectsCount.toLong()) fun Pagination.reverse(objectsCount: Int) = reverse(objectsCount.toLong())
fun Pagination.optionallyReverse(objectsCount: Int, reverse: Boolean) = if (reverse) {
reverse(objectsCount)
} else {
this
}
fun Pagination.optionallyReverse(objectsCount: Long, reverse: Boolean) = if (reverse) {
reverse(objectsCount)
} else {
this
}

View File

@@ -1,5 +1,4 @@
apply plugin: 'maven-publish' apply plugin: 'maven-publish'
apply plugin: 'signing'
task javadocsJar(type: Jar) { task javadocsJar(type: Jar) {
classifier = 'javadoc' classifier = 'javadoc'
@@ -70,7 +69,18 @@ publishing {
} }
} }
signing { if (project.hasProperty("signing.gnupg.keyName")) {
useGpgCmd() apply plugin: 'signing'
sign publishing.publications
signing {
useGpgCmd()
sign publishing.publications
}
task signAll {
tasks.withType(Sign).forEach {
dependsOn(it)
}
}
} }

View File

@@ -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"}}}

View File

@@ -6,13 +6,15 @@ import dev.inmo.micro_utils.repos.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType>( abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType>(
helper: StandardSQLHelper helper: StandardSQLHelper,
replyInFlows: Int = 0,
extraBufferCapacityInFlows: Int = 64
) : WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>, ) : WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>,
AbstractAndroidCRUDRepo<ObjectType, IdType>(helper), AbstractAndroidCRUDRepo<ObjectType, IdType>(helper),
StandardCRUDRepo<ObjectType, IdType, InputValueType> { StandardCRUDRepo<ObjectType, IdType, InputValueType> {
protected val newObjectsChannel = MutableSharedFlow<ObjectType>(64) protected val newObjectsChannel = MutableSharedFlow<ObjectType>(replyInFlows, extraBufferCapacityInFlows)
protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(64) protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(replyInFlows, extraBufferCapacityInFlows)
protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(64) protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(replyInFlows, extraBufferCapacityInFlows)
override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asSharedFlow() override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asSharedFlow()
override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow() override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow()
override val deletedObjectsIdsFlow: Flow<IdType> = deleteObjectsIdsChannel.asSharedFlow() override val deletedObjectsIdsFlow: Flow<IdType> = deleteObjectsIdsChannel.asSharedFlow()

View File

@@ -10,15 +10,16 @@ import org.jetbrains.exposed.sql.transactions.transaction
abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>( abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
flowsChannelsSize: Int = 0, flowsChannelsSize: Int = 0,
tableName: String = "" tableName: String = "",
replyCacheInFlows: Int = 0
) : ) :
AbstractExposedReadCRUDRepo<ObjectType, IdType>(tableName), AbstractExposedReadCRUDRepo<ObjectType, IdType>(tableName),
ExposedCRUDRepo<ObjectType, IdType>, ExposedCRUDRepo<ObjectType, IdType>,
WriteStandardCRUDRepo<ObjectType, IdType, InputValueType> WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>
{ {
protected val newObjectsChannel = MutableSharedFlow<ObjectType>(flowsChannelsSize) protected val newObjectsChannel = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize)
protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(flowsChannelsSize) protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize)
protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(flowsChannelsSize) protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(replyCacheInFlows, flowsChannelsSize)
override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asSharedFlow() override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asSharedFlow()
override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow() override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow()

View File

@@ -19,8 +19,8 @@ open class ExposedKeyValueRepo<Key, Value>(
valueColumnAllocator, valueColumnAllocator,
tableName tableName
) { ) {
private val _onNewValue = MutableSharedFlow<Pair<Key, Value>>() protected val _onNewValue = MutableSharedFlow<Pair<Key, Value>>()
private val _onValueRemoved = MutableSharedFlow<Key>() protected val _onValueRemoved = MutableSharedFlow<Key>()
override val onNewValue: Flow<Pair<Key, Value>> = _onNewValue.asSharedFlow() override val onNewValue: Flow<Pair<Key, Value>> = _onNewValue.asSharedFlow()
override val onValueRemoved: Flow<Key> = _onValueRemoved.asSharedFlow() override val onValueRemoved: Flow<Key> = _onValueRemoved.asSharedFlow()

View File

@@ -12,8 +12,8 @@ open class ExposedReadKeyValueRepo<Key, Value>(
valueColumnAllocator: ColumnAllocator<Value>, valueColumnAllocator: ColumnAllocator<Value>,
tableName: String? = null tableName: String? = null
) : ReadStandardKeyValueRepo<Key, Value>, ExposedRepo, Table(tableName ?: "") { ) : ReadStandardKeyValueRepo<Key, Value>, ExposedRepo, Table(tableName ?: "") {
protected val keyColumn: Column<Key> = keyColumnAllocator() val keyColumn: Column<Key> = keyColumnAllocator()
protected val valueColumn: Column<Value> = valueColumnAllocator() val valueColumn: Column<Value> = valueColumnAllocator()
override val primaryKey: PrimaryKey = PrimaryKey(keyColumn, valueColumn) override val primaryKey: PrimaryKey = PrimaryKey(keyColumn, valueColumn)
init { initTable() } init { initTable() }

View File

@@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.*
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
typealias ExposedKeyValuesRepo<Key, Value> = ExposedOneToManyKeyValueRepo<Key, Value>
open class ExposedOneToManyKeyValueRepo<Key, Value>( open class ExposedOneToManyKeyValueRepo<Key, Value>(
database: Database, database: Database,
keyColumnAllocator: ColumnAllocator<Key>, keyColumnAllocator: ColumnAllocator<Key>,

View File

@@ -3,17 +3,20 @@ package dev.inmo.micro_utils.repos.exposed.onetomany
import dev.inmo.micro_utils.pagination.* import dev.inmo.micro_utils.pagination.*
import dev.inmo.micro_utils.repos.ReadOneToManyKeyValueRepo import dev.inmo.micro_utils.repos.ReadOneToManyKeyValueRepo
import dev.inmo.micro_utils.repos.exposed.* import dev.inmo.micro_utils.repos.exposed.*
import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedReadKeyValueRepo
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
typealias ExposedReadKeyValuesRepo<Key, Value> = ExposedReadOneToManyKeyValueRepo<Key, Value>
open class ExposedReadOneToManyKeyValueRepo<Key, Value>( open class ExposedReadOneToManyKeyValueRepo<Key, Value>(
override val database: Database, override val database: Database,
keyColumnAllocator: ColumnAllocator<Key>, keyColumnAllocator: ColumnAllocator<Key>,
valueColumnAllocator: ColumnAllocator<Value>, valueColumnAllocator: ColumnAllocator<Value>,
tableName: String? = null tableName: String? = null
) : ReadOneToManyKeyValueRepo<Key, Value>, ExposedRepo, Table(tableName ?: "") { ) : ReadOneToManyKeyValueRepo<Key, Value>, ExposedRepo, Table(tableName ?: "") {
protected val keyColumn: Column<Key> = keyColumnAllocator() val keyColumn: Column<Key> = keyColumnAllocator()
protected val valueColumn: Column<Value> = valueColumnAllocator() val valueColumn: Column<Value> = valueColumnAllocator()
init { initTable() } init { initTable() }

View File

@@ -18,8 +18,8 @@ inline fun versionsRepo(database: Database): VersionsRepo<Database> = StandardVe
class ExposedStandardVersionsRepoProxy( class ExposedStandardVersionsRepoProxy(
override val database: Database override val database: Database
) : StandardVersionsRepoProxy<Database>, Table("ExposedVersionsProxy"), ExposedRepo { ) : StandardVersionsRepoProxy<Database>, Table("ExposedVersionsProxy"), ExposedRepo {
private val tableNameColumn = text("tableName") val tableNameColumn = text("tableName")
private val tableVersionColumn = integer("tableName") val tableVersionColumn = integer("tableName")
init { init {
initTable() initTable()

View File

@@ -11,8 +11,7 @@ open class TypedSerializer<T : Any>(
presetSerializers: Map<String, KSerializer<out T>> = emptyMap(), presetSerializers: Map<String, KSerializer<out T>> = emptyMap(),
) : KSerializer<T> { ) : KSerializer<T> {
protected val serializers = presetSerializers.toMutableMap() protected val serializers = presetSerializers.toMutableMap()
@ExperimentalSerializationApi @OptIn(InternalSerializationApi::class)
@InternalSerializationApi
override val descriptor: SerialDescriptor = buildSerialDescriptor( override val descriptor: SerialDescriptor = buildSerialDescriptor(
"TypedSerializer", "TypedSerializer",
SerialKind.CONTEXTUAL SerialKind.CONTEXTUAL
@@ -20,21 +19,8 @@ open class TypedSerializer<T : Any>(
element("type", String.serializer().descriptor) element("type", String.serializer().descriptor)
element("value", ContextualSerializer(kClass).descriptor) element("value", ContextualSerializer(kClass).descriptor)
} }
@InternalSerializationApi
@Deprecated(
"This descriptor was deprecated due to incorrect serial name. You may use it in case something require it, " +
"but it is strongly recommended to migrate onto new descriptor"
)
protected val oldDescriptor: SerialDescriptor = buildSerialDescriptor(
"TextSourceSerializer",
SerialKind.CONTEXTUAL
) {
element("type", String.serializer().descriptor)
element("value", ContextualSerializer(kClass).descriptor)
}
@ExperimentalSerializationApi @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
@InternalSerializationApi
override fun deserialize(decoder: Decoder): T { override fun deserialize(decoder: Decoder): T {
return decoder.decodeStructure(descriptor) { return decoder.decodeStructure(descriptor) {
var type: String? = null var type: String? = null
@@ -58,14 +44,12 @@ open class TypedSerializer<T : Any>(
} }
} }
@ExperimentalSerializationApi @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
@InternalSerializationApi
protected open fun <O: T> CompositeEncoder.encode(value: O) { protected open fun <O: T> CompositeEncoder.encode(value: O) {
encodeSerializableElement(descriptor, 1, value::class.serializer() as KSerializer<O>, value) encodeSerializableElement(descriptor, 1, value::class.serializer() as KSerializer<O>, value)
} }
@ExperimentalSerializationApi @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
@InternalSerializationApi
override fun serialize(encoder: Encoder, value: T) { override fun serialize(encoder: Encoder, value: T) {
encoder.encodeStructure(descriptor) { encoder.encodeStructure(descriptor) {
val valueSerializer = value::class.serializer() val valueSerializer = value::class.serializer()
@@ -98,3 +82,7 @@ operator fun <T : Any> TypedSerializer<T>.minusAssign(kClass: KClass<T>) {
inline fun <reified T : Any> TypedSerializer( inline fun <reified T : Any> TypedSerializer(
presetSerializers: Map<String, KSerializer<out T>> = emptyMap() presetSerializers: Map<String, KSerializer<out T>> = emptyMap()
) = TypedSerializer(T::class, presetSerializers) ) = TypedSerializer(T::class, presetSerializers)
inline fun <reified T : Any> TypedSerializer(
vararg presetSerializers: Pair<String, KSerializer<out T>>
) = TypedSerializer(presetSerializers.toMap())