Compare commits

...

59 Commits

Author SHA1 Message Date
renovate[bot]
3fbb23af20 Update Gradle to v9.4.0 2026-03-04 14:46:08 +00:00
c8ce2a41c6 Merge pull request #646 from InsanusMokrassar/0.29.0
0.29.0
2026-02-24 19:59:12 +06:00
4f270d9047 generate docs for a lot of API (test try) 2026-02-24 18:18:10 +06:00
3df90b1993 update sqlite (wow^^) 2026-02-24 15:40:05 +06:00
8e8915d84c update dependencies 2026-02-24 15:16:45 +06:00
50369e0904 migrate onto 0.29.0 2026-02-24 15:11:43 +06:00
3ae2c84c08 fill changelog 2026-02-15 18:38:07 +06:00
b2e6ab51cb update runCatchingLogging to rethrow CanellationException 2026-02-15 18:35:43 +06:00
d6496f02e6 start 0.28.1 2026-02-15 18:21:57 +06:00
38e98327b4 fill changelog for 0.28.0 2026-02-04 21:26:32 +06:00
c72a6fda5d Merge pull request #638 from InsanusMokrassar/0.28.0
0.28.0
2026-01-27 23:37:56 +06:00
23d58d42ee make it buildable 2026-01-27 16:19:43 +06:00
d972cebf47 start update dependencies 2026-01-24 19:41:35 +06:00
3558a5135b start 0.28.0 2026-01-24 16:12:54 +06:00
82460d2ce3 Merge pull request #633 from InsanusMokrassar/0.27.0
0.27.0
2026-01-15 12:53:41 +06:00
4ce2b7d3b2 update compose versions 2026-01-15 12:07:53 +06:00
2c10bb3c88 update dependencies
io.ktor:ktor-client-core 3.3.2 -> 3.3.3

com.squareup.okio:okio 3.16.2 -> 3.16.4

com.google.devtools.ksp:symbol-processing-api 2.3.2 -> 2.3.4
2026-01-15 11:56:03 +06:00
01bed4a6c9 start 0.27.0 2026-01-15 11:34:35 +06:00
52c4867468 Merge pull request #624 from InsanusMokrassar/0.26.8
0.26.8
2025-11-12 22:47:29 +06:00
92b3fd25e7 fill changelog 2025-11-12 22:25:33 +06:00
e0e1da5082 add suspendPoint 2025-11-12 22:20:17 +06:00
80953f5d09 update ktor and android script config 2025-11-12 18:18:01 +06:00
2849db57f2 start 0.26.8 2025-11-12 18:01:11 +06:00
0170b92272 Merge pull request #622 from InsanusMokrassar/0.26.7
0.26.7
2025-11-05 15:41:10 +06:00
7bcb81400b fill changelog 2025-11-05 15:14:21 +06:00
078aedfb68 update dependencies 2025-11-05 13:42:27 +06:00
cb56bf9793 Revert "add getCurrentLocale and compose translation"
This reverts commit fce47897d5.
2025-11-05 12:29:33 +06:00
b152986b4e improve SmartKeyRWLockerTests and fix SmartSemaphore 2025-11-05 12:27:24 +06:00
fce47897d5 add getCurrentLocale and compose translation 2025-10-31 19:34:09 +06:00
0b701a3e99 start 0.26.7 2025-10-31 11:21:06 +06:00
9dad353957 Merge pull request #619 from InsanusMokrassar/0.26.6
0.26.6
2025-10-19 16:24:37 +06:00
89e16b7bdb update dependencies
io.ktor:ktor-* 3.3.0 -> 3.3.1
com.squareup.okio:okio 3.16.0 -> 3.16.2
org.jetbrains.dokka:dokka-gradle-plugin 2.0.0 -> 2.1.0
2025-10-19 16:23:23 +06:00
c2965da341 start 0.26.6 2025-10-19 15:24:41 +06:00
ffb072dc5f Merge pull request #613 from InsanusMokrassar/0.26.5
0.26.5
2025-10-04 14:39:30 +06:00
a247dbcb02 rollback sqlite update 2025-10-04 14:20:18 +06:00
1dd71175f4 update dependencies 2025-09-30 23:29:04 +06:00
bbe62c0e7b start 0.26.5 2025-09-30 23:06:42 +06:00
9822ff321b Merge pull request #607 from InsanusMokrassar/0.26.4
0.26.4
2025-09-02 01:33:50 +06:00
b485d485ef MPPFilePathSeparator 2025-09-01 22:40:26 +06:00
0b3d445109 start 0.26.4 2025-09-01 22:34:32 +06:00
d7e48940bc Merge pull request #606 from InsanusMokrassar/0.26.3
0.26.3
2025-08-20 19:17:26 +06:00
1049eb0fe7 update dependencies 2025-08-20 19:15:24 +06:00
c871ef5635 start 0.26.3 2025-08-20 18:39:59 +06:00
7edfcb20c4 Merge pull request #593 from InsanusMokrassar/0.26.2
0.26.2
2025-07-31 15:22:34 +06:00
7a1438a2c0 update dependencies 2025-07-31 15:11:42 +06:00
2af8cba8cd rename SpecialMutableStateFlow to MutableRedeliverStateFlow 2025-07-29 17:53:47 +06:00
27d74c0a62 start 0.26.2 2025-07-29 17:46:31 +06:00
f86d1bfe06 Merge pull request #591 from InsanusMokrassar/0.26.1
0.26.1
2025-07-21 23:05:46 +06:00
7cc5972ff7 update dependencies 2025-07-21 22:58:00 +06:00
3bbf978b00 fixes in actorAsync 2025-07-21 22:02:30 +06:00
ed36467600 add opportunity to pass logger in subscribe async 2025-07-21 21:53:03 +06:00
dd0de327fc start 0.26.1 2025-07-21 21:37:50 +06:00
dccd3ce8fd Merge pull request #589 from InsanusMokrassar/0.26.0
0.26.0
2025-07-11 16:46:21 +06:00
fa45e7b696 fix of kspCommonMainKotlinMetadata call 2025-07-11 16:39:08 +06:00
57f009e8aa try to fix build 2025-07-11 14:40:53 +06:00
04b633a5ea fill changelog 2025-07-10 23:44:11 +06:00
20d42b05bb fixed issue with ksp (with https://github.com/google/ksp/issues/2491) 2025-07-10 19:22:08 +06:00
91ba50f1ff start updates 2025-07-10 18:21:57 +06:00
f4476c99f9 start 0.26.0 2025-07-10 17:59:06 +06:00
143 changed files with 3808 additions and 528 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.idea
.vscode
.kotlin
out/*
*.iml

View File

@@ -1,5 +1,131 @@
# Changelog
## 0.29.0
* `Versions`:
* `Kotlin`: `2.3.0` -> `2.3.10`
* `KSLog`: `1.5.2` -> `1.6.0`
* `KSP`: `2.3.4` -> `2.3.6`
* `Compose`: `1.10.0` -> `1.10.1`
* `SQLite`: `3.50.1.0` -> `3.51.2.0`
* `Coroutines`:
* `runCatchingLogging` updated to rethrow `CancellationException` and log other exceptions
## 0.28.0
**THIS VERSION CONTAINS BREAKING CHANGES DUE TO EXPOSED 1.0.0 UPDATE**
* `Versions`:
* `Kotlin`: `2.2.21` -> `2.3.0`
* `Serialization`: `1.9.0` -> `1.10.0`
* `Exposed`: `0.61.0` -> `1.0.0` (**MAJOR VERSION UPDATE**)
* `Ktor`: `3.3.3` -> `3.4.0`
* `NMCP`: `1.2.0` -> `1.2.1`
* `Repos`:
* `Exposed`:
* All Exposed-based repositories have been updated to support Exposed 1.0.0 API changes
* Import paths have been migrated to new `org.jetbrains.exposed.v1.*` package structure
* `Pagination`:
* `Exposed`:
* Updated to use new Exposed 1.0.0 import paths
## 0.27.0
* `Versions`:
* `Ktor`: `3.3.2` -> `3.3.3`
* `Okio`: `3.16.2` -> `3.16.4`
* `KSP`: `2.3.2` -> `2.3.4`
* `Compose`: `1.9.3` -> `1.10.0`
* `Compose Material3`: `1.9.0` -> `1.10.0-alpha05`
## 0.26.8
* `Versions`:
* `KSLog`: `1.5.1` -> `1.5.2`
* `Compose`: `1.9.2` -> `1.9.3`
* `Ktor`: `3.3.1` -> `3.3.2`
* `Coroutines`:
* Add simple suspend function `suspendPoint` which will ensure that current coroutine is active to let it be
destroyable even in case it have non-suspendable nature
## 0.26.7
* `Versions`:
* `Kotlin`: `2.2.20` -> `2.2.21`
* `Compose`: `1.8.2` -> `1.9.2`
* `KSP`: `2.2.20-2.0.3` -> `2.3.1`
* `Coroutines`:
* Fix `SmartSemaphore.waitRelease` to wait for the exact number of permits
* Improve `SmartKeyRWLocker` tests
* `KSP`:
* `Sealed`/`ClassCasts`/`Variations`:
* Add workaround for `NoSuchElementException` to improve processors stability on new `KSP`
* `Koin`:
* `Generator`:
* Handle missing annotation values safely (`NoSuchElementException` workaround)
* `Android`:
* `Pickers`:
* Add dependency `androidx.compose.material:material-icons-extended`
## 0.26.6
* `Versions`:
* `Ktor`: `3.3.0` -> `3.3.1`
* `Okio`: `3.16.0` -> `3.16.2`
## 0.26.5
* `Versions`:
* `Kotlin`: `2.2.10` -> `2.2.20`
* `KSLog`: `1.5.0` -> `1.5.1`
* `Ktor`: `3.2.3` -> `3.3.0`
* `KotlinX Browser`: `0.3` -> `0.5.0`
* `Koin`: `4.1.0` -> `4.1.1`
## 0.26.4
* `Common`:
* Add expect/actual `MPPFilePathSeparator`
* Fix `FileName` realization to take care about system file path separator
## 0.26.3
* `Versions`:
* `Kotlin`: `2.2.0` -> `2.2.10`
* `KSP`: `2.2.0-2.0.2` -> `2.2.10-2.0.2`
* `Android CoreKTX`: `1.16.0` -> `1.17.0`
* `Android Fragment`: `1.8.8` -> `1.8.9`
## 0.26.2
* `Versions`:
* `Ktor`: `3.2.2` -> `3.2.3`
* `Okio`: `3.15.0` -> `3.16.0`
* `Coroutines`:
* Rename `SpecialMutableStateFlow` to `MutableRedeliverStateFlow`
## 0.26.1
* `Versions`:
* `Compose`: `1.8.1` -> `1.8.2`
* `Ktor`: `3.2.1` -> `3.2.2`
* `Coroutines`:
* Add opportunity to pass logger in subscribe async
## 0.26.0
**WARNING!!! SINCE THIS VERSION IF YOU WANT TO USE SOME OF KSP MODULES, SET `ksp.useKSP2=false` IN YOUR `gradle.properties`** (see [gh issue 2491](https://github.com/google/ksp/issues/2491))
* `Versions`:
* `Kotlin`: `2.1.21` -> `2.2.0`
* `Serialization`: `1.8.1` -> `1.9.0`
* `KSLog`: `1.4.2` -> `1.5.0`
* `Ktor`: `3.1.3` -> `3.2.1`
* `Koin`: `4.0.4` -> `4.1.0`
* `Okio`: `3.12.0` -> `3.15.0`
* `KSP`: `2.1.20-1.0.31` -> `2.2.0-2.0.2`
* `kotlin-poet`: `1.18.1` -> `2.2.0`
## 0.25.8
* `Pagination`:

View File

@@ -1,3 +1,6 @@
/**
* Utility functions for creating Android AlertDialogs with simplified API.
*/
@file:Suppress("NOTHING_TO_INLINE", "unused")
package dev.inmo.micro_utils.android.alerts.common
@@ -6,8 +9,21 @@ import android.app.AlertDialog
import android.content.Context
import android.content.DialogInterface
/**
* Type alias for alert dialog button callbacks.
*/
typealias AlertDialogCallback = (DialogInterface) -> Unit
/**
* Creates an [AlertDialog.Builder] template with configurable title and buttons.
* This provides a simplified API for creating alert dialogs with positive, negative, and neutral buttons.
*
* @param title Optional dialog title
* @param positivePair Optional positive button as a pair of (text, callback)
* @param neutralPair Optional neutral button as a pair of (text, callback)
* @param negativePair Optional negative button as a pair of (text, callback)
* @return An [AlertDialog.Builder] configured with the specified parameters
*/
inline fun Context.createAlertDialogTemplate(
title: String? = null,
positivePair: Pair<String, AlertDialogCallback?>? = null,

View File

@@ -13,6 +13,7 @@ kotlin {
androidMain {
dependencies {
api project(":micro_utils.android.smalltextfield")
api libs.jb.compose.icons
}
}
}

View File

@@ -2,6 +2,16 @@ package dev.inmo.micro_utils.android.pickers
import androidx.compose.animation.core.*
/**
* Performs a fling animation with an optional target adjustment.
* If [adjustTarget] is provided, animates to the adjusted target. Otherwise, performs a decay animation.
*
* @param initialVelocity The initial velocity of the fling
* @param animationSpec The decay animation specification
* @param adjustTarget Optional function to adjust the target value based on the calculated target
* @param block Optional block to be executed during the animation
* @return The result of the animation
*/
internal suspend fun Animatable<Float, AnimationVector1D>.fling(
initialVelocity: Float,
animationSpec: DecayAnimationSpec<Float>,

View File

@@ -41,6 +41,18 @@ private inline fun PointerInputScope.checkContains(offset: Offset): Boolean {
// src: https://gist.github.com/vganin/a9a84653a9f48a2d669910fbd48e32d5
/**
* A Compose number picker component that allows users to select a number by dragging, using arrow buttons,
* or manually entering a value.
*
* @param number The currently selected number
* @param modifier The modifier to be applied to the picker
* @param range Optional range of valid numbers. If specified, the picker will be limited to this range
* @param textStyle The text style for displaying numbers
* @param arrowsColor The color of the up/down arrow buttons
* @param allowUseManualInput Whether to allow manual keyboard input for the number
* @param onStateChanged Callback invoked when the selected number changes
*/
@OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
@Composable
fun NumberPicker(

View File

@@ -22,6 +22,18 @@ import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import kotlin.math.*
/**
* A Compose picker component that allows users to select an item from a list by dragging or using arrow buttons.
*
* @param T The type of items in the list
* @param current The currently selected item
* @param dataList The list of items to choose from
* @param modifier The modifier to be applied to the picker
* @param textStyle The text style for displaying items
* @param arrowsColor The color of the up/down arrow buttons
* @param dataToString A composable function to convert items to strings for display. Defaults to [Any.toString]
* @param onStateChanged Callback invoked when the selected item changes
*/
@OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
@Composable
fun <T> SetPicker(

View File

@@ -28,7 +28,7 @@ if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != n
centralPortal {
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')
verificationTimeout = Duration.ofHours(4)
validationTimeout = Duration.ofHours(4)
publishingType = System.getenv('PUBLISHING_TYPE') != "" ? System.getenv('PUBLISHING_TYPE') : "USER_MANAGED"
}
@@ -44,6 +44,11 @@ allprojects {
maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
mavenLocal()
}
it.tasks.withType(AbstractTestTask).configureEach {
it.failOnNoDiscoveredTests = false
}
}
apply from: "./extensions.gradle"

View File

@@ -1,298 +1,890 @@
/**
* Standard HTML/CSS color constants as [HEXAColor] extension properties.
* Provides convenient access to all standard web colors like red, blue, green, etc.
* All colors are defined with full opacity (alpha = 0xFF).
*/
package dev.inmo.micro_utils.colors
import dev.inmo.micro_utils.colors.common.HEXAColor
/**
* Alice Blue - A pale blue color (#F0F8FF).
*/
val HEXAColor.Companion.aliceblue
get() = HEXAColor(0xF0F8FFFFu)
/**
* Antique White - A pale beige color (#FAEBD7).
*/
val HEXAColor.Companion.antiquewhite
get() = HEXAColor(0xFAEBD7FFu)
/**
* Aqua - A bright cyan color (#00FFFF).
*/
val HEXAColor.Companion.aqua
get() = HEXAColor(0x00FFFFFFu)
/**
* Aquamarine - A medium blue-green color (#7FFFD4).
*/
val HEXAColor.Companion.aquamarine
get() = HEXAColor(0x7FFFD4FFu)
/**
* Azure - A pale cyan-blue color (#F0FFFF).
*/
val HEXAColor.Companion.azure
get() = HEXAColor(0xF0FFFFFFu)
/**
* Beige - A pale sandy tan color (#F5F5DC).
*/
val HEXAColor.Companion.beige
get() = HEXAColor(0xF5F5DCFFu)
/**
* Bisque - A pale orange color (#FFE4C4).
*/
val HEXAColor.Companion.bisque
get() = HEXAColor(0xFFE4C4FFu)
/**
* Black - Pure black color (#000000).
*/
val HEXAColor.Companion.black
get() = HEXAColor(0x000000FFu)
/**
* Blanched Almond - A pale peachy color (#FFEBCD).
*/
val HEXAColor.Companion.blanchedalmond
get() = HEXAColor(0xFFEBCDFFu)
/**
* Blue - Pure blue color (#0000FF).
*/
val HEXAColor.Companion.blue
get() = HEXAColor(0x0000FFFFu)
/**
* Blue Violet - A vivid purple-blue color (#8A2BE2).
*/
val HEXAColor.Companion.blueviolet
get() = HEXAColor(0x8A2BE2FFu)
/**
* Brown - A dark reddish-brown color (#A52A2A).
*/
val HEXAColor.Companion.brown
get() = HEXAColor(0xA52A2AFFu)
/**
* Burlywood - A sandy tan color (#DEB887).
*/
val HEXAColor.Companion.burlywood
get() = HEXAColor(0xDEB887FFu)
/**
* Cadet Blue - A grayish-blue color (#5F9EA0).
*/
val HEXAColor.Companion.cadetblue
get() = HEXAColor(0x5F9EA0FFu)
/**
* Chartreuse - A bright yellow-green color (#7FFF00).
*/
val HEXAColor.Companion.chartreuse
get() = HEXAColor(0x7FFF00FFu)
/**
* Chocolate - A medium brown color (#D2691E).
*/
val HEXAColor.Companion.chocolate
get() = HEXAColor(0xD2691EFFu)
/**
* Coral - A vibrant orange-pink color (#FF7F50).
*/
val HEXAColor.Companion.coral
get() = HEXAColor(0xFF7F50FFu)
/**
* Cornflower Blue - A medium blue color (#6495ED).
*/
val HEXAColor.Companion.cornflowerblue
get() = HEXAColor(0x6495EDFFu)
/**
* Cornsilk - A pale yellow color (#FFF8DC).
*/
val HEXAColor.Companion.cornsilk
get() = HEXAColor(0xFFF8DCFFu)
/**
* Crimson - A vivid red color (#DC143C).
*/
val HEXAColor.Companion.crimson
get() = HEXAColor(0xDC143CFFu)
/**
* Cyan - A bright cyan color (#00FFFF).
*/
val HEXAColor.Companion.cyan
get() = HEXAColor(0x00FFFFFFu)
/**
* Dark Blue - A dark blue color (#00008B).
*/
val HEXAColor.Companion.darkblue
get() = HEXAColor(0x00008BFFu)
/**
* Dark Cyan - A dark cyan color (#008B8B).
*/
val HEXAColor.Companion.darkcyan
get() = HEXAColor(0x008B8BFFu)
/**
* Dark Goldenrod - A dark golden yellow color (#B8860B).
*/
val HEXAColor.Companion.darkgoldenrod
get() = HEXAColor(0xB8860BFFu)
/**
* Dark Gray - A dark gray color (#A9A9A9).
*/
val HEXAColor.Companion.darkgray
get() = HEXAColor(0xA9A9A9FFu)
/**
* Dark Green - A dark green color (#006400).
*/
val HEXAColor.Companion.darkgreen
get() = HEXAColor(0x006400FFu)
/**
* Dark Grey - A dark gray color (#A9A9A9).
*/
val HEXAColor.Companion.darkgrey
get() = HEXAColor(0xA9A9A9FFu)
/**
* Dark Khaki - A brownish-tan color (#BDB76B).
*/
val HEXAColor.Companion.darkkhaki
get() = HEXAColor(0xBDB76BFFu)
/**
* Dark Magenta - A dark magenta/purple color (#8B008B).
*/
val HEXAColor.Companion.darkmagenta
get() = HEXAColor(0x8B008BFFu)
/**
* Dark Olive Green - A dark olive green color (#556B2F).
*/
val HEXAColor.Companion.darkolivegreen
get() = HEXAColor(0x556B2FFFu)
/**
* Dark Orange - A vivid dark orange color (#FF8C00).
*/
val HEXAColor.Companion.darkorange
get() = HEXAColor(0xFF8C00FFu)
/**
* Dark Orchid - A dark purple color (#9932CC).
*/
val HEXAColor.Companion.darkorchid
get() = HEXAColor(0x9932CCFFu)
/**
* Dark Red - A dark red color (#8B0000).
*/
val HEXAColor.Companion.darkred
get() = HEXAColor(0x8B0000FFu)
/**
* Dark Salmon - A muted salmon color (#E9967A).
*/
val HEXAColor.Companion.darksalmon
get() = HEXAColor(0xE9967AFFu)
/**
* Dark Sea Green - A muted sea green color (#8FBC8F).
*/
val HEXAColor.Companion.darkseagreen
get() = HEXAColor(0x8FBC8FFFu)
/**
* Dark Slate Blue - A dark grayish-blue color (#483D8B).
*/
val HEXAColor.Companion.darkslateblue
get() = HEXAColor(0x483D8BFFu)
/**
* Dark Slate Gray - A very dark grayish-cyan color (#2F4F4F).
*/
val HEXAColor.Companion.darkslategray
get() = HEXAColor(0x2F4F4FFFu)
/**
* Dark Slate Grey - A very dark grayish-cyan color (#2F4F4F).
*/
val HEXAColor.Companion.darkslategrey
get() = HEXAColor(0x2F4F4FFFu)
/**
* Dark Turquoise - A dark turquoise color (#00CED1).
*/
val HEXAColor.Companion.darkturquoise
get() = HEXAColor(0x00CED1FFu)
/**
* Dark Violet - A dark violet color (#9400D3).
*/
val HEXAColor.Companion.darkviolet
get() = HEXAColor(0x9400D3FFu)
/**
* Deep Pink - A vivid pink color (#FF1493).
*/
val HEXAColor.Companion.deeppink
get() = HEXAColor(0xFF1493FFu)
/**
* Deep Sky Blue - A bright sky blue color (#00BFFF).
*/
val HEXAColor.Companion.deepskyblue
get() = HEXAColor(0x00BFFFFFu)
/**
* Dim Gray - A dim gray color (#696969).
*/
val HEXAColor.Companion.dimgray
get() = HEXAColor(0x696969FFu)
/**
* Dim Grey - A dim gray color (#696969).
*/
val HEXAColor.Companion.dimgrey
get() = HEXAColor(0x696969FFu)
/**
* Dodger Blue - A bright blue color (#1E90FF).
*/
val HEXAColor.Companion.dodgerblue
get() = HEXAColor(0x1E90FFFFu)
/**
* Firebrick - A dark red brick color (#B22222).
*/
val HEXAColor.Companion.firebrick
get() = HEXAColor(0xB22222FFu)
/**
* Floral White - A very pale cream color (#FFFAF0).
*/
val HEXAColor.Companion.floralwhite
get() = HEXAColor(0xFFFAF0FFu)
/**
* Forest Green - A medium forest green color (#228B22).
*/
val HEXAColor.Companion.forestgreen
get() = HEXAColor(0x228B22FFu)
/**
* Fuchsia - A vivid magenta color (#FF00FF).
*/
val HEXAColor.Companion.fuchsia
get() = HEXAColor(0xFF00FFFFu)
/**
* Gainsboro - A light gray color (#DCDCDC).
*/
val HEXAColor.Companion.gainsboro
get() = HEXAColor(0xDCDCDCFFu)
/**
* Ghost White - A very pale blue-white color (#F8F8FF).
*/
val HEXAColor.Companion.ghostwhite
get() = HEXAColor(0xF8F8FFFFu)
/**
* Gold - A bright golden yellow color (#FFD700).
*/
val HEXAColor.Companion.gold
get() = HEXAColor(0xFFD700FFu)
/**
* Goldenrod - A golden yellow color (#DAA520).
*/
val HEXAColor.Companion.goldenrod
get() = HEXAColor(0xDAA520FFu)
/**
* Gray - A medium gray color (#808080).
*/
val HEXAColor.Companion.gray
get() = HEXAColor(0x808080FFu)
/**
* Green - A pure green color (#008000).
*/
val HEXAColor.Companion.green
get() = HEXAColor(0x008000FFu)
/**
* Green Yellow - A bright yellow-green color (#ADFF2F).
*/
val HEXAColor.Companion.greenyellow
get() = HEXAColor(0xADFF2FFFu)
/**
* Grey - A medium gray color (#808080).
*/
val HEXAColor.Companion.grey
get() = HEXAColor(0x808080FFu)
/**
* Honeydew - A very pale green color (#F0FFF0).
*/
val HEXAColor.Companion.honeydew
get() = HEXAColor(0xF0FFF0FFu)
/**
* Hot Pink - A vibrant pink color (#FF69B4).
*/
val HEXAColor.Companion.hotpink
get() = HEXAColor(0xFF69B4FFu)
/**
* Indian Red - A medium red color (#CD5C5C).
*/
val HEXAColor.Companion.indianred
get() = HEXAColor(0xCD5C5CFFu)
/**
* Indigo - A deep blue-violet color (#4B0082).
*/
val HEXAColor.Companion.indigo
get() = HEXAColor(0x4B0082FFu)
/**
* Ivory - A very pale cream color (#FFFFF0).
*/
val HEXAColor.Companion.ivory
get() = HEXAColor(0xFFFFF0FFu)
/**
* Khaki - A light tan color (#F0E68C).
*/
val HEXAColor.Companion.khaki
get() = HEXAColor(0xF0E68CFFu)
/**
* Lavender - A pale purple color (#E6E6FA).
*/
val HEXAColor.Companion.lavender
get() = HEXAColor(0xE6E6FAFFu)
/**
* Lavender Blush - A very pale pink color (#FFF0F5).
*/
val HEXAColor.Companion.lavenderblush
get() = HEXAColor(0xFFF0F5FFu)
/**
* Lawn Green - A bright chartreuse green color (#7CFC00).
*/
val HEXAColor.Companion.lawngreen
get() = HEXAColor(0x7CFC00FFu)
/**
* Lemon Chiffon - A very pale yellow color (#FFFACD).
*/
val HEXAColor.Companion.lemonchiffon
get() = HEXAColor(0xFFFACDFFu)
/**
* Light Blue - A light blue color (#ADD8E6).
*/
val HEXAColor.Companion.lightblue
get() = HEXAColor(0xADD8E6FFu)
/**
* Light Coral - A light coral pink color (#F08080).
*/
val HEXAColor.Companion.lightcoral
get() = HEXAColor(0xF08080FFu)
/**
* Light Cyan - A very pale cyan color (#E0FFFF).
*/
val HEXAColor.Companion.lightcyan
get() = HEXAColor(0xE0FFFFFFu)
/**
* Light Goldenrod Yellow - A pale yellow color (#FAFAD2).
*/
val HEXAColor.Companion.lightgoldenrodyellow
get() = HEXAColor(0xFAFAD2FFu)
/**
* Light Gray - A light gray color (#D3D3D3).
*/
val HEXAColor.Companion.lightgray
get() = HEXAColor(0xD3D3D3FFu)
/**
* Light Green - A light green color (#90EE90).
*/
val HEXAColor.Companion.lightgreen
get() = HEXAColor(0x90EE90FFu)
/**
* Light Grey - A light gray color (#D3D3D3).
*/
val HEXAColor.Companion.lightgrey
get() = HEXAColor(0xD3D3D3FFu)
/**
* Light Pink - A light pink color (#FFB6C1).
*/
val HEXAColor.Companion.lightpink
get() = HEXAColor(0xFFB6C1FFu)
/**
* Light Salmon - A light salmon color (#FFA07A).
*/
val HEXAColor.Companion.lightsalmon
get() = HEXAColor(0xFFA07AFFu)
/**
* Light Sea Green - A medium sea green color (#20B2AA).
*/
val HEXAColor.Companion.lightseagreen
get() = HEXAColor(0x20B2AAFFu)
/**
* Light Sky Blue - A light sky blue color (#87CEFA).
*/
val HEXAColor.Companion.lightskyblue
get() = HEXAColor(0x87CEFAFFu)
/**
* Light Slate Gray - A light slate gray color (#778899).
*/
val HEXAColor.Companion.lightslategray
get() = HEXAColor(0x778899FFu)
/**
* Light Slate Grey - A light slate gray color (#778899).
*/
val HEXAColor.Companion.lightslategrey
get() = HEXAColor(0x778899FFu)
/**
* Light Steel Blue - A light steel blue color (#B0C4DE).
*/
val HEXAColor.Companion.lightsteelblue
get() = HEXAColor(0xB0C4DEFFu)
/**
* Light Yellow - A very pale yellow color (#FFFFE0).
*/
val HEXAColor.Companion.lightyellow
get() = HEXAColor(0xFFFFE0FFu)
/**
* Lime - A bright lime green color (#00FF00).
*/
val HEXAColor.Companion.lime
get() = HEXAColor(0x00FF00FFu)
/**
* Lime Green - A lime green color (#32CD32).
*/
val HEXAColor.Companion.limegreen
get() = HEXAColor(0x32CD32FFu)
/**
* Linen - A pale beige color (#FAF0E6).
*/
val HEXAColor.Companion.linen
get() = HEXAColor(0xFAF0E6FFu)
/**
* Magenta - A bright magenta color (#FF00FF).
*/
val HEXAColor.Companion.magenta
get() = HEXAColor(0xFF00FFFFu)
/**
* Maroon - A dark reddish-brown color (#800000).
*/
val HEXAColor.Companion.maroon
get() = HEXAColor(0x800000FFu)
/**
* Medium Aquamarine - A medium aquamarine color (#66CDAA).
*/
val HEXAColor.Companion.mediumaquamarine
get() = HEXAColor(0x66CDAAFFu)
/**
* Medium Blue - A medium blue color (#0000CD).
*/
val HEXAColor.Companion.mediumblue
get() = HEXAColor(0x0000CDFFu)
/**
* Medium Orchid - A medium orchid purple color (#BA55D3).
*/
val HEXAColor.Companion.mediumorchid
get() = HEXAColor(0xBA55D3FFu)
/**
* Medium Purple - A medium purple color (#9370DB).
*/
val HEXAColor.Companion.mediumpurple
get() = HEXAColor(0x9370DBFFu)
/**
* Medium Sea Green - A medium sea green color (#3CB371).
*/
val HEXAColor.Companion.mediumseagreen
get() = HEXAColor(0x3CB371FFu)
/**
* Medium Slate Blue - A medium slate blue color (#7B68EE).
*/
val HEXAColor.Companion.mediumslateblue
get() = HEXAColor(0x7B68EEFFu)
/**
* Medium Spring Green - A medium spring green color (#00FA9A).
*/
val HEXAColor.Companion.mediumspringgreen
get() = HEXAColor(0x00FA9AFFu)
/**
* Medium Turquoise - A medium turquoise color (#48D1CC).
*/
val HEXAColor.Companion.mediumturquoise
get() = HEXAColor(0x48D1CCFFu)
/**
* Medium Violet Red - A medium violet-red color (#C71585).
*/
val HEXAColor.Companion.mediumvioletred
get() = HEXAColor(0xC71585FFu)
/**
* Midnight Blue - A very dark blue color (#191970).
*/
val HEXAColor.Companion.midnightblue
get() = HEXAColor(0x191970FFu)
/**
* Mint Cream - A very pale mint color (#F5FFFA).
*/
val HEXAColor.Companion.mintcream
get() = HEXAColor(0xF5FFFAFFu)
/**
* Misty Rose - A very pale pink color (#FFE4E1).
*/
val HEXAColor.Companion.mistyrose
get() = HEXAColor(0xFFE4E1FFu)
/**
* Moccasin - A pale peach color (#FFE4B5).
*/
val HEXAColor.Companion.moccasin
get() = HEXAColor(0xFFE4B5FFu)
/**
* Navajo White - A pale peach color (#FFDEAD).
*/
val HEXAColor.Companion.navajowhite
get() = HEXAColor(0xFFDEADFFu)
/**
* Navy - A very dark blue color (#000080).
*/
val HEXAColor.Companion.navy
get() = HEXAColor(0x000080FFu)
/**
* Old Lace - A very pale cream color (#FDF5E6).
*/
val HEXAColor.Companion.oldlace
get() = HEXAColor(0xFDF5E6FFu)
/**
* Olive - A dark yellowish-green color (#808000).
*/
val HEXAColor.Companion.olive
get() = HEXAColor(0x808000FFu)
/**
* Olive Drab - A dark olive green color (#6B8E23).
*/
val HEXAColor.Companion.olivedrab
get() = HEXAColor(0x6B8E23FFu)
/**
* Orange - A bright orange color (#FFA500).
*/
val HEXAColor.Companion.orange
get() = HEXAColor(0xFFA500FFu)
/**
* Orange Red - A bright red-orange color (#FF4500).
*/
val HEXAColor.Companion.orangered
get() = HEXAColor(0xFF4500FFu)
/**
* Orchid - A medium orchid purple color (#DA70D6).
*/
val HEXAColor.Companion.orchid
get() = HEXAColor(0xDA70D6FFu)
/**
* Pale Goldenrod - A pale goldenrod yellow color (#EEE8AA).
*/
val HEXAColor.Companion.palegoldenrod
get() = HEXAColor(0xEEE8AAFFu)
/**
* Pale Green - A pale green color (#98FB98).
*/
val HEXAColor.Companion.palegreen
get() = HEXAColor(0x98FB98FFu)
/**
* Pale Turquoise - A pale turquoise color (#AFEEEE).
*/
val HEXAColor.Companion.paleturquoise
get() = HEXAColor(0xAFEEEEFFu)
/**
* Pale Violet Red - A medium violet-red color (#DB7093).
*/
val HEXAColor.Companion.palevioletred
get() = HEXAColor(0xDB7093FFu)
/**
* Papaya Whip - A pale peach color (#FFEFD5).
*/
val HEXAColor.Companion.papayawhip
get() = HEXAColor(0xFFEFD5FFu)
/**
* Peach Puff - A light peach color (#FFDAB9).
*/
val HEXAColor.Companion.peachpuff
get() = HEXAColor(0xFFDAB9FFu)
/**
* Peru - A medium brown color (#CD853F).
*/
val HEXAColor.Companion.peru
get() = HEXAColor(0xCD853FFFu)
/**
* Pink - A light pink color (#FFC0CB).
*/
val HEXAColor.Companion.pink
get() = HEXAColor(0xFFC0CBFFu)
/**
* Plum - A medium purple color (#DDA0DD).
*/
val HEXAColor.Companion.plum
get() = HEXAColor(0xDDA0DDFFu)
/**
* Powder Blue - A light blue color (#B0E0E6).
*/
val HEXAColor.Companion.powderblue
get() = HEXAColor(0xB0E0E6FFu)
/**
* Purple - A pure purple color (#800080).
*/
val HEXAColor.Companion.purple
get() = HEXAColor(0x800080FFu)
/**
* Red - Pure red color (#FF0000).
*/
val HEXAColor.Companion.red
get() = HEXAColor(0xFF0000FFu)
/**
* Rosy Brown - A rosy brown color (#BC8F8F).
*/
val HEXAColor.Companion.rosybrown
get() = HEXAColor(0xBC8F8FFFu)
/**
* Royal Blue - A vibrant royal blue color (#4169E1).
*/
val HEXAColor.Companion.royalblue
get() = HEXAColor(0x4169E1FFu)
/**
* Saddle Brown - A dark brown color (#8B4513).
*/
val HEXAColor.Companion.saddlebrown
get() = HEXAColor(0x8B4513FFu)
/**
* Salmon - A light salmon pink color (#FA8072).
*/
val HEXAColor.Companion.salmon
get() = HEXAColor(0xFA8072FFu)
/**
* Sandy Brown - A sandy brown color (#F4A460).
*/
val HEXAColor.Companion.sandybrown
get() = HEXAColor(0xF4A460FFu)
/**
* Sea Green - A dark sea green color (#2E8B57).
*/
val HEXAColor.Companion.seagreen
get() = HEXAColor(0x2E8B57FFu)
/**
* Seashell - A very pale pink-orange color (#FFF5EE).
*/
val HEXAColor.Companion.seashell
get() = HEXAColor(0xFFF5EEFFu)
/**
* Sienna - A reddish-brown color (#A0522D).
*/
val HEXAColor.Companion.sienna
get() = HEXAColor(0xA0522DFFu)
/**
* Silver - A light gray-silver color (#C0C0C0).
*/
val HEXAColor.Companion.silver
get() = HEXAColor(0xC0C0C0FFu)
/**
* Sky Blue - A light sky blue color (#87CEEB).
*/
val HEXAColor.Companion.skyblue
get() = HEXAColor(0x87CEEBFFu)
/**
* Slate Blue - A medium slate blue color (#6A5ACD).
*/
val HEXAColor.Companion.slateblue
get() = HEXAColor(0x6A5ACDFFu)
/**
* Slate Gray - A slate gray color (#708090).
*/
val HEXAColor.Companion.slategray
get() = HEXAColor(0x708090FFu)
/**
* Slate Grey - A slate gray color (#708090).
*/
val HEXAColor.Companion.slategrey
get() = HEXAColor(0x708090FFu)
/**
* Snow - A very pale pinkish-white color (#FFFAFA).
*/
val HEXAColor.Companion.snow
get() = HEXAColor(0xFFFAFAFFu)
/**
* Spring Green - A bright spring green color (#00FF7F).
*/
val HEXAColor.Companion.springgreen
get() = HEXAColor(0x00FF7FFFu)
/**
* Steel Blue - A medium steel blue color (#4682B4).
*/
val HEXAColor.Companion.steelblue
get() = HEXAColor(0x4682B4FFu)
/**
* Tan - A light brown tan color (#D2B48C).
*/
val HEXAColor.Companion.tan
get() = HEXAColor(0xD2B48CFFu)
/**
* Teal - A dark cyan-blue color (#008080).
*/
val HEXAColor.Companion.teal
get() = HEXAColor(0x008080FFu)
/**
* Thistle - A light purple-pink color (#D8BFD8).
*/
val HEXAColor.Companion.thistle
get() = HEXAColor(0xD8BFD8FFu)
/**
* Tomato - A vibrant red-orange color (#FF6347).
*/
val HEXAColor.Companion.tomato
get() = HEXAColor(0xFF6347FFu)
/**
* Turquoise - A medium turquoise color (#40E0D0).
*/
val HEXAColor.Companion.turquoise
get() = HEXAColor(0x40E0D0FFu)
/**
* Violet - A violet color (#EE82EE).
*/
val HEXAColor.Companion.violet
get() = HEXAColor(0xEE82EEFFu)
/**
* Wheat - A light tan color (#F5DEB3).
*/
val HEXAColor.Companion.wheat
get() = HEXAColor(0xF5DEB3FFu)
/**
* White - Pure white color (#FFFFFF).
*/
val HEXAColor.Companion.white
get() = HEXAColor(0xFFFFFFFFu)
/**
* White Smoke - A very light gray color (#F5F5F5).
*/
val HEXAColor.Companion.whitesmoke
get() = HEXAColor(0xF5F5F5FFu)
/**
* Yellow - Pure yellow color (#FFFF00).
*/
val HEXAColor.Companion.yellow
get() = HEXAColor(0xFFFF00FFu)
/**
* Yellow Green - A medium yellow-green color (#9ACD32).
*/
val HEXAColor.Companion.yellowgreen
get() = HEXAColor(0x9ACD32FFu)

View File

@@ -2,11 +2,9 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.runComposeUiTest
import dev.inmo.micro_utils.common.compose.LoadableComponent
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
import kotlinx.coroutines.flow.MutableSharedFlow
import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import org.jetbrains.annotations.TestOnly
import kotlin.test.Test
import kotlin.test.assertTrue
@@ -16,8 +14,8 @@ class LoadableComponentTests {
@Test
@TestOnly
fun testSimpleLoad() = runComposeUiTest {
val loadingFlow = SpecialMutableStateFlow<Int>(0)
val loadedFlow = SpecialMutableStateFlow<Int>(0)
val loadingFlow = MutableRedeliverStateFlow<Int>(0)
val loadedFlow = MutableRedeliverStateFlow<Int>(0)
setContent {
LoadableComponent<Int>({
loadingFlow.filter { it == 1 }.first()

View File

@@ -1,5 +1,15 @@
package dev.inmo.micro_utils.common
/**
* Breaks this list into a list of consecutive pairs.
* Each element is paired with the next element in the list.
* For a list of size n, the result will contain n-1 pairs.
*
* Example: `[1, 2, 3, 4].breakAsPairs()` returns `[(1, 2), (2, 3), (3, 4)]`
*
* @param T The type of elements in the list
* @return A list of pairs where each pair consists of consecutive elements
*/
fun <T> List<T>.breakAsPairs(): List<Pair<T, T>> {
val result = mutableListOf<Pair<T, T>>()

View File

@@ -1,5 +1,12 @@
package dev.inmo.micro_utils.common
/**
* Executes the given [block] and returns its result if this Boolean is true, otherwise returns null.
*
* @param T The return type of the block
* @param block The function to execute if this Boolean is true
* @return The result of [block] if true, null otherwise
*/
inline fun <T> Boolean.letIfTrue(block: () -> T): T? {
return if (this) {
block()
@@ -8,6 +15,13 @@ inline fun <T> Boolean.letIfTrue(block: () -> T): T? {
}
}
/**
* Executes the given [block] and returns its result if this Boolean is false, otherwise returns null.
*
* @param T The return type of the block
* @param block The function to execute if this Boolean is false
* @return The result of [block] if false, null otherwise
*/
inline fun <T> Boolean.letIfFalse(block: () -> T): T? {
return if (this) {
null
@@ -16,16 +30,37 @@ inline fun <T> Boolean.letIfFalse(block: () -> T): T? {
}
}
/**
* Executes the given [block] if this Boolean is true and returns this Boolean.
* Similar to [also], but only executes the block when the Boolean is true.
*
* @param block The function to execute if this Boolean is true
* @return This Boolean value
*/
inline fun Boolean.alsoIfTrue(block: () -> Unit): Boolean {
letIfTrue(block)
return this
}
/**
* Executes the given [block] if this Boolean is false and returns this Boolean.
* Similar to [also], but only executes the block when the Boolean is false.
*
* @param block The function to execute if this Boolean is false
* @return This Boolean value
*/
inline fun Boolean.alsoIfFalse(block: () -> Unit): Boolean {
letIfFalse(block)
return this
}
/**
* Alias for [letIfTrue]. Executes the given [block] and returns its result if this Boolean is true.
*
* @param T The return type of the block
* @param block The function to execute if this Boolean is true
* @return The result of [block] if true, null otherwise
*/
inline fun <T> Boolean.ifTrue(block: () -> T): T? {
return if (this) {
block()
@@ -34,6 +69,13 @@ inline fun <T> Boolean.ifTrue(block: () -> T): T? {
}
}
/**
* Alias for [letIfFalse]. Executes the given [block] and returns its result if this Boolean is false.
*
* @param T The return type of the block
* @param block The function to execute if this Boolean is false
* @return The result of [block] if false, null otherwise
*/
inline fun <T> Boolean.ifFalse(block: () -> T): T? {
return if (this) {
null

View File

@@ -6,19 +6,45 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
/**
* A function type that allocates and returns a [ByteArray].
*/
typealias ByteArrayAllocator = () -> ByteArray
/**
* A suspending function type that allocates and returns a [ByteArray].
*/
typealias SuspendByteArrayAllocator = suspend () -> ByteArray
/**
* Converts this [ByteArray] to a [ByteArrayAllocator] that returns this array.
*/
val ByteArray.asAllocator: ByteArrayAllocator
get() = { this }
/**
* Converts this [ByteArray] to a [SuspendByteArrayAllocator] that returns this array.
*/
val ByteArray.asSuspendAllocator: SuspendByteArrayAllocator
get() = { this }
/**
* Converts this [ByteArrayAllocator] to a [SuspendByteArrayAllocator].
*/
val ByteArrayAllocator.asSuspendAllocator: SuspendByteArrayAllocator
get() = { this() }
/**
* Converts this [SuspendByteArrayAllocator] to a [ByteArrayAllocator] by invoking it and
* wrapping the result in a non-suspending allocator.
*/
suspend fun SuspendByteArrayAllocator.asAllocator(): ByteArrayAllocator {
return invoke().asAllocator
}
/**
* Serializer for [ByteArrayAllocator]. Serializes the result of invoking the allocator.
*/
object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> {
private val realSerializer = ByteArraySerializer()
override val descriptor: SerialDescriptor = realSerializer.descriptor

View File

@@ -1,3 +1,10 @@
package dev.inmo.micro_utils.common
/**
* Returns the first non-null element in this iterable.
*
* @param T The type of elements in the iterable (nullable)
* @return The first non-null element
* @throws NoSuchElementException if the iterable contains no non-null elements
*/
fun <T> Iterable<T?>.firstNotNull() = first { it != null }!!

View File

@@ -1,5 +1,19 @@
package dev.inmo.micro_utils.common
/**
* Joins elements of this iterable into a list with separators between elements.
* Each element is transformed using [transform], and separators are generated using [separatorFun].
* Optional [prefix] and [postfix] can be added to the result.
* Null values from transformations or separator function are skipped.
*
* @param I The type of elements in the input iterable
* @param R The type of elements in the result list
* @param separatorFun A function that generates a separator based on the current element
* @param prefix Optional prefix to add at the beginning of the result
* @param postfix Optional postfix to add at the end of the result
* @param transform A function to transform each element
* @return A list of transformed elements with separators
*/
inline fun <I, R> Iterable<I>.joinTo(
separatorFun: (I) -> R?,
prefix: R? = null,
@@ -25,6 +39,20 @@ inline fun <I, R> Iterable<I>.joinTo(
return result
}
/**
* Joins elements of this iterable into a list with a constant separator between elements.
* Each element is transformed using [transform].
* Optional [prefix] and [postfix] can be added to the result.
* Null values from transformations or separators are skipped.
*
* @param I The type of elements in the input iterable
* @param R The type of elements in the result list
* @param separator The separator to insert between elements
* @param prefix Optional prefix to add at the beginning of the result
* @param postfix Optional postfix to add at the end of the result
* @param transform A function to transform each element
* @return A list of transformed elements with separators
*/
inline fun <I, R> Iterable<I>.joinTo(
separator: R? = null,
prefix: R? = null,
@@ -32,18 +60,55 @@ inline fun <I, R> Iterable<I>.joinTo(
transform: (I) -> R?
): List<R> = joinTo({ separator }, prefix, postfix, transform)
/**
* Joins elements of this iterable into a list with separators between elements.
* Separators are generated using [separatorFun].
* Optional [prefix] and [postfix] can be added to the result.
* Null values from separator function are skipped.
*
* @param I The type of elements
* @param separatorFun A function that generates a separator based on the current element
* @param prefix Optional prefix to add at the beginning of the result
* @param postfix Optional postfix to add at the end of the result
* @return A list of elements with separators
*/
inline fun <I> Iterable<I>.joinTo(
separatorFun: (I) -> I?,
prefix: I? = null,
postfix: I? = null
): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it }
/**
* Joins elements of this iterable into a list with a constant separator between elements.
* Optional [prefix] and [postfix] can be added to the result.
* Null separators are skipped.
*
* @param I The type of elements
* @param separator The separator to insert between elements
* @param prefix Optional prefix to add at the beginning of the result
* @param postfix Optional postfix to add at the end of the result
* @return A list of elements with separators
*/
inline fun <I> Iterable<I>.joinTo(
separator: I? = null,
prefix: I? = null,
postfix: I? = null
): List<I> = joinTo<I>({ separator }, prefix, postfix)
/**
* Joins elements of this array into an array with separators between elements.
* Each element is transformed using [transform], and separators are generated using [separatorFun].
* Optional [prefix] and [postfix] can be added to the result.
* Null values from transformations or separator function are skipped.
*
* @param I The type of elements in the input array
* @param R The type of elements in the result array
* @param separatorFun A function that generates a separator based on the current element
* @param prefix Optional prefix to add at the beginning of the result
* @param postfix Optional postfix to add at the end of the result
* @param transform A function to transform each element
* @return An array of transformed elements with separators
*/
inline fun <I, reified R> Array<I>.joinTo(
separatorFun: (I) -> R?,
prefix: R? = null,
@@ -51,6 +116,20 @@ inline fun <I, reified R> Array<I>.joinTo(
transform: (I) -> R?
): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray()
/**
* Joins elements of this array into an array with a constant separator between elements.
* Each element is transformed using [transform].
* Optional [prefix] and [postfix] can be added to the result.
* Null values from transformations or separators are skipped.
*
* @param I The type of elements in the input array
* @param R The type of elements in the result array
* @param separator The separator to insert between elements
* @param prefix Optional prefix to add at the beginning of the result
* @param postfix Optional postfix to add at the end of the result
* @param transform A function to transform each element
* @return An array of transformed elements with separators
*/
inline fun <I, reified R> Array<I>.joinTo(
separator: R? = null,
prefix: R? = null,

View File

@@ -7,7 +7,7 @@ import kotlin.jvm.JvmInline
@JvmInline
value class FileName(val string: String) {
val name: String
get() = withoutSlashAtTheEnd.takeLastWhile { it != '/' }
get() = withoutSlashAtTheEnd.takeLastWhile { it != MPPFilePathSeparator }
val extension: String
get() = name.takeLastWhile { it != '.' }
val nameWithoutExtension: String
@@ -18,7 +18,7 @@ value class FileName(val string: String) {
} ?: filename
}
val withoutSlashAtTheEnd: String
get() = string.dropLastWhile { it == '/' }
get() = string.dropLastWhile { it == MPPFilePathSeparator }
override fun toString(): String = string
}
@@ -26,6 +26,7 @@ value class FileName(val string: String) {
expect class MPPFile
expect val MPPFile.filename: FileName
expect val MPPFilePathSeparator: Char
expect val MPPFile.filesize: Long
expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator
expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator

View File

@@ -2,16 +2,43 @@ package dev.inmo.micro_utils.common
import kotlin.jvm.JvmName
/**
* A bidirectional mapper that can convert between two types [T1] and [T2].
*
* @param T1 The first type
* @param T2 The second type
*/
interface SimpleMapper<T1, T2> {
fun convertToT1(from: T2): T1
fun convertToT2(from: T1): T2
}
/**
* Converts [from] of type [T2] to type [T1] using this mapper.
*
* @param from The value to convert
* @return The converted value of type [T1]
*/
@JvmName("convertFromT2")
fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T2) = convertToT1(from)
/**
* Converts [from] of type [T1] to type [T2] using this mapper.
*
* @param from The value to convert
* @return The converted value of type [T2]
*/
@JvmName("convertFromT1")
fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T1) = convertToT2(from)
/**
* Implementation of [SimpleMapper] that uses lambda functions for conversion.
*
* @param T1 The first type
* @param T2 The second type
* @param t1 Function to convert from [T2] to [T1]
* @param t2 Function to convert from [T1] to [T2]
*/
class SimpleMapperImpl<T1, T2>(
private val t1: (T2) -> T1,
private val t2: (T1) -> T2,
@@ -21,22 +48,58 @@ class SimpleMapperImpl<T1, T2>(
override fun convertToT2(from: T1): T2 = t2.invoke(from)
}
/**
* Creates a [SimpleMapper] using the provided conversion functions.
*
* @param T1 The first type
* @param T2 The second type
* @param t1 Function to convert from [T2] to [T1]
* @param t2 Function to convert from [T1] to [T2]
* @return A new [SimpleMapperImpl] instance
*/
@Suppress("NOTHING_TO_INLINE")
inline fun <T1, T2> simpleMapper(
noinline t1: (T2) -> T1,
noinline t2: (T1) -> T2,
) = SimpleMapperImpl(t1, t2)
/**
* A bidirectional mapper that can convert between two types [T1] and [T2] using suspending functions.
*
* @param T1 The first type
* @param T2 The second type
*/
interface SimpleSuspendableMapper<T1, T2> {
suspend fun convertToT1(from: T2): T1
suspend fun convertToT2(from: T1): T2
}
/**
* Converts [from] of type [T2] to type [T1] using this suspending mapper.
*
* @param from The value to convert
* @return The converted value of type [T1]
*/
@JvmName("convertFromT2")
suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T2) = convertToT1(from)
/**
* Converts [from] of type [T1] to type [T2] using this suspending mapper.
*
* @param from The value to convert
* @return The converted value of type [T2]
*/
@JvmName("convertFromT1")
suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T1) = convertToT2(from)
/**
* Implementation of [SimpleSuspendableMapper] that uses suspending lambda functions for conversion.
*
* @param T1 The first type
* @param T2 The second type
* @param t1 Suspending function to convert from [T2] to [T1]
* @param t2 Suspending function to convert from [T1] to [T2]
*/
class SimpleSuspendableMapperImpl<T1, T2>(
private val t1: suspend (T2) -> T1,
private val t2: suspend (T1) -> T2,
@@ -46,6 +109,15 @@ class SimpleSuspendableMapperImpl<T1, T2>(
override suspend fun convertToT2(from: T1): T2 = t2.invoke(from)
}
/**
* Creates a [SimpleSuspendableMapper] using the provided suspending conversion functions.
*
* @param T1 The first type
* @param T2 The second type
* @param t1 Suspending function to convert from [T2] to [T1]
* @param t2 Suspending function to convert from [T1] to [T2]
* @return A new [SimpleSuspendableMapperImpl] instance
*/
@Suppress("NOTHING_TO_INLINE")
inline fun <T1, T2> simpleSuspendableMapper(
noinline t1: suspend (T2) -> T1,

View File

@@ -1,5 +1,14 @@
package dev.inmo.micro_utils.common
/**
* Pads this sequence to the specified [size] using a custom [inserter] function.
* The [inserter] is repeatedly called until the sequence reaches the desired size.
*
* @param T The type of elements in the sequence
* @param size The target size of the padded sequence
* @param inserter A function that takes the current sequence and returns a new padded sequence
* @return A sequence padded to at least the specified size
*/
inline fun <T> Sequence<T>.padWith(size: Int, inserter: (Sequence<T>) -> Sequence<T>): Sequence<T> {
var result = this
while (result.count() < size) {
@@ -8,10 +17,36 @@ inline fun <T> Sequence<T>.padWith(size: Int, inserter: (Sequence<T>) -> Sequenc
return result
}
/**
* Pads this sequence at the end to the specified [size].
* New elements are generated using [padBlock], which receives the current size as a parameter.
*
* @param T The type of elements in the sequence
* @param size The target size of the padded sequence
* @param padBlock A function that generates padding elements based on the current sequence size
* @return A sequence padded to at least the specified size
*/
inline fun <T> Sequence<T>.padEnd(size: Int, padBlock: (Int) -> T): Sequence<T> = padWith(size) { it + padBlock(it.count()) }
/**
* Pads this sequence at the end to the specified [size] using the given element [o].
*
* @param T The type of elements in the sequence
* @param size The target size of the padded sequence
* @param o The element to use for padding
* @return A sequence padded to at least the specified size
*/
inline fun <T> Sequence<T>.padEnd(size: Int, o: T) = padEnd(size) { o }
/**
* Pads this list to the specified [size] using a custom [inserter] function.
* The [inserter] is repeatedly called until the list reaches the desired size.
*
* @param T The type of elements in the list
* @param size The target size of the padded list
* @param inserter A function that takes the current list and returns a new padded list
* @return A list padded to at least the specified size
*/
inline fun <T> List<T>.padWith(size: Int, inserter: (List<T>) -> List<T>): List<T> {
var result = this
while (result.size < size) {
@@ -19,14 +54,66 @@ inline fun <T> List<T>.padWith(size: Int, inserter: (List<T>) -> List<T>): List<
}
return result
}
/**
* Pads this list at the end to the specified [size].
* New elements are generated using [padBlock], which receives the current size as a parameter.
*
* @param T The type of elements in the list
* @param size The target size of the padded list
* @param padBlock A function that generates padding elements based on the current list size
* @return A list padded to at least the specified size
*/
inline fun <T> List<T>.padEnd(size: Int, padBlock: (Int) -> T): List<T> = asSequence().padEnd(size, padBlock).toList()
/**
* Pads this list at the end to the specified [size] using the given element [o].
*
* @param T The type of elements in the list
* @param size The target size of the padded list
* @param o The element to use for padding
* @return A list padded to at least the specified size
*/
inline fun <T> List<T>.padEnd(size: Int, o: T): List<T> = asSequence().padEnd(size, o).toList()
/**
* Pads this sequence at the start to the specified [size].
* New elements are generated using [padBlock], which receives the current size as a parameter.
*
* @param T The type of elements in the sequence
* @param size The target size of the padded sequence
* @param padBlock A function that generates padding elements based on the current sequence size
* @return A sequence padded to at least the specified size
*/
inline fun <T> Sequence<T>.padStart(size: Int, padBlock: (Int) -> T): Sequence<T> = padWith(size) { sequenceOf(padBlock(it.count())) + it }
/**
* Pads this sequence at the start to the specified [size] using the given element [o].
*
* @param T The type of elements in the sequence
* @param size The target size of the padded sequence
* @param o The element to use for padding
* @return A sequence padded to at least the specified size
*/
inline fun <T> Sequence<T>.padStart(size: Int, o: T) = padStart(size) { o }
/**
* Pads this list at the start to the specified [size].
* New elements are generated using [padBlock], which receives the current size as a parameter.
*
* @param T The type of elements in the list
* @param size The target size of the padded list
* @param padBlock A function that generates padding elements based on the current list size
* @return A list padded to at least the specified size
*/
inline fun <T> List<T>.padStart(size: Int, padBlock: (Int) -> T): List<T> = asSequence().padStart(size, padBlock).toList()
/**
* Pads this list at the start to the specified [size] using the given element [o].
*
* @param T The type of elements in the list
* @param size The target size of the padded list
* @param o The element to use for padding
* @return A list padded to at least the specified size
*/
inline fun <T> List<T>.padStart(size: Int, o: T): List<T> = asSequence().padStart(size, o).toList()

View File

@@ -1,17 +1,39 @@
package dev.inmo.micro_utils.common
/**
* Computes the intersection of this range with [other]. Returns a pair representing
* the intersecting range, or null if the ranges don't overlap.
*
* @param T The type of comparable values in the range
* @param other The other range to intersect with
* @return A pair (start, end) representing the intersection, or null if no intersection exists
*/
fun <T : Comparable<T>> ClosedRange<T>.intersect(other: ClosedRange<T>): Pair<T, T>? = when {
start == other.start && endInclusive == other.endInclusive -> start to endInclusive
start > other.endInclusive || other.start > endInclusive -> null
else -> maxOf(start, other.start) to minOf(endInclusive, other.endInclusive)
}
/**
* Computes the intersection of this [IntRange] with [other].
* Returns the intersecting range, or null if the ranges don't overlap.
*
* @param other The other range to intersect with
* @return An [IntRange] representing the intersection, or null if no intersection exists
*/
fun IntRange.intersect(
other: IntRange
): IntRange? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let {
it.first .. it.second
}
/**
* Computes the intersection of this [LongRange] with [other].
* Returns the intersecting range, or null if the ranges don't overlap.
*
* @param other The other range to intersect with
* @return A [LongRange] representing the intersection, or null if no intersection exists
*/
fun LongRange.intersect(
other: LongRange
): LongRange? = (this as ClosedRange<Long>).intersect(other as ClosedRange<Long>) ?.let {

View File

@@ -1,5 +1,24 @@
package dev.inmo.micro_utils.common
/**
* Returns a new list with the element at index [i] replaced by applying [block] to it.
* All other elements remain unchanged.
*
* @param T The type of elements in the iterable
* @param i The index of the element to replace
* @param block A function that transforms the element at the given index
* @return A new list with the replaced element
*/
fun <T> Iterable<T>.withReplacedAt(i: Int, block: (T) -> T): List<T> = take(i) + block(elementAt(i)) + drop(i + 1)
/**
* Returns a new list with the first occurrence of element [t] replaced by applying [block] to it.
* All other elements remain unchanged.
*
* @param T The type of elements in the iterable
* @param t The element to replace
* @param block A function that transforms the found element
* @return A new list with the replaced element
*/
fun <T> Iterable<T>.withReplaced(t: T, block: (T) -> T): List<T> = withReplacedAt(indexOf(t), block)

View File

@@ -35,6 +35,10 @@ private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().awa
*/
actual val MPPFile.filename: FileName
get() = FileName(name)
actual val MPPFilePathSeparator: Char
get() = '/'
/**
* @suppress
*/

View File

@@ -14,6 +14,10 @@ actual typealias MPPFile = File
*/
actual val MPPFile.filename: FileName
get() = FileName(name)
actual val MPPFilePathSeparator: Char
get() = File.separatorChar
/**
* @suppress
*/

View File

@@ -11,6 +11,10 @@ actual typealias MPPFile = Path
*/
actual val MPPFile.filename: FileName
get() = FileName(toString())
actual val MPPFilePathSeparator: Char = Path.DIRECTORY_SEPARATOR.first()
/**
* @suppress
*/

View File

@@ -37,6 +37,10 @@ private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().awa
*/
actual val MPPFile.filename: FileName
get() = FileName(name)
actual val MPPFilePathSeparator: Char
get() = '/'
/**
* @suppress
*/

View File

@@ -3,7 +3,7 @@ package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce
@@ -16,7 +16,7 @@ import org.jetbrains.compose.web.css.StyleSheet
* to add `Style(stylesheet)` on every compose function call
*/
object StyleSheetsAggregator {
private val _stylesFlow = SpecialMutableStateFlow<Set<CSSRulesHolder>>(emptySet())
private val _stylesFlow = MutableRedeliverStateFlow<Set<CSSRulesHolder>>(emptySet())
val stylesFlow: StateFlow<Set<CSSRulesHolder>> = _stylesFlow.asStateFlow()
@Composable

View File

@@ -2,7 +2,7 @@ import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.test.*
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
import org.jetbrains.annotations.TestOnly
import kotlin.test.Test
@@ -11,7 +11,7 @@ class FlowStateTests {
@Test
@TestOnly
fun simpleTest() = runComposeUiTest {
val flowState = SpecialMutableStateFlow(0)
val flowState = MutableRedeliverStateFlow(0)
setContent {
Button({ flowState.value++ }) { Text("Click") }
Text(flowState.collectAsState().value.toString())

View File

@@ -1,19 +1,34 @@
package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.consumeAsFlow
/**
* Creates an actor-style channel that processes messages asynchronously based on markers.
* Messages with the same marker will be processed sequentially, while messages with different markers can be processed concurrently.
*
* @param T The type of messages to process
* @param channelCapacity The capacity of the underlying channel. Defaults to [Channel.UNLIMITED]
* @param markerFactory A factory function that produces a marker for each message. Messages with the same marker
* will be processed sequentially. Defaults to returning null, meaning all messages will be processed sequentially
* @param logger The logger instance used for logging exceptions. Defaults to [KSLog]
* @param block The suspending function that processes each message
* @return A [Channel] that accepts messages to be processed
*/
fun <T> CoroutineScope.actorAsync(
channelCapacity: Int = Channel.UNLIMITED,
markerFactory: suspend (T) -> Any? = { null },
logger: KSLog = KSLog,
block: suspend (T) -> Unit
): Channel<T> {
val channel = Channel<T>(channelCapacity)
channel.consumeAsFlow().subscribeAsync(this, markerFactory, block)
channel.consumeAsFlow().subscribeAsync(this, markerFactory, logger, block)
return channel
}
@Deprecated("Use standard actosAsync instead", ReplaceWith("actorAsync(channelCapacity, markerFactory, block = block)", "dev.inmo.micro_utils.coroutines.actorAsync"))
inline fun <T> CoroutineScope.safeActorAsync(
channelCapacity: Int = Channel.UNLIMITED,
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,

View File

@@ -3,5 +3,12 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
/**
* Wraps this value in a completed [Deferred]. The resulting [Deferred] is immediately completed with this value.
* Useful for converting synchronous values to [Deferred] in contexts that expect deferred values.
*
* @param T The type of the value
* @return A [Deferred] that is already completed with this value
*/
val <T> T.asDeferred: Deferred<T>
get() = CompletableDeferred(this)

View File

@@ -3,20 +3,53 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
/**
* Convenience property to access [Dispatchers.Main] for UI operations.
*/
inline val UI
get() = Dispatchers.Main
/**
* Convenience property to access [Dispatchers.Default] for CPU-intensive operations.
*/
inline val Default
get() = Dispatchers.Default
/**
* Executes the given [block] in the specified coroutine [context] and returns its result.
* This is a convenience wrapper around [withContext].
*
* @param T The return type of the block
* @param context The [CoroutineContext] in which to execute the block
* @param block The suspending function to execute
* @return The result of executing the block
*/
suspend inline fun <T> doIn(context: CoroutineContext, noinline block: suspend CoroutineScope.() -> T) = withContext(
context,
block
)
/**
* Executes the given [block] on the UI/Main dispatcher and returns its result.
* This is a convenience function for executing UI operations.
*
* @param T The return type of the block
* @param block The suspending function to execute on the UI thread
* @return The result of executing the block
*/
suspend inline fun <T> doInUI(noinline block: suspend CoroutineScope.() -> T) = doIn(
UI,
block
)
/**
* Executes the given [block] on the Default dispatcher and returns its result.
* This is a convenience function for executing CPU-intensive operations.
*
* @param T The return type of the block
* @param block The suspending function to execute on the Default dispatcher
* @return The result of executing the block
*/
suspend inline fun <T> doInDefault(noinline block: suspend CoroutineScope.() -> T) = doIn(
Default,
block

View File

@@ -2,6 +2,14 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.*
/**
* Represents a deferred action that combines a [Deferred] value with a callback to be executed on that value.
*
* @param T The type of the deferred value
* @param O The type of the result after applying the callback
* @param deferred The deferred value to await
* @param callback The suspending function to apply to the deferred value
*/
class DeferredAction<T, O>(
val deferred: Deferred<T>,
val callback: suspend (T) -> O
@@ -9,6 +17,13 @@ class DeferredAction<T, O>(
suspend operator fun invoke() = callback(deferred.await())
}
/**
* A builder for creating multiple deferred computations that can be executed, with only the first completing
* one being used. This is useful for race conditions where you want the result of whichever computation finishes first.
*
* @param T The type of values produced by the deferred computations
* @param scope The [CoroutineScope] in which to create the deferred computations
*/
class DoWithFirstBuilder<T>(
private val scope: CoroutineScope
) {
@@ -22,8 +37,25 @@ class DoWithFirstBuilder<T>(
fun build() = deferreds.toList()
}
/**
* Creates a [DeferredAction] from this [Deferred] and a [callback] function.
*
* @param T The type of the deferred value
* @param O The type of the result after applying the callback
* @param callback The suspending function to apply to the deferred value
* @return A [DeferredAction] combining the deferred and callback
*/
fun <T, O> Deferred<T>.buildAction(callback: suspend (T) -> O) = DeferredAction(this, callback)
/**
* Invokes the first [DeferredAction] whose deferred value completes, executing its callback and returning the result.
* Other deferred actions are cancelled if [cancelOnResult] is true.
*
* @param O The type of the result after applying callbacks
* @param scope The [CoroutineScope] in which to await the deferred values
* @param cancelOnResult If true, cancels all other deferred actions after the first completes. Defaults to true
* @return The result of invoking the first completed deferred action
*/
suspend fun <O> Iterable<DeferredAction<*, O>>.invokeFirstOf(
scope: CoroutineScope,
cancelOnResult: Boolean = true
@@ -33,18 +65,50 @@ suspend fun <O> Iterable<DeferredAction<*, O>>.invokeFirstOf(
}
}
/**
* Invokes the first [DeferredAction] from the given [variants] whose deferred value completes,
* executing its callback and returning the result. Other deferred actions are cancelled if [cancelOnResult] is true.
*
* @param O The type of the result after applying callbacks
* @param scope The [CoroutineScope] in which to await the deferred values
* @param variants The deferred actions to race
* @param cancelOnResult If true, cancels all other deferred actions after the first completes. Defaults to true
* @return The result of invoking the first completed deferred action
*/
suspend fun <O> invokeFirstOf(
scope: CoroutineScope,
vararg variants: DeferredAction<*, O>,
cancelOnResult: Boolean = true
): O = variants.toList().invokeFirstOf(scope, cancelOnResult)
/**
* Awaits the first [Deferred] to complete and invokes the [callback] on its value.
* Other deferred values are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param O The type of the result after applying the callback
* @param scope The [CoroutineScope] in which to await the deferred values
* @param cancelOnResult If true, cancels all other deferred values after the first completes. Defaults to true
* @param callback The suspending function to apply to the first completed value
* @return The result of applying the callback to the first completed value
*/
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)
/**
* Builds multiple deferred computations using [DoWithFirstBuilder] and invokes [callback] on the first one to complete.
* Other deferred computations are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param O The type of the result after applying the callback
* @param cancelOnResult If true, cancels all other computations after the first completes. Defaults to true
* @param block Builder DSL to define the deferred computations
* @param callback The suspending function to apply to the first completed value
* @return The result of applying the callback to the first completed value
*/
suspend fun <T, O> CoroutineScope.invokeOnFirstOf(
cancelOnResult: Boolean = true,
block: DoWithFirstBuilder<T>.() -> Unit,
@@ -54,6 +118,18 @@ suspend fun <T, O> CoroutineScope.invokeOnFirstOf(
cancelOnResult
).let { callback(it) }
/**
* Awaits the first [Deferred] from the given [variants] to complete and invokes the [callback] on its value.
* Other deferred values are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param O The type of the result after applying the callback
* @param scope The [CoroutineScope] in which to await the deferred values
* @param variants The deferred values to race
* @param cancelOnResult If true, cancels all other deferred values after the first completes. Defaults to true
* @param callback The suspending function to apply to the first completed value
* @return The result of applying the callback to the first completed value
*/
suspend fun <T, O> invokeOnFirst(
scope: CoroutineScope,
vararg variants: Deferred<T>,
@@ -61,11 +137,29 @@ suspend fun <T, O> invokeOnFirst(
callback: suspend (T) -> O
): O = variants.toList().invokeOnFirst(scope, cancelOnResult, callback)
/**
* Returns the value of the first [Deferred] from the given [variants] to complete.
* Other deferred values are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param variants The deferred values to race
* @param cancelOnResult If true, cancels all other deferred values after the first completes. Defaults to true
* @return The value of the first completed deferred
*/
suspend fun <T> CoroutineScope.firstOf(
variants: Iterable<Deferred<T>>,
cancelOnResult: Boolean = true
) = variants.invokeOnFirst(this, cancelOnResult) { it }
/**
* Builds multiple deferred computations using [DoWithFirstBuilder] and returns the value of the first one to complete.
* Other deferred computations are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param cancelOnResult If true, cancels all other computations after the first completes. Defaults to true
* @param block Builder DSL to define the deferred computations
* @return The value of the first completed computation
*/
suspend fun <T> CoroutineScope.firstOf(
cancelOnResult: Boolean = true,
block: DoWithFirstBuilder<T>.() -> Unit
@@ -74,11 +168,29 @@ suspend fun <T> CoroutineScope.firstOf(
cancelOnResult
)
/**
* Returns the value of the first [Deferred] from the given [variants] to complete.
* Other deferred values are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param variants The deferred values to race
* @param cancelOnResult If true, cancels all other deferred values after the first completes. Defaults to true
* @return The value of the first completed deferred
*/
suspend fun <T> CoroutineScope.firstOf(
vararg variants: Deferred<T>,
cancelOnResult: Boolean = true
) = firstOf(variants.toList(), cancelOnResult)
/**
* Returns the value of the first [Deferred] from this list to complete, using the given [scope].
* Other deferred values are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param scope The [CoroutineScope] in which to await the deferred values
* @param cancelOnResult If true, cancels all other deferred values after the first completes. Defaults to true
* @return The value of the first completed deferred
*/
suspend fun <T> List<Deferred<T>>.first(
scope: CoroutineScope,
cancelOnResult: Boolean = true

View File

@@ -2,4 +2,11 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.flow.FlowCollector
/**
* Operator function that allows a [FlowCollector] to be invoked like a function to emit a value.
* This is a convenient syntax sugar for [FlowCollector.emit].
*
* @param T The type of values the collector can emit
* @param value The value to emit
*/
suspend inline operator fun <T> FlowCollector<T>.invoke(value: T) = emit(value)

View File

@@ -14,6 +14,15 @@ private value class DebouncedByData<T>(
val millisToData: Pair<Long, T>
)
/**
* Debounces a [Flow] with per-marker timeout control. Values with the same marker will be debounced independently.
* For each marker, only the last value within the timeout period will be emitted.
*
* @param T The type of values emitted by the flow
* @param timeout A function that determines the debounce timeout in milliseconds for each value
* @param markerFactory A function that produces a marker for each value. Values with the same marker are debounced together
* @return A [Flow] that emits debounced values
*/
fun <T> Flow<T>.debouncedBy(timeout: (T) -> Long, markerFactory: (T) -> Any?): Flow<T> = channelFlow {
val jobs = mutableMapOf<Any?, Job>()
val mutex = Mutex()
@@ -36,5 +45,24 @@ fun <T> Flow<T>.debouncedBy(timeout: (T) -> Long, markerFactory: (T) -> Any?): F
}
}
/**
* Debounces a [Flow] with a fixed timeout in milliseconds and per-marker control.
* Values with the same marker will be debounced independently.
*
* @param T The type of values emitted by the flow
* @param timeout The debounce timeout in milliseconds
* @param markerFactory A function that produces a marker for each value. Values with the same marker are debounced together
* @return A [Flow] that emits debounced values
*/
fun <T> Flow<T>.debouncedBy(timeout: Long, markerFactory: (T) -> Any?): Flow<T> = debouncedBy({ timeout }, markerFactory)
/**
* Debounces a [Flow] with a fixed timeout as [Duration] and per-marker control.
* Values with the same marker will be debounced independently.
*
* @param T The type of values emitted by the flow
* @param timeout The debounce timeout as a [Duration]
* @param markerFactory A function that produces a marker for each value. Values with the same marker are debounced together
* @return A [Flow] that emits debounced values
*/
fun <T> Flow<T>.debouncedBy(timeout: Duration, markerFactory: (T) -> Any?): Flow<T> = debouncedBy({ timeout.inWholeMilliseconds }, markerFactory)

View File

@@ -3,4 +3,12 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
/**
* Returns the first non-null element emitted by this [Flow].
* Suspends until a non-null element is found.
*
* @param T The type of elements in the flow
* @return The first non-null element
* @throws NoSuchElementException if the flow completes without emitting a non-null element
*/
suspend fun <T> Flow<T?>.firstNotNull() = first { it != null }!!

View File

@@ -4,6 +4,14 @@ import kotlinx.coroutines.flow.*
import kotlin.js.JsName
import kotlin.jvm.JvmName
/**
* Transforms each inner [Flow] element using the given [mapper] function and flattens the result into a single [Flow].
*
* @param T The type of elements in the inner flows
* @param R The type of elements after applying the mapper
* @param mapper A suspending function to transform each element
* @return A [Flow] of mapped and flattened elements
*/
inline fun <T, R> Flow<Flow<T>>.flatMap(
crossinline mapper: suspend (T) -> R
) = flow {
@@ -14,6 +22,14 @@ inline fun <T, R> Flow<Flow<T>>.flatMap(
}
}
/**
* Transforms each element from inner [Iterable]s using the given [mapper] function and flattens the result into a single [Flow].
*
* @param T The type of elements in the iterables
* @param R The type of elements after applying the mapper
* @param mapper A suspending function to transform each element
* @return A [Flow] of mapped and flattened elements
*/
@JsName("flatMapIterable")
@JvmName("flatMapIterable")
inline fun <T, R> Flow<Iterable<T>>.flatMap(
@@ -22,18 +38,48 @@ inline fun <T, R> Flow<Iterable<T>>.flatMap(
it.asFlow()
}.flatMap(mapper)
/**
* Transforms each inner [Flow] element using the given [mapper] function, flattens the result,
* and filters out null values.
*
* @param T The type of elements in the inner flows
* @param R The type of elements after applying the mapper
* @param mapper A suspending function to transform each element
* @return A [Flow] of non-null mapped and flattened elements
*/
inline fun <T, R> Flow<Flow<T>>.flatMapNotNull(
crossinline mapper: suspend (T) -> R
) = flatMap(mapper).takeNotNull()
/**
* Transforms each element from inner [Iterable]s using the given [mapper] function, flattens the result,
* and filters out null values.
*
* @param T The type of elements in the iterables
* @param R The type of elements after applying the mapper
* @param mapper A suspending function to transform each element
* @return A [Flow] of non-null mapped and flattened elements
*/
@JsName("flatMapNotNullIterable")
@JvmName("flatMapNotNullIterable")
inline fun <T, R> Flow<Iterable<T>>.flatMapNotNull(
crossinline mapper: suspend (T) -> R
) = flatMap(mapper).takeNotNull()
/**
* Flattens a [Flow] of [Flow]s into a single [Flow] by collecting all inner flows sequentially.
*
* @param T The type of elements in the inner flows
* @return A [Flow] containing all elements from all inner flows
*/
fun <T> Flow<Flow<T>>.flatten() = flatMap { it }
/**
* Flattens a [Flow] of [Iterable]s into a single [Flow] by emitting all elements from each iterable.
*
* @param T The type of elements in the iterables
* @return A [Flow] containing all elements from all iterables
*/
@JsName("flattenIterable")
@JvmName("flattenIterable")
fun <T> Flow<Iterable<T>>.flatten() = flatMap { it }

View File

@@ -2,5 +2,18 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.flow.*
/**
* Filters out null values from this [Flow], returning only non-null elements.
*
* @param T The type of elements in the flow (nullable)
* @return A [Flow] containing only non-null elements
*/
fun <T> Flow<T>.takeNotNull() = mapNotNull { it }
/**
* Alias for [takeNotNull]. Filters out null values from this [Flow], returning only non-null elements.
*
* @param T The type of elements in the flow (nullable)
* @return A [Flow] containing only non-null elements
*/
fun <T> Flow<T>.filterNotNull() = takeNotNull()

View File

@@ -1,5 +1,6 @@
package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.flow.*
@@ -8,6 +9,7 @@ import kotlinx.coroutines.sync.withLock
private class SubscribeAsyncReceiver<T>(
val scope: CoroutineScope,
val logger: KSLog,
output: suspend SubscribeAsyncReceiver<T>.(T) -> Unit
) {
private val dataChannel: Channel<T> = Channel(Channel.UNLIMITED)
@@ -15,7 +17,7 @@ private class SubscribeAsyncReceiver<T>(
get() = dataChannel
init {
scope.launchLoggingDropExceptions {
scope.launchLoggingDropExceptions(logger = logger) {
for (data in dataChannel) {
output(data)
}
@@ -33,13 +35,16 @@ private data class AsyncSubscriptionCommandData<T, M>(
val scope: CoroutineScope,
val markerFactory: suspend (T) -> M,
val block: suspend (T) -> Unit,
val logger: KSLog,
val onEmpty: suspend (M) -> Unit
) : AsyncSubscriptionCommand<T, M> {
override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) {
val marker = markerFactory(data)
markersMap.getOrPut(marker) {
SubscribeAsyncReceiver(scope.LinkedSupervisorScope()) {
safelyWithoutExceptions { block(it) }
SubscribeAsyncReceiver(scope.LinkedSupervisorScope(), logger) {
runCatchingLogging(logger = logger) {
block(it)
}
if (isEmpty()) {
onEmpty(marker)
}
@@ -60,9 +65,24 @@ private data class AsyncSubscriptionCommandClearReceiver<T, M>(
}
}
/**
* Subscribes to a [Flow] with asynchronous processing based on markers.
* Each value from the flow will be processed by the [block] function. Values with the same marker
* will be processed sequentially in the same coroutine scope, while values with different markers
* can be processed concurrently in separate coroutine scopes.
*
* @param T The type of values emitted by the flow
* @param M The type of markers used to group values
* @param scope The [CoroutineScope] in which to subscribe to the flow
* @param markerFactory A factory function that produces a marker for each emitted value
* @param logger The logger instance used for logging exceptions. Defaults to [KSLog]
* @param block The suspending function that processes each emitted value
* @return A [Job] representing the subscription that can be cancelled
*/
fun <T, M> Flow<T>.subscribeAsync(
scope: CoroutineScope,
markerFactory: suspend (T) -> M,
logger: KSLog = KSLog,
block: suspend (T) -> Unit
): Job {
val subscope = scope.LinkedSupervisorScope()
@@ -71,8 +91,14 @@ fun <T, M> Flow<T>.subscribeAsync(
it.invoke(markersMap)
}
val job = subscribeLoggingDropExceptions(subscope) { data ->
val dataCommand = AsyncSubscriptionCommandData(data, subscope, markerFactory, block) { marker ->
val job = subscribeLoggingDropExceptions(subscope, logger = logger) { data ->
val dataCommand = AsyncSubscriptionCommandData(
data = data,
scope = subscope,
markerFactory = markerFactory,
block = block,
logger = logger
) { marker ->
actor.send(
AsyncSubscriptionCommandClearReceiver(marker)
)
@@ -85,17 +111,20 @@ fun <T, M> Flow<T>.subscribeAsync(
return job
}
@Deprecated("Renamed", ReplaceWith("subscribeLoggingDropExceptionsAsync(scope, markerFactory, block = block)", "dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptionsAsync"))
fun <T, M> Flow<T>.subscribeSafelyAsync(
scope: CoroutineScope,
markerFactory: suspend (T) -> M,
onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
logger: KSLog = KSLog,
block: suspend (T) -> Unit
) = subscribeAsync(scope, markerFactory) {
) = subscribeAsync(scope, markerFactory, logger) {
safely(onException) {
block(it)
}
}
@Deprecated("Renamed", ReplaceWith("subscribeLoggingDropExceptionsAsync(scope, markerFactory, block = block)", "dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptionsAsync"))
fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
scope: CoroutineScope,
markerFactory: suspend (T) -> M,
@@ -107,11 +136,36 @@ fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
}
}
/**
* Subscribes to a [Flow] with asynchronous processing based on markers, automatically logging and dropping exceptions.
* Each value from the flow will be processed by the [block] function. Values with the same marker
* will be processed sequentially, while values with different markers can be processed concurrently.
* Any exceptions thrown during processing will be logged and dropped without affecting other messages.
*
* @param T The type of values emitted by the flow
* @param M The type of markers used to group values
* @param scope The [CoroutineScope] in which to subscribe to the flow
* @param markerFactory A factory function that produces a marker for each emitted value
* @param logger The logger instance used for logging exceptions. Defaults to [KSLog]
* @param block The suspending function that processes each emitted value
* @return A [Job] representing the subscription that can be cancelled
*/
fun <T, M> Flow<T>.subscribeLoggingDropExceptionsAsync(
scope: CoroutineScope,
markerFactory: suspend (T) -> M,
logger: KSLog = KSLog,
block: suspend (T) -> Unit
) = subscribeAsync(scope, markerFactory, logger) {
block(it)
}
@Deprecated("Renamed", ReplaceWith("subscribeLoggingDropExceptionsAsync(scope, markerFactory, logger, block = block)", "dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptionsAsync"))
fun <T, M> Flow<T>.subscribeSafelySkippingExceptionsAsync(
scope: CoroutineScope,
markerFactory: suspend (T) -> M,
logger: KSLog = KSLog,
block: suspend (T) -> Unit
) = subscribeAsync(scope, markerFactory) {
) = subscribeAsync(scope, markerFactory, logger) {
safelyWithoutExceptions({ /* do nothing */}) {
block(it)
}

View File

@@ -5,4 +5,12 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.merge
/**
* Merges two flows into a single flow. Values from both flows will be emitted as they become available.
* This is a convenient operator syntax for [merge].
*
* @param T The type of elements in the flows
* @param other The flow to merge with this flow
* @return A [Flow] that emits values from both flows
*/
inline operator fun <T> Flow<T>.plus(other: Flow<T>) = merge(this, other)

View File

@@ -6,6 +6,17 @@ import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
/**
* Launches a new coroutine with automatic exception logging. If an exception occurs, it will be logged
* using the provided [logger] and then rethrown.
*
* @param errorMessageBuilder A function to build the error message from the caught exception. Defaults to "Something web wrong"
* @param logger The logger instance to use for logging exceptions. Defaults to [KSLog]
* @param context Additional [CoroutineContext] for the new coroutine. Defaults to [EmptyCoroutineContext]
* @param start The coroutine start option. Defaults to [CoroutineStart.DEFAULT]
* @param block The suspending function to execute in the new coroutine
* @return A [Job] representing the launched coroutine
*/
fun CoroutineScope.launchLogging(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
@@ -18,6 +29,17 @@ fun CoroutineScope.launchLogging(
}.getOrThrow()
}
/**
* Launches a new coroutine with automatic exception logging and dropping. If an exception occurs, it will be logged
* using the provided [logger] and then dropped (not rethrown), allowing the coroutine to complete normally.
*
* @param errorMessageBuilder A function to build the error message from the caught exception. Defaults to "Something web wrong"
* @param logger The logger instance to use for logging exceptions. Defaults to [KSLog]
* @param context Additional [CoroutineContext] for the new coroutine. Defaults to [EmptyCoroutineContext]
* @param start The coroutine start option. Defaults to [CoroutineStart.DEFAULT]
* @param block The suspending function to execute in the new coroutine
* @return A [Job] representing the launched coroutine
*/
fun CoroutineScope.launchLoggingDropExceptions(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
@@ -30,6 +52,18 @@ fun CoroutineScope.launchLoggingDropExceptions(
} // just dropping exception
}
/**
* Creates a new async coroutine with automatic exception logging. If an exception occurs, it will be logged
* using the provided [logger] and then rethrown when the [Deferred] is awaited.
*
* @param T The return type of the async computation
* @param errorMessageBuilder A function to build the error message from the caught exception. Defaults to "Something web wrong"
* @param logger The logger instance to use for logging exceptions. Defaults to [KSLog]
* @param context Additional [CoroutineContext] for the new coroutine. Defaults to [EmptyCoroutineContext]
* @param start The coroutine start option. Defaults to [CoroutineStart.DEFAULT]
* @param block The suspending function to execute that returns a value of type [T]
* @return A [Deferred] representing the async computation
*/
fun <T> CoroutineScope.asyncLogging(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
@@ -42,6 +76,18 @@ fun <T> CoroutineScope.asyncLogging(
}.getOrThrow()
}
/**
* Creates a new async coroutine with automatic exception logging and dropping. If an exception occurs, it will be logged
* using the provided [logger] and wrapped in a [Result], which can be checked when the [Deferred] is awaited.
*
* @param T The return type of the async computation
* @param errorMessageBuilder A function to build the error message from the caught exception. Defaults to "Something web wrong"
* @param logger The logger instance to use for logging exceptions. Defaults to [KSLog]
* @param context Additional [CoroutineContext] for the new coroutine. Defaults to [EmptyCoroutineContext]
* @param start The coroutine start option. Defaults to [CoroutineStart.DEFAULT]
* @param block The suspending function to execute that returns a value of type [T]
* @return A [Deferred] containing a [Result] representing the async computation
*/
fun <T> CoroutineScope.asyncLoggingDropExceptions(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,

View File

@@ -1,7 +1,5 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow
@@ -11,13 +9,12 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.internal.SynchronizedObject
import kotlinx.coroutines.internal.synchronized
import kotlin.coroutines.CoroutineContext
/**
* Works like [StateFlow], but guarantee that latest value update will always be delivered to
* each active subscriber
*/
open class SpecialMutableStateFlow<T>(
open class MutableRedeliverStateFlow<T>(
initialValue: T
) : MutableStateFlow<T>, FlowCollector<T>, MutableSharedFlow<T> {
@OptIn(InternalCoroutinesApi::class)
@@ -68,3 +65,6 @@ open class SpecialMutableStateFlow<T>(
override suspend fun collect(collector: FlowCollector<T>) = sharingFlow.collect(collector)
}
@Deprecated("Renamed to MutableRedeliverStateFlow", ReplaceWith("MutableRedeliverStateFlow<T>"))
typealias SpecialMutableStateFlow<T> = MutableRedeliverStateFlow<T>

View File

@@ -1,3 +1,13 @@
package dev.inmo.micro_utils.coroutines
/**
* Replaces a failed [Result] with a new value computed from the exception.
* If this [Result] is successful, it is returned as-is. If it represents a failure,
* the [onException] handler is called with the exception to compute a replacement value,
* which is then wrapped in a new [Result].
*
* @param T The type of the successful value
* @param onException A function that computes a replacement value from the caught exception
* @return The original [Result] if successful, or a new [Result] containing the replacement value
*/
inline fun <T> Result<T>.replaceIfFailure(onException: (Throwable) -> T) = if (isSuccess) { this } else { runCatching { onException(exceptionOrNull()!!) } }

View File

@@ -2,11 +2,27 @@ package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.e
import kotlin.coroutines.cancellation.CancellationException
/**
* Executes the given [block] within a `runCatching` context and logs any exceptions that occur, excluding
* `CancellationException` which is rethrown. This method simplifies error handling by automatically logging
* the errors using the provided [logger].
*
* @param T The result type of the [block].
* @param R The receiver type on which this function operates.
* @param errorMessageBuilder A lambda to build the error log message. By default, it returns a generic error message.
* @param logger The logging instance used for logging errors. Defaults to [KSLog].
* @param block The code block to execute within the `runCatching` context.
* @return A [Result] representing the outcome of executing the [block].
*/
inline fun <T, R> R.runCatchingLogging(
noinline errorMessageBuilder: R.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
block: R.() -> T
) = runCatching(block).onFailure {
logger.e(it) { errorMessageBuilder(it) }
when (it) {
is CancellationException -> throw it
else -> logger.e(it) { errorMessageBuilder(it) }
}
}

View File

@@ -1,7 +1,6 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
@@ -44,7 +43,7 @@ sealed interface SmartMutex {
* @param locked Preset state of [isLocked] and its internal [_lockStateFlow]
*/
class Mutable(locked: Boolean = false) : SmartMutex {
private val _lockStateFlow = SpecialMutableStateFlow<Boolean>(locked)
private val _lockStateFlow = MutableRedeliverStateFlow<Boolean>(locked)
override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow()
private val internalChangesMutex = Mutex()

View File

@@ -3,6 +3,14 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
/**
* Executes the given [block] and unlocks all provided [lockers] for writing if the block succeeds.
* If the block throws an exception, the lockers will remain locked.
*
* @param lockers Variable number of [SmartRWLocker] instances to unlock on successful execution
* @param block The suspending function to execute
* @return A [Result] containing [Unit] on success or the exception that occurred
*/
suspend inline fun alsoWithUnlockingOnSuccess(
vararg lockers: SmartRWLocker,
block: suspend () -> Unit
@@ -14,6 +22,15 @@ suspend inline fun alsoWithUnlockingOnSuccess(
}
}
/**
* Asynchronously executes the given [block] and unlocks all provided [lockers] for writing if the block succeeds.
* This function launches a new coroutine in the given [scope] and automatically logs and drops any exceptions.
*
* @param scope The [CoroutineScope] in which to launch the coroutine
* @param lockers Variable number of [SmartRWLocker] instances to unlock on successful execution
* @param block The suspending function to execute
* @return A [Job] representing the launched coroutine
*/
fun alsoWithUnlockingOnSuccessAsync(
scope: CoroutineScope,
vararg lockers: SmartRWLocker,

View File

@@ -1,7 +1,6 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
@@ -47,7 +46,7 @@ sealed interface SmartSemaphore {
*/
class Mutable(permits: Int, acquiredPermits: Int = 0) : SmartSemaphore {
override val maxPermits: Int = permits
private val _freePermitsStateFlow = SpecialMutableStateFlow<Int>(permits - acquiredPermits)
private val _freePermitsStateFlow = MutableRedeliverStateFlow<Int>(permits - acquiredPermits)
override val permitsStateFlow: StateFlow<Int> = _freePermitsStateFlow.asStateFlow()
private val internalChangesMutex = Mutex(false)
@@ -73,7 +72,7 @@ sealed interface SmartSemaphore {
acquiredPermits != checkedPermits
}
if (shouldContinue) {
waitRelease()
waitRelease(checkedPermits - acquiredPermits)
}
} while (shouldContinue && currentCoroutineContext().isActive)
} catch (e: Throwable) {

View File

@@ -3,19 +3,49 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
/**
* Creates a [SupervisorJob] linked to this [CoroutineContext]'s job. The new supervisor job will be a child
* of the current job, and optionally combined with [additionalContext].
*
* @param additionalContext Optional additional context to combine with the supervisor job
* @return A [CoroutineContext] containing the new supervisor job
*/
fun CoroutineContext.LinkedSupervisorJob(
additionalContext: CoroutineContext? = null
) = SupervisorJob(job).let { if (additionalContext != null) it + additionalContext else it }
/**
* Creates a [SupervisorJob] linked to this [CoroutineScope]'s job. The new supervisor job will be a child
* of the current scope's job, and optionally combined with [additionalContext].
*
* @param additionalContext Optional additional context to combine with the supervisor job
* @return A [CoroutineContext] containing the new supervisor job
*/
fun CoroutineScope.LinkedSupervisorJob(
additionalContext: CoroutineContext? = null
) = coroutineContext.LinkedSupervisorJob(additionalContext)
/**
* Creates a new [CoroutineScope] with a [SupervisorJob] linked to this [CoroutineContext]'s job.
* The new scope's supervisor job will be a child of the current job, and optionally combined with [additionalContext].
*
* @param additionalContext Optional additional context to combine with the supervisor job
* @return A new [CoroutineScope] with a linked supervisor job
*/
fun CoroutineContext.LinkedSupervisorScope(
additionalContext: CoroutineContext? = null
) = CoroutineScope(
this + LinkedSupervisorJob(additionalContext)
)
/**
* Creates a new [CoroutineScope] with a [SupervisorJob] linked to this [CoroutineScope]'s job.
* The new scope's supervisor job will be a child of the current scope's job, and optionally combined with [additionalContext].
*
* @param additionalContext Optional additional context to combine with the supervisor job
* @return A new [CoroutineScope] with a linked supervisor job
*/
fun CoroutineScope.LinkedSupervisorScope(
additionalContext: CoroutineContext? = null
) = coroutineContext.LinkedSupervisorScope(additionalContext)

View File

@@ -0,0 +1,15 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
/**
* Ensures that the current coroutine context is still active and throws a [kotlinx.coroutines.CancellationException]
* if the coroutine has been canceled.
*
* This function provides a convenient way to check the active status of a coroutine, which is useful
* to identify cancellation points in long-running or suspendable operations.
*
* @throws kotlinx.coroutines.CancellationException if the coroutine context is no longer active.
*/
suspend fun suspendPoint() = currentCoroutineContext().ensureActive()

View File

@@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.test.runTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
@@ -13,184 +14,517 @@ import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
class SmartKeyRWLockerTests {
@Test
fun writeLockKeyFailedOnGlobalWriteLockTest() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKey = "test"
locker.lockWrite()
private lateinit var locker: SmartKeyRWLocker<String>
assertTrue { locker.isWriteLocked() }
assertFails {
realWithTimeout(1.seconds) {
locker.lockWrite(testKey)
}
}
assertFalse { locker.isWriteLocked(testKey) }
locker.unlockWrite()
assertFalse { locker.isWriteLocked() }
realWithTimeout(1.seconds) {
locker.lockWrite(testKey)
}
assertTrue { locker.isWriteLocked(testKey) }
assertTrue { locker.unlockWrite(testKey) }
assertFalse { locker.isWriteLocked(testKey) }
@BeforeTest
fun setup() {
locker = SmartKeyRWLocker()
}
// ==================== Global Read Tests ====================
@Test
fun writeLockKeyFailedOnGlobalReadLockTest() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKey = "test"
fun testGlobalReadAllowsMultipleConcurrentReads() = runTest {
val results = mutableListOf<Boolean>()
locker.acquireRead()
assertEquals(Int.MAX_VALUE - 1, locker.readSemaphore().freePermits)
assertFails {
realWithTimeout(1.seconds) {
locker.lockWrite(testKey)
val jobs = List(5) {
launch {
locker.acquireRead()
delay(100.milliseconds)
results.add(true)
locker.releaseRead()
}
}
assertFalse { locker.isWriteLocked(testKey) }
jobs.joinAll()
locker.releaseRead()
assertEquals(5, results.size)
}
@Test
fun testGlobalReadBlocksGlobalWrite() = runTest {
locker.acquireRead()
var writeAcquired = false
val writeJob = launch {
locker.lockWrite()
writeAcquired = true
locker.unlockWrite()
}
delay(200.milliseconds)
assertFalse(writeAcquired, "Write should be blocked by global read")
locker.releaseRead()
assertEquals(Int.MAX_VALUE, locker.readSemaphore().freePermits)
writeJob.join()
realWithTimeout(1.seconds) {
locker.lockWrite(testKey)
}
assertTrue { locker.isWriteLocked(testKey) }
assertTrue { locker.unlockWrite(testKey) }
assertFalse { locker.isWriteLocked(testKey) }
assertTrue(writeAcquired, "Write should succeed after read released")
}
@Test
fun readLockFailedOnWriteLockKeyTest() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKey = "test"
locker.lockWrite(testKey)
fun testGlobalReadBlocksAllKeyWrites() = runTest {
locker.acquireRead()
assertTrue { locker.isWriteLocked(testKey) }
val writeFlags = mutableMapOf<String, Boolean>()
val keys = listOf("key1", "key2", "key3")
assertFails {
realWithTimeout(1.seconds) {
val jobs = keys.map { key ->
launch {
locker.lockWrite(key)
writeFlags[key] = true
locker.unlockWrite(key)
}
}
delay(200.milliseconds)
assertTrue(writeFlags.isEmpty(), "No writes should succeed while global read active")
locker.releaseRead()
jobs.joinAll()
assertEquals(keys.size, writeFlags.size, "All writes should succeed after global read released")
}
// ==================== Global Write Tests ====================
@Test
fun testGlobalWriteBlocksAllOperations() = runTest {
locker.lockWrite()
var globalReadAcquired = false
var keyReadAcquired = false
var keyWriteAcquired = false
val jobs = listOf(
launch {
locker.acquireRead()
globalReadAcquired = true
locker.releaseRead()
},
launch {
locker.acquireRead("key1")
keyReadAcquired = true
locker.releaseRead("key1")
},
launch {
locker.lockWrite("key2")
keyWriteAcquired = true
locker.unlockWrite("key2")
}
}
assertEquals(locker.readSemaphore().maxPermits - 1, locker.readSemaphore().freePermits)
)
locker.unlockWrite(testKey)
assertFalse { locker.isWriteLocked(testKey) }
delay(200.milliseconds)
assertFalse(globalReadAcquired, "Global read should be blocked")
assertFalse(keyReadAcquired, "Key read should be blocked")
assertFalse(keyWriteAcquired, "Key write should be blocked")
realWithTimeout(1.seconds) {
locker.acquireRead()
}
assertEquals(locker.readSemaphore().maxPermits - 1, locker.readSemaphore().freePermits)
assertTrue { locker.releaseRead() }
assertEquals(locker.readSemaphore().maxPermits, locker.readSemaphore().freePermits)
locker.unlockWrite()
jobs.joinAll()
assertTrue(globalReadAcquired)
assertTrue(keyReadAcquired)
assertTrue(keyWriteAcquired)
}
@Test
fun writeLockFailedOnWriteLockKeyTest() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKey = "test"
locker.lockWrite(testKey)
fun testGlobalWriteIsExclusive() = runTest {
locker.lockWrite()
assertTrue { locker.isWriteLocked(testKey) }
assertFails {
realWithTimeout(1.seconds) {
locker.lockWrite()
}
}
assertFalse(locker.isWriteLocked())
locker.unlockWrite(testKey)
assertFalse { locker.isWriteLocked(testKey) }
realWithTimeout(1.seconds) {
var secondWriteAcquired = false
val job = launch {
locker.lockWrite()
secondWriteAcquired = true
locker.unlockWrite()
}
assertTrue(locker.isWriteLocked())
assertTrue { locker.unlockWrite() }
assertFalse(locker.isWriteLocked())
delay(200.milliseconds)
assertFalse(secondWriteAcquired, "Second global write should be blocked")
locker.unlockWrite()
job.join()
assertTrue(secondWriteAcquired)
}
// ==================== Key Read Tests ====================
@Test
fun readsBlockingGlobalWrite() = runTest {
val locker = SmartKeyRWLocker<String>()
fun testKeyReadAllowsMultipleConcurrentReadsForSameKey() = runTest {
val key = "testKey"
val results = mutableListOf<Boolean>()
val testKeys = (0 until 100).map { "test$it" }
locker.acquireRead(key)
for (i in testKeys.indices) {
val it = testKeys[i]
locker.acquireRead(it)
val previous = testKeys.take(i)
val next = testKeys.drop(i + 1)
previous.forEach {
assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == Int.MAX_VALUE - 1 }
}
next.forEach {
assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == null }
val jobs = List(5) {
launch {
locker.acquireRead(key)
delay(50.milliseconds)
results.add(true)
locker.releaseRead(key)
}
}
for (i in testKeys.indices) {
val it = testKeys[i]
assertFails {
realWithTimeout(13.milliseconds) { locker.lockWrite() }
jobs.joinAll()
locker.releaseRead(key)
assertEquals(5, results.size)
}
@Test
fun testKeyReadAllowsReadsForDifferentKeys() = runTest {
val results = mutableMapOf<String, Boolean>()
locker.acquireRead("key1")
val jobs = listOf("key2", "key3", "key4").map { key ->
launch {
locker.acquireRead(key)
delay(50.milliseconds)
results[key] = true
locker.releaseRead(key)
}
val readPermitsBeforeLock = locker.readSemaphore().freePermits
realWithTimeout(1.seconds) { locker.acquireRead() }
}
jobs.joinAll()
locker.releaseRead("key1")
assertEquals(3, results.size)
}
@Test
fun testKeyReadBlocksWriteForSameKey() = runTest {
val key = "testKey"
locker.acquireRead(key)
var writeAcquired = false
val job = launch {
locker.lockWrite(key)
writeAcquired = true
locker.unlockWrite(key)
}
delay(200.milliseconds)
assertFalse(writeAcquired, "Write for same key should be blocked")
locker.releaseRead(key)
job.join()
assertTrue(writeAcquired)
}
@Test
fun testKeyReadBlocksGlobalWrite() = runTest {
locker.acquireRead("key1")
var globalWriteAcquired = false
val job = launch {
locker.lockWrite()
globalWriteAcquired = true
locker.unlockWrite()
}
delay(200.milliseconds)
assertFalse(globalWriteAcquired, "Global write should be blocked by key read")
locker.releaseRead("key1")
job.join()
assertTrue(globalWriteAcquired)
}
@Test
fun testKeyReadAllowsWriteForDifferentKey() = runTest {
locker.acquireRead("key1")
var writeAcquired = false
val job = launch {
locker.lockWrite("key2")
writeAcquired = true
locker.unlockWrite("key2")
}
job.join()
assertTrue(writeAcquired, "Write for different key should succeed")
locker.releaseRead("key1")
}
// ==================== Key Write Tests ====================
@Test
fun testKeyWriteBlocksReadForSameKey() = runTest {
val key = "testKey"
locker.lockWrite(key)
var readAcquired = false
val job = launch {
locker.acquireRead(key)
readAcquired = true
locker.releaseRead(key)
}
delay(200.milliseconds)
assertFalse(readAcquired, "Read for same key should be blocked")
locker.unlockWrite(key)
job.join()
assertTrue(readAcquired)
}
@Test
fun testKeyWriteBlocksGlobalRead() = runTest {
locker.lockWrite("key1")
var globalReadAcquired = false
val job = launch {
locker.acquireRead()
globalReadAcquired = true
locker.releaseRead()
assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits)
locker.releaseRead(it)
}
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE }
realWithTimeout(1.seconds) { locker.lockWrite() }
assertFails {
realWithTimeout(13.milliseconds) { locker.acquireRead() }
}
assertTrue { locker.unlockWrite() }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE }
delay(200.milliseconds)
assertFalse(globalReadAcquired, "Global read should be blocked by key write")
locker.unlockWrite("key1")
job.join()
assertTrue(globalReadAcquired)
}
@Test
fun writesBlockingGlobalWrite() = runTest {
val locker = SmartKeyRWLocker<String>()
fun testKeyWriteIsExclusiveForSameKey() = runTest {
val key = "testKey"
locker.lockWrite(key)
val testKeys = (0 until 100).map { "test$it" }
var secondWriteAcquired = false
val job = launch {
locker.lockWrite(key)
secondWriteAcquired = true
locker.unlockWrite(key)
}
for (i in testKeys.indices) {
val it = testKeys[i]
locker.lockWrite(it)
val previous = testKeys.take(i)
val next = testKeys.drop(i + 1)
delay(200.milliseconds)
assertFalse(secondWriteAcquired, "Second write for same key should be blocked")
previous.forEach {
assertTrue { locker.writeMutexOrNull(it) ?.isLocked == true }
locker.unlockWrite(key)
job.join()
assertTrue(secondWriteAcquired)
}
@Test
fun testKeyWriteAllowsOperationsOnDifferentKeys() = runTest {
locker.lockWrite("key1")
val results = mutableMapOf<String, Boolean>()
val jobs = listOf(
launch {
locker.acquireRead("key2")
results["read-key2"] = true
locker.releaseRead("key2")
},
launch {
locker.lockWrite("key3")
results["write-key3"] = true
locker.unlockWrite("key3")
}
next.forEach {
assertTrue { locker.writeMutexOrNull(it) ?.isLocked != true }
)
jobs.joinAll()
assertEquals(2, results.size, "Operations on different keys should succeed")
locker.unlockWrite("key1")
}
// ==================== Complex Scenarios ====================
@Test
fun testMultipleReadersThenWriter() = runTest {
val key = "testKey"
val readCount = 5
val readers = mutableListOf<Job>()
repeat(readCount) {
readers.add(launch {
locker.acquireRead(key)
delay(100.milliseconds)
locker.releaseRead(key)
})
}
delay(50.milliseconds) // Let readers acquire
var writerExecuted = false
val writer = launch {
locker.lockWrite(key)
writerExecuted = true
locker.unlockWrite(key)
}
delay(50.milliseconds)
assertFalse(writerExecuted, "Writer should wait for all readers")
readers.joinAll()
writer.join()
assertTrue(writerExecuted, "Writer should execute after all readers done")
}
@Test
fun testWriterThenMultipleReaders() = runTest {
val key = "testKey"
locker.lockWrite(key)
val readerFlags = mutableListOf<Boolean>()
val readers = List(5) {
launch {
locker.acquireRead(key)
readerFlags.add(true)
locker.releaseRead(key)
}
}
for (i in testKeys.indices) {
val it = testKeys[i]
assertFails { realWithTimeout(13.milliseconds) { locker.lockWrite() } }
delay(200.milliseconds)
assertTrue(readerFlags.isEmpty(), "Readers should be blocked by writer")
val readPermitsBeforeLock = locker.readSemaphore().freePermits
assertFails { realWithTimeout(13.milliseconds) { locker.acquireRead() } }
assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits)
locker.unlockWrite(key)
readers.joinAll()
locker.unlockWrite(it)
assertEquals(5, readerFlags.size, "All readers should succeed after writer")
}
@Test
fun testCascadingLocksWithDifferentKeys() = runTest {
val executed = mutableMapOf<String, Boolean>()
launch {
locker.lockWrite("key1")
executed["write-key1-start"] = true
delay(100.milliseconds)
locker.unlockWrite("key1")
executed["write-key1-end"] = true
}
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE }
realWithTimeout(1.seconds) { locker.lockWrite() }
assertFails {
realWithTimeout(13.milliseconds) { locker.acquireRead() }
delay(50.milliseconds)
launch {
locker.acquireRead("key2")
executed["read-key2"] = true
delay(100.milliseconds)
locker.releaseRead("key2")
}
assertTrue { locker.unlockWrite() }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE }
delay(200.milliseconds)
assertTrue(executed["write-key1-start"] == true)
assertTrue(executed["read-key2"] == true)
assertTrue(executed["write-key1-end"] == true)
}
@Test
fun testReleaseWithoutAcquireReturnsFalse() = runTest {
assertFalse(locker.releaseRead(), "Release without acquire should return false")
assertFalse(locker.releaseRead("key1"), "Release without acquire should return false")
}
@Test
fun testUnlockWithoutLockReturnsFalse() = runTest {
assertFalse(locker.unlockWrite(), "Unlock without lock should return false")
assertFalse(locker.unlockWrite("key1"), "Unlock without lock should return false")
}
@Test
fun testProperReleaseReturnsTrue() = runTest {
locker.acquireRead()
assertTrue(locker.releaseRead(), "Release after acquire should return true")
locker.acquireRead("key1")
assertTrue(locker.releaseRead("key1"), "Release after acquire should return true")
}
@Test
fun testProperUnlockReturnsTrue() = runTest {
locker.lockWrite()
assertTrue(locker.unlockWrite(), "Unlock after lock should return true")
locker.lockWrite("key1")
assertTrue(locker.unlockWrite("key1"), "Unlock after lock should return true")
}
// ==================== Stress Tests ====================
@Test
fun stressTestWithMixedOperations() = runTest(timeout = 10.seconds) {
val operations = 100
val keys = listOf("key1", "key2", "key3", "key4", "key5")
val jobs = mutableListOf<Job>()
repeat(operations) { i ->
val key = keys[i % keys.size]
when (i % 4) {
0 -> jobs.add(launch {
locker.acquireRead(key)
delay(10.milliseconds)
locker.releaseRead(key)
})
1 -> jobs.add(launch {
locker.lockWrite(key)
delay(10.milliseconds)
locker.unlockWrite(key)
})
2 -> jobs.add(launch {
locker.acquireRead()
delay(10.milliseconds)
locker.releaseRead()
})
3 -> jobs.add(launch {
locker.lockWrite()
delay(10.milliseconds)
locker.unlockWrite()
})
}
}
jobs.joinAll()
// If we reach here without deadlock or exceptions, test passes
}
@Test
fun testFairnessReadersDontStarveWriters() = runTest(timeout = 5.seconds) {
val key = "testKey"
var writerExecuted = false
// Start continuous readers
val readers = List(10) {
launch {
repeat(5) {
locker.acquireRead(key)
delay(50.milliseconds)
locker.releaseRead(key)
delay(10.milliseconds)
}
}
}
delay(100.milliseconds)
// Try to acquire write lock
val writer = launch {
locker.lockWrite(key)
writerExecuted = true
locker.unlockWrite(key)
}
readers.joinAll()
writer.join()
assertTrue(writerExecuted, "Writer should eventually execute")
}
}

View File

@@ -1,33 +1,31 @@
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
import dev.inmo.micro_utils.coroutines.asDeferred
import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
import dev.inmo.micro_utils.coroutines.subscribe
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class SpecialMutableStateFlowTests {
@Test
fun simpleTest() = runTest {
val specialMutableStateFlow = SpecialMutableStateFlow(0)
specialMutableStateFlow.value = 1
specialMutableStateFlow.first { it == 1 }
assertEquals(1, specialMutableStateFlow.value)
val mutableRedeliverStateFlow = MutableRedeliverStateFlow(0)
mutableRedeliverStateFlow.value = 1
mutableRedeliverStateFlow.first { it == 1 }
assertEquals(1, mutableRedeliverStateFlow.value)
}
@Test
fun specialTest() = runTest {
val specialMutableStateFlow = SpecialMutableStateFlow(0)
val mutableRedeliverStateFlow = MutableRedeliverStateFlow(0)
lateinit var subscriberJob: Job
subscriberJob = specialMutableStateFlow.subscribe(this) {
subscriberJob = mutableRedeliverStateFlow.subscribe(this) {
when (it) {
1 -> specialMutableStateFlow.value = 2
1 -> mutableRedeliverStateFlow.value = 2
2 -> subscriberJob.cancel()
}
}
specialMutableStateFlow.value = 1
mutableRedeliverStateFlow.value = 1
subscriberJob.join()
assertEquals(2, specialMutableStateFlow.value)
assertEquals(2, mutableRedeliverStateFlow.value)
}
}

View File

@@ -2,9 +2,22 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.*
/**
* Convenience property to access [Dispatchers.IO] for I/O-bound operations.
* This dispatcher is optimized for offloading blocking I/O tasks to a shared pool of threads.
*/
val IO
get() = Dispatchers.IO
/**
* Executes the given [block] on the IO dispatcher and returns its result.
* This is a convenience function for executing I/O-bound operations like reading files,
* network requests, or database queries.
*
* @param T The return type of the block
* @param block The suspending function to execute on the IO dispatcher
* @return The result of executing the block
*/
suspend inline fun <T> doInIO(noinline block: suspend CoroutineScope.() -> T) = doIn(
IO,
block

View File

@@ -3,6 +3,16 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
/**
* Launches a coroutine in the current thread using [Dispatchers.Unconfined] and blocks until it completes,
* returning its result. The coroutine will start execution in the current thread and will continue
* in the same thread until the first suspension point.
*
* @param T The return type of the suspending block
* @param block The suspending function to execute in the current thread
* @return The result of the suspending block
* @throws Throwable if the coroutine throws an exception
*/
fun <T> launchInCurrentThread(block: suspend CoroutineScope.() -> T): T {
val scope = CoroutineScope(Dispatchers.Unconfined)
return scope.launchSynchronously(block)

View File

@@ -2,6 +2,16 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.*
/**
* Launches a coroutine and blocks the current thread until the coroutine completes, returning its result.
* This is useful for bridging between suspending and non-suspending code in JVM environments.
* The coroutine is launched with [CoroutineStart.UNDISPATCHED] to start execution immediately.
*
* @param T The return type of the suspending block
* @param block The suspending function to execute synchronously
* @return The result of the suspending block
* @throws Throwable if the coroutine throws an exception
*/
fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T): T {
var result: Result<T>? = null
val objectToSynchronize = Object()
@@ -22,7 +32,31 @@ fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T
return result!!.getOrThrow()
}
/**
* Launches a coroutine in a new [CoroutineScope] with [Dispatchers.Default] and blocks the current thread
* until the coroutine completes, returning its result.
*
* @param T The return type of the suspending block
* @param block The suspending function to execute synchronously
* @return The result of the suspending block
* @throws Throwable if the coroutine throws an exception
*/
fun <T> launchSynchronously(block: suspend CoroutineScope.() -> T): T = CoroutineScope(Dispatchers.Default).launchSynchronously(block)
/**
* Alias for [launchSynchronously]. Launches a coroutine and blocks the current thread until it completes.
*
* @param T The return type of the suspending block
* @param block The suspending function to execute synchronously
* @return The result of the suspending block
*/
fun <T> CoroutineScope.doSynchronously(block: suspend CoroutineScope.() -> T): T = launchSynchronously(block)
/**
* Alias for [launchSynchronously]. Launches a coroutine in a new scope and blocks the current thread until it completes.
*
* @param T The return type of the suspending block
* @param block The suspending function to execute synchronously
* @return The result of the suspending block
*/
fun <T> doSynchronously(block: suspend CoroutineScope.() -> T): T = launchSynchronously(block)

View File

@@ -12,12 +12,35 @@ private val BASE64_INVERSE_ALPHABET = IntArray(256) {
internal fun Int.toBase64(): Char = BASE64_ALPHABET[this]
internal fun Byte.fromBase64(): Byte = BASE64_INVERSE_ALPHABET[toInt() and 0xff].toByte() and BASE64_MASK
/**
* Type alias representing a Base64-encoded string.
*/
typealias EncodedBase64String = String
/**
* Type alias representing a Base64-encoded byte array.
*/
typealias EncodedByteArray = ByteArray
/**
* Encodes this string to Base64 format, returning the result as a string.
*
* @return A Base64-encoded string
*/
fun SourceString.encodeBase64String(): EncodedBase64String = encodeToByteArray().encodeBase64String()
/**
* Encodes this string to Base64 format, returning the result as a byte array.
*
* @return A Base64-encoded byte array
*/
fun SourceString.encodeBase64(): EncodedByteArray = encodeToByteArray().encodeBase64()
/**
* Encodes this byte array to Base64 format, returning the result as a string.
*
* @return A Base64-encoded string with padding ('=') characters
*/
fun SourceBytes.encodeBase64String(): EncodedBase64String = buildString {
var i = 0
while (this@encodeBase64String.size > i) {
@@ -45,11 +68,33 @@ fun SourceBytes.encodeBase64String(): EncodedBase64String = buildString {
i += read
}
}
/**
* Encodes this byte array to Base64 format, returning the result as a byte array.
*
* @return A Base64-encoded byte array
*/
fun SourceBytes.encodeBase64(): EncodedByteArray = encodeBase64String().encodeToByteArray()
/**
* Decodes this Base64-encoded string back to the original byte array.
*
* @return The decoded byte array
*/
fun EncodedBase64String.decodeBase64(): SourceBytes = dropLastWhile { it == BASE64_PAD }.encodeToByteArray().decodeBase64()
/**
* Decodes this Base64-encoded string back to the original string.
*
* @return The decoded string
*/
fun EncodedBase64String.decodeBase64String(): SourceString = decodeBase64().decodeToString()
/**
* Decodes this Base64-encoded byte array back to the original byte array.
*
* @return The decoded byte array
*/
fun EncodedByteArray.decodeBase64(): SourceBytes {
val result = mutableListOf<Byte>()
val data = ByteArray(4)
@@ -74,4 +119,10 @@ fun EncodedByteArray.decodeBase64(): SourceBytes {
return result.toByteArray()
}
/**
* Decodes this Base64-encoded byte array back to the original string.
*
* @return The decoded string
*/
fun EncodedByteArray.decodeBase64String(): SourceString = decodeBase64().decodeToString()

View File

@@ -1,7 +1,16 @@
package dev.inmo.micro_utils.crypto
/**
* Character array used for hexadecimal encoding (lowercase).
*/
val HEX_ARRAY = "0123456789abcdef".toCharArray()
/**
* Converts this byte array to a hexadecimal string representation (lowercase).
* Each byte is represented as two hex characters.
*
* @return A lowercase hex string (e.g., "48656c6c6f" for "Hello")
*/
fun SourceBytes.hex(): String {
val hexChars = CharArray(size * 2)
for (j in indices) {
@@ -12,4 +21,9 @@ fun SourceBytes.hex(): String {
return hexChars.concatToString()
}
/**
* Converts this string to a hexadecimal representation by first encoding it as UTF-8 bytes.
*
* @return A lowercase hex string representation of the UTF-8 encoded bytes
*/
fun SourceString.hex(): String = encodeToByteArray().hex()

View File

@@ -2,7 +2,21 @@ package dev.inmo.micro_utils.crypto
import korlibs.crypto.md5
/**
* Type alias representing an MD5 hash as a hex-encoded string (32 characters).
*/
typealias MD5 = String
/**
* Computes the MD5 hash of this byte array and returns it as a lowercase hex string.
*
* @return The MD5 hash as a 32-character lowercase hex string
*/
fun SourceBytes.md5(): MD5 = md5().hexLower
/**
* Computes the MD5 hash of this string (encoded as UTF-8 bytes) and returns it as a lowercase hex string.
*
* @return The MD5 hash as a 32-character lowercase hex string
*/
fun SourceString.md5(): MD5 = encodeToByteArray().md5().hexLower

View File

@@ -26,7 +26,7 @@ kotlin {
project.parent.subprojects.forEach {
if (
it != project
it.name != project.name
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") }
@@ -44,7 +44,7 @@ kotlin {
project.parent.subprojects.forEach {
if (
it != project
it.name != project.name
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") }
@@ -60,7 +60,7 @@ kotlin {
project.parent.subprojects.forEach {
if (
it != project
it.name != project.name
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
@@ -76,7 +76,7 @@ kotlin {
project.parent.subprojects.forEach {
if (
it != project
it.name != project.name
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }

View File

@@ -1,5 +1,13 @@
package dev.inmo.micro_utils.fsm.common
/**
* Represents a state in a finite state machine (FSM).
* Each state must have an associated context that identifies it uniquely within its chain.
*/
interface State {
/**
* The context object that uniquely identifies this state within a state chain.
* States with the same context are considered to belong to the same chain.
*/
val context: Any
}

View File

@@ -1,6 +1,26 @@
package dev.inmo.micro_utils.fsm.common.utils
/**
* A handler function type for dealing with errors during state handling in a finite state machine.
* The handler receives the state that caused the error and the thrown exception, and can optionally
* return a new state to continue the chain, or null to end the chain.
*
* @param T The state type that caused the error
* @param Throwable The exception that was thrown during state handling
* @return A new state to continue with, or null to end the state chain
*/
typealias StateHandlingErrorHandler<T> = suspend (T, Throwable) -> T?
/**
* The default error handler that returns null for all errors, effectively ending the state chain.
*/
val DefaultStateHandlingErrorHandler: StateHandlingErrorHandler<*> = { _, _ -> null }
/**
* Returns a typed version of the [DefaultStateHandlingErrorHandler].
*
* @param T The state type
* @return A [StateHandlingErrorHandler] for type [T]
*/
inline fun <T> defaultStateHandlingErrorHandler(): StateHandlingErrorHandler<T> = DefaultStateHandlingErrorHandler as StateHandlingErrorHandler<T>

View File

@@ -1,9 +1,15 @@
interface InjectedExecOps {
@Inject //@javax.inject.Inject
ExecOperations getExecOps()
}
private String getCurrentVersionChangelog() {
OutputStream changelogDataOS = new ByteArrayOutputStream()
exec {
def injected = project.objects.newInstance(InjectedExecOps)
injected.execOps.exec {
commandLine 'chmod', "+x", './changelog_parser.sh'
}
exec {
injected.execOps.exec {
standardOutput = changelogDataOS
commandLine './changelog_parser.sh', "${project.version}", 'CHANGELOG.md'
}

View File

@@ -8,6 +8,9 @@ android.useAndroidX=true
android.enableJetifier=true
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=2g
## https://github.com/google/ksp/issues/2491
#ksp.useKSP2=false
# JS NPM
crypto_js_version=4.1.1
@@ -15,5 +18,5 @@ crypto_js_version=4.1.1
# Project data
group=dev.inmo
version=0.25.8
android_code_version=298
version=0.29.0
android_code_version=310

View File

@@ -1,45 +1,47 @@
[versions]
kt = "2.1.21"
kt-serialization = "1.8.1"
kt = "2.3.10"
kt-serialization = "1.10.0"
kt-coroutines = "1.10.2"
kotlinx-browser = "0.3"
kotlinx-browser = "0.5.0"
kslog = "1.4.2"
kslog = "1.6.0"
jb-compose = "1.8.1"
jb-exposed = "0.61.0"
jb-dokka = "2.0.0"
jb-compose = "1.10.1"
jb-compose-material3 = "1.10.0-alpha05"
jb-compose-icons = "1.7.8"
jb-exposed = "1.0.0"
jb-dokka = "2.1.0"
sqlite = "3.50.1.0"
sqlite = "3.51.2.0"
korlibs = "5.4.0"
uuid = "0.8.4"
ktor = "3.1.3"
ktor = "3.4.0"
gh-release = "2.5.2"
koin = "4.0.4"
koin = "4.1.1"
okio = "3.12.0"
okio = "3.16.4"
ksp = "2.1.20-1.0.31"
kotlin-poet = "1.18.1"
ksp = "2.3.6"
kotlin-poet = "2.2.0"
versions = "0.51.0"
nmcp = "0.1.5"
versions = "0.53.0"
nmcp = "1.2.1"
android-gradle = "8.9.+"
android-gradle = "8.12.+"
dexcount = "4.0.0"
android-coreKtx = "1.16.0"
android-coreKtx = "1.17.0"
android-recyclerView = "1.4.0"
android-appCompat = "1.7.1"
android-fragment = "1.8.8"
android-espresso = "3.6.1"
android-test = "1.2.1"
android-fragment = "1.8.9"
android-espresso = "3.7.0"
android-test = "1.3.0"
android-props-minSdk = "21"
android-props-compileSdk = "36"
@@ -88,7 +90,8 @@ jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-
jb-exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "jb-exposed" }
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
jb-compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "jb-compose" }
jb-compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "jb-compose-material3" }
jb-compose-icons = { module = "androidx.compose.material:material-icons-extended", version.ref = "jb-compose-icons" }
android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }

View File

@@ -1,6 +1,9 @@
kotlin {
androidTarget {
publishAllLibraryVariants()
publishLibraryVariants(
"release",
"debug",
)
compilations.all {
kotlinOptions {
jvmTarget = "17"

Binary file not shown.

View File

@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

295
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/usr/bin/env sh
#!/bin/sh
#
# Copyright 2015 the original author or authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,81 +15,114 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,88 +131,118 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

57
gradlew.bat vendored
View File

@@ -13,8 +13,10 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +27,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@@ -54,49 +57,35 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

View File

@@ -10,6 +10,7 @@ repositories {
dependencies {
api project(":micro_utils.koin")
api project(":micro_utils.ksp.generator")
api libs.kotlin.poet
api libs.ksp
}

View File

@@ -26,6 +26,8 @@ import com.squareup.kotlinpoet.asTypeName
import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.toTypeName
import com.squareup.kotlinpoet.ksp.writeTo
import dev.inmo.micro_ksp.generator.safeClassName
import dev.inmo.micro_ksp.generator.withNoSuchElementWorkaround
import dev.inmo.micro_utils.koin.annotations.GenerateGenericKoinDefinition
import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition
import org.koin.core.Koin
@@ -237,19 +239,16 @@ class Processor(
""".trimIndent()
)
ksFile.getAnnotationsByType(GenerateKoinDefinition::class).forEach {
val type = runCatching {
it.type.asTypeName()
}.getOrElse { e ->
if (e is KSTypeNotPresentException) {
e.ksType.toClassName()
} else {
throw e
}
}
val type = safeClassName { it.type }
val targetType = runCatching {
type.parameterizedBy(*(it.typeArgs.takeIf { it.isNotEmpty() } ?.map { it.asTypeName() } ?.toTypedArray() ?: return@runCatching type))
type.parameterizedBy(
*withNoSuchElementWorkaround(emptyArray()) {
it.typeArgs.takeIf { it.isNotEmpty() } ?.map { it.asTypeName() } ?.toTypedArray() ?: return@runCatching type
}
)
}.getOrElse { e ->
when (e) {
is IllegalArgumentException if (e.message ?.contains("no type argument") == true) -> return@getOrElse type
is KSTypeNotPresentException -> e.ksType.toClassName()
}
if (e is KSTypesNotPresentException) {
@@ -258,14 +257,32 @@ class Processor(
throw e
}
}.copy(
nullable = it.nullable
nullable = withNoSuchElementWorkaround(true) { it.nullable }
)
addCodeForType(targetType, it.name, it.nullable, it.generateSingle, it.generateFactory)
addCodeForType(
targetType,
it.name,
withNoSuchElementWorkaround(true) {
it.nullable
},
withNoSuchElementWorkaround(true) {
it.generateSingle
},
withNoSuchElementWorkaround(true) {
it.generateFactory
}
)
}
ksFile.getAnnotationsByType(GenerateGenericKoinDefinition::class).forEach {
val targetType = TypeVariableName("T", Any::class)
addCodeForType(targetType, it.name, it.nullable, it.generateSingle, it.generateFactory)
addCodeForType(
targetType = targetType,
name = it.name,
nullable = withNoSuchElementWorkaround(true) { it.nullable },
generateSingle = withNoSuchElementWorkaround(true) { it.generateSingle },
generateFactory = withNoSuchElementWorkaround(true) { it.generateFactory }
)
}
}.build().let {
File(

View File

@@ -5,7 +5,6 @@ package dev.inmo.micro_utils.koin.generator.test
import kotlin.Any
import kotlin.Boolean
import kotlin.Deprecated
import kotlin.String
import org.koin.core.Koin
import org.koin.core.definition.Definition
@@ -30,95 +29,59 @@ public val Koin.sampleInfo: Test<String>
/**
* @return Definition by key "sampleInfo" with [parameters]
*/
public inline fun Scope.sampleInfo(noinline parameters: ParametersDefinition): Test<String> =
get(named("sampleInfo"), parameters)
public inline fun Scope.sampleInfo(noinline parameters: ParametersDefinition): Test<String> = get(named("sampleInfo"), parameters)
/**
* @return Definition by key "sampleInfo" with [parameters]
*/
public inline fun Koin.sampleInfo(noinline parameters: ParametersDefinition): Test<String> =
get(named("sampleInfo"), parameters)
public inline fun Koin.sampleInfo(noinline parameters: ParametersDefinition): Test<String> = get(named("sampleInfo"), parameters)
/**
* Will register [definition] with [org.koin.core.module.Module.single] and key "sampleInfo"
*/
@Deprecated(
"This definition is old style and should not be used anymore. Use singleSampleInfo instead",
ReplaceWith("singleSampleInfo"),
)
public fun Module.sampleInfoSingle(createdAtStart: Boolean = false,
definition: Definition<Test<String>>): KoinDefinition<Test<String>> =
single(named("sampleInfo"), createdAtStart = createdAtStart, definition = definition)
/**
* Will register [definition] with [org.koin.core.module.Module.single] and key "sampleInfo"
*/
public fun Module.singleSampleInfo(createdAtStart: Boolean = false,
definition: Definition<Test<String>>): KoinDefinition<Test<String>> =
single(named("sampleInfo"), createdAtStart = createdAtStart, definition = definition)
public fun Module.singleSampleInfo(createdAtStart: Boolean = false, definition: Definition<Test<String>>): KoinDefinition<Test<String>> = single(named("sampleInfo"), createdAtStart = createdAtStart, definition = definition)
/**
* Will register [definition] with [org.koin.core.module.Module.factory] and key "sampleInfo"
*/
@Deprecated(
"This definition is old style and should not be used anymore. Use factorySampleInfo instead",
ReplaceWith("factorySampleInfo"),
)
public fun Module.sampleInfoFactory(definition: Definition<Test<String>>):
KoinDefinition<Test<String>> = factory(named("sampleInfo"), definition = definition)
/**
* Will register [definition] with [org.koin.core.module.Module.factory] and key "sampleInfo"
*/
public fun Module.factorySampleInfo(definition: Definition<Test<String>>):
KoinDefinition<Test<String>> = factory(named("sampleInfo"), definition = definition)
public fun Module.factorySampleInfo(definition: Definition<Test<String>>): KoinDefinition<Test<String>> = factory(named("sampleInfo"), definition = definition)
/**
* @return Definition by key "test" with [parameters]
*/
public inline fun <reified T : Any> Scope.test(noinline parameters: ParametersDefinition? = null): T
= get(named("test"), parameters)
public inline fun <reified T : Any> Scope.test(noinline parameters: ParametersDefinition? = null): T = get(named("test"), parameters)
/**
* @return Definition by key "test" with [parameters]
*/
public inline fun <reified T : Any> Koin.test(noinline parameters: ParametersDefinition? = null): T
= get(named("test"), parameters)
public inline fun <reified T : Any> Koin.test(noinline parameters: ParametersDefinition? = null): T = get(named("test"), parameters)
/**
* Will register [definition] with [org.koin.core.module.Module.single] and key "test"
*/
public inline fun <reified T : Any> Module.singleTest(createdAtStart: Boolean = false, noinline
definition: Definition<T>): KoinDefinition<T> = single(named("test"), createdAtStart =
createdAtStart, definition = definition)
public inline fun <reified T : Any> Module.singleTest(createdAtStart: Boolean = false, noinline definition: Definition<T>): KoinDefinition<T> = single(named("test"), createdAtStart = createdAtStart, definition = definition)
/**
* Will register [definition] with [org.koin.core.module.Module.factory] and key "test"
*/
public inline fun <reified T : Any> Module.factoryTest(noinline definition: Definition<T>):
KoinDefinition<T> = factory(named("test"), definition = definition)
public inline fun <reified T : Any> Module.factoryTest(noinline definition: Definition<T>): KoinDefinition<T> = factory(named("test"), definition = definition)
/**
* @return Definition by key "testNullable" with [parameters]
*/
public inline fun <reified T : Any> Scope.testNullable(noinline parameters: ParametersDefinition? =
null): T? = getOrNull(named("testNullable"), parameters)
public inline fun <reified T : Any> Scope.testNullable(noinline parameters: ParametersDefinition? = null): T? = getOrNull(named("testNullable"), parameters)
/**
* @return Definition by key "testNullable" with [parameters]
*/
public inline fun <reified T : Any> Koin.testNullable(noinline parameters: ParametersDefinition? =
null): T? = getOrNull(named("testNullable"), parameters)
public inline fun <reified T : Any> Koin.testNullable(noinline parameters: ParametersDefinition? = null): T? = getOrNull(named("testNullable"), parameters)
/**
* Will register [definition] with [org.koin.core.module.Module.single] and key "testNullable"
*/
public inline fun <reified T : Any> Module.singleTestNullable(createdAtStart: Boolean = false,
noinline definition: Definition<T>): KoinDefinition<T> = single(named("testNullable"),
createdAtStart = createdAtStart, definition = definition)
public inline fun <reified T : Any> Module.singleTestNullable(createdAtStart: Boolean = false, noinline definition: Definition<T>): KoinDefinition<T> = single(named("testNullable"), createdAtStart = createdAtStart, definition = definition)
/**
* Will register [definition] with [org.koin.core.module.Module.factory] and key "testNullable"
*/
public inline fun <reified T : Any> Module.factoryTestNullable(noinline definition: Definition<T>):
KoinDefinition<T> = factory(named("testNullable"), definition = definition)
public inline fun <reified T : Any> Module.factoryTestNullable(noinline definition: Definition<T>): KoinDefinition<T> = factory(named("testNullable"), definition = definition)

View File

@@ -10,6 +10,7 @@ import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.symbol.*
import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ksp.toClassName
import dev.inmo.micro_ksp.generator.withNoSuchElementWorkaround
import dev.inmo.micro_ksp.generator.writeFile
import dev.inmo.micro_utils.ksp.classcasts.ClassCastsExcluded
import dev.inmo.micro_utils.ksp.classcasts.ClassCastsIncluded
@@ -25,7 +26,11 @@ class Processor(
) {
val rootAnnotation = ksClassDeclaration.getAnnotationsByType(ClassCastsIncluded::class).first()
val (includeRegex: Regex?, excludeRegex: Regex?) = rootAnnotation.let {
it.typesRegex.takeIf { it.isNotEmpty() } ?.let(::Regex) to it.excludeRegex.takeIf { it.isNotEmpty() } ?.let(::Regex)
withNoSuchElementWorkaround("") {
it.typesRegex
}.takeIf { it.isNotEmpty() } ?.let(::Regex) to withNoSuchElementWorkaround("") {
it.excludeRegex
}.takeIf { it.isNotEmpty() } ?.let(::Regex)
}
val classesSubtypes = mutableMapOf<KSClassDeclaration, MutableSet<KSClassDeclaration>>()
@@ -49,7 +54,9 @@ class Processor(
when {
potentialSubtype === ksClassDeclaration -> {}
potentialSubtype.isAnnotationPresent(ClassCastsExcluded::class) -> return@forEach
potentialSubtype !is KSClassDeclaration || !potentialSubtype.checkSupertypeLevel(rootAnnotation.levelsToInclude.takeIf { it >= 0 }) -> return@forEach
potentialSubtype !is KSClassDeclaration || !potentialSubtype.checkSupertypeLevel(
withNoSuchElementWorkaround(-1) { rootAnnotation.levelsToInclude }.takeIf { it >= 0 }
) -> return@forEach
excludeRegex ?.matches(simpleName) == true -> return@forEach
includeRegex ?.matches(simpleName) == false -> {}
else -> classesSubtypes.getOrPut(ksClassDeclaration) { mutableSetOf() }.add(potentialSubtype)
@@ -96,7 +103,9 @@ class Processor(
@OptIn(KspExperimental::class)
override fun process(resolver: Resolver): List<KSAnnotated> {
(resolver.getSymbolsWithAnnotation(ClassCastsIncluded::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach {
val prefix = it.getAnnotationsByType(ClassCastsIncluded::class).first().outputFilePrefix
val prefix = withNoSuchElementWorkaround("") {
it.getAnnotationsByType(ClassCastsIncluded::class).first().outputFilePrefix
}
it.writeFile(prefix = prefix, suffix = "ClassCasts") {
FileSpec.builder(
it.packageName.asString(),

View File

@@ -0,0 +1,34 @@
package dev.inmo.micro_ksp.generator
import com.google.devtools.ksp.KSTypeNotPresentException
import com.google.devtools.ksp.KspExperimental
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.asTypeName
import kotlin.reflect.KClass
/**
* Safely retrieves a [ClassName] from a [KClass] getter, handling cases where the type is not
* yet available during annotation processing (KSP).
*
* When [KSTypeNotPresentException] is caught, it extracts the class name information from the
* exception's [com.google.devtools.ksp.symbol.KSType] to construct a [ClassName].
*
* @param classnameGetter A lambda that returns the [KClass] to convert
* @return A KotlinPoet [ClassName] representing the class
* @throws Throwable If an exception other than [KSTypeNotPresentException] occurs
*/
@Suppress("NOTHING_TO_INLINE")
@OptIn(KspExperimental::class)
inline fun safeClassName(classnameGetter: () -> KClass<*>) = runCatching {
classnameGetter().asTypeName()
}.getOrElse { e ->
if (e is KSTypeNotPresentException) {
ClassName(
e.ksType.declaration.packageName.asString(),
e.ksType.declaration.qualifiedName ?.asString() ?.replaceFirst(e.ksType.declaration.packageName.asString(), "")
?: e.ksType.declaration.simpleName.asString()
)
} else {
throw e
}
}

View File

@@ -0,0 +1,12 @@
package dev.inmo.micro_ksp.generator
inline fun <T> withNoSuchElementWorkaround(
default: T,
block: () -> T
): T = runCatching(block).getOrElse {
if (it is NoSuchElementException) {
default
} else {
throw it
}
}

View File

@@ -2,6 +2,13 @@ package dev.inmo.micro_ksp.generator
import com.google.devtools.ksp.symbol.KSClassDeclaration
/**
* Recursively resolves all subclasses of this sealed class declaration.
* For sealed classes, it traverses the entire hierarchy of sealed subclasses.
* For non-sealed classes (leaf nodes), it returns the class itself.
*
* @return A list of all concrete (non-sealed) subclass declarations in the hierarchy
*/
fun KSClassDeclaration.resolveSubclasses(): List<KSClassDeclaration> {
return (getSealedSubclasses().flatMap {
it.resolveSubclasses()

View File

@@ -12,6 +12,7 @@ import com.squareup.kotlinpoet.ksp.toClassName
import dev.inmo.micro_ksp.generator.buildSubFileName
import dev.inmo.micro_ksp.generator.companion
import dev.inmo.micro_ksp.generator.findSubClasses
import dev.inmo.micro_ksp.generator.withNoSuchElementWorkaround
import dev.inmo.micro_ksp.generator.writeFile
import dev.inmo.micro_utils.ksp.sealed.GenerateSealedTypesWorkaround
import dev.inmo.micro_utils.ksp.sealed.GenerateSealedWorkaround
@@ -113,7 +114,7 @@ class Processor(
val annotation = ksClassDeclaration.getGenerateSealedTypesWorkaroundAnnotation
val subClasses = ksClassDeclaration.resolveSubclasses(
searchIn = resolver.getAllFiles(),
allowNonSealed = annotation ?.includeNonSealedSubTypes ?: false
allowNonSealed = withNoSuchElementWorkaround(null) { annotation ?.includeNonSealedSubTypes } ?: false
).distinct()
val subClassesNames = subClasses.filter {
it.getAnnotationsByType(GenerateSealedTypesWorkaround.Exclude::class).count() == 0
@@ -164,7 +165,15 @@ class Processor(
@OptIn(KspExperimental::class)
override fun process(resolver: Resolver): List<KSAnnotated> {
(resolver.getSymbolsWithAnnotation(GenerateSealedWorkaround::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach {
val prefix = (it.getGenerateSealedWorkaroundAnnotation) ?.prefix ?.takeIf {
val prefix = runCatching {
(it.getGenerateSealedWorkaroundAnnotation) ?.prefix
}.getOrElse {
if (it is NoSuchElementException) {
""
} else {
throw it
}
} ?.takeIf {
it.isNotEmpty()
} ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "")
it.writeFile(prefix = prefix, suffix = "SealedWorkaround") {
@@ -184,7 +193,9 @@ class Processor(
}
}
(resolver.getSymbolsWithAnnotation(GenerateSealedTypesWorkaround::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach {
val prefix = (it.getGenerateSealedTypesWorkaroundAnnotation) ?.prefix ?.takeIf {
val prefix = withNoSuchElementWorkaround("") {
(it.getGenerateSealedTypesWorkaroundAnnotation)?.prefix
} ?.takeIf {
it.isNotEmpty()
} ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "")
it.writeFile(prefix = prefix, suffix = "SealedTypesWorkaround") {

View File

@@ -16,6 +16,7 @@ import com.squareup.kotlinpoet.ksp.toTypeName
import dev.inmo.micro_ksp.generator.convertToClassName
import dev.inmo.micro_ksp.generator.convertToClassNames
import dev.inmo.micro_ksp.generator.findSubClasses
import dev.inmo.micro_ksp.generator.withNoSuchElementWorkaround
import dev.inmo.micro_ksp.generator.writeFile
import dev.inmo.micro_utils.ksp.variations.GenerateVariations
import dev.inmo.micro_utils.ksp.variations.GenerationVariant
@@ -218,7 +219,9 @@ class Processor(
@OptIn(KspExperimental::class)
override fun process(resolver: Resolver): List<KSAnnotated> {
(resolver.getSymbolsWithAnnotation(GenerateVariations::class.qualifiedName!!)).filterIsInstance<KSFunctionDeclaration>().forEach {
val prefix = (it.getAnnotationsByType(GenerateVariations::class)).firstOrNull() ?.prefix ?.takeIf {
val prefix = withNoSuchElementWorkaround("") {
(it.getAnnotationsByType(GenerateVariations::class)).firstOrNull() ?.prefix
} ?.takeIf {
it.isNotEmpty()
} ?: it.simpleName.asString().replaceFirst(it.simpleName.asString(), "")
it.writeFile(prefix = prefix, suffix = "GeneratedVariation") {

View File

@@ -4,10 +4,25 @@ import io.ktor.client.call.body
import io.ktor.client.statement.HttpResponse
import io.ktor.http.HttpStatusCode
/**
* Returns the response body as type [T] if the [statusFilter] condition is met, otherwise returns null.
* By default, the filter checks if the status code is [HttpStatusCode.OK].
*
* @param T The type to deserialize the response body to
* @param statusFilter A predicate to determine if the body should be retrieved. Defaults to checking for OK status
* @return The deserialized body of type [T], or null if the filter condition is not met
*/
suspend inline fun <reified T : Any> HttpResponse.bodyOrNull(
statusFilter: (HttpResponse) -> Boolean = { it.status == HttpStatusCode.OK }
) = takeIf(statusFilter) ?.body<T>()
/**
* Returns the response body as type [T] if the status code is not [HttpStatusCode.NoContent], otherwise returns null.
* This is useful for handling responses that may return 204 No Content.
*
* @param T The type to deserialize the response body to
* @return The deserialized body of type [T], or null if the status is No Content
*/
suspend inline fun <reified T : Any> HttpResponse.bodyOrNullOnNoContent() = bodyOrNull<T> {
it.status != HttpStatusCode.NoContent
}

View File

@@ -4,6 +4,13 @@ import io.ktor.client.plugins.ClientRequestException
import io.ktor.client.statement.HttpResponse
import io.ktor.http.isSuccess
/**
* Throws a [ClientRequestException] if this [HttpResponse] does not have a successful status code.
* A status code is considered successful if it's in the 2xx range.
*
* @param unsuccessMessage A lambda that provides the error message to use if the response is unsuccessful
* @throws ClientRequestException if the response status is not successful
*/
inline fun HttpResponse.throwOnUnsuccess(
unsuccessMessage: () -> String
) {

View File

@@ -5,6 +5,12 @@ import dev.inmo.micro_utils.common.filesize
import dev.inmo.micro_utils.ktor.common.input
import io.ktor.client.request.forms.InputProvider
/**
* Creates a Ktor [InputProvider] from this multiplatform file for use in HTTP client requests.
* The input provider knows the file size and can create input streams on demand.
*
* @return An [InputProvider] for reading this file in HTTP requests
*/
fun MPPFile.inputProvider(): InputProvider = InputProvider(filesize) {
input()
}

View File

@@ -1,3 +1,9 @@
package dev.inmo.micro_utils.ktor.client
/**
* A callback function type for tracking upload progress.
*
* @param uploaded The number of bytes uploaded so far
* @param count The total number of bytes to be uploaded
*/
typealias OnUploadCallback = suspend (uploaded: Long, count: Long) -> Unit

View File

@@ -5,6 +5,15 @@ import dev.inmo.micro_utils.ktor.common.*
import io.ktor.client.HttpClient
import io.ktor.client.content.*
/**
* Uploads a file to a temporary storage on the server.
* The server should provide an endpoint that accepts multipart uploads and returns a [TemporalFileId].
*
* @param fullTempUploadDraftPath The full URL path to the temporary upload endpoint
* @param file The file to upload
* @param onUpload Progress callback invoked during upload
* @return A [TemporalFileId] that can be used to reference the uploaded file
*/
expect suspend fun HttpClient.tempUpload(
fullTempUploadDraftPath: String,
file: MPPFile,

View File

@@ -10,6 +10,14 @@ import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.StringFormat
import kotlinx.serialization.json.Json
/**
* Information about a file to upload in a multipart request.
* This allows uploading from custom sources beyond regular files.
*
* @param fileName The name of the file
* @param mimeType The MIME type of the file
* @param inputAllocator A lambda that provides input streams for reading the file data
*/
data class UniUploadFileInfo(
val fileName: FileName,
val mimeType: String,

View File

@@ -1,5 +1,14 @@
package dev.inmo.micro_utils.ktor.common
/**
* Builds a standard URL by combining a base part, subpart, and optional query parameters.
* The base and subpart are joined with a '/', and query parameters are appended.
*
* @param basePart The base part of the URL (e.g., "https://example.com/api")
* @param subpart The subpart of the URL (e.g., "users")
* @param parameters Query parameters as a map. Defaults to an empty map
* @return The complete URL string with query parameters
*/
fun buildStandardUrl(
basePart: String,
subpart: String,
@@ -8,6 +17,15 @@ fun buildStandardUrl(
parameters
)
/**
* Builds a standard URL by combining a base part, subpart, and query parameters as a list.
* The base and subpart are joined with a '/', and query parameters are appended.
*
* @param basePart The base part of the URL (e.g., "https://example.com/api")
* @param subpart The subpart of the URL (e.g., "users")
* @param parameters Query parameters as a list of key-value pairs
* @return The complete URL string with query parameters
*/
fun buildStandardUrl(
basePart: String,
subpart: String,
@@ -16,6 +34,15 @@ fun buildStandardUrl(
parameters
)
/**
* Builds a standard URL by combining a base part, subpart, and vararg query parameters.
* The base and subpart are joined with a '/', and query parameters are appended.
*
* @param basePart The base part of the URL (e.g., "https://example.com/api")
* @param subpart The subpart of the URL (e.g., "users")
* @param parameters Query parameters as vararg key-value pairs
* @return The complete URL string with query parameters
*/
fun buildStandardUrl(
basePart: String,
subpart: String,

View File

@@ -1,3 +1,8 @@
package dev.inmo.micro_utils.ktor.common
/**
* An exception used to indicate a correct/normal closure of a connection or stream.
* This is typically used in WebSocket or network communication scenarios where a
* clean shutdown needs to be distinguished from error conditions.
*/
object CorrectCloseException : Exception()

View File

@@ -2,6 +2,14 @@ package dev.inmo.micro_utils.ktor.common
private val schemaRegex = Regex("^[^:]*://")
/**
* Converts this string to a correct WebSocket URL by ensuring it starts with "ws://" scheme.
* If the URL already starts with "ws", it is returned unchanged.
* If the URL contains a scheme (e.g., "http://"), it is replaced with "ws://".
* If the URL has no scheme, "ws://" is prepended.
*
* @return A properly formatted WebSocket URL
*/
val String.asCorrectWebSocketUrl: String
get() = if (startsWith("ws")) {
this

View File

@@ -2,14 +2,30 @@ package dev.inmo.micro_utils.ktor.common
import korlibs.time.DateTime
/**
* Type alias representing a date-time range with optional start and end times.
* First element is the "from" date-time, second is the "to" date-time.
*/
typealias FromToDateTime = Pair<DateTime?, DateTime?>
/**
* Converts this [FromToDateTime] range to URL query parameters.
* Creates "from" and "to" query parameters with Unix millisecond timestamps.
*
* @return A map of query parameters representing the date-time range
*/
val FromToDateTime.asFromToUrlPart: QueryParams
get() = mapOf(
"from" to first ?.unixMillis ?.toString(),
"to" to second ?.unixMillis ?.toString()
)
/**
* Extracts a [FromToDateTime] range from URL query parameters.
* Looks for "from" and "to" parameters containing Unix millisecond timestamps.
*
* @return A [FromToDateTime] pair extracted from the query parameters
*/
val QueryParams.extractFromToDateTime: FromToDateTime
get() = FromToDateTime(
get("from") ?.toDoubleOrNull() ?.let { DateTime(it) },

View File

@@ -2,4 +2,8 @@ package dev.inmo.micro_utils.ktor.common
import io.ktor.utils.io.core.Input
/**
* A function type that provides an [Input] instance.
* This is useful for lazy or deferred input creation in Ktor operations.
*/
typealias LambdaInputProvider = () -> Input

View File

@@ -3,4 +3,10 @@ package dev.inmo.micro_utils.ktor.common
import dev.inmo.micro_utils.common.MPPFile
import io.ktor.utils.io.core.Input
/**
* Creates a Ktor [Input] from this multiplatform file.
* Platform-specific implementations handle file reading for each supported platform.
*
* @return An [Input] stream for reading this file
*/
expect fun MPPFile.input(): Input

View File

@@ -29,6 +29,13 @@ fun String.includeQueryParams(
queryParams: List<QueryParam>
): String = "$this${if (contains("?")) "&" else "?"}${queryParams.asUrlQuery}"
/**
* Parses this URL query string into a [QueryParams] map.
* Splits on '&' to separate parameters and '=' to separate keys from values.
* Parameters without values will have null as their value.
*
* @return A map of query parameter keys to their values (or null if no value)
*/
val String.parseUrlQuery: QueryParams
get() = split("&").map {
it.split("=").let { pair ->

View File

@@ -5,27 +5,73 @@ package dev.inmo.micro_utils.ktor.common
import kotlinx.serialization.*
import kotlinx.serialization.cbor.Cbor
/**
* Type alias for the standard serialization format used in Ktor utilities, which is [BinaryFormat].
*/
typealias StandardKtorSerialFormat = BinaryFormat
/**
* Type alias for the standard serialization input data type, which is [ByteArray].
*/
typealias StandardKtorSerialInputData = ByteArray
/**
* The standard Ktor serialization format instance, configured as CBOR.
*/
val standardKtorSerialFormat: StandardKtorSerialFormat = Cbor { }
/**
* Decodes data from [StandardKtorSerialInputData] using the standard format.
*
* @param T The type to decode to
* @param deserializationStrategy The deserialization strategy for type [T]
* @param input The byte array input data to decode
* @return The decoded value of type [T]
*/
inline fun <T> StandardKtorSerialFormat.decodeDefault(
deserializationStrategy: DeserializationStrategy<T>,
input: StandardKtorSerialInputData
): T = decodeFromByteArray(deserializationStrategy, input)
/**
* Encodes data to [StandardKtorSerialInputData] using the standard format.
*
* @param T The type to encode
* @param serializationStrategy The serialization strategy for type [T]
* @param data The data to encode
* @return The encoded byte array
*/
inline fun <T> StandardKtorSerialFormat.encodeDefault(
serializationStrategy: SerializationStrategy<T>,
data: T
): StandardKtorSerialInputData = encodeToByteArray(serializationStrategy, data)
/**
* A CBOR instance for serialization operations.
*/
val cbor = Cbor {}
/**
* Decodes data from a hex string using the standard binary format.
*
* @param T The type to decode to
* @param deserializationStrategy The deserialization strategy for type [T]
* @param input The hex string to decode
* @return The decoded value of type [T]
*/
inline fun <T> StandardKtorSerialFormat.decodeHex(
deserializationStrategy: DeserializationStrategy<T>,
input: String
): T = decodeFromHexString(deserializationStrategy, input)
/**
* Encodes data to a hex string using the standard binary format.
*
* @param T The type to encode
* @param serializationStrategy The serialization strategy for type [T]
* @param data The data to encode
* @return The encoded hex string
*/
inline fun <T> StandardKtorSerialFormat.encodeHex(
serializationStrategy: SerializationStrategy<T>,
data: T

View File

@@ -3,8 +3,17 @@ package dev.inmo.micro_utils.ktor.common
import kotlin.jvm.JvmInline
import kotlinx.serialization.Serializable
/**
* The default subdirectory path for storing temporal files during upload operations.
*/
const val DefaultTemporalFilesSubPath = "temp_upload"
/**
* A value class representing a unique identifier for a temporal file.
* Temporal files are typically used for temporary storage during file upload/processing operations.
*
* @param string The string representation of the temporal file ID
*/
@Serializable
@JvmInline
value class TemporalFileId(val string: String)

View File

@@ -4,6 +4,12 @@ import korlibs.time.DateTime
import dev.inmo.micro_utils.ktor.common.FromToDateTime
import io.ktor.http.Parameters
/**
* Extracts a [FromToDateTime] range from Ktor server [Parameters].
* Looks for "from" and "to" parameters containing Unix millisecond timestamps.
*
* @return A [FromToDateTime] pair extracted from the parameters
*/
val Parameters.extractFromToDateTime: FromToDateTime
get() = FromToDateTime(
get("from") ?.toDoubleOrNull() ?.let { DateTime(it) },

View File

@@ -4,6 +4,13 @@ import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall
import io.ktor.server.response.respond
/**
* Retrieves a parameter value by [field] name from the request parameters.
* If the parameter is not present, responds with [HttpStatusCode.BadRequest] (400) and an error message.
*
* @param field The name of the parameter to retrieve
* @return The parameter value, or null if not found (after sending error response)
*/
suspend fun ApplicationCall.getParameterOrSendError(
field: String
) = parameters[field].also {
@@ -12,6 +19,13 @@ suspend fun ApplicationCall.getParameterOrSendError(
}
}
/**
* Retrieves all parameter values by [field] name from the request parameters.
* If the parameter is not present, responds with [HttpStatusCode.BadRequest] (400) and an error message.
*
* @param field The name of the parameter to retrieve
* @return A list of parameter values, or null if not found (after sending error response)
*/
suspend fun ApplicationCall.getParametersOrSendError(
field: String
) = parameters.getAll(field).also {
@@ -20,14 +34,33 @@ suspend fun ApplicationCall.getParametersOrSendError(
}
}
/**
* Retrieves a query parameter value by [field] name from the request.
*
* @param field The name of the query parameter to retrieve
* @return The query parameter value, or null if not found
*/
fun ApplicationCall.getQueryParameter(
field: String
) = request.queryParameters[field]
/**
* Retrieves all query parameter values by [field] name from the request.
*
* @param field The name of the query parameter to retrieve
* @return A list of query parameter values, or null if not found
*/
fun ApplicationCall.getQueryParameters(
field: String
) = request.queryParameters.getAll(field)
/**
* Retrieves a query parameter value by [field] name from the request.
* If the parameter is not present, responds with [HttpStatusCode.BadRequest] (400) and an error message.
*
* @param field The name of the query parameter to retrieve
* @return The query parameter value, or null if not found (after sending error response)
*/
suspend fun ApplicationCall.getQueryParameterOrSendError(
field: String
) = getQueryParameter(field).also {
@@ -36,6 +69,13 @@ suspend fun ApplicationCall.getQueryParameterOrSendError(
}
}
/**
* Retrieves all query parameter values by [field] name from the request.
* If the parameter is not present, responds with [HttpStatusCode.BadRequest] (400) and an error message.
*
* @param field The name of the query parameter to retrieve
* @return A list of query parameter values, or null if not found (after sending error response)
*/
suspend fun ApplicationCall.getQueryParametersOrSendError(
field: String
) = getQueryParameters(field).also {

View File

@@ -4,6 +4,13 @@ import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall
import io.ktor.server.response.respond
/**
* Responds with the given [data] if it's not null, or responds with [HttpStatusCode.NoContent] (204) if it's null.
* This is useful for API endpoints that may return empty results.
*
* @param T The type of data to respond with
* @param data The data to respond with, or null to respond with No Content
*/
suspend inline fun <reified T : Any> ApplicationCall.respondOrNoContent(
data: T?
) {

View File

@@ -23,7 +23,9 @@ dependencies {
implementation libs.ktor.client.java
}
mainClassName="MainKt"
application {
mainClass = "MainKt"
}
java {
sourceCompatibility = JavaVersion.VERSION_17

View File

@@ -5,6 +5,10 @@ import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
/**
* Serializer for [IetfLang] that serializes language codes as their string representation.
* The language code is serialized as a simple string (e.g., "en-US", "fr", "de-DE").
*/
object IetfLangSerializer : KSerializer<IetfLang> {
override val descriptor = String.serializer().descriptor

View File

@@ -1,3 +1,9 @@
package dev.inmo.micro_utils.matrix
/**
* Represents a matrix as a list of rows, where each row is a list of elements.
* This is essentially a 2D structure represented as `List<List<T>>`.
*
* @param T The type of elements in the matrix
*/
typealias Matrix<T> = List<Row<T>>

View File

@@ -1,15 +1,37 @@
package dev.inmo.micro_utils.matrix
/**
* Creates a matrix using a DSL-style builder.
* Allows defining multiple rows using the [MatrixBuilder] API.
*
* @param T The type of elements in the matrix
* @param block A builder lambda to define the matrix structure
* @return A constructed [Matrix]
*/
fun <T> matrix(block: MatrixBuilder<T>.() -> Unit): Matrix<T> {
return MatrixBuilder<T>().also(block).matrix
}
/**
* Creates a single-row matrix using a DSL-style builder.
*
* @param T The type of elements in the matrix
* @param block A builder lambda to define the row elements
* @return A [Matrix] containing a single row
*/
fun <T> flatMatrix(block: RowBuilder<T>.() -> Unit): Matrix<T> {
return MatrixBuilder<T>().apply {
row(block)
}.matrix
}
/**
* Creates a single-row matrix from the provided elements.
*
* @param T The type of elements in the matrix
* @param elements The elements to include in the single row
* @return A [Matrix] containing a single row with the specified elements
*/
fun <T> flatMatrix(vararg elements: T): Matrix<T> {
return MatrixBuilder<T>().apply {
row { elements.forEach { +it } }

View File

@@ -1,3 +1,8 @@
package dev.inmo.micro_utils.matrix
/**
* Represents a single row in a matrix as a list of elements.
*
* @param T The type of elements in the row
*/
typealias Row<T> = List<T>

View File

@@ -1,3 +1,9 @@
package dev.inmo.micro_utils.mime_types
/**
* A custom implementation of [MimeType] that wraps a raw MIME type string.
* Use this when you need to work with MIME types that aren't defined in the standard set.
*
* @param raw The raw MIME type string (e.g., "application/custom", "text/x-custom")
*/
data class CustomMimeType(override val raw: String) : MimeType

View File

@@ -2,9 +2,24 @@ package dev.inmo.micro_utils.mime_types
import kotlinx.serialization.Serializable
/**
* Represents a MIME type (Multipurpose Internet Mail Extensions type).
* A MIME type is a standard way to indicate the nature and format of a document, file, or assortment of bytes.
*
* Examples: "text/html", "application/json", "image/png"
*/
@Serializable(MimeTypeSerializer::class)
interface MimeType {
/**
* The raw MIME type string (e.g., "text/html", "application/json").
*/
val raw: String
/**
* An array of file extensions commonly associated with this MIME type.
* For example, "text/html" might have extensions ["html", "htm"].
* Returns an empty array by default if no extensions are known.
*/
val extensions: Array<String>
get() = emptyArray()
}

Some files were not shown because too many files have changed in this diff Show More