mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 22:39:25 +00:00
Compare commits
148 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
67c595b440 | |||
830b7aee56 | |||
1890608cb3 | |||
bd396959a9 | |||
d5fe19f0a5 | |||
46bfb09415 | |||
a60cb596d1 | |||
6f9d5e2d5f | |||
80bc226ee1 | |||
12e37184e1 | |||
25e9345d02 | |||
ccc4d030c3 | |||
90c0817b6d | |||
527f7bbafe | |||
765a32729f | |||
de783f77a2 | |||
de4c8d104c | |||
88c8c28f45 | |||
57b36826d1 | |||
f81a2f309b | |||
5fc760f4a5 | |||
091cb38339 | |||
3ae9b3e576 | |||
b03b4cbeec | |||
f2c1b3c76a | |||
f3bec34882 | |||
d6aa9fe9c2 | |||
2d5304a770 | |||
88f2c16c82 | |||
490c318d1c | |||
8beaf61a08 | |||
8b61c984eb | |||
e38094df58 | |||
c25e3f5867 | |||
f78e81d175 | |||
3837ae237d | |||
2b6ef8b4ff | |||
6cc0eefb3e | |||
ab11e28bf7 | |||
26d5f5a5f5 | |||
74ae91cba6 | |||
70509c7edd | |||
5a69bd6c63 | |||
091bb1394f | |||
b82c3864a0 | |||
49ee38a936 | |||
c201866c51 | |||
023f841fb5 | |||
76102e9ab3 | |||
b2fc5e2a4d | |||
55aacb8753 | |||
8702846216 | |||
3347a6d189 | |||
47b3b42949 | |||
e985631621 | |||
e15034bfa4 | |||
e5dbcd25dd | |||
3714c02c12 | |||
fa636b4146 | |||
3d825aebc3 | |||
629884a396 | |||
e7a0fa4e8f | |||
1a41f37a9d | |||
07a65e0bb5 | |||
615f7f99c3 | |||
3de5558ed4 | |||
6bbe3a271f | |||
8bee29f683 | |||
c40f0fdcb9 | |||
30d1453a12 | |||
75fa88b00d | |||
0f817ad212 | |||
d7b46ae0d4 | |||
b6be14ecca | |||
33dbfc6f69 | |||
fcacdcd544 | |||
dd2fc5a86f | |||
0f8a6f6bde | |||
1efd94181d | |||
71ff0232aa | |||
63921cd984 | |||
051e03bed3 | |||
a051394f4f |
16
.github/workflows/gradle_build.yml
vendored
Normal file
16
.github/workflows/gradle_build.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up JDK 1.8
|
||||||
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 1.8
|
||||||
|
- name: Build with Gradle
|
||||||
|
run: ./gradlew build
|
@@ -22,5 +22,6 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- stage: build
|
- stage: build
|
||||||
script: ./gradlew build -s -x jvmTest -x jsIrTest -x jsIrBrowserTest -x jsIrNodeTest -x jsLegacyTest -x jsLegacyBrowserTest -x jsLegacyNodeTest
|
script: ./gradlew build -s -x jvmTest -x jsIrTest -x jsIrBrowserTest -x jsIrNodeTest -x jsLegacyTest -x jsLegacyBrowserTest -x jsLegacyNodeTest
|
||||||
- state: test
|
# Tests are temporarily disabled on public travis due to the problems of launching
|
||||||
script: ./gradlew allTests
|
# - state: test
|
||||||
|
# script: ./gradlew allTests
|
||||||
|
239
CHANGELOG.md
239
CHANGELOG.md
@@ -1,5 +1,242 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.4.27
|
||||||
|
|
||||||
|
## 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`
|
||||||
|
* `Android`
|
||||||
|
* Add expand/collapse functionality for horizontal expand/collapse
|
||||||
|
|
||||||
|
## 0.4.12
|
||||||
|
|
||||||
|
* `Coroutines`
|
||||||
|
* `JVM`
|
||||||
|
* Update `launchSynchronously` signature
|
||||||
|
* `Selector`
|
||||||
|
* Project created
|
||||||
|
|
||||||
|
## 0.4.11
|
||||||
|
|
||||||
|
* `Common`
|
||||||
|
* Add `clamp` function
|
||||||
|
|
||||||
|
## 0.4.10
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Klock`: `2.0.0` -> `2.0.1`
|
||||||
|
* `Repo`
|
||||||
|
* Repo `WriteStandardKeyValueRepo` got new method `unsetWithValues`
|
||||||
|
|
||||||
|
## 0.4.9
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Ktor`: `1.4.2` -> `1.4.3`
|
||||||
|
* `Coroutines`:
|
||||||
|
* `launchSynchronously` has been added in JVM
|
||||||
|
* `Repo`
|
||||||
|
* `Common`
|
||||||
|
* In repos different usages of `BroadcastChannel`s has been replaced with `MutableSharedFlow`
|
||||||
|
* `Exposed`
|
||||||
|
* `asObject` open fun has been added in CRUD realization
|
||||||
|
|
||||||
|
## 0.4.8
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Coroutines`: `1.4.1` -> `1.4.2`
|
||||||
|
* `UUID`: `0.2.2` -> `0.2.3`
|
||||||
|
* `Pagination`
|
||||||
|
* Add `PaginatedIterable` and `PaginatedIterator`
|
||||||
|
|
||||||
|
## 0.4.7
|
||||||
|
|
||||||
|
* `Ktor`
|
||||||
|
* `Client`
|
||||||
|
* New class `UnifiedRequester`
|
||||||
|
* `Server`
|
||||||
|
* New class `UnifiedRouter`
|
||||||
|
* `Repos`
|
||||||
|
* `Ktor`
|
||||||
|
* `Client`
|
||||||
|
* Rewriting of all clients on new `UnifiedRequester`
|
||||||
|
* `Server`
|
||||||
|
* Rewriting of all clients on new `UnifiedRouter`
|
||||||
|
|
||||||
|
## 0.4.6
|
||||||
|
|
||||||
|
* `Common`
|
||||||
|
* New annotation `Warning` has been added
|
||||||
|
* `Pagination`
|
||||||
|
* `Common`
|
||||||
|
* `Pagination` got new extension: `Pagination#isFirstPage`
|
||||||
|
* `Coroutines`:
|
||||||
|
* New extension `FlowCollector#invoke` has been added
|
||||||
|
* `Repos`
|
||||||
|
* `Common`
|
||||||
|
* `JVM` (and `Android` since `Android API 26`):
|
||||||
|
* `FileStandardKeyValueRepo` has been added
|
||||||
|
* Add several `typealias`es for each type of repos
|
||||||
|
|
||||||
|
## 0.4.5
|
||||||
|
|
||||||
|
* `Android`
|
||||||
|
* `Alerts`
|
||||||
|
* `Common`
|
||||||
|
* Project has been created
|
||||||
|
* `RecyclerView`
|
||||||
|
* Project has been created
|
||||||
|
* `Common`
|
||||||
|
* Annotation `PreviewFeature` has been added
|
||||||
|
* `Android`
|
||||||
|
* Added tools to work with visibility in more comfortable way
|
||||||
|
* Added tools to work with disabled/enabled state in more comfortable way
|
||||||
|
* Added tools to work with expanded/collapsed state in more comfortable way (in preview mode)
|
||||||
|
|
||||||
|
## 0.4.4
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Klock`: `1.12.1` -> `2.0.0`
|
||||||
|
* `Commons`:
|
||||||
|
* Update left items functionality to include work with `GridLayoutManager`
|
||||||
|
* `Repos`:
|
||||||
|
* Add interface `VersionsRepo`
|
||||||
|
* Add default realization of `VersionsRepo` named `StandardVersionsRepo` which use `StandardVersionsRepoProxy`
|
||||||
|
to get access to some end-store
|
||||||
|
* Add default realization of `StandardVersionsRepoProxy` based on `KeyValue` repos
|
||||||
|
* Add realizations of `StandardVersionsRepoProxy` for exposed and android (`SQL` and `SharedPreferences`)
|
||||||
|
* `Commons`:
|
||||||
|
* In Android fully reworked transactions functions
|
||||||
|
* Now `DatabaseCoroutineContext` is a shortcut for `Dispatchers.IO`
|
||||||
|
|
||||||
|
## 0.4.3
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `1.4.10` -> `1.4.20`
|
||||||
|
* `Common`:
|
||||||
|
* Two new extensions for Android:
|
||||||
|
* `Resources#getSp`
|
||||||
|
* `Resources#getDp`
|
||||||
|
|
||||||
|
## 0.4.2
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* Add `WriteOneToManyKeyValueRepo#set` function and extensions
|
||||||
|
|
||||||
|
## 0.4.1
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* Fixed error in `ExposedKeyValueRepo` related to negative size of shared flow
|
||||||
|
* Fixed error in `ExposedKeyValueRepo` related to avoiding of table initiation
|
||||||
|
|
||||||
## 0.4.0
|
## 0.4.0
|
||||||
|
|
||||||
* `Repos`:
|
* `Repos`:
|
||||||
@@ -224,4 +461,4 @@ All deprecations has been removed
|
|||||||
|
|
||||||
## 0.1.0
|
## 0.1.0
|
||||||
|
|
||||||
Inited :)
|
Inited :)
|
||||||
|
38
README.md
38
README.md
@@ -1 +1,37 @@
|
|||||||
# MicroUtils
|
# MicroUtils
|
||||||
|
|
||||||
|
This is a library with collection of tools for working in Kotlin environment. First of all, this library collection is oriented to use next technologies:
|
||||||
|
|
||||||
|
* [`Kotlin Coroutines`](https://github.com/Kotlin/kotlinx.coroutines)
|
||||||
|
* [`Kotlin Serialization`](https://github.com/Kotlin/kotlinx.serialization)
|
||||||
|
* [`Kotlin Exposed`](https://github.com/JetBrains/Exposed)
|
||||||
|
* [`Ktor`](https://ktor.io)
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary> <b>Android environment</b> </summary>
|
||||||
|
|
||||||
|
You always can look at the <a href="https://github.com/InsanusMokrassar/MicroUtils/blob/master/gradle.properties#L24-L34">properties file</a> to get information about current project dependencies, compile and build tools for `Android` target.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Projects
|
||||||
|
|
||||||
|
* `common` contains common tools for platform which usually are absent out-of-the-box when you starting project
|
||||||
|
* `selector` contains tools to use `Selector` interface with things like `RecyclerView` in android or other selection needs
|
||||||
|
* `coroutines` is a module for `Kotlin Coroutines` with different things like subscribing on flows (`onEach` + `launchIn` shortcut :) )
|
||||||
|
* `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
|
||||||
|
|
||||||
|
Most of complex modules are built with next hierarchy:
|
||||||
|
|
||||||
|
* `common` submodule for `API` things which are common for all platforms
|
||||||
|
* `exposed` submodule contains realizations for exposed tables
|
||||||
|
* `ktor` submodule is usually unavailable directly, because it contains its own submodules for clients and servers
|
||||||
|
* `common` part contains routes which are common for clients and servers
|
||||||
|
* `client` submodule contains clients which are usually using `UnifiedRequester` to make requests using routes from `ktor/common` module and some internal logic of requests
|
||||||
|
* `server` submodule (in most cases `JVM`-only) contains some extensions for `Route` instances which usually will give opportunity to proxy internet requests from `ktor/client` realization to some proxy object
|
||||||
|
1
_config.yml
Normal file
1
_config.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
theme: jekyll-theme-cayman
|
17
android/alerts/common/build.gradle
Normal file
17
android/alerts/common/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: "$mppAndroidProjectPresetPath"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
androidMain {
|
||||||
|
dependencies {
|
||||||
|
api "androidx.appcompat:appcompat-resources:$appcompat_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
android/alerts/common/src/main/AndroidManifest.xml
Normal file
1
android/alerts/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest package="dev.inmo.micro_utils.android.alerts.common"/>
|
@@ -0,0 +1,55 @@
|
|||||||
|
@file:Suppress("NOTHING_TO_INLINE", "unused")
|
||||||
|
|
||||||
|
package dev.inmo.micro_utils.android.alerts.common
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
|
||||||
|
typealias AlertDialogCallback = (DialogInterface) -> Unit
|
||||||
|
|
||||||
|
inline fun Context.createAlertDialogTemplate(
|
||||||
|
title: String? = null,
|
||||||
|
positivePair: Pair<String, AlertDialogCallback?>? = null,
|
||||||
|
neutralPair: Pair<String, AlertDialogCallback?>? = null,
|
||||||
|
negativePair: Pair<String, AlertDialogCallback?>? = null
|
||||||
|
): AlertDialog.Builder {
|
||||||
|
val builder = AlertDialog.Builder(this)
|
||||||
|
|
||||||
|
title ?.let {
|
||||||
|
builder.setTitle(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
positivePair ?. let {
|
||||||
|
builder.setPositiveButton(it.first) { di, _ -> it.second ?. invoke(di) }
|
||||||
|
}
|
||||||
|
negativePair ?. let {
|
||||||
|
builder.setNegativeButton(it.first) { di, _ -> it.second ?. invoke(di) }
|
||||||
|
}
|
||||||
|
neutralPair ?. let {
|
||||||
|
builder.setNeutralButton(it.first) { di, _ -> it.second ?. invoke(di) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun Context.createAlertDialogTemplateWithResources(
|
||||||
|
title: Int? = null,
|
||||||
|
positivePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
neutralPair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
negativePair: Pair<Int, AlertDialogCallback?>? = null
|
||||||
|
): AlertDialog.Builder = createAlertDialogTemplate(
|
||||||
|
title ?.let { getString(it) },
|
||||||
|
positivePair ?.let { getString(it.first) to it.second },
|
||||||
|
neutralPair ?.let { getString(it.first) to it.second },
|
||||||
|
negativePair ?.let { getString(it.first) to it.second }
|
||||||
|
)
|
||||||
|
|
||||||
|
inline fun AlertDialog.setDismissChecker(noinline checker: () -> Boolean) : AlertDialog {
|
||||||
|
setOnDismissListener {
|
||||||
|
if (!checker()) {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
@@ -0,0 +1,38 @@
|
|||||||
|
@file:Suppress("NOTHING_TO_INLINE", "unused")
|
||||||
|
|
||||||
|
package dev.inmo.micro_utils.android.alerts.common
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
|
||||||
|
inline fun <T: View> Context.createCustomViewAlertDialog(
|
||||||
|
title: String? = null,
|
||||||
|
positivePair: Pair<String, AlertDialogCallback?>? = null,
|
||||||
|
neutralPair: Pair<String, AlertDialogCallback?>? = null,
|
||||||
|
negativePair: Pair<String, AlertDialogCallback?>? = null,
|
||||||
|
show: Boolean = true,
|
||||||
|
viewCreator: (Context) -> T
|
||||||
|
): AlertDialog = createAlertDialogTemplate(
|
||||||
|
title, positivePair, neutralPair, negativePair
|
||||||
|
).apply {
|
||||||
|
setView(viewCreator(this@createCustomViewAlertDialog))
|
||||||
|
}.create().apply {
|
||||||
|
if (show) show()
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T: View> Context.createCustomViewAlertDialogWithResources(
|
||||||
|
title: Int? = null,
|
||||||
|
positivePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
neutralPair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
negativePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
show: Boolean = true,
|
||||||
|
viewCreator: (Context) -> T
|
||||||
|
): AlertDialog = createCustomViewAlertDialog(
|
||||||
|
title ?.let { getString(it) },
|
||||||
|
positivePair ?.let { getString(it.first) to it.second },
|
||||||
|
neutralPair ?.let { getString(it.first) to it.second },
|
||||||
|
negativePair ?.let { getString(it.first) to it.second },
|
||||||
|
show,
|
||||||
|
viewCreator
|
||||||
|
)
|
@@ -0,0 +1,45 @@
|
|||||||
|
@file:Suppress("NOTHING_TO_INLINE", "unused")
|
||||||
|
|
||||||
|
package dev.inmo.micro_utils.android.alerts.common
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
|
||||||
|
inline fun Context.createSimpleTextAlertDialog(
|
||||||
|
text: String,
|
||||||
|
title: String? = null,
|
||||||
|
positivePair: Pair<String, AlertDialogCallback?>? = null,
|
||||||
|
neutralPair: Pair<String, AlertDialogCallback?>? = null,
|
||||||
|
negativePair: Pair<String, AlertDialogCallback?>? = null,
|
||||||
|
show: Boolean = true
|
||||||
|
): AlertDialog = createAlertDialogTemplate(
|
||||||
|
title,
|
||||||
|
positivePair,
|
||||||
|
neutralPair,
|
||||||
|
negativePair
|
||||||
|
).apply {
|
||||||
|
setMessage(text)
|
||||||
|
}.create().apply {
|
||||||
|
if (show) {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun Context.createSimpleTextAlertDialog(
|
||||||
|
@StringRes
|
||||||
|
text: Int,
|
||||||
|
@StringRes
|
||||||
|
title: Int? = null,
|
||||||
|
positivePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
neutralPair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
negativePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
show: Boolean = true
|
||||||
|
): AlertDialog = createSimpleTextAlertDialog(
|
||||||
|
getString(text),
|
||||||
|
title ?.let { getString(it) },
|
||||||
|
positivePair ?.let { getString(it.first) to it.second },
|
||||||
|
neutralPair ?.let { getString(it.first) to it.second },
|
||||||
|
negativePair ?.let { getString(it.first) to it.second },
|
||||||
|
show
|
||||||
|
)
|
18
android/alerts/recyclerview/build.gradle
Normal file
18
android/alerts/recyclerview/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
|
id "com.android.library"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$mppAndroidProjectPresetPath"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api internalProject("micro_utils.android.alerts.common")
|
||||||
|
api internalProject("micro_utils.android.recyclerview")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
android/alerts/recyclerview/src/main/AndroidManifest.xml
Normal file
1
android/alerts/recyclerview/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest package="dev.inmo.micro_utils.android.alerts.recyclerview"/>
|
@@ -0,0 +1,65 @@
|
|||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
|
package dev.inmo.micro_utils.android.alerts.recyclerview
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import dev.inmo.micro_utils.android.alerts.common.AlertDialogCallback
|
||||||
|
import dev.inmo.micro_utils.android.recyclerview.*
|
||||||
|
|
||||||
|
data class AlertAction(
|
||||||
|
val title: String,
|
||||||
|
val callback: (DialogInterface) -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
private class ActionViewHolder(
|
||||||
|
container: ViewGroup, dialogInterfaceGetter: () -> DialogInterface
|
||||||
|
) : AbstractStandardViewHolder<AlertAction>(container, android.R.layout.simple_list_item_1) {
|
||||||
|
private lateinit var action: AlertAction
|
||||||
|
private val textView: TextView
|
||||||
|
get() = itemView.findViewById(android.R.id.text1)
|
||||||
|
|
||||||
|
init {
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
action.callback(dialogInterfaceGetter())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(item: AlertAction) {
|
||||||
|
action = item
|
||||||
|
textView.text = item.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ActionsRecyclerViewAdapter(
|
||||||
|
data: List<AlertAction>,
|
||||||
|
private val dialogInterfaceGetter: () -> DialogInterface
|
||||||
|
) : RecyclerViewAdapter<AlertAction>(data) {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<AlertAction> = ActionViewHolder(
|
||||||
|
parent, dialogInterfaceGetter
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.createActionsAlertDialog(
|
||||||
|
actions: List<AlertAction>,
|
||||||
|
title: Int? = null,
|
||||||
|
positivePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
neutralPair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
negativePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
show: Boolean = true
|
||||||
|
): AlertDialog {
|
||||||
|
lateinit var dialogInterface: DialogInterface
|
||||||
|
|
||||||
|
return createRecyclerViewDialog(
|
||||||
|
title, positivePair, neutralPair, negativePair, show
|
||||||
|
) {
|
||||||
|
ActionsRecyclerViewAdapter(
|
||||||
|
actions
|
||||||
|
) {
|
||||||
|
dialogInterface
|
||||||
|
}
|
||||||
|
}.also { dialogInterface = it }
|
||||||
|
}
|
@@ -0,0 +1,43 @@
|
|||||||
|
package dev.inmo.micro_utils.android.alerts.recyclerview
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import dev.inmo.micro_utils.android.alerts.common.AlertDialogCallback
|
||||||
|
import dev.inmo.micro_utils.android.alerts.common.createCustomViewAlertDialogWithResources
|
||||||
|
|
||||||
|
fun Context.createRecyclerViewDialog(
|
||||||
|
title: Int? = null,
|
||||||
|
positivePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
neutralPair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
negativePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
show: Boolean = true,
|
||||||
|
layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(this),
|
||||||
|
marginOfRecyclerView: Int = 8, // dp
|
||||||
|
recyclerViewSetUp: RecyclerView.() -> Unit = {},
|
||||||
|
adapterFactory: () -> RecyclerView.Adapter<*>
|
||||||
|
): AlertDialog {
|
||||||
|
val recyclerView = RecyclerView(this).apply {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
).apply {
|
||||||
|
setMargins(marginOfRecyclerView, marginOfRecyclerView, marginOfRecyclerView, marginOfRecyclerView)
|
||||||
|
}
|
||||||
|
this.layoutManager = layoutManager
|
||||||
|
adapter = adapterFactory()
|
||||||
|
recyclerViewSetUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
return createCustomViewAlertDialogWithResources(
|
||||||
|
title,
|
||||||
|
positivePair,
|
||||||
|
neutralPair,
|
||||||
|
negativePair,
|
||||||
|
show
|
||||||
|
) {
|
||||||
|
recyclerView
|
||||||
|
}
|
||||||
|
}
|
@@ -2,7 +2,6 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
id "kotlin-android-extensions"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppAndroidProjectPresetPath"
|
apply from: "$mppAndroidProjectPresetPath"
|
||||||
|
@@ -1,14 +1,20 @@
|
|||||||
package dev.inmo.micro_utils.android.recyclerview
|
package dev.inmo.micro_utils.android.recyclerview
|
||||||
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.*
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
private inline fun RecyclerView.LayoutManager.findLastVisibleItemPositionGetter(): (() -> Int)? = when (this) {
|
||||||
|
is LinearLayoutManager -> ::findLastVisibleItemPosition
|
||||||
|
is GridLayoutManager -> ::findLastVisibleItemPosition
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
fun RecyclerView.lastVisibleItemFlow(
|
fun RecyclerView.lastVisibleItemFlow(
|
||||||
completingScope: CoroutineScope
|
completingScope: CoroutineScope
|
||||||
): Flow<Int> {
|
): Flow<Int> {
|
||||||
val lastVisibleElementFun: () -> Int = (layoutManager as? LinearLayoutManager) ?.let { it::findLastVisibleItemPosition } ?: error("Currently supported only linear layout manager")
|
val lastVisibleElementFun: () -> Int = layoutManager ?.findLastVisibleItemPositionGetter() ?: error("Currently supported only linear and grid layout manager")
|
||||||
val lastVisibleFlow = MutableStateFlow(lastVisibleElementFun())
|
val lastVisibleFlow = MutableStateFlow(lastVisibleElementFun())
|
||||||
addOnScrollListener(
|
addOnScrollListener(
|
||||||
object : RecyclerView.OnScrollListener() {
|
object : RecyclerView.OnScrollListener() {
|
||||||
|
@@ -8,10 +8,9 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.0.0'
|
classpath 'com.android.tools.build:gradle:4.0.2'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||||
classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$gradle_bintray_plugin_version"
|
|
||||||
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
|
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
|
||||||
classpath "com.github.breadmoirai:github-release:$github_release_plugin_version"
|
classpath "com.github.breadmoirai:github-release:$github_release_plugin_version"
|
||||||
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
|
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
|
||||||
|
@@ -2,7 +2,6 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
id "kotlin-android-extensions"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
@@ -0,0 +1,37 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
@RequiresOptIn(
|
||||||
|
"It is possible, that behaviour of this thing will be changed or removed in future releases",
|
||||||
|
RequiresOptIn.Level.WARNING
|
||||||
|
)
|
||||||
|
@Target(
|
||||||
|
AnnotationTarget.CLASS,
|
||||||
|
AnnotationTarget.CONSTRUCTOR,
|
||||||
|
AnnotationTarget.FIELD,
|
||||||
|
AnnotationTarget.PROPERTY,
|
||||||
|
AnnotationTarget.PROPERTY_GETTER,
|
||||||
|
AnnotationTarget.PROPERTY_SETTER,
|
||||||
|
AnnotationTarget.FUNCTION,
|
||||||
|
AnnotationTarget.TYPE,
|
||||||
|
AnnotationTarget.TYPEALIAS,
|
||||||
|
AnnotationTarget.TYPE_PARAMETER
|
||||||
|
)
|
||||||
|
annotation class PreviewFeature
|
||||||
|
|
||||||
|
@RequiresOptIn(
|
||||||
|
"This thing is marked as warned. See message of warn to get more info",
|
||||||
|
RequiresOptIn.Level.WARNING
|
||||||
|
)
|
||||||
|
@Target(
|
||||||
|
AnnotationTarget.CLASS,
|
||||||
|
AnnotationTarget.CONSTRUCTOR,
|
||||||
|
AnnotationTarget.FIELD,
|
||||||
|
AnnotationTarget.PROPERTY,
|
||||||
|
AnnotationTarget.PROPERTY_GETTER,
|
||||||
|
AnnotationTarget.PROPERTY_SETTER,
|
||||||
|
AnnotationTarget.FUNCTION,
|
||||||
|
AnnotationTarget.TYPE,
|
||||||
|
AnnotationTarget.TYPEALIAS,
|
||||||
|
AnnotationTarget.TYPE_PARAMETER
|
||||||
|
)
|
||||||
|
annotation class Warning(val message: String)
|
@@ -0,0 +1,89 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
const val replacedInCrypto = "Replaced in crypto subproject"
|
||||||
|
|
||||||
|
@Deprecated(replacedInCrypto, ReplaceWith("EncodedBase64String", "dev.inmo.micro_utils.crypto"))
|
||||||
|
typealias EncodedBase64String = String
|
||||||
|
@Deprecated(replacedInCrypto, ReplaceWith("EncodedByteArray", "dev.inmo.micro_utils.crypto"))
|
||||||
|
typealias EncodedByteArray = ByteArray
|
||||||
|
|
||||||
|
@Deprecated(replacedInCrypto, ReplaceWith("encodeBase64String()", "dev.inmo.micro_utils.crypto"))
|
||||||
|
fun String.encodeBase64String(): EncodedBase64String = encodeToByteArray().encodeBase64String()
|
||||||
|
@Deprecated(replacedInCrypto, ReplaceWith("encodeBase64()", "dev.inmo.micro_utils.crypto"))
|
||||||
|
fun String.encodeBase64(): EncodedByteArray = encodeToByteArray().encodeBase64()
|
||||||
|
|
||||||
|
@Deprecated(replacedInCrypto, ReplaceWith("encodeBase64String()", "dev.inmo.micro_utils.crypto"))
|
||||||
|
fun ByteArray.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Deprecated(replacedInCrypto, ReplaceWith("encodeBase64()", "dev.inmo.micro_utils.crypto"))
|
||||||
|
fun ByteArray.encodeBase64(): EncodedByteArray = encodeBase64String().encodeToByteArray()
|
||||||
|
|
||||||
|
@Deprecated(replacedInCrypto, ReplaceWith("decodeBase64()", "dev.inmo.micro_utils.crypto"))
|
||||||
|
fun EncodedBase64String.decodeBase64() = dropLastWhile { it == BASE64_PAD }.encodeToByteArray().decodeBase64()
|
||||||
|
@Deprecated(replacedInCrypto, ReplaceWith("decodeBase64String()", "dev.inmo.micro_utils.crypto"))
|
||||||
|
fun EncodedBase64String.decodeBase64String() = decodeBase64().decodeToString()
|
||||||
|
|
||||||
|
@Deprecated(replacedInCrypto, ReplaceWith("decodeBase64()", "dev.inmo.micro_utils.crypto"))
|
||||||
|
fun EncodedByteArray.decodeBase64(): ByteArray {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
@Deprecated(replacedInCrypto, ReplaceWith("decodeBase64String()", "dev.inmo.micro_utils.crypto"))
|
||||||
|
fun EncodedByteArray.decodeBase64String() = decodeBase64().decodeToString()
|
@@ -0,0 +1,10 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
inline fun <T : Comparable<T>> T.clamp(min: T, max: T): T {
|
||||||
|
return when {
|
||||||
|
this < min -> min
|
||||||
|
this > max -> max
|
||||||
|
else -> this
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,33 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlin.random.Random
|
||||||
|
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,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,13 @@
|
|||||||
|
@file:Suppress("NOTHING_TO_INLINE")
|
||||||
|
|
||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
|
||||||
|
inline fun Resources.getSp(
|
||||||
|
resId: Int
|
||||||
|
) = getDimension(resId) / displayMetrics.scaledDensity
|
||||||
|
|
||||||
|
inline fun Resources.getDp(
|
||||||
|
resId: Int
|
||||||
|
) = getDimension(resId) * displayMetrics.density
|
@@ -0,0 +1,140 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.Animation
|
||||||
|
import android.view.animation.Transformation
|
||||||
|
|
||||||
|
private fun View.performExpand(
|
||||||
|
duration: Long = 500,
|
||||||
|
targetWidth: Int = ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
onMeasured: View.() -> Unit,
|
||||||
|
onPerformAnimation: View.(interpolatedTime: Float, t: Transformation?) -> Unit
|
||||||
|
) {
|
||||||
|
measure(targetWidth, targetHeight)
|
||||||
|
onMeasured()
|
||||||
|
show()
|
||||||
|
val a: Animation = object : Animation() {
|
||||||
|
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
|
||||||
|
super.applyTransformation(interpolatedTime, t)
|
||||||
|
onPerformAnimation(interpolatedTime, t)
|
||||||
|
requestLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun willChangeBounds(): Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
|
a.duration = duration
|
||||||
|
startAnimation(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun View.performCollapse(
|
||||||
|
duration: Long = 500,
|
||||||
|
onPerformAnimation: View.(interpolatedTime: Float, t: Transformation?) -> Unit
|
||||||
|
) {
|
||||||
|
val a: Animation = object : Animation() {
|
||||||
|
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
|
||||||
|
if (interpolatedTime == 1f) {
|
||||||
|
gone()
|
||||||
|
} else {
|
||||||
|
onPerformAnimation(interpolatedTime, t)
|
||||||
|
requestLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun willChangeBounds(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.duration = duration
|
||||||
|
|
||||||
|
startAnimation(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewFeature
|
||||||
|
fun View.expand(
|
||||||
|
duration: Long = 500,
|
||||||
|
targetWidth: Int = ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
) {
|
||||||
|
var measuredHeight = 0
|
||||||
|
performExpand(
|
||||||
|
duration,
|
||||||
|
targetWidth,
|
||||||
|
targetHeight,
|
||||||
|
{
|
||||||
|
measuredHeight = this.measuredHeight
|
||||||
|
}
|
||||||
|
) { interpolatedTime, _ ->
|
||||||
|
layoutParams.height = if (interpolatedTime == 1f) targetHeight else (measuredHeight * interpolatedTime).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewFeature
|
||||||
|
fun View.expandHorizontally(
|
||||||
|
duration: Long = 500,
|
||||||
|
targetWidth: Int = ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
) {
|
||||||
|
var measuredWidth = 0
|
||||||
|
performExpand(
|
||||||
|
duration,
|
||||||
|
targetWidth,
|
||||||
|
targetHeight,
|
||||||
|
{
|
||||||
|
measuredWidth = this.measuredWidth
|
||||||
|
}
|
||||||
|
) { interpolatedTime, _ ->
|
||||||
|
layoutParams.width = if (interpolatedTime == 1f) targetWidth else (measuredWidth * interpolatedTime).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewFeature
|
||||||
|
fun View.collapse(duration: Long = 500) {
|
||||||
|
val initialHeight: Int = measuredHeight
|
||||||
|
performCollapse(duration) { interpolatedTime, _ ->
|
||||||
|
layoutParams.height = initialHeight - (initialHeight * interpolatedTime).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewFeature
|
||||||
|
fun View.collapseHorizontally(duration: Long = 500) {
|
||||||
|
val initialWidth: Int = measuredWidth
|
||||||
|
performCollapse(duration) { interpolatedTime, _ ->
|
||||||
|
layoutParams.width = initialWidth - (initialWidth * interpolatedTime).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewFeature
|
||||||
|
inline val View.isCollapsed
|
||||||
|
get() = visibility == View.GONE
|
||||||
|
|
||||||
|
@PreviewFeature
|
||||||
|
inline val View.isExpanded
|
||||||
|
get() = !isCollapsed
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true in case of expanding
|
||||||
|
*/
|
||||||
|
@PreviewFeature
|
||||||
|
fun View.toggleExpandState(duration: Long = 500): Boolean = if (isCollapsed) {
|
||||||
|
expand(duration)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
collapse(duration)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true in case of expanding
|
||||||
|
*/
|
||||||
|
@PreviewFeature
|
||||||
|
fun View.toggleExpandHorizontallyState(duration: Long = 500): Boolean = if (isCollapsed) {
|
||||||
|
expandHorizontally(duration)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
collapseHorizontally(duration)
|
||||||
|
false
|
||||||
|
}
|
@@ -0,0 +1,34 @@
|
|||||||
|
@file:Suppress("NOTHING_TO_INLINE", "unused")
|
||||||
|
|
||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
|
||||||
|
inline val View.enabled
|
||||||
|
get() = isEnabled
|
||||||
|
|
||||||
|
inline val View.disabled
|
||||||
|
get() = !enabled
|
||||||
|
|
||||||
|
fun View.disable() {
|
||||||
|
if (this is ViewGroup) {
|
||||||
|
(0 until childCount).forEach { getChildAt(it).disable() }
|
||||||
|
}
|
||||||
|
isEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.enable() {
|
||||||
|
if (this is ViewGroup) {
|
||||||
|
(0 until childCount).forEach { getChildAt(it).enable() }
|
||||||
|
}
|
||||||
|
isEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.toggleEnabledState(enabled: Boolean) {
|
||||||
|
if (enabled) {
|
||||||
|
enable()
|
||||||
|
} else {
|
||||||
|
disable()
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,35 @@
|
|||||||
|
@file:Suppress("NOTHING_TO_INLINE", "unused")
|
||||||
|
|
||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
|
||||||
|
inline val View.gone
|
||||||
|
get() = visibility == View.GONE
|
||||||
|
inline fun View.gone() {
|
||||||
|
visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
inline val View.hidden
|
||||||
|
get() = visibility == View.INVISIBLE
|
||||||
|
inline fun View.hide() {
|
||||||
|
visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
inline val View.shown
|
||||||
|
get() = visibility == View.VISIBLE
|
||||||
|
inline fun View.show() {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.toggleVisibility(goneOnHide: Boolean = true) {
|
||||||
|
if (isShown) {
|
||||||
|
if (goneOnHide) {
|
||||||
|
gone()
|
||||||
|
} else {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
@@ -2,7 +2,6 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
id "kotlin-android-extensions"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
@@ -19,7 +19,7 @@ fun <T> CoroutineScope.actor(
|
|||||||
|
|
||||||
inline fun <T> CoroutineScope.safeActor(
|
inline fun <T> CoroutineScope.safeActor(
|
||||||
channelCapacity: Int = Channel.UNLIMITED,
|
channelCapacity: Int = Channel.UNLIMITED,
|
||||||
noinline onException: ExceptionHandler<Unit> = {},
|
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||||
crossinline block: suspend (T) -> Unit
|
crossinline block: suspend (T) -> Unit
|
||||||
): Channel<T> = actor(
|
): Channel<T> = actor(
|
||||||
channelCapacity
|
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)
|
@@ -0,0 +1,5 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
|
|
||||||
|
suspend inline operator fun <T> FlowCollector<T>.invoke(value: T) = emit(value)
|
@@ -16,7 +16,7 @@ inline fun <T> Flow<T>.subscribe(scope: CoroutineScope, noinline block: suspend
|
|||||||
*/
|
*/
|
||||||
inline fun <T> Flow<T>.subscribeSafely(
|
inline fun <T> Flow<T>.subscribeSafely(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
noinline onException: ExceptionHandler<Unit> = { throw it },
|
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||||
noinline block: suspend (T) -> Unit
|
noinline block: suspend (T) -> Unit
|
||||||
) = subscribe(scope) {
|
) = subscribe(scope) {
|
||||||
safely(onException) {
|
safely(onException) {
|
||||||
|
@@ -1,30 +1,156 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
typealias ExceptionHandler<T> = suspend (Throwable) -> T
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key for [SafelyExceptionHandler] which can be used in [CoroutineContext.get] to get current default
|
||||||
|
* [SafelyExceptionHandler]
|
||||||
|
*/
|
||||||
|
@Deprecated("This method will be useless in future major update", ReplaceWith("ContextSafelyExceptionHandlerKey", "dev.inmo.micro_utils.coroutines.ContextSafelyExceptionHandler"))
|
||||||
|
class SafelyExceptionHandlerKey<T> : CoroutineContext.Key<SafelyExceptionHandler<T>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortcut for creating instance of [SafelyExceptionHandlerKey]
|
||||||
|
*/
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
@Deprecated("This method will be useless in future major update", ReplaceWith("ContextSafelyExceptionHandlerKey", "dev.inmo.micro_utils.coroutines.ContextSafelyExceptionHandler"))
|
||||||
|
inline fun <T> safelyExceptionHandlerKey() = SafelyExceptionHandlerKey<T>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for [ExceptionHandler] which can be used in [CoroutineContext] to set local (for [CoroutineContext]) default
|
||||||
|
* [ExceptionHandler]. To get it use [CoroutineContext.get] with key [SafelyExceptionHandlerKey]
|
||||||
|
*
|
||||||
|
* @see SafelyExceptionHandlerKey
|
||||||
|
* @see ExceptionHandler
|
||||||
|
*/
|
||||||
|
@Deprecated("This method will be useless in future major update", ReplaceWith("ContextSafelyExceptionHandler", "dev.inmo.micro_utils.coroutines.ContextSafelyExceptionHandler"))
|
||||||
|
class SafelyExceptionHandler<T>(
|
||||||
|
val handler: ExceptionHandler<T>
|
||||||
|
) : CoroutineContext.Element {
|
||||||
|
override val key: CoroutineContext.Key<*> = safelyExceptionHandlerKey<T>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* 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
|
* @param [onException] Will be called when happen exception inside of [block]. By default will throw exception - this
|
||||||
* exception will be available for catching
|
* exception will be available for catching
|
||||||
|
*
|
||||||
|
* @see defaultSafelyExceptionHandler
|
||||||
|
* @see safelyWithoutExceptions
|
||||||
|
* @see safelyWithContextExceptionHandler
|
||||||
*/
|
*/
|
||||||
suspend inline fun <T> safely(
|
suspend inline fun <T> safely(
|
||||||
noinline onException: ExceptionHandler<T> = { throw it },
|
noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||||
noinline block: suspend CoroutineScope.() -> T
|
noinline block: suspend CoroutineScope.() -> T
|
||||||
): T {
|
): T {
|
||||||
return try {
|
return try {
|
||||||
supervisorScope(block)
|
supervisorScope(block)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
coroutineContext[ContextSafelyExceptionHandlerKey] ?.handler ?.invoke(e)
|
||||||
onException(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(
|
suspend inline fun <T> safelyWithoutExceptions(
|
||||||
noinline block: suspend CoroutineScope.() -> T
|
noinline block: suspend CoroutineScope.() -> T
|
||||||
): T? = safely({ null }, block)
|
): T? = safelyWithoutExceptions(
|
||||||
|
{
|
||||||
|
defaultSafelyWithoutExceptionHandler.invoke(it)
|
||||||
|
null
|
||||||
|
},
|
||||||
|
block
|
||||||
|
)
|
||||||
|
@@ -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
|
@@ -0,0 +1,30 @@
|
|||||||
|
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 objectToSynchronize = java.lang.Object()
|
||||||
|
val launchCallback = {
|
||||||
|
launch {
|
||||||
|
safely(
|
||||||
|
{
|
||||||
|
throwable = it
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
result = block()
|
||||||
|
}
|
||||||
|
synchronized(objectToSynchronize) {
|
||||||
|
objectToSynchronize.notifyAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
synchronized(objectToSynchronize) {
|
||||||
|
launchCallback()
|
||||||
|
objectToSynchronize.wait()
|
||||||
|
}
|
||||||
|
throw throwable ?: return result!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> launchSynchronously(block: suspend CoroutineScope.() -> T): T = CoroutineScope(Dispatchers.Default).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,13 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class LaunchSynchronouslyTest {
|
||||||
|
@Test
|
||||||
|
fun testRunInCoroutine() {
|
||||||
|
(0 .. 10000).forEach {
|
||||||
|
assertEquals(it, launchSynchronously { it })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
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,3 +1,5 @@
|
|||||||
|
apply plugin: 'com.getkeepsafe.dexcount'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion "$android_compileSdkVersion".toInteger()
|
compileSdkVersion "$android_compileSdkVersion".toInteger()
|
||||||
buildToolsVersion "$android_buildToolsVersion"
|
buildToolsVersion "$android_buildToolsVersion"
|
||||||
|
@@ -2,7 +2,6 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
id "kotlin-android-extensions"
|
|
||||||
id "org.jetbrains.dokka"
|
id "org.jetbrains.dokka"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,41 +5,44 @@ kotlin.incremental=true
|
|||||||
kotlin.incremental.js=true
|
kotlin.incremental.js=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
org.gradle.jvmargs=-Xmx3072m
|
|
||||||
|
|
||||||
kotlin_version=1.4.10
|
kotlin_version=1.4.30
|
||||||
kotlin_coroutines_version=1.4.1
|
kotlin_coroutines_version=1.4.2
|
||||||
kotlin_serialisation_core_version=1.0.1
|
kotlin_serialisation_core_version=1.1.0
|
||||||
kotlin_exposed_version=0.28.1
|
kotlin_exposed_version=0.29.1
|
||||||
|
|
||||||
ktor_version=1.4.2
|
ktor_version=1.5.1
|
||||||
|
|
||||||
klockVersion=1.12.1
|
klockVersion=2.0.6
|
||||||
|
|
||||||
gradle_bintray_plugin_version=1.8.5
|
|
||||||
github_release_plugin_version=2.2.12
|
github_release_plugin_version=2.2.12
|
||||||
|
|
||||||
uuidVersion=0.2.2
|
uuidVersion=0.2.3
|
||||||
|
|
||||||
# ANDROID
|
# ANDROID
|
||||||
|
|
||||||
core_ktx_version=1.3.2
|
core_ktx_version=1.3.2
|
||||||
androidx_recycler_version=1.1.0
|
androidx_recycler_version=1.1.0
|
||||||
|
appcompat_version=1.2.0
|
||||||
|
|
||||||
android_minSdkVersion=19
|
android_minSdkVersion=19
|
||||||
android_compileSdkVersion=30
|
android_compileSdkVersion=30
|
||||||
android_buildToolsVersion=30.0.2
|
android_buildToolsVersion=30.0.2
|
||||||
dexcount_version=2.0.0-RC1
|
dexcount_version=2.0.0
|
||||||
junit_version=4.12
|
junit_version=4.12
|
||||||
test_ext_junit_version=1.1.2
|
test_ext_junit_version=1.1.2
|
||||||
espresso_core=3.3.0
|
espresso_core=3.3.0
|
||||||
|
|
||||||
|
# JS NPM
|
||||||
|
|
||||||
|
crypto_js_version=4.0.0
|
||||||
|
|
||||||
# Dokka
|
# Dokka
|
||||||
|
|
||||||
dokka_version=1.4.0
|
dokka_version=1.4.20
|
||||||
|
|
||||||
# Project data
|
# Project data
|
||||||
|
|
||||||
group=dev.inmo
|
group=dev.inmo
|
||||||
version=0.4.0
|
version=0.4.27
|
||||||
android_code_version=4
|
android_code_version=31
|
||||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@@ -2,7 +2,6 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
id "kotlin-android-extensions"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
@@ -25,7 +25,7 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
|
|||||||
val producerScope = this@channelFlow
|
val producerScope = this@channelFlow
|
||||||
do {
|
do {
|
||||||
val reconnect = try {
|
val reconnect = try {
|
||||||
safely ({ throw it }) {
|
safely {
|
||||||
ws(correctedUrl) {
|
ws(correctedUrl) {
|
||||||
for (received in incoming) {
|
for (received in incoming) {
|
||||||
when (received) {
|
when (received) {
|
||||||
@@ -64,10 +64,11 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
|
|||||||
inline fun <T> HttpClient.createStandardWebsocketFlow(
|
inline fun <T> HttpClient.createStandardWebsocketFlow(
|
||||||
url: String,
|
url: String,
|
||||||
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
|
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
|
||||||
deserializer: DeserializationStrategy<T>
|
deserializer: DeserializationStrategy<T>,
|
||||||
|
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||||
) = createStandardWebsocketFlow(
|
) = createStandardWebsocketFlow(
|
||||||
url,
|
url,
|
||||||
checkReconnection
|
checkReconnection
|
||||||
) {
|
) {
|
||||||
standardKtorSerialFormat.decodeDefault(deserializer, it)
|
serialFormat.decodeDefault(deserializer, it)
|
||||||
}
|
}
|
||||||
|
@@ -4,32 +4,61 @@ import dev.inmo.micro_utils.ktor.common.*
|
|||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.request.post
|
import io.ktor.client.request.post
|
||||||
import kotlinx.serialization.DeserializationStrategy
|
import kotlinx.serialization.*
|
||||||
import kotlinx.serialization.SerializationStrategy
|
|
||||||
|
|
||||||
typealias BodyPair<T> = Pair<SerializationStrategy<T>, T>
|
typealias BodyPair<T> = Pair<SerializationStrategy<T>, T>
|
||||||
|
|
||||||
|
class UnifiedRequester(
|
||||||
|
private val client: HttpClient = HttpClient(),
|
||||||
|
private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||||
|
) {
|
||||||
|
suspend fun <ResultType> uniget(
|
||||||
|
url: String,
|
||||||
|
resultDeserializer: DeserializationStrategy<ResultType>
|
||||||
|
): ResultType = client.get<StandardKtorSerialInputData>(
|
||||||
|
url
|
||||||
|
).let {
|
||||||
|
serialFormat.decodeDefault(resultDeserializer, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun <T> encodeUrlQueryValue(
|
||||||
|
serializationStrategy: SerializationStrategy<T>,
|
||||||
|
value: T
|
||||||
|
) = serialFormat.encodeHex(
|
||||||
|
serializationStrategy,
|
||||||
|
value
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> createStandardWebsocketFlow(
|
||||||
|
url: String,
|
||||||
|
checkReconnection: (Throwable?) -> Boolean = { true },
|
||||||
|
deserializer: DeserializationStrategy<T>
|
||||||
|
) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
val defaultRequester = UnifiedRequester()
|
||||||
|
|
||||||
suspend fun <ResultType> HttpClient.uniget(
|
suspend fun <ResultType> HttpClient.uniget(
|
||||||
url: String,
|
url: String,
|
||||||
resultDeserializer: DeserializationStrategy<ResultType>
|
resultDeserializer: DeserializationStrategy<ResultType>
|
||||||
) = get<StandardKtorSerialInputData>(
|
) = defaultRequester.uniget(url, resultDeserializer)
|
||||||
url
|
|
||||||
).let {
|
|
||||||
standardKtorSerialFormat.decodeDefault(resultDeserializer, it)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun <T> SerializationStrategy<T>.encodeUrlQueryValue(value: T) = standardKtorSerialFormat.encodeHex(
|
fun <T> SerializationStrategy<T>.encodeUrlQueryValue(value: T) = defaultRequester.encodeUrlQueryValue(this, value)
|
||||||
this,
|
|
||||||
value
|
|
||||||
)
|
|
||||||
|
|
||||||
suspend fun <BodyType, ResultType> HttpClient.unipost(
|
suspend fun <BodyType, ResultType> HttpClient.unipost(
|
||||||
url: String,
|
url: String,
|
||||||
bodyInfo: BodyPair<BodyType>,
|
bodyInfo: BodyPair<BodyType>,
|
||||||
resultDeserializer: DeserializationStrategy<ResultType>
|
resultDeserializer: DeserializationStrategy<ResultType>
|
||||||
) = post<StandardKtorSerialInputData>(url) {
|
) = defaultRequester.unipost(url, bodyInfo, resultDeserializer)
|
||||||
body = standardKtorSerialFormat.encodeDefault(bodyInfo.first, bodyInfo.second)
|
|
||||||
}.let {
|
|
||||||
standardKtorSerialFormat.decodeDefault(resultDeserializer, it)
|
|
||||||
}
|
|
||||||
|
@@ -2,7 +2,6 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
id "kotlin-android-extensions"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
@@ -3,16 +3,28 @@ package dev.inmo.micro_utils.ktor.common
|
|||||||
typealias QueryParam = Pair<String, String?>
|
typealias QueryParam = Pair<String, String?>
|
||||||
typealias QueryParams = Map<String, String?>
|
typealias QueryParams = Map<String, String?>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create query part which includes key=value pairs separated with &
|
||||||
|
*/
|
||||||
val QueryParams.asUrlQuery: String
|
val QueryParams.asUrlQuery: String
|
||||||
get() = keys.joinToString("&") { "${it}${get(it) ?.let { value -> "=$value" }}" }
|
get() = keys.joinToString("&") { "${it}${get(it) ?.let { value -> "=$value" }}" }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create query part which includes key=value pairs separated with &
|
||||||
|
*/
|
||||||
val List<QueryParam>.asUrlQuery: String
|
val List<QueryParam>.asUrlQuery: String
|
||||||
get() = joinToString("&") { (key, value) -> "${key}${value ?.let { _ -> "=$value" }}" }
|
get() = joinToString("&") { (key, value) -> "${key}${value ?.let { _ -> "=$value" }}" }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create query part which includes key=value pairs separated with & and attach to receiver
|
||||||
|
*/
|
||||||
fun String.includeQueryParams(
|
fun String.includeQueryParams(
|
||||||
queryParams: QueryParams
|
queryParams: QueryParams
|
||||||
): String = "$this${if(queryParams.isNotEmpty()) "${if (contains("?")) "&" else "?"}${queryParams.asUrlQuery}" else ""}"
|
): String = "$this${if(queryParams.isNotEmpty()) "${if (contains("?")) "&" else "?"}${queryParams.asUrlQuery}" else ""}"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create query part which includes key=value pairs separated with & and attach to receiver
|
||||||
|
*/
|
||||||
fun String.includeQueryParams(
|
fun String.includeQueryParams(
|
||||||
queryParams: List<QueryParam>
|
queryParams: List<QueryParam>
|
||||||
): String = "$this${if (contains("?")) "&" else "?"}${queryParams.asUrlQuery}"
|
): String = "$this${if (contains("?")) "&" else "?"}${queryParams.asUrlQuery}"
|
||||||
|
@@ -33,10 +33,11 @@ fun <T> Route.includeWebsocketHandling(
|
|||||||
fun <T> Route.includeWebsocketHandling(
|
fun <T> Route.includeWebsocketHandling(
|
||||||
suburl: String,
|
suburl: String,
|
||||||
flow: Flow<T>,
|
flow: Flow<T>,
|
||||||
serializer: SerializationStrategy<T>
|
serializer: SerializationStrategy<T>,
|
||||||
|
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||||
) = includeWebsocketHandling(
|
) = includeWebsocketHandling(
|
||||||
suburl,
|
suburl,
|
||||||
flow
|
flow
|
||||||
) {
|
) {
|
||||||
standardKtorSerialFormat.encodeDefault(serializer, it)
|
serialFormat.encodeDefault(serializer, it)
|
||||||
}
|
}
|
||||||
|
@@ -3,12 +3,87 @@ package dev.inmo.micro_utils.ktor.server
|
|||||||
import dev.inmo.micro_utils.coroutines.safely
|
import dev.inmo.micro_utils.coroutines.safely
|
||||||
import dev.inmo.micro_utils.ktor.common.*
|
import dev.inmo.micro_utils.ktor.common.*
|
||||||
import io.ktor.application.ApplicationCall
|
import io.ktor.application.ApplicationCall
|
||||||
|
import io.ktor.application.call
|
||||||
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.request.receive
|
import io.ktor.request.receive
|
||||||
import io.ktor.response.respond
|
import io.ktor.response.respond
|
||||||
import io.ktor.response.respondBytes
|
import io.ktor.response.respondBytes
|
||||||
import kotlinx.serialization.DeserializationStrategy
|
import io.ktor.routing.Route
|
||||||
import kotlinx.serialization.SerializationStrategy
|
import io.ktor.util.pipeline.PipelineContext
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.serialization.*
|
||||||
|
|
||||||
|
class UnifiedRouter(
|
||||||
|
private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
||||||
|
private val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
|
||||||
|
) {
|
||||||
|
fun <T> Route.includeWebsocketHandling(
|
||||||
|
suburl: String,
|
||||||
|
flow: Flow<T>,
|
||||||
|
serializer: SerializationStrategy<T>
|
||||||
|
) = includeWebsocketHandling(suburl, flow, serializer, serialFormat)
|
||||||
|
|
||||||
|
suspend fun <T> PipelineContext<*, ApplicationCall>.unianswer(
|
||||||
|
answerSerializer: SerializationStrategy<T>,
|
||||||
|
answer: T
|
||||||
|
) {
|
||||||
|
call.respondBytes (
|
||||||
|
serialFormat.encodeDefault(answerSerializer, answer),
|
||||||
|
serialFormatContentType
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> PipelineContext<*, ApplicationCall>.uniload(
|
||||||
|
deserializer: DeserializationStrategy<T>
|
||||||
|
) = safely {
|
||||||
|
serialFormat.decodeDefault(
|
||||||
|
deserializer,
|
||||||
|
call.receive()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun PipelineContext<*, ApplicationCall>.getParameterOrSendError(
|
||||||
|
field: String
|
||||||
|
) = call.parameters[field].also {
|
||||||
|
if (it == null) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, "Request must contains $field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PipelineContext<*, ApplicationCall>.getQueryParameter(
|
||||||
|
field: String
|
||||||
|
) = call.request.queryParameters[field]
|
||||||
|
|
||||||
|
suspend fun PipelineContext<*, ApplicationCall>.getQueryParameterOrSendError(
|
||||||
|
field: String
|
||||||
|
) = getQueryParameter(field).also {
|
||||||
|
if (it == null) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> PipelineContext<*, ApplicationCall>.decodeUrlQueryValue(
|
||||||
|
field: String,
|
||||||
|
deserializer: DeserializationStrategy<T>
|
||||||
|
) = getQueryParameter(field) ?.let {
|
||||||
|
serialFormat.decodeHex(
|
||||||
|
deserializer,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> PipelineContext<*, ApplicationCall>.decodeUrlQueryValueOrSendError(
|
||||||
|
field: String,
|
||||||
|
deserializer: DeserializationStrategy<T>
|
||||||
|
) = decodeUrlQueryValue(field, deserializer).also {
|
||||||
|
if (it == null) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val defaultUnifiedRouter = UnifiedRouter()
|
||||||
|
|
||||||
suspend fun <T> ApplicationCall.unianswer(
|
suspend fun <T> ApplicationCall.unianswer(
|
||||||
answerSerializer: SerializationStrategy<T>,
|
answerSerializer: SerializationStrategy<T>,
|
||||||
|
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"/>
|
@@ -1,60 +0,0 @@
|
|||||||
apply plugin: 'maven-publish'
|
|
||||||
|
|
||||||
task javadocsJar(type: Jar) {
|
|
||||||
classifier = 'javadoc'
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEvaluate {
|
|
||||||
project.publishing.publications.all {
|
|
||||||
// rename artifacts
|
|
||||||
groupId "${project.group}"
|
|
||||||
if (it.name.contains('kotlinMultiplatform')) {
|
|
||||||
artifactId = "${project.name}"
|
|
||||||
} else {
|
|
||||||
artifactId = "${project.name}-$name"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
publishing {
|
|
||||||
publications.all {
|
|
||||||
artifact javadocsJar
|
|
||||||
|
|
||||||
pom {
|
|
||||||
description = ""
|
|
||||||
name = "${project.name}"
|
|
||||||
url = "https://git.inmo.dev/InsanusMokrassar/MicroUtils_mirror"
|
|
||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
developers {
|
|
||||||
|
|
||||||
developer {
|
|
||||||
id = "InsanusMokrassar"
|
|
||||||
name = "Aleksei Ovsiannikov"
|
|
||||||
email = "ovsyannikov.alexey95@gmail.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
developer {
|
|
||||||
id = "000Sanya"
|
|
||||||
name = "Syrov Aleksandr"
|
|
||||||
email = "000sanya.000sanya@gmail.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
licenses {
|
|
||||||
|
|
||||||
license {
|
|
||||||
name = "Apache Software License 2.0"
|
|
||||||
url = "https://git.inmo.dev/InsanusMokrassar/MicroUtils_mirror/src/master/LICENSE"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -2,7 +2,6 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
id "kotlin-android-extensions"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
@@ -4,7 +4,9 @@ project.group = "$group"
|
|||||||
apply from: "$publishGradlePath"
|
apply from: "$publishGradlePath"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm {
|
||||||
|
compilations.main.kotlinOptions.useIR = true
|
||||||
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
|
@@ -4,7 +4,9 @@ project.group = "$group"
|
|||||||
apply from: "$publishGradlePath"
|
apply from: "$publishGradlePath"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm {
|
||||||
|
compilations.main.kotlinOptions.useIR = true
|
||||||
|
}
|
||||||
js (BOTH) {
|
js (BOTH) {
|
||||||
browser()
|
browser()
|
||||||
nodejs()
|
nodejs()
|
||||||
|
@@ -2,7 +2,6 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
id "kotlin-android-extensions"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
@@ -22,6 +22,12 @@ interface Pagination {
|
|||||||
val size: Int
|
val size: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logical shortcut for comparison that page is 0
|
||||||
|
*/
|
||||||
|
inline val Pagination.isFirstPage
|
||||||
|
get() = page == 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* First number in index of objects. It can be used as offset for databases or other data sources
|
* First number in index of objects. It can be used as offset for databases or other data sources
|
||||||
*/
|
*/
|
||||||
|
@@ -0,0 +1,41 @@
|
|||||||
|
package dev.inmo.micro_utils.pagination.utils
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.pagination.*
|
||||||
|
|
||||||
|
class PaginatedIterator<T>(
|
||||||
|
pageSize: Int,
|
||||||
|
private val countGetter: () -> Long,
|
||||||
|
private val paginationResultGetter: Pagination.() -> PaginationResult<T>
|
||||||
|
) : Iterator<T> {
|
||||||
|
private var pagination = FirstPagePagination(pageSize)
|
||||||
|
private val currentStack = mutableListOf<T>()
|
||||||
|
override fun hasNext(): Boolean = currentStack.isNotEmpty() || (countGetter() < pagination.lastIndexExclusive)
|
||||||
|
|
||||||
|
override fun next(): T {
|
||||||
|
if (currentStack.isEmpty()) {
|
||||||
|
val resultPagination = paginationResultGetter.invoke(pagination)
|
||||||
|
currentStack.addAll(resultPagination.results)
|
||||||
|
require(currentStack.isNotEmpty()) { "There is no elements left" }
|
||||||
|
pagination = resultPagination.nextPage()
|
||||||
|
}
|
||||||
|
return currentStack.removeFirst()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaginatedIterable<T>(
|
||||||
|
private val pageSize: Int,
|
||||||
|
private val countGetter: () -> Long,
|
||||||
|
private val paginationResultGetter: Pagination.() -> PaginationResult<T>
|
||||||
|
) : Iterable<T> {
|
||||||
|
override fun iterator(): Iterator<T> = PaginatedIterator(pageSize, countGetter, paginationResultGetter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will make iterable using incoming [countGetter] and [paginationResultGetter]
|
||||||
|
*/
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
inline fun <T> makeIterable(
|
||||||
|
noinline countGetter: () -> Long,
|
||||||
|
pageSize: Int = defaultMediumPageSize,
|
||||||
|
noinline paginationResultGetter: Pagination.() -> PaginationResult<T>
|
||||||
|
): Iterable<T> = PaginatedIterable(pageSize, countGetter, paginationResultGetter)
|
@@ -2,7 +2,6 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
id "kotlin-android-extensions"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
@@ -1 +1 @@
|
|||||||
{"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"}]},"type":"Multiplatform"}
|
{"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":"It is set of projects with micro tools for avoiding of routines coding","url":"https://git.inmo.dev/InsanusMokrassar/MicroUtils_mirror","vcsUrl":"ssh://git@git.inmo.dev:8322/InsanusMokrassar/MicroUtils_mirror.git","includeGpgSigning":true,"publishToMavenCentral":true,"developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}]}}
|
123
publish.gradle
123
publish.gradle
@@ -1,69 +1,76 @@
|
|||||||
apply plugin: 'com.jfrog.bintray'
|
apply plugin: 'maven-publish'
|
||||||
|
apply plugin: 'signing'
|
||||||
|
|
||||||
apply from: "$publishMavenPath"
|
task javadocsJar(type: Jar) {
|
||||||
|
classifier = 'javadoc'
|
||||||
|
}
|
||||||
|
|
||||||
bintray {
|
publishing {
|
||||||
user = project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER')
|
publications.all {
|
||||||
key = project.hasProperty('BINTRAY_KEY') ? project.property('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
|
artifact javadocsJar
|
||||||
filesSpec {
|
|
||||||
from "${buildDir}/publications/"
|
pom {
|
||||||
eachFile {
|
description = "It is set of projects with micro tools for avoiding of routines coding"
|
||||||
String directorySubname = it.getFile().parentFile.name
|
name = "${project.name}"
|
||||||
if (it.getName() == "module.json") {
|
url = "https://git.inmo.dev/InsanusMokrassar/MicroUtils_mirror"
|
||||||
if (directorySubname == "kotlinMultiplatform") {
|
|
||||||
it.setPath("${project.name}/${project.version}/${project.name}-${project.version}.module")
|
scm {
|
||||||
} else {
|
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"
|
||||||
switch (directorySubname) {
|
url = "ssh://git@git.inmo.dev:8322/InsanusMokrassar/MicroUtils_mirror.git"
|
||||||
case "androidRelease":
|
}
|
||||||
it.setPath("${project.name}-android/${project.version}/${project.name}-android-${project.version}.module")
|
|
||||||
break
|
developers {
|
||||||
case "androidDebug":
|
|
||||||
it.setPath("${project.name}-android-debug/${project.version}/${project.name}-android-debug-${project.version}.module")
|
developer {
|
||||||
break
|
id = "InsanusMokrassar"
|
||||||
default:
|
name = "Aleksei Ovsiannikov"
|
||||||
it.setPath("${project.name}-${directorySubname}/${project.version}/${project.name}-${directorySubname}-${project.version}.module")
|
email = "ovsyannikov.alexey95@gmail.com"
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (directorySubname == "kotlinMultiplatform" && it.getName() == "pom-default.xml") {
|
developer {
|
||||||
it.setPath("${project.name}/${project.version}/${project.name}-${project.version}.pom")
|
id = "000Sanya"
|
||||||
} else {
|
name = "Syrov Aleksandr"
|
||||||
it.exclude()
|
email = "000sanya.000sanya@gmail.com"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
licenses {
|
||||||
|
|
||||||
|
license {
|
||||||
|
name = "Apache Software License 2.0"
|
||||||
|
url = "https://git.inmo.dev/InsanusMokrassar/MicroUtils_mirror/src/master/LICENSE"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
into "${project.group}".replace(".", "/")
|
|
||||||
}
|
repositories {
|
||||||
|
maven {
|
||||||
publish = true
|
name = "bintray"
|
||||||
override = true
|
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 {
|
||||||
pkg {
|
username = project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER')
|
||||||
repo = "MicroUtils"
|
password = project.hasProperty('BINTRAY_KEY') ? project.property('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
|
||||||
name = "${project.name}"
|
}
|
||||||
vcsUrl = "https://github.com/InsanusMokrassar/MicroUtils"
|
|
||||||
licenses = ["Apache-2.0"]
|
|
||||||
version {
|
|
||||||
name = "${project.version}"
|
|
||||||
released = new Date()
|
|
||||||
vcsTag = "${project.version}"
|
|
||||||
gpg {
|
|
||||||
sign = true
|
|
||||||
passphrase = project.hasProperty('signing.gnupg.passphrase') ? project.property('signing.gnupg.passphrase') : System.getenv('signing.gnupg.passphrase')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bintrayUpload.doFirst {
|
signing {
|
||||||
publications = publishing.publications.collect {
|
useGpgCmd()
|
||||||
if (it.name.contains('kotlinMultiplatform')) {
|
sign publishing.publications
|
||||||
null
|
|
||||||
} else {
|
|
||||||
it.name
|
|
||||||
}
|
|
||||||
} - null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bintrayUpload.dependsOn publishToMavenLocal
|
|
@@ -2,7 +2,6 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
id "kotlin-android-extensions"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
@@ -18,6 +17,11 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jvmMain {
|
||||||
|
dependencies {
|
||||||
|
api internalProject("micro_utils.common")
|
||||||
|
}
|
||||||
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api "androidx.core:core-ktx:$core_ktx_version"
|
api "androidx.core:core-ktx:$core_ktx_version"
|
||||||
|
@@ -36,6 +36,7 @@ interface ReadOneToManyKeyValueRepo<Key, Value> : Repo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
typealias ReadKeyValuesRepo<Key,Value> = ReadOneToManyKeyValueRepo<Key, Value>
|
||||||
|
|
||||||
interface WriteOneToManyKeyValueRepo<Key, Value> : Repo {
|
interface WriteOneToManyKeyValueRepo<Key, Value> : Repo {
|
||||||
val onNewValue: Flow<Pair<Key, Value>>
|
val onNewValue: Flow<Pair<Key, Value>>
|
||||||
@@ -47,7 +48,13 @@ interface WriteOneToManyKeyValueRepo<Key, Value> : Repo {
|
|||||||
suspend fun remove(toRemove: Map<Key, List<Value>>)
|
suspend fun remove(toRemove: Map<Key, List<Value>>)
|
||||||
|
|
||||||
suspend fun clear(k: Key)
|
suspend fun clear(k: Key)
|
||||||
|
|
||||||
|
suspend fun set(toSet: Map<Key, List<Value>>) {
|
||||||
|
toSet.keys.forEach { key -> clear(key) }
|
||||||
|
add(toSet)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
typealias WriteKeyValuesRepo<Key,Value> = WriteOneToManyKeyValueRepo<Key, Value>
|
||||||
|
|
||||||
suspend inline fun <Key, Value, REPO : WriteOneToManyKeyValueRepo<Key, Value>> REPO.add(
|
suspend inline fun <Key, Value, REPO : WriteOneToManyKeyValueRepo<Key, Value>> REPO.add(
|
||||||
keysAndValues: List<Pair<Key, List<Value>>>
|
keysAndValues: List<Pair<Key, List<Value>>>
|
||||||
@@ -65,7 +72,24 @@ suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.add(
|
|||||||
k: Key, vararg v: Value
|
k: Key, vararg v: Value
|
||||||
) = add(k, v.toList())
|
) = add(k, v.toList())
|
||||||
|
|
||||||
|
suspend inline fun <Key, Value, REPO : WriteOneToManyKeyValueRepo<Key, Value>> REPO.set(
|
||||||
|
keysAndValues: List<Pair<Key, List<Value>>>
|
||||||
|
) = set(keysAndValues.toMap())
|
||||||
|
|
||||||
|
suspend inline fun <Key, Value, REPO : WriteOneToManyKeyValueRepo<Key, Value>> REPO.set(
|
||||||
|
vararg keysAndValues: Pair<Key, List<Value>>
|
||||||
|
) = set(keysAndValues.toMap())
|
||||||
|
|
||||||
|
suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.set(
|
||||||
|
k: Key, v: List<Value>
|
||||||
|
) = set(mapOf(k to v))
|
||||||
|
|
||||||
|
suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.set(
|
||||||
|
k: Key, vararg v: Value
|
||||||
|
) = set(k, v.toList())
|
||||||
|
|
||||||
interface OneToManyKeyValueRepo<Key, Value> : ReadOneToManyKeyValueRepo<Key, Value>, WriteOneToManyKeyValueRepo<Key, Value>
|
interface OneToManyKeyValueRepo<Key, Value> : ReadOneToManyKeyValueRepo<Key, Value>, WriteOneToManyKeyValueRepo<Key, Value>
|
||||||
|
typealias KeyValuesRepo<Key,Value> = OneToManyKeyValueRepo<Key, Value>
|
||||||
|
|
||||||
suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.remove(
|
suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.remove(
|
||||||
keysAndValues: List<Pair<Key, List<Value>>>
|
keysAndValues: List<Pair<Key, List<Value>>>
|
||||||
|
@@ -10,6 +10,7 @@ interface ReadStandardCRUDRepo<ObjectType, IdType> : Repo {
|
|||||||
suspend fun contains(id: IdType): Boolean
|
suspend fun contains(id: IdType): Boolean
|
||||||
suspend fun count(): Long
|
suspend fun count(): Long
|
||||||
}
|
}
|
||||||
|
typealias ReadCRUDRepo<ObjectType, IdType> = ReadStandardCRUDRepo<ObjectType, IdType>
|
||||||
|
|
||||||
typealias UpdatedValuePair<IdType, ValueType> = Pair<IdType, ValueType>
|
typealias UpdatedValuePair<IdType, ValueType> = Pair<IdType, ValueType>
|
||||||
val <IdType> UpdatedValuePair<IdType, *>.id
|
val <IdType> UpdatedValuePair<IdType, *>.id
|
||||||
@@ -27,6 +28,7 @@ interface WriteStandardCRUDRepo<ObjectType, IdType, InputValueType> : Repo {
|
|||||||
suspend fun update(values: List<UpdatedValuePair<IdType, InputValueType>>): List<ObjectType>
|
suspend fun update(values: List<UpdatedValuePair<IdType, InputValueType>>): List<ObjectType>
|
||||||
suspend fun deleteById(ids: List<IdType>)
|
suspend fun deleteById(ids: List<IdType>)
|
||||||
}
|
}
|
||||||
|
typealias WriteCRUDRepo<ObjectType, IdType, InputValueType> = WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>
|
||||||
|
|
||||||
suspend fun <ObjectType, IdType, InputValueType> WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>.create(
|
suspend fun <ObjectType, IdType, InputValueType> WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>.create(
|
||||||
vararg values: InputValueType
|
vararg values: InputValueType
|
||||||
@@ -39,4 +41,5 @@ suspend fun <ObjectType, IdType, InputValueType> WriteStandardCRUDRepo<ObjectTyp
|
|||||||
) = deleteById(ids.toList())
|
) = deleteById(ids.toList())
|
||||||
|
|
||||||
interface StandardCRUDRepo<ObjectType, IdType, InputValueType> : ReadStandardCRUDRepo<ObjectType, IdType>,
|
interface StandardCRUDRepo<ObjectType, IdType, InputValueType> : ReadStandardCRUDRepo<ObjectType, IdType>,
|
||||||
WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>
|
WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>
|
||||||
|
typealias CRUDRepo<ObjectType, IdType, InputValueType> = StandardCRUDRepo<ObjectType, IdType, InputValueType>
|
@@ -1,7 +1,6 @@
|
|||||||
package dev.inmo.micro_utils.repos
|
package dev.inmo.micro_utils.repos
|
||||||
|
|
||||||
import dev.inmo.micro_utils.pagination.Pagination
|
import dev.inmo.micro_utils.pagination.*
|
||||||
import dev.inmo.micro_utils.pagination.PaginationResult
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface ReadStandardKeyValueRepo<Key, Value> : Repo {
|
interface ReadStandardKeyValueRepo<Key, Value> : Repo {
|
||||||
@@ -12,6 +11,7 @@ interface ReadStandardKeyValueRepo<Key, Value> : Repo {
|
|||||||
suspend fun contains(key: Key): Boolean
|
suspend fun contains(key: Key): Boolean
|
||||||
suspend fun count(): Long
|
suspend fun count(): Long
|
||||||
}
|
}
|
||||||
|
typealias ReadKeyValueRepo<Key,Value> = ReadStandardKeyValueRepo<Key, Value>
|
||||||
|
|
||||||
interface WriteStandardKeyValueRepo<Key, Value> : Repo {
|
interface WriteStandardKeyValueRepo<Key, Value> : Repo {
|
||||||
val onNewValue: Flow<Pair<Key, Value>>
|
val onNewValue: Flow<Pair<Key, Value>>
|
||||||
@@ -19,7 +19,9 @@ interface WriteStandardKeyValueRepo<Key, Value> : Repo {
|
|||||||
|
|
||||||
suspend fun set(toSet: Map<Key, Value>)
|
suspend fun set(toSet: Map<Key, Value>)
|
||||||
suspend fun unset(toUnset: List<Key>)
|
suspend fun unset(toUnset: List<Key>)
|
||||||
|
suspend fun unsetWithValues(toUnset: List<Value>)
|
||||||
}
|
}
|
||||||
|
typealias WriteKeyValueRepo<Key,Value> = WriteStandardKeyValueRepo<Key, Value>
|
||||||
|
|
||||||
suspend inline fun <Key, Value> WriteStandardKeyValueRepo<Key, Value>.set(
|
suspend inline fun <Key, Value> WriteStandardKeyValueRepo<Key, Value>.set(
|
||||||
vararg toSet: Pair<Key, Value>
|
vararg toSet: Pair<Key, Value>
|
||||||
@@ -33,4 +35,17 @@ suspend inline fun <Key, Value> WriteStandardKeyValueRepo<Key, Value>.unset(
|
|||||||
vararg k: Key
|
vararg k: Key
|
||||||
) = unset(k.toList())
|
) = unset(k.toList())
|
||||||
|
|
||||||
interface StandardKeyValueRepo<Key, Value> : ReadStandardKeyValueRepo<Key, Value>, WriteStandardKeyValueRepo<Key, Value>
|
suspend inline fun <Key, Value> WriteStandardKeyValueRepo<Key, Value>.unsetWithValues(
|
||||||
|
vararg v: Value
|
||||||
|
) = unsetWithValues(v.toList())
|
||||||
|
|
||||||
|
interface StandardKeyValueRepo<Key, Value> : ReadStandardKeyValueRepo<Key, Value>, WriteStandardKeyValueRepo<Key, Value> {
|
||||||
|
override suspend fun unsetWithValues(toUnset: List<Value>) = toUnset.forEach { v ->
|
||||||
|
doWithPagination {
|
||||||
|
keys(v, it).also {
|
||||||
|
unset(it.results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
typealias KeyValueRepo<Key,Value> = StandardKeyValueRepo<Key, Value>
|
||||||
|
@@ -105,6 +105,10 @@ open class MapperWriteStandardKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
|
|||||||
k.toOutKey()
|
k.toOutKey()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
override suspend fun unsetWithValues(toUnset: List<FromValue>) = to.unsetWithValues(
|
||||||
|
toUnset.map { it.toOutValue() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("NOTHING_TO_INLINE")
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
@@ -107,6 +107,12 @@ open class MapperWriteOneToManyKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
|
|||||||
}.toMap()
|
}.toMap()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
override suspend fun set(toSet: Map<FromKey, List<FromValue>>) {
|
||||||
|
to.set(
|
||||||
|
toSet.map { (k, vs) -> k.toOutKey() to vs.map { v -> v.toOutValue() } }.toMap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun clear(k: FromKey) = to.clear(k.toOutKey())
|
override suspend fun clear(k: FromKey) = to.clear(k.toOutKey())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,13 @@
|
|||||||
|
package dev.inmo.micro_utils.repos.versions
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.repos.StandardKeyValueRepo
|
||||||
|
import dev.inmo.micro_utils.repos.set
|
||||||
|
|
||||||
|
class KeyValueBasedVersionsRepoProxy<T>(
|
||||||
|
private val keyValueStore: StandardKeyValueRepo<String, Int>,
|
||||||
|
override val database: T
|
||||||
|
) : StandardVersionsRepoProxy<T> {
|
||||||
|
override suspend fun getTableVersion(tableName: String): Int? = keyValueStore.get(tableName)
|
||||||
|
|
||||||
|
override suspend fun updateTableVersion(tableName: String, version: Int) { keyValueStore.set(tableName, version) }
|
||||||
|
}
|
@@ -0,0 +1,36 @@
|
|||||||
|
package dev.inmo.micro_utils.repos.versions
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.repos.Repo
|
||||||
|
|
||||||
|
interface StandardVersionsRepoProxy<T> : Repo {
|
||||||
|
val database: T
|
||||||
|
|
||||||
|
suspend fun getTableVersion(tableName: String): Int?
|
||||||
|
suspend fun updateTableVersion(tableName: String, version: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
class StandardVersionsRepo<T>(
|
||||||
|
private val proxy: StandardVersionsRepoProxy<T>
|
||||||
|
) : VersionsRepo<T> {
|
||||||
|
override suspend fun setTableVersion(
|
||||||
|
tableName: String,
|
||||||
|
version: Int,
|
||||||
|
onCreate: suspend T.() -> Unit,
|
||||||
|
onUpdate: suspend T.(from: Int, to: Int) -> Unit
|
||||||
|
) {
|
||||||
|
var savedVersion = proxy.getTableVersion(tableName)
|
||||||
|
if (savedVersion == null) {
|
||||||
|
proxy.database.onCreate()
|
||||||
|
proxy.updateTableVersion(tableName, version)
|
||||||
|
} else {
|
||||||
|
while (savedVersion != null && savedVersion < version) {
|
||||||
|
val newVersion = savedVersion + 1
|
||||||
|
|
||||||
|
proxy.database.onUpdate(savedVersion, newVersion)
|
||||||
|
|
||||||
|
proxy.updateTableVersion(tableName, newVersion)
|
||||||
|
savedVersion = newVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,31 @@
|
|||||||
|
package dev.inmo.micro_utils.repos.versions
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.repos.Repo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interface has been created due to requirement to work with different versions of databases and make some
|
||||||
|
* migrations between versions
|
||||||
|
*
|
||||||
|
* @param T It is a type of database, which will be used by this repo to retrieve current table version and update it
|
||||||
|
*/
|
||||||
|
interface VersionsRepo<T> : Repo {
|
||||||
|
/**
|
||||||
|
* By default, instance of this interface will check that version of table with name [tableName] is less than
|
||||||
|
* [version] or is absent
|
||||||
|
*
|
||||||
|
* * In case if [tableName] didn't found, will be called [onCreate] and version of table will be set up to [version]
|
||||||
|
* * In case if [tableName] have version less than parameter [version], it will increase version one-by-one
|
||||||
|
* until database version will be equal to [version]
|
||||||
|
*
|
||||||
|
* @param version Current version of table
|
||||||
|
* @param onCreate This callback will be called in case when table have no information about table
|
||||||
|
* @param onUpdate This callback will be called after **iterative** changing of version. It is expected that parameter
|
||||||
|
* "to" will always be greater than "from"
|
||||||
|
*/
|
||||||
|
suspend fun setTableVersion(
|
||||||
|
tableName: String,
|
||||||
|
version: Int,
|
||||||
|
onCreate: suspend T.() -> Unit = {},
|
||||||
|
onUpdate: suspend T.(from: Int, to: Int) -> Unit = { _, _ ->}
|
||||||
|
)
|
||||||
|
}
|
@@ -0,0 +1,183 @@
|
|||||||
|
package dev.inmo.micro_utils.repos
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.Warning
|
||||||
|
import dev.inmo.micro_utils.pagination.*
|
||||||
|
import dev.inmo.micro_utils.pagination.utils.reverse
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.FileSystems
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.StandardWatchEventKinds.*
|
||||||
|
|
||||||
|
private inline val String.isAbsolute
|
||||||
|
get() = startsWith(File.separator)
|
||||||
|
|
||||||
|
class FileReadStandardKeyValueRepo(
|
||||||
|
private val folder: File
|
||||||
|
) : ReadStandardKeyValueRepo<String, File> {
|
||||||
|
init {
|
||||||
|
folder.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun get(k: String): File? {
|
||||||
|
val file = File(folder, k)
|
||||||
|
if (file.exists()) {
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun values(pagination: Pagination, reversed: Boolean): PaginationResult<File> {
|
||||||
|
val count = count()
|
||||||
|
val resultPagination = if (reversed) pagination.reverse(count) else pagination
|
||||||
|
val filesPaths = folder.list() ?.copyOfRange(resultPagination.firstIndex, resultPagination.lastIndex) ?: return emptyPaginationResult()
|
||||||
|
if (reversed) {
|
||||||
|
filesPaths.reverse()
|
||||||
|
}
|
||||||
|
return filesPaths.map { File(folder, it) }.createPaginationResult(
|
||||||
|
resultPagination,
|
||||||
|
count
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult<String> {
|
||||||
|
val count = count()
|
||||||
|
val resultPagination = if (reversed) pagination.reverse(count) else pagination
|
||||||
|
val filesPaths = folder.list() ?.copyOfRange(resultPagination.firstIndex, resultPagination.lastIndex) ?: return emptyPaginationResult()
|
||||||
|
if (reversed) {
|
||||||
|
filesPaths.reverse()
|
||||||
|
}
|
||||||
|
return filesPaths.toList().createPaginationResult(
|
||||||
|
resultPagination,
|
||||||
|
count
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun keys(
|
||||||
|
v: File,
|
||||||
|
pagination: Pagination,
|
||||||
|
reversed: Boolean
|
||||||
|
): PaginationResult<String> {
|
||||||
|
val resultPagination = if (reversed) pagination.reverse(1L) else pagination
|
||||||
|
return if (resultPagination.isFirstPage) {
|
||||||
|
val fileSubpath = v.absolutePath.removePrefix(folder.absolutePath)
|
||||||
|
if (fileSubpath == v.absolutePath) {
|
||||||
|
emptyList()
|
||||||
|
} else {
|
||||||
|
listOf(fileSubpath)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}.createPaginationResult(resultPagination, 1L)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun contains(key: String): Boolean {
|
||||||
|
return File(folder, key).exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun count(): Long = folder.list() ?.size ?.toLong() ?: 0L
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Files watching will not correctly works on Android with version of API lower than API 26
|
||||||
|
*/
|
||||||
|
@Warning("Files watching will not correctly works on Android Platform with version of API lower than API 26")
|
||||||
|
class FileWriteStandardKeyValueRepo(
|
||||||
|
private val folder: File,
|
||||||
|
filesChangedProcessingScope: CoroutineScope? = null
|
||||||
|
) : WriteStandardKeyValueRepo<String, File> {
|
||||||
|
private val _onNewValue = MutableSharedFlow<Pair<String, File>>()
|
||||||
|
override val onNewValue: Flow<Pair<String, File>> = _onNewValue.asSharedFlow()
|
||||||
|
private val _onValueRemoved = MutableSharedFlow<String>()
|
||||||
|
override val onValueRemoved: Flow<String> = _onValueRemoved.asSharedFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
folder.mkdirs()
|
||||||
|
filesChangedProcessingScope ?.let {
|
||||||
|
it.launch {
|
||||||
|
try {
|
||||||
|
val folderPath = folder.toPath()
|
||||||
|
while (isActive) {
|
||||||
|
val key = try {
|
||||||
|
folderPath.register(FileSystems.getDefault().newWatchService(), ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// add verbose way to show that file watching is not working
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
for (event in key.pollEvents()) {
|
||||||
|
val relativeFilePath = (event.context() as? Path) ?: continue
|
||||||
|
val file = relativeFilePath.toFile()
|
||||||
|
val relativePath = file.toRelativeString(folder)
|
||||||
|
|
||||||
|
when (event.kind()) {
|
||||||
|
ENTRY_CREATE, ENTRY_MODIFY -> {
|
||||||
|
launch {
|
||||||
|
_onNewValue.emit(relativePath to file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ENTRY_DELETE -> {
|
||||||
|
launch {
|
||||||
|
_onValueRemoved.emit(relativePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.isValid || folder.exists()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// add verbose way to notify that this functionality is disabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun set(toSet: Map<String, File>) {
|
||||||
|
supervisorScope {
|
||||||
|
toSet.map { (filename, fileSource) ->
|
||||||
|
launch {
|
||||||
|
val file = File(folder, filename)
|
||||||
|
|
||||||
|
file.delete()
|
||||||
|
fileSource.copyTo(file, overwrite = true)
|
||||||
|
_onNewValue.emit(filename to file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.joinAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun unset(toUnset: List<String>) {
|
||||||
|
toUnset.forEach {
|
||||||
|
val file = File(folder, it)
|
||||||
|
if (file.exists()) {
|
||||||
|
file.delete()
|
||||||
|
_onValueRemoved.emit(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun unsetWithValues(toUnset: List<File>) {
|
||||||
|
val keys = toUnset.mapNotNull { v ->
|
||||||
|
val key = v.absolutePath.removePrefix(folder.absolutePath)
|
||||||
|
if (key != v.absolutePath) {
|
||||||
|
key
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset(keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Warning("Files watching will not correctly works on Android Platform with version of API lower than API 26")
|
||||||
|
class FileStandardKeyValueRepo(
|
||||||
|
folder: File,
|
||||||
|
filesChangedProcessingScope: CoroutineScope? = null
|
||||||
|
) : StandardKeyValueRepo<String, File>,
|
||||||
|
WriteStandardKeyValueRepo<String, File> by FileWriteStandardKeyValueRepo(folder, filesChangedProcessingScope),
|
||||||
|
ReadStandardKeyValueRepo<String, File> by FileReadStandardKeyValueRepo(folder)
|
@@ -1,6 +1,6 @@
|
|||||||
package dev.inmo.micro_utils.repos
|
package dev.inmo.micro_utils.repos
|
||||||
|
|
||||||
import kotlinx.coroutines.newSingleThreadContext
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
val DatabaseCoroutineContext: CoroutineContext = newSingleThreadContext("db-context")
|
val DatabaseCoroutineContext: CoroutineContext = Dispatchers.IO
|
||||||
|
@@ -2,26 +2,68 @@ package dev.inmo.micro_utils.repos
|
|||||||
|
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import dev.inmo.micro_utils.coroutines.safely
|
import dev.inmo.micro_utils.coroutines.safely
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
|
private object ContextsPool {
|
||||||
|
private val contexts = mutableListOf<CoroutineContext>()
|
||||||
|
private val mutex = Mutex(locked = false)
|
||||||
|
private val freeContexts = mutableListOf<CoroutineContext>()
|
||||||
|
|
||||||
|
suspend fun acquireContext(): CoroutineContext {
|
||||||
|
return mutex.withLock {
|
||||||
|
freeContexts.removeFirstOrNull() ?: Executors.newSingleThreadExecutor().asCoroutineDispatcher().also {
|
||||||
|
contexts.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun freeContext(context: CoroutineContext) {
|
||||||
|
return mutex.withLock {
|
||||||
|
if (context in contexts && context !in freeContexts) {
|
||||||
|
freeContexts.add(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> use(block: suspend (CoroutineContext) -> T): T = acquireContext().let {
|
||||||
|
try {
|
||||||
|
block(it)
|
||||||
|
} finally {
|
||||||
|
freeContext(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TransactionContext(
|
||||||
|
val databaseContext: CoroutineContext
|
||||||
|
): CoroutineContext.Element {
|
||||||
|
override val key: CoroutineContext.Key<TransactionContext> = TransactionContext
|
||||||
|
|
||||||
|
companion object : CoroutineContext.Key<TransactionContext>
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun <T> SQLiteDatabase.transaction(block: suspend SQLiteDatabase.() -> T): T {
|
suspend fun <T> SQLiteDatabase.transaction(block: suspend SQLiteDatabase.() -> T): T {
|
||||||
return withContext(DatabaseCoroutineContext) {
|
return coroutineContext[TransactionContext] ?.let {
|
||||||
when {
|
withContext(it.databaseContext) {
|
||||||
inTransaction() -> {
|
block()
|
||||||
block()
|
}
|
||||||
}
|
} ?: ContextsPool.use { context ->
|
||||||
else -> {
|
withContext(TransactionContext(context) + context) {
|
||||||
beginTransaction()
|
beginTransaction()
|
||||||
safely(
|
safely(
|
||||||
{
|
{
|
||||||
endTransaction()
|
endTransaction()
|
||||||
throw it
|
throw it
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
block().also {
|
block().also {
|
||||||
setTransactionSuccessful()
|
setTransactionSuccessful()
|
||||||
endTransaction()
|
endTransaction()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,25 +5,11 @@ import android.database.DatabaseErrorHandler
|
|||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import android.database.sqlite.SQLiteOpenHelper
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
import dev.inmo.micro_utils.coroutines.safely
|
import dev.inmo.micro_utils.coroutines.safely
|
||||||
|
import dev.inmo.micro_utils.repos.versions.*
|
||||||
import kotlin.coroutines.Continuation
|
import kotlin.coroutines.Continuation
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
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(
|
class StandardSQLHelper(
|
||||||
context: Context,
|
context: Context,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -36,6 +22,9 @@ class StandardSQLHelper(
|
|||||||
|
|
||||||
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {}
|
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {}
|
||||||
}
|
}
|
||||||
|
val versionsRepo: VersionsRepo<SQLiteOpenHelper> by lazy {
|
||||||
|
StandardVersionsRepo(AndroidSQLStandardVersionsRepoProxy(sqlOpenHelper))
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun <T> writableTransaction(block: suspend SQLiteDatabase.() -> T): T = sqlOpenHelper.writableTransaction(block)
|
suspend fun <T> writableTransaction(block: suspend SQLiteDatabase.() -> T): T = sqlOpenHelper.writableTransaction(block)
|
||||||
|
|
||||||
|
@@ -3,20 +3,18 @@ package dev.inmo.micro_utils.repos.crud
|
|||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import dev.inmo.micro_utils.common.mapNotNullA
|
import dev.inmo.micro_utils.common.mapNotNullA
|
||||||
import dev.inmo.micro_utils.repos.*
|
import dev.inmo.micro_utils.repos.*
|
||||||
import kotlinx.coroutines.channels.BroadcastChannel
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.asFlow
|
|
||||||
|
|
||||||
abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType>(
|
abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType>(
|
||||||
helper: StandardSQLHelper
|
helper: StandardSQLHelper
|
||||||
) : WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>,
|
) : WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>,
|
||||||
AbstractAndroidCRUDRepo<ObjectType, IdType>(helper) {
|
AbstractAndroidCRUDRepo<ObjectType, IdType>(helper) {
|
||||||
protected val newObjectsChannel = BroadcastChannel<ObjectType>(64)
|
protected val newObjectsChannel = MutableSharedFlow<ObjectType>(64)
|
||||||
protected val updateObjectsChannel = BroadcastChannel<ObjectType>(64)
|
protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(64)
|
||||||
protected val deleteObjectsIdsChannel = BroadcastChannel<IdType>(64)
|
protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(64)
|
||||||
override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asFlow()
|
override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asSharedFlow()
|
||||||
override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asFlow()
|
override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow()
|
||||||
override val deletedObjectsIdsFlow: Flow<IdType> = deleteObjectsIdsChannel.asFlow()
|
override val deletedObjectsIdsFlow: Flow<IdType> = deleteObjectsIdsChannel.asSharedFlow()
|
||||||
|
|
||||||
protected abstract suspend fun InputValueType.asContentValues(id: IdType? = null): ContentValues
|
protected abstract suspend fun InputValueType.asContentValues(id: IdType? = null): ContentValues
|
||||||
|
|
||||||
@@ -42,7 +40,7 @@ abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType
|
|||||||
}
|
}
|
||||||
}.also {
|
}.also {
|
||||||
it.forEach {
|
it.forEach {
|
||||||
newObjectsChannel.send(it)
|
newObjectsChannel.emit(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,7 +57,7 @@ abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
deleted.forEach {
|
deleted.forEach {
|
||||||
deleteObjectsIdsChannel.send(it)
|
deleteObjectsIdsChannel.emit(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +74,7 @@ abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return getById(id) ?.also {
|
return getById(id) ?.also {
|
||||||
updateObjectsChannel.send(it)
|
updateObjectsChannel.emit(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +93,7 @@ abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType
|
|||||||
getById(it.first)
|
getById(it.first)
|
||||||
}.also {
|
}.also {
|
||||||
it.forEach {
|
it.forEach {
|
||||||
updateObjectsChannel.send(it)
|
updateObjectsChannel.emit(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,10 +8,7 @@ import dev.inmo.micro_utils.pagination.PaginationResult
|
|||||||
import dev.inmo.micro_utils.pagination.utils.paginate
|
import dev.inmo.micro_utils.pagination.utils.paginate
|
||||||
import dev.inmo.micro_utils.pagination.utils.reverse
|
import dev.inmo.micro_utils.pagination.utils.reverse
|
||||||
import dev.inmo.micro_utils.repos.StandardKeyValueRepo
|
import dev.inmo.micro_utils.repos.StandardKeyValueRepo
|
||||||
import kotlinx.coroutines.channels.BroadcastChannel
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.asFlow
|
|
||||||
|
|
||||||
private val cache = HashMap<String, KeyValueStore<*>>()
|
private val cache = HashMap<String, KeyValueStore<*>>()
|
||||||
|
|
||||||
@@ -37,11 +34,11 @@ class KeyValueStore<T : Any> internal constructor (
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
private val onNewValueChannel = BroadcastChannel<Pair<String, T>>(Channel.BUFFERED)
|
private val onNewValueChannel = MutableSharedFlow<Pair<String, T>>()
|
||||||
private val onValueRemovedChannel = BroadcastChannel<String>(Channel.BUFFERED)
|
private val _onValueRemovedFlow = MutableSharedFlow<String>()
|
||||||
|
|
||||||
override val onNewValue: Flow<Pair<String, T>> = onNewValueChannel.asFlow()
|
override val onNewValue: Flow<Pair<String, T>> = onNewValueChannel.asSharedFlow()
|
||||||
override val onValueRemoved: Flow<String> = onValueRemovedChannel.asFlow()
|
override val onValueRemoved: Flow<String> = _onValueRemovedFlow.asSharedFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
cachedData ?.let {
|
cachedData ?.let {
|
||||||
@@ -131,7 +128,7 @@ class KeyValueStore<T : Any> internal constructor (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
toSet.forEach { (k, v) ->
|
toSet.forEach { (k, v) ->
|
||||||
onNewValueChannel.send(k to v)
|
onNewValueChannel.emit(k to v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +136,18 @@ class KeyValueStore<T : Any> internal constructor (
|
|||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
toUnset.forEach { remove(it) }
|
toUnset.forEach { remove(it) }
|
||||||
}
|
}
|
||||||
toUnset.forEach { onValueRemovedChannel.send(it) }
|
toUnset.forEach { _onValueRemovedFlow.emit(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun unsetWithValues(toUnset: List<T>) {
|
||||||
|
val keysToRemove = sharedPreferences.all.mapNotNull { if (it.value in toUnset) it.key else null }
|
||||||
|
sharedPreferences.edit {
|
||||||
|
keysToRemove.map {
|
||||||
|
remove(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keysToRemove.forEach {
|
||||||
|
_onValueRemovedFlow.emit(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -87,6 +87,29 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun set(toSet: Map<Key, List<Value>>) {
|
||||||
|
val (clearedKeys, inserted) = helper.writableTransaction {
|
||||||
|
toSet.mapNotNull { (k, _) ->
|
||||||
|
if (delete(tableName, "$idColumnName=?", arrayOf(k.asId())) > 0) {
|
||||||
|
k
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} to toSet.flatMap { (k, values) ->
|
||||||
|
values.map { v ->
|
||||||
|
insert(
|
||||||
|
tableName,
|
||||||
|
null,
|
||||||
|
contentValuesOf(idColumnName to k.asId(), valueColumnName to v.asValue())
|
||||||
|
)
|
||||||
|
k to v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clearedKeys.forEach { _onDataCleared.emit(it) }
|
||||||
|
inserted.forEach { newPair -> _onNewValue.emit(newPair) }
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun contains(k: Key): Boolean = helper.readableTransaction {
|
override suspend fun contains(k: Key): Boolean = helper.readableTransaction {
|
||||||
select(tableName, selection = "$idColumnName=?", selectionArgs = arrayOf(k.asId()), limit = FirstPagePagination(1).limitClause()).use {
|
select(tableName, selection = "$idColumnName=?", selectionArgs = arrayOf(k.asId()), limit = FirstPagePagination(1).limitClause()).use {
|
||||||
it.count > 0
|
it.count > 0
|
||||||
|
@@ -0,0 +1,63 @@
|
|||||||
|
package dev.inmo.micro_utils.repos.versions
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
|
import androidx.core.content.contentValuesOf
|
||||||
|
import dev.inmo.micro_utils.repos.*
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will create [VersionsRepo] based on [SQLiteOpenHelper] with table inside of [database]
|
||||||
|
*/
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
inline fun versionsRepo(database: SQLiteOpenHelper): VersionsRepo<SQLiteOpenHelper> = StandardVersionsRepo(
|
||||||
|
AndroidSQLStandardVersionsRepoProxy(database)
|
||||||
|
)
|
||||||
|
|
||||||
|
class AndroidSQLStandardVersionsRepoProxy(
|
||||||
|
override val database: SQLiteOpenHelper
|
||||||
|
) : StandardVersionsRepoProxy<SQLiteOpenHelper> {
|
||||||
|
private val tableName: String = "AndroidSQLStandardVersionsRepo"
|
||||||
|
private val tableNameColumnName = "tableName"
|
||||||
|
private val tableVersionColumnName = "version"
|
||||||
|
|
||||||
|
init {
|
||||||
|
runBlocking(DatabaseCoroutineContext) {
|
||||||
|
database.writableTransaction {
|
||||||
|
createTable(
|
||||||
|
tableName,
|
||||||
|
tableNameColumnName to ColumnType.Text.NOT_NULLABLE,
|
||||||
|
tableVersionColumnName to ColumnType.Numeric.INTEGER()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getTableVersion(tableName: String): Int? = database.writableTransaction {
|
||||||
|
select(
|
||||||
|
this@AndroidSQLStandardVersionsRepoProxy.tableName,
|
||||||
|
selection = "$tableNameColumnName=?",
|
||||||
|
selectionArgs = arrayOf(tableName),
|
||||||
|
limit = limitClause(1)
|
||||||
|
).use {
|
||||||
|
if (it.moveToFirst()) {
|
||||||
|
it.getInt(tableVersionColumnName)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updateTableVersion(tableName: String, version: Int) {
|
||||||
|
database.writableTransaction {
|
||||||
|
val updated = update(
|
||||||
|
this@AndroidSQLStandardVersionsRepoProxy.tableName,
|
||||||
|
contentValuesOf(tableVersionColumnName to version),
|
||||||
|
"$tableNameColumnName=?",
|
||||||
|
arrayOf(tableName)
|
||||||
|
) > 0
|
||||||
|
if (!updated) {
|
||||||
|
insert(this@AndroidSQLStandardVersionsRepoProxy.tableName, null, contentValuesOf(tableNameColumnName to tableName, tableVersionColumnName to version))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,49 @@
|
|||||||
|
@file:Suppress("NOTHING_TO_INLINE")
|
||||||
|
|
||||||
|
package dev.inmo.micro_utils.repos.versions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
|
import androidx.core.content.contentValuesOf
|
||||||
|
import dev.inmo.micro_utils.repos.*
|
||||||
|
import dev.inmo.micro_utils.repos.keyvalue.keyValueStore
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will create [VersionsRepo] based on [T], but versions will be stored in [StandardKeyValueRepo]
|
||||||
|
*
|
||||||
|
* @receiver Will be used to create [KeyValueBasedVersionsRepoProxy] via [keyValueStore] and pass it to [StandardVersionsRepo]
|
||||||
|
*
|
||||||
|
* @see [KeyValueBasedVersionsRepoProxy]
|
||||||
|
* @see [keyValueStore]
|
||||||
|
*/
|
||||||
|
inline fun <T> Context.versionsKeyValueRepo(
|
||||||
|
database: T
|
||||||
|
): VersionsRepo<T> = StandardVersionsRepo(
|
||||||
|
KeyValueBasedVersionsRepoProxy(
|
||||||
|
keyValueStore("SPVersionsRepo"),
|
||||||
|
database
|
||||||
|
)
|
||||||
|
)
|
||||||
|
/**
|
||||||
|
* Will create [VersionsRepo] based on [SQLiteOpenHelper], but versions will be stored in [StandardKeyValueRepo]
|
||||||
|
*
|
||||||
|
* @receiver Will be used to create [StandardKeyValueRepo] via [keyValueStore] and pass it to [StandardVersionsRepo]
|
||||||
|
*
|
||||||
|
* @see [keyValueStore]
|
||||||
|
*/
|
||||||
|
inline fun Context.versionsKeyValueRepoForSQL(
|
||||||
|
database: SQLiteOpenHelper
|
||||||
|
) = versionsKeyValueRepo(database)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will create [VersionsRepo] based on [SQLiteOpenHelper], but versions will be stored in [StandardKeyValueRepo]
|
||||||
|
*
|
||||||
|
* @param context Will be used to create [StandardKeyValueRepo] via [keyValueStore] and pass it to [StandardVersionsRepo]
|
||||||
|
*
|
||||||
|
* @see [keyValueStore]
|
||||||
|
*/
|
||||||
|
inline fun versionsRepo(
|
||||||
|
context: Context,
|
||||||
|
database: SQLiteOpenHelper
|
||||||
|
) = context.versionsKeyValueRepoForSQL(database)
|
@@ -2,7 +2,6 @@ package dev.inmo.micro_utils.repos.exposed
|
|||||||
|
|
||||||
import dev.inmo.micro_utils.repos.UpdatedValuePair
|
import dev.inmo.micro_utils.repos.UpdatedValuePair
|
||||||
import dev.inmo.micro_utils.repos.WriteStandardCRUDRepo
|
import dev.inmo.micro_utils.repos.WriteStandardCRUDRepo
|
||||||
import kotlinx.coroutines.channels.BroadcastChannel
|
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
import org.jetbrains.exposed.sql.statements.InsertStatement
|
import org.jetbrains.exposed.sql.statements.InsertStatement
|
||||||
@@ -25,8 +24,10 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
|
|||||||
override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow()
|
override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow()
|
||||||
override val deletedObjectsIdsFlow: Flow<IdType> = deleteObjectsIdsChannel.asSharedFlow()
|
override val deletedObjectsIdsFlow: Flow<IdType> = deleteObjectsIdsChannel.asSharedFlow()
|
||||||
|
|
||||||
|
@Deprecated("Will be removed in near major update. Override open fun with the same name instead")
|
||||||
abstract val InsertStatement<Number>.asObject: ObjectType
|
abstract val InsertStatement<Number>.asObject: ObjectType
|
||||||
abstract val selectByIds: SqlExpressionBuilder.(List<out IdType>) -> Op<Boolean>
|
protected open fun InsertStatement<Number>.asObject(value: InputValueType): ObjectType = asObject
|
||||||
|
abstract val selectByIds: SqlExpressionBuilder.(List<IdType>) -> Op<Boolean>
|
||||||
|
|
||||||
protected abstract fun insert(value: InputValueType, it: InsertStatement<Number>)
|
protected abstract fun insert(value: InputValueType, it: InsertStatement<Number>)
|
||||||
protected abstract fun update(id: IdType, value: InputValueType, it: UpdateStatement)
|
protected abstract fun update(id: IdType, value: InputValueType, it: UpdateStatement)
|
||||||
@@ -34,7 +35,7 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
|
|||||||
protected open suspend fun onBeforeCreate(value: List<InputValueType>) {}
|
protected open suspend fun onBeforeCreate(value: List<InputValueType>) {}
|
||||||
private fun createWithoutNotification(value: InputValueType): ObjectType {
|
private fun createWithoutNotification(value: InputValueType): ObjectType {
|
||||||
return transaction(database) {
|
return transaction(database) {
|
||||||
insert { insert(value, it) }.asObject
|
insert { insert(value, it) }.asObject(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,10 +43,8 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
|
|||||||
onBeforeCreate(values)
|
onBeforeCreate(values)
|
||||||
return transaction(db = database) {
|
return transaction(db = database) {
|
||||||
values.map { value -> createWithoutNotification(value) }
|
values.map { value -> createWithoutNotification(value) }
|
||||||
}.also {
|
}.onEach {
|
||||||
it.forEach {
|
newObjectsChannel.emit(it)
|
||||||
newObjectsChannel.emit(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,9 +60,11 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
|
|||||||
}
|
}
|
||||||
}.let {
|
}.let {
|
||||||
if (it > 0) {
|
if (it > 0) {
|
||||||
select {
|
transaction(db = database) {
|
||||||
selectById(this, id)
|
select {
|
||||||
}.limit(1).firstOrNull() ?.asObject
|
selectById(this, id)
|
||||||
|
}.limit(1).firstOrNull() ?.asObject
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -83,13 +84,9 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
|
|||||||
return (
|
return (
|
||||||
transaction(db = database) {
|
transaction(db = database) {
|
||||||
values.map { (id, value) -> updateWithoutNotification(id, value) }
|
values.map { (id, value) -> updateWithoutNotification(id, value) }
|
||||||
}.filter {
|
}.filterNotNull()
|
||||||
it != null
|
).onEach {
|
||||||
} as List<ObjectType>
|
updateObjectsChannel.emit(it)
|
||||||
).also {
|
|
||||||
it.forEach {
|
|
||||||
updateObjectsChannel.emit(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected open suspend fun onBeforeDelete(ids: List<IdType>) {}
|
protected open suspend fun onBeforeDelete(ids: List<IdType>) {}
|
||||||
|
@@ -2,6 +2,7 @@ package dev.inmo.micro_utils.repos.exposed.keyvalue
|
|||||||
|
|
||||||
import dev.inmo.micro_utils.repos.StandardKeyValueRepo
|
import dev.inmo.micro_utils.repos.StandardKeyValueRepo
|
||||||
import dev.inmo.micro_utils.repos.exposed.ColumnAllocator
|
import dev.inmo.micro_utils.repos.exposed.ColumnAllocator
|
||||||
|
import dev.inmo.micro_utils.repos.exposed.initTable
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
@@ -18,8 +19,8 @@ open class ExposedKeyValueRepo<Key, Value>(
|
|||||||
valueColumnAllocator,
|
valueColumnAllocator,
|
||||||
tableName
|
tableName
|
||||||
) {
|
) {
|
||||||
private val _onNewValue = MutableSharedFlow<Pair<Key, Value>>(Channel.BUFFERED)
|
private val _onNewValue = MutableSharedFlow<Pair<Key, Value>>()
|
||||||
private val _onValueRemoved = MutableSharedFlow<Key>(Channel.BUFFERED)
|
private val _onValueRemoved = MutableSharedFlow<Key>()
|
||||||
|
|
||||||
override val onNewValue: Flow<Pair<Key, Value>> = _onNewValue.asSharedFlow()
|
override val onNewValue: Flow<Pair<Key, Value>> = _onNewValue.asSharedFlow()
|
||||||
override val onValueRemoved: Flow<Key> = _onValueRemoved.asSharedFlow()
|
override val onValueRemoved: Flow<Key> = _onValueRemoved.asSharedFlow()
|
||||||
@@ -59,4 +60,16 @@ open class ExposedKeyValueRepo<Key, Value>(
|
|||||||
_onValueRemoved.emit(it)
|
_onValueRemoved.emit(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun unsetWithValues(toUnset: List<Value>) {
|
||||||
|
transaction(database) {
|
||||||
|
toUnset.flatMap {
|
||||||
|
val keys = select { valueColumn.eq(it) }.mapNotNull { it[keyColumn] }
|
||||||
|
deleteWhere { keyColumn.inList(keys) }
|
||||||
|
keys
|
||||||
|
}
|
||||||
|
}.distinct().forEach {
|
||||||
|
_onValueRemoved.emit(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,8 +2,7 @@ package dev.inmo.micro_utils.repos.exposed.keyvalue
|
|||||||
|
|
||||||
import dev.inmo.micro_utils.pagination.*
|
import dev.inmo.micro_utils.pagination.*
|
||||||
import dev.inmo.micro_utils.repos.ReadStandardKeyValueRepo
|
import dev.inmo.micro_utils.repos.ReadStandardKeyValueRepo
|
||||||
import dev.inmo.micro_utils.repos.exposed.ColumnAllocator
|
import dev.inmo.micro_utils.repos.exposed.*
|
||||||
import dev.inmo.micro_utils.repos.exposed.ExposedRepo
|
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
@@ -17,6 +16,8 @@ open class ExposedReadKeyValueRepo<Key, Value>(
|
|||||||
protected val valueColumn: Column<Value> = valueColumnAllocator()
|
protected val valueColumn: Column<Value> = valueColumnAllocator()
|
||||||
override val primaryKey: PrimaryKey = PrimaryKey(keyColumn, valueColumn)
|
override val primaryKey: PrimaryKey = PrimaryKey(keyColumn, valueColumn)
|
||||||
|
|
||||||
|
init { initTable() }
|
||||||
|
|
||||||
override suspend fun get(k: Key): Value? = transaction(database) {
|
override suspend fun get(k: Key): Value? = transaction(database) {
|
||||||
select { keyColumn.eq(k) }.limit(1).firstOrNull() ?.getOrNull(valueColumn)
|
select { keyColumn.eq(k) }.limit(1).firstOrNull() ?.getOrNull(valueColumn)
|
||||||
}
|
}
|
||||||
@@ -33,8 +34,8 @@ open class ExposedReadKeyValueRepo<Key, Value>(
|
|||||||
}
|
}
|
||||||
}.createPaginationResult(pagination, count())
|
}.createPaginationResult(pagination, count())
|
||||||
|
|
||||||
override suspend fun keys(value: Value, pagination: Pagination, reversed: Boolean): PaginationResult<Key> = transaction(database) {
|
override suspend fun keys(v: Value, pagination: Pagination, reversed: Boolean): PaginationResult<Key> = transaction(database) {
|
||||||
select { valueColumn.eq(value) }.let {
|
select { valueColumn.eq(v) }.let {
|
||||||
it.count() to it.paginate(pagination, keyColumn to if (reversed) SortOrder.DESC else SortOrder.ASC).map {
|
it.count() to it.paginate(pagination, keyColumn to if (reversed) SortOrder.DESC else SortOrder.ASC).map {
|
||||||
it[keyColumn]
|
it[keyColumn]
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user