diff --git a/CHANGELOG.md b/CHANGELOG.md index 33b7fd6d5f1..3c22980ce0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 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`: diff --git a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/WithReplaced.kt b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/WithReplaced.kt new file mode 100644 index 00000000000..2fa36eaf05d --- /dev/null +++ b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/WithReplaced.kt @@ -0,0 +1,5 @@ +package dev.inmo.micro_utils.common + +fun Iterable.withReplacedAt(i: Int, block: (T) -> T): List = take(i) + block(elementAt(i)) + drop(i + 1) +fun Iterable.withReplaced(t: T, block: (T) -> T): List = withReplacedAt(indexOf(t), block) + diff --git a/common/src/commonTest/kotlin/dev/inmo/micro_utils/common/WithReplacedTest.kt b/common/src/commonTest/kotlin/dev/inmo/micro_utils/common/WithReplacedTest.kt new file mode 100644 index 00000000000..14a410e2af5 --- /dev/null +++ b/common/src/commonTest/kotlin/dev/inmo/micro_utils/common/WithReplacedTest.kt @@ -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()) + } + } +} \ No newline at end of file diff --git a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/FlowDebouncedBy.kt b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/FlowDebouncedBy.kt new file mode 100644 index 00000000000..08e1ffbc97a --- /dev/null +++ b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/FlowDebouncedBy.kt @@ -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( + val millisToData: Pair +) + +fun Flow.debouncedBy(timeout: (T) -> Long, markerFactory: (T) -> Any?): Flow = channelFlow { + val jobs = mutableMapOf() + 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 Flow.debouncedBy(timeout: Long, markerFactory: (T) -> Any?): Flow = debouncedBy({ timeout }, markerFactory) +fun Flow.debouncedBy(timeout: Duration, markerFactory: (T) -> Any?): Flow = debouncedBy({ timeout.inWholeMilliseconds }, markerFactory) diff --git a/coroutines/src/commonTest/kotlin/DebouncedByTests.kt b/coroutines/src/commonTest/kotlin/DebouncedByTests.kt new file mode 100644 index 00000000000..6680374d9c5 --- /dev/null +++ b/coroutines/src/commonTest/kotlin/DebouncedByTests.kt @@ -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() + + 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) } + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index a851906cd11..aa47a95740e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,5 +15,5 @@ crypto_js_version=4.1.1 # Project data group=dev.inmo -version=0.22.4 -android_code_version=270 +version=0.22.5 +android_code_version=271 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9fd232d21b5..9fafdeb524d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,11 +6,11 @@ kt-coroutines = "1.9.0" kslog = "1.3.6" -jb-compose = "1.7.0-beta02" +jb-compose = "1.7.0-rc01" jb-exposed = "0.55.0" jb-dokka = "1.9.20" -sqlite = "3.46.1.2" +sqlite = "3.46.1.3" korlibs = "5.4.0" uuid = "0.8.4" @@ -34,7 +34,7 @@ dexcount = "4.0.0" android-coreKtx = "1.13.1" android-recyclerView = "1.3.2" android-appCompat = "1.7.0" -android-fragment = "1.8.3" +android-fragment = "1.8.4" android-espresso = "3.6.1" android-test = "1.2.1" android-compose-material3 = "1.3.0" diff --git a/ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/server/configurators/KtorApplicationConfigurator.kt b/ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/server/configurators/KtorApplicationConfigurator.kt index 424be129bdc..a75503b323a 100644 --- a/ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/server/configurators/KtorApplicationConfigurator.kt +++ b/ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/server/configurators/KtorApplicationConfigurator.kt @@ -1,7 +1,101 @@ 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 { + @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() { + pluginOrNull(io.ktor.server.routing.Routing) ?.apply { + rootInstaller.apply { invoke() } + } ?: install(io.ktor.server.routing.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> = emptyList(), + private val pathToResource: List> = 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() }