Compare commits

...

46 Commits

Author SHA1 Message Date
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
136 changed files with 3503 additions and 312 deletions

View File

@@ -1,5 +1,109 @@
# Changelog # 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 ## 0.26.1
* `Versions`: * `Versions`:

View File

@@ -1,3 +1,6 @@
/**
* Utility functions for creating Android AlertDialogs with simplified API.
*/
@file:Suppress("NOTHING_TO_INLINE", "unused") @file:Suppress("NOTHING_TO_INLINE", "unused")
package dev.inmo.micro_utils.android.alerts.common package dev.inmo.micro_utils.android.alerts.common
@@ -6,8 +9,21 @@ import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
/**
* Type alias for alert dialog button callbacks.
*/
typealias AlertDialogCallback = (DialogInterface) -> Unit 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( inline fun Context.createAlertDialogTemplate(
title: String? = null, title: String? = null,
positivePair: Pair<String, AlertDialogCallback?>? = null, positivePair: Pair<String, AlertDialogCallback?>? = null,

View File

@@ -13,6 +13,7 @@ kotlin {
androidMain { androidMain {
dependencies { dependencies {
api project(":micro_utils.android.smalltextfield") 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.* 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( internal suspend fun Animatable<Float, AnimationVector1D>.fling(
initialVelocity: Float, initialVelocity: Float,
animationSpec: DecayAnimationSpec<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 // 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) @OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
@Composable @Composable
fun NumberPicker( fun NumberPicker(

View File

@@ -22,6 +22,18 @@ import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.* 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) @OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
@Composable @Composable
fun <T> SetPicker( fun <T> SetPicker(

View File

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

View File

@@ -1,5 +1,15 @@
package dev.inmo.micro_utils.common 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>> { fun <T> List<T>.breakAsPairs(): List<Pair<T, T>> {
val result = mutableListOf<Pair<T, T>>() val result = mutableListOf<Pair<T, T>>()

View File

@@ -1,5 +1,12 @@
package dev.inmo.micro_utils.common 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? { inline fun <T> Boolean.letIfTrue(block: () -> T): T? {
return if (this) { return if (this) {
block() 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? { inline fun <T> Boolean.letIfFalse(block: () -> T): T? {
return if (this) { return if (this) {
null 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 { inline fun Boolean.alsoIfTrue(block: () -> Unit): Boolean {
letIfTrue(block) letIfTrue(block)
return this 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 { inline fun Boolean.alsoIfFalse(block: () -> Unit): Boolean {
letIfFalse(block) letIfFalse(block)
return this 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? { inline fun <T> Boolean.ifTrue(block: () -> T): T? {
return if (this) { return if (this) {
block() 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? { inline fun <T> Boolean.ifFalse(block: () -> T): T? {
return if (this) { return if (this) {
null null

View File

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

View File

@@ -1,3 +1,10 @@
package dev.inmo.micro_utils.common 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 }!! fun <T> Iterable<T?>.firstNotNull() = first { it != null }!!

View File

@@ -1,5 +1,19 @@
package dev.inmo.micro_utils.common 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( inline fun <I, R> Iterable<I>.joinTo(
separatorFun: (I) -> R?, separatorFun: (I) -> R?,
prefix: R? = null, prefix: R? = null,
@@ -25,6 +39,20 @@ inline fun <I, R> Iterable<I>.joinTo(
return result 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( inline fun <I, R> Iterable<I>.joinTo(
separator: R? = null, separator: R? = null,
prefix: R? = null, prefix: R? = null,
@@ -32,18 +60,55 @@ inline fun <I, R> Iterable<I>.joinTo(
transform: (I) -> R? transform: (I) -> R?
): List<R> = joinTo({ separator }, prefix, postfix, transform) ): 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( inline fun <I> Iterable<I>.joinTo(
separatorFun: (I) -> I?, separatorFun: (I) -> I?,
prefix: I? = null, prefix: I? = null,
postfix: I? = null postfix: I? = null
): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it } ): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it }
/**
* 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( inline fun <I> Iterable<I>.joinTo(
separator: I? = null, separator: I? = null,
prefix: I? = null, prefix: I? = null,
postfix: I? = null postfix: I? = null
): List<I> = joinTo<I>({ separator }, prefix, postfix) ): 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( inline fun <I, reified R> Array<I>.joinTo(
separatorFun: (I) -> R?, separatorFun: (I) -> R?,
prefix: R? = null, prefix: R? = null,
@@ -51,6 +116,20 @@ inline fun <I, reified R> Array<I>.joinTo(
transform: (I) -> R? transform: (I) -> R?
): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray() ): 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( inline fun <I, reified R> Array<I>.joinTo(
separator: R? = null, separator: R? = null,
prefix: R? = null, prefix: R? = null,

View File

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

View File

@@ -2,16 +2,43 @@ package dev.inmo.micro_utils.common
import kotlin.jvm.JvmName 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> { interface SimpleMapper<T1, T2> {
fun convertToT1(from: T2): T1 fun convertToT1(from: T2): T1
fun convertToT2(from: T1): T2 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") @JvmName("convertFromT2")
fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T2) = convertToT1(from) 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") @JvmName("convertFromT1")
fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T1) = convertToT2(from) 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>( class SimpleMapperImpl<T1, T2>(
private val t1: (T2) -> T1, private val t1: (T2) -> T1,
private val t2: (T1) -> T2, private val t2: (T1) -> T2,
@@ -21,22 +48,58 @@ class SimpleMapperImpl<T1, T2>(
override fun convertToT2(from: T1): T2 = t2.invoke(from) 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") @Suppress("NOTHING_TO_INLINE")
inline fun <T1, T2> simpleMapper( inline fun <T1, T2> simpleMapper(
noinline t1: (T2) -> T1, noinline t1: (T2) -> T1,
noinline t2: (T1) -> T2, noinline t2: (T1) -> T2,
) = SimpleMapperImpl(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> { interface SimpleSuspendableMapper<T1, T2> {
suspend fun convertToT1(from: T2): T1 suspend fun convertToT1(from: T2): T1
suspend fun convertToT2(from: T1): T2 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") @JvmName("convertFromT2")
suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T2) = convertToT1(from) 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") @JvmName("convertFromT1")
suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T1) = convertToT2(from) 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>( class SimpleSuspendableMapperImpl<T1, T2>(
private val t1: suspend (T2) -> T1, private val t1: suspend (T2) -> T1,
private val t2: suspend (T1) -> T2, private val t2: suspend (T1) -> T2,
@@ -46,6 +109,15 @@ class SimpleSuspendableMapperImpl<T1, T2>(
override suspend fun convertToT2(from: T1): T2 = t2.invoke(from) 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") @Suppress("NOTHING_TO_INLINE")
inline fun <T1, T2> simpleSuspendableMapper( inline fun <T1, T2> simpleSuspendableMapper(
noinline t1: suspend (T2) -> T1, noinline t1: suspend (T2) -> T1,

View File

@@ -1,5 +1,14 @@
package dev.inmo.micro_utils.common 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> { inline fun <T> Sequence<T>.padWith(size: Int, inserter: (Sequence<T>) -> Sequence<T>): Sequence<T> {
var result = this var result = this
while (result.count() < size) { while (result.count() < size) {
@@ -8,10 +17,36 @@ inline fun <T> Sequence<T>.padWith(size: Int, inserter: (Sequence<T>) -> Sequenc
return result 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()) } 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 } 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> { inline fun <T> List<T>.padWith(size: Int, inserter: (List<T>) -> List<T>): List<T> {
var result = this var result = this
while (result.size < size) { while (result.size < size) {
@@ -19,14 +54,66 @@ inline fun <T> List<T>.padWith(size: Int, inserter: (List<T>) -> List<T>): List<
} }
return result 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() 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() 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 } 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 } 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() 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() 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 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 { 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.start && endInclusive == other.endInclusive -> start to endInclusive
start > other.endInclusive || other.start > endInclusive -> null start > other.endInclusive || other.start > endInclusive -> null
else -> maxOf(start, other.start) to minOf(endInclusive, other.endInclusive) 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( fun IntRange.intersect(
other: IntRange other: IntRange
): IntRange? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let { ): IntRange? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let {
it.first .. it.second 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( fun LongRange.intersect(
other: LongRange other: LongRange
): LongRange? = (this as ClosedRange<Long>).intersect(other as ClosedRange<Long>) ?.let { ): LongRange? = (this as ClosedRange<Long>).intersect(other as ClosedRange<Long>) ?.let {

View File

@@ -1,5 +1,24 @@
package dev.inmo.micro_utils.common 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) 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) 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 actual val MPPFile.filename: FileName
get() = FileName(name) get() = FileName(name)
actual val MPPFilePathSeparator: Char
get() = '/'
/** /**
* @suppress * @suppress
*/ */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,18 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.consumeAsFlow 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( fun <T> CoroutineScope.actorAsync(
channelCapacity: Int = Channel.UNLIMITED, channelCapacity: Int = Channel.UNLIMITED,
markerFactory: suspend (T) -> Any? = { null }, markerFactory: suspend (T) -> Any? = { null },

View File

@@ -3,5 +3,12 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred 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> val <T> T.asDeferred: Deferred<T>
get() = CompletableDeferred(this) get() = CompletableDeferred(this)

View File

@@ -3,20 +3,53 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
/**
* Convenience property to access [Dispatchers.Main] for UI operations.
*/
inline val UI inline val UI
get() = Dispatchers.Main get() = Dispatchers.Main
/**
* Convenience property to access [Dispatchers.Default] for CPU-intensive operations.
*/
inline val Default inline val Default
get() = Dispatchers.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( suspend inline fun <T> doIn(context: CoroutineContext, noinline block: suspend CoroutineScope.() -> T) = withContext(
context, context,
block 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( suspend inline fun <T> doInUI(noinline block: suspend CoroutineScope.() -> T) = doIn(
UI, UI,
block 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( suspend inline fun <T> doInDefault(noinline block: suspend CoroutineScope.() -> T) = doIn(
Default, Default,
block block

View File

@@ -2,6 +2,14 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.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>( class DeferredAction<T, O>(
val deferred: Deferred<T>, val deferred: Deferred<T>,
val callback: suspend (T) -> O val callback: suspend (T) -> O
@@ -9,6 +17,13 @@ class DeferredAction<T, O>(
suspend operator fun invoke() = callback(deferred.await()) 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>( class DoWithFirstBuilder<T>(
private val scope: CoroutineScope private val scope: CoroutineScope
) { ) {
@@ -22,8 +37,25 @@ class DoWithFirstBuilder<T>(
fun build() = deferreds.toList() 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) 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( suspend fun <O> Iterable<DeferredAction<*, O>>.invokeFirstOf(
scope: CoroutineScope, scope: CoroutineScope,
cancelOnResult: Boolean = true 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( suspend fun <O> invokeFirstOf(
scope: CoroutineScope, scope: CoroutineScope,
vararg variants: DeferredAction<*, O>, vararg variants: DeferredAction<*, O>,
cancelOnResult: Boolean = true cancelOnResult: Boolean = true
): O = variants.toList().invokeFirstOf(scope, cancelOnResult) ): 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( suspend fun <T, O> Iterable<Deferred<T>>.invokeOnFirst(
scope: CoroutineScope, scope: CoroutineScope,
cancelOnResult: Boolean = true, cancelOnResult: Boolean = true,
callback: suspend (T) -> O callback: suspend (T) -> O
): O = map { it.buildAction(callback) }.invokeFirstOf(scope, cancelOnResult) ): 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( suspend fun <T, O> CoroutineScope.invokeOnFirstOf(
cancelOnResult: Boolean = true, cancelOnResult: Boolean = true,
block: DoWithFirstBuilder<T>.() -> Unit, block: DoWithFirstBuilder<T>.() -> Unit,
@@ -54,6 +118,18 @@ suspend fun <T, O> CoroutineScope.invokeOnFirstOf(
cancelOnResult cancelOnResult
).let { callback(it) } ).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( suspend fun <T, O> invokeOnFirst(
scope: CoroutineScope, scope: CoroutineScope,
vararg variants: Deferred<T>, vararg variants: Deferred<T>,
@@ -61,11 +137,29 @@ suspend fun <T, O> invokeOnFirst(
callback: suspend (T) -> O callback: suspend (T) -> O
): O = variants.toList().invokeOnFirst(scope, cancelOnResult, callback) ): 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( suspend fun <T> CoroutineScope.firstOf(
variants: Iterable<Deferred<T>>, variants: Iterable<Deferred<T>>,
cancelOnResult: Boolean = true cancelOnResult: Boolean = true
) = variants.invokeOnFirst(this, cancelOnResult) { it } ) = 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( suspend fun <T> CoroutineScope.firstOf(
cancelOnResult: Boolean = true, cancelOnResult: Boolean = true,
block: DoWithFirstBuilder<T>.() -> Unit block: DoWithFirstBuilder<T>.() -> Unit
@@ -74,11 +168,29 @@ suspend fun <T> CoroutineScope.firstOf(
cancelOnResult 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( suspend fun <T> CoroutineScope.firstOf(
vararg variants: Deferred<T>, vararg variants: Deferred<T>,
cancelOnResult: Boolean = true cancelOnResult: Boolean = true
) = firstOf(variants.toList(), cancelOnResult) ) = 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( suspend fun <T> List<Deferred<T>>.first(
scope: CoroutineScope, scope: CoroutineScope,
cancelOnResult: Boolean = true cancelOnResult: Boolean = true

View File

@@ -2,4 +2,11 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.flow.FlowCollector 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) 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> 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 { fun <T> Flow<T>.debouncedBy(timeout: (T) -> Long, markerFactory: (T) -> Any?): Flow<T> = channelFlow {
val jobs = mutableMapOf<Any?, Job>() val jobs = mutableMapOf<Any?, Job>()
val mutex = Mutex() 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) 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) 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.Flow
import kotlinx.coroutines.flow.first 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 }!! 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.js.JsName
import kotlin.jvm.JvmName 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( inline fun <T, R> Flow<Flow<T>>.flatMap(
crossinline mapper: suspend (T) -> R crossinline mapper: suspend (T) -> R
) = flow { ) = 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") @JsName("flatMapIterable")
@JvmName("flatMapIterable") @JvmName("flatMapIterable")
inline fun <T, R> Flow<Iterable<T>>.flatMap( inline fun <T, R> Flow<Iterable<T>>.flatMap(
@@ -22,18 +38,48 @@ inline fun <T, R> Flow<Iterable<T>>.flatMap(
it.asFlow() it.asFlow()
}.flatMap(mapper) }.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( inline fun <T, R> Flow<Flow<T>>.flatMapNotNull(
crossinline mapper: suspend (T) -> R crossinline mapper: suspend (T) -> R
) = flatMap(mapper).takeNotNull() ) = 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") @JsName("flatMapNotNullIterable")
@JvmName("flatMapNotNullIterable") @JvmName("flatMapNotNullIterable")
inline fun <T, R> Flow<Iterable<T>>.flatMapNotNull( inline fun <T, R> Flow<Iterable<T>>.flatMapNotNull(
crossinline mapper: suspend (T) -> R crossinline mapper: suspend (T) -> R
) = flatMap(mapper).takeNotNull() ) = 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 } 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") @JsName("flattenIterable")
@JvmName("flattenIterable") @JvmName("flattenIterable")
fun <T> Flow<Iterable<T>>.flatten() = flatMap { it } fun <T> Flow<Iterable<T>>.flatten() = flatMap { it }

View File

@@ -2,5 +2,18 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.flow.* 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 } 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() fun <T> Flow<T>.filterNotNull() = takeNotNull()

View File

@@ -65,6 +65,20 @@ 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( fun <T, M> Flow<T>.subscribeAsync(
scope: CoroutineScope, scope: CoroutineScope,
markerFactory: suspend (T) -> M, markerFactory: suspend (T) -> M,
@@ -122,6 +136,20 @@ 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( fun <T, M> Flow<T>.subscribeLoggingDropExceptionsAsync(
scope: CoroutineScope, scope: CoroutineScope,
markerFactory: suspend (T) -> M, markerFactory: suspend (T) -> M,

View File

@@ -5,4 +5,12 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.merge 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) 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.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext 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( fun CoroutineScope.launchLogging(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" }, errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog, logger: KSLog = KSLog,
@@ -18,6 +29,17 @@ fun CoroutineScope.launchLogging(
}.getOrThrow() }.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( fun CoroutineScope.launchLoggingDropExceptions(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" }, errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog, logger: KSLog = KSLog,
@@ -30,6 +52,18 @@ fun CoroutineScope.launchLoggingDropExceptions(
} // just dropping exception } // 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( fun <T> CoroutineScope.asyncLogging(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" }, errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog, logger: KSLog = KSLog,
@@ -42,6 +76,18 @@ fun <T> CoroutineScope.asyncLogging(
}.getOrThrow() }.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( fun <T> CoroutineScope.asyncLoggingDropExceptions(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" }, errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog, logger: KSLog = KSLog,

View File

@@ -1,7 +1,5 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
@@ -11,13 +9,12 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.internal.SynchronizedObject import kotlinx.coroutines.internal.SynchronizedObject
import kotlinx.coroutines.internal.synchronized import kotlinx.coroutines.internal.synchronized
import kotlin.coroutines.CoroutineContext
/** /**
* Works like [StateFlow], but guarantee that latest value update will always be delivered to * Works like [StateFlow], but guarantee that latest value update will always be delivered to
* each active subscriber * each active subscriber
*/ */
open class SpecialMutableStateFlow<T>( open class MutableRedeliverStateFlow<T>(
initialValue: T initialValue: T
) : MutableStateFlow<T>, FlowCollector<T>, MutableSharedFlow<T> { ) : MutableStateFlow<T>, FlowCollector<T>, MutableSharedFlow<T> {
@OptIn(InternalCoroutinesApi::class) @OptIn(InternalCoroutinesApi::class)
@@ -68,3 +65,6 @@ open class SpecialMutableStateFlow<T>(
override suspend fun collect(collector: FlowCollector<T>) = sharingFlow.collect(collector) 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 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()!!) } } 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.KSLog
import dev.inmo.kslog.common.e 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( inline fun <T, R> R.runCatchingLogging(
noinline errorMessageBuilder: R.(Throwable) -> Any = { "Something web wrong" }, noinline errorMessageBuilder: R.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog, logger: KSLog = KSLog,
block: R.() -> T block: R.() -> T
) = runCatching(block).onFailure { ) = 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 package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -44,7 +43,7 @@ sealed interface SmartMutex {
* @param locked Preset state of [isLocked] and its internal [_lockStateFlow] * @param locked Preset state of [isLocked] and its internal [_lockStateFlow]
*/ */
class Mutable(locked: Boolean = false) : SmartMutex { 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() override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow()
private val internalChangesMutex = Mutex() private val internalChangesMutex = Mutex()

View File

@@ -3,6 +3,14 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job 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( suspend inline fun alsoWithUnlockingOnSuccess(
vararg lockers: SmartRWLocker, vararg lockers: SmartRWLocker,
block: suspend () -> Unit 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( fun alsoWithUnlockingOnSuccessAsync(
scope: CoroutineScope, scope: CoroutineScope,
vararg lockers: SmartRWLocker, vararg lockers: SmartRWLocker,

View File

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

View File

@@ -3,19 +3,49 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext 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( fun CoroutineContext.LinkedSupervisorJob(
additionalContext: CoroutineContext? = null additionalContext: CoroutineContext? = null
) = SupervisorJob(job).let { if (additionalContext != null) it + additionalContext else it } ) = 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( fun CoroutineScope.LinkedSupervisorJob(
additionalContext: CoroutineContext? = null additionalContext: CoroutineContext? = null
) = coroutineContext.LinkedSupervisorJob(additionalContext) ) = 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( fun CoroutineContext.LinkedSupervisorScope(
additionalContext: CoroutineContext? = null additionalContext: CoroutineContext? = null
) = CoroutineScope( ) = CoroutineScope(
this + LinkedSupervisorJob(additionalContext) 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( fun CoroutineScope.LinkedSupervisorScope(
additionalContext: CoroutineContext? = null additionalContext: CoroutineContext? = null
) = coroutineContext.LinkedSupervisorScope(additionalContext) ) = 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.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlin.test.BeforeTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFails import kotlin.test.assertFails
@@ -13,184 +14,517 @@ import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class SmartKeyRWLockerTests { class SmartKeyRWLockerTests {
private lateinit var locker: SmartKeyRWLocker<String>
@BeforeTest
fun setup() {
locker = SmartKeyRWLocker()
}
// ==================== Global Read Tests ====================
@Test @Test
fun writeLockKeyFailedOnGlobalWriteLockTest() = runTest { fun testGlobalReadAllowsMultipleConcurrentReads() = runTest {
val locker = SmartKeyRWLocker<String>() val results = mutableListOf<Boolean>()
val testKey = "test"
locker.acquireRead()
val jobs = List(5) {
launch {
locker.acquireRead()
delay(100.milliseconds)
results.add(true)
locker.releaseRead()
}
}
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()
writeJob.join()
assertTrue(writeAcquired, "Write should succeed after read released")
}
@Test
fun testGlobalReadBlocksAllKeyWrites() = runTest {
locker.acquireRead()
val writeFlags = mutableMapOf<String, Boolean>()
val keys = listOf("key1", "key2", "key3")
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() locker.lockWrite()
assertTrue { locker.isWriteLocked() } var globalReadAcquired = false
var keyReadAcquired = false
var keyWriteAcquired = false
assertFails { val jobs = listOf(
realWithTimeout(1.seconds) { launch {
locker.lockWrite(testKey) locker.acquireRead()
globalReadAcquired = true
locker.releaseRead()
},
launch {
locker.acquireRead("key1")
keyReadAcquired = true
locker.releaseRead("key1")
},
launch {
locker.lockWrite("key2")
keyWriteAcquired = true
locker.unlockWrite("key2")
} }
} )
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")
locker.unlockWrite() locker.unlockWrite()
assertFalse { locker.isWriteLocked() } jobs.joinAll()
realWithTimeout(1.seconds) { assertTrue(globalReadAcquired)
locker.lockWrite(testKey) assertTrue(keyReadAcquired)
} assertTrue(keyWriteAcquired)
assertTrue { locker.isWriteLocked(testKey) }
assertTrue { locker.unlockWrite(testKey) }
assertFalse { locker.isWriteLocked(testKey) }
} }
@Test @Test
fun writeLockKeyFailedOnGlobalReadLockTest() = runTest { fun testGlobalWriteIsExclusive() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKey = "test"
locker.acquireRead()
assertEquals(Int.MAX_VALUE - 1, locker.readSemaphore().freePermits)
assertFails {
realWithTimeout(1.seconds) {
locker.lockWrite(testKey)
}
}
assertFalse { locker.isWriteLocked(testKey) }
locker.releaseRead()
assertEquals(Int.MAX_VALUE, locker.readSemaphore().freePermits)
realWithTimeout(1.seconds) {
locker.lockWrite(testKey)
}
assertTrue { locker.isWriteLocked(testKey) }
assertTrue { locker.unlockWrite(testKey) }
assertFalse { locker.isWriteLocked(testKey) }
}
@Test
fun readLockFailedOnWriteLockKeyTest() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKey = "test"
locker.lockWrite(testKey)
assertTrue { locker.isWriteLocked(testKey) }
assertFails {
realWithTimeout(1.seconds) {
locker.acquireRead()
}
}
assertEquals(locker.readSemaphore().maxPermits - 1, locker.readSemaphore().freePermits)
locker.unlockWrite(testKey)
assertFalse { locker.isWriteLocked(testKey) }
realWithTimeout(1.seconds) {
locker.acquireRead()
}
assertEquals(locker.readSemaphore().maxPermits - 1, locker.readSemaphore().freePermits)
assertTrue { locker.releaseRead() }
assertEquals(locker.readSemaphore().maxPermits, locker.readSemaphore().freePermits)
}
@Test
fun writeLockFailedOnWriteLockKeyTest() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKey = "test"
locker.lockWrite(testKey)
assertTrue { locker.isWriteLocked(testKey) }
assertFails {
realWithTimeout(1.seconds) {
locker.lockWrite() locker.lockWrite()
}
}
assertFalse(locker.isWriteLocked())
locker.unlockWrite(testKey) var secondWriteAcquired = false
assertFalse { locker.isWriteLocked(testKey) } val job = launch {
realWithTimeout(1.seconds) {
locker.lockWrite() locker.lockWrite()
secondWriteAcquired = true
locker.unlockWrite()
} }
assertTrue(locker.isWriteLocked())
assertTrue { locker.unlockWrite() } delay(200.milliseconds)
assertFalse(locker.isWriteLocked()) assertFalse(secondWriteAcquired, "Second global write should be blocked")
locker.unlockWrite()
job.join()
assertTrue(secondWriteAcquired)
} }
// ==================== Key Read Tests ====================
@Test @Test
fun readsBlockingGlobalWrite() = runTest { fun testKeyReadAllowsMultipleConcurrentReadsForSameKey() = runTest {
val locker = SmartKeyRWLocker<String>() val key = "testKey"
val results = mutableListOf<Boolean>()
val testKeys = (0 until 100).map { "test$it" } locker.acquireRead(key)
for (i in testKeys.indices) { val jobs = List(5) {
val it = testKeys[i] launch {
locker.acquireRead(it) locker.acquireRead(key)
val previous = testKeys.take(i) delay(50.milliseconds)
val next = testKeys.drop(i + 1) results.add(true)
locker.releaseRead(key)
previous.forEach {
assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == Int.MAX_VALUE - 1 }
}
next.forEach {
assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == null }
} }
} }
for (i in testKeys.indices) { jobs.joinAll()
val it = testKeys[i] locker.releaseRead(key)
assertFails {
realWithTimeout(13.milliseconds) { locker.lockWrite() } assertEquals(5, results.size)
} }
val readPermitsBeforeLock = locker.readSemaphore().freePermits
realWithTimeout(1.seconds) { locker.acquireRead() } @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)
}
}
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() locker.releaseRead()
assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits)
locker.releaseRead(it)
} }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } delay(200.milliseconds)
realWithTimeout(1.seconds) { locker.lockWrite() } assertFalse(globalReadAcquired, "Global read should be blocked by key write")
assertFails {
realWithTimeout(13.milliseconds) { locker.acquireRead() } locker.unlockWrite("key1")
} job.join()
assertTrue { locker.unlockWrite() }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } assertTrue(globalReadAcquired)
} }
@Test @Test
fun writesBlockingGlobalWrite() = runTest { fun testKeyWriteIsExclusiveForSameKey() = runTest {
val locker = SmartKeyRWLocker<String>() val key = "testKey"
locker.lockWrite(key)
val testKeys = (0 until 100).map { "test$it" } var secondWriteAcquired = false
val job = launch {
for (i in testKeys.indices) { locker.lockWrite(key)
val it = testKeys[i] secondWriteAcquired = true
locker.lockWrite(it) locker.unlockWrite(key)
val previous = testKeys.take(i)
val next = testKeys.drop(i + 1)
previous.forEach {
assertTrue { locker.writeMutexOrNull(it) ?.isLocked == true }
} }
next.forEach {
assertTrue { locker.writeMutexOrNull(it) ?.isLocked != true } delay(200.milliseconds)
assertFalse(secondWriteAcquired, "Second write for same key should be blocked")
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")
}
)
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) { delay(200.milliseconds)
val it = testKeys[i] assertTrue(readerFlags.isEmpty(), "Readers should be blocked by writer")
assertFails { realWithTimeout(13.milliseconds) { locker.lockWrite() } }
val readPermitsBeforeLock = locker.readSemaphore().freePermits locker.unlockWrite(key)
assertFails { realWithTimeout(13.milliseconds) { locker.acquireRead() } } readers.joinAll()
assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits)
locker.unlockWrite(it) assertEquals(5, readerFlags.size, "All readers should succeed after writer")
} }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } @Test
realWithTimeout(1.seconds) { locker.lockWrite() } fun testCascadingLocksWithDifferentKeys() = runTest {
assertFails { val executed = mutableMapOf<String, Boolean>()
realWithTimeout(13.milliseconds) { locker.acquireRead() }
launch {
locker.lockWrite("key1")
executed["write-key1-start"] = true
delay(100.milliseconds)
locker.unlockWrite("key1")
executed["write-key1-end"] = true
} }
assertTrue { locker.unlockWrite() }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } delay(50.milliseconds)
launch {
locker.acquireRead("key2")
executed["read-key2"] = true
delay(100.milliseconds)
locker.releaseRead("key2")
}
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.MutableRedeliverStateFlow
import dev.inmo.micro_utils.coroutines.asDeferred
import dev.inmo.micro_utils.coroutines.subscribe import dev.inmo.micro_utils.coroutines.subscribe
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue
class SpecialMutableStateFlowTests { class SpecialMutableStateFlowTests {
@Test @Test
fun simpleTest() = runTest { fun simpleTest() = runTest {
val specialMutableStateFlow = SpecialMutableStateFlow(0) val mutableRedeliverStateFlow = MutableRedeliverStateFlow(0)
specialMutableStateFlow.value = 1 mutableRedeliverStateFlow.value = 1
specialMutableStateFlow.first { it == 1 } mutableRedeliverStateFlow.first { it == 1 }
assertEquals(1, specialMutableStateFlow.value) assertEquals(1, mutableRedeliverStateFlow.value)
} }
@Test @Test
fun specialTest() = runTest { fun specialTest() = runTest {
val specialMutableStateFlow = SpecialMutableStateFlow(0) val mutableRedeliverStateFlow = MutableRedeliverStateFlow(0)
lateinit var subscriberJob: Job lateinit var subscriberJob: Job
subscriberJob = specialMutableStateFlow.subscribe(this) { subscriberJob = mutableRedeliverStateFlow.subscribe(this) {
when (it) { when (it) {
1 -> specialMutableStateFlow.value = 2 1 -> mutableRedeliverStateFlow.value = 2
2 -> subscriberJob.cancel() 2 -> subscriberJob.cancel()
} }
} }
specialMutableStateFlow.value = 1 mutableRedeliverStateFlow.value = 1
subscriberJob.join() 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.* 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 val IO
get() = Dispatchers.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( suspend inline fun <T> doInIO(noinline block: suspend CoroutineScope.() -> T) = doIn(
IO, IO,
block block

View File

@@ -3,6 +3,16 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers 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 { fun <T> launchInCurrentThread(block: suspend CoroutineScope.() -> T): T {
val scope = CoroutineScope(Dispatchers.Unconfined) val scope = CoroutineScope(Dispatchers.Unconfined)
return scope.launchSynchronously(block) return scope.launchSynchronously(block)

View File

@@ -2,6 +2,16 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.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 { fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T): T {
var result: Result<T>? = null var result: Result<T>? = null
val objectToSynchronize = Object() val objectToSynchronize = Object()
@@ -22,7 +32,31 @@ fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T
return result!!.getOrThrow() 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) 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) 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) 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 Int.toBase64(): Char = BASE64_ALPHABET[this]
internal fun Byte.fromBase64(): Byte = BASE64_INVERSE_ALPHABET[toInt() and 0xff].toByte() and BASE64_MASK 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 typealias EncodedBase64String = String
/**
* Type alias representing a Base64-encoded byte array.
*/
typealias EncodedByteArray = ByteArray 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() 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() 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 { fun SourceBytes.encodeBase64String(): EncodedBase64String = buildString {
var i = 0 var i = 0
while (this@encodeBase64String.size > i) { while (this@encodeBase64String.size > i) {
@@ -45,11 +68,33 @@ fun SourceBytes.encodeBase64String(): EncodedBase64String = buildString {
i += read 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() 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() 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() 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 { fun EncodedByteArray.decodeBase64(): SourceBytes {
val result = mutableListOf<Byte>() val result = mutableListOf<Byte>()
val data = ByteArray(4) val data = ByteArray(4)
@@ -74,4 +119,10 @@ fun EncodedByteArray.decodeBase64(): SourceBytes {
return result.toByteArray() return result.toByteArray()
} }
/**
* Decodes this Base64-encoded byte array back to the original string.
*
* @return The decoded string
*/
fun EncodedByteArray.decodeBase64String(): SourceString = decodeBase64().decodeToString() fun EncodedByteArray.decodeBase64String(): SourceString = decodeBase64().decodeToString()

View File

@@ -1,7 +1,16 @@
package dev.inmo.micro_utils.crypto package dev.inmo.micro_utils.crypto
/**
* Character array used for hexadecimal encoding (lowercase).
*/
val HEX_ARRAY = "0123456789abcdef".toCharArray() 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 { fun SourceBytes.hex(): String {
val hexChars = CharArray(size * 2) val hexChars = CharArray(size * 2)
for (j in indices) { for (j in indices) {
@@ -12,4 +21,9 @@ fun SourceBytes.hex(): String {
return hexChars.concatToString() 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() fun SourceString.hex(): String = encodeToByteArray().hex()

View File

@@ -2,7 +2,21 @@ package dev.inmo.micro_utils.crypto
import korlibs.crypto.md5 import korlibs.crypto.md5
/**
* Type alias representing an MD5 hash as a hex-encoded string (32 characters).
*/
typealias MD5 = String 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 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 fun SourceString.md5(): MD5 = encodeToByteArray().md5().hexLower

View File

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

View File

@@ -1,5 +1,13 @@
package dev.inmo.micro_utils.fsm.common 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 { 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 val context: Any
} }

View File

@@ -1,6 +1,26 @@
package dev.inmo.micro_utils.fsm.common.utils 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? 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 } 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> 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() { private String getCurrentVersionChangelog() {
OutputStream changelogDataOS = new ByteArrayOutputStream() OutputStream changelogDataOS = new ByteArrayOutputStream()
exec {
def injected = project.objects.newInstance(InjectedExecOps)
injected.execOps.exec {
commandLine 'chmod', "+x", './changelog_parser.sh' commandLine 'chmod', "+x", './changelog_parser.sh'
} }
exec { injected.execOps.exec {
standardOutput = changelogDataOS standardOutput = changelogDataOS
commandLine './changelog_parser.sh', "${project.version}", 'CHANGELOG.md' commandLine './changelog_parser.sh', "${project.version}", 'CHANGELOG.md'
} }

View File

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

View File

@@ -1,45 +1,47 @@
[versions] [versions]
kt = "2.2.0" kt = "2.3.10"
kt-serialization = "1.9.0" kt-serialization = "1.10.0"
kt-coroutines = "1.10.2" kt-coroutines = "1.10.2"
kotlinx-browser = "0.3" kotlinx-browser = "0.5.0"
kslog = "1.5.0" kslog = "1.6.0"
jb-compose = "1.8.2" jb-compose = "1.10.1"
jb-exposed = "0.61.0" jb-compose-material3 = "1.10.0-alpha05"
jb-dokka = "2.0.0" 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" korlibs = "5.4.0"
uuid = "0.8.4" uuid = "0.8.4"
ktor = "3.2.2" ktor = "3.4.0"
gh-release = "2.5.2" gh-release = "2.5.2"
koin = "4.1.0" koin = "4.1.1"
okio = "3.15.0" okio = "3.16.4"
ksp = "2.2.0-2.0.2" ksp = "2.3.6"
kotlin-poet = "2.2.0" kotlin-poet = "2.2.0"
versions = "0.52.0" versions = "0.53.0"
nmcp = "1.0.1" nmcp = "1.2.1"
android-gradle = "8.9.+" android-gradle = "8.12.+"
dexcount = "4.0.0" dexcount = "4.0.0"
android-coreKtx = "1.16.0" android-coreKtx = "1.17.0"
android-recyclerView = "1.4.0" android-recyclerView = "1.4.0"
android-appCompat = "1.7.1" android-appCompat = "1.7.1"
android-fragment = "1.8.8" android-fragment = "1.8.9"
android-espresso = "3.6.1" android-espresso = "3.7.0"
android-test = "1.2.1" android-test = "1.3.0"
android-props-minSdk = "21" android-props-minSdk = "21"
android-props-compileSdk = "36" 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" } jb-exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "jb-exposed" }
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" } 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-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" } android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }

View File

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

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -27,6 +27,7 @@ import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.toTypeName import com.squareup.kotlinpoet.ksp.toTypeName
import com.squareup.kotlinpoet.ksp.writeTo import com.squareup.kotlinpoet.ksp.writeTo
import dev.inmo.micro_ksp.generator.safeClassName 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.GenerateGenericKoinDefinition
import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition
import org.koin.core.Koin import org.koin.core.Koin
@@ -240,9 +241,14 @@ class Processor(
ksFile.getAnnotationsByType(GenerateKoinDefinition::class).forEach { ksFile.getAnnotationsByType(GenerateKoinDefinition::class).forEach {
val type = safeClassName { it.type } val type = safeClassName { it.type }
val targetType = runCatching { 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 -> }.getOrElse { e ->
when (e) { when (e) {
is IllegalArgumentException if (e.message ?.contains("no type argument") == true) -> return@getOrElse type
is KSTypeNotPresentException -> e.ksType.toClassName() is KSTypeNotPresentException -> e.ksType.toClassName()
} }
if (e is KSTypesNotPresentException) { if (e is KSTypesNotPresentException) {
@@ -251,14 +257,32 @@ class Processor(
throw e throw e
} }
}.copy( }.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 { ksFile.getAnnotationsByType(GenerateGenericKoinDefinition::class).forEach {
val targetType = TypeVariableName("T", Any::class) 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 { }.build().let {
File( File(

View File

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

View File

@@ -6,6 +6,17 @@ import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.asTypeName import com.squareup.kotlinpoet.asTypeName
import kotlin.reflect.KClass 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") @Suppress("NOTHING_TO_INLINE")
@OptIn(KspExperimental::class) @OptIn(KspExperimental::class)
inline fun safeClassName(classnameGetter: () -> KClass<*>) = runCatching { inline fun safeClassName(classnameGetter: () -> KClass<*>) = runCatching {

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 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> { fun KSClassDeclaration.resolveSubclasses(): List<KSClassDeclaration> {
return (getSealedSubclasses().flatMap { return (getSealedSubclasses().flatMap {
it.resolveSubclasses() 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.buildSubFileName
import dev.inmo.micro_ksp.generator.companion import dev.inmo.micro_ksp.generator.companion
import dev.inmo.micro_ksp.generator.findSubClasses 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_ksp.generator.writeFile
import dev.inmo.micro_utils.ksp.sealed.GenerateSealedTypesWorkaround import dev.inmo.micro_utils.ksp.sealed.GenerateSealedTypesWorkaround
import dev.inmo.micro_utils.ksp.sealed.GenerateSealedWorkaround import dev.inmo.micro_utils.ksp.sealed.GenerateSealedWorkaround
@@ -113,7 +114,7 @@ class Processor(
val annotation = ksClassDeclaration.getGenerateSealedTypesWorkaroundAnnotation val annotation = ksClassDeclaration.getGenerateSealedTypesWorkaroundAnnotation
val subClasses = ksClassDeclaration.resolveSubclasses( val subClasses = ksClassDeclaration.resolveSubclasses(
searchIn = resolver.getAllFiles(), searchIn = resolver.getAllFiles(),
allowNonSealed = annotation ?.includeNonSealedSubTypes ?: false allowNonSealed = withNoSuchElementWorkaround(null) { annotation ?.includeNonSealedSubTypes } ?: false
).distinct() ).distinct()
val subClassesNames = subClasses.filter { val subClassesNames = subClasses.filter {
it.getAnnotationsByType(GenerateSealedTypesWorkaround.Exclude::class).count() == 0 it.getAnnotationsByType(GenerateSealedTypesWorkaround.Exclude::class).count() == 0
@@ -164,7 +165,15 @@ class Processor(
@OptIn(KspExperimental::class) @OptIn(KspExperimental::class)
override fun process(resolver: Resolver): List<KSAnnotated> { override fun process(resolver: Resolver): List<KSAnnotated> {
(resolver.getSymbolsWithAnnotation(GenerateSealedWorkaround::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach { (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.isNotEmpty()
} ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "") } ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "")
it.writeFile(prefix = prefix, suffix = "SealedWorkaround") { it.writeFile(prefix = prefix, suffix = "SealedWorkaround") {
@@ -184,7 +193,9 @@ class Processor(
} }
} }
(resolver.getSymbolsWithAnnotation(GenerateSealedTypesWorkaround::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach { (resolver.getSymbolsWithAnnotation(GenerateSealedTypesWorkaround::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach {
val prefix = (it.getGenerateSealedTypesWorkaroundAnnotation) ?.prefix ?.takeIf { val prefix = withNoSuchElementWorkaround("") {
(it.getGenerateSealedTypesWorkaroundAnnotation)?.prefix
} ?.takeIf {
it.isNotEmpty() it.isNotEmpty()
} ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "") } ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "")
it.writeFile(prefix = prefix, suffix = "SealedTypesWorkaround") { 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.convertToClassName
import dev.inmo.micro_ksp.generator.convertToClassNames import dev.inmo.micro_ksp.generator.convertToClassNames
import dev.inmo.micro_ksp.generator.findSubClasses 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_ksp.generator.writeFile
import dev.inmo.micro_utils.ksp.variations.GenerateVariations import dev.inmo.micro_utils.ksp.variations.GenerateVariations
import dev.inmo.micro_utils.ksp.variations.GenerationVariant import dev.inmo.micro_utils.ksp.variations.GenerationVariant
@@ -218,7 +219,9 @@ class Processor(
@OptIn(KspExperimental::class) @OptIn(KspExperimental::class)
override fun process(resolver: Resolver): List<KSAnnotated> { override fun process(resolver: Resolver): List<KSAnnotated> {
(resolver.getSymbolsWithAnnotation(GenerateVariations::class.qualifiedName!!)).filterIsInstance<KSFunctionDeclaration>().forEach { (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.isNotEmpty()
} ?: it.simpleName.asString().replaceFirst(it.simpleName.asString(), "") } ?: it.simpleName.asString().replaceFirst(it.simpleName.asString(), "")
it.writeFile(prefix = prefix, suffix = "GeneratedVariation") { 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.client.statement.HttpResponse
import io.ktor.http.HttpStatusCode 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( suspend inline fun <reified T : Any> HttpResponse.bodyOrNull(
statusFilter: (HttpResponse) -> Boolean = { it.status == HttpStatusCode.OK } statusFilter: (HttpResponse) -> Boolean = { it.status == HttpStatusCode.OK }
) = takeIf(statusFilter) ?.body<T>() ) = 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> { suspend inline fun <reified T : Any> HttpResponse.bodyOrNullOnNoContent() = bodyOrNull<T> {
it.status != HttpStatusCode.NoContent 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.client.statement.HttpResponse
import io.ktor.http.isSuccess 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( inline fun HttpResponse.throwOnUnsuccess(
unsuccessMessage: () -> String unsuccessMessage: () -> String
) { ) {

View File

@@ -5,6 +5,12 @@ import dev.inmo.micro_utils.common.filesize
import dev.inmo.micro_utils.ktor.common.input import dev.inmo.micro_utils.ktor.common.input
import io.ktor.client.request.forms.InputProvider 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) { fun MPPFile.inputProvider(): InputProvider = InputProvider(filesize) {
input() input()
} }

View File

@@ -1,3 +1,9 @@
package dev.inmo.micro_utils.ktor.client 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 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.HttpClient
import io.ktor.client.content.* 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( expect suspend fun HttpClient.tempUpload(
fullTempUploadDraftPath: String, fullTempUploadDraftPath: String,
file: MPPFile, file: MPPFile,

View File

@@ -10,6 +10,14 @@ import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.StringFormat import kotlinx.serialization.StringFormat
import kotlinx.serialization.json.Json 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( data class UniUploadFileInfo(
val fileName: FileName, val fileName: FileName,
val mimeType: String, val mimeType: String,

View File

@@ -1,5 +1,14 @@
package dev.inmo.micro_utils.ktor.common 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( fun buildStandardUrl(
basePart: String, basePart: String,
subpart: String, subpart: String,
@@ -8,6 +17,15 @@ fun buildStandardUrl(
parameters 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( fun buildStandardUrl(
basePart: String, basePart: String,
subpart: String, subpart: String,
@@ -16,6 +34,15 @@ fun buildStandardUrl(
parameters 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( fun buildStandardUrl(
basePart: String, basePart: String,
subpart: String, subpart: String,

View File

@@ -1,3 +1,8 @@
package dev.inmo.micro_utils.ktor.common 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() object CorrectCloseException : Exception()

View File

@@ -2,6 +2,14 @@ package dev.inmo.micro_utils.ktor.common
private val schemaRegex = Regex("^[^:]*://") 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 val String.asCorrectWebSocketUrl: String
get() = if (startsWith("ws")) { get() = if (startsWith("ws")) {
this this

View File

@@ -2,14 +2,30 @@ package dev.inmo.micro_utils.ktor.common
import korlibs.time.DateTime 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?> 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 val FromToDateTime.asFromToUrlPart: QueryParams
get() = mapOf( get() = mapOf(
"from" to first ?.unixMillis ?.toString(), "from" to first ?.unixMillis ?.toString(),
"to" to second ?.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 val QueryParams.extractFromToDateTime: FromToDateTime
get() = FromToDateTime( get() = FromToDateTime(
get("from") ?.toDoubleOrNull() ?.let { DateTime(it) }, 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 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 typealias LambdaInputProvider = () -> Input

View File

@@ -3,4 +3,10 @@ package dev.inmo.micro_utils.ktor.common
import dev.inmo.micro_utils.common.MPPFile import dev.inmo.micro_utils.common.MPPFile
import io.ktor.utils.io.core.Input 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 expect fun MPPFile.input(): Input

View File

@@ -29,6 +29,13 @@ fun String.includeQueryParams(
queryParams: List<QueryParam> queryParams: List<QueryParam>
): String = "$this${if (contains("?")) "&" else "?"}${queryParams.asUrlQuery}" ): 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 val String.parseUrlQuery: QueryParams
get() = split("&").map { get() = split("&").map {
it.split("=").let { pair -> it.split("=").let { pair ->

View File

@@ -5,27 +5,73 @@ package dev.inmo.micro_utils.ktor.common
import kotlinx.serialization.* import kotlinx.serialization.*
import kotlinx.serialization.cbor.Cbor import kotlinx.serialization.cbor.Cbor
/**
* Type alias for the standard serialization format used in Ktor utilities, which is [BinaryFormat].
*/
typealias StandardKtorSerialFormat = BinaryFormat typealias StandardKtorSerialFormat = BinaryFormat
/**
* Type alias for the standard serialization input data type, which is [ByteArray].
*/
typealias StandardKtorSerialInputData = ByteArray typealias StandardKtorSerialInputData = ByteArray
/**
* The standard Ktor serialization format instance, configured as CBOR.
*/
val standardKtorSerialFormat: StandardKtorSerialFormat = 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( inline fun <T> StandardKtorSerialFormat.decodeDefault(
deserializationStrategy: DeserializationStrategy<T>, deserializationStrategy: DeserializationStrategy<T>,
input: StandardKtorSerialInputData input: StandardKtorSerialInputData
): T = decodeFromByteArray(deserializationStrategy, input) ): 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( inline fun <T> StandardKtorSerialFormat.encodeDefault(
serializationStrategy: SerializationStrategy<T>, serializationStrategy: SerializationStrategy<T>,
data: T data: T
): StandardKtorSerialInputData = encodeToByteArray(serializationStrategy, data) ): StandardKtorSerialInputData = encodeToByteArray(serializationStrategy, data)
/**
* A CBOR instance for serialization operations.
*/
val cbor = Cbor {} 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( inline fun <T> StandardKtorSerialFormat.decodeHex(
deserializationStrategy: DeserializationStrategy<T>, deserializationStrategy: DeserializationStrategy<T>,
input: String input: String
): T = decodeFromHexString(deserializationStrategy, input) ): 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( inline fun <T> StandardKtorSerialFormat.encodeHex(
serializationStrategy: SerializationStrategy<T>, serializationStrategy: SerializationStrategy<T>,
data: T data: T

View File

@@ -3,8 +3,17 @@ package dev.inmo.micro_utils.ktor.common
import kotlin.jvm.JvmInline import kotlin.jvm.JvmInline
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
/**
* The default subdirectory path for storing temporal files during upload operations.
*/
const val DefaultTemporalFilesSubPath = "temp_upload" 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 @Serializable
@JvmInline @JvmInline
value class TemporalFileId(val string: String) 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 dev.inmo.micro_utils.ktor.common.FromToDateTime
import io.ktor.http.Parameters 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 val Parameters.extractFromToDateTime: FromToDateTime
get() = FromToDateTime( get() = FromToDateTime(
get("from") ?.toDoubleOrNull() ?.let { DateTime(it) }, 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.application.ApplicationCall
import io.ktor.server.response.respond 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( suspend fun ApplicationCall.getParameterOrSendError(
field: String field: String
) = parameters[field].also { ) = 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( suspend fun ApplicationCall.getParametersOrSendError(
field: String field: String
) = parameters.getAll(field).also { ) = 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( fun ApplicationCall.getQueryParameter(
field: String field: String
) = request.queryParameters[field] ) = 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( fun ApplicationCall.getQueryParameters(
field: String field: String
) = request.queryParameters.getAll(field) ) = 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( suspend fun ApplicationCall.getQueryParameterOrSendError(
field: String field: String
) = getQueryParameter(field).also { ) = 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( suspend fun ApplicationCall.getQueryParametersOrSendError(
field: String field: String
) = getQueryParameters(field).also { ) = getQueryParameters(field).also {

View File

@@ -4,6 +4,13 @@ import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall import io.ktor.server.application.ApplicationCall
import io.ktor.server.response.respond 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( suspend inline fun <reified T : Any> ApplicationCall.respondOrNoContent(
data: T? data: T?
) { ) {

View File

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

View File

@@ -5,6 +5,10 @@ import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder 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> { object IetfLangSerializer : KSerializer<IetfLang> {
override val descriptor = String.serializer().descriptor override val descriptor = String.serializer().descriptor

View File

@@ -1,3 +1,9 @@
package dev.inmo.micro_utils.matrix 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>> typealias Matrix<T> = List<Row<T>>

View File

@@ -1,15 +1,37 @@
package dev.inmo.micro_utils.matrix 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> { fun <T> matrix(block: MatrixBuilder<T>.() -> Unit): Matrix<T> {
return MatrixBuilder<T>().also(block).matrix 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> { fun <T> flatMatrix(block: RowBuilder<T>.() -> Unit): Matrix<T> {
return MatrixBuilder<T>().apply { return MatrixBuilder<T>().apply {
row(block) row(block)
}.matrix }.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> { fun <T> flatMatrix(vararg elements: T): Matrix<T> {
return MatrixBuilder<T>().apply { return MatrixBuilder<T>().apply {
row { elements.forEach { +it } } row { elements.forEach { +it } }

View File

@@ -1,3 +1,8 @@
package dev.inmo.micro_utils.matrix 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> typealias Row<T> = List<T>

View File

@@ -1,3 +1,9 @@
package dev.inmo.micro_utils.mime_types 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 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 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) @Serializable(MimeTypeSerializer::class)
interface MimeType { interface MimeType {
/**
* The raw MIME type string (e.g., "text/html", "application/json").
*/
val raw: String 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> val extensions: Array<String>
get() = emptyArray() get() = emptyArray()
} }

View File

@@ -8,6 +8,11 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
/**
* Serializer for [MimeType] that serializes MIME types as their raw string representation.
* Uses the [mimeType] factory function to create appropriate [MimeType] instances during deserialization,
* which will return known MIME types when available or create [CustomMimeType] for unknown types.
*/
@Suppress("OPT_IN_USAGE") @Suppress("OPT_IN_USAGE")
@Serializer(MimeType::class) @Serializer(MimeType::class)
object MimeTypeSerializer : KSerializer<MimeType> { object MimeTypeSerializer : KSerializer<MimeType> {

View File

@@ -2,6 +2,16 @@ package dev.inmo.micro_utils.pagination.utils
import dev.inmo.micro_utils.pagination.* import dev.inmo.micro_utils.pagination.*
/**
* Executes [block] for each page in a paginated sequence.
* The [paginationMapper] determines the next pagination to use based on the current result.
* Stops when [paginationMapper] returns null.
*
* @param T The type of items in each page
* @param initialPagination The pagination to start with. Defaults to [FirstPagePagination]
* @param paginationMapper Function that determines the next pagination based on the current result. Return null to stop
* @param block Function that processes each page and returns a [PaginationResult]
*/
inline fun <T> doForAll( inline fun <T> doForAll(
initialPagination: Pagination = FirstPagePagination(), initialPagination: Pagination = FirstPagePagination(),
paginationMapper: (PaginationResult<T>) -> Pagination?, paginationMapper: (PaginationResult<T>) -> Pagination?,
@@ -12,6 +22,14 @@ inline fun <T> doForAll(
} }
} }
/**
* Executes [block] for each page in a paginated sequence, automatically moving to the next page
* until an empty page or the last page is reached.
*
* @param T The type of items in each page
* @param initialPagination The pagination to start with. Defaults to [FirstPagePagination]
* @param block Function that processes each page and returns a [PaginationResult]
*/
inline fun <T> doForAllWithNextPaging( inline fun <T> doForAllWithNextPaging(
initialPagination: Pagination = FirstPagePagination(), initialPagination: Pagination = FirstPagePagination(),
block: (Pagination) -> PaginationResult<T> block: (Pagination) -> PaginationResult<T>
@@ -23,6 +41,14 @@ inline fun <T> doForAllWithNextPaging(
) )
} }
/**
* Executes [block] for each page in a paginated sequence, automatically moving to the next page
* until an empty page or the last page is reached. Uses current page pagination logic.
*
* @param T The type of items in each page
* @param initialPagination The pagination to start with. Defaults to [FirstPagePagination]
* @param block Function that processes each page and returns a [PaginationResult]
*/
inline fun <T> doAllWithCurrentPaging( inline fun <T> doAllWithCurrentPaging(
initialPagination: Pagination = FirstPagePagination(), initialPagination: Pagination = FirstPagePagination(),
block: (Pagination) -> PaginationResult<T> block: (Pagination) -> PaginationResult<T>
@@ -34,6 +60,13 @@ inline fun <T> doAllWithCurrentPaging(
) )
} }
/**
* Alias for [doAllWithCurrentPaging]. Executes [block] for each page in a paginated sequence.
*
* @param T The type of items in each page
* @param initialPagination The pagination to start with. Defaults to [FirstPagePagination]
* @param block Function that processes each page and returns a [PaginationResult]
*/
inline fun <T> doForAllWithCurrentPaging( inline fun <T> doForAllWithCurrentPaging(
initialPagination: Pagination = FirstPagePagination(), initialPagination: Pagination = FirstPagePagination(),
block: (Pagination) -> PaginationResult<T> block: (Pagination) -> PaginationResult<T>

View File

@@ -2,6 +2,16 @@ package dev.inmo.micro_utils.pagination.utils
import dev.inmo.micro_utils.pagination.* import dev.inmo.micro_utils.pagination.*
/**
* Retrieves all items from a paginated source by repeatedly calling [block] with different pagination parameters.
* The [paginationMapper] determines the next pagination to use based on the current result.
*
* @param T The type of items being retrieved
* @param initialPagination The pagination to start with. Defaults to [FirstPagePagination]
* @param paginationMapper Function that determines the next pagination based on the current result. Return null to stop
* @param block Function that retrieves a page of results for the given pagination
* @return A list containing all retrieved items from all pages
*/
inline fun <T> getAll( inline fun <T> getAll(
initialPagination: Pagination = FirstPagePagination(), initialPagination: Pagination = FirstPagePagination(),
paginationMapper: (PaginationResult<T>) -> Pagination?, paginationMapper: (PaginationResult<T>) -> Pagination?,
@@ -16,6 +26,17 @@ inline fun <T> getAll(
return results.toList() return results.toList()
} }
/**
* Retrieves all items from a paginated source using a receiver context.
* This is useful when the pagination logic depends on the receiver object's state.
*
* @param T The type of items being retrieved
* @param R The receiver type
* @param initialPagination The pagination to start with. Defaults to [FirstPagePagination]
* @param paginationMapper Function that determines the next pagination based on the current result
* @param block Function that retrieves a page of results for the given pagination using the receiver context
* @return A list containing all retrieved items from all pages
*/
inline fun <T, R> R.getAllBy( inline fun <T, R> R.getAllBy(
initialPagination: Pagination = FirstPagePagination(), initialPagination: Pagination = FirstPagePagination(),
paginationMapper: R.(PaginationResult<T>) -> Pagination?, paginationMapper: R.(PaginationResult<T>) -> Pagination?,
@@ -26,6 +47,15 @@ inline fun <T, R> R.getAllBy(
{ block(it) } { block(it) }
) )
/**
* Retrieves all items from a paginated source, automatically moving to the next page
* until an empty page or the last page is reached.
*
* @param T The type of items being retrieved
* @param initialPagination The pagination to start with. Defaults to [FirstPagePagination]
* @param block Function that retrieves a page of results for the given pagination
* @return A list containing all retrieved items from all pages
*/
inline fun <T> getAllWithNextPaging( inline fun <T> getAllWithNextPaging(
initialPagination: Pagination = FirstPagePagination(), initialPagination: Pagination = FirstPagePagination(),
block: (Pagination) -> PaginationResult<T> block: (Pagination) -> PaginationResult<T>
@@ -35,6 +65,16 @@ inline fun <T> getAllWithNextPaging(
block block
) )
/**
* Retrieves all items from a paginated source using a receiver context,
* automatically moving to the next page until an empty page or the last page is reached.
*
* @param T The type of items being retrieved
* @param R The receiver type
* @param initialPagination The pagination to start with. Defaults to [FirstPagePagination]
* @param block Function that retrieves a page of results for the given pagination using the receiver context
* @return A list containing all retrieved items from all pages
*/
inline fun <T, R> R.getAllByWithNextPaging( inline fun <T, R> R.getAllByWithNextPaging(
initialPagination: Pagination = FirstPagePagination(), initialPagination: Pagination = FirstPagePagination(),
block: R.(Pagination) -> PaginationResult<T> block: R.(Pagination) -> PaginationResult<T>
@@ -43,6 +83,15 @@ inline fun <T, R> R.getAllByWithNextPaging(
{ block(it) } { block(it) }
) )
/**
* Retrieves all items from a paginated source, automatically moving to the next page
* until an empty page or the last page is reached. Uses current page pagination logic.
*
* @param T The type of items being retrieved
* @param initialPagination The pagination to start with. Defaults to [FirstPagePagination]
* @param block Function that retrieves a page of results for the given pagination
* @return A list containing all retrieved items from all pages
*/
inline fun <T> getAllWithCurrentPaging( inline fun <T> getAllWithCurrentPaging(
initialPagination: Pagination = FirstPagePagination(), initialPagination: Pagination = FirstPagePagination(),
block: (Pagination) -> PaginationResult<T> block: (Pagination) -> PaginationResult<T>
@@ -52,6 +101,17 @@ inline fun <T> getAllWithCurrentPaging(
block block
) )
/**
* Retrieves all items from a paginated source using a receiver context,
* automatically moving to the next page until an empty page or the last page is reached.
* Uses current page pagination logic.
*
* @param T The type of items being retrieved
* @param R The receiver type
* @param initialPagination The pagination to start with. Defaults to [FirstPagePagination]
* @param block Function that retrieves a page of results for the given pagination using the receiver context
* @return A list containing all retrieved items from all pages
*/
inline fun <T, R> R.getAllByWithCurrentPaging( inline fun <T, R> R.getAllByWithCurrentPaging(
initialPagination: Pagination = FirstPagePagination(), initialPagination: Pagination = FirstPagePagination(),
block: R.(Pagination) -> PaginationResult<T> block: R.(Pagination) -> PaginationResult<T>

View File

@@ -1,5 +1,13 @@
package dev.inmo.micro_utils.pagination.utils package dev.inmo.micro_utils.pagination.utils
/**
* Optionally reverses this [Iterable] based on the [reverse] parameter.
* Delegates to specialized implementations for [List] and [Set] for better performance.
*
* @param T The type of items in the iterable
* @param reverse If true, reverses the iterable; otherwise returns it unchanged
* @return The iterable, optionally reversed
*/
fun <T> Iterable<T>.optionallyReverse(reverse: Boolean): Iterable<T> = when (this) { fun <T> Iterable<T>.optionallyReverse(reverse: Boolean): Iterable<T> = when (this) {
is List<T> -> optionallyReverse(reverse) is List<T> -> optionallyReverse(reverse)
is Set<T> -> optionallyReverse(reverse) is Set<T> -> optionallyReverse(reverse)
@@ -9,17 +17,41 @@ fun <T> Iterable<T>.optionallyReverse(reverse: Boolean): Iterable<T> = when (thi
this this
} }
} }
/**
* Optionally reverses this [List] based on the [reverse] parameter.
*
* @param T The type of items in the list
* @param reverse If true, reverses the list; otherwise returns it unchanged
* @return The list, optionally reversed
*/
fun <T> List<T>.optionallyReverse(reverse: Boolean): List<T> = if (reverse) { fun <T> List<T>.optionallyReverse(reverse: Boolean): List<T> = if (reverse) {
reversed() reversed()
} else { } else {
this this
} }
/**
* Optionally reverses this [Set] based on the [reverse] parameter.
* Note that the resulting set may have a different iteration order than the original.
*
* @param T The type of items in the set
* @param reverse If true, reverses the set; otherwise returns it unchanged
* @return The set, optionally reversed
*/
fun <T> Set<T>.optionallyReverse(reverse: Boolean): Set<T> = if (reverse) { fun <T> Set<T>.optionallyReverse(reverse: Boolean): Set<T> = if (reverse) {
reversed().toSet() reversed().toSet()
} else { } else {
this this
} }
/**
* Optionally reverses this [Array] based on the [reverse] parameter.
*
* @param T The type of items in the array
* @param reverse If true, creates a reversed copy of the array; otherwise returns it unchanged
* @return The array, optionally reversed
*/
inline fun <reified T> Array<T>.optionallyReverse(reverse: Boolean) = if (reverse) { inline fun <reified T> Array<T>.optionallyReverse(reverse: Boolean) = if (reverse) {
Array(size) { Array(size) {
get(lastIndex - it) get(lastIndex - it)

View File

@@ -2,6 +2,14 @@ package dev.inmo.micro_utils.pagination.utils
import dev.inmo.micro_utils.pagination.* import dev.inmo.micro_utils.pagination.*
/**
* Paginates this [Iterable] according to the given [Pagination] parameters.
* Returns a [PaginationResult] containing the items within the specified page range.
*
* @param T The type of items in the iterable
* @param with The pagination parameters specifying which page to retrieve
* @return A [PaginationResult] containing the items from the requested page
*/
fun <T> Iterable<T>.paginate(with: Pagination): PaginationResult<T> { fun <T> Iterable<T>.paginate(with: Pagination): PaginationResult<T> {
var i = 0 var i = 0
val result = mutableListOf<T>() val result = mutableListOf<T>()
@@ -20,6 +28,15 @@ fun <T> Iterable<T>.paginate(with: Pagination): PaginationResult<T> {
return result.createPaginationResult(with, i.toLong()) return result.createPaginationResult(with, i.toLong())
} }
/**
* Paginates this [List] according to the given [Pagination] parameters.
* Returns a [PaginationResult] containing the items within the specified page range.
* More efficient than the [Iterable] version as it uses direct indexing.
*
* @param T The type of items in the list
* @param with The pagination parameters specifying which page to retrieve
* @return A [PaginationResult] containing the items from the requested page
*/
fun <T> List<T>.paginate(with: Pagination): PaginationResult<T> { fun <T> List<T>.paginate(with: Pagination): PaginationResult<T> {
if (with.firstIndex >= size || with.lastIndex < 0) { if (with.firstIndex >= size || with.lastIndex < 0) {
return emptyPaginationResult(with, size.toLong()) return emptyPaginationResult(with, size.toLong())
@@ -30,6 +47,14 @@ fun <T> List<T>.paginate(with: Pagination): PaginationResult<T> {
) )
} }
/**
* Paginates this [List] according to the given [Pagination] parameters, optionally in reverse order.
*
* @param T The type of items in the list
* @param with The pagination parameters specifying which page to retrieve
* @param reversed If true, the list will be paginated in reverse order
* @return A [PaginationResult] containing the items from the requested page, optionally reversed
*/
fun <T> List<T>.paginate(with: Pagination, reversed: Boolean): PaginationResult<T> { fun <T> List<T>.paginate(with: Pagination, reversed: Boolean): PaginationResult<T> {
return if (reversed) { return if (reversed) {
val actualPagination = with.optionallyReverse( val actualPagination = with.optionallyReverse(
@@ -42,6 +67,14 @@ fun <T> List<T>.paginate(with: Pagination, reversed: Boolean): PaginationResult<
} }
} }
/**
* Paginates this [Set] according to the given [Pagination] parameters.
* Returns a [PaginationResult] containing the items within the specified page range.
*
* @param T The type of items in the set
* @param with The pagination parameters specifying which page to retrieve
* @return A [PaginationResult] containing the items from the requested page
*/
fun <T> Set<T>.paginate(with: Pagination): PaginationResult<T> { fun <T> Set<T>.paginate(with: Pagination): PaginationResult<T> {
return this.drop(with.firstIndex).take(with.size).createPaginationResult( return this.drop(with.firstIndex).take(with.size).createPaginationResult(
with, with,
@@ -49,6 +82,14 @@ fun <T> Set<T>.paginate(with: Pagination): PaginationResult<T> {
) )
} }
/**
* Paginates this [Set] according to the given [Pagination] parameters, optionally in reverse order.
*
* @param T The type of items in the set
* @param with The pagination parameters specifying which page to retrieve
* @param reversed If true, the set will be paginated in reverse order
* @return A [PaginationResult] containing the items from the requested page, optionally reversed
*/
fun <T> Set<T>.paginate(with: Pagination, reversed: Boolean): PaginationResult<T> { fun <T> Set<T>.paginate(with: Pagination, reversed: Boolean): PaginationResult<T> {
val actualPagination = with.optionallyReverse( val actualPagination = with.optionallyReverse(
size, size,

View File

@@ -2,6 +2,15 @@ package dev.inmo.micro_utils.pagination.utils
import dev.inmo.micro_utils.pagination.* import dev.inmo.micro_utils.pagination.*
/**
* An iterator that lazily fetches items from a paginated data source.
* It automatically fetches the next page when the current page is exhausted.
*
* @param T The type of items being iterated
* @param pageSize The size of each page to fetch
* @param countGetter A function that returns the total count of available items
* @param paginationResultGetter A function that fetches a page of results for a given pagination
*/
class PaginatedIterator<T>( class PaginatedIterator<T>(
pageSize: Int, pageSize: Int,
private val countGetter: () -> Long, private val countGetter: () -> Long,
@@ -22,6 +31,15 @@ class PaginatedIterator<T>(
} }
} }
/**
* An iterable that lazily fetches items from a paginated data source.
* It creates a [PaginatedIterator] that automatically fetches pages as needed.
*
* @param T The type of items being iterated
* @param pageSize The size of each page to fetch
* @param countGetter A function that returns the total count of available items
* @param paginationResultGetter A function that fetches a page of results for a given pagination
*/
class PaginatedIterable<T>( class PaginatedIterable<T>(
private val pageSize: Int, private val pageSize: Int,
private val countGetter: () -> Long, private val countGetter: () -> Long,
@@ -31,7 +49,14 @@ class PaginatedIterable<T>(
} }
/** /**
* Will make iterable using incoming [countGetter] and [paginationResultGetter] * Creates an [Iterable] that lazily fetches items from a paginated data source.
* This is useful for iterating over large datasets without loading all items into memory at once.
*
* @param T The type of items being iterated
* @param countGetter A function that returns the total count of available items
* @param pageSize The size of each page to fetch. Defaults to [defaultPaginationPageSize]
* @param paginationResultGetter A function that fetches a page of results for a given pagination
* @return An [Iterable] that can be used in for-loops or other iterable contexts
*/ */
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
inline fun <T> makeIterable( inline fun <T> makeIterable(

View File

@@ -1,9 +1,8 @@
package dev.inmo.micro_utils.pagination.compose package dev.inmo.micro_utils.pagination.compose
import androidx.compose.runtime.* import androidx.compose.runtime.*
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
import dev.inmo.micro_utils.coroutines.runCatchingLogging
import dev.inmo.micro_utils.pagination.* import dev.inmo.micro_utils.pagination.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -27,8 +26,8 @@ class InfinityPagedComponentContext<T> internal constructor(
private val loader: suspend InfinityPagedComponentContext<T>.(Pagination) -> PaginationResult<T> private val loader: suspend InfinityPagedComponentContext<T>.(Pagination) -> PaginationResult<T>
) { ) {
internal val startPage = SimplePagination(page, size) internal val startPage = SimplePagination(page, size)
internal val latestLoadedPage = SpecialMutableStateFlow<PaginationResult<T>?>(null) internal val latestLoadedPage = MutableRedeliverStateFlow<PaginationResult<T>?>(null)
internal val dataState = SpecialMutableStateFlow<List<T>?>(null) internal val dataState = MutableRedeliverStateFlow<List<T>?>(null)
internal var loadingJob: Job? = null internal var loadingJob: Job? = null
internal val loadingMutex = Mutex() internal val loadingMutex = Mutex()

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