Merge pull request #459 from InsanusMokrassar/0.22.0

0.22.0
This commit is contained in:
InsanusMokrassar 2024-08-11 16:51:30 +06:00 committed by GitHub
commit 1ee9c88ffd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
67 changed files with 5720 additions and 2984 deletions

2
.gitignore vendored
View File

@ -1,4 +1,5 @@
.idea .idea
.kotlin
out/* out/*
*.iml *.iml
target target
@ -17,5 +18,6 @@ publishing.sh
local.* local.*
local/ local/
**/*.local.*
.kotlin/ .kotlin/

View File

@ -1,5 +1,18 @@
# Changelog # Changelog
## 0.22.0
**THIS UPDATE CONTAINS BREAKING CHANGES ACCORDING TO UPDATE UP TO KOTLIN 2.0**
* `Versions`:
* `Kotlin`: `1.9.23` -> `2.0.10`
* `Serialization`: `1.6.3` -> `1.7.1`
* `KSLog`: `1.3.4` -> `1.3.5`
* `Compose`: `1.6.2` -> `1.7.0-alpha02`
* `Exposed`: `0.50.1` -> `0.53.0`
* `AndroidAppCompat`: `1.6.1` -> `1.7.0`
* `AndroidFragment`: `1.7.1` -> `1.8.2`
## 0.21.6 ## 0.21.6
* `KSP`: * `KSP`:

View File

@ -3,6 +3,7 @@ plugins {
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library" id "com.android.library"
alias(libs.plugins.jb.compose) alias(libs.plugins.jb.compose)
alias(libs.plugins.kt.jb.compose)
} }
apply from: "$mppProjectWithSerializationAndComposePresetPath" apply from: "$mppProjectWithSerializationAndComposePresetPath"

View File

@ -3,6 +3,7 @@ plugins {
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library" id "com.android.library"
alias(libs.plugins.jb.compose) alias(libs.plugins.jb.compose)
alias(libs.plugins.kt.jb.compose)
} }
apply from: "$mppProjectWithSerializationAndComposePresetPath" apply from: "$mppProjectWithSerializationAndComposePresetPath"

View File

@ -29,15 +29,6 @@ allprojects {
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" } maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
maven { url "https://nexus.inmo.dev/repository/maven-releases/" } maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
} }
// temporal crutch until legacy tests will be stabled or legacy target will be removed
if (it != rootProject.findProject("docs")) {
tasks.whenTaskAdded { task ->
if(task.name == "jsLegacyBrowserTest" || task.name == "jsLegacyNodeTest") {
task.enabled = false
}
}
}
} }
apply from: "./extensions.gradle" apply from: "./extensions.gradle"

View File

@ -3,6 +3,7 @@ plugins {
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library" id "com.android.library"
alias(libs.plugins.jb.compose) alias(libs.plugins.jb.compose)
alias(libs.plugins.kt.jb.compose)
} }
apply from: "$mppComposeJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath" apply from: "$mppComposeJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"

View File

@ -8,7 +8,7 @@ private inline fun <T> getObject(
additional: MutableList<T>, additional: MutableList<T>,
iterator: Iterator<T> iterator: Iterator<T>
): T? = when { ): T? = when {
additional.isNotEmpty() -> additional.removeFirst() additional.isNotEmpty() -> additional.removeAt(0)
iterator.hasNext() -> iterator.next() iterator.hasNext() -> iterator.next()
else -> null else -> null
} }

View File

@ -3,6 +3,7 @@ plugins {
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library" id "com.android.library"
alias(libs.plugins.jb.compose) alias(libs.plugins.jb.compose)
alias(libs.plugins.kt.jb.compose)
} }
apply from: "$mppComposeJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath" apply from: "$mppComposeJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"

View File

@ -13,7 +13,7 @@ import kotlin.contracts.contract
* * [unlockWrite] will just unlock [writeMutex] * * [unlockWrite] will just unlock [writeMutex]
*/ */
class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked: Boolean = false) { class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked: Boolean = false) {
private val _readSemaphore = SmartSemaphore.Mutable(permits = readPermits, acquiredPermits = 0) private val _readSemaphore = SmartSemaphore.Mutable(permits = readPermits, acquiredPermits = if (writeIsLocked) readPermits else 0)
private val _writeMutex = SmartMutex.Mutable(locked = writeIsLocked) private val _writeMutex = SmartMutex.Mutable(locked = writeIsLocked)
val readSemaphore: SmartSemaphore.Immutable = _readSemaphore.immutable() val readSemaphore: SmartSemaphore.Immutable = _readSemaphore.immutable()

View File

@ -6,6 +6,7 @@ android {
defaultConfig { defaultConfig {
minSdkVersion libs.versions.android.props.minSdk.get().toInteger() minSdkVersion libs.versions.android.props.minSdk.get().toInteger()
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger() targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
versionCode "${android_code_version}".toInteger() versionCode "${android_code_version}".toInteger()
versionName "$version" versionName "$version"

View File

@ -15,5 +15,5 @@ crypto_js_version=4.1.1
# Project data # Project data
group=dev.inmo group=dev.inmo
version=0.21.6 version=0.22.0
android_code_version=265 android_code_version=266

View File

@ -1,15 +1,17 @@
[versions] [versions]
kt = "1.9.23" kt = "2.0.10"
kt-serialization = "1.6.3" kt-serialization = "1.7.1"
kt-coroutines = "1.8.1" kt-coroutines = "1.8.1"
kslog = "1.3.4" kslog = "1.3.5"
jb-compose = "1.6.2" jb-compose = "1.7.0-alpha02"
jb-exposed = "0.50.1" jb-exposed = "0.53.0"
jb-dokka = "1.9.20" jb-dokka = "1.9.20"
sqlite = "3.46.0.1"
korlibs = "5.4.0" korlibs = "5.4.0"
uuid = "0.8.4" uuid = "0.8.4"
@ -21,25 +23,25 @@ koin = "3.5.6"
okio = "3.9.0" okio = "3.9.0"
ksp = "1.9.23-1.0.20" ksp = "2.0.10-1.0.24"
kotlin-poet = "1.16.0" kotlin-poet = "1.18.1"
versions = "0.51.0" versions = "0.51.0"
android-gradle = "8.2.0" android-gradle = "8.2.2"
dexcount = "4.0.0" dexcount = "4.0.0"
android-coreKtx = "1.13.1" android-coreKtx = "1.13.1"
android-recyclerView = "1.3.2" android-recyclerView = "1.3.2"
android-appCompat = "1.6.1" android-appCompat = "1.7.0"
android-fragment = "1.7.1" android-fragment = "1.8.2"
android-espresso = "3.5.1" android-espresso = "3.6.1"
android-test = "1.1.5" android-test = "1.2.1"
android-compose-material3 = "1.2.1" android-compose-material3 = "1.2.1"
android-props-minSdk = "21" android-props-minSdk = "21"
android-props-compileSdk = "34" android-props-compileSdk = "35"
android-props-buildTools = "34.0.0" android-props-buildTools = "35.0.0"
[libraries] [libraries]
@ -52,6 +54,7 @@ kt-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-
kt-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kt-coroutines" } kt-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kt-coroutines" }
kt-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kt-coroutines" } kt-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kt-coroutines" }
kt-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kt-coroutines" } kt-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kt-coroutines" }
kt-coroutines-debug = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-debug", version.ref = "kt-coroutines" }
ktor-io = { module = "io.ktor:ktor-io", version.ref = "ktor" } ktor-io = { module = "io.ktor:ktor-io", version.ref = "ktor" }
@ -79,6 +82,8 @@ koin = { module = "io.insert-koin:koin-core", version.ref = "koin" }
jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-exposed" } jb-exposed = { module = "org.jetbrains.exposed:exposed-core", 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" }
android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" } android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
@ -113,5 +118,6 @@ buildscript-android-dexcount = { module = "com.getkeepsafe.dexcount:dexcount-gra
[plugins] [plugins]
jb-compose = { id = "org.jetbrains.compose", version.ref = "jb-compose" } jb-compose = { id = "org.jetbrains.compose", version.ref = "jb-compose" }
kt-jb-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kt" }
versions = { id = "com.github.ben-manes.versions", version.ref = "versions" } versions = { id = "com.github.ben-manes.versions", version.ref = "versions" }

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

File diff suppressed because one or more lines are too long

View File

@ -22,17 +22,24 @@ private const val baseClassSerializerAnnotationName = "@Serializable(${baseClass
@Serializable @Serializable
private data class LanguageCode( private data class LanguageCode(
@SerialName("alpha2")
val tag: String,
@SerialName("English") @SerialName("English")
val title: String val title: String,
) @SerialName("alpha2")
val alpha: String? = null,
@SerialName("alpha3-b")
val alpha2: String? = null,
@SerialName("alpha3-t")
val alpha3: String? = null,
) {
val tag: String
get() = alpha ?: alpha2 ?: alpha3!!
}
fun String.adaptAsTitle() = if (first().isDigit()) { fun String.adaptAsTitle() = (if (first().isDigit()) {
"L$this" "L$this"
} else { } else {
this this
} }).replace(".", "_").replace("'", "_")
fun String.normalized() = Normalizer.normalize(this, Normalizer.Form.NFD).replace(Regex("[^\\p{ASCII}]"), "") fun String.normalized() = Normalizer.normalize(this, Normalizer.Form.NFD).replace(Regex("[^\\p{ASCII}]"), "")
@ -73,7 +80,10 @@ data class Tag(
val title: String, val title: String,
val tag: String, val tag: String,
val subtags: List<Tag> val subtags: List<Tag>
) ) {
val adaptedTitle
get() = title.adaptAsTitle()
}
private fun printLanguageCodeAndTags( private fun printLanguageCodeAndTags(
tag: Tag, tag: Tag,
@ -81,17 +91,19 @@ private fun printLanguageCodeAndTags(
indents: String = " " indents: String = " "
): String = if (tag.subtags.isEmpty()) { ): String = if (tag.subtags.isEmpty()) {
"""${indents}${baseClassSerializerAnnotationName} """${indents}${baseClassSerializerAnnotationName}
${indents}object ${tag.title} : ${parent ?.title ?: baseClassName}() { override val code: String = "${tag.tag}"${parent ?.let { parent -> "; override val parentLang: ${parent.title} get() = ${parent.title};" } ?: ""} }""" ${indents}object ${tag.adaptedTitle} : ${parent ?.adaptedTitle ?: baseClassName} { override val code: String = "${tag.tag}"${parent ?.let { parent -> "; override val parentLang: ${parent.adaptedTitle} get() = ${parent.adaptedTitle}" } ?: ""}; override fun toString() = code }"""
} else { } else {
""" """
${indents}${baseClassSerializerAnnotationName} ${indents}${baseClassSerializerAnnotationName}
${indents}sealed class ${tag.title} : ${parent ?.title ?: baseClassName}() { ${indents}sealed interface ${tag.adaptedTitle} : ${parent ?.adaptedTitle ?: baseClassName} {
${indents} override val code: String = "${tag.tag}"${parent ?.let { parent -> "\n${indents} override val parentLang: ${parent.title} get() = ${parent.title};" } ?: ""}
${tag.subtags.joinToString("\n") { printLanguageCodeAndTags(it, tag, "${indents} ") }} ${tag.subtags.joinToString("\n") { printLanguageCodeAndTags(it, tag, "${indents} ") }}
${indents} ${baseClassSerializerAnnotationName} ${indents} ${baseClassSerializerAnnotationName}
${indents} companion object : ${tag.title}() ${indents} companion object : ${tag.adaptedTitle} {
${indents} override val code: String = "${tag.tag}"${parent ?.let { parent -> "\n${indents} override val parentLang: ${parent.adaptedTitle} get() = ${parent.adaptedTitle};" } ?: ""}
${indents} override fun toString() = code
${indents} }
${indents}} ${indents}}
""" """
} }
@ -105,23 +117,22 @@ import kotlinx.serialization.Serializable
* https://datahub.io/core/language-codes/ files (base and tags) and create the whole hierarchy using it. * https://datahub.io/core/language-codes/ files (base and tags) and create the whole hierarchy using it.
*/ */
${baseClassSerializerAnnotationName} ${baseClassSerializerAnnotationName}
sealed class $baseClassName { sealed interface $baseClassName {
abstract val code: String val code: String
open val parentLang: $baseClassName? val parentLang: $baseClassName?
get() = null get() = code.split("-").takeIf { it.size > 1 } ?.first() ?.let(::$unknownBaseClassName)
open val withoutDialect: String val withoutDialect: String
get() = parentLang ?.code ?: code get() = parentLang ?.code ?: code
${tags.joinToString("\n") { printLanguageCodeAndTags(it, indents = " ") } } ${tags.joinToString("\n") { printLanguageCodeAndTags(it, indents = " ") } }
$baseClassSerializerAnnotationName $baseClassSerializerAnnotationName
data class $unknownBaseClassName (override val code: String) : $baseClassName() { data class $unknownBaseClassName (override val code: String) : $baseClassName {
override val parentLang = code.dropLastWhile { it != '-' }.removeSuffix("-").takeIf { it.length > 0 } ?.let(::$unknownBaseClassName) override val parentLang = code.dropLastWhile { it != '-' }.removeSuffix("-").takeIf { it.length > 0 } ?.let(::$unknownBaseClassName)
} }
@Deprecated("Renamed", ReplaceWith("$baseClassName.$unknownBaseClassName", "${if (prePackage.isNotEmpty()) "$prePackage." else ""}$baseClassName.$unknownBaseClassName")) @Deprecated("Renamed", ReplaceWith("$baseClassName.$unknownBaseClassName", "${if (prePackage.isNotEmpty()) "$prePackage." else ""}$baseClassName.$unknownBaseClassName"))
val $oldUnknownBaseClassName = $unknownBaseClassName val $oldUnknownBaseClassName
get() = $unknownBaseClassName
override fun toString() = code
} }
@Deprecated("Renamed", ReplaceWith("$baseClassName", "${if (prePackage.isNotEmpty()) "$prePackage." else ""}$baseClassName")) @Deprecated("Renamed", ReplaceWith("$baseClassName", "${if (prePackage.isNotEmpty()) "$prePackage." else ""}$baseClassName"))
typealias $oldBaseClassName = $baseClassName typealias $oldBaseClassName = $baseClassName
@ -133,7 +144,7 @@ fun createStringConverterCode(tags: List<Tag>, prePackage: String): String {
pretitle: String = baseClassName, pretitle: String = baseClassName,
indents: String = " " indents: String = " "
): String { ): String {
val currentTitle = "$pretitle.${tag.title}" val currentTitle = "$pretitle.${tag.adaptedTitle}"
return """${indents}$currentTitle.code -> $currentTitle${if (tag.subtags.isNotEmpty()) tag.subtags.joinToString("\n", "\n") { createDeserializeVariantForTag(it, currentTitle, indents) } else ""}""" return """${indents}$currentTitle.code -> $currentTitle${if (tag.subtags.isNotEmpty()) tag.subtags.joinToString("\n", "\n") { createDeserializeVariantForTag(it, currentTitle, indents) } else ""}"""
} }
fun createInheritorVariantForTag( fun createInheritorVariantForTag(
@ -141,7 +152,7 @@ fun createStringConverterCode(tags: List<Tag>, prePackage: String): String {
pretitle: String = baseClassName, pretitle: String = baseClassName,
indents: String = " " indents: String = " "
): String { ): String {
val currentTitle = "$pretitle.${tag.title}" val currentTitle = "$pretitle.${tag.adaptedTitle}"
val subtags = if (tag.subtags.isNotEmpty()) { val subtags = if (tag.subtags.isNotEmpty()) {
tag.subtags.joinToString(",\n", ",\n") { createInheritorVariantForTag(it, currentTitle, "$indents ") } tag.subtags.joinToString(",\n", ",\n") { createInheritorVariantForTag(it, currentTitle, "$indents ") }
} else { } else {
@ -155,7 +166,7 @@ fun createStringConverterCode(tags: List<Tag>, prePackage: String): String {
indents: String = " ", indents: String = " ",
codeSuffix: String = "" codeSuffix: String = ""
): String { ): String {
val currentTitle = "$pretitle.${tag.title}" val currentTitle = "$pretitle.${tag.adaptedTitle}"
val subtags = if (tag.subtags.isNotEmpty()) { val subtags = if (tag.subtags.isNotEmpty()) {
tag.subtags.joinToString(",\n", ",\n") { createInheritorVariantForMapForTag(it, currentTitle, "$indents ", codeSuffix) } tag.subtags.joinToString(",\n", ",\n") { createInheritorVariantForMapForTag(it, currentTitle, "$indents ", codeSuffix) }
} else { } else {
@ -266,7 +277,7 @@ suspend fun main(vararg args: String) {
File(outputFolder, "LanguageCodes.kt").apply { File(outputFolder, "LanguageCodes.kt").apply {
delete() delete()
createNewFile() createNewFile()
writeText(targetPackagePrefix + buildKtFileContent(tags, targetPackage ?: "")) writeText("@file:Suppress(\"SERIALIZER_TYPE_INCOMPATIBLE\")\n\n" + targetPackagePrefix + buildKtFileContent(tags, targetPackage ?: ""))
} }
File(outputFolder, "StringToLanguageCodes.kt").apply { File(outputFolder, "StringToLanguageCodes.kt").apply {

File diff suppressed because one or more lines are too long

View File

@ -12,8 +12,20 @@ kotlin {
} }
} }
js (IR) { js (IR) {
browser() browser {
nodejs() testTask {
useMocha {
timeout = "60000"
}
}
}
nodejs {
testTask {
useMocha {
timeout = "60000"
}
}
}
} }
androidTarget { androidTarget {
publishAllLibraryVariants() publishAllLibraryVariants()
@ -77,7 +89,6 @@ kotlin {
jsTest { jsTest {
dependencies { dependencies {
implementation kotlin('test-js') implementation kotlin('test-js')
implementation kotlin('test-junit')
} }
} }
nativeMain.dependsOn commonMain nativeMain.dependsOn commonMain

View File

@ -12,8 +12,20 @@ kotlin {
} }
} }
js (IR) { js (IR) {
browser() browser {
nodejs() testTask {
useMocha {
timeout = "60000"
}
}
}
nodejs {
testTask {
useMocha {
timeout = "60000"
}
}
}
} }
androidTarget { androidTarget {
publishAllLibraryVariants() publishAllLibraryVariants()
@ -36,7 +48,7 @@ kotlin {
} }
commonTest { commonTest {
dependencies { dependencies {
implementation kotlin('test-common') implementation kotlin('test')
implementation kotlin('test-annotations-common') implementation kotlin('test-annotations-common')
implementation libs.kt.coroutines.test implementation libs.kt.coroutines.test
} }
@ -57,7 +69,6 @@ kotlin {
jsTest { jsTest {
dependencies { dependencies {
implementation kotlin('test-js') implementation kotlin('test-js')
implementation kotlin('test-junit')
} }
} }
nativeMain.dependsOn commonMain nativeMain.dependsOn commonMain

View File

@ -12,8 +12,20 @@ kotlin {
} }
} }
js (IR) { js (IR) {
browser() browser {
nodejs() testTask {
useMocha {
timeout = "60000"
}
}
}
nodejs {
testTask {
useMocha {
timeout = "60000"
}
}
}
} }
androidTarget { androidTarget {
publishAllLibraryVariants() publishAllLibraryVariants()
@ -48,7 +60,6 @@ kotlin {
jsTest { jsTest {
dependencies { dependencies {
implementation kotlin('test-js') implementation kotlin('test-js')
implementation kotlin('test-junit')
} }
} }
androidUnitTest { androidUnitTest {
@ -58,16 +69,6 @@ kotlin {
implementation libs.android.espresso implementation libs.android.espresso
} }
} }
mingwX64Test {
dependencies {
implementation kotlin('test-junit')
}
}
linuxX64Test {
dependencies {
implementation kotlin('test-junit')
}
}
androidMain.dependsOn jvmMain androidMain.dependsOn jvmMain
} }

View File

@ -12,8 +12,20 @@ kotlin {
} }
} }
js (IR) { js (IR) {
browser() browser {
nodejs() testTask {
useMocha {
timeout = "60000"
}
}
}
nodejs {
testTask {
useMocha {
timeout = "60000"
}
}
}
} }
linuxX64() linuxX64()
mingwX64() mingwX64()
@ -42,7 +54,6 @@ kotlin {
jsTest { jsTest {
dependencies { dependencies {
implementation kotlin('test-js') implementation kotlin('test-js')
implementation kotlin('test-junit')
} }
} }

View File

@ -12,8 +12,20 @@ kotlin {
} }
} }
js (IR) { js (IR) {
browser() browser {
nodejs() testTask {
useMocha {
timeout = "60000"
}
}
}
nodejs {
testTask {
useMocha {
timeout = "60000"
}
}
}
} }
linuxX64() linuxX64()
mingwX64() mingwX64()
@ -41,17 +53,6 @@ kotlin {
jsTest { jsTest {
dependencies { dependencies {
implementation kotlin('test-js') implementation kotlin('test-js')
implementation kotlin('test-junit')
}
}
mingwX64Test {
dependencies {
implementation kotlin('test-junit')
}
}
linuxX64Test {
dependencies {
implementation kotlin('test-junit')
} }
} }

View File

@ -12,8 +12,20 @@ kotlin {
} }
} }
js (IR) { js (IR) {
browser() browser {
nodejs() testTask {
useMocha {
timeout = "60000"
}
}
}
nodejs {
testTask {
useMocha {
timeout = "60000"
}
}
}
} }
androidTarget { androidTarget {
publishAllLibraryVariants() publishAllLibraryVariants()
@ -58,7 +70,6 @@ kotlin {
jsTest { jsTest {
dependencies { dependencies {
implementation kotlin('test-js') implementation kotlin('test-js')
implementation kotlin('test-junit')
} }
} }
androidUnitTest { androidUnitTest {
@ -69,13 +80,6 @@ kotlin {
implementation compose.uiTest implementation compose.uiTest
} }
} }
androidInstrumentedTest {
dependencies {
implementation kotlin('test-junit')
implementation libs.android.test.junit
implementation libs.android.espresso
}
}
} }
} }

View File

@ -98,7 +98,7 @@ open class FullCRUDCacheRepo<ObjectType, IdType, InputValueType>(
kvCache: KeyValueRepo<IdType, ObjectType>, kvCache: KeyValueRepo<IdType, ObjectType>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default), scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
skipStartInvalidate: Boolean = false, skipStartInvalidate: Boolean = false,
locker: SmartRWLocker = SmartRWLocker(), locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate),
idGetter: (ObjectType) -> IdType idGetter: (ObjectType) -> IdType
) : FullReadCRUDCacheRepo<ObjectType, IdType>( ) : FullReadCRUDCacheRepo<ObjectType, IdType>(
parentRepo, parentRepo,
@ -116,10 +116,23 @@ open class FullCRUDCacheRepo<ObjectType, IdType, InputValueType>(
CRUDRepo<ObjectType, IdType, InputValueType> { CRUDRepo<ObjectType, IdType, InputValueType> {
init { init {
if (!skipStartInvalidate) { if (!skipStartInvalidate) {
scope.launchSafelyWithoutExceptions { invalidate() } scope.launchSafelyWithoutExceptions {
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() { override suspend fun invalidate() {
actualizeAll() actualizeAll()
} }

View File

@ -126,26 +126,40 @@ fun <Key, Value> WriteKeyValueRepo<Key, Value>.caching(
) = FullWriteKeyValueCacheRepo(this, kvCache, scope) ) = FullWriteKeyValueCacheRepo(this, kvCache, scope)
open class FullKeyValueCacheRepo<Key,Value>( open class FullKeyValueCacheRepo<Key,Value>(
protected open val parentRepo: KeyValueRepo<Key, Value>, override val parentRepo: KeyValueRepo<Key, Value>,
kvCache: KeyValueRepo<Key, Value>, kvCache: KeyValueRepo<Key, Value>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default), scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
skipStartInvalidate: Boolean = false, skipStartInvalidate: Boolean = false,
locker: SmartRWLocker = SmartRWLocker() locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate),
) : FullWriteKeyValueCacheRepo<Key,Value>(parentRepo, kvCache, scope), ) : //FullWriteKeyValueCacheRepo<Key,Value>(parentRepo, kvCache, scope),
KeyValueRepo<Key,Value>, KeyValueRepo<Key,Value>,
ReadKeyValueRepo<Key, Value> by FullReadKeyValueCacheRepo( WriteKeyValueRepo<Key,Value> by parentRepo,
FullReadKeyValueCacheRepo<Key, Value>(
parentRepo, parentRepo,
kvCache, kvCache,
locker locker
) { ) {
init { init {
if (!skipStartInvalidate) { if (!skipStartInvalidate) {
scope.launchSafelyWithoutExceptions { invalidate() } scope.launchSafelyWithoutExceptions {
if (locker.writeMutex.isLocked) {
initialInvalidate()
} else {
invalidate()
}
}
} }
} }
override suspend fun unsetWithValues(toUnset: List<Value>) = parentRepo.unsetWithValues(toUnset) override suspend fun unsetWithValues(toUnset: List<Value>) = parentRepo.unsetWithValues(toUnset)
protected open suspend fun initialInvalidate() {
try {
kvCache.actualizeAll(parentRepo, locker = null)
} finally {
locker.unlockWrite()
}
}
override suspend fun invalidate() { override suspend fun invalidate() {
kvCache.actualizeAll(parentRepo, locker) kvCache.actualizeAll(parentRepo, locker)
} }
@ -154,6 +168,28 @@ open class FullKeyValueCacheRepo<Key,Value>(
parentRepo.clear() parentRepo.clear()
kvCache.clear() kvCache.clear()
} }
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>.fullyCached( fun <Key, Value> KeyValueRepo<Key, Value>.fullyCached(

View File

@ -10,6 +10,7 @@ import dev.inmo.micro_utils.pagination.utils.*
import dev.inmo.micro_utils.repos.* import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.cache.util.ActualizeAllClearMode import dev.inmo.micro_utils.repos.cache.util.ActualizeAllClearMode
import dev.inmo.micro_utils.repos.cache.util.actualizeAll import dev.inmo.micro_utils.repos.cache.util.actualizeAll
import dev.inmo.micro_utils.repos.pagination.maxPagePagination
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@ -65,6 +66,42 @@ open class FullReadKeyValuesCacheRepo<Key,Value>(
) )
} }
override suspend fun getAll(k: Key, reversed: Boolean): List<Value> {
return doOrTakeAndActualizeWithWriteLock(
{
get(k) ?.optionallyReverse(reversed).optionalOrAbsentIfNull
},
{ getAll(k, reversed) },
{ kvCache.set(k, it.optionallyReverse(reversed)) }
)
}
override suspend fun getAll(reverseLists: Boolean): Map<Key, List<Value>> {
return doOrTakeAndActualizeWithWriteLock(
{
getAll().takeIf { it.isNotEmpty() } ?.let {
if (reverseLists) {
it.mapValues { it.value.reversed() }
} else {
it
}
}.optionalOrAbsentIfNull
},
{ getAll(reverseLists) },
{
kvCache.set(
it.let {
if (reverseLists) {
it.mapValues { it.value.reversed() }
} else {
it
}
}
)
}
)
}
override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult<Key> { override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult<Key> {
return doOrTakeAndActualize( return doOrTakeAndActualize(
{ {
@ -163,17 +200,23 @@ fun <Key, Value> WriteKeyValuesRepo<Key, Value>.caching(
) = FullWriteKeyValuesCacheRepo(this, kvCache, scope, locker) ) = FullWriteKeyValuesCacheRepo(this, kvCache, scope, locker)
open class FullKeyValuesCacheRepo<Key,Value>( open class FullKeyValuesCacheRepo<Key,Value>(
protected open val parentRepo: KeyValuesRepo<Key, Value>, override val parentRepo: KeyValuesRepo<Key, Value>,
kvCache: KeyValueRepo<Key, List<Value>>, kvCache: KeyValueRepo<Key, List<Value>>,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default), scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
skipStartInvalidate: Boolean = false, skipStartInvalidate: Boolean = false,
locker: SmartRWLocker = SmartRWLocker(), locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate),
) : FullWriteKeyValuesCacheRepo<Key, Value>(parentRepo, kvCache, scope, locker), ) : KeyValuesRepo<Key, Value>,
KeyValuesRepo<Key, Value>, FullReadKeyValuesCacheRepo<Key, Value>(parentRepo, kvCache, locker),
ReadKeyValuesRepo<Key, Value> by FullReadKeyValuesCacheRepo(parentRepo, kvCache, locker) { WriteKeyValuesRepo<Key, Value> by parentRepo {
init { init {
if (!skipStartInvalidate) { if (!skipStartInvalidate) {
scope.launchSafelyWithoutExceptions { invalidate() } scope.launchSafelyWithoutExceptions {
if (locker.writeMutex.isLocked) {
initialInvalidate()
} else {
invalidate()
}
}
} }
} }
@ -185,16 +228,75 @@ open class FullKeyValuesCacheRepo<Key,Value>(
} }
} }
protected open suspend fun initialInvalidate() {
try {
kvCache.actualizeAll(parentRepo, locker = null)
} finally {
locker.unlockWrite()
}
}
override suspend fun invalidate() { override suspend fun invalidate() {
kvCache.actualizeAll(parentRepo, locker = locker) kvCache.actualizeAll(parentRepo, locker = locker)
} }
override suspend fun set(toSet: Map<Key, List<Value>>) { override suspend fun set(toSet: Map<Key, List<Value>>) {
super<KeyValuesRepo>.set(toSet) locker.withWriteLock {
parentRepo.set(toSet)
kvCache.set(
toSet.filter {
parentRepo.contains(it.key)
}
)
}
} }
override suspend fun removeWithValue(v: Value) { override suspend fun add(toAdd: Map<Key, List<Value>>) {
super<FullWriteKeyValuesCacheRepo>.removeWithValue(v) 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)
}
} }
} }

View File

@ -0,0 +1,142 @@
package full
import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.repos.MapCRUDRepo
import dev.inmo.micro_utils.repos.MapKeyValueRepo
import dev.inmo.micro_utils.repos.cache.full.FullCRUDCacheRepo
import dev.inmo.micro_utils.repos.create
import dev.inmo.micro_utils.repos.deleteById
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class FullCRUDCacheRepoTests {
data class New(
val data: String
)
data class Registered(
val id: String,
val data: String
)
@Test
fun creatingWorksProperly() = runTest {
val testData = (0 until 1000).map {
("$it-" + uuid4().toString())
}
val updatedTestData = (0 until 1000).map {
("$it-" + uuid4().toString())
}
val kvCache = MapKeyValueRepo<String, Registered>()
val crudRepo = MapCRUDRepo<Registered, String, New>(
{ new, id, old ->
Registered(id, new.data)
}
) {
val id = uuid4().toString()
id to Registered(id, it.data)
}
val cacheRepo = FullCRUDCacheRepo(
crudRepo,
kvCache,
idGetter = { it.id }
)
val registereds = testData.map {
val created = cacheRepo.create(New(it)).first()
assertEquals(it, created.data)
assertEquals(kvCache.get(created.id), created)
assertEquals(crudRepo.getById(created.id), created)
created
}
cacheRepo.getAll().forEach { (id, value) ->
assertTrue {
registereds.first {
it.id == id
} == value
}
}
val updatedRegistereds = registereds.mapIndexed { i, it ->
val updated = cacheRepo.update(it.id, New(updatedTestData[i])) ?: error("Unable to update data for $it")
assertEquals(updatedTestData[i], updated.data)
assertEquals(kvCache.get(updated.id), updated)
assertEquals(crudRepo.getById(updated.id), updated)
updated
}
cacheRepo.getAll().forEach { (id, value) ->
assertTrue {
updatedRegistereds.first {
it.id == id
} == value
}
}
}
@Test
fun precachingWorksProperly() = runTest {
val testData = (0 until 1000).map {
(it.toString() + uuid4().toString())
}
val kvCache = MapKeyValueRepo<String, Registered>()
val crudRepo = MapCRUDRepo<Registered, String, New>(
{ new, id, old ->
Registered(id, new.data)
}
) {
val id = uuid4().toString()
id to Registered(id, it.data)
}
val registereds = crudRepo.create(testData.map { New(it) })
val cacheRepo = FullCRUDCacheRepo(
crudRepo,
kvCache,
idGetter = { it.id }
)
cacheRepo.getAll().forEach { (id, value) ->
assertTrue {
registereds.first {
it.id == id
} == value
}
}
}
@Test
fun removingWorksProperly() = runTest {
val testData = (0 until 1000).map {
(it.toString() + uuid4().toString())
}
val kvCache = MapKeyValueRepo<String, Registered>()
val crudRepo = MapCRUDRepo<Registered, String, New>(
{ new, id, old ->
Registered(id, new.data)
}
) {
val id = uuid4().toString()
id to Registered(id, it.data)
}
val registereds = crudRepo.create(testData.map { New(it) })
val cacheRepo = FullCRUDCacheRepo(
crudRepo,
kvCache,
idGetter = { it.id }
)
registereds.forEach {
val id = it.id
assertTrue {
cacheRepo.getAll()[id] == it
}
cacheRepo.deleteById(id)
assertFalse {
cacheRepo.contains(id)
}
}
assertEquals(0, cacheRepo.count())
}
}

View File

@ -0,0 +1,92 @@
package full
import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.cache.full.FullKeyValueCacheRepo
import korlibs.time.seconds
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class FullKeyValueCacheRepoTests {
@Test
fun creatingWorksProperly() = runTest(timeout = 120.seconds) {
val testData = (0 until 1000).associate {
("$it-" + uuid4().toString()) to "$it-" + uuid4().toString()
}
val updatedTestData = testData.keys.associateWith {
"$it-" + uuid4().toString()
}
val kvCache = MapKeyValueRepo<String, String>()
val kvRepo = MapKeyValueRepo<String, String>()
val cacheRepo = FullKeyValueCacheRepo(
kvRepo,
kvCache
)
testData.forEach {
cacheRepo.set(it.key, it.value)
assertEquals(it.value, cacheRepo.get(it.key))
assertEquals(it.value, kvRepo.get(it.key))
assertEquals(it.value, kvCache.get(it.key))
}
updatedTestData.forEach {
cacheRepo.set(it.key, it.value)
assertEquals(cacheRepo.get(it.key), it.value)
assertEquals(kvRepo.get(it.key), it.value)
assertEquals(kvCache.get(it.key), it.value)
}
}
@Test
fun precachingWorksProperly() = runTest {
val testData = (0 until 1000).associate {
(it.toString() + uuid4().toString()) to uuid4().toString()
}
val kvCache = MapKeyValueRepo<String, String>()
val kvRepo = MapKeyValueRepo<String, String>()
kvRepo.set(testData)
val cacheRepo = FullKeyValueCacheRepo(
kvRepo,
kvCache
)
cacheRepo.getAll().forEach { (id, value) ->
assertTrue {
testData[id] == value
}
}
}
@Test
fun unsettingWorksProperly() = runTest {
val testData = (0 until 1000).associate {
(it.toString() + uuid4().toString()) to uuid4().toString()
}
val kvCache = MapKeyValueRepo<String, String>()
val kvRepo = MapKeyValueRepo<String, String>()
kvRepo.set(testData)
val cacheRepo = FullKeyValueCacheRepo(
kvRepo,
kvCache
)
testData.forEach {
val id = it.key
assertTrue {
cacheRepo.getAll()[id] == it.value
}
cacheRepo.unset(id)
assertFalse {
cacheRepo.contains(id)
}
}
assertEquals(0, cacheRepo.count())
}
}

View File

@ -0,0 +1,58 @@
package full
import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.cache.full.FullKeyValuesCacheRepo
import dev.inmo.micro_utils.repos.pagination.maxPagePagination
import korlibs.time.days
import korlibs.time.seconds
import korlibs.time.years
import kotlinx.coroutines.test.runTest
import kotlin.test.*
class FullKeyValuesCacheRepoTests {
@Test
fun creatingWorksProperly() = runTest(timeout = 120.seconds) {
val testData = (0 until 1000).associate {
("$it-" + uuid4().toString()) to (0 until 1000).map {
"$it-" + uuid4().toString()
}.sorted()
}
val updatedTestData = testData.keys.associateWith {
(0 until 1000).map {
"$it-" + uuid4().toString()
}.sorted()
}
val addedData = testData.keys.associateWith {
"$it-" + uuid4().toString()
}
val kvCache = MapKeyValueRepo<String, List<String>>()
val kvRepo = MapKeyValuesRepo<String, String>()
val cacheRepo = FullKeyValuesCacheRepo(
kvRepo,
kvCache
)
testData.forEach {
cacheRepo.set(it.key, it.value)
assertContentEquals(it.value, cacheRepo.getAll(it.key))
assertContentEquals(it.value, kvRepo.getAll(it.key))
assertContentEquals(it.value, kvCache.get(it.key) ?.sorted())
}
updatedTestData.forEach {
cacheRepo.set(it.key, it.value)
assertContentEquals(it.value, cacheRepo.getAll(it.key))
assertContentEquals(it.value, kvRepo.getAll(it.key))
assertContentEquals(it.value, kvCache.get(it.key) ?.sorted())
}
addedData.forEach {
cacheRepo.add(it.key, it.value)
assertTrue(cacheRepo.contains(it.key, it.value))
assertTrue(kvRepo.contains(it.key, it.value))
assertTrue(kvCache.get(it.key) !!.contains(it.value))
}
}
}

View File

@ -0,0 +1,69 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
project.version = "$version"
project.group = "$group"
kotlin {
jvm {
compilations.main {
kotlinOptions {
jvmTarget = "17"
}
}
}
js (IR) {
browser()
nodejs()
}
androidTarget {
compilations.all {
kotlinOptions {
jvmTarget = "17"
}
}
}
linuxX64()
mingwX64()
linuxArm64()
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
api libs.kt.serialization
api kotlin('test')
api kotlin('test-annotations-common')
api libs.kt.coroutines.test
api project(":micro_utils.repos.common")
}
}
jvmMain {
dependencies {
implementation kotlin('test-junit')
}
}
jsMain {
dependencies {
implementation kotlin('test-js')
}
}
nativeMain.dependsOn commonMain
linuxX64Main.dependsOn nativeMain
mingwX64Main.dependsOn nativeMain
linuxArm64Main.dependsOn nativeMain
androidMain.dependsOn jvmMain
}
}
apply from: "$defaultAndroidSettingsPresetPath"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

View File

@ -0,0 +1,82 @@
package dev.inmo.micro_utils.repos.common.tests
import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.repos.CRUDRepo
import dev.inmo.micro_utils.repos.create
import dev.inmo.micro_utils.repos.deleteById
import korlibs.time.seconds
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.Serializable
import kotlin.test.*
abstract class CommonCRUDRepoTests : CommonRepoTests<CRUDRepo<CommonCRUDRepoTests.Registered, String, CommonCRUDRepoTests.New>>() {
@Serializable
data class New(
val data: String
)
@Serializable
data class Registered(
val id: String,
val data: String
)
@Test
fun creatingWorksProperly() = runTest(timeout = 120.seconds) {
val crudRepo = repoCreator()
val testData = (0 until testSequencesSize).map {
("$it-" + uuid4().toString())
}
val updatedTestData = (0 until 1000).map {
("$it-" + uuid4().toString())
}
val registereds = testData.map {
val created = crudRepo.create(New(it)).first()
assertEquals(it, created.data)
assertEquals(crudRepo.getById(created.id), created)
created
}
crudRepo.getAll().forEach { (id, value) ->
assertTrue {
registereds.first {
it.id == id
} == value
}
}
val updatedRegistereds = registereds.mapIndexed { i, it ->
val updated = crudRepo.update(it.id, New(updatedTestData[i])) ?: error("Unable to update data for $it")
assertEquals(updatedTestData[i], updated.data)
assertEquals(crudRepo.getById(updated.id), updated)
updated
}
crudRepo.getAll().forEach { (id, value) ->
assertTrue {
updatedRegistereds.first {
it.id == id
} == value
}
}
}
@Test
fun removingWorksProperly() = runTest {
val crudRepo = repoCreator()
val testData = (0 until testSequencesSize).map {
(it.toString() + uuid4().toString())
}
val registereds = crudRepo.create(testData.map { New(it) })
registereds.forEach {
val id = it.id
assertTrue {
crudRepo.getAll()[id] == it
}
crudRepo.deleteById(id)
assertFalse {
crudRepo.contains(id)
}
}
assertEquals(0, crudRepo.count())
}
}

View File

@ -0,0 +1,55 @@
package dev.inmo.micro_utils.repos.common.tests
import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.repos.*
import korlibs.time.seconds
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
abstract class CommonKeyValueRepoTests : CommonRepoTests<KeyValueRepo<String, String>>() {
@Test
fun creatingWorksProperly() = runTest(timeout = 120.seconds) {
val repo = repoCreator()
val testData = (0 until testSequencesSize).associate {
("$it-" + uuid4().toString()) to "$it-" + uuid4().toString()
}
val updatedTestData = testData.keys.associateWith {
"$it-" + uuid4().toString()
}
testData.forEach {
repo.set(it.key, it.value)
assertEquals(it.value, repo.get(it.key))
}
updatedTestData.forEach {
repo.set(it.key, it.value)
assertEquals(repo.get(it.key), it.value)
}
}
@Test
fun unsettingWorksProperly() = runTest {
val repo = repoCreator()
val testData = (0 until testSequencesSize).associate {
(it.toString() + uuid4().toString()) to uuid4().toString()
}
repo.set(testData)
testData.forEach {
val id = it.key
assertTrue {
repo.getAll()[id] == it.value
}
repo.unset(id)
assertFalse {
repo.contains(id)
}
}
assertEquals(0, repo.count())
}
}

View File

@ -0,0 +1,49 @@
package dev.inmo.micro_utils.repos.common.tests
import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.repos.*
import korlibs.time.seconds
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import kotlin.test.*
abstract class CommonKeyValuesRepoTests : CommonRepoTests<KeyValuesRepo<String, String>>() {
@Test
fun creatingWorksProperly() = runTest(timeout = 120.seconds) {
val repo = repoCreator()
val testData = (0 until testSequencesSize).associate {
("$it-" + uuid4().toString()) to (0 until 1000).map {
"$it-" + uuid4().toString()
}.sorted()
}
val updatedTestData = testData.keys.associateWith {
(0 until 1000).map {
"$it-" + uuid4().toString()
}.sorted()
}
val addedData = testData.keys.associateWith {
"$it-" + uuid4().toString()
}
updatedTestData.map {
launch {
repo.set(it.key, it.value)
assertContentEquals(it.value.sorted(), repo.getAll(it.key).sorted())
}
}.joinAll()
updatedTestData.map {
launch {
repo.set(it.key, it.value)
val all = repo.getAll(it.key)
assertContentEquals(it.value.sorted(), all.sorted())
}
}.joinAll()
addedData.forEach {
repo.add(it.key, it.value)
assertTrue(repo.contains(it.key, it.value))
}
}
}

View File

@ -0,0 +1,6 @@
package dev.inmo.micro_utils.repos.common.tests
abstract class CommonRepoTests<T> {
protected open val testSequencesSize = 1000
protected abstract val repoCreator: suspend () -> T
}

View File

@ -14,5 +14,12 @@ kotlin {
api internalProject("micro_utils.pagination.exposed") api internalProject("micro_utils.pagination.exposed")
} }
} }
jvmTest {
dependencies {
api libs.sqlite
api libs.jb.exposed.jdbc
api project(":micro_utils.repos.common.tests")
}
}
} }
} }

View File

@ -52,9 +52,9 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
* *
* @return In case when id for the model has been created new [IdType] should be returned * @return In case when id for the model has been created new [IdType] should be returned
*/ */
protected open fun createAndInsertId(value: InputValueType, it: InsertStatement<Number>): IdType? = null protected open fun createAndInsertId(value: InputValueType, it: UpdateBuilder<Int>): IdType? = null
protected open fun insert(value: InputValueType, it: InsertStatement<Number>) { protected open fun insert(value: InputValueType, it: UpdateBuilder<Int>) {
val id = createAndInsertId(value, it) val id = createAndInsertId(value, it)
update(id, value, it as UpdateBuilder<Int>) update(id, value, it as UpdateBuilder<Int>)
} }

View File

@ -23,11 +23,11 @@ abstract class AbstractExposedKeyValueRepo<Key, Value>(
override val onValueRemoved: Flow<Key> = _onValueRemoved.asSharedFlow() override val onValueRemoved: Flow<Key> = _onValueRemoved.asSharedFlow()
protected abstract fun update(k: Key, v: Value, it: UpdateBuilder<Int>) protected abstract fun update(k: Key, v: Value, it: UpdateBuilder<Int>)
protected abstract fun insertKey(k: Key, v: Value, it: InsertStatement<Number>) protected abstract fun insertKey(k: Key, v: Value, it: UpdateBuilder<Int>)
protected open fun insert(k: Key, v: Value, it: InsertStatement<Number>) { protected open fun insert(k: Key, v: Value, it: UpdateBuilder<Int>) {
insertKey(k, v, it) insertKey(k, v, it)
update(k, v, it as UpdateBuilder<Int>) update(k, v, it)
} }
override suspend fun set(toSet: Map<Key, Value>) { override suspend fun set(toSet: Map<Key, Value>) {

View File

@ -5,6 +5,7 @@ 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.InsertStatement
import org.jetbrains.exposed.sql.statements.UpdateBuilder
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
abstract class AbstractExposedKeyValuesRepo<Key, Value>( abstract class AbstractExposedKeyValuesRepo<Key, Value>(
@ -26,7 +27,7 @@ abstract class AbstractExposedKeyValuesRepo<Key, Value>(
override val onDataCleared: Flow<Key> override val onDataCleared: Flow<Key>
get() = _onDataCleared.asSharedFlow() get() = _onDataCleared.asSharedFlow()
protected abstract fun insert(k: Key, v: Value, it: InsertStatement<Number>) protected abstract fun insert(k: Key, v: Value, it: UpdateBuilder<Int>)
override suspend fun add(toAdd: Map<Key, List<Value>>) { override suspend fun add(toAdd: Map<Key, List<Value>>) {
transaction(database) { transaction(database) {
@ -48,6 +49,28 @@ abstract class AbstractExposedKeyValuesRepo<Key, Value>(
}.forEach { _onNewValue.emit(it) } }.forEach { _onNewValue.emit(it) }
} }
override suspend fun set(toSet: Map<Key, List<Value>>) {
if (toSet.isEmpty()) return
val prepreparedData = toSet.flatMap { (k, vs) ->
vs.map { v ->
k to v
}
}
transaction(database) {
deleteWhere {
selectByIds(it, toSet.keys.toList())
}
batchInsert(
prepreparedData,
) { (k, v) ->
insert(k, v, this)
}.map {
it.asKey to it.asObject
}
}.forEach { _onNewValue.emit(it) }
}
override suspend fun remove(toRemove: Map<Key, List<Value>>) { override suspend fun remove(toRemove: Map<Key, List<Value>>) {
transaction(database) { transaction(database) {
toRemove.keys.flatMap { k -> toRemove.keys.flatMap { k ->

View File

@ -83,8 +83,8 @@ abstract class AbstractExposedReadKeyValuesRepo<Key, Value>(
} }
} }
override suspend fun getAll(k: Key, reverseLists: Boolean): List<Value> = transaction(database) { override suspend fun getAll(k: Key, reversed: Boolean): List<Value> = transaction(database) {
val query = if (reverseLists) { val query = if (reversed) {
selectAll().where { selectById(k) }.orderBy(keyColumn, SortOrder.DESC) selectAll().where { selectById(k) }.orderBy(keyColumn, SortOrder.DESC)
} else { } else {
selectAll().where { selectById(k) } selectAll().where { selectById(k) }

View File

@ -0,0 +1,21 @@
package full
import com.benasher44.uuid.uuid4
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.transactionManager
import org.sqlite.JDBC
import java.io.File
import java.sql.Connection
fun filename() = "${uuid4()}.local.sql".also {
val file = File(it)
file.createNewFile()
file.deleteOnExit()
}
fun createDatabase(filename: String) = Database.connect(
url = "jdbc:sqlite:$filename",
driver = JDBC::class.qualifiedName!!
).also {
it.transactionManager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE
it.connector().close()
}

View File

@ -0,0 +1,62 @@
package full
import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.repos.CRUDRepo
import dev.inmo.micro_utils.repos.common.tests.CommonCRUDRepoTests
import dev.inmo.micro_utils.repos.exposed.AbstractExposedCRUDRepo
import dev.inmo.micro_utils.repos.exposed.initTable
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.ISqlExpressionBuilder
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.statements.InsertStatement
import org.jetbrains.exposed.sql.statements.UpdateBuilder
import java.io.File
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
class ExposedCRUDRepoTests : CommonCRUDRepoTests() {
class Repo(override val database: Database) : AbstractExposedCRUDRepo<Registered, String, New>() {
val idColumn = text("_id")
val dataColumn = text("data")
override val primaryKey: PrimaryKey = PrimaryKey(idColumn)
override val ResultRow.asId: String
get() = get(idColumn)
override val ResultRow.asObject: Registered
get() = Registered(
asId,
get(dataColumn)
)
override val selectById: ISqlExpressionBuilder.(String) -> Op<Boolean> = { idColumn.eq(it) }
init {
initTable()
}
override fun update(id: String?, value: New, it: UpdateBuilder<Int>) {
it[idColumn] = id ?: uuid4().toString()
it[dataColumn] = value.data
}
override fun InsertStatement<Number>.asObject(value: New): Registered {
return Registered(
get(idColumn),
get(dataColumn)
)
}
}
val filename = filename()
var database: Database? = null
override val repoCreator: suspend () -> CRUDRepo<Registered, String, New> = { Repo(database!!) }
@BeforeTest
fun beforeTest() {
database = createDatabase(filename)
}
@AfterTest
fun afterTest() {
database = null
File(filename).delete()
}
}

View File

@ -0,0 +1,56 @@
package full
import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.common.tests.CommonKeyValueRepoTests
import dev.inmo.micro_utils.repos.exposed.initTable
import dev.inmo.micro_utils.repos.exposed.keyvalue.AbstractExposedKeyValueRepo
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.ISqlExpressionBuilder
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.statements.UpdateBuilder
import java.io.File
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
class ExposedKeyValueRepoTests : CommonKeyValueRepoTests() {
class Repo(override val database: Database) : AbstractExposedKeyValueRepo<String, String>(database) {
override val keyColumn = text("_id")
val dataColumn = text("data")
override val primaryKey: PrimaryKey = PrimaryKey(keyColumn)
override val ResultRow.asKey: String
get() = get(keyColumn)
override val selectByValue: ISqlExpressionBuilder.(String) -> Op<Boolean> = { dataColumn.eq(it) }
override val ResultRow.asObject: String
get() = get(dataColumn)
override val selectById: ISqlExpressionBuilder.(String) -> Op<Boolean> = { keyColumn.eq(it) }
init {
initTable()
}
override fun update(k: String, v: String, it: UpdateBuilder<Int>) {
it[keyColumn] = k
it[dataColumn] = v
}
override fun insertKey(k: String, v: String, it: UpdateBuilder<Int>) {
it[keyColumn] = k
}
}
val filename = filename()
var database: Database? = null
@BeforeTest
fun beforeTest() {
database = createDatabase(filename)
}
@AfterTest
fun afterTest() {
database = null
File(filename).delete()
}
override val repoCreator: suspend () -> KeyValueRepo<String, String> = { Repo(database!!) }
}

View File

@ -0,0 +1,52 @@
package full
import dev.inmo.micro_utils.repos.KeyValuesRepo
import dev.inmo.micro_utils.repos.common.tests.CommonKeyValuesRepoTests
import dev.inmo.micro_utils.repos.exposed.initTable
import dev.inmo.micro_utils.repos.exposed.onetomany.AbstractExposedKeyValuesRepo
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.ISqlExpressionBuilder
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.statements.UpdateBuilder
import java.io.File
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
class ExposedKeyValuesRepoTests : CommonKeyValuesRepoTests() {
override val testSequencesSize: Int = 100
class Repo(override val database: Database) : AbstractExposedKeyValuesRepo<String, String>(database) {
override val keyColumn = text("_id")
val dataColumn = text("data")
override val ResultRow.asKey: String
get() = get(keyColumn)
override val selectByValue: ISqlExpressionBuilder.(String) -> Op<Boolean> = { dataColumn.eq(it) }
override val ResultRow.asObject: String
get() = get(dataColumn)
override val selectById: ISqlExpressionBuilder.(String) -> Op<Boolean> = { keyColumn.eq(it) }
init {
initTable()
}
override fun insert(k: String, v: String, it: UpdateBuilder<Int>) {
it[keyColumn] = k
it[dataColumn] = v
}
}
val filename = filename()
var database: Database? = null
@BeforeTest
fun beforeTest() {
database = createDatabase(filename)
}
@AfterTest
fun afterTest() {
database = null
File(filename).delete()
}
override val repoCreator: suspend () -> KeyValuesRepo<String, String> = { Repo(database!!) }
}

View File

@ -14,5 +14,10 @@ kotlin {
api internalProject("micro_utils.coroutines") api internalProject("micro_utils.coroutines")
} }
} }
commonTest {
dependencies {
api project(":micro_utils.repos.common.tests")
}
}
} }
} }

View File

@ -24,7 +24,9 @@ class ReadMapKeyValueRepo<Key, Value>(
) : ReadKeyValueRepo<Key, Value> { ) : ReadKeyValueRepo<Key, Value> {
constructor(map: Map<Key, Value> = emptyMap()) : this(map, SmartRWLocker()) constructor(map: Map<Key, Value> = emptyMap()) : this(map, SmartRWLocker())
override suspend fun get(k: Key): Value? = locker.withReadAcquire { map[k] } override suspend fun get(k: Key): Value? = locker.withReadAcquire {
map[k]
}
override suspend fun values( override suspend fun values(
pagination: Pagination, pagination: Pagination,
@ -100,11 +102,13 @@ class WriteMapKeyValueRepo<Key, Value>(
constructor(map: MutableMap<Key, Value> = mutableMapOf()) : this(map, SmartRWLocker()) constructor(map: MutableMap<Key, Value> = mutableMapOf()) : this(map, SmartRWLocker())
override suspend fun set(toSet: Map<Key, Value>) { override suspend fun set(toSet: Map<Key, Value>) {
if (toSet.isEmpty()) return
locker.withWriteLock { map.putAll(toSet) } locker.withWriteLock { map.putAll(toSet) }
toSet.forEach { (k, v) -> _onNewValue.emit(k to v) } toSet.forEach { (k, v) -> _onNewValue.emit(k to v) }
} }
override suspend fun unset(toUnset: List<Key>) { override suspend fun unset(toUnset: List<Key>) {
if (toUnset.isEmpty()) return
locker.withWriteLock { locker.withWriteLock {
toUnset.mapNotNull { k -> toUnset.mapNotNull { k ->
map.remove(k) ?.let { _ -> k } map.remove(k) ?.let { _ -> k }

View File

@ -4,6 +4,7 @@ import dev.inmo.micro_utils.coroutines.SmartRWLocker
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.*
import dev.inmo.micro_utils.pagination.utils.optionallyReverse
import dev.inmo.micro_utils.pagination.utils.paginate import dev.inmo.micro_utils.pagination.utils.paginate
import dev.inmo.micro_utils.pagination.utils.reverse import dev.inmo.micro_utils.pagination.utils.reverse
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@ -33,6 +34,20 @@ class MapReadKeyValuesRepo<Key, Value>(
) )
} }
override suspend fun getAll(k: Key, reversed: Boolean): List<Value> {
return locker.withReadAcquire { map[k] ?.optionallyReverse(reversed) ?: return emptyList() }
}
override suspend fun getAll(reverseLists: Boolean): Map<Key, List<Value>> {
return locker.withReadAcquire {
if (reverseLists) {
map.mapValues { it.value.reversed() }
} else {
map.toMap()
}
}
}
override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult<Key> { override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult<Key> {
val keys = locker.withReadAcquire { val keys = locker.withReadAcquire {
map.keys map.keys
@ -108,7 +123,24 @@ class MapWriteKeyValuesRepo<Key, Value>(
} }
} }
override suspend fun set(toSet: Map<Key, List<Value>>) {
if (toSet.isEmpty()) return
locker.withWriteLock {
map.putAll(
toSet.mapValues { it.value.toMutableList() }
)
}
toSet.forEach { (k, v) ->
v.forEach {
_onNewValue.emit(k to it)
}
}
}
override suspend fun remove(toRemove: Map<Key, List<Value>>) { override suspend fun remove(toRemove: Map<Key, List<Value>>) {
if (toRemove.isEmpty()) return
val removed = mutableListOf<Pair<Key, Value>>() val removed = mutableListOf<Pair<Key, Value>>()
val cleared = mutableListOf<Key>() val cleared = mutableListOf<Key>()
locker.withWriteLock { locker.withWriteLock {

View File

@ -0,0 +1,19 @@
package full
import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.repos.CRUDRepo
import dev.inmo.micro_utils.repos.MapCRUDRepo
import dev.inmo.micro_utils.repos.common.tests.CommonCRUDRepoTests
class InMemoryCRUDRepoTests : CommonCRUDRepoTests() {
override val repoCreator: suspend () -> CRUDRepo<Registered, String, New> = {
MapCRUDRepo(
{ new, id, old ->
Registered(id, new.data)
}
) {
val id = uuid4().toString()
id to Registered(id, it.data)
}
}
}

View File

@ -0,0 +1,9 @@
package full
import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.MapKeyValueRepo
import dev.inmo.micro_utils.repos.common.tests.CommonKeyValueRepoTests
class InMemoryKeyValueRepoTests : CommonKeyValueRepoTests() {
override val repoCreator: suspend () -> KeyValueRepo<String, String> = { MapKeyValueRepo() }
}

View File

@ -0,0 +1,9 @@
package full
import dev.inmo.micro_utils.repos.KeyValuesRepo
import dev.inmo.micro_utils.repos.MapKeyValuesRepo
import dev.inmo.micro_utils.repos.common.tests.CommonKeyValuesRepoTests
class InMemoryKeyValuesRepoTests : CommonKeyValuesRepoTests() {
override val repoCreator: suspend () -> KeyValuesRepo<String, String> = { MapKeyValuesRepo() }
}

View File

@ -16,6 +16,7 @@ kotlin {
jvmTest { jvmTest {
dependencies { dependencies {
implementation internalProject("micro_utils.repos.common") implementation internalProject("micro_utils.repos.common")
implementation internalProject("micro_utils.repos.common.tests")
implementation internalProject("micro_utils.repos.ktor.client") implementation internalProject("micro_utils.repos.ktor.client")
implementation internalProject("micro_utils.repos.ktor.server") implementation internalProject("micro_utils.repos.ktor.server")
implementation internalProject("micro_utils.repos.inmemory") implementation internalProject("micro_utils.repos.inmemory")

View File

@ -1,3 +1,5 @@
package dev.inmo.micro_utils.repos.ktor
import com.benasher44.uuid.uuid4 import com.benasher44.uuid.uuid4
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View File

@ -1,4 +1,8 @@
package dev.inmo.micro_utils.repos.ktor
import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.repos.* import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.common.tests.CommonCRUDRepoTests
import dev.inmo.micro_utils.repos.ktor.client.crud.KtorCRUDRepoClient import dev.inmo.micro_utils.repos.ktor.client.crud.KtorCRUDRepoClient
import dev.inmo.micro_utils.repos.ktor.server.crud.configureCRUDRepoRoutes import dev.inmo.micro_utils.repos.ktor.server.crud.configureCRUDRepoRoutes
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
@ -8,17 +12,53 @@ import io.ktor.serialization.kotlinx.KotlinxWebsocketSerializationConverter
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.install import io.ktor.server.application.install
import io.ktor.server.cio.CIO import io.ktor.server.cio.CIO
import io.ktor.server.engine.*
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.routing.routing import io.ktor.server.routing.routing
import io.ktor.server.websocket.WebSockets import io.ktor.server.websocket.WebSockets
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestResult
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class CRUDTests { class KtorCRUDRepoTests : CommonCRUDRepoTests() {
@OptIn(ExperimentalCoroutinesApi::class) private var engine: ApplicationEngine? = null
@BeforeTest
fun beforeTest() {
engine = KtorRepoTestsHelper.beforeTest {
configureCRUDRepoRoutes(
MapCRUDRepo<Registered, String, New>(
{ new, id, old ->
Registered(id, new.data)
}
) {
val id = uuid4().toString()
id to Registered(id, it.data)
}
) {
it
}
}
}
@AfterTest
fun afterTest() {
engine ?.let(KtorRepoTestsHelper::afterTest)
}
override val repoCreator: suspend () -> CRUDRepo<Registered, String, New> = {
KtorCRUDRepoClient<Registered, String, New>(
"http://127.0.0.1:23456",
KtorRepoTestsHelper.client(),
ContentType.Application.Json
) {
it
}
}
@Test @Test
fun testCRUDFunctions() { fun testCRUDFunctions() {
runTest { runTest {
@ -33,7 +73,7 @@ class CRUDTests {
} }
val server = io.ktor.server.engine.embeddedServer( val server = io.ktor.server.engine.embeddedServer(
CIO, CIO,
23456, 34567,
"127.0.0.1" "127.0.0.1"
) { ) {
install(ContentNegotiation) { install(ContentNegotiation) {
@ -60,7 +100,7 @@ class CRUDTests {
} }
} }
val crudClient = KtorCRUDRepoClient<ComplexData, Int, SimpleData>( val crudClient = KtorCRUDRepoClient<ComplexData, Int, SimpleData>(
"http://127.0.0.1:23456", "http://127.0.0.1:34567",
client, client,
ContentType.Application.Json ContentType.Application.Json
) { ) {

View File

@ -1,8 +1,13 @@
package dev.inmo.micro_utils.repos.ktor
import dev.inmo.micro_utils.pagination.firstPageWithOneElementPagination import dev.inmo.micro_utils.pagination.firstPageWithOneElementPagination
import dev.inmo.micro_utils.pagination.utils.getAllWithNextPaging import dev.inmo.micro_utils.pagination.utils.getAllWithNextPaging
import dev.inmo.micro_utils.repos.* import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.common.tests.CommonKeyValueRepoTests
import dev.inmo.micro_utils.repos.ktor.client.key.value.KtorKeyValueRepoClient import dev.inmo.micro_utils.repos.ktor.client.key.value.KtorKeyValueRepoClient
import dev.inmo.micro_utils.repos.ktor.client.key.values.KtorKeyValuesRepoClient
import dev.inmo.micro_utils.repos.ktor.server.key.value.configureKeyValueRepoRoutes import dev.inmo.micro_utils.repos.ktor.server.key.value.configureKeyValueRepoRoutes
import dev.inmo.micro_utils.repos.ktor.server.key.values.configureKeyValuesRepoRoutes
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.plugins.logging.Logging import io.ktor.client.plugins.logging.Logging
import io.ktor.http.ContentType import io.ktor.http.ContentType
@ -10,17 +15,47 @@ import io.ktor.serialization.kotlinx.KotlinxWebsocketSerializationConverter
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.install import io.ktor.server.application.install
import io.ktor.server.cio.CIO import io.ktor.server.cio.CIO
import io.ktor.server.engine.*
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.routing.routing import io.ktor.server.routing.routing
import io.ktor.server.websocket.WebSockets import io.ktor.server.websocket.WebSockets
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestResult
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlin.test.* import kotlin.test.*
class KVTests { class KtorKeyValueRepoTests : CommonKeyValueRepoTests() {
@OptIn(ExperimentalCoroutinesApi::class) private var engine: ApplicationEngine? = null
override val repoCreator: suspend () -> KeyValueRepo<String, String> = {
KtorKeyValueRepoClient(
"http://127.0.0.1:23456",
KtorRepoTestsHelper.client(),
ContentType.Application.Json,
String.serializer(),
String.serializer(),
Json
)
}
@BeforeTest
fun beforeTest() {
engine = KtorRepoTestsHelper.beforeTest {
configureKeyValueRepoRoutes(
MapKeyValueRepo(),
String.serializer(),
String.serializer(),
Json
)
}
}
@AfterTest
fun afterTest() {
engine ?.let(KtorRepoTestsHelper::afterTest)
}
@Test @Test
fun testKVFunctions() { fun testKVFunctions() {
runTest { runTest {
@ -28,7 +63,7 @@ class KVTests {
val repo = MapKeyValueRepo<Int, ComplexData>(map) val repo = MapKeyValueRepo<Int, ComplexData>(map)
val server = io.ktor.server.engine.embeddedServer( val server = io.ktor.server.engine.embeddedServer(
CIO, CIO,
23456, 34567,
"127.0.0.1" "127.0.0.1"
) { ) {
install(ContentNegotiation) { install(ContentNegotiation) {
@ -56,7 +91,7 @@ class KVTests {
} }
} }
val crudClient = KtorKeyValueRepoClient<Int, ComplexData>( val crudClient = KtorKeyValueRepoClient<Int, ComplexData>(
"http://127.0.0.1:23456", "http://127.0.0.1:34567",
client, client,
ContentType.Application.Json, ContentType.Application.Json,
Int.serializer(), Int.serializer(),

View File

@ -1,26 +1,59 @@
package dev.inmo.micro_utils.repos.ktor
import dev.inmo.micro_utils.pagination.firstPageWithOneElementPagination import dev.inmo.micro_utils.pagination.firstPageWithOneElementPagination
import dev.inmo.micro_utils.pagination.utils.getAllWithNextPaging import dev.inmo.micro_utils.pagination.utils.getAllWithNextPaging
import dev.inmo.micro_utils.repos.* import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.common.tests.CommonKeyValuesRepoTests
import dev.inmo.micro_utils.repos.ktor.client.key.values.KtorKeyValuesRepoClient import dev.inmo.micro_utils.repos.ktor.client.key.values.KtorKeyValuesRepoClient
import dev.inmo.micro_utils.repos.ktor.server.key.values.configureKeyValuesRepoRoutes import dev.inmo.micro_utils.repos.ktor.server.key.values.configureKeyValuesRepoRoutes
import io.ktor.client.HttpClient import io.ktor.client.*
import io.ktor.client.plugins.logging.Logging import io.ktor.client.plugins.logging.*
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.serialization.kotlinx.KotlinxWebsocketSerializationConverter import io.ktor.serialization.kotlinx.*
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.install import io.ktor.server.application.*
import io.ktor.server.cio.CIO import io.ktor.server.cio.*
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.engine.*
import io.ktor.server.routing.routing import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.websocket.WebSockets import io.ktor.server.routing.*
import kotlinx.coroutines.ExperimentalCoroutinesApi import io.ktor.server.websocket.*
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlin.test.* import kotlin.test.*
class KVsTests { class KtorKeyValuesRepoTests : CommonKeyValuesRepoTests() {
@OptIn(ExperimentalCoroutinesApi::class) private var engine: ApplicationEngine? = null
override val testSequencesSize: Int
get() = 100
override val repoCreator: suspend () -> KeyValuesRepo<String, String> = {
KtorKeyValuesRepoClient(
"http://127.0.0.1:23456",
KtorRepoTestsHelper.client(),
ContentType.Application.Json,
String.serializer(),
String.serializer(),
Json
)
}
@BeforeTest
fun beforeTest() {
engine = KtorRepoTestsHelper.beforeTest {
configureKeyValuesRepoRoutes(
MapKeyValuesRepo(),
String.serializer(),
String.serializer(),
Json
)
}
}
@AfterTest
fun afterTest() {
engine ?.let(KtorRepoTestsHelper::afterTest)
}
@Test @Test
fun testKVsFunctions() { fun testKVsFunctions() {
runTest { runTest {
@ -28,7 +61,7 @@ class KVsTests {
val repo = MapKeyValuesRepo(map) val repo = MapKeyValuesRepo(map)
val server = io.ktor.server.engine.embeddedServer( val server = io.ktor.server.engine.embeddedServer(
CIO, CIO,
23456, 34567,
"127.0.0.1" "127.0.0.1"
) { ) {
install(ContentNegotiation) { install(ContentNegotiation) {
@ -42,7 +75,7 @@ class KVsTests {
repo, repo,
Int.serializer(), Int.serializer(),
ComplexData.serializer(), ComplexData.serializer(),
Json {} Json
) )
} }
}.start(false) }.start(false)
@ -56,7 +89,7 @@ class KVsTests {
} }
} }
val crudClient = KtorKeyValuesRepoClient( val crudClient = KtorKeyValuesRepoClient(
"http://127.0.0.1:23456", "http://127.0.0.1:34567",
client, client,
ContentType.Application.Json, ContentType.Application.Json,
Int.serializer(), Int.serializer(),

View File

@ -0,0 +1,44 @@
package dev.inmo.micro_utils.repos.ktor
import io.ktor.client.*
import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.cio.*
import io.ktor.server.engine.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.routing.*
import io.ktor.server.websocket.*
import kotlinx.serialization.json.Json
object KtorRepoTestsHelper {
fun beforeTest(routingConfigurator: Routing.() -> Unit): ApplicationEngine {
return embeddedServer(
CIO,
23456,
"127.0.0.1"
) {
install(ContentNegotiation) {
json()
}
install(WebSockets) {
contentConverter = KotlinxWebsocketSerializationConverter(Json)
}
routing(routingConfigurator)
}.start(false)
}
fun afterTest(engine: ApplicationEngine) {
engine.stop()
}
fun client(): HttpClient = HttpClient {
install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) {
json()
}
install(Logging)
install(io.ktor.client.plugins.websocket.WebSockets) {
contentConverter = KotlinxWebsocketSerializationConverter(Json)
}
}
}

View File

@ -1,3 +1,5 @@
package dev.inmo.micro_utils.repos.ktor
import com.benasher44.uuid.uuid4 import com.benasher44.uuid.uuid4
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View File

@ -18,6 +18,7 @@ String[] includes = [
":language_codes", ":language_codes",
":language_codes:generator", ":language_codes:generator",
":repos:common", ":repos:common",
":repos:common:tests",
":repos:generator", ":repos:generator",
":repos:generator:test", ":repos:generator:test",
":repos:cache", ":repos:cache",

View File

@ -13,6 +13,7 @@ kotlin {
api libs.kslog api libs.kslog
api libs.kt.reflect api libs.kt.reflect
api project(":micro_utils.coroutines") api project(":micro_utils.coroutines")
api libs.uuid
} }
} }
jsMain { jsMain {

View File

@ -1,5 +1,42 @@
package dev.inmo.micro_utils.startup.plugin package dev.inmo.micro_utils.startup.plugin
import com.benasher44.uuid.uuid4
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
expect object StartPluginSerializer : KSerializer<StartPlugin> object StartPluginSerializer : KSerializer<StartPlugin> {
private val registeredPlugins = mutableMapOf<String, StartPlugin>()
private val registeredPluginsByPlugin = mutableMapOf<StartPlugin, String>()
override val descriptor: SerialDescriptor = String.serializer().descriptor
override fun deserialize(decoder: Decoder): StartPlugin {
alternativeDeserialize(decoder) ?.let { return it }
val name = decoder.decodeString()
return registeredPlugins[name] ?: error("Unable to find startup plugin for $name")
}
override fun serialize(encoder: Encoder, value: StartPlugin) {
if (alternativeSerialize(encoder, value)) {
return
}
val name = registeredPluginsByPlugin[value] ?: uuid4().toString().also {
registeredPlugins[it] = value
registeredPluginsByPlugin[value] = it
}
encoder.encodeString(name)
}
/**
* Register plugin inside of this [KSerializer]. Since plugin has been registered, you may use its [name] in any
* serialized [dev.inmo.micro_utils.startup.launcher.Config] to retrieve [plugin] you passed here
*/
fun registerPlugin(name: String, plugin: StartPlugin) {
registeredPlugins[name] = plugin
}
}
internal expect fun alternativeDeserialize(decoder: Decoder): StartPlugin?
internal expect fun alternativeSerialize(encoder: Encoder, value: StartPlugin): Boolean

View File

@ -0,0 +1,13 @@
package dev.inmo.micro_utils.startup.plugin
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
internal actual fun alternativeDeserialize(decoder: Decoder): StartPlugin? {
return null
}
internal actual fun alternativeSerialize(
encoder: Encoder,
value: StartPlugin
): Boolean = false

View File

@ -1,35 +0,0 @@
package dev.inmo.micro_utils.startup.plugin
import com.benasher44.uuid.uuid4
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
actual object StartPluginSerializer : KSerializer<StartPlugin> {
private val registeredPlugins = mutableMapOf<String, StartPlugin>()
private val registeredPluginsByPlugin = mutableMapOf<StartPlugin, String>()
override val descriptor: SerialDescriptor = String.serializer().descriptor
override fun deserialize(decoder: Decoder): StartPlugin {
val name = decoder.decodeString()
return registeredPlugins[name] ?: error("Unable to find startup plugin for $name")
}
override fun serialize(encoder: Encoder, value: StartPlugin) {
val name = registeredPluginsByPlugin[value] ?: uuid4().toString().also {
registeredPlugins[it] = value
registeredPluginsByPlugin[value] = it
}
encoder.encodeString(name)
}
/**
* Register plugin inside of this [KSerializer]. Since plugin has been registered, you may use its [name] in any
* serialized [dev.inmo.micro_utils.startup.launcher.Config] to retrieve [plugin] you passed here
*/
fun registerPlugin(name: String, plugin: StartPlugin) {
registeredPlugins[name] = plugin
}
}

View File

@ -0,0 +1,17 @@
package dev.inmo.micro_utils.startup.plugin
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
internal actual fun alternativeDeserialize(decoder: Decoder): StartPlugin? {
val kclass = Class.forName(decoder.decodeString()).kotlin
return (kclass.objectInstance ?: kclass.constructors.first { it.parameters.isEmpty() }.call()) as StartPlugin
}
internal actual fun alternativeSerialize(
encoder: Encoder,
value: StartPlugin
): Boolean {
encoder.encodeString(value::class.java.canonicalName ?: return false)
return true
}

View File

@ -1,23 +0,0 @@
package dev.inmo.micro_utils.startup.plugin
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
actual object StartPluginSerializer : KSerializer<StartPlugin> {
override val descriptor: SerialDescriptor
get() = String.serializer().descriptor
override fun deserialize(decoder: Decoder): StartPlugin {
val kclass = Class.forName(decoder.decodeString()).kotlin
return (kclass.objectInstance ?: kclass.constructors.first { it.parameters.isEmpty() }.call()) as StartPlugin
}
override fun serialize(encoder: Encoder, value: StartPlugin) {
encoder.encodeString(
value::class.java.canonicalName
)
}
}

View File

@ -1,36 +0,0 @@
package dev.inmo.micro_utils.startup.plugin
import com.benasher44.uuid.uuid4
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlin.reflect.KClass
actual object StartPluginSerializer : KSerializer<StartPlugin> {
private val registeredPlugins = mutableMapOf<String, StartPlugin>()
private val registeredPluginsByPlugin = mutableMapOf<StartPlugin, String>()
override val descriptor: SerialDescriptor = String.serializer().descriptor
override fun deserialize(decoder: Decoder): StartPlugin {
val name = decoder.decodeString()
return registeredPlugins[name] ?: error("Unable to find startup plugin for $name")
}
override fun serialize(encoder: Encoder, value: StartPlugin) {
val name = registeredPluginsByPlugin[value] ?: uuid4().toString().also {
registeredPlugins[it] = value
registeredPluginsByPlugin[value] = it
}
encoder.encodeString(name)
}
/**
* Register plugin inside of this [KSerializer]. Since plugin has been registered, you may use its [name] in any
* serialized [dev.inmo.micro_utils.startup.launcher.Config] to retrieve [plugin] you passed here
*/
fun registerPlugin(name: String, plugin: StartPlugin) {
registeredPlugins[name] = plugin
}
}

View File

@ -1,36 +0,0 @@
package dev.inmo.micro_utils.startup.plugin
import com.benasher44.uuid.uuid4
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlin.reflect.KClass
actual object StartPluginSerializer : KSerializer<StartPlugin> {
private val registeredPlugins = mutableMapOf<String, StartPlugin>()
private val registeredPluginsByPlugin = mutableMapOf<StartPlugin, String>()
override val descriptor: SerialDescriptor = String.serializer().descriptor
override fun deserialize(decoder: Decoder): StartPlugin {
val name = decoder.decodeString()
return registeredPlugins[name] ?: error("Unable to find startup plugin for $name")
}
override fun serialize(encoder: Encoder, value: StartPlugin) {
val name = registeredPluginsByPlugin[value] ?: uuid4().toString().also {
registeredPlugins[it] = value
registeredPluginsByPlugin[value] = it
}
encoder.encodeString(name)
}
/**
* Register plugin inside of this [KSerializer]. Since plugin has been registered, you may use its [name] in any
* serialized [dev.inmo.micro_utils.startup.launcher.Config] to retrieve [plugin] you passed here
*/
fun registerPlugin(name: String, plugin: StartPlugin) {
registeredPlugins[name] = plugin
}
}

View File

@ -0,0 +1,13 @@
package dev.inmo.micro_utils.startup.plugin
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
internal actual fun alternativeDeserialize(decoder: Decoder): StartPlugin? {
return null
}
internal actual fun alternativeSerialize(
encoder: Encoder,
value: StartPlugin
): Boolean = false