Compare commits

...

102 Commits

Author SHA1 Message Date
260399e965 AbstractExposedKeyValuesRepo#set fix 2025-02-19 23:30:54 +06:00
de72843b8e Retry 2025-02-19 23:29:49 +06:00
b589142d9f update dependencies 2025-02-19 14:18:08 +06:00
f479c85869 remove redundant K2 enabling (enabled by default since kotlin 2.0 2025-02-19 14:13:53 +06:00
26992c039a more fixes to god of fixes 2025-02-19 14:13:02 +06:00
ef50e1a24f try K2? 2025-02-19 12:28:47 +06:00
2f201670d2 fix? 2025-02-18 16:35:16 +06:00
90c80573a0 potential fix for DirectFullKeyValuesCacheRepo 2025-02-18 15:36:15 +06:00
059519cdca fixes in FullKeyValueCacheRepo 2025-02-18 15:25:49 +06:00
55647e2e2f start 0.24.7 2025-02-18 15:10:08 +06:00
1802be68ef Merge pull request #544 from InsanusMokrassar/0.24.6
0.24.6
2025-02-15 10:05:05 +06:00
aa79496a36 upfill changelog 2025-02-15 10:04:12 +06:00
4bfa4c32aa fix of #541 2025-02-14 13:16:14 +06:00
d8ca29eab1 start deprecation of safely functions 2025-02-14 12:37:40 +06:00
f7a9f2e13d fixes and forcing uppercase for new files 2025-02-13 12:13:15 +06:00
af286d3d53 add disabling of publishing for ksp test modules 2025-02-13 11:04:25 +06:00
9a20e8155e update dependnecies 2025-02-13 10:33:40 +06:00
b81af7e1c8 fill changelog 2025-02-13 09:09:01 +06:00
74563bbac9 small hotfix 2025-02-12 21:20:14 +06:00
07ad79f0b3 add including of annotations from source function on variation generation 2025-02-12 21:19:40 +06:00
5c28255e06 fixes for generics 2025-02-12 14:32:14 +06:00
99783f281d generate sample functions 2025-02-12 12:37:08 +06:00
a5008c3d15 first version of parameters variations 2025-02-12 12:20:22 +06:00
f807f2beeb start add variations generator 2025-02-11 13:50:58 +06:00
db34b25ef2 start 0.24.6 2025-02-10 13:35:52 +06:00
96daf11fd4 Merge pull request #538 from InsanusMokrassar/0.24.5
0.24.5
2025-01-29 09:08:08 +06:00
f7e98dfd2d update kotlin 2025-01-29 08:56:37 +06:00
61277e92bd update dependencies 2025-01-27 09:35:54 +06:00
32ef9f399f start 0.24.5 2025-01-27 09:21:39 +06:00
54e6ca5dc3 Merge pull request #535 from InsanusMokrassar/0.24.4
0.24.4
2025-01-19 09:11:54 +06:00
a8e226786d improve selectByIds and fill changelog 2025-01-17 19:46:29 +06:00
ce717a4c9f improvements in FSM 2025-01-17 18:34:54 +06:00
fd41bf0ae7 start 0.24.4 2025-01-17 14:41:09 +06:00
b2b68bf29f Merge pull request #533 from InsanusMokrassar/0.24.3
0.24.3
2025-01-15 13:36:29 +06:00
b87c29c354 replace tests of GenerateSealed*Workaround to be jvm-only 2025-01-15 13:25:58 +06:00
24977822c9 fixes in GenerateSealedTypesWorkaround 2025-01-15 13:22:07 +06:00
647daa8627 start 0.24.3 2025-01-15 13:21:37 +06:00
a372efacb1 Merge pull request #532 from InsanusMokrassar/0.24.2
0.24.2
2025-01-14 10:13:53 +06:00
f40d33db2a update exposed 2025-01-14 10:09:16 +06:00
a5eb0fbd24 add opportunity to generate types lists for sealed subtypes 2025-01-14 09:57:52 +06:00
307c8030af start 0.24.2 2025-01-14 09:57:52 +06:00
a9016465fa update dependencies 2025-01-14 09:57:32 +06:00
358b70eb5f start 0.24.1 2025-01-14 09:57:32 +06:00
6fcbb80a71 Merge pull request #530 from InsanusMokrassar/renovate/migrate-config
Migrate renovate config
2025-01-10 08:45:48 +06:00
renovate[bot]
643f6c420b Migrate config renovate.json 2025-01-10 02:41:30 +00:00
ac68b0b941 Merge pull request #519 from InsanusMokrassar/0.24.0
0.24.0
2024-12-21 09:15:11 +06:00
d62f67bd88 update dokka 2024-12-21 09:13:10 +06:00
8718c5e310 update dependencies and add kdocs for percentage 2024-12-21 09:02:31 +06:00
e8273ab80c Progress -> Percentage 2024-12-08 22:34:28 +06:00
2718605987 start 0.24.0 2024-12-08 22:27:05 +06:00
d99538d80b Merge pull request #517 from InsanusMokrassar/0.23.2
0.23.2
2024-12-06 13:47:33 +06:00
ce7a1e4e21 update dependencies 2024-12-06 13:17:09 +06:00
921734763d update workflows 2024-12-04 10:19:41 +06:00
18c608f569 small improvement in AccumulatorFlow 2024-12-04 10:16:19 +06:00
b915f6ece2 start 0.23.2 2024-12-04 10:12:39 +06:00
24347b422c Merge pull request #513 from InsanusMokrassar/0.23.1
0.23.1
2024-11-25 15:31:41 +06:00
e5f4ae647f update dependencies 2024-11-25 15:27:55 +06:00
72202b8a21 add StyleSheetsAggregator 2024-11-25 15:11:30 +06:00
dbc14d41de start 0.23.1 2024-11-15 21:39:17 +06:00
1dca5ea00d Merge pull request #499 from InsanusMokrassar/0.23.0
0.23.0
2024-10-30 15:07:58 +06:00
f03d1d788c fill changelog and update ktor 2024-10-30 15:06:19 +06:00
fcac6f9fa8 update ksp 2024-10-29 17:29:46 +06:00
ca0cd433c9 Merge branch 'master' into 0.23.0 2024-10-29 17:23:05 +06:00
39589fdbd0 Merge pull request #507 from InsanusMokrassar/0.22.9
0.22.9
2024-10-29 13:42:30 +06:00
605fc3cff9 start add direct caches 2024-10-29 13:14:41 +06:00
12cd6f48f8 start 0.22.9 2024-10-29 12:00:58 +06:00
1f57478d10 Update build.yml 2024-10-27 13:53:10 +06:00
7ef4c5d282 Merge branch 'master' into 0.23.0 2024-10-27 13:42:14 +06:00
f09d92be32 Merge pull request #506 from InsanusMokrassar/0.22.8
0.22.8
2024-10-24 16:32:18 +06:00
6245b36bdb add pads and breakAsPairs extensions 2024-10-24 15:30:10 +06:00
54cc353bcc start 0.22.8 2024-10-24 15:26:02 +06:00
b7abba099c Merge pull request #503 from InsanusMokrassar/0.22.7
hotfix due to deprecated annotation
2024-10-19 15:40:14 +06:00
c5dbd10335 hotfix due to deprecated annotation 2024-10-19 15:39:00 +06:00
a44e3e953d Merge pull request #502 from InsanusMokrassar/0.22.7
0.22.7
2024-10-19 15:26:37 +06:00
ee2521cb01 update dependencies 2024-10-19 15:21:00 +06:00
4625dfb857 replace GenerateSealedWorkaround 2024-10-19 13:44:37 +06:00
b9a2653066 start 0.22.7 2024-10-19 13:41:25 +06:00
fcaa327660 Merge pull request #501 from InsanusMokrassar/0.22.6
0.22.6
2024-10-17 22:40:53 +06:00
496117d517 improvements in ksp parts 2024-10-17 21:04:41 +06:00
8ce7d37b72 start 0.22.6 2024-10-17 20:52:45 +06:00
25391609b9 Update libs.versions.toml 2024-10-12 08:37:44 +06:00
f0b7b9c5e5 migrate to 0.23.0 2024-10-11 23:38:53 +06:00
301cdaa2c2 fix of build after migration (wat) 2024-10-11 23:32:11 +06:00
fce7ec8912 start migration onto ktor 3.0 2024-10-10 21:53:26 +06:00
24bd403549 start 0.22.6 2024-10-10 21:38:49 +06:00
46c89c48a9 Merge pull request #492 from InsanusMokrassar/0.22.5
0.22.5
2024-10-06 12:38:13 +06:00
bad9a53fdb update dependencies 2024-10-06 12:28:48 +06:00
0bce7bd60a fix of #488 2024-10-06 12:25:42 +06:00
2f70a1cfb4 solution of #489 2024-10-01 20:30:45 +06:00
bfb6e738ee add debouncedBy extension 2024-09-26 23:50:57 +06:00
c7ad9aae07 start 0.22.5 2024-09-26 23:17:00 +06:00
fecd719239 Merge pull request #491 from InsanusMokrassar/0.22.4
0.22.4
2024-09-25 22:53:53 +06:00
18d6ac31b5 update exposed 2024-09-25 22:09:50 +06:00
d8dbebfc7e start 0.22.4 2024-09-25 21:03:18 +06:00
16463d0eb9 Merge pull request #487 from InsanusMokrassar/0.22.3
0.22.3
2024-09-20 13:27:16 +06:00
837cac644d add extensions in JS 2024-09-20 13:14:50 +06:00
e83e0a8535 update dependencies 2024-09-20 12:44:10 +06:00
2e309c31a6 start 0.22.3 2024-09-20 12:18:17 +06:00
625db02651 Merge pull request #479 from InsanusMokrassar/0.22.2
0.22.2
2024-08-30 20:57:10 +06:00
e3144c08c1 update dependencies 2024-08-30 20:48:34 +06:00
40e25970ad start 0.22.2 2024-08-30 20:46:07 +06:00
4ccb8a8d11 Merge pull request #477 from InsanusMokrassar/0.22.1
0.22.1
2024-08-29 15:02:34 +06:00
110 changed files with 2590 additions and 357 deletions

View File

@@ -17,8 +17,9 @@ jobs:
mv gradle.properties.tmp gradle.properties mv gradle.properties.tmp gradle.properties
- name: Build - name: Build
run: ./gradlew build run: ./gradlew build
- name: Publish - name: Publish to InmoNexus
continue-on-error: true continue-on-error: true
run: ./gradlew publishAllPublicationsToGiteaRepository run: ./gradlew publishAllPublicationsToInmoNexusRepository
env: env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} INMONEXUS_USER: ${{ secrets.INMONEXUS_USER }}
INMONEXUS_PASSWORD: ${{ secrets.INMONEXUS_PASSWORD }}

View File

@@ -1,5 +1,193 @@
# Changelog # Changelog
## 0.24.7
* `Versions`:
* `SQLite`: `3.49.0.0` -> `3.49.1.0`
* `Common`:
* Add `retryOnFailure` utility for simple retries code writing
* `Repos`:
* `Cache`:
* Fix of `FullKeyValueCacheRepo` fields usage
* `Exposed`:
* `AbstractExposedKeyValuesRepo` will produce `onValueRemoved` event on `set` if some data has been removed
## 0.24.6
* `Versions`:
* `KSLog`: `1.4.0` -> `1.4.1`
* `Exposed`: `0.58.0` -> `0.59.0`
* `SQLite`: `3.48.0.0` -> `3.49.0.0`
* `AndroidFragment`: `1.8.5` -> `1.8.6`
* `Coroutines`:
* Safely functions has been replaced with `Logging` variations (resolve of [#541](https://github.com/InsanusMokrassar/MicroUtils/issues/541))
* `KSP`:
* `Variations`:
* Module has been created
## 0.24.5
* `Versions`:
* `Kotlin`: `2.1.0` -> `2.1.10`
* `SQLite`: `3.47.2.0` -> `3.48.0.0`
* `Koin`: `4.0.1` -> `4.0.2`
* `Android RecyclerView`: `1.3.2` -> `1.4.0`
## 0.24.4
* `Repos`:
* `Exposed`:
* Improve `CommonExposedRepo.selectByIds`
* `FSM`:
* Fixes and improvements
## 0.24.3
* `Ksp`:
* `Sealed`:
* Fixes in processing of `GenerateSealedTypesWorkaround` annotations
## 0.24.2
* `Versions`:
* `Exposed`: `0.57.0` -> `0.58.0`
* `Ksp`:
* `Sealed`:
* Add annotation `GenerateSealedTypesWorkaround` which allow to generate `subtypes` lists
## 0.24.1
* `Versions`:
* `Serialization`: `1.7.3` -> `1.8.0`
* `SQLite`: `3.47.1.0` -> `3.47.2.0`
* `Koin`: `4.0.0` -> `3.10.2`
* `OKio`: `3.9.1` -> `3.10.2`
## 0.24.0
* `Versions`:
* `Coroutines`: `1.9.0` -> `1.10.1`
* `KSLog`: `1.3.6` -> `1.4.0`
* `Compose`: `1.7.1` -> `1.7.3`
* `Ktor`: `3.0.2` -> `3.0.3`
* `Common`:
* Rename `Progress` to more common `Percentage`. `Progress` now is typealias
* Fix of `Progress.compareTo` extension
## 0.23.2
* `Versions`:
* `Kotlin`: `2.0.21` -> `2.1.0`
* `Exposed`: `0.56.0` -> `0.57.0`
* `Xerial SQLite`: `3.47.0.0` -> `3.47.1.0`
* `Ktor`: `3.0.1` -> `3.0.2`
* `Coroutines`:
* Small refactor in `AccumulatorFlow` to use `runCatching` instead of `runCatchingSafely`
## 0.23.1
* `Versions`:
* `Compose`: `1.7.0` -> `1.7.1`
* `Exposed`: `0.55.0` -> `0.56.0`
* `Xerial SQLite`: `3.46.1.3` -> `3.47.0.0`
* `Android CoreKTX`: `1.13.1` -> `1.15.0`
* `Android Fragment`: `1.8.4` -> `1.8.5`
* `Coroutines`:
* `Compose`:
* Add `StyleSheetsAggregator`
## 0.23.0
**THIS UPDATE MAY CONTAINS SOME BREAKING CHANGES (INCLUDING BREAKING CHANGES IN BYTECODE LAYER) RELATED TO UPDATE OF
KTOR DEPENDENCY**
**THIS UPDATE CONTAINS CHANGES ACCORDING TO MIGRATION [GUIDE FROM KTOR](https://ktor.io/docs/migrating-3.html)**
* `Versions`:
* `Ktor`: `2.3.12` -> `3.0.1`
* `Ktor`:
* `Common`:
* Extension `Input.downloadToTempFile` has changed its receiver to `Source`. Its API can be broken
* `Client`:
* Extension `HttpClient.tempUpload` has changed type of `onUpload` argument from `OnUploadCallback` to `ProgressListener`
* All extensions `HttpClient.uniUpload` have changed type of `onUpload` argument from `OnUploadCallback` to `ProgressListener`
* `Server`:
* Remove redundant `ApplicationCall.respond` extension due to its presence in the ktor library
## 0.22.9
* `Repos`:
* `Cache`:
* Add direct caching repos
## 0.22.8
* `Common`:
* Add `List.breakAsPairs` extension
* Add `Sequence.padWith`/`Sequence.padStart`/`Sequence.padEnd` and `List.padWith`/`List.padStart`/`List.padEnd` extensions
## 0.22.7
* `Versions`:
* `Kotlin`: `2.0.20` -> `2.0.21`
* `Compose`: `1.7.0-rc01` -> `1.7.0`
* `KSP`:
* `Sealed`:
* Change package of `GenerateSealedWorkaround`. Migration: replace `dev.inmo.microutils.kps.sealed.GenerateSealedWorkaround` -> `dev.inmo.micro_utils.ksp.sealed.GenerateSealedWorkaround`
## 0.22.6
* `KSP`:
* `Generator`:
* Add extension `KSClassDeclaration.buildSubFileName`
* Add extension `KSClassDeclaration.companion`
* Add extension `KSClassDeclaration.resolveSubclasses`
* `Sealed`:
* Improvements
## 0.22.5
* `Versions`:
* `Compose`: `1.7.0-beta02` -> `1.7.0-rc01`
* `SQLite`: `3.46.1.2` -> `3.46.1.3`
* `AndroidXFragment`: `1.8.3` -> `1.8.4`
* `Common`:
* Add extension `withReplacedAt`/`withReplaced` ([#489](https://github.com/InsanusMokrassar/MicroUtils/issues/489))
* `Coroutines`:
* Add extension `Flow.debouncedBy`
* `Ktor`:
* `Server`:
* Add `KtorApplicationConfigurator.Routing.Static` as solution for [#488](https://github.com/InsanusMokrassar/MicroUtils/issues/488)
## 0.22.4
* `Versions`:
* `Exposed`: `0.54.0` -> `0.55.0`
* `SQLite`: `3.46.1.0` -> `3.46.1.2`
## 0.22.3
* `Versions`:
* `Serialization`: `1.7.2` -> `1.7.3`
* `Coroutines`: `1.8.1` -> `1.9.0`
* `Compose`: `1.7.0-alpha03` -> `1.7.0-beta02`
* `Koin`: `3.5.6` -> `4.0.0`
* `Okio`: `3.9.0` -> `3.9.1`
* `AndroidFragment`: `1.8.2` -> `1.8.3`
* `androidx.compose.material3:material3` has been replaced with `org.jetbrains.compose.material3:material3`
* `Common`:
* `JS`:
* Add several useful extensions
* `Compose`:
* `JS`:
* Add several useful extensions
## 0.22.2
* `Versions`:
* `Exposed`: `0.53.0` -> `0.54.0`
* `SQLite`: `3.46.0.1` -> `3.46.1.0`
## 0.22.1 ## 0.22.1
* `Versions`: * `Versions`:

View File

@@ -12,7 +12,7 @@ kotlin {
sourceSets { sourceSets {
androidMain { androidMain {
dependencies { dependencies {
api libs.android.compose.material3 api libs.jb.compose.material3
} }
} }
} }

View File

@@ -0,0 +1,19 @@
package dev.inmo.micro_utils.common.compose
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.w3c.dom.Element
operator fun <T : Element> AttrBuilderContext<T>?.plus(
other: AttrBuilderContext<T>?
) = when (this) {
null -> other ?: {}
else -> when (other) {
null -> this ?: {}
else -> {
{
invoke(this)
other(this)
}
}
}
}

View File

@@ -0,0 +1,22 @@
package dev.inmo.micro_utils.common.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.DisposableEffectResult
import androidx.compose.runtime.DisposableEffectScope
import org.jetbrains.compose.web.attributes.AttrsScope
import org.jetbrains.compose.web.dom.ElementScope
import org.w3c.dom.Element
/**
* This function must be called in the context of your tag content. It works like default [AttrsScope.ref],
* but able to be used several times. Uses [DisposableEffect] under the hood
*/
@Composable
fun <T : Element> ElementScope<T>.ref(
block: DisposableEffectScope.(T) -> DisposableEffectResult
) {
DisposableEffect(0) {
block(scopeElement)
}
}

View File

@@ -0,0 +1,11 @@
package dev.inmo.micro_utils.common.compose
import org.jetbrains.compose.web.dom.AttrBuilderContext
fun tagClasses(vararg classnames: String): AttrBuilderContext<*> = {
classes(*classnames)
}
fun tagId(id: String): AttrBuilderContext<*> = {
id(id)
}

View File

@@ -0,0 +1,13 @@
package dev.inmo.micro_utils.common
fun <T> List<T>.breakAsPairs(): List<Pair<T, T>> {
val result = mutableListOf<Pair<T, T>>()
for (i in 0 until size - 1) {
val first = get(i)
val second = get(i + 1)
result.add(first to second)
}
return result
}

View File

@@ -0,0 +1,32 @@
package dev.inmo.micro_utils.common
inline fun <T> Sequence<T>.padWith(size: Int, inserter: (Sequence<T>) -> Sequence<T>): Sequence<T> {
var result = this
while (result.count() < size) {
result = inserter(result)
}
return result
}
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, o: T) = padEnd(size) { o }
inline fun <T> List<T>.padWith(size: Int, inserter: (List<T>) -> List<T>): List<T> {
var result = this
while (result.size < size) {
result = inserter(result)
}
return result
}
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, o: T): List<T> = asSequence().padEnd(size, o).toList()
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, o: T) = padStart(size) { o }
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, o: T): List<T> = asSequence().padStart(size, o).toList()

View File

@@ -0,0 +1,76 @@
package dev.inmo.micro_utils.common
import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline
/**
* Contains [of1] as main value, where 100% of percentage is when of1 == 1
*
* @see invoke
* @see partOfTotal
* @see of100
*/
@Serializable
@JvmInline
value class Percentage private constructor(
/**
* Value of percentage. When it equals to 1, means 100%
*/
val of1: Double
) {
/**
* Same as [of1], but float (using [Double.toFloat])
*/
val of1Float
get() = of1.toFloat()
/**
* Represent this percentage as common percentage where 100% is 100%
*/
val of100
get() = of1 * 100
/**
* Same as [of100], but float (using [Double.toFloat])
*/
val of100Float
get() = of100.toFloat()
/**
* Same as [of100], but int (using [Double.toInt])
*/
val of100Int
get() = of100.toInt()
companion object {
val rangeOfValues = 0.0 .. 1.0
val START = Percentage(rangeOfValues.start)
val COMPLETED = Percentage(rangeOfValues.endInclusive)
operator fun invoke(of1: Double) = Percentage(of1.coerceIn(rangeOfValues))
operator fun invoke(part: Number, total: Number) = Percentage(
part.toDouble() / total.toDouble()
)
fun of1(of1: Double) = Percentage(of1 = of1)
fun of100(of100: Double) = Percentage(of1 = of100 / 100)
fun partOfTotal(part: Number, total: Number) = Percentage(part = part, total = total)
}
}
typealias Progress = Percentage
/**
* Will return [this] [Progress] if [Percentage.of1] in `0 .. 1` range
*/
fun Progress.ensureStrictOrNull(): Progress? = if (of1 in Percentage.rangeOfValues) this else null
/**
* Will return [this] [Progress] if [Percentage.of1] in `0 .. 1` range. Otherwise, will throw error
* [IllegalArgumentException] due to [require] failure
*/
fun Progress.ensureStrictOrThrow(): Progress {
require(of1 in Percentage.rangeOfValues) {
"For strict checks value of percentage must be in ${Percentage.rangeOfValues}, but actual value is $of1"
}
return this
}

View File

@@ -0,0 +1,80 @@
@file:Suppress(
"RemoveRedundantCallsOfConversionMethods",
"RedundantVisibilityModifier",
)
package dev.inmo.micro_utils.common
import kotlin.Byte
import kotlin.Double
import kotlin.Float
import kotlin.Int
import kotlin.Long
import kotlin.Short
import kotlin.Suppress
public operator fun Percentage.plus(other: Percentage): Percentage = Percentage(of1 + other.of1)
public operator fun Percentage.minus(other: Percentage): Percentage = Percentage(of1 - other.of1)
public operator fun Percentage.plus(i: Byte): Percentage = Percentage((of1 + i).toDouble())
public operator fun Percentage.minus(i: Byte): Percentage = Percentage((of1 - i).toDouble())
public operator fun Percentage.times(i: Byte): Percentage = Percentage((of1 * i).toDouble())
public operator fun Percentage.div(i: Byte): Percentage = Percentage((of1 / i).toDouble())
public operator fun Percentage.rem(i: Byte): Percentage = Percentage((of1 % i).toDouble())
public operator fun Percentage.plus(i: Short): Percentage = Percentage((of1 + i).toDouble())
public operator fun Percentage.minus(i: Short): Percentage = Percentage((of1 - i).toDouble())
public operator fun Percentage.times(i: Short): Percentage = Percentage((of1 * i).toDouble())
public operator fun Percentage.div(i: Short): Percentage = Percentage((of1 / i).toDouble())
public operator fun Percentage.rem(i: Short): Percentage = Percentage((of1 % i).toDouble())
public operator fun Percentage.plus(i: Int): Percentage = Percentage((of1 + i).toDouble())
public operator fun Percentage.minus(i: Int): Percentage = Percentage((of1 - i).toDouble())
public operator fun Percentage.times(i: Int): Percentage = Percentage((of1 * i).toDouble())
public operator fun Percentage.div(i: Int): Percentage = Percentage((of1 / i).toDouble())
public operator fun Percentage.rem(i: Int): Percentage = Percentage((of1 % i).toDouble())
public operator fun Percentage.plus(i: Long): Percentage = Percentage((of1 + i).toDouble())
public operator fun Percentage.minus(i: Long): Percentage = Percentage((of1 - i).toDouble())
public operator fun Percentage.times(i: Long): Percentage = Percentage((of1 * i).toDouble())
public operator fun Percentage.div(i: Long): Percentage = Percentage((of1 / i).toDouble())
public operator fun Percentage.rem(i: Long): Percentage = Percentage((of1 % i).toDouble())
public operator fun Percentage.plus(i: Float): Percentage = Percentage((of1 + i).toDouble())
public operator fun Percentage.minus(i: Float): Percentage = Percentage((of1 - i).toDouble())
public operator fun Percentage.times(i: Float): Percentage = Percentage((of1 * i).toDouble())
public operator fun Percentage.div(i: Float): Percentage = Percentage((of1 / i).toDouble())
public operator fun Percentage.rem(i: Float): Percentage = Percentage((of1 % i).toDouble())
public operator fun Percentage.plus(i: Double): Percentage = Percentage((of1 + i).toDouble())
public operator fun Percentage.minus(i: Double): Percentage = Percentage((of1 - i).toDouble())
public operator fun Percentage.times(i: Double): Percentage = Percentage((of1 * i).toDouble())
public operator fun Percentage.div(i: Double): Percentage = Percentage((of1 / i).toDouble())
public operator fun Percentage.rem(i: Double): Percentage = Percentage((of1 % i).toDouble())
public operator fun Percentage.compareTo(other: Percentage): Int = (of1.compareTo(other.of1))

View File

@@ -1,37 +0,0 @@
package dev.inmo.micro_utils.common
import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline
@Serializable
@JvmInline
value class Progress private constructor(
val of1: Double
) {
val of1Float
get() = of1.toFloat()
val of100
get() = of1 * 100
val of100Float
get() = of100.toFloat()
val of100Int
get() = of100.toInt()
init {
require(of1 in rangeOfValues) {
"Progress main value should be in $rangeOfValues, but incoming value is $of1"
}
}
companion object {
val rangeOfValues = 0.0 .. 1.0
val START = Progress(rangeOfValues.start)
val COMPLETED = Progress(rangeOfValues.endInclusive)
operator fun invoke(of1: Double) = Progress(of1.coerceIn(rangeOfValues))
operator fun invoke(part: Number, total: Number) = Progress(
part.toDouble() / total.toDouble()
)
}
}

View File

@@ -1,80 +0,0 @@
@file:Suppress(
"RemoveRedundantCallsOfConversionMethods",
"RedundantVisibilityModifier",
)
package dev.inmo.micro_utils.common
import kotlin.Byte
import kotlin.Double
import kotlin.Float
import kotlin.Int
import kotlin.Long
import kotlin.Short
import kotlin.Suppress
public operator fun Progress.plus(other: Progress): Progress = Progress(of1 + other.of1)
public operator fun Progress.minus(other: Progress): Progress = Progress(of1 - other.of1)
public operator fun Progress.plus(i: Byte): Progress = Progress((of1 + i).toDouble())
public operator fun Progress.minus(i: Byte): Progress = Progress((of1 - i).toDouble())
public operator fun Progress.times(i: Byte): Progress = Progress((of1 * i).toDouble())
public operator fun Progress.div(i: Byte): Progress = Progress((of1 / i).toDouble())
public operator fun Progress.rem(i: Byte): Progress = Progress((of1 % i).toDouble())
public operator fun Progress.plus(i: Short): Progress = Progress((of1 + i).toDouble())
public operator fun Progress.minus(i: Short): Progress = Progress((of1 - i).toDouble())
public operator fun Progress.times(i: Short): Progress = Progress((of1 * i).toDouble())
public operator fun Progress.div(i: Short): Progress = Progress((of1 / i).toDouble())
public operator fun Progress.rem(i: Short): Progress = Progress((of1 % i).toDouble())
public operator fun Progress.plus(i: Int): Progress = Progress((of1 + i).toDouble())
public operator fun Progress.minus(i: Int): Progress = Progress((of1 - i).toDouble())
public operator fun Progress.times(i: Int): Progress = Progress((of1 * i).toDouble())
public operator fun Progress.div(i: Int): Progress = Progress((of1 / i).toDouble())
public operator fun Progress.rem(i: Int): Progress = Progress((of1 % i).toDouble())
public operator fun Progress.plus(i: Long): Progress = Progress((of1 + i).toDouble())
public operator fun Progress.minus(i: Long): Progress = Progress((of1 - i).toDouble())
public operator fun Progress.times(i: Long): Progress = Progress((of1 * i).toDouble())
public operator fun Progress.div(i: Long): Progress = Progress((of1 / i).toDouble())
public operator fun Progress.rem(i: Long): Progress = Progress((of1 % i).toDouble())
public operator fun Progress.plus(i: Float): Progress = Progress((of1 + i).toDouble())
public operator fun Progress.minus(i: Float): Progress = Progress((of1 - i).toDouble())
public operator fun Progress.times(i: Float): Progress = Progress((of1 * i).toDouble())
public operator fun Progress.div(i: Float): Progress = Progress((of1 / i).toDouble())
public operator fun Progress.rem(i: Float): Progress = Progress((of1 % i).toDouble())
public operator fun Progress.plus(i: Double): Progress = Progress((of1 + i).toDouble())
public operator fun Progress.minus(i: Double): Progress = Progress((of1 - i).toDouble())
public operator fun Progress.times(i: Double): Progress = Progress((of1 * i).toDouble())
public operator fun Progress.div(i: Double): Progress = Progress((of1 / i).toDouble())
public operator fun Progress.rem(i: Double): Progress = Progress((of1 % i).toDouble())
public operator fun Progress.compareTo(other: Progress): Int = (of1 - other.of1).toInt()

View File

@@ -0,0 +1,26 @@
package dev.inmo.micro_utils.common
/**
* Will try to execute [action] and, if any exception will happen, execution will be retried.
* This process will happen at most [count] times. There is no any limits on [count] value, but [action] will run at
* least once and [retryOnFailure] will return its result if it is successful
*/
inline fun <T> retryOnFailure(count: Int, action: () -> T): T {
var triesCount = 0
while (true) {
val result = runCatching {
action()
}.onFailure {
triesCount++
if (triesCount >= count) {
throw it
} else {
null
}
}
if (result.isSuccess) return result.getOrThrow()
}
error("Unreachable code: retry must throw latest exception if error happen or success value if not")
}

View File

@@ -0,0 +1,5 @@
package dev.inmo.micro_utils.common
fun <T> Iterable<T>.withReplacedAt(i: Int, block: (T) -> T): List<T> = take(i) + block(elementAt(i)) + drop(i + 1)
fun <T> Iterable<T>.withReplaced(t: T, block: (T) -> T): List<T> = withReplacedAt(indexOf(t), block)

View File

@@ -0,0 +1,29 @@
package dev.inmo.micro_utils.common
import kotlin.test.Test
import kotlin.test.assertEquals
class PercentageTests {
@Test
fun testCompareTo() {
val step = 0.01
var i = Percentage.START.of1
while (i <= Percentage.COMPLETED.of1) {
val percentageI = Percentage(i)
var j = Percentage.START.of1
while (j <= Percentage.COMPLETED.of1) {
val percentageJ = Percentage(j)
assertEquals(percentageI.of1.compareTo(percentageJ.of1), percentageI.compareTo(percentageJ))
assertEquals(percentageI.of1 > percentageJ.of1, percentageI > percentageJ)
assertEquals(percentageI.of1 < percentageJ.of1, percentageI < percentageJ)
j += step
}
i += step
}
}
}

View File

@@ -0,0 +1,21 @@
package dev.inmo.micro_utils.common
import kotlin.test.Test
import kotlin.test.assertEquals
class WithReplacedTest {
@Test
fun testReplaced() {
val data = 0 until 10
val testData = Int.MAX_VALUE
for (i in 0 until data.last) {
val withReplaced = data.withReplacedAt(i) {
testData
}
val dataAsMutableList = data.toMutableList()
dataAsMutableList[i] = testData
assertEquals(withReplaced, dataAsMutableList.toList())
}
}
}

View File

@@ -0,0 +1,13 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.window
fun copyToClipboard(text: String): Boolean {
return runCatching {
window.navigator.clipboard.writeText(
text
)
}.onFailure {
it.printStackTrace()
}.isSuccess
}

View File

@@ -0,0 +1,29 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.window
import org.w3c.files.Blob
import org.w3c.files.BlobPropertyBag
import kotlin.js.json
external class ClipboardItem(data: dynamic)
inline fun Blob.convertToClipboardItem(): ClipboardItem {
val itemData: dynamic = json(this.type to this)
return ClipboardItem(itemData)
}
suspend fun copyImageURLToClipboard(imageUrl: String): Boolean {
return runCatching {
val response = window.fetch(imageUrl).await()
val blob = response.blob().await()
val data = arrayOf(
Blob(
arrayOf(blob),
BlobPropertyBag("image/png")
).convertToClipboardItem()
).asDynamic()
window.navigator.clipboard.write(data)
}.onFailure {
it.printStackTrace()
}.isSuccess
}

View File

@@ -11,6 +11,7 @@ kotlin {
commonMain { commonMain {
dependencies { dependencies {
api libs.kt.coroutines api libs.kt.coroutines
api libs.kslog
} }
} }
jsMain { jsMain {

View File

@@ -0,0 +1,66 @@
package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce
import org.jetbrains.compose.web.css.CSSRulesHolder
import org.jetbrains.compose.web.css.Style
import org.jetbrains.compose.web.css.StyleSheet
/**
* Aggregator of Compose CSS StyleSheet. Allowing to add [StyleSheet] in it and draw it in one place without requiring
* to add `Style(stylesheet)` on every compose function call
*/
object StyleSheetsAggregator {
private val _stylesFlow = SpecialMutableStateFlow<Set<CSSRulesHolder>>(emptySet())
val stylesFlow: StateFlow<Set<CSSRulesHolder>> = _stylesFlow.asStateFlow()
@Composable
fun draw() {
_stylesFlow.debounce(13L).collectAsState(emptySet()).value.forEach {
Style(it)
}
}
/**
* Adding [styleSheet] into the [Set] of included stylesheets. If you called [enableStyleSheetsAggregator],
* new styles will be enabled in the document
*/
fun addStyleSheet(styleSheet: CSSRulesHolder) {
_stylesFlow.value += styleSheet
}
/**
* Removed [styleSheet] into the [Set] of included stylesheets
*/
fun removeStyleSheet(styleSheet: CSSRulesHolder) {
_stylesFlow.value -= styleSheet
}
}
/**
* Drawing [StyleSheetsAggregator] in place. You may pass [Set] of [CSSRulesHolder]/[StyleSheet]s as preset of styles
*/
@Composable
fun enableStyleSheetsAggregator(
stylesPreset: Set<CSSRulesHolder> = emptySet(),
) {
remember {
stylesPreset.forEach {
StyleSheetsAggregator.addStyleSheet(it)
}
}
StyleSheetsAggregator.draw()
}
/**
* Will include [this] [CSSRulesHolder]/[StyleSheet] in the [StyleSheetsAggregator] using its
* [StyleSheetsAggregator.addStyleSheet]
*/
fun CSSRulesHolder.includeInStyleSheetsAggregator() {
StyleSheetsAggregator.addStyleSheet(this)
}

View File

@@ -68,9 +68,9 @@ class AccumulatorFlow<T>(
override suspend fun collectSafely(collector: FlowCollector<T>) { override suspend fun collectSafely(collector: FlowCollector<T>) {
val channel = Channel<T>(Channel.UNLIMITED, BufferOverflow.SUSPEND) val channel = Channel<T>(Channel.UNLIMITED, BufferOverflow.SUSPEND)
steps.send(SubscribeAccumulatorFlowStep(channel)) steps.send(SubscribeAccumulatorFlowStep(channel))
val result = runCatchingSafely { val result = runCatching {
for (data in channel) { for (data in channel) {
val emitResult = runCatchingSafely { val emitResult = runCatching {
collector.emit(data) collector.emit(data)
} }
if (emitResult.isSuccess || emitResult.exceptionOrNull() is CancellationException) { if (emitResult.isSuccess || emitResult.exceptionOrNull() is CancellationException) {

View File

@@ -0,0 +1,40 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.jvm.JvmInline
import kotlin.time.Duration
@JvmInline
private value class DebouncedByData<T>(
val millisToData: Pair<Long, T>
)
fun <T> Flow<T>.debouncedBy(timeout: (T) -> Long, markerFactory: (T) -> Any?): Flow<T> = channelFlow {
val jobs = mutableMapOf<Any?, Job>()
val mutex = Mutex()
subscribe(this) {
mutex.withLock {
val marker = markerFactory(it)
lateinit var job: Job
job = async {
delay(timeout(it))
mutex.withLock {
if (jobs[marker] === job) {
this@channelFlow.send(it)
jobs.remove(marker)
}
}
}
jobs[marker] ?.cancel()
jobs[marker] = job
}
}
}
fun <T> Flow<T>.debouncedBy(timeout: Long, markerFactory: (T) -> Any?): Flow<T> = debouncedBy({ timeout }, markerFactory)
fun <T> Flow<T>.debouncedBy(timeout: Duration, markerFactory: (T) -> Any?): Flow<T> = debouncedBy({ timeout.inWholeMilliseconds }, markerFactory)

View File

@@ -2,6 +2,7 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@@ -16,6 +17,45 @@ inline fun <T> Flow<T>.subscribe(scope: CoroutineScope, noinline block: suspend
* Use [subscribe], but all [block]s will be called inside of [safely] function. * Use [subscribe], but all [block]s will be called inside of [safely] function.
* Use [onException] to set up your reaction for [Throwable]s * Use [onException] to set up your reaction for [Throwable]s
*/ */
inline fun <T> Flow<T>.subscribeLogging(
scope: CoroutineScope,
noinline errorMessageBuilder: T.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
noinline block: suspend (T) -> Unit
) = subscribe(scope) {
it.runCatchingLogging(
errorMessageBuilder,
logger
) {
block(it)
}.getOrThrow()
}
/**
* Use [subscribeSafelyWithoutExceptions], but all exceptions will be passed to [defaultSafelyExceptionHandler]
*/
inline fun <T> Flow<T>.subscribeLoggingDropExceptions(
scope: CoroutineScope,
noinline errorMessageBuilder: T.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
noinline block: suspend (T) -> Unit
) = subscribe(scope) {
it.runCatchingLogging(
errorMessageBuilder,
logger
) {
block(it)
}
}
/**
* Use [subscribe], but all [block]s will be called inside of [safely] function.
* Use [onException] to set up your reaction for [Throwable]s
*/
@Deprecated(
"Will be removed soon due to replacement by subscribeLogging",
ReplaceWith("this.subscribeLogging(scope = scope, block = block)")
)
inline fun <T> Flow<T>.subscribeSafely( inline fun <T> Flow<T>.subscribeSafely(
scope: CoroutineScope, scope: CoroutineScope,
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler, noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
@@ -29,6 +69,10 @@ inline fun <T> Flow<T>.subscribeSafely(
/** /**
* Use [subscribeSafelyWithoutExceptions], but all exceptions will be passed to [defaultSafelyExceptionHandler] * Use [subscribeSafelyWithoutExceptions], but all exceptions will be passed to [defaultSafelyExceptionHandler]
*/ */
@Deprecated(
"Will be removed soon due to replacement by subscribeLoggingDropExceptions",
ReplaceWith("this.subscribeLoggingDropExceptions(scope = scope, block = block)")
)
inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions( inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions(
scope: CoroutineScope, scope: CoroutineScope,
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
@@ -42,6 +86,10 @@ inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions(
/** /**
* Use [subscribeSafelyWithoutExceptions], but all exceptions inside of [safely] will be skipped * Use [subscribeSafelyWithoutExceptions], but all exceptions inside of [safely] will be skipped
*/ */
@Deprecated(
"Will be removed soon due to replacement by subscribeLoggingDropExceptions",
ReplaceWith("this.subscribeLoggingDropExceptions(scope = scope, block = block)")
)
inline fun <T> Flow<T>.subscribeSafelySkippingExceptions( inline fun <T> Flow<T>.subscribeSafelySkippingExceptions(
scope: CoroutineScope, scope: CoroutineScope,
noinline block: suspend (T) -> Unit noinline block: suspend (T) -> Unit

View File

@@ -15,7 +15,7 @@ private class SubscribeAsyncReceiver<T>(
get() = dataChannel get() = dataChannel
init { init {
scope.launchSafelyWithoutExceptions { scope.launchLoggingDropExceptions {
for (data in dataChannel) { for (data in dataChannel) {
output(data) output(data)
} }

View File

@@ -15,6 +15,10 @@ import kotlin.coroutines.coroutineContext
* *
* @return [Result] with result of [block] if no exceptions or [Result] from [onException] execution * @return [Result] with result of [block] if no exceptions or [Result] from [onException] execution
*/ */
@Deprecated(
"This function become redundant since coroutines correctly handling throwing exceptions",
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { onException(it) }")
)
suspend inline fun <T> runCatchingSafely( suspend inline fun <T> runCatchingSafely(
onException: ExceptionHandler<T>, onException: ExceptionHandler<T>,
block: suspend () -> T block: suspend () -> T
@@ -29,6 +33,10 @@ suspend inline fun <T> runCatchingSafely(
} }
} }
@Deprecated(
"This function become redundant since coroutines correctly handling throwing exceptions",
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { onException(it) }")
)
suspend inline fun <T, R> R.runCatchingSafely( suspend inline fun <T, R> R.runCatchingSafely(
onException: ExceptionHandler<T>, onException: ExceptionHandler<T>,
block: suspend R.() -> T block: suspend R.() -> T
@@ -39,10 +47,18 @@ suspend inline fun <T, R> R.runCatchingSafely(
/** /**
* Launching [runCatchingSafely] with [defaultSafelyExceptionHandler] as `onException` parameter * Launching [runCatchingSafely] with [defaultSafelyExceptionHandler] as `onException` parameter
*/ */
@Deprecated(
"This function become redundant since coroutines correctly handling throwing exceptions",
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { defaultSafelyExceptionHandler(it) }")
)
suspend inline fun <T> runCatchingSafely( suspend inline fun <T> runCatchingSafely(
block: suspend () -> T block: suspend () -> T
): Result<T> = runCatchingSafely(defaultSafelyExceptionHandler, block) ): Result<T> = runCatchingSafely(defaultSafelyExceptionHandler, block)
@Deprecated(
"This function become redundant since coroutines correctly handling throwing exceptions",
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { defaultSafelyExceptionHandler(it) }")
)
suspend inline fun <T, R> R.runCatchingSafely( suspend inline fun <T, R> R.runCatchingSafely(
block: suspend R.() -> T block: suspend R.() -> T
): Result<T> = runCatchingSafely<T> { ): Result<T> = runCatchingSafely<T> {
@@ -73,6 +89,9 @@ suspend fun contextSafelyExceptionHandler() = coroutineContext[ContextSafelyExce
* After all, will be called [withContext] method with created [ContextSafelyExceptionHandler] and block which will call * After all, will be called [withContext] method with created [ContextSafelyExceptionHandler] and block which will call
* [safely] method with [safelyExceptionHandler] as onException parameter and [block] as execution block * [safely] method with [safelyExceptionHandler] as onException parameter and [block] as execution block
*/ */
@Deprecated(
"This function become redundant since coroutines correctly handling throwing exceptions",
)
suspend fun <T> safelyWithContextExceptionHandler( suspend fun <T> safelyWithContextExceptionHandler(
contextExceptionHandler: ExceptionHandler<Unit>, contextExceptionHandler: ExceptionHandler<Unit>,
safelyExceptionHandler: ExceptionHandler<T> = defaultSafelyExceptionHandler, safelyExceptionHandler: ExceptionHandler<T> = defaultSafelyExceptionHandler,
@@ -94,6 +113,10 @@ suspend fun <T> safelyWithContextExceptionHandler(
* *
* @see runCatchingSafely * @see runCatchingSafely
*/ */
@Deprecated(
"This function become redundant since coroutines correctly handling throwing exceptions",
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { onException(it) }.getOrThrow()")
)
suspend inline fun <T> safely( suspend inline fun <T> safely(
onException: ExceptionHandler<T>, onException: ExceptionHandler<T>,
block: suspend () -> T block: suspend () -> T
@@ -104,9 +127,17 @@ suspend inline fun <T> safely(
* *
* @see runCatchingSafely * @see runCatchingSafely
*/ */
@Deprecated(
"This function become redundant since coroutines correctly handling throwing exceptions",
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { defaultSafelyExceptionHandler(it) }.getOrThrow()")
)
suspend inline fun <T> safely( suspend inline fun <T> safely(
block: suspend () -> T block: suspend () -> T
): T = safely(defaultSafelyExceptionHandler, block) ): T = safely(defaultSafelyExceptionHandler, block)
@Deprecated(
"This function become redundant since coroutines correctly handling throwing exceptions",
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { defaultSafelyExceptionHandler(it) }.getOrThrow()")
)
suspend inline fun <T, R> R.safely( suspend inline fun <T, R> R.safely(
block: suspend R.() -> T block: suspend R.() -> T
): T = safely<T> { block() } ): T = safely<T> { block() }
@@ -137,11 +168,19 @@ val defaultSafelyWithoutExceptionHandlerWithNull: ExceptionHandler<Nothing?> = {
* Shortcut for [safely] with exception handler, that as expected must return null in case of impossible creating of * Shortcut for [safely] with exception handler, that as expected must return null in case of impossible creating of
* result from exception (instead of throwing it, by default always returns null) * result from exception (instead of throwing it, by default always returns null)
*/ */
@Deprecated(
"This function become redundant since coroutines correctly handling throwing exceptions",
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { onException(it) }.getOrNull()")
)
suspend fun <T> safelyWithoutExceptions( suspend fun <T> safelyWithoutExceptions(
onException: ExceptionHandler<T> = defaultSafelyExceptionHandler, onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
block: suspend () -> T block: suspend () -> T
): T? = runCatchingSafely(onException, block).getOrNull() ): T? = runCatchingSafely(onException, block).getOrNull()
@Deprecated(
"This function become redundant since coroutines correctly handling throwing exceptions",
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { onException(it) }.getOrNull()")
)
suspend fun <T> runCatchingSafelyWithoutExceptions( suspend fun <T> runCatchingSafelyWithoutExceptions(
onException: ExceptionHandler<T?> = defaultSafelyExceptionHandler, onException: ExceptionHandler<T?> = defaultSafelyExceptionHandler,
block: suspend () -> T block: suspend () -> T

View File

@@ -0,0 +1,55 @@
package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.e
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
fun CoroutineScope.launchLogging(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
) = launch(context, start) {
runCatching { block() }.onFailure {
logger.e(it) { errorMessageBuilder(it) }
}.getOrThrow()
}
fun CoroutineScope.launchLoggingDropExceptions(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
) = launch(context, start) {
runCatching { block() }.onFailure {
logger.e(it) { errorMessageBuilder(it) }
} // just dropping exception
}
fun <T> CoroutineScope.asyncLogging(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
) = async(context, start) {
runCatching { block() }.onFailure {
logger.e(it) { errorMessageBuilder(it) }
}.getOrThrow()
}
fun <T> CoroutineScope.asyncLoggingDropExceptions(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
) = async(context, start) {
runCatching { block() }.onFailure {
logger.e(it) { errorMessageBuilder(it) }
}
}

View File

@@ -4,6 +4,10 @@ import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
@Deprecated(
"This method will be removed soon. Use launchLogging instead",
ReplaceWith("this.launchLogging(context = context, start = start, block = block)")
)
fun CoroutineScope.launchSafely( fun CoroutineScope.launchSafely(
context: CoroutineContext = EmptyCoroutineContext, context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT, start: CoroutineStart = CoroutineStart.DEFAULT,
@@ -15,6 +19,10 @@ fun CoroutineScope.launchSafely(
} }
} }
@Deprecated(
"This method will be removed soon. Use launchLoggingDropExceptions instead",
ReplaceWith("this.launchLoggingDropExceptions(context = context, start = start, block = block)")
)
fun CoroutineScope.launchSafelyWithoutExceptions( fun CoroutineScope.launchSafelyWithoutExceptions(
context: CoroutineContext = EmptyCoroutineContext, context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT, start: CoroutineStart = CoroutineStart.DEFAULT,
@@ -26,6 +34,10 @@ fun CoroutineScope.launchSafelyWithoutExceptions(
} }
} }
@Deprecated(
"This method will be removed soon. Use asyncLogging instead",
ReplaceWith("this.asyncLogging(context = context, start = start, block = block)")
)
fun <T> CoroutineScope.asyncSafely( fun <T> CoroutineScope.asyncSafely(
context: CoroutineContext = EmptyCoroutineContext, context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT, start: CoroutineStart = CoroutineStart.DEFAULT,
@@ -37,6 +49,10 @@ fun <T> CoroutineScope.asyncSafely(
} }
} }
@Deprecated(
"This method will be removed soon. Use asyncLoggingDropExceptions instead",
ReplaceWith("this.asyncLoggingDropExceptions(context = context, start = start, block = block)")
)
fun <T> CoroutineScope.asyncSafelyWithoutExceptions( fun <T> CoroutineScope.asyncSafelyWithoutExceptions(
context: CoroutineContext = EmptyCoroutineContext, context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT, start: CoroutineStart = CoroutineStart.DEFAULT,

View File

@@ -0,0 +1,3 @@
package dev.inmo.micro_utils.coroutines
inline fun <T> Result<T>.replaceIfFailure(onException: (Throwable) -> T) = if (isSuccess) { this } else { runCatching { onException(exceptionOrNull()!!) } }

View File

@@ -0,0 +1,12 @@
package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.e
inline fun <T, R> R.runCatchingLogging(
noinline errorMessageBuilder: R.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
block: R.() -> T
) = runCatching(block).onFailure {
logger.e(it) { errorMessageBuilder(it) }
}

View File

@@ -0,0 +1,42 @@
import dev.inmo.micro_utils.coroutines.debouncedBy
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class DebouncedByTests {
@Test
fun testThatParallelDebouncingWorksCorrectly() = runTest {
val dataToMarkerFactories = listOf(
1 to 0,
2 to 1,
3 to 2,
4 to 0,
5 to 1,
6 to 2,
7 to 0,
8 to 1,
9 to 2,
)
val collected = mutableListOf<Int>()
dataToMarkerFactories.asFlow().debouncedBy(10L) {
it.second
}.collect {
when (it.second) {
0 -> assertEquals(7, it.first)
1 -> assertEquals(8, it.first)
2 -> assertEquals(9, it.first)
else -> error("wtf")
}
collected.add(it.first)
}
val expectedList = listOf(7, 8, 9)
assertEquals(expectedList, collected)
assertTrue { collected.containsAll(expectedList) }
assertTrue { expectedList.containsAll(collected) }
}
}

View File

@@ -12,6 +12,7 @@ kotlin {
dependencies { dependencies {
api project(":micro_utils.common") api project(":micro_utils.common")
api project(":micro_utils.coroutines") api project(":micro_utils.coroutines")
api libs.kslog
} }
} }
} }

View File

@@ -1,5 +1,7 @@
package dev.inmo.micro_utils.fsm.common package dev.inmo.micro_utils.fsm.common
import dev.inmo.kslog.common.TagLogger
import dev.inmo.kslog.common.e
import dev.inmo.micro_utils.common.Optional import dev.inmo.micro_utils.common.Optional
import dev.inmo.micro_utils.coroutines.* import dev.inmo.micro_utils.coroutines.*
import dev.inmo.micro_utils.fsm.common.utils.StateHandlingErrorHandler import dev.inmo.micro_utils.fsm.common.utils.StateHandlingErrorHandler
@@ -68,6 +70,7 @@ open class DefaultStatesMachine <T: State>(
protected val handlers: List<CheckableHandlerHolder<in T, T>>, protected val handlers: List<CheckableHandlerHolder<in T, T>>,
protected val onStateHandlingErrorHandler: StateHandlingErrorHandler<T> = defaultStateHandlingErrorHandler() protected val onStateHandlingErrorHandler: StateHandlingErrorHandler<T> = defaultStateHandlingErrorHandler()
) : StatesMachine<T> { ) : StatesMachine<T> {
protected val logger = TagLogger(this::class.simpleName!!)
/** /**
* Will call [launchStateHandling] for state handling * Will call [launchStateHandling] for state handling
*/ */
@@ -96,7 +99,13 @@ open class DefaultStatesMachine <T: State>(
statesJobsMutex.withLock { statesJobsMutex.withLock {
statesJobs[actualState] ?.cancel() statesJobs[actualState] ?.cancel()
statesJobs[actualState] = scope.launch { statesJobs[actualState] = scope.launch {
performUpdate(actualState) runCatching {
performUpdate(actualState)
}.onFailure {
logger.e(it) {
"Unable to perform update of state from $actualState"
}
}.getOrThrow()
}.also { job -> }.also { job ->
job.invokeOnCompletion { _ -> job.invokeOnCompletion { _ ->
scope.launch { scope.launch {
@@ -119,8 +128,10 @@ open class DefaultStatesMachine <T: State>(
*/ */
override fun start(scope: CoroutineScope): Job { override fun start(scope: CoroutineScope): Job {
val supervisorScope = scope.LinkedSupervisorScope() val supervisorScope = scope.LinkedSupervisorScope()
supervisorScope.launchSafelyWithoutExceptions { supervisorScope.launchLoggingDropExceptions {
(statesManager.getActiveStates().asFlow() + statesManager.onStartChain).subscribeSafelyWithoutExceptions(supervisorScope) { (statesManager.getActiveStates().asFlow() + statesManager.onStartChain).subscribeSafelyWithoutExceptions(
supervisorScope
) {
supervisorScope.launch { performStateUpdate(Optional.absent(), it, supervisorScope) } supervisorScope.launch { performStateUpdate(Optional.absent(), it, supervisorScope) }
} }
statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(supervisorScope) { statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(supervisorScope) {
@@ -131,7 +142,7 @@ open class DefaultStatesMachine <T: State>(
statesJobsMutex.withLock { statesJobsMutex.withLock {
val stateInMap = statesJobs.keys.firstOrNull { stateInMap -> stateInMap == removedState } val stateInMap = statesJobs.keys.firstOrNull { stateInMap -> stateInMap == removedState }
if (stateInMap === removedState) { if (stateInMap === removedState) {
statesJobs[stateInMap] ?.cancel() statesJobs[stateInMap]?.cancel()
} }
} }
} }

View File

@@ -9,7 +9,12 @@ interface StatesManager<T : State> {
/** /**
* Must set current set using [State.context] * It is expected, that [new] state will be saved in manager.
*
* If [new] context will not be equal to [old] one, it must do some check of availability for replacement
* of potentially exists state on [new] context. If this state can't be replaced, it will throw [IllegalStateException]
*
* @throws IllegalStateException - in case when [new] [State] can't be set
*/ */
suspend fun update(old: T, new: T) suspend fun update(old: T, new: T)

View File

@@ -1,5 +1,6 @@
package dev.inmo.micro_utils.fsm.common package dev.inmo.micro_utils.fsm.common
import dev.inmo.kslog.common.e
import dev.inmo.micro_utils.common.* import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.fsm.common.utils.StateHandlingErrorHandler import dev.inmo.micro_utils.fsm.common.utils.StateHandlingErrorHandler
import dev.inmo.micro_utils.fsm.common.utils.defaultStateHandlingErrorHandler import dev.inmo.micro_utils.fsm.common.utils.defaultStateHandlingErrorHandler
@@ -44,7 +45,13 @@ open class DefaultUpdatableStatesMachine<T : State>(
val job = previousState.mapOnPresented { val job = previousState.mapOnPresented {
statesJobs.remove(it) statesJobs.remove(it)
} ?.takeIf { it.isActive } ?: scope.launch { } ?.takeIf { it.isActive } ?: scope.launch {
performUpdate(actualState) runCatching {
performUpdate(actualState)
}.onFailure {
logger.e(it) {
"Unable to perform update of state up to $actualState"
}
}.getOrThrow()
}.also { job -> }.also { job ->
job.invokeOnCompletion { _ -> job.invokeOnCompletion { _ ->
scope.launch { scope.launch {

View File

@@ -1,10 +1,11 @@
package dev.inmo.micro_utils.fsm.common.managers package dev.inmo.micro_utils.fsm.common.managers
import dev.inmo.micro_utils.coroutines.SmartRWLocker
import dev.inmo.micro_utils.coroutines.withReadAcquire
import dev.inmo.micro_utils.coroutines.withWriteLock
import dev.inmo.micro_utils.fsm.common.State import dev.inmo.micro_utils.fsm.common.State
import dev.inmo.micro_utils.fsm.common.StatesManager import dev.inmo.micro_utils.fsm.common.StatesManager
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/** /**
* Implement this repo if you want to use some custom repo for [DefaultStatesManager] * Implement this repo if you want to use some custom repo for [DefaultStatesManager]
@@ -19,6 +20,14 @@ interface DefaultStatesManagerRepo<T : State> {
* NOT be removed * NOT be removed
*/ */
suspend fun removeState(state: T) suspend fun removeState(state: T)
/**
* Semantically, calls [removeState] and then [set]
*/
suspend fun removeAndSet(toRemove: T, toSet: T) {
removeState(toRemove)
set(toSet)
}
/** /**
* @return Current list of available and saved states * @return Current list of available and saved states
*/ */
@@ -58,7 +67,7 @@ open class DefaultStatesManager<T : State>(
protected val _onEndChain = MutableSharedFlow<T>(0) protected val _onEndChain = MutableSharedFlow<T>(0)
override val onEndChain: Flow<T> = _onEndChain.asSharedFlow() override val onEndChain: Flow<T> = _onEndChain.asSharedFlow()
protected val mapMutex = Mutex() protected val internalLocker = SmartRWLocker()
constructor( constructor(
repo: DefaultStatesManagerRepo<T>, repo: DefaultStatesManagerRepo<T>,
@@ -68,28 +77,30 @@ open class DefaultStatesManager<T : State>(
onUpdateContextsConflictResolver = onContextsConflictResolver onUpdateContextsConflictResolver = onContextsConflictResolver
) )
override suspend fun update(old: T, new: T) = mapMutex.withLock { override suspend fun update(old: T, new: T) = internalLocker.withWriteLock {
val stateByOldContext: T? = repo.getContextState(old.context) val stateByOldContext: T? = repo.getContextState(old.context)
when { when {
stateByOldContext != old -> return@withLock stateByOldContext != old -> return@withWriteLock
stateByOldContext == null || old.context == new.context -> { old.context == new.context -> {
repo.removeState(old) repo.removeAndSet(old, new)
repo.set(new)
_onChainStateUpdated.emit(old to new) _onChainStateUpdated.emit(old to new)
} }
else -> { old.context != new.context -> {
val stateOnNewOneContext = repo.getContextState(new.context) val stateOnNewOneContext = repo.getContextState(new.context)
if (stateOnNewOneContext == null || onUpdateContextsConflictResolver(old, new, stateOnNewOneContext)) { if (stateOnNewOneContext == null || onUpdateContextsConflictResolver(old, new, stateOnNewOneContext)) {
stateOnNewOneContext ?.let { endChainWithoutLock(it) } stateOnNewOneContext ?.let { endChainWithoutLock(it) }
repo.removeState(old) repo.removeAndSet(old, new)
repo.set(new)
_onChainStateUpdated.emit(old to new) _onChainStateUpdated.emit(old to new)
} else {
error(
"Unable to update state from $old to $new due to false answer from $onUpdateContextsConflictResolver and state on old context $stateOnNewOneContext"
)
} }
} }
} }
} }
override suspend fun startChain(state: T) = mapMutex.withLock { override suspend fun startChain(state: T) = internalLocker.withWriteLock {
val stateOnContext = repo.getContextState(state.context) val stateOnContext = repo.getContextState(state.context)
if (stateOnContext == null || onStartContextsConflictResolver(stateOnContext, state)) { if (stateOnContext == null || onStartContextsConflictResolver(stateOnContext, state)) {
stateOnContext ?.let { stateOnContext ?.let {
@@ -108,11 +119,13 @@ open class DefaultStatesManager<T : State>(
} }
override suspend fun endChain(state: T) { override suspend fun endChain(state: T) {
mapMutex.withLock { internalLocker.withWriteLock {
endChainWithoutLock(state) endChainWithoutLock(state)
} }
} }
override suspend fun getActiveStates(): List<T> = repo.getStates() override suspend fun getActiveStates(): List<T> = internalLocker.withReadAcquire {
repo.getStates()
}
} }

View File

@@ -1,25 +1,59 @@
package dev.inmo.micro_utils.fsm.repos.common package dev.inmo.micro_utils.fsm.repos.common
import dev.inmo.kslog.common.TagLogger
import dev.inmo.kslog.common.i
import dev.inmo.micro_utils.coroutines.SmartRWLocker
import dev.inmo.micro_utils.coroutines.withReadAcquire
import dev.inmo.micro_utils.coroutines.withWriteLock
import dev.inmo.micro_utils.fsm.common.State import dev.inmo.micro_utils.fsm.common.State
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
import dev.inmo.micro_utils.repos.* import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.pagination.getAll import dev.inmo.micro_utils.repos.pagination.getAll
import dev.inmo.micro_utils.repos.unset
class KeyValueBasedDefaultStatesManagerRepo<T : State>( class KeyValueBasedDefaultStatesManagerRepo<T : State>(
private val keyValueRepo: KeyValueRepo<Any, T> private val keyValueRepo: KeyValueRepo<Any, T>
) : DefaultStatesManagerRepo<T> { ) : DefaultStatesManagerRepo<T> {
private val locker = SmartRWLocker()
private val logger = TagLogger("KeyValueBasedDefaultStatesManagerRepo")
override suspend fun set(state: T) { override suspend fun set(state: T) {
keyValueRepo.set(state.context, state) locker.withWriteLock {
} keyValueRepo.set(state.context, state)
logger.i { "Set ${state.context} value to $state" }
override suspend fun removeState(state: T) {
if (keyValueRepo.get(state.context) == state) {
keyValueRepo.unset(state.context)
} }
} }
override suspend fun getStates(): List<T> = keyValueRepo.getAll { keys(it) }.map { it.second } override suspend fun removeState(state: T) {
override suspend fun getContextState(context: Any): T? = keyValueRepo.get(context) locker.withWriteLock {
if (keyValueRepo.get(state.context) == state) {
keyValueRepo.unset(state.context)
logger.i { "Unset $state" }
}
}
}
override suspend fun contains(context: Any): Boolean = keyValueRepo.contains(context) override suspend fun removeAndSet(toRemove: T, toSet: T) {
locker.withWriteLock {
when {
toRemove.context == toSet.context -> {
keyValueRepo.set(toSet.context, toSet)
}
else -> {
keyValueRepo.set(toSet.context, toSet)
keyValueRepo.unset(toRemove)
}
}
}
}
override suspend fun getStates(): List<T> = locker.withReadAcquire {
keyValueRepo.getAll { keys(it) }.map { it.second }
}
override suspend fun getContextState(context: Any): T? = locker.withReadAcquire {
keyValueRepo.get(context)
}
override suspend fun contains(context: Any): Boolean = locker.withReadAcquire {
keyValueRepo.contains(context)
}
} }

View File

@@ -15,5 +15,5 @@ crypto_js_version=4.1.1
# Project data # Project data
group=dev.inmo group=dev.inmo
version=0.22.1 version=0.24.7
android_code_version=267 android_code_version=287

View File

@@ -1,43 +1,42 @@
[versions] [versions]
kt = "2.0.20" kt = "2.1.10"
kt-serialization = "1.7.2" kt-serialization = "1.8.0"
kt-coroutines = "1.8.1" kt-coroutines = "1.10.1"
kslog = "1.3.6" kslog = "1.4.1"
jb-compose = "1.7.0-alpha03" jb-compose = "1.7.3"
jb-exposed = "0.53.0" jb-exposed = "0.59.0"
jb-dokka = "1.9.20" jb-dokka = "2.0.0"
sqlite = "3.46.0.1" sqlite = "3.49.1.0"
korlibs = "5.4.0" korlibs = "5.4.0"
uuid = "0.8.4" uuid = "0.8.4"
ktor = "2.3.12" ktor = "3.1.0"
gh-release = "2.5.2" gh-release = "2.5.2"
koin = "3.5.6" koin = "4.0.2"
okio = "3.9.0" okio = "3.10.2"
ksp = "2.0.20-1.0.24" ksp = "2.1.10-1.0.30"
kotlin-poet = "1.18.1" kotlin-poet = "1.18.1"
versions = "0.51.0" versions = "0.51.0"
android-gradle = "8.2.2" android-gradle = "8.7.+"
dexcount = "4.0.0" dexcount = "4.0.0"
android-coreKtx = "1.13.1" android-coreKtx = "1.15.0"
android-recyclerView = "1.3.2" android-recyclerView = "1.4.0"
android-appCompat = "1.7.0" android-appCompat = "1.7.0"
android-fragment = "1.8.2" android-fragment = "1.8.6"
android-espresso = "3.6.1" android-espresso = "3.6.1"
android-test = "1.2.1" android-test = "1.2.1"
android-compose-material3 = "1.2.1"
android-props-minSdk = "21" android-props-minSdk = "21"
android-props-compileSdk = "35" android-props-compileSdk = "35"
@@ -85,11 +84,11 @@ 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" }
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" }
android-appCompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "android-appCompat" } android-appCompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "android-appCompat" }
android-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "android-compose-material3" }
android-fragment = { module = "androidx.fragment:fragment", version.ref = "android-fragment" } android-fragment = { module = "androidx.fragment:fragment", version.ref = "android-fragment" }
android-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" } android-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" }
android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" } android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" }

View File

@@ -1,3 +1,7 @@
if (ext.getProperties()["do_publish"] == false) {
return
}
apply plugin: 'maven-publish' apply plugin: 'maven-publish'
task javadocsJar(type: Jar) { task javadocsJar(type: Jar) {

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.9-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -5,6 +5,8 @@ plugins {
id "com.google.devtools.ksp" id "com.google.devtools.ksp"
} }
ext.do_publish = false
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project" apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,28 +1,38 @@
package dev.inmo.micro_ksp.generator package dev.inmo.micro_ksp.generator
import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSDeclaration
import com.google.devtools.ksp.symbol.KSFile import com.google.devtools.ksp.symbol.KSFile
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FileSpec
import java.io.File import java.io.File
fun KSClassDeclaration.writeFile( fun KSDeclaration.writeFile(
prefix: String = "", prefix: String = "",
suffix: String = "", suffix: String = "",
relatedPath: String = "", relatedPath: String = "",
force: Boolean = false, force: Boolean = false,
forceUppercase: Boolean = true,
fileSpecBuilder: () -> FileSpec fileSpecBuilder: () -> FileSpec
) { ) {
val containingFile = containingFile!! val containingFile = containingFile!!
val simpleName = if (forceUppercase) {
val rawSimpleName = simpleName.asString()
rawSimpleName.replaceFirst(rawSimpleName.first().toString(), rawSimpleName.first().uppercase())
} else {
simpleName.asString()
}
File( File(
File( File(
File(containingFile.filePath).parent, File(containingFile.filePath).parent,
relatedPath relatedPath
), ),
"$prefix${simpleName.asString()}$suffix.kt" "$prefix${simpleName}$suffix.kt"
).takeIf { force || !it.exists() } ?.apply { ).takeIf { force || !it.exists() } ?.apply {
parentFile.mkdirs() parentFile.mkdirs()
val fileSpec = fileSpecBuilder()
writer().use { writer -> writer().use { writer ->
fileSpecBuilder().writeTo(writer) fileSpec.writeTo(writer)
} }
} }
} }
@@ -42,8 +52,9 @@ fun KSFile.writeFile(
"$prefix${fileName.dropLastWhile { it != '.' }.removeSuffix(".")}$suffix.kt" "$prefix${fileName.dropLastWhile { it != '.' }.removeSuffix(".")}$suffix.kt"
).takeIf { force || !it.exists() } ?.apply { ).takeIf { force || !it.exists() } ?.apply {
parentFile.mkdirs() parentFile.mkdirs()
val fileSpec = fileSpecBuilder()
writer().use { writer -> writer().use { writer ->
fileSpecBuilder().writeTo(writer) fileSpec.writeTo(writer)
} }
} }
} }

View File

@@ -0,0 +1,25 @@
package dev.inmo.micro_ksp.generator
import com.google.devtools.ksp.KSTypeNotPresentException
import com.google.devtools.ksp.KSTypesNotPresentException
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.symbol.KSType
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.ksp.toClassName
import kotlin.reflect.KClass
@OptIn(KspExperimental::class)
inline fun convertToClassName(getter: () -> KClass<*>) = try {
getter().asClassName()
} catch (e: KSTypeNotPresentException) {
e.ksType.toClassName()
}
@OptIn(KspExperimental::class)
inline fun convertToClassNames(getter: () -> List<KClass<*>>) = try {
getter().map { it.asClassName() }
} catch (e: KSTypesNotPresentException) {
e.ksTypes.map {
it.toClassName()
}
}

View File

@@ -0,0 +1,13 @@
package dev.inmo.micro_ksp.generator
import com.google.devtools.ksp.symbol.KSClassDeclaration
val KSClassDeclaration.buildSubFileName: String
get() {
val parentDeclarationCaptured = parentDeclaration
val simpleNameString = simpleName.asString()
return when (parentDeclarationCaptured) {
is KSClassDeclaration -> parentDeclarationCaptured.buildSubFileName
else -> ""
} + simpleNameString
}

View File

@@ -0,0 +1,8 @@
package dev.inmo.micro_ksp.generator
import com.google.devtools.ksp.symbol.KSClassDeclaration
val KSClassDeclaration.companion
get() = declarations.firstNotNullOfOrNull {
(it as? KSClassDeclaration)?.takeIf { it.isCompanionObject }
}

View File

@@ -0,0 +1,11 @@
package dev.inmo.micro_ksp.generator
import com.google.devtools.ksp.symbol.KSClassDeclaration
fun KSClassDeclaration.resolveSubclasses(): List<KSClassDeclaration> {
return (getSealedSubclasses().flatMap {
it.resolveSubclasses()
}.ifEmpty {
sequenceOf(this)
}).toList()
}

View File

@@ -0,0 +1,17 @@
package dev.inmo.micro_utils.ksp.sealed.generator
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getAnnotationsByType
import com.google.devtools.ksp.symbol.KSClassDeclaration
import dev.inmo.micro_utils.ksp.sealed.GenerateSealedTypesWorkaround
import dev.inmo.micro_utils.ksp.sealed.GenerateSealedWorkaround
import dev.inmo.microutils.kps.sealed.GenerateSealedWorkaround as OldGenerateSealedWorkaround
@OptIn(KspExperimental::class)
val KSClassDeclaration.getGenerateSealedWorkaroundAnnotation
get() = (getAnnotationsByType(GenerateSealedWorkaround::class).firstOrNull() ?: getAnnotationsByType(OldGenerateSealedWorkaround::class).firstOrNull())
@OptIn(KspExperimental::class)
val KSClassDeclaration.getGenerateSealedTypesWorkaroundAnnotation
get() = getAnnotationsByType(GenerateSealedTypesWorkaround::class).firstOrNull()

View File

@@ -6,19 +6,17 @@ import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.symbol.* import com.google.devtools.ksp.symbol.*
import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.asTypeName
import com.squareup.kotlinpoet.ksp.toClassName import com.squareup.kotlinpoet.ksp.toClassName
import dev.inmo.micro_ksp.generator.buildSubFileName
import dev.inmo.micro_ksp.generator.companion
import dev.inmo.micro_ksp.generator.findSubClasses import dev.inmo.micro_ksp.generator.findSubClasses
import dev.inmo.micro_ksp.generator.writeFile import dev.inmo.micro_ksp.generator.writeFile
import dev.inmo.microutils.kps.sealed.GenerateSealedWorkaround import dev.inmo.micro_utils.ksp.sealed.GenerateSealedTypesWorkaround
import dev.inmo.micro_utils.ksp.sealed.GenerateSealedWorkaround
import java.io.File import java.io.File
import kotlin.reflect.KClass
class Processor( class Processor(
private val codeGenerator: CodeGenerator private val codeGenerator: CodeGenerator
@@ -51,10 +49,10 @@ class Processor(
ksClassDeclaration: KSClassDeclaration, ksClassDeclaration: KSClassDeclaration,
resolver: Resolver resolver: Resolver
) { ) {
val annotation = ksClassDeclaration.getAnnotationsByType(GenerateSealedWorkaround::class).first() val annotation = ksClassDeclaration.getGenerateSealedWorkaroundAnnotation
val subClasses = ksClassDeclaration.resolveSubclasses( val subClasses = ksClassDeclaration.resolveSubclasses(
searchIn = resolver.getAllFiles(), searchIn = resolver.getAllFiles(),
allowNonSealed = annotation.includeNonSealedSubTypes allowNonSealed = annotation ?.includeNonSealedSubTypes ?: false
).distinct() ).distinct()
val subClassesNames = subClasses.filter { val subClassesNames = subClasses.filter {
when (it.classKind) { when (it.classKind) {
@@ -93,7 +91,10 @@ class Processor(
) )
addFunction( addFunction(
FunSpec.builder("values").apply { FunSpec.builder("values").apply {
receiver(ClassName(className.packageName, *className.simpleNames.toTypedArray(), "Companion")) val companion = ksClassDeclaration.takeIf { it.isCompanionObject } ?.toClassName()
?: ksClassDeclaration.companion ?.toClassName()
?: ClassName(className.packageName, *className.simpleNames.toTypedArray(), "Companion")
receiver(companion)
returns(setType) returns(setType)
addCode( addCode(
CodeBlock.of( CodeBlock.of(
@@ -104,10 +105,68 @@ class Processor(
) )
} }
@OptIn(KspExperimental::class)
private fun FileSpec.Builder.generateSealedTypesWorkaround(
ksClassDeclaration: KSClassDeclaration,
resolver: Resolver
) {
val annotation = ksClassDeclaration.getGenerateSealedTypesWorkaroundAnnotation
val subClasses = ksClassDeclaration.resolveSubclasses(
searchIn = resolver.getAllFiles(),
allowNonSealed = annotation ?.includeNonSealedSubTypes ?: false
).distinct()
val subClassesNames = subClasses.filter {
it.getAnnotationsByType(GenerateSealedTypesWorkaround.Exclude::class).count() == 0
}.sortedBy {
(it.getAnnotationsByType(GenerateSealedTypesWorkaround.Order::class).firstOrNull()) ?.order ?: 0
}.map {
it.toClassName()
}.toList()
val className = ksClassDeclaration.toClassName()
val setType = Set::class.asTypeName().parameterizedBy(
KClass::class.asTypeName().parameterizedBy(
TypeVariableName(
"out ${ksClassDeclaration.asStarProjectedType().toClassName().simpleNames.joinToString(".")}",
)
)
)
addProperty(
PropertySpec.builder(
"subtypes",
setType
).apply {
modifiers.add(
KModifier.PRIVATE
)
initializer(
CodeBlock.of(
"""setOf(${subClassesNames.joinToString(",\n") { it.simpleNames.joinToString(".") + "::class" }})"""
)
)
}.build()
)
addFunction(
FunSpec.builder("subtypes").apply {
val companion = ksClassDeclaration.takeIf { it.isCompanionObject } ?.toClassName()
?: ksClassDeclaration.companion ?.toClassName()
?: ClassName(className.packageName, *className.simpleNames.toTypedArray(), "Companion")
receiver(companion)
returns(setType)
addCode(
CodeBlock.of(
"""return subtypes"""
)
)
}.build()
)
}
@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.getAnnotationsByType(GenerateSealedWorkaround::class).first().prefix val prefix = (it.getGenerateSealedWorkaroundAnnotation) ?.prefix ?.takeIf {
it.isNotEmpty()
} ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "")
it.writeFile(prefix = prefix, suffix = "SealedWorkaround") { it.writeFile(prefix = prefix, suffix = "SealedWorkaround") {
FileSpec.builder( FileSpec.builder(
it.packageName.asString(), it.packageName.asString(),
@@ -124,6 +183,26 @@ class Processor(
}.build() }.build()
} }
} }
(resolver.getSymbolsWithAnnotation(GenerateSealedTypesWorkaround::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach {
val prefix = (it.getGenerateSealedTypesWorkaroundAnnotation) ?.prefix ?.takeIf {
it.isNotEmpty()
} ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "")
it.writeFile(prefix = prefix, suffix = "SealedTypesWorkaround") {
FileSpec.builder(
it.packageName.asString(),
"${it.simpleName.getShortName()}SealedTypesWorkaround"
).apply {
addFileComment(
"""
THIS CODE HAVE BEEN GENERATED AUTOMATICALLY
TO REGENERATE IT JUST DELETE FILE
ORIGINAL FILE: ${it.containingFile ?.fileName}
""".trimIndent()
)
generateSealedTypesWorkaround(it, resolver)
}.build()
}
}
return emptyList() return emptyList()
} }

View File

@@ -5,6 +5,8 @@ plugins {
id "com.google.devtools.ksp" id "com.google.devtools.ksp"
} }
ext.do_publish = false
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project" apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,14 +1,19 @@
package dev.inmo.micro_utils.ksp.sealed.generator.test package dev.inmo.micro_utils.ksp.sealed.generator.test
import dev.inmo.microutils.kps.sealed.GenerateSealedWorkaround import dev.inmo.micro_utils.ksp.sealed.GenerateSealedTypesWorkaround
import dev.inmo.micro_utils.ksp.sealed.GenerateSealedWorkaround
@GenerateSealedWorkaround @GenerateSealedWorkaround
@GenerateSealedTypesWorkaround
sealed interface Test { sealed interface Test {
@GenerateSealedWorkaround.Order(2) @GenerateSealedWorkaround.Order(2)
@GenerateSealedTypesWorkaround.Exclude
object A : Test object A : Test
@GenerateSealedWorkaround.Exclude @GenerateSealedWorkaround.Exclude
@GenerateSealedTypesWorkaround.Order(2)
object B : Test object B : Test
@GenerateSealedWorkaround.Order(0) @GenerateSealedWorkaround.Order(0)
@GenerateSealedTypesWorkaround.Order(0)
object C : Test object C : Test
// Required for successful sealed workaround generation // Required for successful sealed workaround generation

View File

@@ -0,0 +1,12 @@
// THIS CODE HAVE BEEN GENERATED AUTOMATICALLY
// TO REGENERATE IT JUST DELETE FILE
// ORIGINAL FILE: Test.kt
package dev.inmo.micro_utils.ksp.`sealed`.generator.test
import kotlin.collections.Set
import kotlin.reflect.KClass
private val subtypes: Set<KClass<out Test>> = setOf(Test.C::class,
Test.B::class)
public fun Test.Companion.subtypes(): Set<KClass<out Test>> = subtypes

View File

@@ -0,0 +1,40 @@
import dev.inmo.micro_utils.ksp.sealed.generator.test.subtypes
import dev.inmo.micro_utils.ksp.sealed.generator.test.values
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class TestTests {
@Test
fun testThatAfterCompilationTestWorkaroundsHaveCorrectValues() {
val correctValues = arrayOf(
dev.inmo.micro_utils.ksp.sealed.generator.test.Test.C,
dev.inmo.micro_utils.ksp.sealed.generator.test.Test.A,
)
val correctSubtypes = arrayOf(
dev.inmo.micro_utils.ksp.sealed.generator.test.Test.C::class,
dev.inmo.micro_utils.ksp.sealed.generator.test.Test.B::class,
)
assertEquals(
correctValues.size, dev.inmo.micro_utils.ksp.sealed.generator.test.Test.values().size
)
correctValues.forEachIndexed { index, value ->
assertTrue(
value === dev.inmo.micro_utils.ksp.sealed.generator.test.Test.values().elementAt(index)
)
}
assertEquals(
correctSubtypes.size, dev.inmo.micro_utils.ksp.sealed.generator.test.Test.subtypes().size
)
correctSubtypes.forEachIndexed { index, value ->
assertTrue(
value.qualifiedName != null
)
assertTrue(
value.qualifiedName === dev.inmo.micro_utils.ksp.sealed.generator.test.Test.subtypes().elementAt(index).qualifiedName
)
}
}
}

View File

@@ -0,0 +1,15 @@
package dev.inmo.micro_utils.ksp.sealed
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS)
annotation class GenerateSealedTypesWorkaround(
val prefix: String = "",
val includeNonSealedSubTypes: Boolean = false,
) {
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS)
annotation class Order(val order: Int)
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS)
annotation class Exclude
}

View File

@@ -1,10 +1,10 @@
package dev.inmo.microutils.kps.sealed package dev.inmo.micro_utils.ksp.sealed
@Retention(AnnotationRetention.BINARY) @Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
annotation class GenerateSealedWorkaround( annotation class GenerateSealedWorkaround(
val prefix: String = "", val prefix: String = "",
val includeNonSealedSubTypes: Boolean = false val includeNonSealedSubTypes: Boolean = false,
) { ) {
@Retention(AnnotationRetention.BINARY) @Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)

View File

@@ -0,0 +1,6 @@
package dev.inmo.microutils.kps.sealed
import dev.inmo.micro_utils.ksp.sealed.GenerateSealedWorkaround
@Deprecated("Replaced", ReplaceWith("GenerateSealedWorkaround", "dev.inmo.micro_utils.ksp.sealed.GenerateSealedWorkaround"))
typealias GenerateSealedWorkaround = GenerateSealedWorkaround

View File

@@ -0,0 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -0,0 +1,22 @@
plugins {
id "org.jetbrains.kotlin.jvm"
}
apply from: "$publish_jvm"
repositories {
mavenCentral()
}
dependencies {
implementation libs.kt.stdlib
api project(":micro_utils.ksp.generator")
api project(":micro_utils.ksp.variations")
api libs.kotlin.poet
api libs.ksp
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

View File

@@ -0,0 +1,243 @@
package dev.inmo.micro_utils.ksp.variations.generator
import com.google.devtools.ksp.KSTypeNotPresentException
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getAnnotationsByType
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.symbol.*
import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.ksp.toAnnotationSpec
import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.toKModifier
import com.squareup.kotlinpoet.ksp.toTypeName
import dev.inmo.micro_ksp.generator.convertToClassName
import dev.inmo.micro_ksp.generator.convertToClassNames
import dev.inmo.micro_ksp.generator.findSubClasses
import dev.inmo.micro_ksp.generator.writeFile
import dev.inmo.micro_utils.ksp.variations.GenerateVariations
import dev.inmo.micro_utils.ksp.variations.GenerationVariant
import kotlin.math.pow
class Processor(
private val codeGenerator: CodeGenerator
) : SymbolProcessor {
private fun KSClassDeclaration.findSealedConnection(potentialSealedParent: KSClassDeclaration): Boolean {
val targetClassname = potentialSealedParent.qualifiedName ?.asString()
return superTypes.any {
val itAsDeclaration = it.resolve().declaration as? KSClassDeclaration ?: return@any false
targetClassname == (itAsDeclaration.qualifiedName ?.asString()) || (itAsDeclaration.getSealedSubclasses().any() && itAsDeclaration.findSealedConnection(potentialSealedParent))
}
}
private fun KSClassDeclaration.resolveSubclasses(
searchIn: Sequence<KSAnnotated>,
allowNonSealed: Boolean
): Sequence<KSClassDeclaration> {
return findSubClasses(searchIn).let {
if (allowNonSealed) {
it
} else {
it.filter {
it.findSealedConnection(this)
}
}
}
}
@OptIn(KspExperimental::class)
private fun FileSpec.Builder.generateVariations(
ksFunctionDeclaration: KSFunctionDeclaration,
resolver: Resolver
) {
val annotation = ksFunctionDeclaration.getAnnotationsByType(GenerateVariations::class).first()
val variations: List<Pair<List<GenerationVariant>, KSValueParameter>> = ksFunctionDeclaration.parameters.mapNotNull {
val variationAnnotations = it.getAnnotationsByType(GenerationVariant::class).toList()
variationAnnotations to it
}
val accumulatedGenerations = mutableSetOf<Pair<FunSpec, Map<String, String>>>()
val baseFunctionParameters = ksFunctionDeclaration.parameters.mapNotNull {
ParameterSpec
.builder(
it.name ?.asString() ?: return@mapNotNull null,
it.type.toTypeName(),
)
.apply {
if (it.isCrossInline) {
addModifiers(KModifier.CROSSINLINE)
}
if (it.isVal) {
addModifiers(KModifier.VALUE)
}
if (it.isNoInline) {
addModifiers(KModifier.NOINLINE)
}
if (it.isVararg) {
addModifiers(KModifier.VARARG)
}
it.annotations.forEach {
if (it.shortName.asString() == GenerationVariant::class.simpleName) return@forEach
addAnnotation(it.toAnnotationSpec(omitDefaultValues = false))
}
}
.build() to it.hasDefault
}
val baseFunctionFunSpecs = mutableListOf<Pair<FunSpec, Map<String, String>>>()
let {
var defaultParametersIndicator = 0u
val maxIndicator = baseFunctionParameters.filter { it.second }.foldIndexed(0u) { index, acc, _ ->
2.0.pow(index).toUInt() + acc
}
while (defaultParametersIndicator <= maxIndicator) {
var currentDefaultParameterIndex = 0u
val baseFunctionDefaults = mutableMapOf<String, String>()
val funSpec = FunSpec.builder(ksFunctionDeclaration.simpleName.asString()).apply {
modifiers.addAll(ksFunctionDeclaration.modifiers.mapNotNull { it.toKModifier() })
ksFunctionDeclaration.annotations.forEach {
if (it.shortName.asString() == GenerateVariations::class.simpleName) return@forEach
addAnnotation(it.toAnnotationSpec(omitDefaultValues = false))
}
ksFunctionDeclaration.extensionReceiver ?.let {
receiver(it.toTypeName())
}
ksFunctionDeclaration.returnType ?.let {
returns(it.toTypeName())
}
}
baseFunctionParameters.forEach { (parameter, hasDefault) ->
if (hasDefault) {
val shouldBeIncluded = (2.0.pow(currentDefaultParameterIndex.toInt()).toUInt()).and(defaultParametersIndicator) > 0u
currentDefaultParameterIndex++
if (!shouldBeIncluded) {
return@forEach
}
}
funSpec.addParameter(parameter)
val name = parameter.name
val defaultValueString = if (parameter.modifiers.contains(KModifier.VARARG)) {
"*$name"
} else {
"$name"
}
baseFunctionDefaults[parameter.name] = defaultValueString
}
baseFunctionFunSpecs.add(
funSpec.build() to baseFunctionDefaults.toMap()
)
defaultParametersIndicator++
}
}
variations.forEach { (variations, parameter) ->
(baseFunctionFunSpecs + accumulatedGenerations).forEach { (accumulatedGeneration, baseDefaults) ->
if ((parameter.name ?.asString() ?: "this") !in baseDefaults.keys) {
return@forEach
}
variations.forEach { variation ->
val defaults = mutableMapOf<String, String>()
accumulatedGenerations.add(
FunSpec.builder(accumulatedGeneration.name).apply {
modifiers.addAll(accumulatedGeneration.modifiers)
accumulatedGeneration.annotations.forEach {
addAnnotation(it)
}
accumulatedGeneration.receiverType ?.let {
receiver(it)
}
returns(accumulatedGeneration.returnType)
accumulatedGeneration.parameters.forEach {
val actualName = if (variation.argName.isEmpty()) it.name else variation.argName
parameters.add(
(if (it.name == (parameter.name ?.asString() ?: "this")) {
val type = convertToClassName { variation.type }
val genericTypes = convertToClassNames { variation.genericTypes.toList() }
ParameterSpec
.builder(
actualName,
if (genericTypes.isEmpty()) {
type
} else {
type.parameterizedBy(
*genericTypes.toTypedArray()
)
}
)
.apply {
addModifiers(it.modifiers)
val defaultValueString = """
with(${actualName}) {${
if (it.modifiers.contains(KModifier.VARARG)) {
"map { it.${variation.conversion} }.toTypedArray()"
} else {
"${variation.conversion}"
}
}}
""".trimIndent()
defaults[it.name] = defaultValueString
}
} else {
it.toBuilder().apply {
defaults[it.name] = it.name
}
})
.apply {
it.annotations.forEach {
addAnnotation(it)
}
}
.build()
)
}
val parameters = accumulatedGeneration.parameters.joinToString(", ") {
val itName = it.name
"""
$itName = ${defaults[itName] ?: baseDefaults[itName] ?: itName}
""".trimIndent()
}
addCode(
"""
return ${ksFunctionDeclaration.simpleName.asString()}(
$parameters
)
""".trimIndent()
)
}.build() to defaults.toMap()
)
}
}
}
accumulatedGenerations.forEach {
addFunction(it.first)
}
}
@OptIn(KspExperimental::class)
override fun process(resolver: Resolver): List<KSAnnotated> {
(resolver.getSymbolsWithAnnotation(GenerateVariations::class.qualifiedName!!)).filterIsInstance<KSFunctionDeclaration>().forEach {
val prefix = (it.getAnnotationsByType(GenerateVariations::class)).firstOrNull() ?.prefix ?.takeIf {
it.isNotEmpty()
} ?: it.simpleName.asString().replaceFirst(it.simpleName.asString(), "")
it.writeFile(prefix = prefix, suffix = "GeneratedVariation") {
FileSpec.builder(
it.packageName.asString(),
"${it.simpleName.getShortName().let { it.replaceFirst(it.first().toString(), it.first().uppercase()) }}GeneratedVariation"
).apply {
addFileComment(
"""
THIS CODE HAVE BEEN GENERATED AUTOMATICALLY
TO REGENERATE IT JUST DELETE FILE
ORIGINAL FILE: ${it.containingFile ?.fileName}
""".trimIndent()
)
generateVariations(it, resolver)
}.build()
}
}
return emptyList()
}
}

View File

@@ -0,0 +1,11 @@
package dev.inmo.micro_utils.ksp.variations.generator
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
class Provider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = Processor(
environment.codeGenerator
)
}

View File

@@ -0,0 +1 @@
dev.inmo.micro_utils.ksp.variations.generator.Provider

View File

@@ -0,0 +1,30 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
id "com.google.devtools.ksp"
}
ext.do_publish = false
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
kotlin {
sourceSets {
commonMain {
dependencies {
implementation libs.kt.stdlib
api project(":micro_utils.ksp.variations")
}
}
}
}
dependencies {
add("kspCommonMainMetadata", project(":micro_utils.ksp.variations.generator"))
}
ksp {
}

View File

@@ -0,0 +1,61 @@
// THIS CODE HAVE BEEN GENERATED AUTOMATICALLY
// TO REGENERATE IT JUST DELETE FILE
// ORIGINAL FILE: SampleFun.kt
package dev.inmo.micro_utils.ksp.variations.generator.test
import kotlin.Boolean
import kotlin.Int
import kotlin.String
import kotlin.Unit
public suspend fun SimpleType.sample2(arg12: Int): Unit = sample2(
arg1 = with(arg12) {toString()}
)
public suspend fun SimpleType.sample2(arg12: Int, arg2: Int): Unit = sample2(
arg1 = with(arg12) {toString()}, arg2 = arg2
)
public suspend fun SimpleType.sample2(arg12: Int, arg3: Boolean): Unit = sample2(
arg1 = with(arg12) {toString()}, arg3 = arg3
)
public suspend fun SimpleType.sample2(
arg12: Int,
arg2: Int,
arg3: Boolean,
): Unit = sample2(
arg1 = with(arg12) {toString()}, arg2 = arg2, arg3 = arg3
)
public suspend fun SimpleType.sample2(arg22: String): Unit = sample2(
arg2 = with(arg22) {toInt()}
)
public suspend fun SimpleType.sample2(arg1: String, arg22: String): Unit = sample2(
arg1 = arg1, arg2 = with(arg22) {toInt()}
)
public suspend fun SimpleType.sample2(arg22: String, arg3: Boolean): Unit = sample2(
arg2 = with(arg22) {toInt()}, arg3 = arg3
)
public suspend fun SimpleType.sample2(
arg1: String,
arg22: String,
arg3: Boolean,
): Unit = sample2(
arg1 = arg1, arg2 = with(arg22) {toInt()}, arg3 = arg3
)
public suspend fun SimpleType.sample2(arg12: Int, arg22: String): Unit = sample2(
arg12 = arg12, arg2 = with(arg22) {toInt()}
)
public suspend fun SimpleType.sample2(
arg12: Int,
arg22: String,
arg3: Boolean,
): Unit = sample2(
arg12 = arg12, arg2 = with(arg22) {toInt()}, arg3 = arg3
)

View File

@@ -0,0 +1,52 @@
package dev.inmo.micro_utils.ksp.variations.generator.test
import dev.inmo.micro_utils.ksp.variations.GenerateVariations
import dev.inmo.micro_utils.ksp.variations.GenerationVariant
data class SimpleType(
val value: String
)
data class GenericType<T>(
val value: T
)
@GenerateVariations
fun sample(
@GenerationVariant(
SimpleType::class,
"value",
)
@GenerationVariant(
GenericType::class,
"value.toString()",
genericTypes = arrayOf(Int::class)
)
example: String = "12"
) = println(example)
@GenerateVariations
fun sampleVararg(
@GenerationVariant(
SimpleType::class,
"value",
)
vararg example: String = arrayOf("12")
) = println(example.joinToString())
@GenerateVariations
suspend fun SimpleType.sample2(
@GenerationVariant(
Int::class,
"toString()",
"arg12",
)
arg1: String = "1",
@GenerationVariant(
String::class,
"toInt()",
"arg22",
)
arg2: Int = 2,
arg3: Boolean = false
) = println(arg1)

View File

@@ -0,0 +1,15 @@
// THIS CODE HAVE BEEN GENERATED AUTOMATICALLY
// TO REGENERATE IT JUST DELETE FILE
// ORIGINAL FILE: SampleFun.kt
package dev.inmo.micro_utils.ksp.variations.generator.test
import kotlin.Int
import kotlin.Unit
public fun sample(example: SimpleType): Unit = sample(
example = with(example) {value}
)
public fun sample(example: GenericType<Int>): Unit = sample(
example = with(example) {value.toString()}
)

View File

@@ -0,0 +1,10 @@
// THIS CODE HAVE BEEN GENERATED AUTOMATICALLY
// TO REGENERATE IT JUST DELETE FILE
// ORIGINAL FILE: SampleFun.kt
package dev.inmo.micro_utils.ksp.variations.generator.test
import kotlin.Unit
public fun sampleVararg(vararg example: SimpleType): Unit = sampleVararg(
example = with(example) {map { it.value }.toTypedArray()}
)

View File

@@ -0,0 +1,7 @@
package dev.inmo.micro_utils.ksp.variations
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.FUNCTION)
annotation class GenerateVariations(
val prefix: String = ""
)

View File

@@ -0,0 +1,18 @@
package dev.inmo.micro_utils.ksp.variations
import kotlin.reflect.KClass
/**
* @param argName New argument name. Default - empty - means "use default arg name"
* @param type Qualified class name, like "dev.inmo.micro_utils.ksp.variants.GenerationVariant"
* @param conversion Conversion string with `this`
*/
@Retention(AnnotationRetention.BINARY)
@Repeatable
@Target(AnnotationTarget.TYPE_PARAMETER, AnnotationTarget.VALUE_PARAMETER)
annotation class GenerationVariant(
val type: KClass<*>,
val conversion: String,
val argName: String = "",
vararg val genericTypes: KClass<*>
)

View File

@@ -3,9 +3,10 @@ package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.ktor.common.* import dev.inmo.micro_utils.ktor.common.*
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.content.*
expect suspend fun HttpClient.tempUpload( expect suspend fun HttpClient.tempUpload(
fullTempUploadDraftPath: String, fullTempUploadDraftPath: String,
file: MPPFile, file: MPPFile,
onUpload: OnUploadCallback = { _, _ -> } onUpload: ProgressListener = ProgressListener { _, _ -> }
): TemporalFileId ): TemporalFileId

View File

@@ -4,8 +4,8 @@ import dev.inmo.micro_utils.common.FileName
import dev.inmo.micro_utils.common.MPPFile import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.ktor.common.LambdaInputProvider import dev.inmo.micro_utils.ktor.common.LambdaInputProvider
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.content.*
import io.ktor.http.Headers import io.ktor.http.Headers
import io.ktor.utils.io.core.Input
import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.StringFormat import kotlinx.serialization.StringFormat
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@@ -31,7 +31,7 @@ expect suspend fun <T> HttpClient.uniUpload(
resultDeserializer: DeserializationStrategy<T>, resultDeserializer: DeserializationStrategy<T>,
headers: Headers = Headers.Empty, headers: Headers = Headers.Empty,
stringFormat: StringFormat = Json, stringFormat: StringFormat = Json,
onUpload: OnUploadCallback = { _, _ -> } onUpload: ProgressListener = ProgressListener { _, _ -> }
): T? ): T?
/** /**
@@ -46,7 +46,7 @@ suspend fun <T> HttpClient.uniUpload(
additionalData: Map<String, Any> = emptyMap(), additionalData: Map<String, Any> = emptyMap(),
headers: Headers = Headers.Empty, headers: Headers = Headers.Empty,
stringFormat: StringFormat = Json, stringFormat: StringFormat = Json,
onUpload: OnUploadCallback = { _, _ -> } onUpload: ProgressListener = ProgressListener { _, _ -> }
): T? = uniUpload( ): T? = uniUpload(
url, url,
additionalData + ("bytes" to file), additionalData + ("bytes" to file),
@@ -68,7 +68,7 @@ suspend fun <T> HttpClient.uniUpload(
additionalData: Map<String, Any> = emptyMap(), additionalData: Map<String, Any> = emptyMap(),
headers: Headers = Headers.Empty, headers: Headers = Headers.Empty,
stringFormat: StringFormat = Json, stringFormat: StringFormat = Json,
onUpload: OnUploadCallback = { _, _ -> } onUpload: ProgressListener = ProgressListener { _, _ -> }
): T? = uniUpload( ): T? = uniUpload(
url, url,
additionalData + ("bytes" to info), additionalData + ("bytes" to info),
@@ -93,7 +93,7 @@ suspend fun <T> HttpClient.uniUpload(
additionalData: Map<String, Any> = emptyMap(), additionalData: Map<String, Any> = emptyMap(),
headers: Headers = Headers.Empty, headers: Headers = Headers.Empty,
stringFormat: StringFormat = Json, stringFormat: StringFormat = Json,
onUpload: OnUploadCallback = { _, _ -> } onUpload: ProgressListener = ProgressListener { _, _ -> }
): T? = uniUpload( ): T? = uniUpload(
url, url,
UniUploadFileInfo(fileName, mimeType, inputAllocator), UniUploadFileInfo(fileName, mimeType, inputAllocator),

View File

@@ -2,19 +2,18 @@ package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.coroutines.LinkedSupervisorJob import dev.inmo.micro_utils.coroutines.LinkedSupervisorJob
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
import dev.inmo.micro_utils.ktor.common.TemporalFileId import dev.inmo.micro_utils.ktor.common.TemporalFileId
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.content.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.w3c.dom.mediasource.ENDED
import org.w3c.dom.mediasource.ReadyState
import org.w3c.xhr.* import org.w3c.xhr.*
import org.w3c.xhr.XMLHttpRequest.Companion.DONE import org.w3c.xhr.XMLHttpRequest.Companion.DONE
suspend fun tempUpload( suspend fun tempUpload(
fullTempUploadDraftPath: String, fullTempUploadDraftPath: String,
file: MPPFile, file: MPPFile,
onUpload: OnUploadCallback onUpload: ProgressListener
): TemporalFileId { ): TemporalFileId {
val formData = FormData() val formData = FormData()
val answer = CompletableDeferred<TemporalFileId>(currentCoroutineContext().job) val answer = CompletableDeferred<TemporalFileId>(currentCoroutineContext().job)
@@ -28,7 +27,7 @@ suspend fun tempUpload(
val request = XMLHttpRequest() val request = XMLHttpRequest()
request.responseType = XMLHttpRequestResponseType.TEXT request.responseType = XMLHttpRequestResponseType.TEXT
request.upload.onprogress = { request.upload.onprogress = {
subscope.launchSafelyWithoutExceptions { onUpload(it.loaded.toLong(), it.total.toLong()) } subscope.launchLoggingDropExceptions { onUpload.onProgress(it.loaded.toLong(), it.total.toLong()) }
} }
request.onload = { request.onload = {
if (request.status == 200.toShort()) { if (request.status == 200.toShort()) {
@@ -60,5 +59,5 @@ suspend fun tempUpload(
actual suspend fun HttpClient.tempUpload( actual suspend fun HttpClient.tempUpload(
fullTempUploadDraftPath: String, fullTempUploadDraftPath: String,
file: MPPFile, file: MPPFile,
onUpload: OnUploadCallback onUpload: ProgressListener
): TemporalFileId = dev.inmo.micro_utils.ktor.client.tempUpload(fullTempUploadDraftPath, file, onUpload) ): TemporalFileId = dev.inmo.micro_utils.ktor.client.tempUpload(fullTempUploadDraftPath, file, onUpload)

View File

@@ -1,10 +1,10 @@
package dev.inmo.micro_utils.ktor.client package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.common.Progress
import dev.inmo.micro_utils.coroutines.LinkedSupervisorJob import dev.inmo.micro_utils.coroutines.LinkedSupervisorJob
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.content.*
import io.ktor.http.Headers import io.ktor.http.Headers
import io.ktor.utils.io.core.readBytes import io.ktor.utils.io.core.readBytes
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
@@ -36,7 +36,7 @@ actual suspend fun <T> HttpClient.uniUpload(
resultDeserializer: DeserializationStrategy<T>, resultDeserializer: DeserializationStrategy<T>,
headers: Headers, headers: Headers,
stringFormat: StringFormat, stringFormat: StringFormat,
onUpload: OnUploadCallback onUpload: ProgressListener
): T? { ): T? {
val formData = FormData() val formData = FormData()
val answer = CompletableDeferred<T?>(currentCoroutineContext().job) val answer = CompletableDeferred<T?>(currentCoroutineContext().job)
@@ -66,7 +66,7 @@ actual suspend fun <T> HttpClient.uniUpload(
} }
request.responseType = XMLHttpRequestResponseType.TEXT request.responseType = XMLHttpRequestResponseType.TEXT
request.upload.onprogress = { request.upload.onprogress = {
subscope.launchSafelyWithoutExceptions { onUpload(it.loaded.toLong(), it.total.toLong()) } subscope.launchLoggingDropExceptions { onUpload.onProgress(it.loaded.toLong(), it.total.toLong()) }
} }
request.onload = { request.onload = {
if (request.status == 200.toShort()) { if (request.status == 200.toShort()) {

View File

@@ -4,6 +4,7 @@ import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.common.filename import dev.inmo.micro_utils.common.filename
import dev.inmo.micro_utils.ktor.common.TemporalFileId import dev.inmo.micro_utils.ktor.common.TemporalFileId
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.content.*
import io.ktor.client.plugins.onUpload import io.ktor.client.plugins.onUpload
import io.ktor.client.request.forms.formData import io.ktor.client.request.forms.formData
import io.ktor.client.request.forms.submitFormWithBinaryData import io.ktor.client.request.forms.submitFormWithBinaryData
@@ -18,7 +19,7 @@ internal val MPPFile.mimeType: String
actual suspend fun HttpClient.tempUpload( actual suspend fun HttpClient.tempUpload(
fullTempUploadDraftPath: String, fullTempUploadDraftPath: String,
file: MPPFile, file: MPPFile,
onUpload: OnUploadCallback onUpload: ProgressListener
): TemporalFileId { ): TemporalFileId {
val inputProvider = file.inputProvider() val inputProvider = file.inputProvider()
val fileId = submitFormWithBinaryData( val fileId = submitFormWithBinaryData(

View File

@@ -1,8 +1,7 @@
package dev.inmo.micro_utils.ktor.client package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.Progress
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.mergeHeaders import io.ktor.client.content.*
import io.ktor.client.plugins.onUpload import io.ktor.client.plugins.onUpload
import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.InputProvider import io.ktor.client.request.forms.InputProvider
@@ -20,7 +19,6 @@ import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.StringFormat import kotlinx.serialization.StringFormat
import kotlinx.serialization.encodeToString
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
import java.io.File import java.io.File
@@ -39,7 +37,7 @@ actual suspend fun <T> HttpClient.uniUpload(
resultDeserializer: DeserializationStrategy<T>, resultDeserializer: DeserializationStrategy<T>,
headers: Headers, headers: Headers,
stringFormat: StringFormat, stringFormat: StringFormat,
onUpload: OnUploadCallback onUpload: ProgressListener
): T? { ): T? {
val withBinary = data.values.any { it is File || it is UniUploadFileInfo } val withBinary = data.values.any { it is File || it is UniUploadFileInfo }
@@ -76,7 +74,7 @@ actual suspend fun <T> HttpClient.uniUpload(
appendAll(headers) appendAll(headers)
} }
onUpload { bytesSentTotal, contentLength -> onUpload { bytesSentTotal, contentLength ->
onUpload(bytesSentTotal, contentLength) onUpload.onProgress(bytesSentTotal, contentLength)
} }
} }

View File

@@ -5,6 +5,7 @@ import dev.inmo.micro_utils.common.filename
import dev.inmo.micro_utils.ktor.common.TemporalFileId import dev.inmo.micro_utils.ktor.common.TemporalFileId
import dev.inmo.micro_utils.mime_types.getMimeTypeOrAny import dev.inmo.micro_utils.mime_types.getMimeTypeOrAny
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.content.*
import io.ktor.client.plugins.onUpload import io.ktor.client.plugins.onUpload
import io.ktor.client.request.forms.formData import io.ktor.client.request.forms.formData
import io.ktor.client.request.forms.submitFormWithBinaryData import io.ktor.client.request.forms.submitFormWithBinaryData
@@ -18,7 +19,7 @@ internal val MPPFile.mimeType: String
actual suspend fun HttpClient.tempUpload( actual suspend fun HttpClient.tempUpload(
fullTempUploadDraftPath: String, fullTempUploadDraftPath: String,
file: MPPFile, file: MPPFile,
onUpload: OnUploadCallback onUpload: ProgressListener
): TemporalFileId { ): TemporalFileId {
val inputProvider = file.inputProvider() val inputProvider = file.inputProvider()
val fileId = submitFormWithBinaryData( val fileId = submitFormWithBinaryData(

View File

@@ -1,9 +1,8 @@
package dev.inmo.micro_utils.ktor.client package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.common.Progress
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.mergeHeaders import io.ktor.client.content.*
import io.ktor.client.plugins.onUpload import io.ktor.client.plugins.onUpload
import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.InputProvider import io.ktor.client.request.forms.InputProvider
@@ -21,7 +20,6 @@ import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.StringFormat import kotlinx.serialization.StringFormat
import kotlinx.serialization.encodeToString
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
/** /**
@@ -39,7 +37,7 @@ actual suspend fun <T> HttpClient.uniUpload(
resultDeserializer: DeserializationStrategy<T>, resultDeserializer: DeserializationStrategy<T>,
headers: Headers, headers: Headers,
stringFormat: StringFormat, stringFormat: StringFormat,
onUpload: OnUploadCallback onUpload: ProgressListener
): T? { ): T? {
val withBinary = data.values.any { it is MPPFile || it is UniUploadFileInfo } val withBinary = data.values.any { it is MPPFile || it is UniUploadFileInfo }
@@ -75,9 +73,7 @@ actual suspend fun <T> HttpClient.uniUpload(
headers { headers {
appendAll(headers) appendAll(headers)
} }
onUpload { bytesSentTotal, contentLength -> onUpload(onUpload)
onUpload(bytesSentTotal, contentLength)
}
} }
val response = if (withBinary) { val response = if (withBinary) {

View File

@@ -5,6 +5,7 @@ import dev.inmo.micro_utils.common.filename
import dev.inmo.micro_utils.ktor.common.TemporalFileId import dev.inmo.micro_utils.ktor.common.TemporalFileId
import dev.inmo.micro_utils.mime_types.getMimeTypeOrAny import dev.inmo.micro_utils.mime_types.getMimeTypeOrAny
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.content.*
import io.ktor.client.plugins.onUpload import io.ktor.client.plugins.onUpload
import io.ktor.client.request.forms.formData import io.ktor.client.request.forms.formData
import io.ktor.client.request.forms.submitFormWithBinaryData import io.ktor.client.request.forms.submitFormWithBinaryData
@@ -18,7 +19,7 @@ internal val MPPFile.mimeType: String
actual suspend fun HttpClient.tempUpload( actual suspend fun HttpClient.tempUpload(
fullTempUploadDraftPath: String, fullTempUploadDraftPath: String,
file: MPPFile, file: MPPFile,
onUpload: OnUploadCallback onUpload: ProgressListener
): TemporalFileId { ): TemporalFileId {
val inputProvider = file.inputProvider() val inputProvider = file.inputProvider()
val fileId = submitFormWithBinaryData( val fileId = submitFormWithBinaryData(

View File

@@ -1,9 +1,8 @@
package dev.inmo.micro_utils.ktor.client package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.common.Progress
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.mergeHeaders import io.ktor.client.content.*
import io.ktor.client.plugins.onUpload import io.ktor.client.plugins.onUpload
import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.InputProvider import io.ktor.client.request.forms.InputProvider
@@ -21,7 +20,6 @@ import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.StringFormat import kotlinx.serialization.StringFormat
import kotlinx.serialization.encodeToString
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
/** /**
@@ -39,7 +37,7 @@ actual suspend fun <T> HttpClient.uniUpload(
resultDeserializer: DeserializationStrategy<T>, resultDeserializer: DeserializationStrategy<T>,
headers: Headers, headers: Headers,
stringFormat: StringFormat, stringFormat: StringFormat,
onUpload: OnUploadCallback onUpload: ProgressListener
): T? { ): T? {
val withBinary = data.values.any { it is MPPFile || it is UniUploadFileInfo } val withBinary = data.values.any { it is MPPFile || it is UniUploadFileInfo }
@@ -75,9 +73,7 @@ actual suspend fun <T> HttpClient.uniUpload(
headers { headers {
appendAll(headers) appendAll(headers)
} }
onUpload { bytesSentTotal, contentLength -> onUpload(onUpload)
onUpload(bytesSentTotal, contentLength)
}
} }
val response = if (withBinary) { val response = if (withBinary) {

View File

@@ -5,6 +5,7 @@ import dev.inmo.micro_utils.common.filename
import dev.inmo.micro_utils.ktor.common.TemporalFileId import dev.inmo.micro_utils.ktor.common.TemporalFileId
import dev.inmo.micro_utils.mime_types.getMimeTypeOrAny import dev.inmo.micro_utils.mime_types.getMimeTypeOrAny
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.content.*
import io.ktor.client.plugins.onUpload import io.ktor.client.plugins.onUpload
import io.ktor.client.request.forms.formData import io.ktor.client.request.forms.formData
import io.ktor.client.request.forms.submitFormWithBinaryData import io.ktor.client.request.forms.submitFormWithBinaryData
@@ -18,7 +19,7 @@ internal val MPPFile.mimeType: String
actual suspend fun HttpClient.tempUpload( actual suspend fun HttpClient.tempUpload(
fullTempUploadDraftPath: String, fullTempUploadDraftPath: String,
file: MPPFile, file: MPPFile,
onUpload: OnUploadCallback onUpload: ProgressListener
): TemporalFileId { ): TemporalFileId {
val inputProvider = file.inputProvider() val inputProvider = file.inputProvider()
val fileId = submitFormWithBinaryData( val fileId = submitFormWithBinaryData(

View File

@@ -1,9 +1,8 @@
package dev.inmo.micro_utils.ktor.client package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.common.Progress
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.mergeHeaders import io.ktor.client.content.*
import io.ktor.client.plugins.onUpload import io.ktor.client.plugins.onUpload
import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.InputProvider import io.ktor.client.request.forms.InputProvider
@@ -21,7 +20,6 @@ import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.StringFormat import kotlinx.serialization.StringFormat
import kotlinx.serialization.encodeToString
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
/** /**
@@ -39,7 +37,7 @@ actual suspend fun <T> HttpClient.uniUpload(
resultDeserializer: DeserializationStrategy<T>, resultDeserializer: DeserializationStrategy<T>,
headers: Headers, headers: Headers,
stringFormat: StringFormat, stringFormat: StringFormat,
onUpload: OnUploadCallback onUpload: ProgressListener
): T? { ): T? {
val withBinary = data.values.any { it is MPPFile || it is UniUploadFileInfo } val withBinary = data.values.any { it is MPPFile || it is UniUploadFileInfo }
@@ -75,9 +73,7 @@ actual suspend fun <T> HttpClient.uniUpload(
headers { headers {
appendAll(headers) appendAll(headers)
} }
onUpload { bytesSentTotal, contentLength -> onUpload(onUpload)
onUpload(bytesSentTotal, contentLength)
}
} }
val response = if (withBinary) { val response = if (withBinary) {

View File

@@ -1,13 +1,12 @@
package dev.inmo.micro_utils.ktor.common package dev.inmo.micro_utils.ktor.common
import io.ktor.utils.io.core.Input import io.ktor.utils.io.streams.*
import io.ktor.utils.io.core.copyTo import kotlinx.io.Source
import io.ktor.utils.io.streams.asOutput import kotlinx.io.readTo
import java.io.File import java.io.File
import java.io.InputStream
import java.util.UUID import java.util.UUID
fun Input.downloadToTempFile( fun Source.downloadToTempFile(
fileName: String = UUID.randomUUID().toString(), fileName: String = UUID.randomUUID().toString(),
fileExtension: String? = ".temp", fileExtension: String? = ".temp",
folder: File? = null folder: File? = null
@@ -17,7 +16,7 @@ fun Input.downloadToTempFile(
folder folder
).apply { ).apply {
outputStream().use { outputStream().use {
copyTo(it.asOutput()) it.writePacket(this@downloadToTempFile)
} }
deleteOnExit() deleteOnExit()
} }

View File

@@ -1,15 +0,0 @@
package dev.inmo.micro_utils.ktor.server
import io.ktor.server.application.ApplicationCall
import io.ktor.server.response.responseType
import io.ktor.util.InternalAPI
import io.ktor.util.reflect.TypeInfo
@InternalAPI
suspend fun <T : Any> ApplicationCall.respond(
message: T,
typeInfo: TypeInfo
) {
response.responseType = typeInfo
response.pipeline.execute(this, message as Any)
}

View File

@@ -4,13 +4,13 @@ import com.benasher44.uuid.uuid4
import io.ktor.http.content.PartData import io.ktor.http.content.PartData
import io.ktor.utils.io.copyTo import io.ktor.utils.io.copyTo
import io.ktor.utils.io.core.copyTo import io.ktor.utils.io.core.copyTo
import io.ktor.utils.io.jvm.javaio.copyTo import io.ktor.utils.io.jvm.javaio.*
import io.ktor.utils.io.streams.asOutput import io.ktor.utils.io.streams.*
import java.io.File import java.io.File
fun PartData.FileItem.download(target: File) { fun PartData.FileItem.download(target: File) {
provider().use { input -> provider().toInputStream().use { input ->
target.outputStream().asOutput().use { target.outputStream().use {
input.copyTo(it) input.copyTo(it)
} }
} }
@@ -25,9 +25,9 @@ fun PartData.FileItem.downloadToTemporalFile(): File {
} }
fun PartData.BinaryItem.download(target: File) { fun PartData.BinaryItem.download(target: File) {
provider().use { input -> provider().inputStream().use { input ->
target.outputStream().use { target.outputStream().use {
input.copyTo(it.asOutput()) input.copyTo(it)
} }
} }
} }

View File

@@ -1,7 +1,7 @@
package dev.inmo.micro_utils.ktor.server package dev.inmo.micro_utils.ktor.server
import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator
import io.ktor.server.application.Application import io.ktor.server.application.*
import io.ktor.server.cio.CIO import io.ktor.server.cio.CIO
import io.ktor.server.cio.CIOApplicationEngine import io.ktor.server.cio.CIOApplicationEngine
import io.ktor.server.engine.* import io.ktor.server.engine.*
@@ -11,20 +11,22 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati
engine: ApplicationEngineFactory<TEngine, TConfiguration>, engine: ApplicationEngineFactory<TEngine, TConfiguration>,
host: String = "localhost", host: String = "localhost",
port: Int = Random.nextInt(1024, 65535), port: Int = Random.nextInt(1024, 65535),
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {}, additionalEngineEnvironmentConfigurator: EngineConnectorBuilder.() -> Unit = {},
additionalConfigurationConfigurator: TConfiguration.() -> Unit = {}, additionalConfigurationConfigurator: TConfiguration.() -> Unit = {},
environment: ApplicationEnvironment = applicationEnvironment(),
block: Application.() -> Unit block: Application.() -> Unit
): TEngine = embeddedServer( ): EmbeddedServer<TEngine, TConfiguration> = embeddedServer<TEngine, TConfiguration>(
engine, engine,
applicationEngineEnvironment { environment,
module(block) {
connector { connector {
this.host = host this.host = host
this.port = port this.port = port
additionalEngineEnvironmentConfigurator()
} }
additionalEngineEnvironmentConfigurator() additionalConfigurationConfigurator()
}, },
additionalConfigurationConfigurator module = block
) )
/** /**
@@ -35,15 +37,17 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati
fun createKtorServer( fun createKtorServer(
host: String = "localhost", host: String = "localhost",
port: Int = Random.nextInt(1024, 65535), port: Int = Random.nextInt(1024, 65535),
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {}, additionalEngineEnvironmentConfigurator: EngineConnectorBuilder.() -> Unit = {},
additionalConfigurationConfigurator: CIOApplicationEngine.Configuration.() -> Unit = {}, additionalConfigurationConfigurator: CIOApplicationEngine.Configuration.() -> Unit = {},
environment: ApplicationEnvironment = applicationEnvironment(),
block: Application.() -> Unit block: Application.() -> Unit
): CIOApplicationEngine = createKtorServer( ): EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration> = createKtorServer(
CIO, CIO,
host, host,
port, port,
additionalEngineEnvironmentConfigurator, additionalEngineEnvironmentConfigurator,
additionalConfigurationConfigurator, additionalConfigurationConfigurator,
environment,
block block
) )
@@ -51,15 +55,17 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati
engine: ApplicationEngineFactory<TEngine, TConfiguration>, engine: ApplicationEngineFactory<TEngine, TConfiguration>,
host: String = "localhost", host: String = "localhost",
port: Int = Random.nextInt(1024, 65535), port: Int = Random.nextInt(1024, 65535),
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {}, additionalEngineEnvironmentConfigurator: EngineConnectorBuilder.() -> Unit = {},
additionalConfigurationConfigurator: TConfiguration.() -> Unit = {}, additionalConfigurationConfigurator: TConfiguration.() -> Unit = {},
environment: ApplicationEnvironment = applicationEnvironment(),
configurators: List<KtorApplicationConfigurator> configurators: List<KtorApplicationConfigurator>
): TEngine = createKtorServer( ): EmbeddedServer<TEngine, TConfiguration> = createKtorServer(
engine, engine,
host, host,
port, port,
additionalEngineEnvironmentConfigurator, additionalEngineEnvironmentConfigurator,
additionalConfigurationConfigurator additionalConfigurationConfigurator,
environment,
) { ) {
configurators.forEach { it.apply { configure() } } configurators.forEach { it.apply { configure() } }
} }
@@ -73,6 +79,7 @@ fun createKtorServer(
host: String = "localhost", host: String = "localhost",
port: Int = Random.nextInt(1024, 65535), port: Int = Random.nextInt(1024, 65535),
configurators: List<KtorApplicationConfigurator>, configurators: List<KtorApplicationConfigurator>,
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {}, additionalEngineEnvironmentConfigurator: EngineConnectorBuilder.() -> Unit = {},
additionalConfigurationConfigurator: CIOApplicationEngine.Configuration.() -> Unit = {}, additionalConfigurationConfigurator: CIOApplicationEngine.Configuration.() -> Unit = {},
): ApplicationEngine = createKtorServer(CIO, host, port, additionalEngineEnvironmentConfigurator, additionalConfigurationConfigurator, configurators) environment: ApplicationEnvironment = applicationEnvironment(),
): EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration> = createKtorServer(CIO, host, port, additionalEngineEnvironmentConfigurator, additionalConfigurationConfigurator, environment, configurators)

View File

@@ -3,7 +3,7 @@ package dev.inmo.micro_utils.ktor.server
import com.benasher44.uuid.uuid4 import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.common.FileName import dev.inmo.micro_utils.common.FileName
import dev.inmo.micro_utils.common.MPPFile import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
import dev.inmo.micro_utils.ktor.common.DefaultTemporalFilesSubPath import dev.inmo.micro_utils.ktor.common.DefaultTemporalFilesSubPath
import dev.inmo.micro_utils.ktor.common.TemporalFileId import dev.inmo.micro_utils.ktor.common.TemporalFileId
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
@@ -17,6 +17,7 @@ import io.ktor.server.response.respondText
import io.ktor.server.routing.Route import io.ktor.server.routing.Route
import io.ktor.server.routing.post import io.ktor.server.routing.post
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@@ -26,7 +27,10 @@ import java.nio.file.attribute.FileTime
class TemporalFilesRoutingConfigurator( class TemporalFilesRoutingConfigurator(
private val subpath: String = DefaultTemporalFilesSubPath, private val subpath: String = DefaultTemporalFilesSubPath,
private val temporalFilesUtilizer: TemporalFilesUtilizer = TemporalFilesUtilizer private val temporalFilesUtilizer: TemporalFilesUtilizer = TemporalFilesUtilizer,
filesFlowReplay: Int = 0,
filesFlowExtraBufferCapacity: Int = Int.MAX_VALUE,
filesFlowOnBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
) : ApplicationRoutingConfigurator.Element { ) : ApplicationRoutingConfigurator.Element {
interface TemporalFilesUtilizer { interface TemporalFilesUtilizer {
fun start(filesMap: MutableMap<TemporalFileId, MPPFile>, filesMutex: Mutex, onNewFileFlow: Flow<TemporalFileId>): Job fun start(filesMap: MutableMap<TemporalFileId, MPPFile>, filesMutex: Mutex, onNewFileFlow: Flow<TemporalFileId>): Job
@@ -40,7 +44,7 @@ class TemporalFilesRoutingConfigurator(
filesMap: MutableMap<TemporalFileId, MPPFile>, filesMap: MutableMap<TemporalFileId, MPPFile>,
filesMutex: Mutex, filesMutex: Mutex,
onNewFileFlow: Flow<TemporalFileId> onNewFileFlow: Flow<TemporalFileId>
): Job = scope.launchSafelyWithoutExceptions { ): Job = scope.launchLoggingDropExceptions {
while (currentCoroutineContext().isActive) { while (currentCoroutineContext().isActive) {
val filesWithCreationInfo = filesMap.mapNotNull { (fileId, file) -> val filesWithCreationInfo = filesMap.mapNotNull { (fileId, file) ->
fileId to ((Files.getAttribute(file.toPath(), "creationTime") as? FileTime) ?.toMillis() ?: return@mapNotNull null) fileId to ((Files.getAttribute(file.toPath(), "creationTime") as? FileTime) ?.toMillis() ?: return@mapNotNull null)
@@ -74,7 +78,11 @@ class TemporalFilesRoutingConfigurator(
private val temporalFilesMap = mutableMapOf<TemporalFileId, MPPFile>() private val temporalFilesMap = mutableMapOf<TemporalFileId, MPPFile>()
private val temporalFilesMutex = Mutex() private val temporalFilesMutex = Mutex()
private val filesFlow = MutableSharedFlow<TemporalFileId>() private val filesFlow = MutableSharedFlow<TemporalFileId>(
replay = filesFlowReplay,
extraBufferCapacity = filesFlowExtraBufferCapacity,
onBufferOverflow = filesFlowOnBufferOverflow
)
val utilizerJob = temporalFilesUtilizer.start(temporalFilesMap, temporalFilesMutex, filesFlow.asSharedFlow()) val utilizerJob = temporalFilesUtilizer.start(temporalFilesMap, temporalFilesMutex, filesFlow.asSharedFlow())
override fun Route.invoke() { override fun Route.invoke() {
@@ -111,7 +119,7 @@ class TemporalFilesRoutingConfigurator(
temporalFilesMap[fileId] = file temporalFilesMap[fileId] = file
} }
call.respondText(fileId.string) call.respondText(fileId.string)
launchSafelyWithoutExceptions { filesFlow.emit(fileId) } filesFlow.emit(fileId)
} ?: call.respond(HttpStatusCode.BadRequest) } ?: call.respond(HttpStatusCode.BadRequest)
} }
} }

View File

@@ -6,6 +6,7 @@ import dev.inmo.micro_utils.ktor.common.downloadToTempFile
import io.ktor.http.content.* import io.ktor.http.content.*
import io.ktor.server.application.ApplicationCall import io.ktor.server.application.ApplicationCall
import io.ktor.server.request.receiveMultipart import io.ktor.server.request.receiveMultipart
import io.ktor.utils.io.*
import io.ktor.utils.io.core.* import io.ktor.utils.io.core.*
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
@@ -47,7 +48,7 @@ suspend fun ApplicationCall.uniloadMultipart(
onBinaryChannelItem onBinaryChannelItem
) { ) {
when (it.name) { when (it.name) {
"bytes" -> resultInput = it.provider() "bytes" -> resultInput = it.provider().readBuffer()
else -> onCustomFileItem(it) else -> onCustomFileItem(it)
} }
} }

View File

@@ -2,8 +2,7 @@ package dev.inmo.micro_utils.ktor.server.configurators
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator.Element import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator.Element
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.routing.Route import io.ktor.server.routing.*
import io.ktor.server.routing.Routing
import kotlinx.serialization.Contextual import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -19,9 +18,7 @@ class ApplicationRoutingConfigurator(
} }
override fun Application.configure() { override fun Application.configure() {
pluginOrNull(Routing) ?.apply { routing {
rootInstaller.apply { invoke() }
} ?: install(Routing) {
rootInstaller.apply { invoke() } rootInstaller.apply { invoke() }
} }
} }

View File

@@ -1,7 +1,99 @@
package dev.inmo.micro_utils.ktor.server.configurators package dev.inmo.micro_utils.ktor.server.configurators
import io.ktor.server.application.Application import io.ktor.server.application.*
import io.ktor.server.http.content.*
import io.ktor.server.plugins.cachingheaders.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.routing.*
import io.ktor.server.sessions.*
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import java.io.File
interface KtorApplicationConfigurator { interface KtorApplicationConfigurator {
@Serializable
class Routing(
private val elements: List<@Contextual Element>
) : KtorApplicationConfigurator {
fun interface Element { operator fun Route.invoke() }
private val rootInstaller = Element {
elements.forEach {
it.apply { invoke() }
}
}
override fun Application.configure() {
routing {
rootInstaller.apply { invoke() }
}
}
/**
* @param pathToFolder Contains [Pair]s where firsts are paths in urls and seconds are folders file paths
* @param pathToResource Contains [Pair]s where firsts are paths in urls and seconds are packages in resources
*/
class Static(
private val pathToFolder: List<Pair<String, String>> = emptyList(),
private val pathToResource: List<Pair<String, String>> = emptyList(),
) : Element {
override fun Route.invoke() {
pathToFolder.forEach {
staticFiles(
it.first,
File(it.second)
)
}
pathToResource.forEach {
staticResources(
it.first,
it.second
)
}
}
}
}
class StatusPages(
private val elements: List<@Contextual Element>
) : KtorApplicationConfigurator {
fun interface Element { operator fun StatusPagesConfig.invoke() }
override fun Application.configure() {
install(StatusPages) {
elements.forEach {
it.apply { invoke() }
}
}
}
}
class Sessions(
private val elements: List<@Contextual Element>
) : KtorApplicationConfigurator {
fun interface Element { operator fun SessionsConfig.invoke() }
override fun Application.configure() {
install(Sessions) {
elements.forEach {
it.apply { invoke() }
}
}
}
}
class CachingHeaders(
private val elements: List<@Contextual Element>
) : KtorApplicationConfigurator {
fun interface Element { operator fun CachingHeadersConfig.invoke() }
override fun Application.configure() {
install(CachingHeaders) {
elements.forEach {
it.apply { invoke() }
}
}
}
}
fun Application.configure() fun Application.configure()
} }

View File

@@ -2,19 +2,19 @@ package dev.inmo.micro_utils.pagination
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
fun Query.paginate(with: Pagination, orderBy: Pair<Expression<*>, SortOrder>? = null) = limit( fun Query.paginate(with: Pagination, orderBy: Pair<Expression<*>, SortOrder>? = null) =
with.size, limit(with.size)
with.firstIndex.toLong() .offset(with.firstIndex.toLong())
).let { .let {
if (orderBy != null) { if (orderBy != null) {
it.orderBy( it.orderBy(
orderBy.first, orderBy.first,
orderBy.second orderBy.second
) )
} else { } else {
it it
}
} }
}
fun Query.paginate(with: Pagination, orderBy: Expression<*>?, reversed: Boolean = false) = paginate( fun Query.paginate(with: Pagination, orderBy: Expression<*>?, reversed: Boolean = false) = paginate(
with, with,

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": [
"config:base" "config:recommended"
] ]
} }

View File

@@ -2,7 +2,7 @@ package dev.inmo.micro_utils.repos.cache.full
import dev.inmo.micro_utils.common.* import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.coroutines.SmartRWLocker import dev.inmo.micro_utils.coroutines.SmartRWLocker
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
import dev.inmo.micro_utils.coroutines.withReadAcquire import dev.inmo.micro_utils.coroutines.withReadAcquire
import dev.inmo.micro_utils.coroutines.withWriteLock import dev.inmo.micro_utils.coroutines.withWriteLock
import dev.inmo.micro_utils.pagination.Pagination import dev.inmo.micro_utils.pagination.Pagination
@@ -17,7 +17,7 @@ import kotlinx.coroutines.Dispatchers
open class FullReadCRUDCacheRepo<ObjectType, IdType>( open class FullReadCRUDCacheRepo<ObjectType, IdType>(
protected open val parentRepo: ReadCRUDRepo<ObjectType, IdType>, protected open val parentRepo: ReadCRUDRepo<ObjectType, IdType>,
protected open val kvCache: KeyValueRepo<IdType, ObjectType>, protected open val kvCache: KeyValueRepo<IdType, ObjectType>,
protected val locker: SmartRWLocker = SmartRWLocker(), protected open val locker: SmartRWLocker = SmartRWLocker(),
protected open val idGetter: (ObjectType) -> IdType protected open val idGetter: (ObjectType) -> IdType
) : ReadCRUDRepo<ObjectType, IdType>, FullCacheRepo { ) : ReadCRUDRepo<ObjectType, IdType>, FullCacheRepo {
protected suspend inline fun <T> doOrTakeAndActualize( protected suspend inline fun <T> doOrTakeAndActualize(
@@ -95,11 +95,11 @@ fun <ObjectType, IdType> ReadCRUDRepo<ObjectType, IdType>.cached(
open class FullCRUDCacheRepo<ObjectType, IdType, InputValueType>( open class FullCRUDCacheRepo<ObjectType, IdType, InputValueType>(
override val parentRepo: CRUDRepo<ObjectType, IdType, InputValueType>, override val parentRepo: CRUDRepo<ObjectType, IdType, InputValueType>,
kvCache: KeyValueRepo<IdType, ObjectType>, override val kvCache: KeyValueRepo<IdType, ObjectType>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default), scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
skipStartInvalidate: Boolean = false, skipStartInvalidate: Boolean = false,
locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate), override val locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate),
idGetter: (ObjectType) -> IdType override val idGetter: (ObjectType) -> IdType
) : FullReadCRUDCacheRepo<ObjectType, IdType>( ) : FullReadCRUDCacheRepo<ObjectType, IdType>(
parentRepo, parentRepo,
kvCache, kvCache,
@@ -116,7 +116,7 @@ open class FullCRUDCacheRepo<ObjectType, IdType, InputValueType>(
CRUDRepo<ObjectType, IdType, InputValueType> { CRUDRepo<ObjectType, IdType, InputValueType> {
init { init {
if (!skipStartInvalidate) { if (!skipStartInvalidate) {
scope.launchSafelyWithoutExceptions { scope.launchLoggingDropExceptions {
if (locker.writeMutex.isLocked) { if (locker.writeMutex.isLocked) {
initialInvalidate() initialInvalidate()
} else { } else {

View File

@@ -2,7 +2,7 @@ package dev.inmo.micro_utils.repos.cache.full
import dev.inmo.micro_utils.common.* import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.coroutines.SmartRWLocker import dev.inmo.micro_utils.coroutines.SmartRWLocker
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
import dev.inmo.micro_utils.coroutines.withReadAcquire import dev.inmo.micro_utils.coroutines.withReadAcquire
import dev.inmo.micro_utils.coroutines.withWriteLock import dev.inmo.micro_utils.coroutines.withWriteLock
import dev.inmo.micro_utils.pagination.Pagination import dev.inmo.micro_utils.pagination.Pagination
@@ -17,7 +17,7 @@ import kotlinx.coroutines.flow.*
open class FullReadKeyValueCacheRepo<Key,Value>( open class FullReadKeyValueCacheRepo<Key,Value>(
protected open val parentRepo: ReadKeyValueRepo<Key, Value>, protected open val parentRepo: ReadKeyValueRepo<Key, Value>,
protected open val kvCache: KeyValueRepo<Key, Value>, protected open val kvCache: KeyValueRepo<Key, Value>,
protected val locker: SmartRWLocker = SmartRWLocker() protected open val locker: SmartRWLocker = SmartRWLocker()
) : ReadKeyValueRepo<Key, Value>, FullCacheRepo { ) : ReadKeyValueRepo<Key, Value>, FullCacheRepo {
protected suspend inline fun <T> doOrTakeAndActualize( protected suspend inline fun <T> doOrTakeAndActualize(
action: KeyValueRepo<Key, Value>.() -> Optional<T>, action: KeyValueRepo<Key, Value>.() -> Optional<T>,
@@ -127,10 +127,10 @@ fun <Key, Value> WriteKeyValueRepo<Key, Value>.caching(
open class FullKeyValueCacheRepo<Key,Value>( open class FullKeyValueCacheRepo<Key,Value>(
override val parentRepo: KeyValueRepo<Key, Value>, override val parentRepo: KeyValueRepo<Key, Value>,
kvCache: KeyValueRepo<Key, Value>, override val kvCache: KeyValueRepo<Key, Value>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default), scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
skipStartInvalidate: Boolean = false, skipStartInvalidate: Boolean = false,
locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate), override val locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate),
) : //FullWriteKeyValueCacheRepo<Key,Value>(parentRepo, kvCache, scope), ) : //FullWriteKeyValueCacheRepo<Key,Value>(parentRepo, kvCache, scope),
KeyValueRepo<Key,Value>, KeyValueRepo<Key,Value>,
WriteKeyValueRepo<Key,Value> by parentRepo, WriteKeyValueRepo<Key,Value> by parentRepo,
@@ -141,7 +141,7 @@ open class FullKeyValueCacheRepo<Key,Value>(
) { ) {
init { init {
if (!skipStartInvalidate) { if (!skipStartInvalidate) {
scope.launchSafelyWithoutExceptions { scope.launchLoggingDropExceptions {
if (locker.writeMutex.isLocked) { if (locker.writeMutex.isLocked) {
initialInvalidate() initialInvalidate()
} else { } else {

View File

@@ -2,7 +2,7 @@ package dev.inmo.micro_utils.repos.cache.full
import dev.inmo.micro_utils.common.* import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.coroutines.SmartRWLocker import dev.inmo.micro_utils.coroutines.SmartRWLocker
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
import dev.inmo.micro_utils.coroutines.withReadAcquire import dev.inmo.micro_utils.coroutines.withReadAcquire
import dev.inmo.micro_utils.coroutines.withWriteLock import dev.inmo.micro_utils.coroutines.withWriteLock
import dev.inmo.micro_utils.pagination.* import dev.inmo.micro_utils.pagination.*
@@ -18,7 +18,7 @@ import kotlinx.coroutines.flow.*
open class FullReadKeyValuesCacheRepo<Key,Value>( open class FullReadKeyValuesCacheRepo<Key,Value>(
protected open val parentRepo: ReadKeyValuesRepo<Key, Value>, protected open val parentRepo: ReadKeyValuesRepo<Key, Value>,
protected open val kvCache: KeyValueRepo<Key, List<Value>>, protected open val kvCache: KeyValueRepo<Key, List<Value>>,
protected val locker: SmartRWLocker = SmartRWLocker(), protected open val locker: SmartRWLocker = SmartRWLocker(),
) : ReadKeyValuesRepo<Key, Value>, FullCacheRepo { ) : ReadKeyValuesRepo<Key, Value>, FullCacheRepo {
protected suspend inline fun <T> doOrTakeAndActualize( protected suspend inline fun <T> doOrTakeAndActualize(
action: KeyValueRepo<Key, List<Value>>.() -> Optional<T>, action: KeyValueRepo<Key, List<Value>>.() -> Optional<T>,
@@ -201,16 +201,16 @@ fun <Key, Value> WriteKeyValuesRepo<Key, Value>.caching(
open class FullKeyValuesCacheRepo<Key,Value>( open class FullKeyValuesCacheRepo<Key,Value>(
override val parentRepo: KeyValuesRepo<Key, Value>, override val parentRepo: KeyValuesRepo<Key, Value>,
kvCache: KeyValueRepo<Key, List<Value>>, override val kvCache: KeyValueRepo<Key, List<Value>>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default), scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
skipStartInvalidate: Boolean = false, skipStartInvalidate: Boolean = false,
locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate), override val locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate),
) : KeyValuesRepo<Key, Value>, ) : KeyValuesRepo<Key, Value>,
FullReadKeyValuesCacheRepo<Key, Value>(parentRepo, kvCache, locker), FullReadKeyValuesCacheRepo<Key, Value>(parentRepo, kvCache, locker),
WriteKeyValuesRepo<Key, Value> by parentRepo { WriteKeyValuesRepo<Key, Value> by parentRepo {
init { init {
if (!skipStartInvalidate) { if (!skipStartInvalidate) {
scope.launchSafelyWithoutExceptions { scope.launchLoggingDropExceptions {
if (locker.writeMutex.isLocked) { if (locker.writeMutex.isLocked) {
initialInvalidate() initialInvalidate()
} else { } else {

View File

@@ -0,0 +1,113 @@
package dev.inmo.micro_utils.repos.cache.full.direct
import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.coroutines.SmartRWLocker
import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
import dev.inmo.micro_utils.coroutines.withReadAcquire
import dev.inmo.micro_utils.coroutines.withWriteLock
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.cache.*
import dev.inmo.micro_utils.repos.cache.util.ActualizeAllClearMode
import dev.inmo.micro_utils.repos.cache.util.actualizeAll
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
open class DirectFullReadCRUDCacheRepo<ObjectType, IdType>(
protected open val parentRepo: ReadCRUDRepo<ObjectType, IdType>,
protected open val kvCache: KeyValueRepo<IdType, ObjectType>,
protected open val locker: SmartRWLocker = SmartRWLocker(),
protected open val idGetter: (ObjectType) -> IdType
) : ReadCRUDRepo<ObjectType, IdType>, DirectFullCacheRepo {
protected open suspend fun actualizeAll() {
kvCache.actualizeAll(parentRepo, locker = locker)
}
override suspend fun getByPagination(pagination: Pagination): PaginationResult<ObjectType> = locker.withReadAcquire {
kvCache.values(pagination)
}
override suspend fun getIdsByPagination(pagination: Pagination): PaginationResult<IdType> = locker.withReadAcquire {
kvCache.keys(pagination)
}
override suspend fun count(): Long = locker.withReadAcquire {
kvCache.count()
}
override suspend fun contains(id: IdType): Boolean = locker.withReadAcquire {
kvCache.contains(id)
}
override suspend fun getAll(): Map<IdType, ObjectType> = locker.withReadAcquire {
kvCache.getAll()
}
override suspend fun getById(id: IdType): ObjectType? = locker.withReadAcquire {
kvCache.get(id)
}
override suspend fun invalidate() {
actualizeAll()
}
}
fun <ObjectType, IdType> ReadCRUDRepo<ObjectType, IdType>.directlyCached(
kvCache: KeyValueRepo<IdType, ObjectType>,
locker: SmartRWLocker = SmartRWLocker(),
idGetter: (ObjectType) -> IdType
) = DirectFullReadCRUDCacheRepo(this, kvCache, locker, idGetter)
open class DirectFullCRUDCacheRepo<ObjectType, IdType, InputValueType>(
override val parentRepo: CRUDRepo<ObjectType, IdType, InputValueType>,
override val kvCache: KeyValueRepo<IdType, ObjectType>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
skipStartInvalidate: Boolean = false,
override val locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate),
idGetter: (ObjectType) -> IdType
) : DirectFullReadCRUDCacheRepo<ObjectType, IdType>(
parentRepo,
kvCache,
locker,
idGetter
),
WriteCRUDRepo<ObjectType, IdType, InputValueType> by WriteCRUDCacheRepo(
parentRepo,
kvCache,
scope,
locker,
idGetter
),
CRUDRepo<ObjectType, IdType, InputValueType> {
init {
if (!skipStartInvalidate) {
scope.launchLoggingDropExceptions {
if (locker.writeMutex.isLocked) {
initialInvalidate()
} else {
invalidate()
}
}
}
}
protected open suspend fun initialInvalidate() {
try {
kvCache.actualizeAll(parentRepo, locker = null)
} finally {
locker.unlockWrite()
}
}
override suspend fun invalidate() {
actualizeAll()
}
}
fun <ObjectType, IdType, InputType> CRUDRepo<ObjectType, IdType, InputType>.directFullyCached(
kvCache: KeyValueRepo<IdType, ObjectType> = MapKeyValueRepo(),
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
skipStartInvalidate: Boolean = false,
locker: SmartRWLocker = SmartRWLocker(),
idGetter: (ObjectType) -> IdType
) = DirectFullCRUDCacheRepo(this, kvCache, scope, skipStartInvalidate, locker, idGetter)

View File

@@ -0,0 +1,13 @@
package dev.inmo.micro_utils.repos.cache.full.direct
import dev.inmo.micro_utils.repos.cache.full.FullCacheRepo
/**
* Repos-inheritors MUST realize their methods via next logic:
*
* * Reloading of data in cache must be reactive (e.g. via Flow) or direct mutation methods usage (override set and
* mutate cache inside, for example)
* * All reading methods must take data from cache via synchronization with [dev.inmo.micro_utils.coroutines.SmartRWLocker]
*/
interface DirectFullCacheRepo : FullCacheRepo {
}

View File

@@ -0,0 +1,177 @@
package dev.inmo.micro_utils.repos.cache.full.direct
import dev.inmo.micro_utils.coroutines.SmartRWLocker
import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
import dev.inmo.micro_utils.coroutines.withReadAcquire
import dev.inmo.micro_utils.coroutines.withWriteLock
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.cache.full.FullKeyValueCacheRepo
import dev.inmo.micro_utils.repos.cache.full.FullReadKeyValueCacheRepo
import dev.inmo.micro_utils.repos.cache.full.FullWriteKeyValueCacheRepo
import dev.inmo.micro_utils.repos.cache.util.actualizeAll
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
open class DirectFullReadKeyValueCacheRepo<Key, Value>(
protected open val parentRepo: ReadKeyValueRepo<Key, Value>,
protected open val kvCache: KeyValueRepo<Key, Value>,
protected open val locker: SmartRWLocker = SmartRWLocker()
) : DirectFullCacheRepo, ReadKeyValueRepo<Key, Value> {
protected open suspend fun actualizeAll() {
kvCache.actualizeAll(parentRepo, locker)
}
override suspend fun get(k: Key): Value? = locker.withReadAcquire {
kvCache.get(k)
}
override suspend fun values(pagination: Pagination, reversed: Boolean): PaginationResult<Value> = locker.withReadAcquire {
kvCache.values(pagination, reversed)
}
override suspend fun count(): Long = locker.withReadAcquire {
kvCache.count()
}
override suspend fun contains(key: Key): Boolean = locker.withReadAcquire {
kvCache.contains(key)
}
override suspend fun getAll(): Map<Key, Value> = locker.withReadAcquire {
kvCache.getAll()
}
override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult<Key> = locker.withReadAcquire {
kvCache.keys(pagination, reversed)
}
override suspend fun keys(v: Value, pagination: Pagination, reversed: Boolean): PaginationResult<Key> = locker.withReadAcquire {
kvCache.keys(v, pagination, reversed)
}
override suspend fun invalidate() {
actualizeAll()
}
}
fun <Key, Value> ReadKeyValueRepo<Key, Value>.directlyCached(
kvCache: KeyValueRepo<Key, Value>,
locker: SmartRWLocker = SmartRWLocker()
) = DirectFullReadKeyValueCacheRepo(this, kvCache, locker)
open class DirectFullWriteKeyValueCacheRepo<Key, Value>(
protected open val parentRepo: WriteKeyValueRepo<Key, Value>,
protected open val kvCache: KeyValueRepo<Key, Value>,
protected val locker: SmartRWLocker = SmartRWLocker(),
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
) : DirectFullCacheRepo, WriteKeyValueRepo<Key, Value> by parentRepo {
override val onNewValue: Flow<Pair<Key, Value>>
get() = parentRepo.onNewValue
override val onValueRemoved: Flow<Key>
get() = parentRepo.onValueRemoved
protected val onNewJob = parentRepo.onNewValue.onEach {
locker.withWriteLock {
kvCache.set(it.first, it.second)
}
}.launchIn(scope)
protected val onRemoveJob = parentRepo.onValueRemoved.onEach {
locker.withWriteLock {
kvCache.unset(it)
}
}.launchIn(scope)
override suspend fun invalidate() {
locker.withWriteLock {
kvCache.clear()
}
}
override suspend fun unsetWithValues(toUnset: List<Value>) = parentRepo.unsetWithValues(toUnset)
}
fun <Key, Value> WriteKeyValueRepo<Key, Value>.directlyCached(
kvCache: KeyValueRepo<Key, Value>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) = DirectFullWriteKeyValueCacheRepo(this, kvCache, scope = scope)
open class DirectFullKeyValueCacheRepo<Key, Value>(
override val parentRepo: KeyValueRepo<Key, Value>,
override val kvCache: KeyValueRepo<Key, Value>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
skipStartInvalidate: Boolean = false,
override val locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate),
) : DirectFullCacheRepo,
KeyValueRepo<Key, Value> ,
WriteKeyValueRepo<Key, Value> by DirectFullWriteKeyValueCacheRepo(
parentRepo,
kvCache,
locker,
scope
),
DirectFullReadKeyValueCacheRepo<Key, Value>(parentRepo, kvCache, locker) {
init {
if (!skipStartInvalidate) {
scope.launchLoggingDropExceptions {
if (locker.writeMutex.isLocked) {
initialInvalidate()
} else {
invalidate()
}
}
}
}
protected open suspend fun initialInvalidate() {
try {
kvCache.actualizeAll(parentRepo, locker = null)
} finally {
locker.unlockWrite()
}
}
override suspend fun invalidate() {
kvCache.actualizeAll(parentRepo, locker)
}
override suspend fun clear() {
parentRepo.clear()
kvCache.clear()
}
override suspend fun unsetWithValues(toUnset: List<Value>) = parentRepo.unsetWithValues(toUnset)
override suspend fun set(toSet: Map<Key, Value>) {
locker.withWriteLock {
parentRepo.set(toSet)
kvCache.set(
toSet.filter {
parentRepo.contains(it.key)
}
)
}
}
override suspend fun unset(toUnset: List<Key>) {
locker.withWriteLock {
parentRepo.unset(toUnset)
kvCache.unset(
toUnset.filter {
!parentRepo.contains(it)
}
)
}
}
}
fun <Key, Value> KeyValueRepo<Key, Value>.directlyFullyCached(
kvCache: KeyValueRepo<Key, Value> = MapKeyValueRepo(),
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
skipStartInvalidate: Boolean = false,
locker: SmartRWLocker = SmartRWLocker()
) = DirectFullKeyValueCacheRepo(this, kvCache, scope, skipStartInvalidate, locker)

View File

@@ -0,0 +1,243 @@
package dev.inmo.micro_utils.repos.cache.full.direct
import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.coroutines.SmartRWLocker
import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
import dev.inmo.micro_utils.coroutines.withReadAcquire
import dev.inmo.micro_utils.coroutines.withWriteLock
import dev.inmo.micro_utils.pagination.*
import dev.inmo.micro_utils.pagination.utils.*
import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.cache.util.ActualizeAllClearMode
import dev.inmo.micro_utils.repos.cache.util.actualizeAll
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
open class DirectFullReadKeyValuesCacheRepo<Key,Value>(
protected open val parentRepo: ReadKeyValuesRepo<Key, Value>,
protected open val kvCache: KeyValueRepo<Key, List<Value>>,
protected open val locker: SmartRWLocker = SmartRWLocker(),
) : ReadKeyValuesRepo<Key, Value>, DirectFullCacheRepo {
protected open suspend fun actualizeKey(k: Key) {
kvCache.actualizeAll(locker = locker, clearMode = ActualizeAllClearMode.Never) {
mapOf(k to parentRepo.getAll(k))
}
}
protected open suspend fun actualizeAll() {
kvCache.actualizeAll(parentRepo, locker = locker)
}
override suspend fun get(k: Key, pagination: Pagination, reversed: Boolean): PaginationResult<Value> {
return locker.withReadAcquire {
kvCache.get(k) ?.paginate(
pagination.let { if (reversed) it.reverse(count(k)) else it }
) ?.let {
if (reversed) it.copy(results = it.results.reversed()) else it
}
} ?: emptyPaginationResult()
}
override suspend fun getAll(k: Key, reversed: Boolean): List<Value> {
return locker.withReadAcquire {
kvCache.get(k) ?.optionallyReverse(reversed)
} ?: emptyList()
}
override suspend fun getAll(reverseLists: Boolean): Map<Key, List<Value>> {
return locker.withReadAcquire {
kvCache.getAll().takeIf { it.isNotEmpty() } ?.let {
if (reverseLists) {
it.mapValues { it.value.reversed() }
} else {
it
}
}
} ?: emptyMap()
}
override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult<Key> {
return locker.withReadAcquire {
kvCache.keys(pagination, reversed).takeIf { it.results.isNotEmpty() }
} ?: emptyPaginationResult()
}
override suspend fun count(): Long = locker.withReadAcquire { kvCache.count() }
override suspend fun count(k: Key): Long = locker.withReadAcquire { kvCache.get(k) ?.size } ?.toLong() ?: 0L
override suspend fun contains(k: Key, v: Value): Boolean = locker.withReadAcquire { kvCache.get(k) ?.contains(v) } == true
override suspend fun contains(k: Key): Boolean = locker.withReadAcquire { kvCache.contains(k) }
override suspend fun keys(
v: Value,
pagination: Pagination,
reversed: Boolean
): PaginationResult<Key> {
val keys = locker.withReadAcquire {
getAllWithNextPaging { kvCache.keys(it) }.filter { kvCache.get(it)?.contains(v) == true }
.optionallyReverse(reversed)
}
val result = if (keys.isNotEmpty()) {
keys.paginate(pagination.optionallyReverse(keys.size, reversed)).takeIf { it.results.isNotEmpty() }
} else {
null
}
return result ?: emptyPaginationResult()
}
override suspend fun invalidate() {
actualizeAll()
}
}
fun <Key, Value> ReadKeyValuesRepo<Key, Value>.directlyCached(
kvCache: KeyValueRepo<Key, List<Value>>,
locker: SmartRWLocker = SmartRWLocker(),
) = DirectFullReadKeyValuesCacheRepo(this, kvCache, locker)
open class DirectFullWriteKeyValuesCacheRepo<Key,Value>(
parentRepo: WriteKeyValuesRepo<Key, Value>,
protected open val kvCache: KeyValueRepo<Key, List<Value>>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
protected val locker: SmartRWLocker = SmartRWLocker(),
) : WriteKeyValuesRepo<Key, Value> by parentRepo, DirectFullCacheRepo {
protected val onNewJob = parentRepo.onNewValue.onEach {
locker.withWriteLock {
kvCache.set(
it.first,
kvCache.get(it.first) ?.plus(it.second) ?: listOf(it.second)
)
}
}.launchIn(scope)
protected val onRemoveJob = parentRepo.onValueRemoved.onEach {
locker.withWriteLock {
kvCache.set(
it.first,
kvCache.get(it.first)?.minus(it.second) ?: return@onEach
)
}
}.launchIn(scope)
override suspend fun invalidate() {
locker.withWriteLock {
kvCache.clear()
}
}
}
fun <Key, Value> WriteKeyValuesRepo<Key, Value>.directlyCached(
kvCache: KeyValueRepo<Key, List<Value>>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
locker: SmartRWLocker = SmartRWLocker(),
) = DirectFullWriteKeyValuesCacheRepo(this, kvCache, scope, locker)
open class DirectFullKeyValuesCacheRepo<Key,Value>(
override val parentRepo: KeyValuesRepo<Key, Value>,
override val kvCache: KeyValueRepo<Key, List<Value>>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
skipStartInvalidate: Boolean = false,
override val locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate),
) : KeyValuesRepo<Key, Value>,
DirectFullReadKeyValuesCacheRepo<Key, Value>(parentRepo, kvCache, locker),
WriteKeyValuesRepo<Key, Value> by parentRepo {
init {
if (!skipStartInvalidate) {
scope.launchLoggingDropExceptions {
if (locker.writeMutex.isLocked) {
initialInvalidate()
} else {
invalidate()
}
}
}
}
override suspend fun clearWithValue(v: Value) {
doAllWithCurrentPaging {
keys(v, it).also {
remove(it.results.associateWith { listOf(v) })
}
}
}
protected open suspend fun initialInvalidate() {
try {
kvCache.actualizeAll(parentRepo, locker = null)
} finally {
locker.unlockWrite()
}
}
override suspend fun invalidate() {
kvCache.actualizeAll(parentRepo, locker = locker)
}
override suspend fun set(toSet: Map<Key, List<Value>>) {
locker.withWriteLock {
parentRepo.set(toSet)
kvCache.set(
toSet.filter {
parentRepo.contains(it.key)
}
)
}
}
override suspend fun add(toAdd: Map<Key, List<Value>>) {
locker.withWriteLock {
parentRepo.add(toAdd)
toAdd.forEach {
val filtered = it.value.filter { v ->
parentRepo.contains(it.key, v)
}.ifEmpty {
return@forEach
}
kvCache.set(
it.key,
(kvCache.get(it.key) ?: emptyList()) + filtered
)
}
}
}
override suspend fun remove(toRemove: Map<Key, List<Value>>) {
locker.withWriteLock {
parentRepo.remove(toRemove)
toRemove.forEach {
val filtered = it.value.filter { v ->
!parentRepo.contains(it.key, v)
}.ifEmpty {
return@forEach
}.toSet()
val resultList = (kvCache.get(it.key) ?: emptyList()) - filtered
if (resultList.isEmpty()) {
kvCache.unset(it.key)
} else {
kvCache.set(
it.key,
resultList
)
}
}
}
}
override suspend fun clear(k: Key) {
locker.withWriteLock {
parentRepo.clear(k)
if (parentRepo.contains(k)) {
return@withWriteLock
}
kvCache.unset(k)
}
}
}
fun <Key, Value> KeyValuesRepo<Key, Value>.directlyFullyCached(
kvCache: KeyValueRepo<Key, List<Value>> = MapKeyValueRepo(),
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
skipStartInvalidate: Boolean = false,
locker: SmartRWLocker = SmartRWLocker(),
) = DirectFullKeyValuesCacheRepo(this, kvCache, scope, skipStartInvalidate, locker)

View File

@@ -137,7 +137,7 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
override suspend fun deleteById(ids: List<IdType>) { override suspend fun deleteById(ids: List<IdType>) {
onBeforeDelete(ids) onBeforeDelete(ids)
transaction(db = database) { transaction(db = database) {
val deleted = deleteWhere(null, null) { selectByIds(it, ids) } val deleted = deleteWhere { selectByIds(it, ids) }
if (deleted == ids.size) { if (deleted == ids.size) {
ids ids
} else { } else {

View File

@@ -8,8 +8,16 @@ interface CommonExposedRepo<IdType, ObjectType> : ExposedRepo {
val selectById: ISqlExpressionBuilder.(IdType) -> Op<Boolean> val selectById: ISqlExpressionBuilder.(IdType) -> Op<Boolean>
val selectByIds: ISqlExpressionBuilder.(List<IdType>) -> Op<Boolean> val selectByIds: ISqlExpressionBuilder.(List<IdType>) -> Op<Boolean>
get() = { get() = {
it.foldRight<IdType, Op<Boolean>?>(null) { id, acc -> if (it.isEmpty()) {
acc ?.or(selectById(id)) ?: selectById(id) Op.FALSE
} ?: Op.FALSE } else {
var op = it.firstOrNull() ?.let { selectById(it) } ?: Op.FALSE
var i = 1
while (i < it.size) {
op = op.or(selectById(it[i]))
i++
}
op
}
} }
} }

View File

@@ -4,7 +4,6 @@ import dev.inmo.micro_utils.repos.KeyValuesRepo
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.statements.InsertStatement
import org.jetbrains.exposed.sql.statements.UpdateBuilder import org.jetbrains.exposed.sql.statements.UpdateBuilder
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
@@ -57,18 +56,48 @@ abstract class AbstractExposedKeyValuesRepo<Key, Value>(
} }
} }
transaction(database) { val (oldObjects, insertedResults) = transaction(database) {
val oldObjects = selectAll().where { selectByIds(toSet.keys.toList()) }.map { it.asKey to it.asObject }
deleteWhere { deleteWhere {
selectByIds(it, toSet.keys.toList()) selectByIds(it, toSet.keys.toList())
} }
batchInsert( val inserted = batchInsert(
prepreparedData, prepreparedData,
) { (k, v) -> ) { (k, v) ->
insert(k, v, this) insert(k, v, this)
}.map { }.map {
it.asKey to it.asObject it.asKey to it.asObject
} }
}.forEach { _onNewValue.emit(it) } oldObjects to inserted
}.let {
val mappedFirst = it
.first
.asSequence()
.groupBy { it.first }
.mapValues { it.value.map { it.second }.toSet() }
val mappedSecond = it
.second
.asSequence()
.groupBy { it.first }
.mapValues { it.value.map { it.second }.toSet() }
mappedFirst to mappedSecond
}
val deletedResults = oldObjects.mapNotNull { (k, vs) ->
k to vs.filter { v ->
insertedResults[k] ?.contains(v) != true
}.ifEmpty { return@mapNotNull null }
}
deletedResults.forEach { (k, vs) ->
vs.forEach { v ->
_onValueRemoved.emit(k to v)
}
}
insertedResults.forEach { (k, vs) ->
vs.forEach { v ->
_onNewValue.emit(k to v)
}
}
} }
override suspend fun remove(toRemove: Map<Key, List<Value>>) { override suspend fun remove(toRemove: Map<Key, List<Value>>) {

View File

@@ -8,7 +8,7 @@ import dev.inmo.micro_utils.repos.ktor.common.key_value.*
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.request.post import io.ktor.client.request.post
import io.ktor.http.* import io.ktor.http.*
import io.ktor.util.InternalAPI import io.ktor.utils.io.InternalAPI
import io.ktor.util.reflect.TypeInfo import io.ktor.util.reflect.TypeInfo
import io.ktor.util.reflect.typeInfo import io.ktor.util.reflect.typeInfo
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow

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