mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 14:29:24 +00:00
Compare commits
149 Commits
Author | SHA1 | Date | |
---|---|---|---|
13ca419473 | |||
b80f1a0773 | |||
e85101c74e | |||
90668bdf63 | |||
e1a00079a5 | |||
e3add4df42 | |||
ced1a3bccb | |||
8d13a14343 | |||
7238e1ea8a | |||
bd0423f243 | |||
b0441c134c | |||
5860901c30 | |||
df4eaea4b9 | |||
e9223d5502 | |||
702c5a3e5d | |||
ccbed95cdc | |||
c4e2c06cf5 | |||
a6135738a3 | |||
ad1ea985b8 | |||
2058950d07 | |||
4dc27f4489 | |||
122daa3220 | |||
e30928e23d | |||
30292306bb | |||
2f9aa585f1 | |||
9e02c3e5ff | |||
7f813a519b | |||
b5072486b4 | |||
50c1cd8215 | |||
ed4812e6d8 | |||
59e0e751f1 | |||
a5ae5e6c2b | |||
a2b87e63c9 | |||
d95c283653 | |||
6a9ae0c148 | |||
040dd517d8 | |||
9663c1ca64 | |||
ffc2d23be7 | |||
49b009e59b | |||
778b6a555b | |||
b3730998e9 | |||
837758aebe | |||
6ac4149aa1 | |||
278584ae6a | |||
126f9d5f41 | |||
ce15ff4e0a | |||
382b956beb | |||
36deab4909 | |||
550fc59d9d | |||
9100a57458 | |||
74d9bbccd9 | |||
75e602a349 | |||
e73644db10 | |||
148e6bdae7 | |||
1b540199f0 | |||
30b70e9984 | |||
c1557cff27 | |||
2d662f91b3 | |||
c4a08e52e5 | |||
08c371c142 | |||
8e62dd460c | |||
1f9302dc94 | |||
16f445f699 | |||
b4abd564ec | |||
14ffafb0a7 | |||
e0cc780887 | |||
ab7d277167 | |||
8308c1df4d | |||
12c29f5180 | |||
2963098870 | |||
9f56b0a26d | |||
75851312fd | |||
3a3be138a5 | |||
fb7d1f18b0 | |||
d1c6c7696a | |||
5f38d9635d | |||
8185ea87b1 | |||
98bd07d025 | |||
c502c70a21 | |||
d933dc532b | |||
42594f0656 | |||
4b83ca19c3 | |||
bab13f5e83 | |||
14c5f5a26c | |||
b26a4f24d4 | |||
498ec673dc | |||
5c67ab6aec | |||
f78359b5d7 | |||
dbce612cb2 | |||
4322ffdb0a | |||
123b848d74 | |||
b120fbd2b1 | |||
d41c3c2de4 | |||
a3cabd7a9e | |||
3dde486126 | |||
5978122dda | |||
4b4806ce34 | |||
987c6cc709 | |||
012c7e9bdf | |||
d06bb265e5 | |||
e2fb8bf21e | |||
60dea2a518 | |||
3877f49278 | |||
a299a5b505 | |||
a03f7201d2 | |||
94e26ee8a0 | |||
2d8ad47083 | |||
e9aec238a1 | |||
844c33166c | |||
cfe9f2159f | |||
61bccd5f48 | |||
e5a608a315 | |||
9b4f35eea1 | |||
602ae2385b | |||
48a4246aac | |||
a4020cb484 | |||
d074f29b82 | |||
a67bef6853 | |||
14e666f18d | |||
cce914091c | |||
d7cd3db8e2 | |||
0d8f844314 | |||
abd0cb2031 | |||
79ef03ed0c | |||
a3087cb650 | |||
9acb9af338 | |||
0c9283eb87 | |||
493d838201 | |||
c0523469fa | |||
4f00eaa6d4 | |||
9706524572 | |||
3174b84367 | |||
6cccd5ff6c | |||
972857268d | |||
f3cd92cc5e | |||
502a49644c | |||
51719b5868 | |||
17821bd094 | |||
6a89ffaa8a | |||
d5a8d0f4d4 | |||
9739bd871e | |||
632d2545d4 | |||
ecfa273a81 | |||
a36828116e | |||
14aa9ca26c | |||
069e51f2ff | |||
a15cbdfb1a | |||
4af8114eda | |||
90dc84e900 |
21
.github/workflows/dokka_push.yml
vendored
Normal file
21
.github/workflows/dokka_push.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Publish KDocs
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
publishing:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
- name: Build
|
||||
run: ./gradlew dokkaHtml
|
||||
- name: Publish KDocs
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./dokka/build/dokka/html
|
||||
publish_branch: kdocs
|
24
.github/workflows/packages_push.yml
vendored
Normal file
24
.github/workflows/packages_push.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
name: Publish package to GitHub Packages
|
||||
on: [push]
|
||||
jobs:
|
||||
publishing:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
- name: Rewrite version
|
||||
run: |
|
||||
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
|
||||
cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp
|
||||
rm gradle.properties
|
||||
mv gradle.properties.tmp gradle.properties
|
||||
- name: Build
|
||||
run: ./gradlew build
|
||||
- name: Publish
|
||||
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidDebugPublication -x signAndroidReleasePublication -x signKotlinMultiplatformPublication
|
||||
env:
|
||||
GITHUBPACKAGES_USER: ${{ github.actor }}
|
||||
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
236
CHANGELOG.md
236
CHANGELOG.md
@@ -1,5 +1,241 @@
|
||||
# Changelog
|
||||
|
||||
## 0.5.2
|
||||
|
||||
* `Ktor`:
|
||||
* `Client`:
|
||||
* Fixes in `UnifiedRequester`
|
||||
|
||||
## 0.5.1
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin Serialization`: `1.2.0` -> `1.2.1`
|
||||
|
||||
## 0.5.0
|
||||
|
||||
**Notice**: This version is still depend on Kotlin
|
||||
Exposed 0.31.1. That means that this version
|
||||
may work improperly in modules based on Kotlin
|
||||
Exposed
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin Exposed`: `0.30.2` -> `0.31.1`
|
||||
* `Kotlin Coroutines`: `1.4.3` -> `1.5.0`
|
||||
* `RecyclerView`: `1.1.0` -> `1.2.0`
|
||||
* `Ktor`: `1.5.3` -> `1.5.4`
|
||||
* `Klock`: `2.0.7` -> `2.1.0`
|
||||
* `UUID`: `0.2.4` -> `0.3.0`
|
||||
* **ALL DEPRECATIONS WERE REMOVED**
|
||||
* `Android`:
|
||||
* `Alerts`:
|
||||
* `RecyclerView`:
|
||||
* Classes `ActionViewHolder` and `ActionsRecyclerViewAdapter` became public
|
||||
* `Coroutines`:
|
||||
* New extension and function `doSynchronously` which are the same as `launchSynchronously`
|
||||
* New extensions `doInDefault` and `doInIO`
|
||||
|
||||
## 0.4.36
|
||||
|
||||
* All `Android` targets inside common mpp modules now includes JVM code
|
||||
|
||||
## 0.4.35
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin Exposed`: `0.30.1` -> `0.30.2`
|
||||
* `Serialization`:
|
||||
* `TypedSerializer`:
|
||||
* Project has been inited
|
||||
|
||||
## 0.4.34
|
||||
|
||||
* `Versions`:
|
||||
* `uuid`: `0.2.3` -> `0.2.4`
|
||||
* `Repos`:
|
||||
* `AbstractExposedCRUDRepo` now implements `StandardCRUDRepo`
|
||||
* `AbstractMutableAndroidCRUDRepo` now implements `StandardCRUDRepo`
|
||||
|
||||
## 0.4.33
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `1.5.2` -> `1.5.3`
|
||||
* `Coroutines`
|
||||
* Add `WeakJob` workaround:
|
||||
* `CoroutineScope#weakLaunch`
|
||||
* `CoroutineScope#weakAsync`
|
||||
|
||||
## 0.4.32
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin Exposed`: `0.29.1` -> `0.30.1`
|
||||
|
||||
## 0.4.31
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.4.31` -> `1.4.32`
|
||||
* `Pagination`:
|
||||
* New extensions `PaginationResult.changeResultsUnchecked` and `PaginationResult.changeResults` for mapping results
|
||||
with the same parameters, but different data
|
||||
* Extension `PaginationResult.thisPageIfNotEmpty` now is typed and will return `PaginationResult?` with the same
|
||||
generic type as income `PaginationResult`
|
||||
* New extension `PaginationResult.currentPageIfNotEmpty` - shortcut for `PaginationResult.thisPageIfNotEmpty`
|
||||
* New common functions. They were created as replacements for currently available for more comfortable work
|
||||
with repos pagination:
|
||||
* `doForAll`
|
||||
* `doForAllWithNextPaging`
|
||||
* `doForAllWithCurrentPaging`
|
||||
* `getAll`
|
||||
* `getAllWithNextPaging`
|
||||
* `getAllWithCurrentPaging`
|
||||
* `Coroutines`:
|
||||
* Rewrite `subscribeSafelyWithoutExceptions`
|
||||
* Now `subscribeSafelyWithoutExceptions` will use default handler instead of skipping
|
||||
* New extension `subscribeSafelySkippingExceptions`
|
||||
* `Repos`
|
||||
* New subproject `repos.cache` - this subproject will contain repos with data caching mechanisms
|
||||
* Most old `doForAll` methods have been deprecated
|
||||
|
||||
## 0.4.30
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.0.6` -> `2.0.7`
|
||||
* `Pagination`:
|
||||
* New variable `defaultPaginationPageSize` has been added to be able to change default pagination size
|
||||
* Add new value `firstPageWithOneElementPagination`
|
||||
|
||||
## 0.4.29
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.4.2` -> `1.4.3`
|
||||
* `Repos`:
|
||||
* `Common`
|
||||
* `Android`:
|
||||
* New `blockingReadableTransaction`/`blockingWritableTransaction`
|
||||
* Android databases realizations now use blocking transactions where it is possible
|
||||
* Several improvements in transactions work
|
||||
|
||||
## 0.4.28
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.4.30` -> `1.4.31`
|
||||
* `Ktor`: `1.5.1` -> `1.5.2`
|
||||
* `Coroutines`
|
||||
* Add `createActionsActor`/`createSafeActionsActor` and `doWithSuspending`
|
||||
|
||||
## 0.4.27
|
||||
|
||||
* `Repos`
|
||||
* `Exposed`
|
||||
* Fix in `AbstractExposedWriteCRUDRepo`
|
||||
|
||||
## 0.4.26
|
||||
|
||||
* `Versions`:
|
||||
* `Serialization`: `1.0.1` -> `1.1.0`
|
||||
|
||||
## 0.4.25
|
||||
|
||||
* `Matrix`:
|
||||
* Subproject has been created
|
||||
|
||||
## 0.4.24
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.4.21` -> `1.4.30`
|
||||
* `Klock`: `2.0.4` -> `2.0.6`
|
||||
* `Coroutines`:
|
||||
* New class `DoWithFirstBuilder`
|
||||
* Several new extensions like `firstOf`/`first`/`invokeOnFirstOf`
|
||||
|
||||
## 0.4.23
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `1.5.0` -> `1.5.1`
|
||||
* `Serialization`
|
||||
* `Base64`
|
||||
* New serializer `Base64BytesToFromStringSerializer` has been added
|
||||
|
||||
## 0.4.22
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.28.1` -> `0.29.1`
|
||||
* `Klock`: `2.0.2` -> `2.0.4`
|
||||
|
||||
## 0.4.21
|
||||
|
||||
* `Common`
|
||||
* `JS`
|
||||
* Extension `DataView#toByteArray` has been added
|
||||
* Extension `ArrayBuffer#toByteArray` has been added
|
||||
* Extension `ByteArray#toDataView` has been added
|
||||
* Extension `ByteArray#toArrayBuffer` has been added
|
||||
* `Coroutines`
|
||||
* `JS`
|
||||
* Extension `Blob#toByteArray` has been added
|
||||
* `Crypto`
|
||||
* Subproject has been created
|
||||
* `Serialization`
|
||||
* `Base64`
|
||||
* Currently, it is not depended on `common` project and use `crypto` instead
|
||||
|
||||
## 0.4.20
|
||||
|
||||
* `Serialization`
|
||||
* `Encapsulator`:
|
||||
* Has been created
|
||||
|
||||
## 0.4.19
|
||||
|
||||
* `Coroutines`:
|
||||
* New extension `Iterable<Deferred>#awaitFirstWithDeferred` has been added to identify which of `Deferred`s was
|
||||
normally completed
|
||||
* New extensions `Iterable<Deferred<T>>#invokeOnFirst` and `Iterable<DeferredAction<*, O>>.invokeFirstOf` have been
|
||||
added
|
||||
|
||||
## 0.4.18
|
||||
|
||||
* `Coroutines`:
|
||||
* New extension `Iterable<Deferred>#awaitFirst` has been added
|
||||
* `Serialization`
|
||||
* `Base 64`
|
||||
* New `Base64ByteArraySerializer` has been added
|
||||
|
||||
## 0.4.17
|
||||
|
||||
* `Common`
|
||||
* Functionality for decode/encode base 64 to/from `ByteArray`/`String` have been added
|
||||
* `Serialization`
|
||||
* `Base 64`
|
||||
* Project has been initiated
|
||||
|
||||
## 0.4.16
|
||||
|
||||
* `Coroutines`:
|
||||
* `safely`:
|
||||
* New `safelyWithoutExceptions` function may accept `onException` parameter with nullable result
|
||||
* Old `safelyWithoutExceptions` now using `defaultSafelyWithoutExceptionHandler` to handle exceptions "like in
|
||||
`safely`", but it is expected that `defaultSafelyWithoutExceptionHandler` will not throw any exception
|
||||
|
||||
## 0.4.15
|
||||
|
||||
* `Coroutines`:
|
||||
* `safely`:
|
||||
* `SafelyExceptionHandlerKey` has been deprecated
|
||||
* `SafelyExceptionHandler` has been deprecated
|
||||
* `ContextSafelyExceptionHandlerKey` has been added
|
||||
* `ContextSafelyExceptionHandler` has been added
|
||||
* `safelyWithContextExceptionHandler` has been added
|
||||
|
||||
## 0.4.14
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.4.20` -> `1.4.21`
|
||||
* `Ktor`: `1.4.3` -> `1.5.0`
|
||||
* `Klock`: `2.0.1` -> `2.0.2`
|
||||
* `Coroutines`:
|
||||
* Add global variable `defaultSafelyExceptionHandler`
|
||||
* Add `SafelyExceptionHandlerKey` and `SafelyExceptionHandler` classes to be able to overwrite
|
||||
`defaultSafelyExceptionHandler` using context of coroutine
|
||||
|
||||
## 0.4.13
|
||||
|
||||
* `Common`
|
||||
|
@@ -22,6 +22,7 @@ You always can look at the <a href="https://github.com/InsanusMokrassar/MicroUti
|
||||
* `ktor` is a set of modules for `client`s and `server`s
|
||||
* `mime_types` is NOT lightweight set of `MimeType`s with a lot of different objected and serializable (with `Kotlin Serialization`) mime types
|
||||
* `pagination` is a complex of modules (explanation in [Complex modules structure](#complex-modules-structure) section) for lightweight pagination
|
||||
* `serialization` is a collection of projects with serializers for `kotlinx.serialization`
|
||||
* `repos` is a complex of modules (explanation in [Complex modules structure](#complex-modules-structure) section) for `KeyValue`/`OneToMany`/`CRUD` repos created to be able to exclude some heavy dependencies when you need some simple and lightweight typical repositories
|
||||
|
||||
## Complex modules structure
|
||||
|
@@ -15,7 +15,7 @@ data class AlertAction(
|
||||
val callback: (DialogInterface) -> Unit
|
||||
)
|
||||
|
||||
private class ActionViewHolder(
|
||||
class ActionViewHolder(
|
||||
container: ViewGroup, dialogInterfaceGetter: () -> DialogInterface
|
||||
) : AbstractStandardViewHolder<AlertAction>(container, android.R.layout.simple_list_item_1) {
|
||||
private lateinit var action: AlertAction
|
||||
@@ -34,7 +34,7 @@ private class ActionViewHolder(
|
||||
}
|
||||
}
|
||||
|
||||
private class ActionsRecyclerViewAdapter(
|
||||
class ActionsRecyclerViewAdapter(
|
||||
data: List<AlertAction>,
|
||||
private val dialogInterfaceGetter: () -> DialogInterface
|
||||
) : RecyclerViewAdapter<AlertAction>(data) {
|
||||
|
11
build.gradle
11
build.gradle
@@ -8,7 +8,7 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.0.2'
|
||||
classpath 'com.android.tools.build:gradle:4.1.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
|
||||
@@ -25,6 +25,15 @@ allprojects {
|
||||
google()
|
||||
maven { url "https://kotlin.bintray.com/kotlinx" }
|
||||
}
|
||||
|
||||
// temporal crutch until legacy tests will be stabled or legacy target will be removed
|
||||
if (it != rootProject.findProject("docs")) {
|
||||
tasks.whenTaskAdded { task ->
|
||||
if(task.name == "jsLegacyBrowserTest" || task.name == "jsLegacyNodeTest") {
|
||||
task.enabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "./extensions.gradle"
|
||||
|
@@ -0,0 +1,15 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import org.khronos.webgl.*
|
||||
|
||||
fun DataView.toByteArray() = ByteArray(this.byteLength) {
|
||||
getInt8(it)
|
||||
}
|
||||
|
||||
fun ArrayBuffer.toByteArray() = Int8Array(this) as ByteArray
|
||||
|
||||
fun ByteArray.toDataView() = DataView(ArrayBuffer(size)).also {
|
||||
forEachIndexed { i, byte -> it.setInt8(i, byte) }
|
||||
}
|
||||
|
||||
fun ByteArray.toArrayBuffer() = toDataView().buffer
|
@@ -0,0 +1,46 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlin.coroutines.*
|
||||
|
||||
interface ActorAction<T> {
|
||||
suspend operator fun invoke(): T
|
||||
}
|
||||
|
||||
/**
|
||||
* Planned to use with [doWithSuspending]. Will execute incoming lambdas sequentially
|
||||
*
|
||||
* @see actor
|
||||
*/
|
||||
fun CoroutineScope.createActionsActor() = actor<suspend () -> Unit> {
|
||||
it()
|
||||
}
|
||||
|
||||
/**
|
||||
* Planned to use with [doWithSuspending]. Will execute incoming lambdas sequentially
|
||||
*
|
||||
* @see safeActor
|
||||
*/
|
||||
inline fun CoroutineScope.createSafeActionsActor(
|
||||
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler
|
||||
) = safeActor<suspend () -> Unit>(Channel.UNLIMITED, onException) {
|
||||
it()
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be use with actor created by [createActionsActor] or [createSafeActionsActor]. Will send lambda which will
|
||||
* execute [action] and return result.
|
||||
*
|
||||
* @see suspendCoroutine
|
||||
* @see safely
|
||||
*/
|
||||
suspend fun <T> Channel<suspend () -> Unit>.doWithSuspending(
|
||||
action: ActorAction<T>
|
||||
) = suspendCoroutine<T> {
|
||||
offer {
|
||||
safely({ e -> it.resumeWithException(e) }) {
|
||||
it.resume(action())
|
||||
}
|
||||
}
|
||||
}
|
@@ -19,7 +19,7 @@ fun <T> CoroutineScope.actor(
|
||||
|
||||
inline fun <T> CoroutineScope.safeActor(
|
||||
channelCapacity: Int = Channel.UNLIMITED,
|
||||
noinline onException: ExceptionHandler<Unit> = {},
|
||||
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||
crossinline block: suspend (T) -> Unit
|
||||
): Channel<T> = actor(
|
||||
channelCapacity
|
||||
|
@@ -0,0 +1,37 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.*
|
||||
|
||||
suspend fun <T> Iterable<Deferred<T>>.awaitFirstWithDeferred(
|
||||
scope: CoroutineScope,
|
||||
cancelOnResult: Boolean = true
|
||||
): Pair<Deferred<T>, T> = suspendCoroutine<Pair<Deferred<T>, T>> { continuation ->
|
||||
scope.launch(SupervisorJob()) {
|
||||
val scope = this
|
||||
forEach {
|
||||
scope.launch {
|
||||
continuation.resume(it to it.await())
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
if (cancelOnResult) {
|
||||
forEach {
|
||||
try {
|
||||
it.cancel()
|
||||
} catch (e: IllegalStateException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> Iterable<Deferred<T>>.awaitFirst(
|
||||
scope: CoroutineScope,
|
||||
cancelOnResult: Boolean = true
|
||||
): T = awaitFirstWithDeferred(scope, cancelOnResult).second
|
||||
suspend fun <T> Iterable<Deferred<T>>.awaitFirst(
|
||||
cancelOthers: Boolean = true
|
||||
): T = awaitFirst(CoroutineScope(coroutineContext), cancelOthers)
|
@@ -0,0 +1,85 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class DeferredAction<T, O>(
|
||||
val deferred: Deferred<T>,
|
||||
val callback: suspend (T) -> O
|
||||
) {
|
||||
suspend operator fun invoke() = callback(deferred.await())
|
||||
}
|
||||
|
||||
class DoWithFirstBuilder<T>(
|
||||
private val scope: CoroutineScope
|
||||
) {
|
||||
private val deferreds = mutableListOf<Deferred<T>>()
|
||||
operator fun plus(block: suspend CoroutineScope.() -> T) {
|
||||
deferreds.add(scope.async(start = CoroutineStart.LAZY, block = block))
|
||||
}
|
||||
inline fun add(noinline block: suspend CoroutineScope.() -> T) = plus(block)
|
||||
inline fun include(noinline block: suspend CoroutineScope.() -> T) = plus(block)
|
||||
|
||||
fun build() = deferreds.toList()
|
||||
}
|
||||
|
||||
fun <T, O> Deferred<T>.buildAction(callback: suspend (T) -> O) = DeferredAction(this, callback)
|
||||
|
||||
suspend fun <O> Iterable<DeferredAction<*, O>>.invokeFirstOf(
|
||||
scope: CoroutineScope,
|
||||
cancelOnResult: Boolean = true
|
||||
): O {
|
||||
return map { it.deferred }.awaitFirstWithDeferred(scope, cancelOnResult).let { result ->
|
||||
first { it.deferred == result.first }.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <O> invokeFirstOf(
|
||||
scope: CoroutineScope,
|
||||
vararg variants: DeferredAction<*, O>,
|
||||
cancelOnResult: Boolean = true
|
||||
): O = variants.toList().invokeFirstOf(scope, cancelOnResult)
|
||||
|
||||
suspend fun <T, O> Iterable<Deferred<T>>.invokeOnFirst(
|
||||
scope: CoroutineScope,
|
||||
cancelOnResult: Boolean = true,
|
||||
callback: suspend (T) -> O
|
||||
): O = map { it.buildAction(callback) }.invokeFirstOf(scope, cancelOnResult)
|
||||
|
||||
suspend fun <T, O> CoroutineScope.invokeOnFirstOf(
|
||||
cancelOnResult: Boolean = true,
|
||||
block: DoWithFirstBuilder<T>.() -> Unit,
|
||||
callback: suspend (T) -> O
|
||||
) = firstOf(
|
||||
DoWithFirstBuilder<T>(this).apply(block).build(),
|
||||
cancelOnResult
|
||||
).let { callback(it) }
|
||||
|
||||
suspend fun <T, O> invokeOnFirst(
|
||||
scope: CoroutineScope,
|
||||
vararg variants: Deferred<T>,
|
||||
cancelOnResult: Boolean = true,
|
||||
callback: suspend (T) -> O
|
||||
): O = variants.toList().invokeOnFirst(scope, cancelOnResult, callback)
|
||||
|
||||
suspend fun <T> CoroutineScope.firstOf(
|
||||
variants: Iterable<Deferred<T>>,
|
||||
cancelOnResult: Boolean = true
|
||||
) = variants.invokeOnFirst(this, cancelOnResult) { it }
|
||||
|
||||
suspend fun <T> CoroutineScope.firstOf(
|
||||
cancelOnResult: Boolean = true,
|
||||
block: DoWithFirstBuilder<T>.() -> Unit
|
||||
) = firstOf(
|
||||
DoWithFirstBuilder<T>(this).apply(block).build(),
|
||||
cancelOnResult
|
||||
)
|
||||
|
||||
suspend fun <T> CoroutineScope.firstOf(
|
||||
vararg variants: Deferred<T>,
|
||||
cancelOnResult: Boolean = true
|
||||
) = firstOf(variants.toList(), cancelOnResult)
|
||||
|
||||
suspend fun <T> List<Deferred<T>>.first(
|
||||
scope: CoroutineScope,
|
||||
cancelOnResult: Boolean = true
|
||||
) = scope.firstOf(this, cancelOnResult)
|
@@ -16,7 +16,7 @@ inline fun <T> Flow<T>.subscribe(scope: CoroutineScope, noinline block: suspend
|
||||
*/
|
||||
inline fun <T> Flow<T>.subscribeSafely(
|
||||
scope: CoroutineScope,
|
||||
noinline onException: ExceptionHandler<Unit> = { throw it },
|
||||
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||
noinline block: suspend (T) -> Unit
|
||||
) = subscribe(scope) {
|
||||
safely(onException) {
|
||||
@@ -25,13 +25,25 @@ inline fun <T> Flow<T>.subscribeSafely(
|
||||
}
|
||||
|
||||
/**
|
||||
* Use [subscribeSafelyWithoutExceptions], but all exceptions inside of [safely] will be skipped
|
||||
* Use [subscribeSafelyWithoutExceptions], but all exceptions will be passed to [defaultSafelyExceptionHandler]
|
||||
*/
|
||||
inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions(
|
||||
scope: CoroutineScope,
|
||||
noinline block: suspend (T) -> Unit
|
||||
) = subscribeSafely(
|
||||
scope,
|
||||
{},
|
||||
block
|
||||
)
|
||||
) = subscribe(scope) {
|
||||
safelyWithoutExceptions {
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use [subscribeSafelyWithoutExceptions], but all exceptions inside of [safely] will be skipped
|
||||
*/
|
||||
inline fun <T> Flow<T>.subscribeSafelySkippingExceptions(
|
||||
scope: CoroutineScope,
|
||||
noinline block: suspend (T) -> Unit
|
||||
) = subscribe(scope) {
|
||||
safelyWithoutExceptions({ /* skip exceptions */ }) {
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
|
@@ -1,30 +1,128 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
typealias ExceptionHandler<T> = suspend (Throwable) -> T
|
||||
|
||||
/**
|
||||
* This instance will be used in all calls of [safely] where exception handler has not been passed
|
||||
*/
|
||||
var defaultSafelyExceptionHandler: ExceptionHandler<Nothing> = { throw it }
|
||||
|
||||
/**
|
||||
* This instance will be used in all calls of [safelyWithoutExceptions] as an exception handler for [safely] call
|
||||
*/
|
||||
var defaultSafelyWithoutExceptionHandler: ExceptionHandler<Unit> = {
|
||||
try {
|
||||
defaultSafelyExceptionHandler(it)
|
||||
} catch (e: Throwable) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This key can (and will) be used to get [ContextSafelyExceptionHandler] from [coroutineContext] of suspend functions
|
||||
* and in [ContextSafelyExceptionHandler] for defining of its [CoroutineContext.Element.key]
|
||||
*
|
||||
* @see safelyWithContextExceptionHandler
|
||||
* @see ContextSafelyExceptionHandler
|
||||
*/
|
||||
object ContextSafelyExceptionHandlerKey : CoroutineContext.Key<ContextSafelyExceptionHandler>
|
||||
|
||||
/**
|
||||
* [ExceptionHandler] wrapper which was created to make possible to use [handler] across all coroutines calls
|
||||
*
|
||||
* @see safelyWithContextExceptionHandler
|
||||
* @see ContextSafelyExceptionHandlerKey
|
||||
*/
|
||||
class ContextSafelyExceptionHandler(
|
||||
val handler: ExceptionHandler<Unit>
|
||||
) : CoroutineContext.Element {
|
||||
override val key: CoroutineContext.Key<*>
|
||||
get() = ContextSafelyExceptionHandlerKey
|
||||
}
|
||||
|
||||
/**
|
||||
* @return [ContextSafelyExceptionHandler] from [coroutineContext] by key [ContextSafelyExceptionHandlerKey] if
|
||||
* exists
|
||||
*
|
||||
* @see ContextSafelyExceptionHandler
|
||||
* @see ContextSafelyExceptionHandlerKey
|
||||
*/
|
||||
suspend inline fun contextSafelyExceptionHandler() = coroutineContext[ContextSafelyExceptionHandlerKey]
|
||||
|
||||
/**
|
||||
* This method will set new [coroutineContext] with [ContextSafelyExceptionHandler]. In case if [coroutineContext]
|
||||
* already contains [ContextSafelyExceptionHandler], [ContextSafelyExceptionHandler.handler] will be used BEFORE
|
||||
* [contextExceptionHandler] in case of exception.
|
||||
*
|
||||
* After all, will be called [withContext] method with created [ContextSafelyExceptionHandler] and block which will call
|
||||
* [safely] method with [safelyExceptionHandler] as onException parameter and [block] as execution block
|
||||
*/
|
||||
suspend fun <T> safelyWithContextExceptionHandler(
|
||||
contextExceptionHandler: ExceptionHandler<Unit>,
|
||||
safelyExceptionHandler: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
): T {
|
||||
val contextSafelyExceptionHandler = contextSafelyExceptionHandler() ?.handler ?.let { oldHandler ->
|
||||
ContextSafelyExceptionHandler {
|
||||
oldHandler(it)
|
||||
contextExceptionHandler(it)
|
||||
}
|
||||
} ?: ContextSafelyExceptionHandler(contextExceptionHandler)
|
||||
return withContext(contextSafelyExceptionHandler) {
|
||||
safely(safelyExceptionHandler, block)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It will run [block] inside of [supervisorScope] to avoid problems with catching of exceptions
|
||||
*
|
||||
* Priorities of [ExceptionHandler]s:
|
||||
*
|
||||
* * [onException] In case if custom (will be used anyway if not [defaultSafelyExceptionHandler])
|
||||
* * [CoroutineContext.get] with [SafelyExceptionHandlerKey] as key
|
||||
* * [defaultSafelyExceptionHandler]
|
||||
*
|
||||
* @param [onException] Will be called when happen exception inside of [block]. By default will throw exception - this
|
||||
* exception will be available for catching
|
||||
*
|
||||
* @see defaultSafelyExceptionHandler
|
||||
* @see safelyWithoutExceptions
|
||||
* @see safelyWithContextExceptionHandler
|
||||
*/
|
||||
suspend inline fun <T> safely(
|
||||
noinline onException: ExceptionHandler<T> = { throw it },
|
||||
noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
): T {
|
||||
return try {
|
||||
supervisorScope(block)
|
||||
} catch (e: Throwable) {
|
||||
coroutineContext[ContextSafelyExceptionHandlerKey] ?.handler ?.invoke(e)
|
||||
onException(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut for [safely] without exception handler (instead of this you will receive null as a result)
|
||||
* Shortcut for [safely] with exception handler, that as expected must return null in case of impossible creating of
|
||||
* result from exception (instead of throwing it)
|
||||
*/
|
||||
suspend inline fun <T> safelyWithoutExceptions(
|
||||
noinline onException: ExceptionHandler<T?>,
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
): T? = safely(onException, block)
|
||||
|
||||
/**
|
||||
* Shortcut for [safely] without exception handler (instead of this you will always receive null as a result)
|
||||
*/
|
||||
suspend inline fun <T> safelyWithoutExceptions(
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
): T? = safely({ null }, block)
|
||||
): T? = safelyWithoutExceptions(
|
||||
{
|
||||
defaultSafelyWithoutExceptionHandler.invoke(it)
|
||||
null
|
||||
},
|
||||
block
|
||||
)
|
||||
|
@@ -0,0 +1,31 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
private fun CoroutineScope.createWeakSubScope() = CoroutineScope(coroutineContext.minusKey(Job)).also { newScope ->
|
||||
coroutineContext.job.invokeOnCompletion { newScope.cancel() }
|
||||
}
|
||||
|
||||
fun CoroutineScope.weakLaunch(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
): Job {
|
||||
val scope = createWeakSubScope()
|
||||
val job = scope.launch(context, start, block)
|
||||
job.invokeOnCompletion { scope.cancel() }
|
||||
return job
|
||||
}
|
||||
|
||||
fun <T> CoroutineScope.weakAsync(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
): Deferred<T> {
|
||||
val scope = createWeakSubScope()
|
||||
val deferred = scope.async(context, start, block)
|
||||
deferred.invokeOnCompletion { scope.cancel() }
|
||||
return deferred
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.await
|
||||
import org.khronos.webgl.Int8Array
|
||||
import org.w3c.fetch.Response
|
||||
import org.w3c.files.Blob
|
||||
|
||||
suspend fun Blob.toByteArray() = Int8Array(
|
||||
Response(this).arrayBuffer().await()
|
||||
) as ByteArray
|
@@ -3,17 +3,16 @@ package dev.inmo.micro_utils.coroutines
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T): T {
|
||||
var throwable: Throwable? = null
|
||||
var result: T? = null
|
||||
val deferred = CompletableDeferred<T>()
|
||||
val objectToSynchronize = java.lang.Object()
|
||||
val launchCallback = {
|
||||
launch {
|
||||
safely(
|
||||
{
|
||||
throwable = it
|
||||
deferred.completeExceptionally(it)
|
||||
}
|
||||
) {
|
||||
result = block()
|
||||
deferred.complete(block())
|
||||
}
|
||||
synchronized(objectToSynchronize) {
|
||||
objectToSynchronize.notifyAll()
|
||||
@@ -24,7 +23,10 @@ fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T
|
||||
launchCallback()
|
||||
objectToSynchronize.wait()
|
||||
}
|
||||
throw throwable ?: return result!!
|
||||
return deferred.getCompleted()
|
||||
}
|
||||
|
||||
fun <T> launchSynchronously(block: suspend CoroutineScope.() -> T): T = CoroutineScope(Dispatchers.Default).launchSynchronously(block)
|
||||
|
||||
fun <T> CoroutineScope.doSynchronously(block: suspend CoroutineScope.() -> T): T = launchSynchronously(block)
|
||||
fun <T> doSynchronously(block: suspend CoroutineScope.() -> T): T = launchSynchronously(block)
|
||||
|
@@ -0,0 +1,34 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class AwaitFirstTests {
|
||||
private fun CoroutineScope.createTestDeferred(value: Int, wait: Long = 100000) = async(start = CoroutineStart.LAZY) { delay(wait); value }
|
||||
@Test
|
||||
fun testThatAwaitFirstIsWorkingCorrectly() {
|
||||
val baseScope = CoroutineScope(Dispatchers.Default)
|
||||
val resultDeferred = baseScope.createTestDeferred(-1, 0)
|
||||
val deferreds = listOf(
|
||||
baseScope.async { createTestDeferred(0) },
|
||||
baseScope.async { createTestDeferred(1) },
|
||||
baseScope.async { createTestDeferred(2) },
|
||||
resultDeferred
|
||||
)
|
||||
val controlJob = baseScope.launch {
|
||||
delay(1000000)
|
||||
}
|
||||
val result = baseScope.launchSynchronously {
|
||||
val result = deferreds.awaitFirst(baseScope)
|
||||
|
||||
assertTrue(baseScope.isActive)
|
||||
assertTrue(controlJob.isActive)
|
||||
|
||||
result
|
||||
}
|
||||
assertEquals(baseScope.launchSynchronously { resultDeferred.await() }, result)
|
||||
assertTrue(deferreds.all { it == resultDeferred || it.isCancelled })
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import dev.inmo.micro_utils.coroutines.asDeferred
|
||||
import dev.inmo.micro_utils.coroutines.launchSynchronously
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.test.*
|
||||
|
||||
class DoWithFirstTests {
|
||||
@Test
|
||||
fun testHandleOneOf() {
|
||||
val scope = CoroutineScope(Dispatchers.Default)
|
||||
val happenedDeferreds = mutableListOf<Int>()
|
||||
val deferredWhichMustHappen = (-1).asDeferred
|
||||
scope.launchSynchronously {
|
||||
scope.launch {
|
||||
((0 until 100).map {
|
||||
DeferredAction(
|
||||
scope.async { delay(10000); it },
|
||||
happenedDeferreds::add
|
||||
)
|
||||
} + DeferredAction(deferredWhichMustHappen, happenedDeferreds::add)).invokeFirstOf(scope)
|
||||
}.join()
|
||||
}
|
||||
assertEquals(1, happenedDeferreds.size)
|
||||
assertEquals(scope.launchSynchronously { deferredWhichMustHappen.await() }, happenedDeferreds.first())
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.test.Test
|
||||
|
||||
class HandleSafelyCoroutineContextTest {
|
||||
@Test
|
||||
fun testHandleSafelyCoroutineContext() {
|
||||
val scope = CoroutineScope(Dispatchers.Default)
|
||||
var contextHandlerHappen = false
|
||||
var localHandlerHappen = false
|
||||
var defaultHandlerHappen = false
|
||||
defaultSafelyExceptionHandler = {
|
||||
defaultHandlerHappen = true
|
||||
throw it
|
||||
}
|
||||
val contextHandler: ExceptionHandler<Unit> = {
|
||||
contextHandlerHappen = true
|
||||
}
|
||||
val checkJob = scope.launch {
|
||||
safelyWithContextExceptionHandler(contextHandler) {
|
||||
safely(
|
||||
{
|
||||
localHandlerHappen = true
|
||||
}
|
||||
) {
|
||||
error("That must happen :)")
|
||||
}
|
||||
println(coroutineContext)
|
||||
error("That must happen too:)")
|
||||
}
|
||||
}
|
||||
launchSynchronously { checkJob.join() }
|
||||
assert(contextHandlerHappen)
|
||||
assert(localHandlerHappen)
|
||||
assert(defaultHandlerHappen)
|
||||
}
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import org.junit.Test
|
||||
|
||||
class WeakJob {
|
||||
@Test
|
||||
fun `test that weak jobs works correctly`() {
|
||||
val scope = CoroutineScope(Dispatchers.Default)
|
||||
lateinit var weakLaunchJob: Job
|
||||
lateinit var weakAsyncJob: Job
|
||||
scope.launchSynchronously {
|
||||
val completeDeferred = Job()
|
||||
coroutineScope {
|
||||
weakLaunchJob = weakLaunch {
|
||||
while (isActive) {
|
||||
delay(100L)
|
||||
}
|
||||
}
|
||||
weakAsyncJob = weakAsync {
|
||||
while (isActive) {
|
||||
delay(100L)
|
||||
}
|
||||
}
|
||||
|
||||
coroutineContext.job.invokeOnCompletion {
|
||||
scope.launch {
|
||||
delay(1000L)
|
||||
completeDeferred.complete()
|
||||
}
|
||||
}
|
||||
launch { delay(1000L); cancel() }
|
||||
}
|
||||
completeDeferred.join()
|
||||
}
|
||||
|
||||
assert(!weakLaunchJob.isActive)
|
||||
assert(!weakAsyncJob.isActive)
|
||||
}
|
||||
}
|
@@ -8,3 +8,11 @@ suspend inline fun <T> doInUI(noinline block: suspend CoroutineScope.() -> T) =
|
||||
Dispatchers.Main,
|
||||
block
|
||||
)
|
||||
suspend inline fun <T> doInDefault(noinline block: suspend CoroutineScope.() -> T) = withContext(
|
||||
Dispatchers.Default,
|
||||
block
|
||||
)
|
||||
suspend inline fun <T> doInIO(noinline block: suspend CoroutineScope.() -> T) = withContext(
|
||||
Dispatchers.IO,
|
||||
block
|
||||
)
|
||||
|
30
crypto/build.gradle
Normal file
30
crypto/build.gradle
Normal file
@@ -0,0 +1,30 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.common")
|
||||
}
|
||||
}
|
||||
jsMain {
|
||||
dependencies {
|
||||
api npm("crypto-js", "$crypto_js_version")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
sourceSets {
|
||||
main {
|
||||
java.srcDirs += [ "src/jvmMain/kotlin" ]
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
import kotlin.experimental.and
|
||||
|
||||
private const val BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
private const val BASE64_MASK: Byte = 0x3f
|
||||
private const val BASE64_PAD = '='
|
||||
private val BASE64_INVERSE_ALPHABET = IntArray(256) {
|
||||
BASE64_ALPHABET.indexOf(it.toChar())
|
||||
}
|
||||
|
||||
internal fun Int.toBase64(): Char = BASE64_ALPHABET[this]
|
||||
internal fun Byte.fromBase64(): Byte = BASE64_INVERSE_ALPHABET[toInt() and 0xff].toByte() and BASE64_MASK
|
||||
|
||||
typealias EncodedBase64String = String
|
||||
typealias EncodedByteArray = ByteArray
|
||||
|
||||
fun SourceString.encodeBase64String(): EncodedBase64String = encodeToByteArray().encodeBase64String()
|
||||
fun SourceString.encodeBase64(): EncodedByteArray = encodeToByteArray().encodeBase64()
|
||||
|
||||
fun SourceBytes.encodeBase64String(): EncodedBase64String = buildString {
|
||||
var i = 0
|
||||
while (this@encodeBase64String.size > i) {
|
||||
val read = kotlin.math.min(3, this@encodeBase64String.size - i)
|
||||
val data = ByteArray(3) {
|
||||
if (it < read) {
|
||||
this@encodeBase64String[it + i]
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
val padSize = (data.size - read) * 8 / 6
|
||||
val chunk = ((data[0].toInt() and 0xFF) shl 16) or
|
||||
((data[1].toInt() and 0xFF) shl 8) or
|
||||
(data[2].toInt() and 0xFF)
|
||||
|
||||
for (index in data.size downTo padSize) {
|
||||
val char = (chunk shr (6 * index)) and BASE64_MASK.toInt()
|
||||
append(char.toBase64())
|
||||
}
|
||||
|
||||
repeat(padSize) { append(BASE64_PAD) }
|
||||
|
||||
i += read
|
||||
}
|
||||
}
|
||||
fun SourceBytes.encodeBase64(): EncodedByteArray = encodeBase64String().encodeToByteArray()
|
||||
|
||||
fun EncodedBase64String.decodeBase64(): SourceBytes = dropLastWhile { it == BASE64_PAD }.encodeToByteArray().decodeBase64()
|
||||
fun EncodedBase64String.decodeBase64String(): SourceString = decodeBase64().decodeToString()
|
||||
|
||||
fun EncodedByteArray.decodeBase64(): SourceBytes {
|
||||
val result = mutableListOf<Byte>()
|
||||
val data = ByteArray(4)
|
||||
(0 until size step 4).forEach { i ->
|
||||
var read = 0
|
||||
for (j in 0 until 4) {
|
||||
if (j + i < size) {
|
||||
data[j] = get(j + i)
|
||||
read++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
val chunk = data.foldIndexed(0) { index, result, current ->
|
||||
result or (current.fromBase64().toInt() shl ((3 - index) * 6))
|
||||
}
|
||||
for (index in data.size - 2 downTo (data.size - read)) {
|
||||
val origin = (chunk shr (8 * index)) and 0xff
|
||||
result.add(origin.toByte())
|
||||
}
|
||||
}
|
||||
|
||||
return result.toByteArray()
|
||||
}
|
||||
fun EncodedByteArray.decodeBase64String(): SourceString = decodeBase64().decodeToString()
|
@@ -0,0 +1,11 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
/**
|
||||
* This typealias shows that corresponding [ByteArray] is an raw data
|
||||
*/
|
||||
typealias SourceBytes = ByteArray
|
||||
/**
|
||||
* This typealias shows that corresponding [String] is an raw data
|
||||
*/
|
||||
typealias SourceString = String
|
||||
|
@@ -0,0 +1,6 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
typealias MD5 = String
|
||||
|
||||
expect fun SourceBytes.md5(): MD5
|
||||
fun SourceString.md5(): MD5 = encodeToByteArray().md5()
|
@@ -0,0 +1,32 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
|
||||
|
||||
class Base64Text {
|
||||
|
||||
val testText = """Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"""
|
||||
val base64Text = """TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdCwgc2VkIGRvIGVpdXNtb2QgdGVtcG9yIGluY2lkaWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWduYSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQgZXhlcmNpdGF0aW9uIHVsbGFtY28gbGFib3JpcyBuaXNpIHV0IGFsaXF1aXAgZXggZWEgY29tbW9kbyBjb25zZXF1YXQuIER1aXMgYXV0ZSBpcnVyZSBkb2xvciBpbiByZXByZWhlbmRlcml0IGluIHZvbHVwdGF0ZSB2ZWxpdCBlc3NlIGNpbGx1bSBkb2xvcmUgZXUgZnVnaWF0IG51bGxhIHBhcmlhdHVyLiBFeGNlcHRldXIgc2ludCBvY2NhZWNhdCBjdXBpZGF0YXQgbm9uIHByb2lkZW50LCBzdW50IGluIGN1bHBhIHF1aSBvZmZpY2lhIGRlc2VydW50IG1vbGxpdCBhbmltIGlkIGVzdCBsYWJvcnVtLgpTZWQgdXQgcGVyc3BpY2lhdGlzIHVuZGUgb21uaXMgaXN0ZSBuYXR1cyBlcnJvciBzaXQgdm9sdXB0YXRlbSBhY2N1c2FudGl1bSBkb2xvcmVtcXVlIGxhdWRhbnRpdW0sIHRvdGFtIHJlbSBhcGVyaWFtLCBlYXF1ZSBpcHNhIHF1YWUgYWIgaWxsbyBpbnZlbnRvcmUgdmVyaXRhdGlzIGV0IHF1YXNpIGFyY2hpdGVjdG8gYmVhdGFlIHZpdGFlIGRpY3RhIHN1bnQgZXhwbGljYWJvLiBOZW1vIGVuaW0gaXBzYW0gdm9sdXB0YXRlbSBxdWlhIHZvbHVwdGFzIHNpdCBhc3Blcm5hdHVyIGF1dCBvZGl0IGF1dCBmdWdpdCwgc2VkIHF1aWEgY29uc2VxdXVudHVyIG1hZ25pIGRvbG9yZXMgZW9zIHF1aSByYXRpb25lIHZvbHVwdGF0ZW0gc2VxdWkgbmVzY2l1bnQuIE5lcXVlIHBvcnJvIHF1aXNxdWFtIGVzdCwgcXVpIGRvbG9yZW0gaXBzdW0gcXVpYSBkb2xvciBzaXQgYW1ldCwgY29uc2VjdGV0dXIsIGFkaXBpc2NpIHZlbGl0LCBzZWQgcXVpYSBub24gbnVtcXVhbSBlaXVzIG1vZGkgdGVtcG9yYSBpbmNpZHVudCB1dCBsYWJvcmUgZXQgZG9sb3JlIG1hZ25hbSBhbGlxdWFtIHF1YWVyYXQgdm9sdXB0YXRlbS4gVXQgZW5pbSBhZCBtaW5pbWEgdmVuaWFtLCBxdWlzIG5vc3RydW0gZXhlcmNpdGF0aW9uZW0gdWxsYW0gY29ycG9yaXMgc3VzY2lwaXQgbGFib3Jpb3NhbSwgbmlzaSB1dCBhbGlxdWlkIGV4IGVhIGNvbW1vZGkgY29uc2VxdWF0dXI/IFF1aXMgYXV0ZW0gdmVsIGV1bSBpdXJlIHJlcHJlaGVuZGVyaXQgcXVpIGluIGVhIHZvbHVwdGF0ZSB2ZWxpdCBlc3NlIHF1YW0gbmloaWwgbW9sZXN0aWFlIGNvbnNlcXVhdHVyLCB2ZWwgaWxsdW0gcXVpIGRvbG9yZW0gZXVtIGZ1Z2lhdCBxdW8gdm9sdXB0YXMgbnVsbGEgcGFyaWF0dXI/"""
|
||||
|
||||
@Test
|
||||
fun decodeEncode() {
|
||||
val encoded = testText.encodeBase64String()
|
||||
assertEquals(base64Text, encoded)
|
||||
val decoded = encoded.decodeBase64String()
|
||||
assertEquals(testText, decoded)
|
||||
}
|
||||
|
||||
val urlTestText = "https://example.com?isitexample=1"
|
||||
val urlTestBase64Text = "aHR0cHM6Ly9leGFtcGxlLmNvbT9pc2l0ZXhhbXBsZT0x"
|
||||
|
||||
@Test
|
||||
fun decodeEncodeUrl() {
|
||||
val encoded = urlTestText.encodeBase64String()
|
||||
assertEquals(urlTestBase64Text, encoded)
|
||||
val decoded = encoded.decodeBase64String()
|
||||
assertEquals(urlTestText, decoded)
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class MD5Test {
|
||||
val source = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
|
||||
val expectedMD5 = "db89bb5ceab87f9c0fcc2ab36c189c2c"
|
||||
@Test
|
||||
fun testMD5() {
|
||||
var i = 0
|
||||
source.md5().forEach {
|
||||
assertEquals(expectedMD5[i], it)
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
external interface CryptoJs {
|
||||
fun MD5(data: String): String
|
||||
}
|
||||
|
||||
@JsModule("crypto-js")
|
||||
@JsNonModule
|
||||
external val CryptoJS: CryptoJs
|
@@ -0,0 +1,3 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
actual fun SourceBytes.md5(): MD5 = CryptoJS.MD5(decodeToString())
|
@@ -0,0 +1,9 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
import java.math.BigInteger
|
||||
import java.security.MessageDigest
|
||||
|
||||
actual fun SourceBytes.md5(): MD5 = BigInteger(
|
||||
1,
|
||||
MessageDigest.getInstance("MD5").digest(this)
|
||||
).toString(16)
|
1
crypto/src/main/AndroidManifest.xml
Normal file
1
crypto/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.crypto"/>
|
@@ -1,5 +1,30 @@
|
||||
apply plugin: 'com.getkeepsafe.dexcount'
|
||||
|
||||
ext {
|
||||
jvmKotlinFolderFile = {
|
||||
String sep = File.separator
|
||||
return new File("${project.projectDir}${sep}src${sep}jvmMain${sep}kotlin")
|
||||
}
|
||||
|
||||
enableIncludingJvmCodeInAndroidPart = {
|
||||
File jvmKotlinFolder = jvmKotlinFolderFile()
|
||||
if (jvmKotlinFolder.exists()) {
|
||||
android.sourceSets.main.java.srcDirs += jvmKotlinFolder.path
|
||||
}
|
||||
}
|
||||
|
||||
disableIncludingJvmCodeInAndroidPart = {
|
||||
File jvmKotlinFolder = jvmKotlinFolderFile()
|
||||
String[] oldDirs = android.sourceSets.main.java.srcDirs
|
||||
android.sourceSets.main.java.srcDirs = []
|
||||
for (oldDir in oldDirs) {
|
||||
if (oldDir != jvmKotlinFolder.path) {
|
||||
android.sourceSets.main.java.srcDirs += oldDir
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion "$android_compileSdkVersion".toInteger()
|
||||
buildToolsVersion "$android_buildToolsVersion"
|
||||
@@ -35,6 +60,8 @@ android {
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
String sep = File.separator
|
||||
main.java.srcDirs += "src${sep}main${sep}kotlin"
|
||||
enableIncludingJvmCodeInAndroidPart()
|
||||
}
|
||||
}
|
@@ -20,11 +20,11 @@ allprojects {
|
||||
|
||||
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
|
||||
|
||||
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization"
|
||||
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject"
|
||||
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject"
|
||||
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
|
||||
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
|
||||
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
|
||||
|
||||
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings"
|
||||
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle"
|
||||
|
||||
publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle"
|
||||
publishMavenPath = "${rootProject.projectDir.absolutePath}/maven.publish.gradle"
|
||||
|
@@ -5,40 +5,45 @@ kotlin.incremental=true
|
||||
kotlin.incremental.js=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
org.gradle.jvmargs=-Xmx2g
|
||||
|
||||
kotlin_version=1.4.20
|
||||
kotlin_coroutines_version=1.4.2
|
||||
kotlin_serialisation_core_version=1.0.1
|
||||
kotlin_exposed_version=0.28.1
|
||||
kotlin_version=1.5.0
|
||||
kotlin_coroutines_version=1.5.0
|
||||
kotlin_serialisation_core_version=1.2.1
|
||||
kotlin_exposed_version=0.31.1
|
||||
|
||||
ktor_version=1.4.3
|
||||
ktor_version=1.5.4
|
||||
|
||||
klockVersion=2.0.1
|
||||
klockVersion=2.1.0
|
||||
|
||||
github_release_plugin_version=2.2.12
|
||||
|
||||
uuidVersion=0.2.3
|
||||
uuidVersion=0.3.0
|
||||
|
||||
# ANDROID
|
||||
|
||||
core_ktx_version=1.3.2
|
||||
androidx_recycler_version=1.1.0
|
||||
androidx_recycler_version=1.2.0
|
||||
appcompat_version=1.2.0
|
||||
|
||||
android_minSdkVersion=19
|
||||
android_compileSdkVersion=30
|
||||
android_buildToolsVersion=30.0.2
|
||||
android_buildToolsVersion=30.0.3
|
||||
dexcount_version=2.0.0
|
||||
junit_version=4.12
|
||||
test_ext_junit_version=1.1.2
|
||||
espresso_core=3.3.0
|
||||
|
||||
# JS NPM
|
||||
|
||||
crypto_js_version=4.0.0
|
||||
|
||||
# Dokka
|
||||
|
||||
dokka_version=1.4.20
|
||||
dokka_version=1.4.32
|
||||
|
||||
# Project data
|
||||
|
||||
group=dev.inmo
|
||||
version=0.4.13
|
||||
android_code_version=17
|
||||
version=0.5.2
|
||||
android_code_version=43
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@@ -25,7 +25,7 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
|
||||
val producerScope = this@channelFlow
|
||||
do {
|
||||
val reconnect = try {
|
||||
safely ({ throw it }) {
|
||||
safely {
|
||||
ws(correctedUrl) {
|
||||
for (received in incoming) {
|
||||
when (received) {
|
||||
|
@@ -15,30 +15,21 @@ class UnifiedRequester(
|
||||
suspend fun <ResultType> uniget(
|
||||
url: String,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>
|
||||
): ResultType = client.get<StandardKtorSerialInputData>(
|
||||
url
|
||||
).let {
|
||||
serialFormat.decodeDefault(resultDeserializer, it)
|
||||
}
|
||||
|
||||
): ResultType = client.uniget(url, resultDeserializer, serialFormat)
|
||||
|
||||
fun <T> encodeUrlQueryValue(
|
||||
serializationStrategy: SerializationStrategy<T>,
|
||||
value: T
|
||||
) = serialFormat.encodeHex(
|
||||
serializationStrategy,
|
||||
value
|
||||
) = serializationStrategy.encodeUrlQueryValue(
|
||||
value,
|
||||
serialFormat
|
||||
)
|
||||
|
||||
suspend fun <BodyType, ResultType> unipost(
|
||||
url: String,
|
||||
bodyInfo: BodyPair<BodyType>,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>
|
||||
) = client.post<StandardKtorSerialInputData>(url) {
|
||||
body = serialFormat.encodeDefault(bodyInfo.first, bodyInfo.second)
|
||||
}.let {
|
||||
serialFormat.decodeDefault(resultDeserializer, it)
|
||||
}
|
||||
) = client.unipost(url, bodyInfo, resultDeserializer, serialFormat)
|
||||
|
||||
fun <T> createStandardWebsocketFlow(
|
||||
url: String,
|
||||
@@ -51,14 +42,30 @@ val defaultRequester = UnifiedRequester()
|
||||
|
||||
suspend fun <ResultType> HttpClient.uniget(
|
||||
url: String,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>
|
||||
) = defaultRequester.uniget(url, resultDeserializer)
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
) = get<StandardKtorSerialInputData>(
|
||||
url
|
||||
).let {
|
||||
serialFormat.decodeDefault(resultDeserializer, it)
|
||||
}
|
||||
|
||||
|
||||
fun <T> SerializationStrategy<T>.encodeUrlQueryValue(value: T) = defaultRequester.encodeUrlQueryValue(this, value)
|
||||
fun <T> SerializationStrategy<T>.encodeUrlQueryValue(
|
||||
value: T,
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
) = serialFormat.encodeHex(
|
||||
this,
|
||||
value
|
||||
)
|
||||
|
||||
suspend fun <BodyType, ResultType> HttpClient.unipost(
|
||||
url: String,
|
||||
bodyInfo: BodyPair<BodyType>,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>
|
||||
) = defaultRequester.unipost(url, bodyInfo, resultDeserializer)
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
) = post<StandardKtorSerialInputData>(url) {
|
||||
body = serialFormat.encodeDefault(bodyInfo.first, bodyInfo.second)
|
||||
}.let {
|
||||
serialFormat.decodeDefault(resultDeserializer, it)
|
||||
}
|
||||
|
7
matrix/build.gradle
Normal file
7
matrix/build.gradle
Normal file
@@ -0,0 +1,7 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
@@ -0,0 +1,3 @@
|
||||
package dev.inmo.micro_utils.matrix
|
||||
|
||||
typealias Matrix<T> = List<Row<T>>
|
@@ -0,0 +1,13 @@
|
||||
package dev.inmo.micro_utils.matrix
|
||||
|
||||
class MatrixBuilder<T> {
|
||||
private val mutMatrix: MutableList<List<T>> = ArrayList()
|
||||
val matrix: Matrix<T>
|
||||
get() = mutMatrix
|
||||
|
||||
fun row(t: List<T>) = mutMatrix.add(t)
|
||||
operator fun List<T>.unaryPlus() = row(this)
|
||||
}
|
||||
|
||||
fun <T> MatrixBuilder<T>.row(block: RowBuilder<T>.() -> Unit) = +RowBuilder<T>().also(block).row
|
||||
fun <T> MatrixBuilder<T>.row(vararg elements: T) = +elements.toList()
|
@@ -0,0 +1,17 @@
|
||||
package dev.inmo.micro_utils.matrix
|
||||
|
||||
fun <T> matrix(block: MatrixBuilder<T>.() -> Unit): Matrix<T> {
|
||||
return MatrixBuilder<T>().also(block).matrix
|
||||
}
|
||||
|
||||
fun <T> flatMatrix(block: RowBuilder<T>.() -> Unit): Matrix<T> {
|
||||
return MatrixBuilder<T>().apply {
|
||||
row(block)
|
||||
}.matrix
|
||||
}
|
||||
|
||||
fun <T> flatMatrix(vararg elements: T): Matrix<T> {
|
||||
return MatrixBuilder<T>().apply {
|
||||
row { elements.forEach { +it } }
|
||||
}.matrix
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
package dev.inmo.micro_utils.matrix
|
||||
|
||||
typealias Row<T> = List<T>
|
@@ -0,0 +1,14 @@
|
||||
package dev.inmo.micro_utils.matrix
|
||||
|
||||
class RowBuilder<T> {
|
||||
private val mutRow: MutableList<T> = ArrayList()
|
||||
val row: Row<T>
|
||||
get() = mutRow
|
||||
|
||||
fun column(t: T) = mutRow.add(t)
|
||||
operator fun T.unaryPlus() = column(this)
|
||||
}
|
||||
|
||||
fun <T> row(block: RowBuilder<T>.() -> Unit): List<T> = RowBuilder<T>().also(block).row
|
||||
fun <T> RowBuilder<T>.columns(elements: List<T>) = elements.forEach(::column)
|
||||
fun <T> RowBuilder<T>.columns(vararg elements: T) = elements.forEach(::column)
|
@@ -0,0 +1,45 @@
|
||||
package dev.inmo.micro_utils.matrix
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class SimpleTest {
|
||||
@Test
|
||||
fun simpleTest() {
|
||||
val expected = listOf(
|
||||
listOf(1, 2, 3),
|
||||
listOf(4, 5, 6)
|
||||
)
|
||||
|
||||
val fromMatrixWithVarargs = matrix<Int> {
|
||||
row(1, 2, 3)
|
||||
row(4, 5, 6)
|
||||
}
|
||||
|
||||
val fromMatrixWithColumnsVarargs = matrix<Int> {
|
||||
row {
|
||||
columns(1, 2, 3)
|
||||
}
|
||||
row {
|
||||
columns(4, 5, 6)
|
||||
}
|
||||
}
|
||||
|
||||
val fromMatrixWithSimpleAdd = matrix<Int> {
|
||||
row {
|
||||
column(1)
|
||||
column(2)
|
||||
column(3)
|
||||
}
|
||||
row {
|
||||
column(4)
|
||||
column(5)
|
||||
column(6)
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(expected, fromMatrixWithVarargs)
|
||||
assertEquals(expected, fromMatrixWithColumnsVarargs)
|
||||
assertEquals(expected, fromMatrixWithSimpleAdd)
|
||||
}
|
||||
}
|
1
matrix/src/main/AndroidManifest.xml
Normal file
1
matrix/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.matrix"/>
|
@@ -4,7 +4,9 @@ project.group = "$group"
|
||||
apply from: "$publishGradlePath"
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
jvm {
|
||||
compilations.main.kotlinOptions.useIR = true
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
@@ -4,7 +4,9 @@ project.group = "$group"
|
||||
apply from: "$publishGradlePath"
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
jvm {
|
||||
compilations.main.kotlinOptions.useIR = true
|
||||
}
|
||||
js (BOTH) {
|
||||
browser()
|
||||
nodejs()
|
@@ -12,6 +12,22 @@ data class PaginationResult<T>(
|
||||
|
||||
fun <T> emptyPaginationResult() = PaginationResult<T>(0, 0, emptyList(), 0)
|
||||
|
||||
/**
|
||||
* @return New [PaginationResult] with [data] without checking of data sizes equality
|
||||
*/
|
||||
fun <I, O> PaginationResult<I>.changeResultsUnchecked(
|
||||
data: List<O>
|
||||
): PaginationResult<O> = PaginationResult(page, pagesNumber, data, size)
|
||||
/**
|
||||
* @return New [PaginationResult] with [data] <b>with</b> checking of data sizes equality
|
||||
*/
|
||||
fun <I, O> PaginationResult<I>.changeResults(
|
||||
data: List<O>
|
||||
): PaginationResult<O> {
|
||||
require(data.size == results.size)
|
||||
return changeResultsUnchecked(data)
|
||||
}
|
||||
|
||||
fun <T> List<T>.createPaginationResult(
|
||||
pagination: Pagination,
|
||||
commonObjectsNumber: Long
|
||||
|
@@ -7,14 +7,17 @@ const val defaultMediumPageSize = 5
|
||||
const val defaultLargePageSize = 10
|
||||
const val defaultExtraLargePageSize = 15
|
||||
|
||||
var defaultPaginationPageSize = defaultMediumPageSize
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE", "FunctionName")
|
||||
inline fun FirstPagePagination(size: Int = defaultMediumPageSize) =
|
||||
inline fun FirstPagePagination(size: Int = defaultPaginationPageSize) =
|
||||
SimplePagination(
|
||||
page = 0,
|
||||
size = size
|
||||
)
|
||||
|
||||
val emptyPagination = Pagination(0, 0)
|
||||
val firstPageWithOneElementPagination = FirstPagePagination(1)
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun Pagination.nextPage() =
|
||||
|
@@ -21,8 +21,10 @@ inline fun PaginationResult<*>.nextPageIfNotEmpty() = if (results.isNotEmpty())
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun PaginationResult<*>.thisPageIfNotEmpty(): Pagination? = if (results.isNotEmpty()) {
|
||||
inline fun <T> PaginationResult<T>.thisPageIfNotEmpty(): PaginationResult<T>? = if (results.isNotEmpty()) {
|
||||
this
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
inline fun <T> PaginationResult<T>.currentPageIfNotEmpty() = thisPageIfNotEmpty()
|
||||
|
@@ -0,0 +1,35 @@
|
||||
package dev.inmo.micro_utils.pagination.utils
|
||||
|
||||
import dev.inmo.micro_utils.pagination.*
|
||||
|
||||
suspend fun <T> doForAll(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
paginationMapper: (PaginationResult<T>) -> Pagination?,
|
||||
block: suspend (Pagination) -> PaginationResult<T>
|
||||
) {
|
||||
doWithPagination(initialPagination) {
|
||||
block(it).let(paginationMapper)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> doForAllWithNextPaging(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
block: suspend (Pagination) -> PaginationResult<T>
|
||||
) {
|
||||
doForAll(
|
||||
initialPagination,
|
||||
{ it.nextPageIfNotEmpty() },
|
||||
block
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun <T> doAllWithCurrentPaging(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
block: suspend (Pagination) -> PaginationResult<T>
|
||||
) {
|
||||
doForAll(
|
||||
initialPagination,
|
||||
{ it.currentPageIfNotEmpty() },
|
||||
block
|
||||
)
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
package dev.inmo.micro_utils.pagination.utils
|
||||
|
||||
import dev.inmo.micro_utils.pagination.*
|
||||
|
||||
suspend fun <T> getAll(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
paginationMapper: (PaginationResult<T>) -> Pagination?,
|
||||
block: suspend (Pagination) -> PaginationResult<T>
|
||||
): List<T> {
|
||||
val results = mutableListOf<T>()
|
||||
doForAll(initialPagination, paginationMapper) {
|
||||
block(it).also {
|
||||
results.addAll(it.results)
|
||||
}
|
||||
}
|
||||
return results.toList()
|
||||
}
|
||||
|
||||
suspend fun <T> getAllWithNextPaging(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
block: suspend (Pagination) -> PaginationResult<T>
|
||||
): List<T> = getAll(
|
||||
initialPagination,
|
||||
{ it.nextPageIfNotEmpty() },
|
||||
block
|
||||
)
|
||||
|
||||
suspend fun <T> getAllWithCurrentPaging(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
block: suspend (Pagination) -> PaginationResult<T>
|
||||
): List<T> = getAll(
|
||||
initialPagination,
|
||||
{ it.currentPageIfNotEmpty() },
|
||||
block
|
||||
)
|
@@ -36,6 +36,6 @@ class PaginatedIterable<T>(
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T> makeIterable(
|
||||
noinline countGetter: () -> Long,
|
||||
pageSize: Int = defaultMediumPageSize,
|
||||
pageSize: Int = defaultPaginationPageSize,
|
||||
noinline paginationResultGetter: Pagination.() -> PaginationResult<T>
|
||||
): Iterable<T> = PaginatedIterable(pageSize, countGetter, paginationResultGetter)
|
||||
|
@@ -18,6 +18,6 @@ val Pagination.asUrlQueryArrayParts
|
||||
val Map<String, String?>.extractPagination: Pagination
|
||||
get() = SimplePagination(
|
||||
get(paginationPageKey) ?.toIntOrNull() ?: 0,
|
||||
get(paginationSizeKey) ?.toIntOrNull() ?: defaultMediumPageSize
|
||||
get(paginationSizeKey) ?.toIntOrNull() ?: defaultPaginationPageSize
|
||||
)
|
||||
|
||||
|
@@ -6,7 +6,7 @@ import io.ktor.http.Parameters
|
||||
val Parameters.extractPagination: Pagination
|
||||
get() = SimplePagination(
|
||||
get("page") ?.toIntOrNull() ?: 0,
|
||||
get("size") ?.toIntOrNull() ?: defaultMediumPageSize
|
||||
get("size") ?.toIntOrNull() ?: defaultPaginationPageSize
|
||||
)
|
||||
|
||||
val ApplicationCall.extractPagination: Pagination
|
||||
|
@@ -1 +0,0 @@
|
||||
{"bintrayConfig":{"repo":"MicroUtils","packageName":"${project.name}","packageVcs":"https://github.com/InsanusMokrassar/MicroUtils","autoPublish":true,"overridePublish":true},"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://git.inmo.dev/InsanusMokrassar/MicroUtils_mirror/src/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"","url":"https://git.inmo.dev/InsanusMokrassar/MicroUtils_mirror","vcsUrl":"ssh://git@git.inmo.dev:8322/InsanusMokrassar/MicroUtils_mirror.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}]}}
|
@@ -1,37 +1,22 @@
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'signing'
|
||||
|
||||
task javadocsJar(type: Jar) {
|
||||
classifier = 'javadoc'
|
||||
}
|
||||
task sourceJar (type : Jar) {
|
||||
classifier = 'sources'
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
project.publishing.publications.all {
|
||||
// rename artifacts
|
||||
groupId "${project.group}"
|
||||
if (it.name.contains('kotlinMultiplatform')) {
|
||||
artifactId = "${project.name}"
|
||||
artifact sourceJar
|
||||
} else {
|
||||
artifactId = "${project.name}-$name"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications.all {
|
||||
artifact javadocsJar
|
||||
|
||||
pom {
|
||||
description = ""
|
||||
description = "It is set of projects with micro tools for avoiding of routines coding"
|
||||
name = "${project.name}"
|
||||
url = "https://git.inmo.dev/InsanusMokrassar/MicroUtils_mirror"
|
||||
url = "https://github.com/InsanusMokrassar/MicroUtils/"
|
||||
|
||||
scm {
|
||||
developerConnection = "scm:git:[fetch=]ssh://git@git.inmo.dev:8322/InsanusMokrassar/MicroUtils_mirror.git[push=]ssh://git@git.inmo.dev:8322/InsanusMokrassar/MicroUtils_mirror.git"
|
||||
url = "ssh://git@git.inmo.dev:8322/InsanusMokrassar/MicroUtils_mirror.git"
|
||||
developerConnection = "scm:git:[fetch=]https://github.com/InsanusMokrassar/MicroUtils.git[push=]https://github.com/InsanusMokrassar/MicroUtils.git"
|
||||
url = "https://github.com/InsanusMokrassar/MicroUtils.git"
|
||||
}
|
||||
|
||||
developers {
|
||||
@@ -55,22 +40,37 @@ publishing {
|
||||
|
||||
license {
|
||||
name = "Apache Software License 2.0"
|
||||
url = "https://git.inmo.dev/InsanusMokrassar/MicroUtils_mirror/src/master/LICENSE"
|
||||
url = "https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
name = "bintray"
|
||||
url = uri("https://api.bintray.com/maven/${project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER')}/MicroUtils/${project.name}/;publish=1;override=1")
|
||||
credentials {
|
||||
username = project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER')
|
||||
password = project.hasProperty('BINTRAY_KEY') ? project.property('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
|
||||
if ((project.hasProperty('GITHUBPACKAGES_USER') || System.getenv('GITHUBPACKAGES_USER') != null) && (project.hasProperty('GITHUBPACKAGES_PASSWORD') || System.getenv('GITHUBPACKAGES_PASSWORD') != null)) {
|
||||
maven {
|
||||
name = "GithubPackages"
|
||||
url = uri("https://maven.pkg.github.com/InsanusMokrassar/MicroUtils")
|
||||
credentials {
|
||||
username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER')
|
||||
password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD')
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
|
||||
maven {
|
||||
name = "sonatype"
|
||||
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
|
||||
credentials {
|
||||
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
|
||||
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signing {
|
||||
useGpgCmd()
|
||||
sign publishing.publications
|
||||
}
|
||||
|
1
publish.kpsb
Normal file
1
publish.kpsb
Normal file
@@ -0,0 +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/"}]}}
|
18
repos/cache/build.gradle
vendored
Normal file
18
repos/cache/build.gradle
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api internalProject("micro_utils.repos.common")
|
||||
api internalProject("micro_utils.repos.inmemory")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/CRUDCacheRepo.kt
vendored
Normal file
24
repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/CRUDCacheRepo.kt
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
package dev.inmo.micro_utils.repos.cache
|
||||
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
open class CRUDCacheRepo<ObjectType, IdType, InputValueType>(
|
||||
protected val parentRepo: CRUDRepo<ObjectType, IdType, InputValueType>,
|
||||
protected val kvCache: KVCache<IdType, ObjectType>,
|
||||
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
|
||||
protected val idGetter: (ObjectType) -> IdType
|
||||
) : CRUDRepo<ObjectType, IdType, InputValueType> by parentRepo {
|
||||
protected val onNewJob = parentRepo.newObjectsFlow.onEach { kvCache.set(idGetter(it), it) }.launchIn(scope)
|
||||
protected val onUpdatedJob = parentRepo.updatedObjectsFlow.onEach { kvCache.set(idGetter(it), it) }.launchIn(scope)
|
||||
protected val onRemoveJob = parentRepo.deletedObjectsIdsFlow.onEach { kvCache.unset(it) }.launchIn(scope)
|
||||
|
||||
override suspend fun getById(id: IdType): ObjectType? = kvCache.get(id) ?: (parentRepo.getById(id) ?.also {
|
||||
kvCache.set(id, it)
|
||||
})
|
||||
|
||||
override suspend fun contains(id: IdType): Boolean = kvCache.contains(id) || parentRepo.contains(id)
|
||||
}
|
41
repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/KVCache.kt
vendored
Normal file
41
repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/KVCache.kt
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
package dev.inmo.micro_utils.repos.cache
|
||||
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import dev.inmo.micro_utils.pagination.utils.getAllWithNextPaging
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
interface KVCache<K, V> : KeyValueRepo<K, V>
|
||||
|
||||
open class SimpleKVCache<K, V>(
|
||||
protected val cachedValuesCount: Int,
|
||||
private val kvParent: KeyValueRepo<K, V> = MapKeyValueRepo<K, V>()
|
||||
) : KVCache<K, V>, KeyValueRepo<K, V> by kvParent {
|
||||
protected open val cacheStack = ArrayList<K>(cachedValuesCount)
|
||||
protected val syncMutex = Mutex()
|
||||
|
||||
protected suspend fun makeUnset(toUnset: List<K>) {
|
||||
cacheStack.removeAll(toUnset)
|
||||
kvParent.unset(toUnset)
|
||||
}
|
||||
|
||||
override suspend fun set(toSet: Map<K, V>) {
|
||||
syncMutex.withLock {
|
||||
if (toSet.size > cachedValuesCount) {
|
||||
cacheStack.clear()
|
||||
|
||||
kvParent.unset(getAllWithNextPaging { kvParent.keys(it) })
|
||||
val keysToInclude = toSet.keys.drop(toSet.size - cachedValuesCount)
|
||||
|
||||
cacheStack.addAll(keysToInclude)
|
||||
kvParent.set(keysToInclude.associateWith { toSet.getValue(it) })
|
||||
} else {
|
||||
makeUnset(cacheStack.take(toSet.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun unset(toUnset: List<K>) {
|
||||
syncMutex.withLock { makeUnset(toUnset) }
|
||||
}
|
||||
}
|
20
repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/KeyValueCacheRepo.kt
vendored
Normal file
20
repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/KeyValueCacheRepo.kt
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
package dev.inmo.micro_utils.repos.cache
|
||||
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
open class KeyValueCacheRepo<Key,Value>(
|
||||
protected val parentRepo: KeyValueRepo<Key, Value>,
|
||||
protected val kvCache: KVCache<Key, Value>,
|
||||
scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
) : KeyValueRepo<Key,Value> by parentRepo {
|
||||
protected val onNewJob = parentRepo.onNewValue.onEach { kvCache.set(it.first, it.second) }.launchIn(scope)
|
||||
protected val onRemoveJob = parentRepo.onValueRemoved.onEach { kvCache.unset(it) }.launchIn(scope)
|
||||
|
||||
override suspend fun get(k: Key): Value? = kvCache.get(k) ?: parentRepo.get(k) ?.also { kvCache.set(k, it) }
|
||||
override suspend fun contains(key: Key): Boolean = kvCache.contains(key) || parentRepo.contains(key)
|
||||
}
|
37
repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/KeyValuesCacheRepo.kt
vendored
Normal file
37
repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/KeyValuesCacheRepo.kt
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
package dev.inmo.micro_utils.repos.cache
|
||||
|
||||
import dev.inmo.micro_utils.pagination.Pagination
|
||||
import dev.inmo.micro_utils.pagination.PaginationResult
|
||||
import dev.inmo.micro_utils.pagination.utils.paginate
|
||||
import dev.inmo.micro_utils.pagination.utils.reverse
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
open class KeyValuesCacheRepo<Key,Value>(
|
||||
protected val parentRepo: KeyValuesRepo<Key, Value>,
|
||||
protected val kvCache: KVCache<Key, List<Value>>,
|
||||
scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
) : KeyValuesRepo<Key,Value> by parentRepo {
|
||||
protected val onNewJob = parentRepo.onNewValue.onEach { kvCache.set(it.first, kvCache.get(it.first) ?.plus(it.second) ?: listOf(it.second)) }.launchIn(scope)
|
||||
protected val onRemoveJob = parentRepo.onValueRemoved.onEach { kvCache.set(it.first, kvCache.get(it.first) ?.minus(it.second) ?: return@onEach) }.launchIn(scope)
|
||||
protected val onDataClearedJob = parentRepo.onDataCleared.onEach { kvCache.unset(it) }.launchIn(scope)
|
||||
|
||||
override suspend fun get(k: Key, pagination: Pagination, reversed: Boolean): PaginationResult<Value> {
|
||||
return kvCache.get(k) ?.paginate(
|
||||
pagination.let { if (reversed) it.reverse(count(k)) else it }
|
||||
) ?.let {
|
||||
if (reversed) it.copy(results = it.results.reversed()) else it
|
||||
} ?: parentRepo.get(k, pagination, reversed)
|
||||
}
|
||||
override suspend fun getAll(k: Key, reversed: Boolean): List<Value> {
|
||||
return kvCache.get(k) ?.let {
|
||||
if (reversed) it.reversed() else it
|
||||
} ?: parentRepo.getAll(k, reversed)
|
||||
}
|
||||
override suspend fun contains(k: Key, v: Value): Boolean = kvCache.get(k) ?.contains(v) ?: parentRepo.contains(k, v)
|
||||
override suspend fun contains(k: Key): Boolean = kvCache.contains(k) || parentRepo.contains(k)
|
||||
}
|
1
repos/cache/src/main/AndroidManifest.xml
vendored
Normal file
1
repos/cache/src/main/AndroidManifest.xml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.repos.cache"/>
|
@@ -30,4 +30,6 @@ kotlin {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disableIncludingJvmCodeInAndroidPart()
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package dev.inmo.micro_utils.repos
|
||||
|
||||
import dev.inmo.micro_utils.pagination.*
|
||||
import dev.inmo.micro_utils.pagination.utils.getAllWithNextPaging
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ReadOneToManyKeyValueRepo<Key, Value> : Repo {
|
||||
@@ -12,14 +13,12 @@ interface ReadOneToManyKeyValueRepo<Key, Value> : Repo {
|
||||
suspend fun count(k: Key): Long
|
||||
suspend fun count(): Long
|
||||
|
||||
suspend fun getAll(k: Key, reversed: Boolean = false): List<Value> = mutableListOf<Value>().also { list ->
|
||||
doWithPagination {
|
||||
get(k, it).also {
|
||||
list.addAll(it.results)
|
||||
}.nextPageIfNotEmpty()
|
||||
}
|
||||
if (reversed) {
|
||||
list.reverse()
|
||||
suspend fun getAll(k: Key, reversed: Boolean = false): List<Value> {
|
||||
val results = getAllWithNextPaging { get(k, it) }
|
||||
return if (reversed) {
|
||||
results.reversed()
|
||||
} else {
|
||||
results
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package dev.inmo.micro_utils.repos
|
||||
|
||||
import dev.inmo.micro_utils.pagination.*
|
||||
import dev.inmo.micro_utils.pagination.utils.doAllWithCurrentPaging
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ReadStandardKeyValueRepo<Key, Value> : Repo {
|
||||
@@ -41,7 +42,7 @@ suspend inline fun <Key, Value> WriteStandardKeyValueRepo<Key, Value>.unsetWithV
|
||||
|
||||
interface StandardKeyValueRepo<Key, Value> : ReadStandardKeyValueRepo<Key, Value>, WriteStandardKeyValueRepo<Key, Value> {
|
||||
override suspend fun unsetWithValues(toUnset: List<Value>) = toUnset.forEach { v ->
|
||||
doWithPagination {
|
||||
doAllWithCurrentPaging {
|
||||
keys(v, it).also {
|
||||
unset(it.results)
|
||||
}
|
||||
|
@@ -1,31 +1,13 @@
|
||||
package dev.inmo.micro_utils.repos.pagination
|
||||
|
||||
import dev.inmo.micro_utils.pagination.*
|
||||
import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging
|
||||
import dev.inmo.micro_utils.pagination.utils.getAllWithNextPaging
|
||||
import dev.inmo.micro_utils.repos.ReadStandardCRUDRepo
|
||||
|
||||
suspend inline fun <T, ID, REPO : ReadStandardCRUDRepo<T, ID>> REPO.doForAll(
|
||||
@Suppress("REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE")
|
||||
methodCaller: suspend REPO.(Pagination) -> PaginationResult<T>,
|
||||
block: (List<T>) -> Unit
|
||||
) {
|
||||
doWithPagination {
|
||||
methodCaller(it).also {
|
||||
block(it.results)
|
||||
}.nextPageIfNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun <T, ID, REPO : ReadStandardCRUDRepo<T, ID>> REPO.doForAll(
|
||||
block: (List<T>) -> Unit
|
||||
) = doForAll({ getByPagination(it) }, block)
|
||||
|
||||
suspend inline fun <T, ID, REPO : ReadStandardCRUDRepo<T, ID>> REPO.getAll(
|
||||
@Suppress("REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE")
|
||||
methodCaller: suspend REPO.(Pagination) -> PaginationResult<T>
|
||||
): List<T> {
|
||||
val resultList = mutableListOf<T>()
|
||||
doForAll(methodCaller) {
|
||||
resultList.addAll(it)
|
||||
}
|
||||
return resultList
|
||||
crossinline methodCaller: suspend REPO.(Pagination) -> PaginationResult<T>
|
||||
): List<T> = getAllWithNextPaging {
|
||||
methodCaller(this, it)
|
||||
}
|
||||
|
@@ -1,31 +1,15 @@
|
||||
package dev.inmo.micro_utils.repos.pagination
|
||||
|
||||
import dev.inmo.micro_utils.pagination.*
|
||||
import dev.inmo.micro_utils.pagination.utils.getAllWithNextPaging
|
||||
import dev.inmo.micro_utils.repos.ReadStandardKeyValueRepo
|
||||
|
||||
suspend inline fun <Key, Value, REPO : ReadStandardKeyValueRepo<Key, Value>> REPO.doForAll(
|
||||
@Suppress("REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE")
|
||||
methodCaller: suspend REPO.(Pagination) -> PaginationResult<Key>,
|
||||
block: (List<Pair<Key, Value>>) -> Unit
|
||||
) {
|
||||
doWithPagination {
|
||||
methodCaller(it).also { keys ->
|
||||
block(keys.results.mapNotNull { key -> get(key) ?.let { value -> key to value } })
|
||||
}.nextPageIfNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun <Key, Value, REPO : ReadStandardKeyValueRepo<Key, Value>> REPO.doForAll(
|
||||
block: (List<Pair<Key, Value>>) -> Unit
|
||||
) = doForAll({ keys(it, false) }, block)
|
||||
|
||||
suspend inline fun <Key, Value, REPO : ReadStandardKeyValueRepo<Key, Value>> REPO.getAll(
|
||||
@Suppress("REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE")
|
||||
methodCaller: suspend REPO.(Pagination) -> PaginationResult<Key>
|
||||
): List<Pair<Key, Value>> {
|
||||
val resultList = mutableListOf<Pair<Key, Value>>()
|
||||
doForAll(methodCaller) {
|
||||
resultList.addAll(it)
|
||||
}
|
||||
return resultList
|
||||
crossinline methodCaller: suspend REPO.(Pagination) -> PaginationResult<Key>
|
||||
): List<Pair<Key, Value>> = getAllWithNextPaging {
|
||||
val result = methodCaller(it)
|
||||
result.changeResultsUnchecked(
|
||||
result.results.mapNotNull { it to (get(it) ?: return@mapNotNull null) }
|
||||
)
|
||||
}
|
||||
|
@@ -1,41 +1,17 @@
|
||||
package dev.inmo.micro_utils.repos.pagination
|
||||
|
||||
import dev.inmo.micro_utils.pagination.*
|
||||
import dev.inmo.micro_utils.pagination.utils.getAllWithNextPaging
|
||||
import dev.inmo.micro_utils.repos.ReadOneToManyKeyValueRepo
|
||||
|
||||
suspend inline fun <Key, Value, REPO : ReadOneToManyKeyValueRepo<Key, Value>> REPO.doForAll(
|
||||
@Suppress("REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE")
|
||||
methodCaller: suspend REPO.(Pagination) -> PaginationResult<Key>,
|
||||
block: (List<Pair<Key, List<Value>>>) -> Unit
|
||||
) {
|
||||
doWithPagination {
|
||||
methodCaller(it).also { keys ->
|
||||
block(
|
||||
keys.results.mapNotNull { key ->
|
||||
val values = mutableListOf<Value>()
|
||||
doWithPagination {
|
||||
get(key, it).also {
|
||||
values.addAll(it.results)
|
||||
}.nextPageIfNotEmpty()
|
||||
}
|
||||
key to values
|
||||
}
|
||||
)
|
||||
}.nextPageIfNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun <Key, Value, REPO : ReadOneToManyKeyValueRepo<Key, Value>> REPO.doForAll(
|
||||
block: (List<Pair<Key, List<Value>>>) -> Unit
|
||||
) = doForAll({ keys(it, false) }, block)
|
||||
|
||||
suspend inline fun <Key, Value, REPO : ReadOneToManyKeyValueRepo<Key, Value>> REPO.getAll(
|
||||
@Suppress("REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE")
|
||||
methodCaller: suspend REPO.(Pagination) -> PaginationResult<Key>
|
||||
): List<Pair<Key, List<Value>>> {
|
||||
val resultList = mutableListOf<Pair<Key, List<Value>>>()
|
||||
doForAll(methodCaller) {
|
||||
resultList.addAll(it)
|
||||
}
|
||||
return resultList
|
||||
crossinline methodCaller: suspend REPO.(Pagination) -> PaginationResult<Key>
|
||||
): List<Pair<Key, List<Value>>> = getAllWithNextPaging {
|
||||
val keysResult = methodCaller(it)
|
||||
keysResult.changeResultsUnchecked(
|
||||
keysResult.results.map { k ->
|
||||
k to getAll(k)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@@ -18,19 +18,16 @@ class StandardVersionsRepo<T>(
|
||||
onCreate: suspend T.() -> Unit,
|
||||
onUpdate: suspend T.(from: Int, to: Int) -> Unit
|
||||
) {
|
||||
var savedVersion = proxy.getTableVersion(tableName)
|
||||
if (savedVersion == null) {
|
||||
var currentVersion = proxy.getTableVersion(tableName)
|
||||
if (currentVersion == null) {
|
||||
proxy.database.onCreate()
|
||||
proxy.updateTableVersion(tableName, version)
|
||||
} else {
|
||||
while (savedVersion != null && savedVersion < version) {
|
||||
val newVersion = savedVersion + 1
|
||||
}
|
||||
while (currentVersion == null || currentVersion < version) {
|
||||
val oldVersion = currentVersion ?: 0
|
||||
currentVersion = oldVersion + 1
|
||||
proxy.database.onUpdate(oldVersion, currentVersion)
|
||||
|
||||
proxy.database.onUpdate(savedVersion, newVersion)
|
||||
|
||||
proxy.updateTableVersion(tableName, newVersion)
|
||||
savedVersion = newVersion
|
||||
}
|
||||
proxy.updateTableVersion(tableName, currentVersion)
|
||||
}
|
||||
}
|
||||
}
|
@@ -26,20 +26,39 @@ fun SQLiteDatabase.createTable(
|
||||
}
|
||||
}
|
||||
|
||||
fun Cursor.getString(columnName: String) = getString(
|
||||
getColumnIndex(columnName)
|
||||
/**
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
fun Cursor.getString(columnName: String): String = getString(
|
||||
getColumnIndexOrThrow(columnName)
|
||||
)
|
||||
|
||||
fun Cursor.getLong(columnName: String) = getLong(
|
||||
getColumnIndex(columnName)
|
||||
/**
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
fun Cursor.getShort(columnName: String): Short = getShort(
|
||||
getColumnIndexOrThrow(columnName)
|
||||
)
|
||||
|
||||
fun Cursor.getInt(columnName: String) = getInt(
|
||||
getColumnIndex(columnName)
|
||||
/**
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
fun Cursor.getLong(columnName: String): Long = getLong(
|
||||
getColumnIndexOrThrow(columnName)
|
||||
)
|
||||
|
||||
fun Cursor.getDouble(columnName: String) = getDouble(
|
||||
getColumnIndex(columnName)
|
||||
/**
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
fun Cursor.getInt(columnName: String): Int = getInt(
|
||||
getColumnIndexOrThrow(columnName)
|
||||
)
|
||||
|
||||
/**
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
fun Cursor.getDouble(columnName: String): Double = getDouble(
|
||||
getColumnIndexOrThrow(columnName)
|
||||
)
|
||||
|
||||
fun SQLiteDatabase.select(
|
||||
|
@@ -32,7 +32,9 @@ private object ContextsPool {
|
||||
|
||||
suspend fun <T> use(block: suspend (CoroutineContext) -> T): T = acquireContext().let {
|
||||
try {
|
||||
block(it)
|
||||
safely {
|
||||
block(it)
|
||||
}
|
||||
} finally {
|
||||
freeContext(it)
|
||||
}
|
||||
@@ -48,11 +50,12 @@ class TransactionContext(
|
||||
}
|
||||
|
||||
suspend fun <T> SQLiteDatabase.transaction(block: suspend SQLiteDatabase.() -> T): T {
|
||||
return coroutineContext[TransactionContext] ?.let {
|
||||
withContext(it.databaseContext) {
|
||||
coroutineContext[TransactionContext] ?.let {
|
||||
return withContext(it.databaseContext) {
|
||||
block()
|
||||
}
|
||||
} ?: ContextsPool.use { context ->
|
||||
}
|
||||
return ContextsPool.use { context ->
|
||||
withContext(TransactionContext(context) + context) {
|
||||
beginTransaction()
|
||||
safely(
|
||||
@@ -70,18 +73,18 @@ suspend fun <T> SQLiteDatabase.transaction(block: suspend SQLiteDatabase.() -> T
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> SQLiteDatabase.inlineTransaction(block: SQLiteDatabase.() -> T): T {
|
||||
inline fun <T> SQLiteDatabase.inlineTransaction(crossinline block: SQLiteDatabase.() -> T): T {
|
||||
return when {
|
||||
inTransaction() -> block()
|
||||
else -> {
|
||||
beginTransaction()
|
||||
try {
|
||||
block().also {
|
||||
setTransactionSuccessful()
|
||||
}
|
||||
block().also { setTransactionSuccessful() }
|
||||
} finally {
|
||||
endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> SQLiteDatabase.blockingTransaction(block: SQLiteDatabase.() -> T): T = inlineTransaction(block)
|
||||
|
@@ -5,32 +5,19 @@ import android.database.DatabaseErrorHandler
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import dev.inmo.micro_utils.coroutines.safely
|
||||
import dev.inmo.micro_utils.repos.keyvalue.keyValueStore
|
||||
import dev.inmo.micro_utils.repos.versions.*
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
private data class CallbackContinuationPair<T> (
|
||||
val callback: suspend SQLiteDatabase.() -> T,
|
||||
val continuation: Continuation<T>
|
||||
) {
|
||||
suspend fun SQLiteDatabase.execute() {
|
||||
safely(
|
||||
{
|
||||
continuation.resumeWithException(it)
|
||||
}
|
||||
) {
|
||||
continuation.resume(callback())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class StandardSQLHelper(
|
||||
context: Context,
|
||||
name: String,
|
||||
factory: SQLiteDatabase.CursorFactory? = null,
|
||||
version: Int = 1,
|
||||
errorHandler: DatabaseErrorHandler? = null
|
||||
errorHandler: DatabaseErrorHandler? = null,
|
||||
useSharedPreferencesForVersions: Boolean = false
|
||||
) {
|
||||
val sqlOpenHelper = object : SQLiteOpenHelper(context, name, factory, version, errorHandler) {
|
||||
override fun onCreate(db: SQLiteDatabase?) {}
|
||||
@@ -38,7 +25,16 @@ class StandardSQLHelper(
|
||||
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {}
|
||||
}
|
||||
val versionsRepo: VersionsRepo<SQLiteOpenHelper> by lazy {
|
||||
StandardVersionsRepo(AndroidSQLStandardVersionsRepoProxy(sqlOpenHelper))
|
||||
StandardVersionsRepo(
|
||||
if (useSharedPreferencesForVersions) {
|
||||
KeyValueBasedVersionsRepoProxy(
|
||||
context.keyValueStore("AndroidSPStandardVersionsRepo"),
|
||||
sqlOpenHelper
|
||||
)
|
||||
} else {
|
||||
AndroidSQLStandardVersionsRepoProxy(sqlOpenHelper)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun <T> writableTransaction(block: suspend SQLiteDatabase.() -> T): T = sqlOpenHelper.writableTransaction(block)
|
||||
@@ -46,6 +42,20 @@ class StandardSQLHelper(
|
||||
suspend fun <T> readableTransaction(block: suspend SQLiteDatabase.() -> T): T = sqlOpenHelper.readableTransaction(block)
|
||||
}
|
||||
|
||||
fun <T> SQLiteOpenHelper.blockingWritableTransaction(block: SQLiteDatabase.() -> T): T {
|
||||
return writableDatabase.blockingTransaction(block)
|
||||
}
|
||||
fun <T> SQLiteOpenHelper.blockingReadableTransaction(block: SQLiteDatabase.() -> T): T {
|
||||
return readableDatabase.blockingTransaction(block)
|
||||
}
|
||||
|
||||
fun <T> StandardSQLHelper.blockingWritableTransaction(block: SQLiteDatabase.() -> T): T {
|
||||
return sqlOpenHelper.blockingWritableTransaction(block)
|
||||
}
|
||||
fun <T> StandardSQLHelper.blockingReadableTransaction(block: SQLiteDatabase.() -> T): T {
|
||||
return sqlOpenHelper.blockingReadableTransaction(block)
|
||||
}
|
||||
|
||||
suspend fun <T> SQLiteOpenHelper.writableTransaction(block: suspend SQLiteDatabase.() -> T): T {
|
||||
return writableDatabase.transaction(block)
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ abstract class AbstractAndroidCRUDRepo<ObjectType, IdType>(
|
||||
it.count
|
||||
}.toLong()
|
||||
|
||||
override suspend fun contains(id: IdType): Boolean = helper.readableTransaction {
|
||||
override suspend fun contains(id: IdType): Boolean = helper.blockingReadableTransaction {
|
||||
select(
|
||||
tableName,
|
||||
null,
|
||||
|
@@ -8,7 +8,8 @@ import kotlinx.coroutines.flow.*
|
||||
abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType>(
|
||||
helper: StandardSQLHelper
|
||||
) : WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>,
|
||||
AbstractAndroidCRUDRepo<ObjectType, IdType>(helper) {
|
||||
AbstractAndroidCRUDRepo<ObjectType, IdType>(helper),
|
||||
StandardCRUDRepo<ObjectType, IdType, InputValueType> {
|
||||
protected val newObjectsChannel = MutableSharedFlow<ObjectType>(64)
|
||||
protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(64)
|
||||
protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(64)
|
||||
@@ -19,9 +20,10 @@ abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType
|
||||
protected abstract suspend fun InputValueType.asContentValues(id: IdType? = null): ContentValues
|
||||
|
||||
override suspend fun create(values: List<InputValueType>): List<ObjectType> {
|
||||
val indexes = helper.writableTransaction {
|
||||
values.map {
|
||||
insert(tableName, null, it.asContentValues())
|
||||
val valuesContentValues = values.map { it.asContentValues() }
|
||||
val indexes = helper.blockingWritableTransaction {
|
||||
valuesContentValues.map {
|
||||
insert(tableName, null, it)
|
||||
}
|
||||
}
|
||||
return helper.readableTransaction {
|
||||
@@ -47,8 +49,8 @@ abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType
|
||||
|
||||
override suspend fun deleteById(ids: List<IdType>) {
|
||||
val deleted = mutableListOf<IdType>()
|
||||
helper.writableTransaction {
|
||||
ids.forEach { id ->
|
||||
helper.blockingWritableTransaction {
|
||||
for (id in ids) {
|
||||
delete(tableName, "$idColumnName=?", arrayOf(id.asId)).also {
|
||||
if (it > 0) {
|
||||
deleted.add(id)
|
||||
@@ -56,15 +58,15 @@ abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType
|
||||
}
|
||||
}
|
||||
}
|
||||
deleted.forEach {
|
||||
deleteObjectsIdsChannel.emit(it)
|
||||
for (deletedItem in deleted) {
|
||||
deleteObjectsIdsChannel.emit(deletedItem)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun update(id: IdType, value: InputValueType): ObjectType? {
|
||||
val asContentValues = value.asContentValues(id)
|
||||
if (asContentValues.keySet().isNotEmpty()) {
|
||||
helper.writableTransaction {
|
||||
helper.blockingWritableTransaction {
|
||||
update(
|
||||
tableName,
|
||||
asContentValues,
|
||||
@@ -79,11 +81,12 @@ abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType
|
||||
}
|
||||
|
||||
override suspend fun update(values: List<UpdatedValuePair<IdType, InputValueType>>): List<ObjectType> {
|
||||
val contentValues = values.map { (id, value) -> id to value.asContentValues(id) }
|
||||
helper.writableTransaction {
|
||||
values.forEach { (id, value) ->
|
||||
contentValues.forEach { (id, contentValues) ->
|
||||
update(
|
||||
tableName,
|
||||
value.asContentValues(id),
|
||||
contentValues,
|
||||
"$idColumnName=?",
|
||||
arrayOf(id.asId)
|
||||
)
|
||||
@@ -98,5 +101,5 @@ abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun count(): Long = helper.readableTransaction { select(tableName).use { it.count.toLong() } }
|
||||
override suspend fun count(): Long = helper.blockingReadableTransaction { select(tableName).use { it.count.toLong() } }
|
||||
}
|
@@ -42,9 +42,9 @@ class KeyValueStore<T : Any> internal constructor (
|
||||
|
||||
init {
|
||||
cachedData ?.let {
|
||||
sharedPreferences.all.forEach {
|
||||
if (it.value != null) {
|
||||
cachedData[it.key] = it.value as Any
|
||||
for ((key, value) in sharedPreferences.all) {
|
||||
if (value != null) {
|
||||
cachedData[key] = value
|
||||
}
|
||||
}
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||
@@ -113,7 +113,7 @@ class KeyValueStore<T : Any> internal constructor (
|
||||
|
||||
override suspend fun set(toSet: Map<String, T>) {
|
||||
sharedPreferences.edit {
|
||||
toSet.forEach { (k, v) ->
|
||||
for ((k, v) in toSet) {
|
||||
when(v) {
|
||||
is Int -> putInt(k, v)
|
||||
is Long -> putLong(k, v)
|
||||
@@ -127,16 +127,20 @@ class KeyValueStore<T : Any> internal constructor (
|
||||
}
|
||||
}
|
||||
}
|
||||
toSet.forEach { (k, v) ->
|
||||
for ((k, v) in toSet) {
|
||||
onNewValueChannel.emit(k to v)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun unset(toUnset: List<String>) {
|
||||
sharedPreferences.edit {
|
||||
toUnset.forEach { remove(it) }
|
||||
for (item in toUnset) {
|
||||
remove(item)
|
||||
}
|
||||
}
|
||||
for (it in toUnset) {
|
||||
_onValueRemovedFlow.emit(it)
|
||||
}
|
||||
toUnset.forEach { _onValueRemovedFlow.emit(it) }
|
||||
}
|
||||
|
||||
override suspend fun unsetWithValues(toUnset: List<T>) {
|
||||
|
@@ -42,22 +42,20 @@ class OneToManyAndroidRepo<Key, Value>(
|
||||
private fun String.asKey(): Key = internalSerialFormat.decodeFromString(keySerializer, this)
|
||||
|
||||
init {
|
||||
runBlocking(DatabaseCoroutineContext) {
|
||||
helper.writableTransaction {
|
||||
createTable(
|
||||
tableName,
|
||||
internalId to internalIdType,
|
||||
idColumnName to ColumnType.Text.NOT_NULLABLE,
|
||||
valueColumnName to ColumnType.Text.NULLABLE
|
||||
)
|
||||
}
|
||||
helper.blockingWritableTransaction {
|
||||
createTable(
|
||||
tableName,
|
||||
internalId to internalIdType,
|
||||
idColumnName to ColumnType.Text.NOT_NULLABLE,
|
||||
valueColumnName to ColumnType.Text.NULLABLE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun add(toAdd: Map<Key, List<Value>>) {
|
||||
val added = mutableListOf<Pair<Key, Value>>()
|
||||
helper.writableTransaction {
|
||||
toAdd.forEach { (k, values) ->
|
||||
helper.blockingWritableTransaction {
|
||||
for ((k, values) in toAdd) {
|
||||
values.forEach { v ->
|
||||
insert(
|
||||
tableName,
|
||||
@@ -78,7 +76,7 @@ class OneToManyAndroidRepo<Key, Value>(
|
||||
}
|
||||
|
||||
override suspend fun clear(k: Key) {
|
||||
helper.writableTransaction {
|
||||
helper.blockingWritableTransaction {
|
||||
delete(tableName, "$idColumnName=?", arrayOf(k.asId()))
|
||||
}.also {
|
||||
if (it > 0) {
|
||||
@@ -88,7 +86,7 @@ class OneToManyAndroidRepo<Key, Value>(
|
||||
}
|
||||
|
||||
override suspend fun set(toSet: Map<Key, List<Value>>) {
|
||||
val (clearedKeys, inserted) = helper.writableTransaction {
|
||||
val (clearedKeys, inserted) = helper.blockingWritableTransaction {
|
||||
toSet.mapNotNull { (k, _) ->
|
||||
if (delete(tableName, "$idColumnName=?", arrayOf(k.asId())) > 0) {
|
||||
k
|
||||
@@ -110,13 +108,13 @@ class OneToManyAndroidRepo<Key, Value>(
|
||||
inserted.forEach { newPair -> _onNewValue.emit(newPair) }
|
||||
}
|
||||
|
||||
override suspend fun contains(k: Key): Boolean = helper.readableTransaction {
|
||||
override suspend fun contains(k: Key): Boolean = helper.blockingReadableTransaction {
|
||||
select(tableName, selection = "$idColumnName=?", selectionArgs = arrayOf(k.asId()), limit = FirstPagePagination(1).limitClause()).use {
|
||||
it.count > 0
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun contains(k: Key, v: Value): Boolean = helper.readableTransaction {
|
||||
override suspend fun contains(k: Key, v: Value): Boolean = helper.blockingReadableTransaction {
|
||||
select(
|
||||
tableName,
|
||||
selection = "$idColumnName=? AND $valueColumnName=?",
|
||||
@@ -127,7 +125,7 @@ class OneToManyAndroidRepo<Key, Value>(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun count(): Long =helper.readableTransaction {
|
||||
override suspend fun count(): Long =helper.blockingReadableTransaction {
|
||||
select(
|
||||
tableName
|
||||
).use {
|
||||
@@ -135,7 +133,7 @@ class OneToManyAndroidRepo<Key, Value>(
|
||||
}
|
||||
}.toLong()
|
||||
|
||||
override suspend fun count(k: Key): Long = helper.readableTransaction {
|
||||
override suspend fun count(k: Key): Long = helper.blockingReadableTransaction {
|
||||
select(tableName, selection = "$idColumnName=?", selectionArgs = arrayOf(k.asId()), limit = FirstPagePagination(1).limitClause()).use {
|
||||
it.count
|
||||
}
|
||||
@@ -147,7 +145,7 @@ class OneToManyAndroidRepo<Key, Value>(
|
||||
reversed: Boolean
|
||||
): PaginationResult<Value> = count(k).let { count ->
|
||||
val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
|
||||
helper.readableTransaction {
|
||||
helper.blockingReadableTransaction {
|
||||
select(
|
||||
tableName,
|
||||
selection = "$idColumnName=?",
|
||||
@@ -173,7 +171,7 @@ class OneToManyAndroidRepo<Key, Value>(
|
||||
reversed: Boolean
|
||||
): PaginationResult<Key> = count().let { count ->
|
||||
val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
|
||||
helper.readableTransaction {
|
||||
helper.blockingReadableTransaction {
|
||||
select(
|
||||
tableName,
|
||||
limit = resultPagination.limitClause()
|
||||
@@ -198,7 +196,7 @@ class OneToManyAndroidRepo<Key, Value>(
|
||||
reversed: Boolean
|
||||
): PaginationResult<Key> = count().let { count ->
|
||||
val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
|
||||
helper.readableTransaction {
|
||||
helper.blockingReadableTransaction {
|
||||
select(
|
||||
tableName,
|
||||
selection = "$valueColumnName=?",
|
||||
@@ -220,7 +218,7 @@ class OneToManyAndroidRepo<Key, Value>(
|
||||
}
|
||||
|
||||
override suspend fun remove(toRemove: Map<Key, List<Value>>) {
|
||||
helper.writableTransaction {
|
||||
helper.blockingWritableTransaction {
|
||||
toRemove.flatMap { (k, vs) ->
|
||||
vs.mapNotNullA { v ->
|
||||
if (delete(tableName, "$idColumnName=? AND $valueColumnName=?", arrayOf(k.asId(), v.asValue())) > 0) {
|
||||
|
@@ -21,14 +21,12 @@ class AndroidSQLStandardVersionsRepoProxy(
|
||||
private val tableVersionColumnName = "version"
|
||||
|
||||
init {
|
||||
runBlocking(DatabaseCoroutineContext) {
|
||||
database.writableTransaction {
|
||||
createTable(
|
||||
tableName,
|
||||
tableNameColumnName to ColumnType.Text.NOT_NULLABLE,
|
||||
tableVersionColumnName to ColumnType.Numeric.INTEGER()
|
||||
)
|
||||
}
|
||||
database.blockingWritableTransaction {
|
||||
createTable(
|
||||
tableName,
|
||||
tableNameColumnName to ColumnType.Text.NOT_NULLABLE,
|
||||
tableVersionColumnName to ColumnType.Numeric.INTEGER()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package dev.inmo.micro_utils.repos.exposed
|
||||
|
||||
import dev.inmo.micro_utils.repos.StandardCRUDRepo
|
||||
|
||||
abstract class AbstractExposedCRUDRepo<ObjectType, IdType, InputValueType>(
|
||||
flowsChannelsSize: Int = 0,
|
||||
tableName: String = ""
|
||||
@@ -8,4 +10,5 @@ abstract class AbstractExposedCRUDRepo<ObjectType, IdType, InputValueType>(
|
||||
flowsChannelsSize,
|
||||
tableName
|
||||
),
|
||||
ExposedCRUDRepo<ObjectType, IdType>
|
||||
ExposedCRUDRepo<ObjectType, IdType>,
|
||||
StandardCRUDRepo<ObjectType, IdType, InputValueType>
|
||||
|
@@ -24,9 +24,7 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
|
||||
override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow()
|
||||
override val deletedObjectsIdsFlow: Flow<IdType> = deleteObjectsIdsChannel.asSharedFlow()
|
||||
|
||||
@Deprecated("Will be removed in near major update. Override open fun with the same name instead")
|
||||
abstract val InsertStatement<Number>.asObject: ObjectType
|
||||
protected open fun InsertStatement<Number>.asObject(value: InputValueType): ObjectType = asObject
|
||||
protected abstract fun InsertStatement<Number>.asObject(value: InputValueType): ObjectType
|
||||
abstract val selectByIds: SqlExpressionBuilder.(List<IdType>) -> Op<Boolean>
|
||||
|
||||
protected abstract fun insert(value: InputValueType, it: InsertStatement<Number>)
|
||||
@@ -60,9 +58,11 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
|
||||
}
|
||||
}.let {
|
||||
if (it > 0) {
|
||||
select {
|
||||
selectById(this, id)
|
||||
}.limit(1).firstOrNull() ?.asObject
|
||||
transaction(db = database) {
|
||||
select {
|
||||
selectById(this, id)
|
||||
}.limit(1).firstOrNull() ?.asObject
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
@@ -1,8 +0,0 @@
|
||||
package dev.inmo.micro_utils.repos.ktor.common.one_to_many
|
||||
|
||||
@Deprecated("Replaced", ReplaceWith("keyParameterName", "dev.inmo.micro_utils.repos.ktor.common.keyParameterName"))
|
||||
const val keyParameterName = dev.inmo.micro_utils.repos.ktor.common.keyParameterName
|
||||
@Deprecated("Replaced", ReplaceWith("valueParameterName", "dev.inmo.micro_utils.repos.ktor.common.valueParameterName"))
|
||||
const val valueParameterName = dev.inmo.micro_utils.repos.ktor.common.valueParameterName
|
||||
@Deprecated("Replaced", ReplaceWith("reversedParameterName", "dev.inmo.micro_utils.repos.ktor.common.reversedParameterName"))
|
||||
const val reversedParameterName = dev.inmo.micro_utils.repos.ktor.common.reversedParameterName
|
17
serialization/base64/build.gradle
Normal file
17
serialization/base64/build.gradle
Normal file
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.crypto")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package dev.inmo.micro_utils.serialization.base64
|
||||
|
||||
import dev.inmo.micro_utils.crypto.*
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
/**
|
||||
* Converts [ByteArray] into base64 encoded string due to serialization and decode base64 encoded string into bytes
|
||||
*/
|
||||
object Base64BytesToFromStringSerializer: KSerializer<ByteArray> {
|
||||
override val descriptor: SerialDescriptor = String.serializer().descriptor
|
||||
override fun deserialize(decoder: Decoder): SourceBytes = decoder.decodeString().decodeBase64()
|
||||
override fun serialize(encoder: Encoder, value: SourceBytes) = encoder.encodeString(value.encodeBase64String())
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
package dev.inmo.micro_utils.serialization.base64
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
open class Base64Serializer<T>(
|
||||
private val converterFrom: (T) -> String,
|
||||
private val converterTo: (String) -> T,
|
||||
) : KSerializer<T> {
|
||||
override val descriptor: SerialDescriptor = Base64BytesToFromStringSerializer.descriptor
|
||||
override fun deserialize(decoder: Decoder): T = converterTo(Base64BytesToFromStringSerializer.deserialize(decoder).decodeToString())
|
||||
override fun serialize(encoder: Encoder, value: T) = Base64BytesToFromStringSerializer.serialize(encoder, converterFrom(value).encodeToByteArray())
|
||||
}
|
||||
|
||||
object Base64StringSerializer : Base64Serializer<String>({ it }, { it })
|
||||
object Base64ByteArraySerializer : Base64Serializer<ByteArray>({ it.decodeToString() }, { it.encodeToByteArray() })
|
1
serialization/base64/src/main/AndroidManifest.xml
Normal file
1
serialization/base64/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.serialization.base64"/>
|
7
serialization/encapsulator/build.gradle
Normal file
7
serialization/encapsulator/build.gradle
Normal file
@@ -0,0 +1,7 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
@@ -0,0 +1,23 @@
|
||||
package dev.inmo.micro_utils.serialization.encapsulator
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
data class Encapsulator<T : Any>(
|
||||
val klass: KClass<T>,
|
||||
val serializer: KSerializer<T>
|
||||
) {
|
||||
fun <O> encapsulate(
|
||||
value: Any,
|
||||
callback: KSerializer<T>.(T) -> O
|
||||
): O? = if (klass.isInstance(value)) {
|
||||
callback(serializer, value as T)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Any> Encapsulator<T>.tryEncode(encoder: Encoder, value: Any) = encapsulate(value) {
|
||||
encoder.encodeSerializableValue(this, it)
|
||||
}
|
1
serialization/encapsulator/src/main/AndroidManifest.xml
Normal file
1
serialization/encapsulator/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.serialization.encapsulator"/>
|
7
serialization/typed_serializer/build.gradle
Normal file
7
serialization/typed_serializer/build.gradle
Normal file
@@ -0,0 +1,7 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
@@ -0,0 +1,74 @@
|
||||
package dev.inmo.micro_utils.serialization.typed_serializer
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.descriptors.*
|
||||
import kotlinx.serialization.encoding.*
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
open class TypedSerializer<T : Any>(
|
||||
kClass: KClass<T>,
|
||||
presetSerializers: Map<String, KSerializer<out T>> = emptyMap()
|
||||
) : KSerializer<T> {
|
||||
protected val serializers = presetSerializers.toMutableMap()
|
||||
@InternalSerializationApi
|
||||
override open val descriptor: SerialDescriptor = buildSerialDescriptor(
|
||||
"TextSourceSerializer",
|
||||
SerialKind.CONTEXTUAL
|
||||
) {
|
||||
element("type", String.serializer().descriptor)
|
||||
element("value", ContextualSerializer(kClass).descriptor)
|
||||
}
|
||||
|
||||
@InternalSerializationApi
|
||||
override open fun deserialize(decoder: Decoder): T {
|
||||
return decoder.decodeStructure(descriptor) {
|
||||
var type: String? = null
|
||||
lateinit var result: T
|
||||
while (true) {
|
||||
when (val index = decodeElementIndex(descriptor)) {
|
||||
0 -> type = decodeStringElement(descriptor, 0)
|
||||
1 -> {
|
||||
require(type != null) { "Type is null, but it is expected that was inited already" }
|
||||
result = decodeSerializableElement(
|
||||
descriptor,
|
||||
1,
|
||||
serializers.getValue(type)
|
||||
)
|
||||
}
|
||||
CompositeDecoder.DECODE_DONE -> break
|
||||
else -> error("Unexpected index: $index")
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@InternalSerializationApi
|
||||
protected open fun <O: T> CompositeEncoder.encode(value: O) {
|
||||
encodeSerializableElement(descriptor, 1, value::class.serializer() as KSerializer<O>, value)
|
||||
}
|
||||
|
||||
@InternalSerializationApi
|
||||
override open fun serialize(encoder: Encoder, value: T) {
|
||||
encoder.encodeStructure(descriptor) {
|
||||
val valueSerializer = value::class.serializer()
|
||||
val type = serializers.keys.first { serializers[it] == valueSerializer }
|
||||
encodeStringElement(descriptor, 0, type)
|
||||
encode(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
open fun <O: T> include(type: String, serializer: KSerializer<O>) {
|
||||
serializers[type] = serializer
|
||||
}
|
||||
|
||||
open fun exclude(type: String) {
|
||||
serializers.remove(type)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> TypedSerializer(
|
||||
presetSerializers: Map<String, KSerializer<out T>> = emptyMap()
|
||||
) = TypedSerializer(T::class, presetSerializers)
|
@@ -0,0 +1,40 @@
|
||||
package dev.inmo.micro_utils.serialization.typed_serializer
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.random.Random
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class TypedSerializerTests {
|
||||
interface Example {
|
||||
val number: Number
|
||||
}
|
||||
val serialFormat = Json { }
|
||||
|
||||
@Serializable
|
||||
data class Example1(override val number: Long) : Example
|
||||
|
||||
@Serializable
|
||||
data class Example2(override val number: Double) : Example
|
||||
|
||||
@Test
|
||||
fun testThatSerializerWorksCorrectly() {
|
||||
val serializer = TypedSerializer(
|
||||
mapOf(
|
||||
"long" to Example1.serializer(),
|
||||
"double" to Example2.serializer()
|
||||
)
|
||||
)
|
||||
|
||||
val value1 = Example1(Random.nextLong())
|
||||
val value2 = Example2(Random.nextDouble())
|
||||
|
||||
val list = listOf(value1, value2)
|
||||
val serialized = serialFormat.encodeToString(ListSerializer(serializer), list)
|
||||
val deserialized = serialFormat.decodeFromString(ListSerializer(serializer), serialized)
|
||||
|
||||
assertEquals(list, deserialized)
|
||||
}
|
||||
}
|
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.serialization.typed_serializer"/>
|
@@ -2,6 +2,8 @@ rootProject.name='micro_utils'
|
||||
|
||||
String[] includes = [
|
||||
":common",
|
||||
":matrix",
|
||||
":crypto",
|
||||
":selector:common",
|
||||
":pagination:common",
|
||||
":pagination:exposed",
|
||||
@@ -9,6 +11,7 @@ String[] includes = [
|
||||
":pagination:ktor:server",
|
||||
":mime_types",
|
||||
":repos:common",
|
||||
":repos:cache",
|
||||
":repos:exposed",
|
||||
":repos:inmemory",
|
||||
":repos:ktor:client",
|
||||
@@ -21,6 +24,9 @@ String[] includes = [
|
||||
":android:recyclerview",
|
||||
":android:alerts:common",
|
||||
":android:alerts:recyclerview",
|
||||
":serialization:base64",
|
||||
":serialization:encapsulator",
|
||||
":serialization:typed_serializer",
|
||||
|
||||
":dokka"
|
||||
]
|
||||
|
Reference in New Issue
Block a user