Compare commits

...

47 Commits

Author SHA1 Message Date
0d8f844314 updates in coroutines 2021-01-19 13:28:50 +06:00
abd0cb2031 start 0.4.19 2021-01-19 13:24:38 +06:00
79ef03ed0c Merge pull request #39 from InsanusMokrassar/0.4.18
0.4.18
2021-01-17 17:17:39 +06:00
a3087cb650 awaitFirst 2021-01-17 17:15:54 +06:00
9acb9af338 Base64ByteArraySerializer 2021-01-17 16:53:24 +06:00
0c9283eb87 start 0.4.18 2021-01-17 16:49:10 +06:00
493d838201 Merge branch '0.4.17' 2021-01-14 21:22:42 +06:00
c0523469fa remove "Serializer" annotation at Base64StringSerializer 2021-01-14 21:22:25 +06:00
4f00eaa6d4 Merge pull request #37 from InsanusMokrassar/0.4.17
0.4.17
2021-01-14 20:55:45 +06:00
9706524572 update readme 2021-01-14 20:55:32 +06:00
3174b84367 base64 tools 2021-01-14 20:51:13 +06:00
6cccd5ff6c start add base64 2021-01-14 14:22:26 +06:00
972857268d start 0.4.17 2021-01-14 14:22:09 +06:00
f3cd92cc5e Merge pull request #36 from InsanusMokrassar/0.4.16
0.4.16
2021-01-08 00:56:23 +06:00
502a49644c safelyWithoutException updates 2021-01-07 10:22:28 +06:00
51719b5868 start 0.4.16 2021-01-07 10:08:39 +06:00
17821bd094 Merge pull request #35 from InsanusMokrassar/0.4.15
0.4.15
2020-12-22 23:18:58 +06:00
6a89ffaa8a rework of context exceptions 2020-12-22 23:17:28 +06:00
d5a8d0f4d4 Revert "unworking version"
This reverts commit 9739bd871e.
2020-12-22 22:45:43 +06:00
9739bd871e unworking version 2020-12-22 22:45:36 +06:00
632d2545d4 start 0.4.15 2020-12-22 21:39:43 +06:00
ecfa273a81 Merge pull request #33 from InsanusMokrassar/0.4.14
0.4.14
2020-12-22 16:00:50 +06:00
a36828116e safely fixes 2020-12-22 15:42:11 +06:00
14aa9ca26c update documentation 2020-12-22 15:21:25 +06:00
069e51f2ff add defaultSafelyExceptionHandler and SafelyExceptionHandler 2020-12-22 15:16:06 +06:00
a15cbdfb1a update versions 2020-12-22 14:55:01 +06:00
4af8114eda start 0.4.14 2020-12-22 14:48:02 +06:00
90dc84e900 Merge pull request #32 from InsanusMokrassar/0.4.13
0.4.13
2020-12-15 13:39:22 +06:00
67c595b440 small refactor in expand/collapse 2020-12-15 13:26:34 +06:00
830b7aee56 horizontal expand/collapse 2020-12-15 13:25:08 +06:00
1890608cb3 start 0.4.13 2020-12-15 12:49:50 +06:00
bd396959a9 Merge pull request #31 from InsanusMokrassar/0.4.12
0.4.12
2020-12-14 21:11:57 +06:00
d5fe19f0a5 update changelog 2020-12-14 20:20:30 +06:00
46bfb09415 launchSynchronously signature update 2020-12-14 20:06:46 +06:00
a60cb596d1 add functionality to selector 2020-12-14 20:01:37 +06:00
6f9d5e2d5f add itemSelectionFlow 2020-12-14 19:45:14 +06:00
80bc226ee1 selector docs and several refactorings 2020-12-14 19:36:07 +06:00
12e37184e1 add selector 2020-12-14 19:17:16 +06:00
25e9345d02 start 0.4.12 2020-12-14 18:44:24 +06:00
ccc4d030c3 Merge pull request #30 from InsanusMokrassar/0.4.11
0.4.11
2020-12-07 22:44:29 +06:00
90c0817b6d add clamp 2020-12-07 22:39:23 +06:00
527f7bbafe start 0.4.11 2020-12-07 22:36:32 +06:00
765a32729f Update README.md 2020-12-05 19:01:36 +06:00
de783f77a2 Update README.md 2020-12-05 18:59:28 +06:00
de4c8d104c Set theme jekyll-theme-cayman 2020-12-05 18:57:46 +06:00
88c8c28f45 Update README.md 2020-12-05 18:57:12 +06:00
57b36826d1 Merge pull request #29 from InsanusMokrassar/0.4.10
0.4.10
2020-12-05 18:36:24 +06:00
27 changed files with 879 additions and 55 deletions

View File

@@ -1,5 +1,77 @@
# Changelog
## 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`:

View File

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

@@ -0,0 +1 @@
theme: jekyll-theme-cayman

View File

@@ -0,0 +1,77 @@
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
typealias EncodedBase64String = String
typealias EncodedByteArray = ByteArray
fun String.encodeBase64String(): EncodedBase64String = encodeToByteArray().encodeBase64String()
fun String.encodeBase64(): EncodedByteArray = encodeToByteArray().encodeBase64()
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
}
}
fun ByteArray.encodeBase64(): EncodedByteArray = encodeBase64String().encodeToByteArray()
fun EncodedBase64String.decodeBase64() = dropLastWhile { it == BASE64_PAD }.encodeToByteArray().decodeBase64()
fun EncodedBase64String.decodeBase64String() = decodeBase64().decodeToString()
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()
}
fun EncodedByteArray.decodeBase64String() = decodeBase64().decodeToString()

View File

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

View File

@@ -0,0 +1,31 @@
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)
}
}

View File

@@ -5,23 +5,44 @@ import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.Transformation
@PreviewFeature
fun View.expand(
private fun View.performExpand(
duration: Long = 500,
targetWidth: Int = ViewGroup.LayoutParams.MATCH_PARENT,
targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT
targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT,
onMeasured: View.() -> Unit,
onPerformAnimation: View.(interpolatedTime: Float, t: Transformation?) -> Unit
) {
measure(targetWidth, targetHeight)
val measuredHeight: Int = measuredHeight
layoutParams.height = 0
visibility = View.VISIBLE
onMeasured()
show()
val a: Animation = object : Animation() {
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
super.applyTransformation(interpolatedTime, t)
layoutParams.height = if (interpolatedTime == 1f) targetHeight else (measuredHeight * interpolatedTime).toInt()
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
}
@@ -32,27 +53,58 @@ fun View.expand(
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
val a: Animation = object : Animation() {
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
if (interpolatedTime == 1f) {
visibility = View.GONE
} else {
layoutParams.height = initialHeight - (initialHeight * interpolatedTime).toInt()
requestLayout()
}
}
override fun willChangeBounds(): Boolean {
return true
}
performCollapse(duration) { interpolatedTime, _ ->
layoutParams.height = initialHeight - (initialHeight * interpolatedTime).toInt()
}
}
a.duration = duration
startAnimation(a)
@PreviewFeature
fun View.collapseHorizontally(duration: Long = 500) {
val initialWidth: Int = measuredWidth
performCollapse(duration) { interpolatedTime, _ ->
layoutParams.width = initialWidth - (initialWidth * interpolatedTime).toInt()
}
}
@PreviewFeature
@@ -74,3 +126,15 @@ fun View.toggleExpandState(duration: Long = 500): Boolean = if (isCollapsed) {
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
}

View File

@@ -19,7 +19,7 @@ fun <T> CoroutineScope.actor(
inline fun <T> CoroutineScope.safeActor(
channelCapacity: Int = Channel.UNLIMITED,
noinline onException: ExceptionHandler<Unit> = {},
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
crossinline block: suspend (T) -> Unit
): Channel<T> = actor(
channelCapacity

View File

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

View File

@@ -0,0 +1,40 @@
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())
}
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> invokeOnFirst(
scope: CoroutineScope,
vararg variants: Deferred<T>,
cancelOnResult: Boolean = true,
callback: suspend (T) -> O
): O = variants.toList().invokeOnFirst(scope, cancelOnResult, callback)

View File

@@ -16,7 +16,7 @@ inline fun <T> Flow<T>.subscribe(scope: CoroutineScope, noinline block: suspend
*/
inline fun <T> Flow<T>.subscribeSafely(
scope: CoroutineScope,
noinline onException: ExceptionHandler<Unit> = { throw it },
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
noinline block: suspend (T) -> Unit
) = subscribe(scope) {
safely(onException) {

View File

@@ -1,30 +1,156 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.coroutineContext
typealias ExceptionHandler<T> = suspend (Throwable) -> T
/**
* This instance will be used in all calls of [safely] where exception handler has not been passed
*/
var defaultSafelyExceptionHandler: ExceptionHandler<Nothing> = { throw it }
/**
* This instance will be used in all calls of [safelyWithoutExceptions] as an exception handler for [safely] call
*/
var defaultSafelyWithoutExceptionHandler: ExceptionHandler<Unit> = {
try {
defaultSafelyExceptionHandler(it)
} catch (e: Throwable) {
// do nothing
}
}
/**
* 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
*
* Priorities of [ExceptionHandler]s:
*
* * [onException] In case if custom (will be used anyway if not [defaultSafelyExceptionHandler])
* * [CoroutineContext.get] with [SafelyExceptionHandlerKey] as key
* * [defaultSafelyExceptionHandler]
*
* @param [onException] Will be called when happen exception inside of [block]. By default will throw exception - this
* exception will be available for catching
*
* @see defaultSafelyExceptionHandler
* @see safelyWithoutExceptions
* @see safelyWithContextExceptionHandler
*/
suspend inline fun <T> safely(
noinline onException: ExceptionHandler<T> = { throw it },
noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
noinline block: suspend CoroutineScope.() -> T
): T {
return try {
supervisorScope(block)
} catch (e: Throwable) {
coroutineContext[ContextSafelyExceptionHandlerKey] ?.handler ?.invoke(e)
onException(e)
}
}
/**
* Shortcut for [safely] without exception handler (instead of this you will receive null as a result)
* Shortcut for [safely] with exception handler, that as expected must return null in case of impossible creating of
* result from exception (instead of throwing it)
*/
suspend inline fun <T> safelyWithoutExceptions(
noinline onException: ExceptionHandler<T?>,
noinline block: suspend CoroutineScope.() -> T
): T? = safely(onException, block)
/**
* Shortcut for [safely] without exception handler (instead of this you will always receive null as a result)
*/
suspend inline fun <T> safelyWithoutExceptions(
noinline block: suspend CoroutineScope.() -> T
): T? = safely({ null }, block)
): T? = safelyWithoutExceptions(
{
defaultSafelyWithoutExceptionHandler.invoke(it)
null
},
block
)

View File

@@ -2,12 +2,12 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.*
fun <T> launchSynchronously(scope: CoroutineScope = CoroutineScope(Dispatchers.Default), block: suspend CoroutineScope.() -> T): T {
fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T): T {
var throwable: Throwable? = null
var result: T? = null
val objectToSynchronize = java.lang.Object()
val launchCallback = {
scope.launch {
launch {
safely(
{
throwable = it
@@ -26,3 +26,5 @@ fun <T> launchSynchronously(scope: CoroutineScope = CoroutineScope(Dispatchers.D
}
throw throwable ?: return result!!
}
fun <T> launchSynchronously(block: suspend CoroutineScope.() -> T): T = CoroutineScope(Dispatchers.Default).launchSynchronously(block)

View File

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

View File

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

View File

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

View File

@@ -6,14 +6,14 @@ kotlin.incremental.js=true
android.useAndroidX=true
android.enableJetifier=true
kotlin_version=1.4.20
kotlin_version=1.4.21
kotlin_coroutines_version=1.4.2
kotlin_serialisation_core_version=1.0.1
kotlin_exposed_version=0.28.1
ktor_version=1.4.3
ktor_version=1.5.0
klockVersion=2.0.1
klockVersion=2.0.2
github_release_plugin_version=2.2.12
@@ -40,5 +40,5 @@ dokka_version=1.4.20
# Project data
group=dev.inmo
version=0.4.10
android_code_version=14
version=0.4.19
android_code_version=23

View File

@@ -25,7 +25,7 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
val producerScope = this@channelFlow
do {
val reconnect = try {
safely ({ throw it }) {
safely {
ws(correctedUrl) {
for (received in incoming) {
when (received) {

View File

@@ -10,21 +10,6 @@ import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
private data class CallbackContinuationPair<T> (
val callback: suspend SQLiteDatabase.() -> T,
val continuation: Continuation<T>
) {
suspend fun SQLiteDatabase.execute() {
safely(
{
continuation.resumeWithException(it)
}
) {
continuation.resume(callback())
}
}
}
class StandardSQLHelper(
context: Context,
name: String,

View File

@@ -0,0 +1,17 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
}
}
}
}

View File

@@ -0,0 +1,168 @@
package dev.inmo.micro_utils.selector
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
/**
* Unified interface which can be used in any system which require some selection functionality
*/
interface Selector<T> {
val selectedItems: List<T>
val itemSelected: SharedFlow<T>
val itemUnselected: SharedFlow<T>
suspend fun toggleSelection(element: T)
suspend fun forceSelect(element: T)
suspend fun forceDeselect(element: T)
suspend fun clearSelection()
}
@Suppress("NOTHING_TO_INLINE")
inline operator fun <T> Selector<T>.contains(element: T) = selectedItems.contains(element)
@Suppress("NOTHING_TO_INLINE")
inline fun <T> Selector<T>.nothingSelected(): Boolean = selectedItems.isEmpty()
suspend inline fun <T> Selector<T>.toggleSelection(elements: List<T>) = elements.forEach { toggleSelection(it) }
suspend inline fun <T> Selector<T>.forceSelect(elements: List<T>) = elements.forEach { forceSelect(it) }
suspend inline fun <T> Selector<T>.forceDeselect(elements: List<T>) = elements.forEach { forceDeselect(it) }
suspend inline fun <T> Selector<T>.toggleSelection(firstElement: T, vararg elements: T) = toggleSelection(listOf(firstElement) + elements.toList())
suspend inline fun <T> Selector<T>.forceSelect(firstElement: T, vararg elements: T) = forceSelect(listOf(firstElement) + elements.toList())
suspend inline fun <T> Selector<T>.forceDeselect(firstElement: T, vararg elements: T) = forceDeselect(listOf(firstElement) + elements.toList())
/**
* Realization of [Selector] with one or without selected element. This realization will always have empty
* [selectedItems] when nothing selected and one element in [selectedItems] when something selected. Contains
* [selectedItem] value for simple access to currently selected item.
*
* On calling of [toggleSelection] previous selection will be erased and [itemUnselected] will emit this element.
*
* @param safeChanges Set to false to disable using of [mutex] for synchronizing changes on [toggleSelection]
*/
class SingleSelector<T>(
selectedItem: T? = null,
safeChanges: Boolean = true
) : Selector<T> {
var selectedItem: T? = selectedItem
private set
override val selectedItems: List<T>
get() = selectedItem ?.let { listOf(it) } ?: emptyList()
private val _itemSelected = MutableSharedFlow<T>()
override val itemSelected: SharedFlow<T> = _itemSelected.asSharedFlow()
private val _itemUnselected = MutableSharedFlow<T>()
override val itemUnselected: SharedFlow<T> = _itemUnselected.asSharedFlow()
private val mutex = if (safeChanges) {
Mutex()
} else {
null
}
override suspend fun forceDeselect(element: T) {
mutex ?.lock()
if (selectedItem == element) {
selectedItem = null
_itemUnselected.emit(element)
}
mutex ?.unlock()
}
override suspend fun forceSelect(element: T) {
mutex ?.lock()
if (selectedItem != element) {
selectedItem = element
_itemSelected.emit(element)
}
mutex ?.unlock()
}
override suspend fun toggleSelection(element: T) {
mutex ?.lock()
if (selectedItem == element) {
selectedItem = null
_itemUnselected.emit(element)
} else {
val previouslySelected = selectedItem
selectedItem = null
if (previouslySelected != null) {
_itemUnselected.emit(previouslySelected)
}
selectedItem = element
_itemSelected.emit(element)
}
mutex ?.unlock()
}
override suspend fun clearSelection() {
selectedItem ?.let { forceDeselect(it) }
}
}
/**
* Realization of [Selector] with multiple selected elements. On calling of [toggleSelection] this realization will select passed element OR deselect it if it is already in
* [selectedItems]
*
* @param safeChanges Set to false to disable using of [mutex] for synchronizing changes on [toggleSelection]
*/
class MultipleSelector<T>(
selectedItems: List<T> = emptyList(),
safeChanges: Boolean = true
) : Selector<T> {
private val _selectedItems: MutableList<T> = selectedItems.toMutableList()
override val selectedItems: List<T> = _selectedItems
private val _itemSelected = MutableSharedFlow<T>()
override val itemSelected: SharedFlow<T> = _itemSelected.asSharedFlow()
private val _itemUnselected = MutableSharedFlow<T>()
override val itemUnselected: SharedFlow<T> = _itemUnselected.asSharedFlow()
private val mutex = if (safeChanges) {
Mutex()
} else {
null
}
override suspend fun forceDeselect(element: T) {
mutex ?.lock()
if (_selectedItems.remove(element)) {
_itemUnselected.emit(element)
}
mutex ?.unlock()
}
override suspend fun forceSelect(element: T) {
mutex ?.lock()
if (element !in _selectedItems && _selectedItems.add(element)) {
_itemSelected.emit(element)
}
mutex ?.unlock()
}
override suspend fun toggleSelection(element: T) {
mutex ?.lock()
if (_selectedItems.remove(element)) {
_itemUnselected.emit(element)
} else {
_selectedItems.add(element)
_itemSelected.emit(element)
}
mutex ?.unlock()
}
override suspend fun clearSelection() {
mutex ?.lock()
val preSelectedItems = _selectedItems.toList()
_selectedItems.clear()
preSelectedItems.forEach { _itemUnselected.emit(it) }
mutex ?.unlock()
}
}
@Suppress("FunctionName", "NOTHING_TO_INLINE")
inline fun <T> Selector(
multiple: Boolean,
safeChanges: Boolean = true
): Selector<T> = if (multiple) {
MultipleSelector(safeChanges = safeChanges)
} else {
SingleSelector(safeChanges = safeChanges)
}

View File

@@ -0,0 +1,17 @@
package dev.inmo.micro_utils.selector
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
/**
* @return Returned [SharedFlow] will emit true when [element] has been selected in [this] [Selector] and will emit
* false when this [element] was deselected
*
* @see [Selector]
* @see [Selector.itemSelected]
* @see [Selector.itemUnselected]
*/
fun <T> Selector<T>.itemSelectionFlow(element: T, scope: CoroutineScope): SharedFlow<Boolean> = MutableSharedFlow<Boolean>().apply {
itemSelected.onEach { if (it == element) emit(true) }.launchIn(scope)
itemUnselected.onEach { if (it == element) emit(false) }.launchIn(scope)
}.asSharedFlow()

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.selector"/>

View File

@@ -0,0 +1,17 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":micro_utils.common")
}
}
}
}

View File

@@ -0,0 +1,21 @@
package dev.inmo.micro_utils.serialization.base64
import dev.inmo.micro_utils.common.*
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
open class Base64Serializer<T>(
private val converterFrom: (T) -> String,
private val converterTo: (String) -> T,
) : KSerializer<T> {
override val descriptor: SerialDescriptor = String.serializer().descriptor
override fun deserialize(decoder: Decoder): T = converterTo(decoder.decodeString().decodeBase64String())
override fun serialize(encoder: Encoder, value: T) = encoder.encodeString(converterFrom(value).encodeBase64String())
}
object Base64StringSerializer : Base64Serializer<String>({ it }, { it })
object Base64ByteArraySerializer : Base64Serializer<ByteArray>({ it.decodeToString() }, { it.encodeToByteArray() })

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.serialization.base64"/>

View File

@@ -2,6 +2,7 @@ rootProject.name='micro_utils'
String[] includes = [
":common",
":selector:common",
":pagination:common",
":pagination:exposed",
":pagination:ktor:common",
@@ -20,6 +21,7 @@ String[] includes = [
":android:recyclerview",
":android:alerts:common",
":android:alerts:recyclerview",
":serialization:base64",
":dokka"
]