mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 22:39:25 +00:00
Compare commits
108 Commits
Author | SHA1 | Date | |
---|---|---|---|
00cc26d874 | |||
02520636ad | |||
76813fae8e | |||
4693483c2b | |||
6dbd12df59 | |||
9e84dc5031 | |||
8f790360bc | |||
808375cea6 | |||
d9c15db9de | |||
cf2be8ed43 | |||
97339c9b1d | |||
eb35903de9 | |||
43e88e00da | |||
1e51f9d77b | |||
dad738357c | |||
cb828ab3f2 | |||
aefaf4a8cc | |||
b29f37a251 | |||
88854020ac | |||
14ebe01fc6 | |||
771aed0f0f | |||
16c57fcd6a | |||
75397a7ccb | |||
b05404f828 | |||
edea942874 | |||
8a8f568b9a | |||
4dc8d30c52 | |||
cb4e08e823 | |||
c443bf4fa0 | |||
a6982de822 | |||
4f1a663e75 | |||
dab262d626 | |||
87a3f61ca6 | |||
506e937a68 | |||
5a037c76dd | |||
313f622f7e | |||
6cba1fe1a2 | |||
fd2d0e80b7 | |||
96ab2e8aca | |||
0202988cae | |||
d619d59947 | |||
85b3e48d18 | |||
7a9b7d98a1 | |||
b212acfcaf | |||
3a45e5dc70 | |||
73190518d5 | |||
03f78180dc | |||
1c0b8cf842 | |||
a1624ea2a9 | |||
23a050cf1e | |||
916f2f96f4 | |||
00cc214754 | |||
b2e38f72b9 | |||
e7107d238d | |||
ed9ebdbd1a | |||
e80676d3d2 | |||
02d02fa8f2 | |||
bd783fb74f | |||
50386adf70 | |||
f4ee6c2890 | |||
d45aef9fe5 | |||
a56cd3dddd | |||
419e7070ee | |||
612cf40b5f | |||
8b39882e83 | |||
e639ae172b | |||
d0446850ae | |||
c48465b90b | |||
f419fd03d2 | |||
494812a660 | |||
eb78f21eec | |||
4bda70268b | |||
f037ce4371 | |||
3d2196e35d | |||
a74f061b02 | |||
11ade14676 | |||
eb562d8784 | |||
1ee5b4bfd4 | |||
d97892080b | |||
6f37125724 | |||
ed1baaade7 | |||
bb9669f8fd | |||
bdac715d48 | |||
acf4971298 | |||
249bc83a8c | |||
0fbb92f03f | |||
ca27cb3f82 | |||
3a5771a0cc | |||
527a2a91ac | |||
6763e5c4c6 | |||
06918d8310 | |||
89ccaa1b57 | |||
5d0bdb9bcf | |||
31fdcf74a5 | |||
afca09cc1d | |||
531d89d9db | |||
6bbbea0bc3 | |||
e337cd98c8 | |||
bcbab3b380 | |||
fb63de7568 | |||
aa45a4ab13 | |||
2af7e2f681 | |||
34fd9edce0 | |||
2a4cb8c5f9 | |||
50ea40bc3a | |||
a77654052d | |||
88aafce552 | |||
4e95d6bfff |
6
.github/workflows/dokka_push.yml
vendored
6
.github/workflows/dokka_push.yml
vendored
@@ -10,10 +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 31.0.0 dx
|
- name: Fix android 32.0.0 dx
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: cd /usr/local/lib/android/sdk/build-tools/31.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
|
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./gradlew dokkaHtml
|
run: ./gradlew dokkaHtml
|
||||||
- name: Publish KDocs
|
- name: Publish KDocs
|
||||||
|
8
.github/workflows/packages_push.yml
vendored
8
.github/workflows/packages_push.yml
vendored
@@ -8,10 +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 31.0.0 dx
|
- name: Fix android 32.0.0 dx
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: cd /usr/local/lib/android/sdk/build-tools/31.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
|
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 "[^/]*$"`"
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
run: ./gradlew build
|
run: ./gradlew build
|
||||||
- name: Publish
|
- name: Publish
|
||||||
continue-on-error: true
|
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
|
||||||
env:
|
env:
|
||||||
GITHUBPACKAGES_USER: ${{ github.actor }}
|
GITHUBPACKAGES_USER: ${{ github.actor }}
|
||||||
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,5 +11,6 @@ out/
|
|||||||
|
|
||||||
secret.gradle
|
secret.gradle
|
||||||
local.properties
|
local.properties
|
||||||
|
kotlin-js-store
|
||||||
|
|
||||||
publishing.sh
|
publishing.sh
|
||||||
|
188
CHANGELOG.md
188
CHANGELOG.md
@@ -1,5 +1,193 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.9.12
|
||||||
|
|
||||||
|
* `Common`:
|
||||||
|
* `JS`:
|
||||||
|
* New function `openLink`
|
||||||
|
* New function `selectFile`
|
||||||
|
* New function `triggerDownloadFile`
|
||||||
|
* `Compose`:
|
||||||
|
* Created :)
|
||||||
|
* `Common`:
|
||||||
|
* `DefaultDisposableEffectResult` as a default realization of `DisposableEffectResult`
|
||||||
|
* `JS`:
|
||||||
|
* `openLink` on top of `openLink` with `String` target from common
|
||||||
|
* `Coroutines`:
|
||||||
|
* `Compose`:
|
||||||
|
* `Common`:
|
||||||
|
* New extension `Flow.toMutableState`
|
||||||
|
* New extension `StateFlow.toMutableState`
|
||||||
|
* `JS`:
|
||||||
|
* New function `selectFileOrThrow` on top of `selectFile` from `common`
|
||||||
|
* New function `selectFileOrNull` on top of `selectFile` from `common`
|
||||||
|
|
||||||
|
## 0.9.11
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Klock`: `2.6.1` -> `2.6.2`
|
||||||
|
* `Coroutines`:
|
||||||
|
* `Compose`:
|
||||||
|
* Created :)
|
||||||
|
* New extensions and function:
|
||||||
|
* `Composition#linkWithJob`
|
||||||
|
* `Composition#linkWithContext`
|
||||||
|
* `renderComposableAndLinkToContext`
|
||||||
|
|
||||||
|
## 0.9.10
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Klock`: `2.5.2` -> `2.6.1`
|
||||||
|
* Ktor:
|
||||||
|
* Client:
|
||||||
|
* New function `UnifiedRequester#createStandardWebsocketFlow` without `checkReconnection` arg
|
||||||
|
* Server:
|
||||||
|
* Now it is possible to filter data in `Route#includeWebsocketHandling`
|
||||||
|
* Callback in `Route#includeWebsocketHandling` and dependent methods is `suspend` since now
|
||||||
|
* Add `URLProtocol` support in `Route#includeWebsocketHandling` and dependent methods
|
||||||
|
|
||||||
|
## 0.9.9
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Klock`: `2.5.1` -> `2.5.2`
|
||||||
|
* `Common`:
|
||||||
|
* Add new diff tool - `applyDiff`
|
||||||
|
* Implementation of `IntersectionObserver` in JS part (copypaste of [this](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0) comment)
|
||||||
|
|
||||||
|
## 0.9.8
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Exposed`: `0.37.2` -> `0.37.3`
|
||||||
|
* `Klock`: `2.4.13` -> `2.5.1`
|
||||||
|
* `AppCompat`: `1.4.0` -> `1.4.1`
|
||||||
|
|
||||||
|
## 0.9.7
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Exposed`:
|
||||||
|
* Fix in `ExposedOneToManyKeyValueRepo` - now it will not use `insertIgnore`
|
||||||
|
* `Ktor`:
|
||||||
|
* `Server`:
|
||||||
|
* `Route#includeWebsocketHandling` now will check that `WebSockets` feature and install it if not
|
||||||
|
|
||||||
|
## 0.9.6
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Exposed`:
|
||||||
|
* Fix in `ExposedOneToManyKeyValueRepo` - now it will not use `deleteIgnoreWhere`
|
||||||
|
|
||||||
|
## 0.9.5
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Klock`: `2.4.12` -> `2.4.13`
|
||||||
|
|
||||||
|
## 0.9.4
|
||||||
|
|
||||||
|
* `Pagination`:
|
||||||
|
* `Common`:
|
||||||
|
* Add several `optionallyReverse` functions
|
||||||
|
* `Common`:
|
||||||
|
* Changes in `Either`:
|
||||||
|
* Now `Either` uses `optionalT1` and `optionalT2` as main properties
|
||||||
|
* `Either#t1` and `Either#t2` are deprecated
|
||||||
|
* New extensions `Either#mapOnFirst` and `Either#mapOnSecond`
|
||||||
|
|
||||||
|
## 0.9.3
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `UUID`: `0.3.1` -> `0.4.0`
|
||||||
|
|
||||||
|
## 0.9.2
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Klock`: `2.4.10` -> `2.4.12`
|
||||||
|
|
||||||
|
## 0.9.1
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Exposed`:
|
||||||
|
* Default realizations of standard interfaces for exposed DB are using public fields for now:
|
||||||
|
* `ExposedReadKeyValueRepo`
|
||||||
|
* `ExposedReadOneToManyKeyValueRepo`
|
||||||
|
* `ExposedStandardVersionsRepoProxy`
|
||||||
|
* New typealiases for one to many exposed realizations:
|
||||||
|
* `ExposedReadKeyValuesRepo`
|
||||||
|
* `ExposedKeyValuesRepo`
|
||||||
|
|
||||||
|
## 0.9.0
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `1.5.31` -> `1.6.10`
|
||||||
|
* `Coroutines`: `1.5.2` -> `1.6.0`
|
||||||
|
* `Serialization`: `1.3.1` -> `1.3.2`
|
||||||
|
* `Exposed`: `0.36.2` -> `0.37.2`
|
||||||
|
* `Ktor`: `1.6.5` -> `1.6.7`
|
||||||
|
* `Klock`: `2.4.8` -> `2.4.10`
|
||||||
|
|
||||||
|
## 0.8.9
|
||||||
|
|
||||||
|
* `Ktor`:
|
||||||
|
* `Server`:
|
||||||
|
* Fixes in `uniloadMultipart`
|
||||||
|
* `Client`:
|
||||||
|
* Fixes in `unimultipart`
|
||||||
|
* `FSM`:
|
||||||
|
* Fixes in `DefaultUpdatableStatesMachine`
|
||||||
|
|
||||||
|
## 0.8.8
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `AppCompat`: `1.3.1` -> `1.4.0`
|
||||||
|
* Android Compile SDK: `31.0.0` -> `32.0.0`
|
||||||
|
* `FSM`:
|
||||||
|
* `DefaultStatesMachine` now is extendable
|
||||||
|
* New type `UpdatableStatesMachine` with default realization`DefaultUpdatableStatesMachine`
|
||||||
|
|
||||||
|
## 0.8.7
|
||||||
|
|
||||||
|
* `Ktor`:
|
||||||
|
* `Client`:
|
||||||
|
* `UnifiedRequester` now have no private fields
|
||||||
|
* Add preview work with multipart
|
||||||
|
* `Server`
|
||||||
|
* `UnifiedRouter` now have no private fields
|
||||||
|
* Add preview work with multipart
|
||||||
|
|
||||||
|
## 0.8.6
|
||||||
|
|
||||||
|
* `Common`:
|
||||||
|
* `Either` extensions `onFirst` and `onSecond` now accept not `crossinline` callbacks
|
||||||
|
* All `joinTo` now accept not `crossinline` callbacks
|
||||||
|
|
||||||
|
## 0.8.5
|
||||||
|
|
||||||
|
* `Common`:
|
||||||
|
* `repeatOnFailure`
|
||||||
|
|
||||||
|
## 0.8.4
|
||||||
|
|
||||||
|
* `Ktor`:
|
||||||
|
* `Server`:
|
||||||
|
* Several new `createKtorServer`
|
||||||
|
|
||||||
|
## 0.8.3
|
||||||
|
|
||||||
|
* `Common`:
|
||||||
|
* Ranges intersection functionality
|
||||||
|
* New type `Optional`
|
||||||
|
* `Pagination`:
|
||||||
|
* `Pagination` now extends `ClosedRange<Int>`
|
||||||
|
* `Pagination` intersection functionality
|
||||||
|
|
||||||
|
## 0.8.2
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Klock`: `2.4.7` -> `2.4.8`
|
||||||
|
* `Serialization`: `1.3.0` -> `1.3.1`
|
||||||
|
* `FSM`:
|
||||||
|
* Now it is possible to pass any `CheckableHandlerHolder` in `FSMBuilder`
|
||||||
|
* Now `StatesMachine` works with `CheckableHandlerHolder` instead of `CustomizableHandlerHolder`
|
||||||
|
|
||||||
## 0.8.1
|
## 0.8.1
|
||||||
|
|
||||||
* `Versions`:
|
* `Versions`:
|
||||||
|
@@ -10,7 +10,7 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api "androidx.appcompat:appcompat-resources:$appcompat_version"
|
api libs.android.appCompat.resources
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,13 +10,13 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
api libs.kt.coroutines
|
||||||
api project(":micro_utils.common")
|
api project(":micro_utils.common")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api "androidx.recyclerview:recyclerview:$androidx_recycler_version"
|
api libs.android.recyclerView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
build.gradle
12
build.gradle
@@ -7,12 +7,12 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.1.3'
|
classpath libs.buildscript.kt.gradle
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath libs.buildscript.kt.serialization
|
||||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
classpath libs.buildscript.jb.dokka
|
||||||
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
|
classpath libs.buildscript.gh.release
|
||||||
classpath "com.github.breadmoirai:github-release:$github_release_plugin_version"
|
classpath libs.buildscript.android.gradle
|
||||||
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
|
classpath libs.buildscript.android.dexcount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
18
common/compose/build.gradle
Normal file
18
common/compose/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
|
id "com.android.library"
|
||||||
|
alias(libs.plugins.jb.compose)
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api project(":micro_utils.common")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
package dev.inmo.micro_utils.common.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.DisposableEffectResult
|
||||||
|
|
||||||
|
class DefaultDisposableEffectResult(
|
||||||
|
private val onDispose: () -> Unit
|
||||||
|
) : DisposableEffectResult {
|
||||||
|
override fun dispose() {
|
||||||
|
onDispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val DoNothing = DefaultDisposableEffectResult {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@@ -0,0 +1,10 @@
|
|||||||
|
package dev.inmo.micro_utils.common.compose
|
||||||
|
|
||||||
|
import org.jetbrains.compose.web.attributes.ATarget
|
||||||
|
|
||||||
|
fun openLink(link: String, mode: ATarget = ATarget.Blank, features: String = "") = dev.inmo.micro_utils.common.openLink(
|
||||||
|
link,
|
||||||
|
mode.targetStr,
|
||||||
|
features
|
||||||
|
)
|
||||||
|
|
1
common/compose/src/main/AndroidManifest.xml
Normal file
1
common/compose/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest package="dev.inmo.micro_utils.common.compose"/>
|
@@ -153,3 +153,22 @@ inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDif
|
|||||||
inline fun <T> Iterable<T>.calculateStrictDiff(
|
inline fun <T> Iterable<T>.calculateStrictDiff(
|
||||||
other: Iterable<T>
|
other: Iterable<T>
|
||||||
) = calculateDiff(other, strictComparison = true)
|
) = calculateDiff(other, strictComparison = true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this]
|
||||||
|
* mutable list
|
||||||
|
*/
|
||||||
|
fun <T> MutableList<T>.applyDiff(
|
||||||
|
source: Iterable<T>,
|
||||||
|
strictComparison: Boolean = false
|
||||||
|
) = calculateDiff(source, strictComparison).let {
|
||||||
|
for (i in it.removed.indices.sortedDescending()) {
|
||||||
|
removeAt(it.removed[i].index)
|
||||||
|
}
|
||||||
|
it.added.forEach { (i, t) ->
|
||||||
|
add(i, t)
|
||||||
|
}
|
||||||
|
it.replaced.forEach { (_, new) ->
|
||||||
|
set(new.index, new.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -6,7 +6,7 @@ import kotlinx.serialization.descriptors.*
|
|||||||
import kotlinx.serialization.encoding.*
|
import kotlinx.serialization.encoding.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Realization of this interface will contains at least one not null - [t1] or [t2]
|
* Realization of this interface will contains at least one not null - [optionalT1] or [optionalT2]
|
||||||
*
|
*
|
||||||
* @see EitherFirst
|
* @see EitherFirst
|
||||||
* @see EitherSecond
|
* @see EitherSecond
|
||||||
@@ -14,11 +14,19 @@ import kotlinx.serialization.encoding.*
|
|||||||
* @see Either.Companion.second
|
* @see Either.Companion.second
|
||||||
* @see Either.onFirst
|
* @see Either.onFirst
|
||||||
* @see Either.onSecond
|
* @see Either.onSecond
|
||||||
|
* @see Either.mapOnFirst
|
||||||
|
* @see Either.mapOnSecond
|
||||||
*/
|
*/
|
||||||
@Serializable(EitherSerializer::class)
|
@Serializable(EitherSerializer::class)
|
||||||
sealed interface Either<T1, T2> {
|
sealed interface Either<T1, T2> {
|
||||||
|
val optionalT1: Optional<T1>
|
||||||
|
val optionalT2: Optional<T2>
|
||||||
|
@Deprecated("Use optionalT1 instead", ReplaceWith("optionalT1"))
|
||||||
val t1: T1?
|
val t1: T1?
|
||||||
|
get() = optionalT1.dataOrNull()
|
||||||
|
@Deprecated("Use optionalT2 instead", ReplaceWith("optionalT2"))
|
||||||
val t2: T2?
|
val t2: T2?
|
||||||
|
get() = optionalT2.dataOrNull()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun <T1, T2> serializer(
|
fun <T1, T2> serializer(
|
||||||
@@ -32,8 +40,7 @@ class EitherSerializer<T1, T2>(
|
|||||||
t1Serializer: KSerializer<T1>,
|
t1Serializer: KSerializer<T1>,
|
||||||
t2Serializer: KSerializer<T2>,
|
t2Serializer: KSerializer<T2>,
|
||||||
) : KSerializer<Either<T1, T2>> {
|
) : KSerializer<Either<T1, T2>> {
|
||||||
@ExperimentalSerializationApi
|
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||||
@InternalSerializationApi
|
|
||||||
override val descriptor: SerialDescriptor = buildSerialDescriptor(
|
override val descriptor: SerialDescriptor = buildSerialDescriptor(
|
||||||
"TypedSerializer",
|
"TypedSerializer",
|
||||||
SerialKind.CONTEXTUAL
|
SerialKind.CONTEXTUAL
|
||||||
@@ -44,8 +51,7 @@ class EitherSerializer<T1, T2>(
|
|||||||
private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer)
|
private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer)
|
||||||
private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer)
|
private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer)
|
||||||
|
|
||||||
@ExperimentalSerializationApi
|
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||||
@InternalSerializationApi
|
|
||||||
override fun deserialize(decoder: Decoder): Either<T1, T2> {
|
override fun deserialize(decoder: Decoder): Either<T1, T2> {
|
||||||
return decoder.decodeStructure(descriptor) {
|
return decoder.decodeStructure(descriptor) {
|
||||||
var type: String? = null
|
var type: String? = null
|
||||||
@@ -77,8 +83,7 @@ class EitherSerializer<T1, T2>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ExperimentalSerializationApi
|
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||||
@InternalSerializationApi
|
|
||||||
override fun serialize(encoder: Encoder, value: Either<T1, T2>) {
|
override fun serialize(encoder: Encoder, value: Either<T1, T2>) {
|
||||||
encoder.encodeStructure(descriptor) {
|
encoder.encodeStructure(descriptor) {
|
||||||
when (value) {
|
when (value) {
|
||||||
@@ -96,25 +101,25 @@ class EitherSerializer<T1, T2>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This type [Either] will always have not nullable [t1]
|
* This type [Either] will always have not nullable [optionalT1]
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class EitherFirst<T1, T2>(
|
data class EitherFirst<T1, T2>(
|
||||||
override val t1: T1
|
override val t1: T1
|
||||||
) : Either<T1, T2> {
|
) : Either<T1, T2> {
|
||||||
override val t2: T2?
|
override val optionalT1: Optional<T1> = t1.optional
|
||||||
get() = null
|
override val optionalT2: Optional<T2> = Optional.absent()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This type [Either] will always have not nullable [t2]
|
* This type [Either] will always have not nullable [optionalT2]
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class EitherSecond<T1, T2>(
|
data class EitherSecond<T1, T2>(
|
||||||
override val t2: T2
|
override val t2: T2
|
||||||
) : Either<T1, T2> {
|
) : Either<T1, T2> {
|
||||||
override val t1: T1?
|
override val optionalT1: Optional<T1> = Optional.absent()
|
||||||
get() = null
|
override val optionalT2: Optional<T2> = t2.optional
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -127,23 +132,35 @@ inline fun <T1, T2> Either.Companion.first(t1: T1): Either<T1, T2> = EitherFirst
|
|||||||
inline fun <T1, T2> Either.Companion.second(t2: T2): Either<T1, T2> = EitherSecond(t2)
|
inline fun <T1, T2> Either.Companion.second(t2: T2): Either<T1, T2> = EitherSecond(t2)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will call [block] in case when [Either.t1] of [this] is not null
|
* Will call [block] in case when [this] is [EitherFirst]
|
||||||
*/
|
*/
|
||||||
inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(crossinline block: (T1) -> Unit): E {
|
inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(block: (T1) -> Unit): E {
|
||||||
val t1 = t1
|
optionalT1.onPresented(block)
|
||||||
t1 ?.let(block)
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will call [block] in case when [Either.t2] of [this] is not null
|
* Will call [block] in case when [this] is [EitherSecond]
|
||||||
*/
|
*/
|
||||||
inline fun <T1, T2, E : Either<T1, T2>> E.onSecond(crossinline block: (T2) -> Unit): E {
|
inline fun <T1, T2, E : Either<T1, T2>> E.onSecond(block: (T2) -> Unit): E {
|
||||||
val t2 = t2
|
optionalT2.onPresented(block)
|
||||||
t2 ?.let(block)
|
|
||||||
return this
|
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) {
|
inline fun <reified T1, reified T2> Any.either() = when (this) {
|
||||||
is T1 -> Either.first<T1, T2>(this)
|
is T1 -> Either.first<T1, T2>(this)
|
||||||
is T2 -> Either.second<T1, T2>(this)
|
is T2 -> Either.second<T1, T2>(this)
|
||||||
|
@@ -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()
|
||||||
|
@@ -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()
|
||||||
|
|
||||||
|
@@ -0,0 +1,92 @@
|
|||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This type represents [T] as not only potentially nullable data, but also as a data which can not be presented. This
|
||||||
|
* type will be useful in cases when [T] is nullable and null as valuable data too in time of data absence should be
|
||||||
|
* presented by some third type.
|
||||||
|
*
|
||||||
|
* Let's imagine, you have nullable name in some database. In case when name is not nullable everything is clear - null
|
||||||
|
* will represent absence of row in the database. In case when name is nullable null will be a little bit dual-meaning,
|
||||||
|
* cause this null will say nothing about availability of the row (of course, it is exaggerated example)
|
||||||
|
*
|
||||||
|
* @see Optional.presented
|
||||||
|
* @see Optional.absent
|
||||||
|
* @see Optional.optional
|
||||||
|
* @see Optional.onPresented
|
||||||
|
* @see Optional.onAbsent
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class Optional<T> internal constructor(
|
||||||
|
@Warning("It is unsafe to use this data directly")
|
||||||
|
val data: T?,
|
||||||
|
@Warning("It is unsafe to use this data directly")
|
||||||
|
val dataPresented: Boolean
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Will create [Optional] with presented data
|
||||||
|
*/
|
||||||
|
fun <T> presented(data: T) = Optional(data, true)
|
||||||
|
/**
|
||||||
|
* Will create [Optional] without data
|
||||||
|
*/
|
||||||
|
fun <T> absent() = Optional<T>(null, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline val <T> T.optional
|
||||||
|
get() = Optional.presented(this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||||
|
*/
|
||||||
|
inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply {
|
||||||
|
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||||
|
*/
|
||||||
|
inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run {
|
||||||
|
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will call [block] when data absent ([Optional.dataPresented] == false)
|
||||||
|
*/
|
||||||
|
inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply {
|
||||||
|
if (!dataPresented) { block() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||||
|
*/
|
||||||
|
inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run {
|
||||||
|
if (!dataPresented) { @Suppress("UNCHECKED_CAST") block() } else null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise
|
||||||
|
*/
|
||||||
|
fun <T> Optional<T>.dataOrNull() = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or throw [throwable] otherwise
|
||||||
|
*/
|
||||||
|
fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
|
||||||
|
*/
|
||||||
|
inline fun <T> Optional<T>.dataOrElse(block: () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
|
||||||
|
*/
|
||||||
|
@Deprecated("dataOrElse now is inline", ReplaceWith("dataOrElse", "dev.inmo.micro_utils.common.dataOrElse"))
|
||||||
|
suspend fun <T> Optional<T>.dataOrElseSuspendable(block: suspend () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
|
@@ -0,0 +1,19 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
fun <T : Comparable<T>> ClosedRange<T>.intersect(other: ClosedRange<T>): Pair<T, T>? = when {
|
||||||
|
start == other.start && endInclusive == other.endInclusive -> start to endInclusive
|
||||||
|
start > other.endInclusive || other.start > endInclusive -> null
|
||||||
|
else -> maxOf(start, other.start) to minOf(endInclusive, other.endInclusive)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun IntRange.intersect(
|
||||||
|
other: IntRange
|
||||||
|
): IntRange? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let {
|
||||||
|
it.first .. it.second
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LongRange.intersect(
|
||||||
|
other: LongRange
|
||||||
|
): LongRange? = (this as ClosedRange<Long>).intersect(other as ClosedRange<Long>) ?.let {
|
||||||
|
it.first .. it.second
|
||||||
|
}
|
@@ -0,0 +1,21 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the given [action] until getting of successful result specified number of [times].
|
||||||
|
*
|
||||||
|
* A zero-based index of current iteration is passed as a parameter to [action].
|
||||||
|
*/
|
||||||
|
inline fun <R> repeatOnFailure(
|
||||||
|
times: Int,
|
||||||
|
onEachFailure: (Throwable) -> Unit = {},
|
||||||
|
action: (Int) -> R
|
||||||
|
): Optional<R> {
|
||||||
|
repeat(times) {
|
||||||
|
runCatching {
|
||||||
|
action(it)
|
||||||
|
}.onFailure(onEachFailure).onSuccess {
|
||||||
|
return Optional.presented(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.absent()
|
||||||
|
}
|
@@ -54,7 +54,7 @@ class DiffUtilsTests {
|
|||||||
val oldList = (0 until 10).map { it.toString() }
|
val oldList = (0 until 10).map { it.toString() }
|
||||||
val withIndex = oldList.withIndex()
|
val withIndex = oldList.withIndex()
|
||||||
|
|
||||||
for (step in 0 until oldList.size) {
|
for (step in oldList.indices) {
|
||||||
for ((i, v) in withIndex) {
|
for ((i, v) in withIndex) {
|
||||||
val mutable = oldList.toMutableList()
|
val mutable = oldList.toMutableList()
|
||||||
val changes = (
|
val changes = (
|
||||||
@@ -73,4 +73,78 @@ class DiffUtilsTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testThatSimpleRemoveApplyWorks() {
|
||||||
|
val oldList = (0 until 10).toList()
|
||||||
|
val withIndex = oldList.withIndex()
|
||||||
|
|
||||||
|
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||||
|
for ((i, _) in withIndex) {
|
||||||
|
if (i + count > oldList.lastIndex) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val removedSublist = oldList.subList(i, i + count)
|
||||||
|
val mutableOldList = oldList.toMutableList()
|
||||||
|
val targetList = oldList - removedSublist
|
||||||
|
|
||||||
|
mutableOldList.applyDiff(targetList)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
targetList,
|
||||||
|
mutableOldList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testThatSimpleAddApplyWorks() {
|
||||||
|
val oldList = (0 until 10).map { it.toString() }
|
||||||
|
val withIndex = oldList.withIndex()
|
||||||
|
|
||||||
|
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||||
|
for ((i, v) in withIndex) {
|
||||||
|
if (i + count > oldList.lastIndex) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val addedSublist = oldList.subList(i, i + count).map { "added$it" }
|
||||||
|
val mutable = oldList.toMutableList()
|
||||||
|
mutable.addAll(i, addedSublist)
|
||||||
|
val mutableOldList = oldList.toMutableList()
|
||||||
|
|
||||||
|
mutableOldList.applyDiff(mutable)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
mutable,
|
||||||
|
mutableOldList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testThatSimpleChangesApplyWorks() {
|
||||||
|
val oldList = (0 until 10).map { it.toString() }
|
||||||
|
val withIndex = oldList.withIndex()
|
||||||
|
|
||||||
|
for (step in oldList.indices) {
|
||||||
|
for ((i, v) in withIndex) {
|
||||||
|
val mutable = oldList.toMutableList()
|
||||||
|
val changes = (
|
||||||
|
if (step == 0) i until oldList.size else (i until oldList.size step step)
|
||||||
|
).map { index ->
|
||||||
|
IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also {
|
||||||
|
mutable[index] = it.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val mutableOldList = oldList.toMutableList()
|
||||||
|
mutableOldList.applyDiff(mutable)
|
||||||
|
assertEquals(
|
||||||
|
mutable,
|
||||||
|
mutableOldList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,124 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import org.w3c.dom.DOMRectReadOnly
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
|
||||||
|
external interface IntersectionObserverOptions {
|
||||||
|
/**
|
||||||
|
* An Element or Document object which is an ancestor of the intended target, whose bounding rectangle will be
|
||||||
|
* considered the viewport. Any part of the target not visible in the visible area of the root is not considered
|
||||||
|
* visible.
|
||||||
|
*/
|
||||||
|
var root: Element?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A string which specifies a set of offsets to add to the root's bounding_box when calculating intersections,
|
||||||
|
* effectively shrinking or growing the root for calculation purposes. The syntax is approximately the same as that
|
||||||
|
* for the CSS margin property; see The root element and root margin in Intersection Observer API for more
|
||||||
|
* information on how the margin works and the syntax. The default is "0px 0px 0px 0px".
|
||||||
|
*/
|
||||||
|
var rootMargin: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Either a single number or an array of numbers between 0.0 and 1.0, specifying a ratio of intersection area to
|
||||||
|
* total bounding box area for the observed target. A value of 0.0 means that even a single visible pixel counts as
|
||||||
|
* the target being visible. 1.0 means that the entire target element is visible. See Thresholds in Intersection
|
||||||
|
* Observer API for a more in-depth description of how thresholds are used. The default is a threshold of 0.0.
|
||||||
|
*/
|
||||||
|
var threshold: Array<Number>?
|
||||||
|
}
|
||||||
|
fun IntersectionObserverOptions(
|
||||||
|
block: IntersectionObserverOptions.() -> Unit = {}
|
||||||
|
): IntersectionObserverOptions = js("{}").unsafeCast<IntersectionObserverOptions>().apply(block)
|
||||||
|
|
||||||
|
external interface IntersectionObserverEntry {
|
||||||
|
/**
|
||||||
|
* Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in
|
||||||
|
* the documentation for Element.getBoundingClientRect().
|
||||||
|
*/
|
||||||
|
val boundingClientRect: DOMRectReadOnly
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ratio of the intersectionRect to the boundingClientRect.
|
||||||
|
*/
|
||||||
|
val intersectionRatio: Number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a DOMRectReadOnly representing the target's visible area.
|
||||||
|
*/
|
||||||
|
val intersectionRect: DOMRectReadOnly
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Boolean value which is true if the target element intersects with the intersection observer's root. If this is
|
||||||
|
* true, then, the IntersectionObserverEntry describes a transition into a state of intersection; if it's false,
|
||||||
|
* then you know the transition is from intersecting to not-intersecting.
|
||||||
|
*/
|
||||||
|
val isIntersecting: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a DOMRectReadOnly for the intersection observer's root.
|
||||||
|
*/
|
||||||
|
val rootBounds: DOMRectReadOnly
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Element whose intersection with the root changed.
|
||||||
|
*/
|
||||||
|
val target: Element
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DOMHighResTimeStamp indicating the time at which the intersection was recorded, relative to the
|
||||||
|
* IntersectionObserver's time origin.
|
||||||
|
*/
|
||||||
|
val time: Double
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias IntersectionObserverCallback = (entries: Array<IntersectionObserverEntry>, observer: IntersectionObserver) -> Unit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is just an implementation from [this commentary](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0)
|
||||||
|
* of Kotlin JS issue related to the absence of [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver)
|
||||||
|
*/
|
||||||
|
external class IntersectionObserver(callback: IntersectionObserverCallback) {
|
||||||
|
constructor(callback: IntersectionObserverCallback, options: IntersectionObserverOptions)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Element or Document whose bounds are used as the bounding box when testing for intersection. If no root value
|
||||||
|
* was passed to the constructor or its value is null, the top-level document's viewport is used.
|
||||||
|
*/
|
||||||
|
val root: Element
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An offset rectangle applied to the root's bounding box when calculating intersections, effectively shrinking or
|
||||||
|
* growing the root for calculation purposes. The value returned by this property may not be the same as the one
|
||||||
|
* specified when calling the constructor as it may be changed to match internal requirements. Each offset can be
|
||||||
|
* expressed in pixels (px) or as a percentage (%). The default is "0px 0px 0px 0px".
|
||||||
|
*/
|
||||||
|
val rootMargin: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of thresholds, sorted in increasing numeric order, where each threshold is a ratio of intersection area to
|
||||||
|
* bounding box area of an observed target. Notifications for a target are generated when any of the thresholds are
|
||||||
|
* crossed for that target. If no value was passed to the constructor, 0 is used.
|
||||||
|
*/
|
||||||
|
val thresholds: Array<Number>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the IntersectionObserver object from observing any target.
|
||||||
|
*/
|
||||||
|
fun disconnect()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells the IntersectionObserver a target element to observe.
|
||||||
|
*/
|
||||||
|
fun observe(targetElement: Element)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of IntersectionObserverEntry objects for all observed targets.
|
||||||
|
*/
|
||||||
|
fun takeRecords(): Array<IntersectionObserverEntry>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells the IntersectionObserver to stop observing a particular target element.
|
||||||
|
*/
|
||||||
|
fun unobserve(targetElement: Element)
|
||||||
|
}
|
@@ -2,8 +2,7 @@ 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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,6 +23,11 @@ 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()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,5 +44,11 @@ actual val MPPFile.filesize: Long
|
|||||||
* @suppress
|
* @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.bytesAllocatorSync: ByteArrayAllocator
|
||||||
|
get() = ::readBytes
|
||||||
|
/**
|
||||||
|
* @suppress
|
||||||
|
*/
|
||||||
|
@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
|
||||||
|
@@ -0,0 +1,8 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlinx.browser.window
|
||||||
|
|
||||||
|
fun openLink(link: String, target: String = "_blank", features: String = "") {
|
||||||
|
window.open(link, target, features) ?.focus()
|
||||||
|
}
|
||||||
|
|
@@ -0,0 +1,30 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlinx.browser.document
|
||||||
|
import kotlinx.dom.createElement
|
||||||
|
import org.w3c.dom.HTMLElement
|
||||||
|
import org.w3c.dom.HTMLInputElement
|
||||||
|
import org.w3c.files.get
|
||||||
|
|
||||||
|
fun selectFile(
|
||||||
|
inputSetup: (HTMLInputElement) -> Unit = {},
|
||||||
|
onFailure: (Throwable) -> Unit = {},
|
||||||
|
onFile: (MPPFile) -> Unit
|
||||||
|
) {
|
||||||
|
(document.createElement("input") {
|
||||||
|
(this as HTMLInputElement).apply {
|
||||||
|
type = "file"
|
||||||
|
onchange = {
|
||||||
|
runCatching {
|
||||||
|
files ?.get(0) ?: error("File must not be null")
|
||||||
|
}.onSuccess {
|
||||||
|
onFile(it)
|
||||||
|
}.onFailure {
|
||||||
|
onFailure(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inputSetup(this)
|
||||||
|
}
|
||||||
|
} as HTMLElement).click()
|
||||||
|
}
|
||||||
|
|
@@ -0,0 +1,14 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlinx.browser.document
|
||||||
|
import org.w3c.dom.HTMLAnchorElement
|
||||||
|
|
||||||
|
fun triggerDownloadFile(filename: String, fileLink: String) {
|
||||||
|
val hiddenElement = document.createElement("a") as HTMLAnchorElement
|
||||||
|
|
||||||
|
hiddenElement.href = fileLink
|
||||||
|
hiddenElement.target = "_blank"
|
||||||
|
hiddenElement.download = filename
|
||||||
|
hiddenElement.click()
|
||||||
|
}
|
||||||
|
|
@@ -22,6 +22,11 @@ actual val MPPFile.filesize: Long
|
|||||||
/**
|
/**
|
||||||
* @suppress
|
* @suppress
|
||||||
*/
|
*/
|
||||||
|
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
||||||
|
get() = ::readBytes
|
||||||
|
/**
|
||||||
|
* @suppress
|
||||||
|
*/
|
||||||
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||||
get() = {
|
get() = {
|
||||||
doInIO {
|
doInIO {
|
||||||
|
@@ -10,12 +10,17 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
api libs.kt.coroutines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jsMain {
|
||||||
|
dependencies {
|
||||||
|
api project(":micro_utils.common")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
api libs.kt.coroutines.android
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
19
coroutines/compose/build.gradle
Normal file
19
coroutines/compose/build.gradle
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
|
id "com.android.library"
|
||||||
|
alias(libs.plugins.jb.compose)
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api libs.kt.coroutines
|
||||||
|
api project(":micro_utils.coroutines")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,22 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
fun <T> Flow<T>.toMutableState(
|
||||||
|
initial: T,
|
||||||
|
scope: CoroutineScope
|
||||||
|
): MutableState<T> {
|
||||||
|
val state = mutableStateOf(initial)
|
||||||
|
subscribeSafelyWithoutExceptions(scope) { state.value = it }
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> StateFlow<T>.toMutableState(
|
||||||
|
scope: CoroutineScope
|
||||||
|
): MutableState<T> = toMutableState(value, scope)
|
||||||
|
|
@@ -0,0 +1,14 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.job
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
fun Composition.linkWithJob(job: Job) {
|
||||||
|
job.invokeOnCompletion {
|
||||||
|
this@linkWithJob.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Composition.linkWithContext(coroutineContext: CoroutineContext) = linkWithJob(coroutineContext.job)
|
@@ -0,0 +1,16 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import org.jetbrains.compose.web.dom.DOMScope
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
|
||||||
|
suspend fun <TElement : Element> renderComposableAndLinkToContext(
|
||||||
|
root: TElement,
|
||||||
|
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
|
||||||
|
content: @Composable DOMScope<TElement>.() -> Unit
|
||||||
|
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
|
||||||
|
linkWithContext(
|
||||||
|
currentCoroutineContext()
|
||||||
|
)
|
||||||
|
}
|
1
coroutines/compose/src/main/AndroidManifest.xml
Normal file
1
coroutines/compose/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest package="dev.inmo.micro_utils.coroutines.compose"/>
|
@@ -0,0 +1,42 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
|
import dev.inmo.micro_utils.common.selectFile
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import org.w3c.dom.HTMLInputElement
|
||||||
|
|
||||||
|
suspend fun selectFileOrThrow(
|
||||||
|
inputSetup: (HTMLInputElement) -> Unit = {}
|
||||||
|
): MPPFile {
|
||||||
|
val result = CompletableDeferred<MPPFile>()
|
||||||
|
|
||||||
|
selectFile(
|
||||||
|
inputSetup,
|
||||||
|
{
|
||||||
|
result.completeExceptionally(it)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
result.complete(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun selectFileOrNull(
|
||||||
|
inputSetup: (HTMLInputElement) -> Unit = {},
|
||||||
|
onFailure: (Throwable) -> Unit = {}
|
||||||
|
): MPPFile? {
|
||||||
|
val result = CompletableDeferred<MPPFile?>()
|
||||||
|
|
||||||
|
selectFile(
|
||||||
|
inputSetup,
|
||||||
|
{
|
||||||
|
result.complete(null)
|
||||||
|
onFailure(it)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
result.complete(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.await()
|
||||||
|
}
|
@@ -26,12 +26,12 @@ ext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion "$android_compileSdkVersion".toInteger()
|
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||||
buildToolsVersion "$android_buildToolsVersion"
|
buildToolsVersion libs.versions.android.props.buildTools.get()
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion "$android_minSdkVersion".toInteger()
|
minSdkVersion libs.versions.android.props.minSdk.get().toInteger()
|
||||||
targetSdkVersion "$android_compileSdkVersion".toInteger()
|
targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||||
versionCode "${android_code_version}".toInteger()
|
versionCode "${android_code_version}".toInteger()
|
||||||
versionName "$version"
|
versionName "$version"
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,7 @@ allprojects {
|
|||||||
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
|
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
|
||||||
|
|
||||||
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
|
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
|
||||||
|
mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle"
|
||||||
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
|
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
|
||||||
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
|
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
|
||||||
|
|
||||||
|
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,11 @@
|
|||||||
package dev.inmo.micro_utils.fsm.common
|
package dev.inmo.micro_utils.fsm.common
|
||||||
|
|
||||||
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
|
import dev.inmo.micro_utils.common.Optional
|
||||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
import dev.inmo.micro_utils.common.onPresented
|
||||||
|
import dev.inmo.micro_utils.coroutines.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default [StatesMachine] may [startChain] and use inside logic for handling [State]s. By default you may use
|
* Default [StatesMachine] may [startChain] and use inside logic for handling [State]s. By default you may use
|
||||||
@@ -12,7 +15,7 @@ import kotlinx.coroutines.*
|
|||||||
interface StatesMachine<T : State> : StatesHandler<T, T> {
|
interface StatesMachine<T : State> : StatesHandler<T, T> {
|
||||||
suspend fun launchStateHandling(
|
suspend fun launchStateHandling(
|
||||||
state: T,
|
state: T,
|
||||||
handlers: List<CustomizableHandlerHolder<in T, T>>
|
handlers: List<CheckableHandlerHolder<in T, T>>
|
||||||
): T? {
|
): T? {
|
||||||
return handlers.firstOrNull { it.checkHandleable(state) } ?.run {
|
return handlers.firstOrNull { it.checkHandleable(state) } ?.run {
|
||||||
handleState(state)
|
handleState(state)
|
||||||
@@ -35,24 +38,60 @@ interface StatesMachine<T : State> : StatesHandler<T, T> {
|
|||||||
*/
|
*/
|
||||||
operator fun <T: State> invoke(
|
operator fun <T: State> invoke(
|
||||||
statesManager: StatesManager<T>,
|
statesManager: StatesManager<T>,
|
||||||
handlers: List<CustomizableHandlerHolder<in T, T>>
|
handlers: List<CheckableHandlerHolder<in T, T>>
|
||||||
) = DefaultStatesMachine(statesManager, handlers)
|
) = DefaultStatesMachine(statesManager, handlers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default realization of [StatesMachine]. It uses [statesManager] for incapsulation of [State]s storing and contexts
|
* Default realization of [StatesMachine]. It uses [statesManager] for incapsulation of [State]s storing and contexts
|
||||||
* resolving, and uses [launchStateHandling] for [State] handling
|
* resolving, and uses [launchStateHandling] for [State] handling.
|
||||||
|
*
|
||||||
|
* This class suppose to be extended in case you wish some custom behaviour inside of [launchStateHandling], for example
|
||||||
*/
|
*/
|
||||||
class DefaultStatesMachine <T: State>(
|
open class DefaultStatesMachine <T: State>(
|
||||||
private val statesManager: StatesManager<T>,
|
protected val statesManager: StatesManager<T>,
|
||||||
private val handlers: List<CustomizableHandlerHolder<in T, T>>
|
protected val handlers: List<CheckableHandlerHolder<in T, T>>,
|
||||||
) : StatesMachine<T> {
|
) : StatesMachine<T> {
|
||||||
/**
|
/**
|
||||||
* Will call [launchStateHandling] for state handling
|
* Will call [launchStateHandling] for state handling
|
||||||
*/
|
*/
|
||||||
override suspend fun StatesMachine<in T>.handleState(state: T): T? = launchStateHandling(state, handlers)
|
override suspend fun StatesMachine<in T>.handleState(state: T): T? = launchStateHandling(state, handlers)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This
|
||||||
|
*/
|
||||||
|
protected val statesJobs = mutableMapOf<T, Job>()
|
||||||
|
protected val statesJobsMutex = Mutex()
|
||||||
|
|
||||||
|
protected open suspend fun performUpdate(state: T) {
|
||||||
|
val newState = launchStateHandling(state, handlers)
|
||||||
|
if (newState != null) {
|
||||||
|
statesManager.update(state, newState)
|
||||||
|
} else {
|
||||||
|
statesManager.endChain(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
|
||||||
|
statesJobsMutex.withLock {
|
||||||
|
statesJobs[actualState] ?.cancel()
|
||||||
|
statesJobs[actualState] = scope.launch {
|
||||||
|
performUpdate(actualState)
|
||||||
|
}.also { job ->
|
||||||
|
job.invokeOnCompletion { _ ->
|
||||||
|
scope.launch {
|
||||||
|
statesJobsMutex.withLock {
|
||||||
|
if (statesJobs[actualState] == job) {
|
||||||
|
statesJobs.remove(actualState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch handling of states. On [statesManager] [StatesManager.onStartChain],
|
* Launch handling of states. On [statesManager] [StatesManager.onStartChain],
|
||||||
* [statesManager] [StatesManager.onChainStateUpdated] will be called lambda with performing of state. If
|
* [statesManager] [StatesManager.onChainStateUpdated] will be called lambda with performing of state. If
|
||||||
@@ -60,23 +99,15 @@ class DefaultStatesMachine <T: State>(
|
|||||||
* [StatesManager.endChain].
|
* [StatesManager.endChain].
|
||||||
*/
|
*/
|
||||||
override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
|
override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
|
||||||
val statePerformer: suspend (T) -> Unit = { state: T ->
|
|
||||||
val newState = launchStateHandling(state, handlers)
|
|
||||||
if (newState != null) {
|
|
||||||
statesManager.update(state, newState)
|
|
||||||
} else {
|
|
||||||
statesManager.endChain(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
@@ -7,16 +7,26 @@ import kotlin.reflect.KClass
|
|||||||
|
|
||||||
class FSMBuilder<T : State>(
|
class FSMBuilder<T : State>(
|
||||||
var statesManager: StatesManager<T> = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()),
|
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 }
|
var defaultStateHandler: StatesHandler<T, T>? = StatesHandler { null }
|
||||||
) {
|
) {
|
||||||
private var states = mutableListOf<CustomizableHandlerHolder<T, T>>()
|
private var states = mutableListOf<CheckableHandlerHolder<T, T>>()
|
||||||
|
|
||||||
|
fun add(handler: CheckableHandlerHolder<T, T>) {
|
||||||
|
states.add(handler)
|
||||||
|
}
|
||||||
|
|
||||||
fun <I : T> add(kClass: KClass<I>, handler: StatesHandler<I, T>) {
|
fun <I : T> add(kClass: KClass<I>, handler: StatesHandler<I, T>) {
|
||||||
states.add(CheckableHandlerHolder(kClass, false, handler))
|
add(CheckableHandlerHolder(kClass, false, handler))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <I : T> add(filter: suspend (state: State) -> Boolean, handler: StatesHandler<I, T>) {
|
fun <I : T> add(filter: suspend (state: State) -> Boolean, handler: StatesHandler<I, T>) {
|
||||||
states.add(handler.holder(filter))
|
add(handler.holder(filter))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <I : T> addStrict(kClass: KClass<I>, handler: StatesHandler<I, T>) {
|
fun <I : T> addStrict(kClass: KClass<I>, handler: StatesHandler<I, T>) {
|
||||||
@@ -38,7 +48,7 @@ class FSMBuilder<T : State>(
|
|||||||
add(filter, handler)
|
add(filter, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun build() = StatesMachine(
|
fun build() = fsmBuilder(
|
||||||
statesManager,
|
statesManager,
|
||||||
states.toList().let { list ->
|
states.toList().let { list ->
|
||||||
defaultStateHandler ?.let { list + it.holder { true } } ?: list
|
defaultStateHandler ?.let { list + it.holder { true } } ?: list
|
||||||
|
@@ -7,43 +7,12 @@ android.useAndroidX=true
|
|||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
org.gradle.jvmargs=-Xmx2g
|
org.gradle.jvmargs=-Xmx2g
|
||||||
|
|
||||||
kotlin_version=1.5.31
|
|
||||||
kotlin_coroutines_version=1.5.2
|
|
||||||
kotlin_serialisation_core_version=1.3.0
|
|
||||||
kotlin_exposed_version=0.36.2
|
|
||||||
|
|
||||||
ktor_version=1.6.5
|
|
||||||
|
|
||||||
klockVersion=2.4.7
|
|
||||||
|
|
||||||
github_release_plugin_version=2.2.12
|
|
||||||
|
|
||||||
uuidVersion=0.3.1
|
|
||||||
|
|
||||||
# ANDROID
|
|
||||||
|
|
||||||
core_ktx_version=1.7.0
|
|
||||||
androidx_recycler_version=1.2.1
|
|
||||||
appcompat_version=1.3.1
|
|
||||||
|
|
||||||
android_minSdkVersion=19
|
|
||||||
android_compileSdkVersion=31
|
|
||||||
android_buildToolsVersion=31.0.0
|
|
||||||
dexcount_version=3.0.0
|
|
||||||
junit_version=4.12
|
|
||||||
test_ext_junit_version=1.1.2
|
|
||||||
espresso_core=3.3.0
|
|
||||||
|
|
||||||
# JS NPM
|
# JS NPM
|
||||||
|
|
||||||
crypto_js_version=4.1.1
|
crypto_js_version=4.1.1
|
||||||
|
|
||||||
# Dokka
|
|
||||||
|
|
||||||
dokka_version=1.5.31
|
|
||||||
|
|
||||||
# Project data
|
# Project data
|
||||||
|
|
||||||
group=dev.inmo
|
group=dev.inmo
|
||||||
version=0.8.1
|
version=0.9.12
|
||||||
android_code_version=81
|
android_code_version=102
|
||||||
|
77
gradle/libs.versions.toml
Normal file
77
gradle/libs.versions.toml
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
[versions]
|
||||||
|
|
||||||
|
kt = "1.6.10"
|
||||||
|
kt-serialization = "1.3.2"
|
||||||
|
kt-coroutines = "1.6.0"
|
||||||
|
|
||||||
|
jb-compose = "1.1.0"
|
||||||
|
jb-exposed = "0.37.3"
|
||||||
|
jb-dokka = "1.6.10"
|
||||||
|
|
||||||
|
klock = "2.6.2"
|
||||||
|
uuid = "0.4.0"
|
||||||
|
|
||||||
|
ktor = "1.6.7"
|
||||||
|
|
||||||
|
gh-release = "2.2.12"
|
||||||
|
|
||||||
|
android-gradle = "7.0.4"
|
||||||
|
dexcount = "3.0.1"
|
||||||
|
|
||||||
|
android-coreKtx = "1.7.0"
|
||||||
|
android-recyclerView = "1.2.1"
|
||||||
|
android-appCompat = "1.4.1"
|
||||||
|
android-espresso = "3.3.0"
|
||||||
|
android-test = "1.1.2"
|
||||||
|
|
||||||
|
android-props-minSdk = "19"
|
||||||
|
android-props-compileSdk = "32"
|
||||||
|
android-props-buildTools = "32.0.0"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
|
||||||
|
kt-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kt" }
|
||||||
|
|
||||||
|
kt-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kt-serialization" }
|
||||||
|
kt-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kt-serialization" }
|
||||||
|
|
||||||
|
kt-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kt-coroutines" }
|
||||||
|
kt-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kt-coroutines" }
|
||||||
|
|
||||||
|
|
||||||
|
ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||||
|
ktor-client-java = { module = "io.ktor:ktor-client-java", version.ref = "ktor" }
|
||||||
|
ktor-server = { module = "io.ktor:ktor-server", version.ref = "ktor" }
|
||||||
|
ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" }
|
||||||
|
ktor-server-host-common = { module = "io.ktor:ktor-server-host-common", version.ref = "ktor" }
|
||||||
|
ktor-websockets = { module = "io.ktor:ktor-websockets", version.ref = "ktor" }
|
||||||
|
|
||||||
|
|
||||||
|
klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "klock" }
|
||||||
|
uuid = { module = "com.benasher44:uuid", version.ref = "uuid" }
|
||||||
|
|
||||||
|
|
||||||
|
jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-exposed" }
|
||||||
|
|
||||||
|
|
||||||
|
android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
|
||||||
|
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }
|
||||||
|
android-appCompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "android-appCompat" }
|
||||||
|
android-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" }
|
||||||
|
android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" }
|
||||||
|
|
||||||
|
|
||||||
|
kt-test-js = { module = "org.jetbrains.kotlin:kotlin-test-js", version.ref = "kt" }
|
||||||
|
kt-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kt" }
|
||||||
|
|
||||||
|
|
||||||
|
buildscript-kt-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kt" }
|
||||||
|
buildscript-kt-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kt" }
|
||||||
|
buildscript-jb-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "jb-dokka" }
|
||||||
|
buildscript-gh-release = { module = "com.github.breadmoirai:github-release", version.ref = "gh-release" }
|
||||||
|
buildscript-android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle" }
|
||||||
|
buildscript-android-dexcount = { module = "com.getkeepsafe.dexcount:dexcount-gradle-plugin", version.ref = "dexcount" }
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
|
||||||
|
jb-compose = { id = "org.jetbrains.compose", version.ref = "jb-compose" }
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@@ -12,7 +12,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
api internalProject("micro_utils.ktor.common")
|
api internalProject("micro_utils.ktor.common")
|
||||||
api internalProject("micro_utils.coroutines")
|
api internalProject("micro_utils.coroutines")
|
||||||
api "io.ktor:ktor-client-core:$ktor_version"
|
api libs.ktor.client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import dev.inmo.micro_utils.coroutines.safely
|
|||||||
import dev.inmo.micro_utils.ktor.common.*
|
import dev.inmo.micro_utils.ktor.common.*
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.features.websocket.ws
|
import io.ktor.client.features.websocket.ws
|
||||||
|
import io.ktor.client.request.HttpRequestBuilder
|
||||||
import io.ktor.http.cio.websocket.Frame
|
import io.ktor.http.cio.websocket.Frame
|
||||||
import io.ktor.http.cio.websocket.readBytes
|
import io.ktor.http.cio.websocket.readBytes
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -17,6 +18,7 @@ import kotlinx.serialization.DeserializationStrategy
|
|||||||
inline fun <T> HttpClient.createStandardWebsocketFlow(
|
inline fun <T> HttpClient.createStandardWebsocketFlow(
|
||||||
url: String,
|
url: String,
|
||||||
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
|
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
|
||||||
|
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||||
crossinline conversation: suspend (StandardKtorSerialInputData) -> T
|
crossinline conversation: suspend (StandardKtorSerialInputData) -> T
|
||||||
): Flow<T> {
|
): Flow<T> {
|
||||||
val correctedUrl = url.asCorrectWebSocketUrl
|
val correctedUrl = url.asCorrectWebSocketUrl
|
||||||
@@ -26,7 +28,7 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
|
|||||||
do {
|
do {
|
||||||
val reconnect = try {
|
val reconnect = try {
|
||||||
safely {
|
safely {
|
||||||
ws(correctedUrl) {
|
ws(correctedUrl, requestBuilder) {
|
||||||
for (received in incoming) {
|
for (received in incoming) {
|
||||||
when (received) {
|
when (received) {
|
||||||
is Frame.Binary -> producerScope.send(conversation(received.readBytes()))
|
is Frame.Binary -> producerScope.send(conversation(received.readBytes()))
|
||||||
@@ -65,10 +67,12 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
|
|||||||
url: String,
|
url: String,
|
||||||
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
|
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
|
||||||
deserializer: DeserializationStrategy<T>,
|
deserializer: DeserializationStrategy<T>,
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
||||||
|
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||||
) = createStandardWebsocketFlow(
|
) = createStandardWebsocketFlow(
|
||||||
url,
|
url,
|
||||||
checkReconnection
|
checkReconnection,
|
||||||
|
requestBuilder
|
||||||
) {
|
) {
|
||||||
serialFormat.decodeDefault(deserializer, it)
|
serialFormat.decodeDefault(deserializer, it)
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
package dev.inmo.micro_utils.ktor.client
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
|
import io.ktor.client.request.forms.InputProvider
|
||||||
|
|
||||||
|
expect suspend fun MPPFile.inputProvider(): InputProvider
|
@@ -1,16 +1,20 @@
|
|||||||
package dev.inmo.micro_utils.ktor.client
|
package dev.inmo.micro_utils.ktor.client
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
|
import dev.inmo.micro_utils.common.filename
|
||||||
import dev.inmo.micro_utils.ktor.common.*
|
import dev.inmo.micro_utils.ktor.common.*
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.request.post
|
import io.ktor.client.request.forms.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.utils.io.core.ByteReadPacket
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
|
|
||||||
typealias BodyPair<T> = Pair<SerializationStrategy<T>, T>
|
typealias BodyPair<T> = Pair<SerializationStrategy<T>, T>
|
||||||
|
|
||||||
class UnifiedRequester(
|
class UnifiedRequester(
|
||||||
private val client: HttpClient = HttpClient(),
|
val client: HttpClient = HttpClient(),
|
||||||
private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||||
) {
|
) {
|
||||||
suspend fun <ResultType> uniget(
|
suspend fun <ResultType> uniget(
|
||||||
url: String,
|
url: String,
|
||||||
@@ -31,11 +35,66 @@ class UnifiedRequester(
|
|||||||
resultDeserializer: DeserializationStrategy<ResultType>
|
resultDeserializer: DeserializationStrategy<ResultType>
|
||||||
) = client.unipost(url, bodyInfo, resultDeserializer, serialFormat)
|
) = client.unipost(url, bodyInfo, resultDeserializer, serialFormat)
|
||||||
|
|
||||||
|
suspend fun <ResultType> unimultipart(
|
||||||
|
url: String,
|
||||||
|
filename: String,
|
||||||
|
inputProvider: InputProvider,
|
||||||
|
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||||
|
mimetype: String = "*/*",
|
||||||
|
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||||
|
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||||
|
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||||
|
): ResultType = client.unimultipart(url, filename, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat)
|
||||||
|
|
||||||
|
suspend fun <BodyType, ResultType> unimultipart(
|
||||||
|
url: String,
|
||||||
|
filename: String,
|
||||||
|
inputProvider: InputProvider,
|
||||||
|
otherData: BodyPair<BodyType>,
|
||||||
|
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||||
|
mimetype: String = "*/*",
|
||||||
|
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||||
|
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||||
|
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||||
|
): ResultType = client.unimultipart(url, filename, otherData, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat)
|
||||||
|
|
||||||
|
suspend fun <ResultType> unimultipart(
|
||||||
|
url: String,
|
||||||
|
mppFile: MPPFile,
|
||||||
|
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||||
|
mimetype: String = "*/*",
|
||||||
|
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||||
|
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||||
|
requestBuilder: HttpRequestBuilder.() -> Unit = {}
|
||||||
|
): ResultType = client.unimultipart(
|
||||||
|
url, mppFile, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun <BodyType, ResultType> unimultipart(
|
||||||
|
url: String,
|
||||||
|
mppFile: MPPFile,
|
||||||
|
otherData: BodyPair<BodyType>,
|
||||||
|
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||||
|
mimetype: String = "*/*",
|
||||||
|
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||||
|
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||||
|
requestBuilder: HttpRequestBuilder.() -> Unit = {}
|
||||||
|
): ResultType = client.unimultipart(
|
||||||
|
url, mppFile, otherData, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat
|
||||||
|
)
|
||||||
|
|
||||||
fun <T> createStandardWebsocketFlow(
|
fun <T> createStandardWebsocketFlow(
|
||||||
url: String,
|
url: String,
|
||||||
checkReconnection: (Throwable?) -> Boolean = { true },
|
checkReconnection: (Throwable?) -> Boolean,
|
||||||
deserializer: DeserializationStrategy<T>
|
deserializer: DeserializationStrategy<T>,
|
||||||
) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat)
|
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||||
|
) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat, requestBuilder)
|
||||||
|
|
||||||
|
fun <T> createStandardWebsocketFlow(
|
||||||
|
url: String,
|
||||||
|
deserializer: DeserializationStrategy<T>,
|
||||||
|
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||||
|
) = createStandardWebsocketFlow(url, { true }, deserializer, requestBuilder)
|
||||||
}
|
}
|
||||||
|
|
||||||
val defaultRequester = UnifiedRequester()
|
val defaultRequester = UnifiedRequester()
|
||||||
@@ -69,3 +128,124 @@ suspend fun <BodyType, ResultType> HttpClient.unipost(
|
|||||||
}.let {
|
}.let {
|
||||||
serialFormat.decodeDefault(resultDeserializer, it)
|
serialFormat.decodeDefault(resultDeserializer, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun <ResultType> HttpClient.unimultipart(
|
||||||
|
url: String,
|
||||||
|
filename: String,
|
||||||
|
inputProvider: InputProvider,
|
||||||
|
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||||
|
mimetype: String = "*/*",
|
||||||
|
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||||
|
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||||
|
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||||
|
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||||
|
): ResultType = submitFormWithBinaryData<StandardKtorSerialInputData>(
|
||||||
|
url,
|
||||||
|
formData = formData {
|
||||||
|
append(
|
||||||
|
"bytes",
|
||||||
|
inputProvider,
|
||||||
|
Headers.build {
|
||||||
|
append(HttpHeaders.ContentType, mimetype)
|
||||||
|
append(HttpHeaders.ContentDisposition, "filename=\"$filename\"")
|
||||||
|
dataHeadersBuilder()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
additionalParametersBuilder()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
requestBuilder()
|
||||||
|
}.let { serialFormat.decodeDefault(resultDeserializer, it) }
|
||||||
|
|
||||||
|
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
|
||||||
|
url: String,
|
||||||
|
filename: String,
|
||||||
|
otherData: BodyPair<BodyType>,
|
||||||
|
inputProvider: InputProvider,
|
||||||
|
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||||
|
mimetype: String = "*/*",
|
||||||
|
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||||
|
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||||
|
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||||
|
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||||
|
): ResultType = unimultipart(
|
||||||
|
url,
|
||||||
|
filename,
|
||||||
|
inputProvider,
|
||||||
|
resultDeserializer,
|
||||||
|
mimetype,
|
||||||
|
additionalParametersBuilder = {
|
||||||
|
val serialized = serialFormat.encodeDefault(otherData.first, otherData.second)
|
||||||
|
append(
|
||||||
|
"data",
|
||||||
|
InputProvider(serialized.size.toLong()) {
|
||||||
|
ByteReadPacket(serialized)
|
||||||
|
},
|
||||||
|
Headers.build {
|
||||||
|
append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType)
|
||||||
|
append(HttpHeaders.ContentDisposition, "filename=data.bytes")
|
||||||
|
dataHeadersBuilder()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
additionalParametersBuilder()
|
||||||
|
},
|
||||||
|
dataHeadersBuilder,
|
||||||
|
requestBuilder,
|
||||||
|
serialFormat
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun <ResultType> HttpClient.unimultipart(
|
||||||
|
url: String,
|
||||||
|
mppFile: MPPFile,
|
||||||
|
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||||
|
mimetype: String = "*/*",
|
||||||
|
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||||
|
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||||
|
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||||
|
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||||
|
): ResultType = unimultipart(
|
||||||
|
url,
|
||||||
|
mppFile.filename.string,
|
||||||
|
mppFile.inputProvider(),
|
||||||
|
resultDeserializer,
|
||||||
|
mimetype,
|
||||||
|
additionalParametersBuilder,
|
||||||
|
dataHeadersBuilder,
|
||||||
|
requestBuilder,
|
||||||
|
serialFormat
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
|
||||||
|
url: String,
|
||||||
|
mppFile: MPPFile,
|
||||||
|
otherData: BodyPair<BodyType>,
|
||||||
|
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||||
|
mimetype: String = "*/*",
|
||||||
|
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||||
|
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||||
|
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||||
|
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||||
|
): ResultType = unimultipart(
|
||||||
|
url,
|
||||||
|
mppFile,
|
||||||
|
resultDeserializer,
|
||||||
|
mimetype,
|
||||||
|
additionalParametersBuilder = {
|
||||||
|
val serialized = serialFormat.encodeDefault(otherData.first, otherData.second)
|
||||||
|
append(
|
||||||
|
"data",
|
||||||
|
InputProvider(serialized.size.toLong()) {
|
||||||
|
ByteReadPacket(serialized)
|
||||||
|
},
|
||||||
|
Headers.build {
|
||||||
|
append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType)
|
||||||
|
append(HttpHeaders.ContentDisposition, "filename=data.bytes")
|
||||||
|
dataHeadersBuilder()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
additionalParametersBuilder()
|
||||||
|
},
|
||||||
|
dataHeadersBuilder,
|
||||||
|
requestBuilder,
|
||||||
|
serialFormat
|
||||||
|
)
|
||||||
|
@@ -0,0 +1,11 @@
|
|||||||
|
package dev.inmo.micro_utils.ktor.client
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.*
|
||||||
|
import io.ktor.client.request.forms.InputProvider
|
||||||
|
import io.ktor.utils.io.core.ByteReadPacket
|
||||||
|
|
||||||
|
actual suspend fun MPPFile.inputProvider(): InputProvider = bytes().let {
|
||||||
|
InputProvider(it.size.toLong()) {
|
||||||
|
ByteReadPacket(it)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,9 @@
|
|||||||
|
package dev.inmo.micro_utils.ktor.client
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
|
import io.ktor.client.request.forms.InputProvider
|
||||||
|
import io.ktor.utils.io.streams.asInput
|
||||||
|
|
||||||
|
actual suspend fun MPPFile.inputProvider(): InputProvider = InputProvider(length()) {
|
||||||
|
inputStream().asInput()
|
||||||
|
}
|
@@ -10,8 +10,9 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api "org.jetbrains.kotlinx:kotlinx-serialization-cbor:$kotlin_serialisation_core_version"
|
api internalProject("micro_utils.common")
|
||||||
api "com.soywiz.korlibs.klock:klock:$klockVersion"
|
api libs.kt.serialization.cbor
|
||||||
|
api libs.klock
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,10 +16,10 @@ kotlin {
|
|||||||
|
|
||||||
jvmMain {
|
jvmMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api "io.ktor:ktor-server:$ktor_version"
|
api libs.ktor.server
|
||||||
api "io.ktor:ktor-server-cio:$ktor_version"
|
api libs.ktor.server.cio
|
||||||
api "io.ktor:ktor-server-host-common:$ktor_version"
|
api libs.ktor.server.host.common
|
||||||
api "io.ktor:ktor-websockets:$ktor_version"
|
api libs.ktor.websockets
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,29 +2,31 @@ package dev.inmo.micro_utils.ktor.server
|
|||||||
|
|
||||||
import dev.inmo.micro_utils.coroutines.safely
|
import dev.inmo.micro_utils.coroutines.safely
|
||||||
import dev.inmo.micro_utils.ktor.common.*
|
import dev.inmo.micro_utils.ktor.common.*
|
||||||
|
import io.ktor.application.featureOrNull
|
||||||
|
import io.ktor.application.install
|
||||||
|
import io.ktor.http.URLProtocol
|
||||||
import io.ktor.http.cio.websocket.*
|
import io.ktor.http.cio.websocket.*
|
||||||
import io.ktor.routing.Route
|
import io.ktor.routing.Route
|
||||||
import io.ktor.websocket.webSocket
|
import io.ktor.routing.application
|
||||||
|
import io.ktor.websocket.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.serialization.SerializationStrategy
|
import kotlinx.serialization.SerializationStrategy
|
||||||
|
|
||||||
private suspend fun DefaultWebSocketSession.checkReceivedAndCloseIfExists() {
|
|
||||||
if (incoming.tryReceive() != null) {
|
|
||||||
close()
|
|
||||||
throw CorrectCloseException
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> Route.includeWebsocketHandling(
|
fun <T> Route.includeWebsocketHandling(
|
||||||
suburl: String,
|
suburl: String,
|
||||||
flow: Flow<T>,
|
flow: Flow<T>,
|
||||||
converter: (T) -> StandardKtorSerialInputData
|
protocol: URLProtocol = URLProtocol.WS,
|
||||||
|
converter: suspend WebSocketServerSession.(T) -> StandardKtorSerialInputData?
|
||||||
) {
|
) {
|
||||||
webSocket(suburl) {
|
application.apply {
|
||||||
|
featureOrNull(io.ktor.websocket.WebSockets) ?: install(io.ktor.websocket.WebSockets)
|
||||||
|
}
|
||||||
|
webSocket(suburl, protocol.name) {
|
||||||
safely {
|
safely {
|
||||||
flow.collect {
|
flow.collect {
|
||||||
send(converter(it))
|
converter(it) ?.let { data ->
|
||||||
|
send(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,10 +36,24 @@ fun <T> Route.includeWebsocketHandling(
|
|||||||
suburl: String,
|
suburl: String,
|
||||||
flow: Flow<T>,
|
flow: Flow<T>,
|
||||||
serializer: SerializationStrategy<T>,
|
serializer: SerializationStrategy<T>,
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
||||||
|
protocol: URLProtocol = URLProtocol.WS,
|
||||||
|
filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null
|
||||||
) = includeWebsocketHandling(
|
) = includeWebsocketHandling(
|
||||||
suburl,
|
suburl,
|
||||||
flow
|
flow,
|
||||||
) {
|
protocol,
|
||||||
serialFormat.encodeDefault(serializer, it)
|
converter = if (filter == null) {
|
||||||
}
|
{
|
||||||
|
serialFormat.encodeDefault(serializer, it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
{
|
||||||
|
if (filter(it)) {
|
||||||
|
serialFormat.encodeDefault(serializer, it)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@@ -1,28 +1,39 @@
|
|||||||
package dev.inmo.micro_utils.ktor.server
|
package dev.inmo.micro_utils.ktor.server
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.*
|
||||||
import dev.inmo.micro_utils.coroutines.safely
|
import dev.inmo.micro_utils.coroutines.safely
|
||||||
import dev.inmo.micro_utils.ktor.common.*
|
import dev.inmo.micro_utils.ktor.common.*
|
||||||
import io.ktor.application.ApplicationCall
|
import io.ktor.application.ApplicationCall
|
||||||
import io.ktor.application.call
|
import io.ktor.application.call
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.*
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.content.PartData
|
||||||
|
import io.ktor.http.content.forEachPart
|
||||||
import io.ktor.request.receive
|
import io.ktor.request.receive
|
||||||
|
import io.ktor.request.receiveMultipart
|
||||||
import io.ktor.response.respond
|
import io.ktor.response.respond
|
||||||
import io.ktor.response.respondBytes
|
import io.ktor.response.respondBytes
|
||||||
import io.ktor.routing.Route
|
import io.ktor.routing.Route
|
||||||
|
import io.ktor.util.asStream
|
||||||
|
import io.ktor.util.cio.writeChannel
|
||||||
import io.ktor.util.pipeline.PipelineContext
|
import io.ktor.util.pipeline.PipelineContext
|
||||||
|
import io.ktor.utils.io.core.*
|
||||||
|
import io.ktor.websocket.WebSocketServerSession
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
|
import java.io.File
|
||||||
|
import java.io.File.createTempFile
|
||||||
|
|
||||||
class UnifiedRouter(
|
class UnifiedRouter(
|
||||||
private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
||||||
private val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
|
val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
|
||||||
) {
|
) {
|
||||||
fun <T> Route.includeWebsocketHandling(
|
fun <T> Route.includeWebsocketHandling(
|
||||||
suburl: String,
|
suburl: String,
|
||||||
flow: Flow<T>,
|
flow: Flow<T>,
|
||||||
serializer: SerializationStrategy<T>
|
serializer: SerializationStrategy<T>,
|
||||||
) = includeWebsocketHandling(suburl, flow, serializer, serialFormat)
|
protocol: URLProtocol = URLProtocol.WS,
|
||||||
|
filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null
|
||||||
|
) = includeWebsocketHandling(suburl, flow, serializer, serialFormat, protocol, filter)
|
||||||
|
|
||||||
suspend fun <T> PipelineContext<*, ApplicationCall>.unianswer(
|
suspend fun <T> PipelineContext<*, ApplicationCall>.unianswer(
|
||||||
answerSerializer: SerializationStrategy<T>,
|
answerSerializer: SerializationStrategy<T>,
|
||||||
@@ -104,6 +115,139 @@ suspend fun <T> ApplicationCall.uniload(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun ApplicationCall.uniloadMultipart(
|
||||||
|
onFormItem: (PartData.FormItem) -> Unit = {},
|
||||||
|
onCustomFileItem: (PartData.FileItem) -> Unit = {},
|
||||||
|
onBinaryContent: (PartData.BinaryItem) -> Unit = {}
|
||||||
|
) = safely {
|
||||||
|
val multipartData = receiveMultipart()
|
||||||
|
|
||||||
|
var resultInput: Input? = null
|
||||||
|
|
||||||
|
multipartData.forEachPart {
|
||||||
|
when (it) {
|
||||||
|
is PartData.FormItem -> onFormItem(it)
|
||||||
|
is PartData.FileItem -> {
|
||||||
|
when (it.name) {
|
||||||
|
"bytes" -> resultInput = it.provider()
|
||||||
|
else -> onCustomFileItem(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is PartData.BinaryItem -> onBinaryContent(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resultInput ?: error("Bytes has not been received")
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> ApplicationCall.uniloadMultipart(
|
||||||
|
deserializer: DeserializationStrategy<T>,
|
||||||
|
onFormItem: (PartData.FormItem) -> Unit = {},
|
||||||
|
onCustomFileItem: (PartData.FileItem) -> Unit = {},
|
||||||
|
onBinaryContent: (PartData.BinaryItem) -> Unit = {}
|
||||||
|
): Pair<Input, T> {
|
||||||
|
var data: Optional<T>? = null
|
||||||
|
val resultInput = uniloadMultipart(
|
||||||
|
onFormItem,
|
||||||
|
{
|
||||||
|
if (it.name == "data") {
|
||||||
|
data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional
|
||||||
|
} else {
|
||||||
|
onCustomFileItem(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onBinaryContent
|
||||||
|
)
|
||||||
|
|
||||||
|
val completeData = data ?: error("Data has not been received")
|
||||||
|
return resultInput to (completeData.dataOrNull().let { it as T })
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> ApplicationCall.uniloadMultipartFile(
|
||||||
|
deserializer: DeserializationStrategy<T>,
|
||||||
|
onFormItem: (PartData.FormItem) -> Unit = {},
|
||||||
|
onCustomFileItem: (PartData.FileItem) -> Unit = {},
|
||||||
|
onBinaryContent: (PartData.BinaryItem) -> Unit = {},
|
||||||
|
) = safely {
|
||||||
|
val multipartData = receiveMultipart()
|
||||||
|
|
||||||
|
var resultInput: MPPFile? = null
|
||||||
|
var data: Optional<T>? = null
|
||||||
|
|
||||||
|
multipartData.forEachPart {
|
||||||
|
when (it) {
|
||||||
|
is PartData.FormItem -> onFormItem(it)
|
||||||
|
is PartData.FileItem -> {
|
||||||
|
when (it.name) {
|
||||||
|
"bytes" -> {
|
||||||
|
val name = FileName(it.originalFileName ?: error("File name is unknown for default part"))
|
||||||
|
resultInput = MPPFile.createTempFile(
|
||||||
|
name.nameWithoutExtension.let {
|
||||||
|
var resultName = it
|
||||||
|
while (resultName.length < 3) {
|
||||||
|
resultName += "_"
|
||||||
|
}
|
||||||
|
resultName
|
||||||
|
},
|
||||||
|
".${name.extension}"
|
||||||
|
).apply {
|
||||||
|
outputStream().use { fileStream ->
|
||||||
|
it.provider().asStream().copyTo(fileStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"data" -> data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional
|
||||||
|
else -> onCustomFileItem(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is PartData.BinaryItem -> onBinaryContent(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val completeData = data ?: error("Data has not been received")
|
||||||
|
(resultInput ?: error("Bytes has not been received")) to (completeData.dataOrNull().let { it as T })
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun ApplicationCall.uniloadMultipartFile(
|
||||||
|
onFormItem: (PartData.FormItem) -> Unit = {},
|
||||||
|
onCustomFileItem: (PartData.FileItem) -> Unit = {},
|
||||||
|
onBinaryContent: (PartData.BinaryItem) -> Unit = {},
|
||||||
|
) = safely {
|
||||||
|
val multipartData = receiveMultipart()
|
||||||
|
|
||||||
|
var resultInput: MPPFile? = null
|
||||||
|
|
||||||
|
multipartData.forEachPart {
|
||||||
|
when (it) {
|
||||||
|
is PartData.FormItem -> onFormItem(it)
|
||||||
|
is PartData.FileItem -> {
|
||||||
|
if (it.name == "bytes") {
|
||||||
|
val name = FileName(it.originalFileName ?: error("File name is unknown for default part"))
|
||||||
|
resultInput = MPPFile.createTempFile(
|
||||||
|
name.nameWithoutExtension.let {
|
||||||
|
var resultName = it
|
||||||
|
while (resultName.length < 3) {
|
||||||
|
resultName += "_"
|
||||||
|
}
|
||||||
|
resultName
|
||||||
|
},
|
||||||
|
".${name.extension}"
|
||||||
|
).apply {
|
||||||
|
outputStream().use { fileStream ->
|
||||||
|
it.provider().asStream().copyTo(fileStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onCustomFileItem(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is PartData.BinaryItem -> onBinaryContent(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resultInput ?: error("Bytes has not been received")
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun ApplicationCall.getParameterOrSendError(
|
suspend fun ApplicationCall.getParameterOrSendError(
|
||||||
field: String
|
field: String
|
||||||
) = parameters[field].also {
|
) = parameters[field].also {
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
package dev.inmo.micro_utils.ktor.server
|
package dev.inmo.micro_utils.ktor.server
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator
|
||||||
import io.ktor.application.Application
|
import io.ktor.application.Application
|
||||||
import io.ktor.server.cio.CIO
|
import io.ktor.server.cio.CIO
|
||||||
import io.ktor.server.engine.*
|
import io.ktor.server.engine.*
|
||||||
@@ -31,3 +32,27 @@ fun createKtorServer(
|
|||||||
port: Int = Random.nextInt(1024, 65535),
|
port: Int = Random.nextInt(1024, 65535),
|
||||||
block: Application.() -> Unit
|
block: Application.() -> Unit
|
||||||
): ApplicationEngine = createKtorServer(CIO, host, port, block)
|
): ApplicationEngine = createKtorServer(CIO, host, port, block)
|
||||||
|
|
||||||
|
fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration> createKtorServer(
|
||||||
|
engine: ApplicationEngineFactory<TEngine, TConfiguration>,
|
||||||
|
host: String = "localhost",
|
||||||
|
port: Int = Random.nextInt(1024, 65535),
|
||||||
|
configurators: List<KtorApplicationConfigurator>
|
||||||
|
): TEngine = createKtorServer(
|
||||||
|
engine,
|
||||||
|
host,
|
||||||
|
port
|
||||||
|
) {
|
||||||
|
configurators.forEach { it.apply { configure() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create server with [CIO] server engine without starting of it
|
||||||
|
*
|
||||||
|
* @see ApplicationEngine.start
|
||||||
|
*/
|
||||||
|
fun createKtorServer(
|
||||||
|
host: String = "localhost",
|
||||||
|
port: Int = Random.nextInt(1024, 65535),
|
||||||
|
configurators: List<KtorApplicationConfigurator>
|
||||||
|
): ApplicationEngine = createKtorServer(CIO, host, port, configurators)
|
||||||
|
@@ -4,8 +4,8 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath libs.buildscript.kt.gradle
|
||||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
classpath libs.buildscript.kt.serialization
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,11 +16,11 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation libs.kt.stdlib
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_core_version"
|
implementation libs.kt.serialization
|
||||||
|
|
||||||
implementation "io.ktor:ktor-client-core:$ktor_version"
|
implementation libs.ktor.client
|
||||||
implementation "io.ktor:ktor-client-java:$ktor_version"
|
implementation libs.ktor.client.java
|
||||||
}
|
}
|
||||||
|
|
||||||
mainClassName="MainKt"
|
mainClassName="MainKt"
|
||||||
|
52
mime_types/mimes_generator/mime_generator.py
Normal file
52
mime_types/mimes_generator/mime_generator.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import pandas as pd
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
def fix_name(category, raw_name):
|
||||||
|
splitted = raw_name.replace('-', '+').replace('.', '+').replace(',', '+').split('+')
|
||||||
|
out1 = ""
|
||||||
|
for s in splitted:
|
||||||
|
out1 += s.capitalize()
|
||||||
|
|
||||||
|
result = ""
|
||||||
|
if out1[0].isdigit():
|
||||||
|
result += category[0].capitalize()
|
||||||
|
result += out1
|
||||||
|
else:
|
||||||
|
result += out1
|
||||||
|
return result
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
df = pd.read_html(open('table.html', 'r'))
|
||||||
|
mimes = []
|
||||||
|
for row in df[0].iterrows():
|
||||||
|
mime = row[1][1]
|
||||||
|
mime_category = mime.split('/', 1)[0]
|
||||||
|
mime_name = mime.split('/', 1)[1]
|
||||||
|
mimes.append({
|
||||||
|
'mime_category': mime_category,
|
||||||
|
'mime_name': mime_name,
|
||||||
|
})
|
||||||
|
|
||||||
|
# codegen
|
||||||
|
|
||||||
|
mimes.sort(key=lambda x: x['mime_category'])
|
||||||
|
grouped = itertools.groupby(mimes, lambda x: x['mime_category'])
|
||||||
|
code = ''
|
||||||
|
code2 = 'internal val knownMimeTypes: Set<MimeType> = setOf(\n'
|
||||||
|
code2 += ' KnownMimeTypes.Any,\n'
|
||||||
|
for key, group in grouped:
|
||||||
|
group_name = key.capitalize()
|
||||||
|
code += '@Serializable(MimeTypeSerializer::class)\nsealed class %s(raw: String) : MimeType, KnownMimeTypes(raw) {\n' % group_name
|
||||||
|
code += ' @Serializable(MimeTypeSerializer::class)\n object Any: %s ("%s/*")\n' % (group_name, key)
|
||||||
|
for mime in group:
|
||||||
|
name = fix_name(mime['mime_category'], mime['mime_name'])
|
||||||
|
code += ' @Serializable(MimeTypeSerializer::class)\n object %s: %s ("%s/%s")\n' % (name, group_name, mime['mime_category'], mime['mime_name'])
|
||||||
|
code2 += ' KnownMimeTypes.%s.%s,\n' % (group_name, name)
|
||||||
|
code += '}\n\n'
|
||||||
|
code2 += ')\n'
|
||||||
|
with open('out1.txt', 'w') as file:
|
||||||
|
file.write(code)
|
||||||
|
with open('out2.txt', 'w') as file:
|
||||||
|
file.write(code2)
|
@@ -23,7 +23,7 @@ kotlin {
|
|||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin('stdlib')
|
implementation kotlin('stdlib')
|
||||||
api "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_core_version"
|
implementation libs.kt.serialization
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
commonTest {
|
commonTest {
|
||||||
@@ -46,8 +46,8 @@ kotlin {
|
|||||||
androidTest {
|
androidTest {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin('test-junit')
|
implementation kotlin('test-junit')
|
||||||
implementation "androidx.test.ext:junit:$test_ext_junit_version"
|
implementation libs.android.test.junit
|
||||||
implementation "androidx.test.espresso:espresso-core:$espresso_core"
|
implementation libs.android.espresso
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
72
mppProjectWithSerializationAndCompose.gradle
Normal file
72
mppProjectWithSerializationAndCompose.gradle
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
project.version = "$version"
|
||||||
|
project.group = "$group"
|
||||||
|
|
||||||
|
apply from: "$publishGradlePath"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvm {
|
||||||
|
compilations.main {
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
js (IR) {
|
||||||
|
browser()
|
||||||
|
nodejs()
|
||||||
|
}
|
||||||
|
android {
|
||||||
|
publishAllLibraryVariants()
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
implementation kotlin('stdlib')
|
||||||
|
implementation libs.kt.serialization
|
||||||
|
implementation compose.runtime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commonTest {
|
||||||
|
dependencies {
|
||||||
|
implementation kotlin('test-common')
|
||||||
|
implementation kotlin('test-annotations-common')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jvmMain {
|
||||||
|
dependencies {
|
||||||
|
implementation compose.desktop.currentOs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jvmTest {
|
||||||
|
dependencies {
|
||||||
|
implementation kotlin('test-junit')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jsMain {
|
||||||
|
dependencies {
|
||||||
|
implementation compose.web.core
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jsTest {
|
||||||
|
dependencies {
|
||||||
|
implementation kotlin('test-js')
|
||||||
|
implementation kotlin('test-junit')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
androidTest {
|
||||||
|
dependencies {
|
||||||
|
implementation kotlin('test-junit')
|
||||||
|
implementation libs.android.test.junit
|
||||||
|
implementation libs.android.espresso
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$defaultAndroidSettingsPresetPath"
|
||||||
|
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
@@ -5,3 +5,13 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api project(":micro_utils.common")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -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]
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
|
@@ -14,7 +14,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
jvmMain {
|
jvmMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api "org.jetbrains.exposed:exposed-core:$kotlin_exposed_version"
|
api libs.jb.exposed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,8 +15,8 @@ kotlin {
|
|||||||
|
|
||||||
jvmMain {
|
jvmMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api "io.ktor:ktor-server:$ktor_version"
|
api libs.ktor.server
|
||||||
api "io.ktor:ktor-server-host-common:$ktor_version"
|
api libs.ktor.server.host.common
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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'
|
||||||
@@ -69,8 +68,19 @@ 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1 +1 @@
|
|||||||
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","includeGpgSigning":true,"developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}]}}
|
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}}}
|
@@ -10,10 +10,10 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
api libs.kt.coroutines
|
||||||
api internalProject("micro_utils.pagination.common")
|
api internalProject("micro_utils.pagination.common")
|
||||||
|
|
||||||
api "com.benasher44:uuid:$uuidVersion"
|
api libs.uuid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api "androidx.core:core-ktx:$core_ktx_version"
|
api libs.android.coreKtx
|
||||||
api internalProject("micro_utils.common")
|
api internalProject("micro_utils.common")
|
||||||
api internalProject("micro_utils.coroutines")
|
api internalProject("micro_utils.coroutines")
|
||||||
}
|
}
|
||||||
|
@@ -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() }
|
||||||
|
@@ -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>,
|
||||||
@@ -34,10 +35,15 @@ open class ExposedOneToManyKeyValueRepo<Key, Value>(
|
|||||||
if (select { keyColumn.eq(k).and(valueColumn.eq(v)) }.limit(1).count() > 0) {
|
if (select { keyColumn.eq(k).and(valueColumn.eq(v)) }.limit(1).count() > 0) {
|
||||||
return@mapNotNull null
|
return@mapNotNull null
|
||||||
}
|
}
|
||||||
insertIgnore {
|
val insertResult = insert {
|
||||||
it[keyColumn] = k
|
it[keyColumn] = k
|
||||||
it[valueColumn] = v
|
it[valueColumn] = v
|
||||||
}.getOrNull(keyColumn) ?.let { k to v }
|
}
|
||||||
|
if (insertResult.insertedCount > 0) {
|
||||||
|
k to v
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
}
|
}
|
||||||
}.forEach { _onNewValue.emit(it) }
|
}.forEach { _onNewValue.emit(it) }
|
||||||
@@ -47,7 +53,7 @@ open class ExposedOneToManyKeyValueRepo<Key, Value>(
|
|||||||
transaction(database) {
|
transaction(database) {
|
||||||
toRemove.keys.flatMap { k ->
|
toRemove.keys.flatMap { k ->
|
||||||
toRemove[k] ?.mapNotNull { v ->
|
toRemove[k] ?.mapNotNull { v ->
|
||||||
if (deleteIgnoreWhere { keyColumn.eq(k).and(valueColumn.eq(v)) } > 0 ) {
|
if (deleteWhere { keyColumn.eq(k).and(valueColumn.eq(v)) } > 0 ) {
|
||||||
k to v
|
k to v
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
@@ -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() }
|
||||||
|
|
||||||
|
@@ -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()
|
||||||
|
@@ -10,8 +10,8 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
api libs.kt.coroutines
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
@@ -21,8 +20,7 @@ open class TypedSerializer<T : Any>(
|
|||||||
element("value", ContextualSerializer(kClass).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
|
||||||
@@ -46,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()
|
||||||
|
@@ -2,6 +2,7 @@ rootProject.name='micro_utils'
|
|||||||
|
|
||||||
String[] includes = [
|
String[] includes = [
|
||||||
":common",
|
":common",
|
||||||
|
":common:compose",
|
||||||
":matrix",
|
":matrix",
|
||||||
":crypto",
|
":crypto",
|
||||||
":selector:common",
|
":selector:common",
|
||||||
@@ -23,6 +24,7 @@ String[] includes = [
|
|||||||
":ktor:common",
|
":ktor:common",
|
||||||
":ktor:client",
|
":ktor:client",
|
||||||
":coroutines",
|
":coroutines",
|
||||||
|
":coroutines:compose",
|
||||||
":android:recyclerview",
|
":android:recyclerview",
|
||||||
":android:alerts:common",
|
":android:alerts:common",
|
||||||
":android:alerts:recyclerview",
|
":android:alerts:recyclerview",
|
||||||
@@ -38,11 +40,13 @@ String[] includes = [
|
|||||||
|
|
||||||
|
|
||||||
includes.each { originalName ->
|
includes.each { originalName ->
|
||||||
String projectDirectory = "${rootProject.projectDir.getAbsolutePath()}${originalName.replaceAll(":", File.separator)}"
|
String projectDirectory = "${rootProject.projectDir.getAbsolutePath()}${originalName.replace(":", File.separator)}"
|
||||||
String projectName = "${rootProject.name}${originalName.replaceAll(":", ".")}"
|
String projectName = "${rootProject.name}${originalName.replace(":", ".")}"
|
||||||
String projectIdentifier = ":${projectName}"
|
String projectIdentifier = ":${projectName}"
|
||||||
include projectIdentifier
|
include projectIdentifier
|
||||||
ProjectDescriptor project = project(projectIdentifier)
|
ProjectDescriptor project = project(projectIdentifier)
|
||||||
project.name = projectName
|
project.name = projectName
|
||||||
project.projectDir = new File(projectDirectory)
|
project.projectDir = new File(projectDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enableFeaturePreview("VERSION_CATALOGS")
|
||||||
|
Reference in New Issue
Block a user