Compare commits

...

68 Commits

Author SHA1 Message Date
ef530624b9 remove redundant pluginManagement 2022-04-28 20:30:08 +06:00
9b9e7dd88f fixes in websockets 2022-04-28 19:43:52 +06:00
a13cc9e961 add error propagation for websockets when websockets has not been installed 2022-04-28 18:40:18 +06:00
0d2b923378 fix signature of createStandardWebsocketFlow 2022-04-28 18:16:14 +06:00
fba84c8ac8 optimize imports in ktor server 2022-04-27 18:30:28 +06:00
db10fe1b2c several fixes 2022-04-27 18:24:51 +06:00
175dd980f8 several warnings fixes 2022-04-27 15:15:03 +06:00
8364020671 migration onto new ktor and other dependencies 2022-04-27 14:39:21 +06:00
eba44cd394 update dependencies 2022-04-26 13:36:55 +06:00
b3bac8015a start 0.10.0 2022-04-26 13:35:00 +06:00
0b48afd251 Merge pull request #147 from InsanusMokrassar/0.9.24
0.9.24
2022-04-21 17:20:45 +06:00
19857930a4 small refactor in actual MPPFileInput 2022-04-21 16:45:44 +06:00
d0dbe3ed2f MPPFile#input 2022-04-21 16:12:58 +06:00
8b7e78b63a start 0.9.24 2022-04-21 16:12:21 +06:00
92a4ecb523 Merge pull request #146 from InsanusMokrassar/0.9.23
0.9.23
2022-04-20 23:46:06 +06:00
6a5ad4d728 Update CHANGELOG.md 2022-04-20 23:41:59 +06:00
be4aa8daac fixes in inheritance of interfaces 2022-04-20 23:14:06 +06:00
b5eac37782 improvements in ExposedCRUDRepo 2022-04-20 18:41:42 +06:00
b1ad3c5a39 start 0.9.23 2022-04-20 18:22:21 +06:00
ba16bad029 Merge pull request #145 from InsanusMokrassar/0.9.22
0.9.22
2022-04-19 13:55:13 +06:00
ca8ae4cd72 improve ktor server part 2022-04-19 11:16:04 +06:00
53d35d74b3 start 0.9.22 2022-04-19 11:14:50 +06:00
49c139e235 Merge pull request #144 from InsanusMokrassar/0.9.21
0.9.21
2022-04-17 08:56:59 +06:00
caf9c821f3 fixes in AbstractExposedWriteCRUDRepo 2022-04-15 00:49:32 +06:00
ca4c6db96f start 0.9.21 2022-04-15 00:44:28 +06:00
6b2298c752 Merge pull request #143 from InsanusMokrassar/0.9.20
0.9.20
2022-04-13 16:02:12 +06:00
a1bf43def9 upfixes 2022-04-13 16:01:13 +06:00
15e9254e00 fixes in OneToManyAndroidRepo and adding of CursorIterator 2022-04-13 15:54:48 +06:00
afe5a72c6f small fix of docs 2022-04-12 15:32:25 +06:00
750a8b9ecf start 0.9.20 2022-04-12 15:13:25 +06:00
27fc3f93e0 Merge pull request #142 from InsanusMokrassar/0.9.19
0.9.19
2022-04-08 01:16:13 +06:00
8166d4b99b Update CHANGELOG.md 2022-04-08 01:16:01 +06:00
b61d2ae2eb fixes in versions repo 2022-04-07 15:41:08 +06:00
4790fe0aea get back either serializer fun 2022-04-07 09:59:58 +06:00
bc37b11cee downgrade kotlin 2022-04-07 09:56:23 +06:00
223fed910f fix for column name of table version in exposed standard versions repo proxy 2022-04-07 00:43:55 +06:00
b85ab7b061 update coroutines 2022-04-06 11:47:06 +06:00
888dc299c9 update kotlin 2022-04-04 10:06:57 +06:00
e113dc28ed start 0.9.19 2022-04-04 10:05:56 +06:00
31e55d2307 Merge pull request #141 from InsanusMokrassar/0.9.18
0.9.18
2022-03-31 20:38:43 +06:00
e90645f248 Element#onActionOutside 2022-03-31 14:48:15 +06:00
4bb7ba2571 start 0.9.18 2022-03-31 14:31:04 +06:00
8d31c25bf8 Merge pull request #140 from InsanusMokrassar/0.9.17
0.9.17
2022-03-27 17:47:11 +06:00
c7ee1c28b2 fix of docs for DefaultStatesManager 2022-03-27 17:43:01 +06:00
99b09c8b28 add conflicts resolver for default states managers 2022-03-27 17:15:23 +06:00
a328c4425a Update CHANGELOG.md 2022-03-27 13:57:08 +06:00
c0f61ca896 Update libs.versions.toml 2022-03-26 19:11:44 +06:00
86e70c0961 js visibility of objects extensions 2022-03-26 11:01:40 +06:00
d87a3a039f start 0.9.17 2022-03-26 10:57:50 +06:00
6279a2c40a Merge pull request #139 from InsanusMokrassar/0.9.16
0.9.16
2022-03-18 00:10:25 +06:00
f377ebea88 add renderComposableAndLinkToRoot 2022-03-17 19:16:31 +06:00
7373fef964 add js onRemoved for nodes and different extensions for Composition 2022-03-17 14:56:14 +06:00
41ef86dbda start 0.9.16 2022-03-17 14:56:14 +06:00
7bc2b2336d Merge pull request #138 from InsanusMokrassar/0.9.15
0.9.15
2022-03-16 19:47:28 +06:00
0a615e6d78 cancel jobs of completed states in DefaultStatesMachine 2022-03-16 19:35:14 +06:00
8f928e16e1 FSM updates 2022-03-16 18:40:21 +06:00
72bc8da1e7 start 0.9.15 2022-03-16 17:59:29 +06:00
51e349a5db Merge pull request #137 from InsanusMokrassar/0.9.14
0.9.14 (upfixes)
2022-03-15 16:32:24 +06:00
3cbb19ba2c fixes and versions updates 2022-03-15 16:30:32 +06:00
ae3a5bf45d Merge pull request #136 from InsanusMokrassar/0.9.14
0.9.14
2022-03-15 16:22:35 +06:00
9c667f4b78 several fixes in actual temporal upload of js client 2022-03-15 15:37:41 +06:00
21195e1bcb temporal files uploading functionality 2022-03-15 15:33:45 +06:00
03117ac565 start 0.9.14 2022-03-15 15:31:58 +06:00
d13fbdf176 Merge pull request #135 from InsanusMokrassar/0.9.13
0.9.13
2022-03-13 17:14:23 +06:00
7cecc0e0b6 Update CHANGELOG.md 2022-03-13 14:08:01 +06:00
203e781f5d Update libs.versions.toml 2022-03-13 14:06:16 +06:00
3eb6cd77cd start 0.9.13 2022-03-13 14:05:24 +06:00
51855b2405 Merge pull request #134 from InsanusMokrassar/0.9.12
0.9.12
2022-03-12 01:46:23 +06:00
66 changed files with 867 additions and 191 deletions

View File

@@ -1,5 +1,99 @@
# Changelog
## 0.10.0
* `Versions`:
* `Kotlin`: `1.6.10` -> `1.6.21`
* `Compose`: `1.1.1` -> `1.2.0-alpha01-dev675`
* `Exposed`: `0.37.3` -> `0.38.2`
* `Ktor`: `1.6.8` -> `2.0.0`
* `Dokka`: `1.6.10` -> `1.6.21`
## 0.9.24
* `Ktor`:
* `Common`:
* New extension fun `MPPFile#input`
## 0.9.23
* `Repos`:
* `Exposed`:
* New property `ExposedRepo#selectAll` to retrieve all the rows in the table
## 0.9.22
* `Ktor`:
* `Server`:
* Now `createKtorServer` fun is fully customizable
## 0.9.21
* `Repos`:
* `Exposed`:
* fixes in `AbstractExposedWriteCRUDRepo`
## 0.9.20
* `Repos`:
* `Common`:
* Fixes in `OneToManyAndroidRepo`
* New `CursorIterator`
## 0.9.19
* `Versions`:
* `Coroutines`: `1.6.0` -> `1.6.1`
* `Repos`:
* `Exposed`:
* Fixes in `ExposedStandardVersionsRepoProxy`
## 0.9.18
* `Common`
* New extensions for `Element`: `Element#onActionOutside` and `Element#onClickOutside`
## 0.9.17
* `Common`:
* New extensions `Element#onVisibilityChanged`, `Element#onVisible` and `Element#onInvisible`
* `Coroutines`:
* New extension `Element.visibilityFlow()`
* `FSM`:
* Now it is possible to resolve conflicts on `startChain`
## 0.9.16
* `Versions`:
* `Klock`: `2.6.3` -> `2.7.0`
* `Common`:
* New extension `Node#onRemoved`
* `Compose`:
* New extension `Composition#linkWithRoot` for removing of composition with root element
* `Coroutines`:
* `Compose`:
* New function `renderComposableAndLinkToContextAndRoot` with linking of composition to root element
## 0.9.15
* `FSM`:
* Rename `DefaultUpdatableStatesMachine#compare` to `DefaultUpdatableStatesMachine#shouldReplaceJob`
* `DefaultStatesManager` now is extendable
* `DefaultStatesMachine` will stop all jobs of states which was removed from `statesManager`
## 0.9.14
* `Versions`:
* `Klock`: `2.6.2` -> `2.6.3`
* `Ktor`: `1.6.7` -> `1.6.8`
* `Ktor`:
* Add temporal files uploading functionality (for clients to upload and for server to receive)
## 0.9.13
* `Versions`:
* `Compose`: `1.1.0` -> `1.1.1`
## 0.9.12
* `Common`:

View File

@@ -21,6 +21,7 @@ allprojects {
mavenLocal()
mavenCentral()
google()
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
}
// temporal crutch until legacy tests will be stabled or legacy target will be removed

View File

@@ -0,0 +1,9 @@
package dev.inmo.micro_utils.common.compose
import androidx.compose.runtime.Composition
import dev.inmo.micro_utils.common.onRemoved
import org.w3c.dom.Element
fun Composition.linkWithElement(element: Element) {
element.onRemoved { dispose() }
}

View File

@@ -0,0 +1,13 @@
package dev.inmo.micro_utils.common.compose
import androidx.compose.runtime.*
import org.jetbrains.compose.web.dom.DOMScope
import org.w3c.dom.Element
fun <TElement : Element> renderComposableAndLinkToRoot(
root: TElement,
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
content: @Composable DOMScope<TElement>.() -> Unit
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
linkWithElement(root)
}

View File

@@ -1,3 +1,5 @@
@file:Suppress("OPT_IN_IS_NOT_ENABLED")
package dev.inmo.micro_utils.common
@RequiresOptIn(

View File

@@ -43,6 +43,7 @@ private inline fun <T> performChanges(
if (oldOneEqualToNewObject || newOneEqualToOldObject) {
changedList.addAll(
potentialChanges.take(i).mapNotNull {
@Suppress("UNCHECKED_CAST")
if (it.first != null && it.second != null) it as Pair<IndexedValue<T>, IndexedValue<T>> else null
}
)
@@ -121,7 +122,10 @@ fun <T> Iterable<T>.calculateDiff(
when {
oldObject === newObject || (oldObject == newObject && !strictComparison) -> {
changedObjects.addAll(potentiallyChangedObjects.map { it as Pair<IndexedValue<T>, IndexedValue<T>> })
changedObjects.addAll(potentiallyChangedObjects.map {
@Suppress("UNCHECKED_CAST")
it as Pair<IndexedValue<T>, IndexedValue<T>>
})
potentiallyChangedObjects.clear()
}
else -> {

View File

@@ -27,20 +27,13 @@ sealed interface Either<T1, T2> {
@Deprecated("Use optionalT2 instead", ReplaceWith("optionalT2"))
val t2: T2?
get() = optionalT2.dataOrNull()
companion object {
fun <T1, T2> serializer(
t1Serializer: KSerializer<T1>,
t2Serializer: KSerializer<T2>,
): KSerializer<Either<T1, T2>> = EitherSerializer(t1Serializer, t2Serializer)
}
}
class EitherSerializer<T1, T2>(
t1Serializer: KSerializer<T1>,
t2Serializer: KSerializer<T2>,
) : KSerializer<Either<T1, T2>> {
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
@OptIn(InternalSerializationApi::class)
override val descriptor: SerialDescriptor = buildSerialDescriptor(
"TypedSerializer",
SerialKind.CONTEXTUAL
@@ -51,7 +44,6 @@ class EitherSerializer<T1, T2>(
private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer)
private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer)
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
override fun deserialize(decoder: Decoder): Either<T1, T2> {
return decoder.decodeStructure(descriptor) {
var type: String? = null
@@ -83,7 +75,6 @@ class EitherSerializer<T1, T2>(
}
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
override fun serialize(encoder: Encoder, value: Either<T1, T2>) {
encoder.encodeStructure(descriptor) {
when (value) {

View File

@@ -32,7 +32,7 @@ class DiffUtilsTests {
val withIndex = oldList.withIndex()
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
for ((i, v) in withIndex) {
for ((i, _) in withIndex) {
if (i + count > oldList.lastIndex) {
continue
}
@@ -55,7 +55,7 @@ class DiffUtilsTests {
val withIndex = oldList.withIndex()
for (step in oldList.indices) {
for ((i, v) in withIndex) {
for ((i, _) in withIndex) {
val mutable = oldList.toMutableList()
val changes = (
if (step == 0) i until oldList.size else (i until oldList.size step step)
@@ -104,7 +104,7 @@ class DiffUtilsTests {
val withIndex = oldList.withIndex()
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
for ((i, v) in withIndex) {
for ((i, _) in withIndex) {
if (i + count > oldList.lastIndex) {
continue
}
@@ -129,15 +129,20 @@ class DiffUtilsTests {
val withIndex = oldList.withIndex()
for (step in oldList.indices) {
for ((i, v) in withIndex) {
for ((i, _) in withIndex) {
val mutable = oldList.toMutableList()
val changes = (
if (step == 0) i until oldList.size else (i until oldList.size step step)
).map { index ->
val newList = if (step == 0) {
i until oldList.size
} else {
i until oldList.size step step
}
newList.forEach { index ->
IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also {
mutable[index] = it.value
}
}
val mutableOldList = oldList.toMutableList()
mutableOldList.applyDiff(mutable)
assertEquals(

View File

@@ -0,0 +1,61 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import org.w3c.dom.*
fun Node.onRemoved(block: () -> Unit): MutationObserver {
lateinit var observer: MutationObserver
observer = MutationObserver { _, _ ->
fun checkIfRemoved(node: Node): Boolean {
return node.parentNode != document && (node.parentNode ?.let { checkIfRemoved(it) } ?: true)
}
if (checkIfRemoved(this)) {
observer.disconnect()
block()
}
}
observer.observe(document, MutationObserverInit(childList = true, subtree = true))
return observer
}
fun Element.onVisibilityChanged(block: IntersectionObserverEntry.(Float, IntersectionObserver) -> Unit): IntersectionObserver {
var previousIntersectionRatio = -1f
val observer = IntersectionObserver { entries, observer ->
entries.forEach {
if (previousIntersectionRatio != it.intersectionRatio) {
previousIntersectionRatio = it.intersectionRatio.toFloat()
it.block(previousIntersectionRatio, observer)
}
}
}
observer.observe(this)
return observer
}
fun Element.onVisible(block: Element.(IntersectionObserver) -> Unit) {
var previous = -1f
onVisibilityChanged { intersectionRatio, observer ->
if (previous != intersectionRatio) {
if (intersectionRatio > 0 && previous == 0f) {
block(observer)
}
previous = intersectionRatio
}
}
}
fun Element.onInvisible(block: Element.(IntersectionObserver) -> Unit): IntersectionObserver {
var previous = -1f
return onVisibilityChanged { intersectionRatio, observer ->
if (previous != intersectionRatio) {
if (intersectionRatio == 0f && previous != 0f) {
block(observer)
}
previous = intersectionRatio
}
}
}

View File

@@ -0,0 +1,38 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import org.w3c.dom.*
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventListener
fun Element.onActionOutside(type: String, options: dynamic = null, callback: (Event) -> Unit): EventListener {
lateinit var observer: MutationObserver
val listener = EventListener {
val elementsToCheck = mutableListOf<Element>(this@onActionOutside)
while (it.target != this@onActionOutside && elementsToCheck.isNotEmpty()) {
val childrenGettingElement = elementsToCheck.removeFirst()
for (i in 0 until childrenGettingElement.childElementCount) {
elementsToCheck.add(childrenGettingElement.children[i] ?: continue)
}
}
if (elementsToCheck.isEmpty()) {
callback(it)
}
}
if (options == null) {
document.addEventListener(type, listener)
} else {
document.addEventListener(type, listener, options)
}
observer = onRemoved {
if (options == null) {
document.removeEventListener(type, listener)
} else {
document.removeEventListener(type, listener, options)
}
observer.disconnect()
}
return listener
}
fun Element.onClickOutside(options: dynamic = null, callback: (Event) -> Unit) = onActionOutside("click", options, callback)

View File

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

View File

@@ -16,6 +16,7 @@ fun <T> Flow<T>.toMutableState(
return state
}
@Suppress("NOTHING_TO_INLINE")
inline fun <T> StateFlow<T>.toMutableState(
scope: CoroutineScope
): MutableState<T> = toMutableState(value, scope)

View File

@@ -1,6 +1,7 @@
package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.*
import dev.inmo.micro_utils.common.compose.linkWithElement
import kotlinx.coroutines.*
import org.jetbrains.compose.web.dom.DOMScope
import org.w3c.dom.Element
@@ -14,3 +15,12 @@ suspend fun <TElement : Element> renderComposableAndLinkToContext(
currentCoroutineContext()
)
}
suspend fun <TElement : Element> renderComposableAndLinkToContextAndRoot(
root: TElement,
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
content: @Composable DOMScope<TElement>.() -> Unit
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
linkWithContext(currentCoroutineContext())
linkWithElement(root)
}

View File

@@ -0,0 +1,28 @@
package dev.inmo.micro_utils.coroutines
import dev.inmo.micro_utils.common.onRemoved
import dev.inmo.micro_utils.common.onVisibilityChanged
import kotlinx.coroutines.flow.*
import org.w3c.dom.Element
fun Element.visibilityFlow(): Flow<Boolean> = channelFlow {
var previousData: Boolean? = null
val observer = onVisibilityChanged { intersectionRatio, _ ->
val currentData = intersectionRatio > 0
if (currentData != previousData) {
trySend(currentData)
}
previousData = currentData
}
val removeObserver = onRemoved {
observer.disconnect()
close()
}
invokeOnClose {
observer.disconnect()
removeObserver.disconnect()
}
}

View File

@@ -105,6 +105,16 @@ open class DefaultStatesMachine <T: State>(
statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) {
launch { performStateUpdate(Optional.presented(it.first), it.second, scope.LinkedSupervisorScope()) }
}
statesManager.onEndChain.subscribeSafelyWithoutExceptions(this) { removedState ->
launch {
statesJobsMutex.withLock {
val stateInMap = statesJobs.keys.firstOrNull { stateInMap -> stateInMap == removedState }
if (stateInMap === removedState) {
statesJobs[stateInMap] ?.cancel()
}
}
}
}
statesManager.getActiveStates().forEach {
launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }

View File

@@ -26,6 +26,12 @@ open class DefaultUpdatableStatesMachine<T : State>(
), UpdatableStatesMachine<T> {
protected val jobsStates = mutableMapOf<Job, T>()
/**
* Realization of this update will use the [Job] of [previousState] in [statesJobs] and [jobsStates] if
* [previousState] is [Optional.presented] and [shouldReplaceJob] has returned true for [previousState] and [actualState]. In
* other words, [Job] of [previousState] WILL NOT be replaced with the new one if they are "equal". Equality of
* states is solved in [shouldReplaceJob] and can be rewritten in subclasses
*/
override suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
statesJobsMutex.withLock {
if (compare(previousState, actualState)) {
@@ -52,7 +58,13 @@ open class DefaultUpdatableStatesMachine<T : State>(
}
}
protected open suspend fun compare(previous: Optional<T>, new: T): Boolean = previous.dataOrNull() != new
/**
* Compare if [previous] potentially lead to the same behaviour with [new]
*/
protected open suspend fun shouldReplaceJob(previous: Optional<T>, new: T): Boolean = previous.dataOrNull() != new
@Deprecated("Overwrite shouldReplaceJob instead")
protected open suspend fun compare(previous: Optional<T>, new: T): Boolean = shouldReplaceJob(previous, new)
override suspend fun updateChain(currentState: T, newState: T) {
statesManager.update(currentState, newState)

View File

@@ -37,36 +37,49 @@ interface DefaultStatesManagerRepo<T : State> {
/**
* @param repo This repo will be used as repository for storing states. All operations with this repo will happen BEFORE
* any event will be sent to [onChainStateUpdated], [onStartChain] or [onEndChain]. By default will be used
* any event will be sent to [onChainStateUpdated], [onStartChain] or [onEndChain]. By default, will be used
* [InMemoryDefaultStatesManagerRepo] or you may create custom [DefaultStatesManagerRepo] and pass as [repo] parameter
* @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
* @param onStartContextsConflictResolver Receive current [State] and the state passed with [startChain]. In case when
* this callback will return true, currently placed on the [State.context] [State] will be replaced by new state
* with [endChain] with current state
* @param onUpdateContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
* key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
* new state by using [endChain] with that state
*/
class DefaultStatesManager<T : State>(
private val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
private val onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
open class DefaultStatesManager<T : State>(
protected val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
protected val onStartContextsConflictResolver: suspend (current: T, new: T) -> Boolean = { _, _ -> true },
protected val onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
) : StatesManager<T> {
private val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
protected val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow()
private val _onStartChain = MutableSharedFlow<T>(0)
protected val _onStartChain = MutableSharedFlow<T>(0)
override val onStartChain: Flow<T> = _onStartChain.asSharedFlow()
private val _onEndChain = MutableSharedFlow<T>(0)
protected val _onEndChain = MutableSharedFlow<T>(0)
override val onEndChain: Flow<T> = _onEndChain.asSharedFlow()
private val mapMutex = Mutex()
protected val mapMutex = Mutex()
constructor(
repo: DefaultStatesManagerRepo<T>,
onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean
) : this (
repo,
onUpdateContextsConflictResolver = onContextsConflictResolver
)
override suspend fun update(old: T, new: T) = mapMutex.withLock {
val stateByOldContext: T? = repo.getContextState(old.context)
when {
stateByOldContext != old -> return@withLock
stateByOldContext == null || old.context == new.context -> {
repo.removeState(old)
repo.set(new)
_onChainStateUpdated.emit(old to new)
}
else -> {
val stateOnNewOneContext = repo.getContextState(new.context)
if (stateOnNewOneContext == null || onContextsConflictResolver(old, new, stateOnNewOneContext)) {
if (stateOnNewOneContext == null || onUpdateContextsConflictResolver(old, new, stateOnNewOneContext)) {
stateOnNewOneContext ?.let { endChainWithoutLock(it) }
repo.removeState(old)
repo.set(new)
@@ -77,13 +90,17 @@ class DefaultStatesManager<T : State>(
}
override suspend fun startChain(state: T) = mapMutex.withLock {
if (!repo.contains(state.context)) {
val stateOnContext = repo.getContextState(state.context)
if (stateOnContext == null || onStartContextsConflictResolver(stateOnContext, state)) {
stateOnContext ?.let {
endChainWithoutLock(it)
}
repo.set(state)
_onStartChain.emit(state)
}
}
private suspend fun endChainWithoutLock(state: T) {
protected open suspend fun endChainWithoutLock(state: T) {
if (repo.getContextState(state.context) == state) {
repo.removeState(state)
_onEndChain.emit(state)

View File

@@ -12,5 +12,6 @@ import kotlinx.coroutines.flow.*
*/
@Deprecated("Use DefaultStatesManager instead", ReplaceWith("DefaultStatesManager"))
fun <T: State> InMemoryStatesManager(
onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
) = DefaultStatesManager(onContextsConflictResolver = onContextsConflictResolver)
onStartContextsConflictResolver: suspend (old: T, new: T) -> Boolean = { _, _ -> true },
onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
) = DefaultStatesManager(onStartContextsConflictResolver = onStartContextsConflictResolver, onUpdateContextsConflictResolver = onUpdateContextsConflictResolver)

View File

@@ -14,5 +14,5 @@ crypto_js_version=4.1.1
# Project data
group=dev.inmo
version=0.9.12
android_code_version=102
version=0.10.0
android_code_version=115

View File

@@ -1,19 +1,19 @@
[versions]
kt = "1.6.10"
kt = "1.6.21"
kt-serialization = "1.3.2"
kt-coroutines = "1.6.0"
kt-coroutines = "1.6.1"
jb-compose = "1.1.0"
jb-exposed = "0.37.3"
jb-dokka = "1.6.10"
jb-compose = "1.2.0-alpha01-dev675"
jb-exposed = "0.38.2"
jb-dokka = "1.6.21"
klock = "2.6.2"
klock = "2.7.0"
uuid = "0.4.0"
ktor = "1.6.7"
ktor = "2.0.0"
gh-release = "2.2.12"
gh-release = "2.3.7"
android-gradle = "7.0.4"
dexcount = "3.0.1"
@@ -39,12 +39,15 @@ kt-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", vers
kt-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kt-coroutines" }
ktor-io = { module = "io.ktor:ktor-io", version.ref = "ktor" }
ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-java = { module = "io.ktor:ktor-client-java", version.ref = "ktor" }
ktor-server = { module = "io.ktor:ktor-server", version.ref = "ktor" }
ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" }
ktor-server-host-common = { module = "io.ktor:ktor-server-host-common", version.ref = "ktor" }
ktor-websockets = { module = "io.ktor:ktor-websockets", version.ref = "ktor" }
ktor-server-websockets = { module = "io.ktor:ktor-server-websockets", version.ref = "ktor" }
ktor-server-statusPages = { module = "io.ktor:ktor-server-status-pages", version.ref = "ktor" }
klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "klock" }

View File

@@ -1,14 +1,18 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.coroutines.safely
import dev.inmo.micro_utils.ktor.common.*
import io.ktor.client.HttpClient
import io.ktor.client.features.websocket.ws
import io.ktor.client.plugins.pluginOrNull
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.ws
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.readBytes
import io.ktor.websocket.Frame
import io.ktor.websocket.readBytes
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.isActive
import kotlinx.serialization.DeserializationStrategy
/**
@@ -17,43 +21,41 @@ import kotlinx.serialization.DeserializationStrategy
*/
inline fun <T> HttpClient.createStandardWebsocketFlow(
url: String,
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
crossinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {},
crossinline conversation: suspend (StandardKtorSerialInputData) -> T
): Flow<T> {
pluginOrNull(WebSockets) ?: error("Plugin $WebSockets must be installed for using createStandardWebsocketFlow")
val correctedUrl = url.asCorrectWebSocketUrl
return channelFlow {
val producerScope = this@channelFlow
do {
val reconnect = try {
safely {
ws(correctedUrl, requestBuilder) {
for (received in incoming) {
when (received) {
is Frame.Binary -> producerScope.send(conversation(received.readBytes()))
else -> {
producerScope.close()
return@ws
}
val reconnect = runCatchingSafely {
ws(correctedUrl, requestBuilder) {
for (received in incoming) {
when (received) {
is Frame.Binary -> send(conversation(received.data))
else -> {
close()
return@ws
}
}
}
}
checkReconnection(null)
} catch (e: Throwable) {
}.getOrElse { e ->
checkReconnection(e).also {
if (!it) {
producerScope.close(e)
close(e)
}
}
}
} while (reconnect)
if (!producerScope.isClosedForSend) {
safely(
{ it.printStackTrace() }
) {
producerScope.close()
} while (reconnect && isActive)
if (isActive) {
safely {
close()
}
}
}
@@ -65,8 +67,8 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
*/
inline fun <T> HttpClient.createStandardWebsocketFlow(
url: String,
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
deserializer: DeserializationStrategy<T>,
crossinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {},
) = createStandardWebsocketFlow(

View File

@@ -4,8 +4,10 @@ import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.common.filename
import dev.inmo.micro_utils.ktor.common.*
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.client.statement.readBytes
import io.ktor.http.*
import io.ktor.utils.io.core.ByteReadPacket
import kotlinx.serialization.*
@@ -85,16 +87,16 @@ class UnifiedRequester(
fun <T> createStandardWebsocketFlow(
url: String,
checkReconnection: (Throwable?) -> Boolean,
checkReconnection: suspend (Throwable?) -> Boolean,
deserializer: DeserializationStrategy<T>,
requestBuilder: HttpRequestBuilder.() -> Unit = {},
) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat, requestBuilder)
) = client.createStandardWebsocketFlow(url, deserializer, checkReconnection, serialFormat, requestBuilder)
fun <T> createStandardWebsocketFlow(
url: String,
deserializer: DeserializationStrategy<T>,
requestBuilder: HttpRequestBuilder.() -> Unit = {},
) = createStandardWebsocketFlow(url, { true }, deserializer, requestBuilder)
) = createStandardWebsocketFlow(url, { true }, deserializer, requestBuilder)
}
val defaultRequester = UnifiedRequester()
@@ -103,10 +105,8 @@ suspend fun <ResultType> HttpClient.uniget(
url: String,
resultDeserializer: DeserializationStrategy<ResultType>,
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
) = get<StandardKtorSerialInputData>(
url
).let {
serialFormat.decodeDefault(resultDeserializer, it)
) = get(url).let {
serialFormat.decodeDefault(resultDeserializer, it.body<StandardKtorSerialInputData>())
}
@@ -123,10 +123,12 @@ suspend fun <BodyType, ResultType> HttpClient.unipost(
bodyInfo: BodyPair<BodyType>,
resultDeserializer: DeserializationStrategy<ResultType>,
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
) = post<StandardKtorSerialInputData>(url) {
body = serialFormat.encodeDefault(bodyInfo.first, bodyInfo.second)
) = post(url) {
setBody(
serialFormat.encodeDefault(bodyInfo.first, bodyInfo.second)
)
}.let {
serialFormat.decodeDefault(resultDeserializer, it)
serialFormat.decodeDefault(resultDeserializer, it.body<StandardKtorSerialInputData>())
}
suspend fun <ResultType> HttpClient.unimultipart(
@@ -139,7 +141,7 @@ suspend fun <ResultType> HttpClient.unimultipart(
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
): ResultType = submitFormWithBinaryData<StandardKtorSerialInputData>(
): ResultType = submitFormWithBinaryData(
url,
formData = formData {
append(
@@ -155,7 +157,7 @@ suspend fun <ResultType> HttpClient.unimultipart(
}
) {
requestBuilder()
}.let { serialFormat.decodeDefault(resultDeserializer, it) }
}.let { serialFormat.decodeDefault(resultDeserializer, it.body<StandardKtorSerialInputData>()) }
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
url: String,

View File

@@ -0,0 +1,19 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.ktor.common.*
import io.ktor.client.HttpClient
expect suspend fun HttpClient.tempUpload(
fullTempUploadDraftPath: String,
file: MPPFile,
onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> }
): TemporalFileId
suspend fun UnifiedRequester.tempUpload(
fullTempUploadDraftPath: String,
file: MPPFile,
onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> }
): TemporalFileId = client.tempUpload(
fullTempUploadDraftPath, file, onUpload
)

View File

@@ -0,0 +1,58 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.ktor.common.TemporalFileId
import io.ktor.client.HttpClient
import kotlinx.coroutines.*
import org.w3c.xhr.*
suspend fun tempUpload(
fullTempUploadDraftPath: String,
file: MPPFile,
onUpload: (Long, Long) -> Unit
): TemporalFileId {
val formData = FormData()
val answer = CompletableDeferred<TemporalFileId>()
formData.append(
"data",
file
)
val request = XMLHttpRequest()
request.responseType = XMLHttpRequestResponseType.TEXT
request.upload.onprogress = {
onUpload(it.loaded.toLong(), it.total.toLong())
}
request.onload = {
if (request.status == 200.toShort()) {
answer.complete(TemporalFileId(request.responseText))
} else {
answer.completeExceptionally(Exception("Something went wrong: $it"))
}
}
request.onerror = {
answer.completeExceptionally(Exception("Something went wrong: $it"))
}
request.open("POST", fullTempUploadDraftPath, true)
request.send(formData)
val handle = currentCoroutineContext().job.invokeOnCompletion {
runCatching {
request.abort()
}
}
return runCatching {
answer.await()
}.also {
handle.dispose()
}.getOrThrow()
}
actual suspend fun HttpClient.tempUpload(
fullTempUploadDraftPath: String,
file: MPPFile,
onUpload: (uploaded: Long, count: Long) -> Unit
): TemporalFileId = dev.inmo.micro_utils.ktor.client.tempUpload(fullTempUploadDraftPath, file, onUpload)

View File

@@ -0,0 +1,40 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.common.filename
import dev.inmo.micro_utils.ktor.common.TemporalFileId
import io.ktor.client.HttpClient
import io.ktor.client.plugins.onUpload
import io.ktor.client.request.forms.formData
import io.ktor.client.request.forms.submitFormWithBinaryData
import io.ktor.client.statement.bodyAsText
import io.ktor.http.Headers
import io.ktor.http.HttpHeaders
import java.net.URLConnection
internal val MPPFile.mimeType: String
get() = URLConnection.getFileNameMap().getContentTypeFor(filename.name) ?: "*/*"
actual suspend fun HttpClient.tempUpload(
fullTempUploadDraftPath: String,
file: MPPFile,
onUpload: (Long, Long) -> Unit
): TemporalFileId {
val inputProvider = file.inputProvider()
val fileId = submitFormWithBinaryData(
fullTempUploadDraftPath,
formData = formData {
append(
"data",
inputProvider,
Headers.build {
append(HttpHeaders.ContentType, file.mimeType)
append(HttpHeaders.ContentDisposition, "filename=\"${file.filename.string}\"")
}
)
}
) {
onUpload(onUpload)
}.bodyAsText()
return TemporalFileId(fileId)
}

View File

@@ -13,6 +13,8 @@ kotlin {
api internalProject("micro_utils.common")
api libs.kt.serialization.cbor
api libs.klock
api libs.uuid
api libs.ktor.io
}
}
}

View File

@@ -0,0 +1,6 @@
package dev.inmo.micro_utils.ktor.common
import dev.inmo.micro_utils.common.MPPFile
import io.ktor.utils.io.core.Input
expect fun MPPFile.input(): Input

View File

@@ -1,3 +1,5 @@
@file:Suppress("NOTHING_TO_INLINE")
package dev.inmo.micro_utils.ktor.common
import kotlinx.serialization.*

View File

@@ -0,0 +1,10 @@
package dev.inmo.micro_utils.ktor.common
import kotlin.jvm.JvmInline
import kotlinx.serialization.Serializable
const val DefaultTemporalFilesSubPath = "temp_upload"
@Serializable
@JvmInline
value class TemporalFileId(val string: String)

View File

@@ -0,0 +1,7 @@
package dev.inmo.micro_utils.ktor.common
import dev.inmo.micro_utils.common.*
import io.ktor.utils.io.core.ByteReadPacket
import io.ktor.utils.io.core.Input
actual fun MPPFile.input(): Input = ByteReadPacket(readBytes())

View File

@@ -0,0 +1,7 @@
package dev.inmo.micro_utils.ktor.common
import dev.inmo.micro_utils.common.MPPFile
import io.ktor.utils.io.core.Input
import io.ktor.utils.io.streams.asInput
actual fun MPPFile.input(): Input = inputStream().asInput()

View File

@@ -19,7 +19,8 @@ kotlin {
api libs.ktor.server
api libs.ktor.server.cio
api libs.ktor.server.host.common
api libs.ktor.websockets
api libs.ktor.server.websockets
api libs.ktor.server.statusPages
}
}
}

View File

@@ -2,26 +2,26 @@ package dev.inmo.micro_utils.ktor.server
import dev.inmo.micro_utils.coroutines.safely
import dev.inmo.micro_utils.ktor.common.*
import io.ktor.application.featureOrNull
import io.ktor.application.install
import io.ktor.http.URLProtocol
import io.ktor.http.cio.websocket.*
import io.ktor.routing.Route
import io.ktor.routing.application
import io.ktor.websocket.*
import io.ktor.server.application.install
import io.ktor.server.application.pluginOrNull
import io.ktor.server.routing.Route
import io.ktor.server.routing.application
import io.ktor.server.websocket.*
import io.ktor.websocket.send
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.SerializationStrategy
fun <T> Route.includeWebsocketHandling(
suburl: String,
flow: Flow<T>,
protocol: URLProtocol = URLProtocol.WS,
protocol: URLProtocol? = null,
converter: suspend WebSocketServerSession.(T) -> StandardKtorSerialInputData?
) {
application.apply {
featureOrNull(io.ktor.websocket.WebSockets) ?: install(io.ktor.websocket.WebSockets)
pluginOrNull(WebSockets) ?: install(WebSockets)
}
webSocket(suburl, protocol.name) {
webSocket(suburl, protocol ?.name) {
safely {
flow.collect {
converter(it) ?.let { data ->
@@ -37,7 +37,7 @@ fun <T> Route.includeWebsocketHandling(
flow: Flow<T>,
serializer: SerializationStrategy<T>,
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
protocol: URLProtocol = URLProtocol.WS,
protocol: URLProtocol? = null,
filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null
) = includeWebsocketHandling(
suburl,

View File

@@ -3,25 +3,21 @@ package dev.inmo.micro_utils.ktor.server
import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.coroutines.safely
import dev.inmo.micro_utils.ktor.common.*
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.http.*
import io.ktor.http.content.PartData
import io.ktor.http.content.forEachPart
import io.ktor.request.receive
import io.ktor.request.receiveMultipart
import io.ktor.response.respond
import io.ktor.response.respondBytes
import io.ktor.routing.Route
import io.ktor.util.asStream
import io.ktor.util.cio.writeChannel
import io.ktor.http.content.*
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.call
import io.ktor.server.request.receive
import io.ktor.server.request.receiveMultipart
import io.ktor.server.response.respond
import io.ktor.server.response.respondBytes
import io.ktor.server.routing.Route
import io.ktor.server.websocket.WebSocketServerSession
import io.ktor.util.pipeline.PipelineContext
import io.ktor.utils.io.core.*
import io.ktor.websocket.WebSocketServerSession
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.*
import java.io.File
import java.io.File.createTempFile
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerializationStrategy
class UnifiedRouter(
val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
@@ -31,7 +27,7 @@ class UnifiedRouter(
suburl: String,
flow: Flow<T>,
serializer: SerializationStrategy<T>,
protocol: URLProtocol = URLProtocol.WS,
protocol: URLProtocol? = null,
filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null
) = includeWebsocketHandling(suburl, flow, serializer, serialFormat, protocol, filter)
@@ -92,6 +88,11 @@ class UnifiedRouter(
call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
}
}
companion object {
val default
get() = defaultUnifiedRouter
}
}
val defaultUnifiedRouter = UnifiedRouter()
@@ -192,7 +193,9 @@ suspend fun <T> ApplicationCall.uniloadMultipartFile(
".${name.extension}"
).apply {
outputStream().use { fileStream ->
it.provider().asStream().copyTo(fileStream)
it.streamProvider().use {
it.copyTo(fileStream)
}
}
}
}
@@ -234,7 +237,9 @@ suspend fun ApplicationCall.uniloadMultipartFile(
".${name.extension}"
).apply {
outputStream().use { fileStream ->
it.provider().asStream().copyTo(fileStream)
it.streamProvider().use {
it.copyTo(fileStream)
}
}
}
} else {

View File

@@ -1,8 +1,9 @@
package dev.inmo.micro_utils.ktor.server
import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator
import io.ktor.application.Application
import io.ktor.server.application.Application
import io.ktor.server.cio.CIO
import io.ktor.server.cio.CIOApplicationEngine
import io.ktor.server.engine.*
import kotlin.random.Random
@@ -10,17 +11,21 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati
engine: ApplicationEngineFactory<TEngine, TConfiguration>,
host: String = "localhost",
port: Int = Random.nextInt(1024, 65535),
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
additionalConfigurationConfigurator: TConfiguration.() -> Unit = {},
block: Application.() -> Unit
): TEngine {
val env = applicationEngineEnvironment {
): TEngine = embeddedServer(
engine,
applicationEngineEnvironment {
module(block)
connector {
this@connector.host = host
this@connector.port = port
this.host = host
this.port = port
}
}
return embeddedServer(engine, env)
}
additionalEngineEnvironmentConfigurator()
},
additionalConfigurationConfigurator
)
/**
* Create server with [CIO] server engine without starting of it
@@ -30,18 +35,31 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati
fun createKtorServer(
host: String = "localhost",
port: Int = Random.nextInt(1024, 65535),
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
additionalConfigurationConfigurator: CIOApplicationEngine.Configuration.() -> Unit = {},
block: Application.() -> Unit
): ApplicationEngine = createKtorServer(CIO, host, port, block)
): CIOApplicationEngine = createKtorServer(
CIO,
host,
port,
additionalEngineEnvironmentConfigurator,
additionalConfigurationConfigurator,
block
)
fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration> createKtorServer(
engine: ApplicationEngineFactory<TEngine, TConfiguration>,
host: String = "localhost",
port: Int = Random.nextInt(1024, 65535),
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
additionalConfigurationConfigurator: TConfiguration.() -> Unit = {},
configurators: List<KtorApplicationConfigurator>
): TEngine = createKtorServer(
engine,
host,
port
port,
additionalEngineEnvironmentConfigurator,
additionalConfigurationConfigurator
) {
configurators.forEach { it.apply { configure() } }
}
@@ -54,5 +72,7 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati
fun createKtorServer(
host: String = "localhost",
port: Int = Random.nextInt(1024, 65535),
configurators: List<KtorApplicationConfigurator>
): ApplicationEngine = createKtorServer(CIO, host, port, configurators)
configurators: List<KtorApplicationConfigurator>,
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
additionalConfigurationConfigurator: CIOApplicationEngine.Configuration.() -> Unit = {},
): ApplicationEngine = createKtorServer(CIO, host, port, additionalEngineEnvironmentConfigurator, additionalConfigurationConfigurator, configurators)

View File

@@ -0,0 +1,132 @@
package dev.inmo.micro_utils.ktor.server
import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.common.FileName
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
import dev.inmo.micro_utils.ktor.common.DefaultTemporalFilesSubPath
import dev.inmo.micro_utils.ktor.common.TemporalFileId
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.PartData
import io.ktor.http.content.streamProvider
import io.ktor.server.application.call
import io.ktor.server.request.receiveMultipart
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import io.ktor.server.routing.post
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.File
import java.nio.file.Files
import java.nio.file.attribute.FileTime
class TemporalFilesRoutingConfigurator(
private val subpath: String = DefaultTemporalFilesSubPath,
private val unifiedRouter: UnifiedRouter = UnifiedRouter.default,
private val temporalFilesUtilizer: TemporalFilesUtilizer = TemporalFilesUtilizer
) : ApplicationRoutingConfigurator.Element {
interface TemporalFilesUtilizer {
fun start(filesMap: MutableMap<TemporalFileId, MPPFile>, filesMutex: Mutex, onNewFileFlow: Flow<TemporalFileId>): Job
companion object : TemporalFilesUtilizer {
class ByTimerUtilizer(
private val removeMillis: Long,
private val scope: CoroutineScope
) : TemporalFilesUtilizer {
override fun start(
filesMap: MutableMap<TemporalFileId, MPPFile>,
filesMutex: Mutex,
onNewFileFlow: Flow<TemporalFileId>
): Job = scope.launchSafelyWithoutExceptions {
while (isActive) {
val filesWithCreationInfo = filesMap.mapNotNull { (fileId, file) ->
fileId to ((Files.getAttribute(file.toPath(), "creationTime") as? FileTime) ?.toMillis() ?: return@mapNotNull null)
}
if (filesWithCreationInfo.isEmpty()) {
delay(removeMillis)
continue
}
var min = filesWithCreationInfo.first()
for (fileWithCreationInfo in filesWithCreationInfo) {
if (fileWithCreationInfo.second < min.second) {
min = fileWithCreationInfo
}
}
delay(System.currentTimeMillis() - (min.second + removeMillis))
filesMutex.withLock {
filesMap.remove(min.first)
} ?.delete()
}
}
}
override fun start(
filesMap: MutableMap<TemporalFileId, MPPFile>,
filesMutex: Mutex,
onNewFileFlow: Flow<TemporalFileId>
): Job = Job()
}
}
private val temporalFilesMap = mutableMapOf<TemporalFileId, MPPFile>()
private val temporalFilesMutex = Mutex()
private val filesFlow = MutableSharedFlow<TemporalFileId>()
val utilizerJob = temporalFilesUtilizer.start(temporalFilesMap, temporalFilesMutex, filesFlow.asSharedFlow())
override fun Route.invoke() {
post(subpath) {
unifiedRouter.apply {
val multipart = call.receiveMultipart()
var fileInfo: Pair<TemporalFileId, MPPFile>? = null
var part = multipart.readPart()
while (part != null) {
if (part is PartData.FileItem) {
break
}
part = multipart.readPart()
}
part ?.let {
if (it is PartData.FileItem) {
val fileId = TemporalFileId(uuid4().toString())
val fileName = it.originalFileName ?.let { FileName(it) } ?: return@let
fileInfo = fileId to File.createTempFile(fileId.string, ".${fileName.extension}").apply {
outputStream().use { outputStream ->
it.streamProvider().use {
it.copyTo(outputStream)
}
}
deleteOnExit()
}
}
}
fileInfo ?.also { (fileId, file) ->
temporalFilesMutex.withLock {
temporalFilesMap[fileId] = file
}
call.respond(fileId.string)
launchSafelyWithoutExceptions { filesFlow.emit(fileId) }
} ?: call.respond(HttpStatusCode.BadRequest)
}
}
}
suspend fun removeTemporalFile(temporalFileId: TemporalFileId) {
temporalFilesMutex.withLock {
temporalFilesMap.remove(temporalFileId)
}
}
fun getTemporalFile(temporalFileId: TemporalFileId) = temporalFilesMap[temporalFileId]
suspend fun getAndRemoveTemporalFile(temporalFileId: TemporalFileId) = temporalFilesMutex.withLock {
temporalFilesMap.remove(temporalFileId)
}
}

View File

@@ -1,14 +1,15 @@
package dev.inmo.micro_utils.ktor.server.configurators
import io.ktor.application.Application
import io.ktor.application.install
import io.ktor.features.CachingHeaders
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.plugins.cachingheaders.CachingHeaders
import io.ktor.server.plugins.cachingheaders.CachingHeadersConfig
import kotlinx.serialization.Contextual
data class ApplicationCachingHeadersConfigurator(
private val elements: List<@Contextual Element>
) : KtorApplicationConfigurator {
fun interface Element { operator fun CachingHeaders.Configuration.invoke() }
fun interface Element { operator fun CachingHeadersConfig.invoke() }
override fun Application.configure() {
install(CachingHeaders) {

View File

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

View File

@@ -1,14 +1,15 @@
package dev.inmo.micro_utils.ktor.server.configurators
import io.ktor.application.Application
import io.ktor.application.install
import io.ktor.sessions.Sessions
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.sessions.Sessions
import io.ktor.server.sessions.SessionsConfig
import kotlinx.serialization.Contextual
class ApplicationSessionsConfigurator(
private val elements: List<@Contextual Element>
) : KtorApplicationConfigurator {
fun interface Element { operator fun Sessions.Configuration.invoke() }
fun interface Element { operator fun SessionsConfig.invoke() }
override fun Application.configure() {
install(Sessions) {

View File

@@ -1,6 +1,6 @@
package dev.inmo.micro_utils.ktor.server.configurators
import io.ktor.application.Application
import io.ktor.server.application.Application
interface KtorApplicationConfigurator {
fun Application.configure()

View File

@@ -1,14 +1,15 @@
package dev.inmo.micro_utils.ktor.server.configurators
import io.ktor.application.Application
import io.ktor.application.install
import io.ktor.features.StatusPages
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.plugins.statuspages.StatusPages
import io.ktor.server.plugins.statuspages.StatusPagesConfig
import kotlinx.serialization.Contextual
class StatusPagesConfigurator(
private val elements: List<@Contextual Element>
) : KtorApplicationConfigurator {
fun interface Element { operator fun StatusPages.Configuration.invoke() }
fun interface Element { operator fun StatusPagesConfig.invoke() }
override fun Application.configure() {
install(StatusPages) {

View File

@@ -1,5 +1,6 @@
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
@@ -164,7 +165,7 @@ suspend fun main(vararg args: String) {
val ietfLanguageCodes = json.decodeFromString(
ListSerializer(LanguageCode.serializer()),
client.get(ietfLanguageCodesLink)
client.get(ietfLanguageCodesLink).bodyAsText()
).map {
it.copy(
title = it.title
@@ -175,7 +176,7 @@ suspend fun main(vararg args: String) {
}
val ietfLanguageCodesWithTagsMap = json.decodeFromString(
ListSerializer(LanguageCodeWithTag.serializer()),
client.get(ietfLanguageCodesAdditionalTagsLink)
client.get(ietfLanguageCodesAdditionalTagsLink).bodyAsText()
).filter { it.withSubtag != it.tag }.groupBy { it.tag }
val tags = ietfLanguageCodes.map {

View File

@@ -1,7 +1,6 @@
package dev.inmo.micro_utils.mime_types
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
@@ -16,6 +15,7 @@ fun mimeType(raw: String) = mimesCache.getOrPut(raw) {
internal fun parseMimeType(raw: String): MimeType = CustomMimeType(raw)
@Suppress("OPT_IN_USAGE")
@Serializer(MimeType::class)
object MimeTypeSerializer : KSerializer<MimeType> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("mimeType", PrimitiveKind.STRING)

View File

@@ -27,4 +27,5 @@ inline fun <T> PaginationResult<T>.thisPageIfNotEmpty(): PaginationResult<T>? =
null
}
@Suppress("NOTHING_TO_INLINE")
inline fun <T> PaginationResult<T>.currentPageIfNotEmpty() = thisPageIfNotEmpty()

View File

@@ -6,7 +6,7 @@ import dev.inmo.micro_utils.pagination.*
* Example:
*
* * `|__f__l_______________________|` will be transformed to `|_______________________f__l__|`
* * `|__f__l_|` will be transformed to `|__f__l_|`
* * `|__f__l_|` will be transformed to `|_f__l__|`
*
* @return Reversed version of this [Pagination]
*/

View File

@@ -1,7 +1,7 @@
package dev.inmo.micro_utils.pagination
import io.ktor.application.ApplicationCall
import io.ktor.http.Parameters
import io.ktor.server.application.ApplicationCall
val Parameters.extractPagination: Pagination
get() = SimplePagination(

View File

@@ -1,5 +1,6 @@
package dev.inmo.micro_utils.repos
@Suppress("UNCHECKED_CAST")
interface MapperRepo<FromKey, FromValue, ToKey, ToValue> {
suspend fun FromKey.toOutKey() = this as ToKey
suspend fun FromValue.toOutValue() = this as ToValue

View File

@@ -126,9 +126,10 @@ inline fun <reified FromKey, reified FromValue, reified ToKey, reified ToValue>
mapper(keyFromToTo, valueFromToTo, keyToToFrom, valueToToFrom)
)
@Suppress("DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE")
open class MapperStandardKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
private val to: StandardKeyValueRepo<ToKey, ToValue>,
mapper: MapperRepo<FromKey, FromValue, ToKey, ToValue>
private val mapper: MapperRepo<FromKey, FromValue, ToKey, ToValue>
) : StandardKeyValueRepo<FromKey, FromValue>,
MapperRepo<FromKey, FromValue, ToKey, ToValue> by mapper,
ReadStandardKeyValueRepo<FromKey, FromValue> by MapperReadStandardKeyValueRepo(to, mapper),

View File

@@ -132,6 +132,7 @@ inline fun <reified FromKey, reified FromValue, reified ToKey, reified ToValue>
mapper(keyFromToTo, valueFromToTo, keyToToFrom, valueToToFrom)
)
@Suppress("DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE")
open class MapperOneToManyKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
private val to: OneToManyKeyValueRepo<ToKey, ToValue>,
mapper: MapperRepo<FromKey, FromValue, ToKey, ToValue>

View File

@@ -13,12 +13,12 @@ interface VersionsRepo<T> : Repo {
* By default, instance of this interface will check that version of table with name [tableName] is less than
* [version] or is absent
*
* * In case if [tableName] didn't found, will be called [onCreate] and version of table will be set up to [version]
* * In case if [tableName] have version less than parameter [version], it will increase version one-by-one
* until database version will be equal to [version]
* In case if [tableName] didn't found, will be called [onCreate]. Then in case if [tableName] have version less
* than parameter [version] or null, it will increase version one-by-one until database version will be equal to
* [version]
*
* @param version Current version of table
* @param onCreate This callback will be called in case when table have no information about table
* @param onCreate This callback will be called in case when repo have no information about table
* @param onUpdate This callback will be called after **iterative** changing of version. It is expected that parameter
* "to" will always be greater than "from"
*/

View File

@@ -175,9 +175,11 @@ class FileWriteStandardKeyValueRepo(
}
@Warning("Files watching will not correctly works on Android Platform with version of API lower than API 26")
@Suppress("DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE")
class FileStandardKeyValueRepo(
folder: File,
filesChangedProcessingScope: CoroutineScope? = null
) : StandardKeyValueRepo<String, File>,
WriteStandardKeyValueRepo<String, File> by FileWriteStandardKeyValueRepo(folder, filesChangedProcessingScope),
ReadStandardKeyValueRepo<String, File> by FileReadStandardKeyValueRepo(folder)
ReadStandardKeyValueRepo<String, File> by FileReadStandardKeyValueRepo(folder) {
}

View File

@@ -0,0 +1,27 @@
package dev.inmo.micro_utils.repos
import android.database.Cursor
class CursorIterator(
private val c: Cursor
) : Iterator<Cursor> {
private var i = 0
init {
c.moveToFirst()
}
override fun hasNext(): Boolean {
return i < c.count
}
override fun next(): Cursor {
i++
return if (c.moveToNext()) {
c
} else {
throw NoSuchElementException()
}
}
}
operator fun Cursor.iterator(): CursorIterator = CursorIterator(this)

View File

@@ -16,6 +16,7 @@ fun <T : Any> Context.keyValueStore(
name: String = "default",
cacheValues: Boolean = false
): StandardKeyValueRepo<String, T> {
@Suppress("UNCHECKED_CAST")
return cache.getOrPut(name) {
KeyValueStore<T>(this, name, cacheValues)
} as KeyValueStore<T>
@@ -62,6 +63,7 @@ class KeyValueStore<T : Any> internal constructor (
}
override suspend fun get(k: String): T? {
@Suppress("UNCHECKED_CAST")
return (cachedData ?. get(k) ?: sharedPreferences.all[k]) as? T
}
@@ -73,7 +75,10 @@ class KeyValueStore<T : Any> internal constructor (
PaginationResult(
it.page,
it.pagesNumber,
it.results.map { it as T }.let { if (reversed) it.reversed() else it },
it.results.map {
@Suppress("UNCHECKED_CAST")
it as T
}.let { if (reversed) it.reversed() else it },
it.size
)
}

View File

@@ -143,7 +143,12 @@ class OneToManyAndroidRepo<Key, Value>(
}.toLong()
override suspend fun count(k: Key): Long = helper.blockingReadableTransaction {
selectDistinct(tableName, columns = valueColumnArray, selection = "$idColumnName=?", selectionArgs = arrayOf(k.keyAsString()), limit = FirstPagePagination(1).limitClause()).use {
selectDistinct(
tableName,
columns = valueColumnArray,
selection = "$idColumnName=?",
selectionArgs = arrayOf(k.keyAsString())
).use {
it.count
}
}.toLong()

View File

@@ -17,13 +17,19 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
ExposedCRUDRepo<ObjectType, IdType>,
WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>
{
protected val newObjectsChannel = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize)
protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize)
protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(replyCacheInFlows, flowsChannelsSize)
protected val _newObjectsFlow = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize)
protected val _updatedObjectsFlow = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize)
protected val _deletedObjectsIdsFlow = MutableSharedFlow<IdType>(replyCacheInFlows, flowsChannelsSize)
@Deprecated("Renamed", ReplaceWith("_newObjectsFlow"))
protected val newObjectsChannel = _newObjectsFlow
@Deprecated("Renamed", ReplaceWith("_updatedObjectsFlow"))
protected val updateObjectsChannel = _updatedObjectsFlow
@Deprecated("Renamed", ReplaceWith("_deletedObjectsIdsFlow"))
protected val deleteObjectsIdsChannel = _deletedObjectsIdsFlow
override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asSharedFlow()
override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow()
override val deletedObjectsIdsFlow: Flow<IdType> = deleteObjectsIdsChannel.asSharedFlow()
override val newObjectsFlow: Flow<ObjectType> = _newObjectsFlow.asSharedFlow()
override val updatedObjectsFlow: Flow<ObjectType> = _updatedObjectsFlow.asSharedFlow()
override val deletedObjectsIdsFlow: Flow<IdType> = _deletedObjectsIdsFlow.asSharedFlow()
protected abstract fun InsertStatement<Number>.asObject(value: InputValueType): ObjectType
abstract val selectByIds: SqlExpressionBuilder.(List<IdType>) -> Op<Boolean>
@@ -43,7 +49,7 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
return transaction(db = database) {
values.map { value -> createWithoutNotification(value) }
}.onEach {
newObjectsChannel.emit(it)
_newObjectsFlow.emit(it)
}
}
@@ -74,7 +80,7 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
onBeforeUpdate(listOf(id to value))
return updateWithoutNotification(id, value).also {
if (it != null) {
updateObjectsChannel.emit(it)
_updatedObjectsFlow.emit(it)
}
}
}
@@ -85,16 +91,25 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
values.map { (id, value) -> updateWithoutNotification(id, value) }
}.filterNotNull()
).onEach {
updateObjectsChannel.emit(it)
_updatedObjectsFlow.emit(it)
}
}
protected open suspend fun onBeforeDelete(ids: List<IdType>) {}
override suspend fun deleteById(ids: List<IdType>) {
onBeforeDelete(ids)
transaction(db = database) {
deleteWhere(null, null) {
val deleted = deleteWhere(null, null) {
selectByIds(ids)
}
if (deleted == ids.size) {
ids
} else {
ids.filter {
select { selectById(it) }.limit(1).none()
}
}
}.forEach {
_deletedObjectsIdsFlow.emit(it)
}
}
}

View File

@@ -1,8 +1,10 @@
package dev.inmo.micro_utils.repos.exposed
import dev.inmo.micro_utils.repos.Repo
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.*
interface ExposedRepo : Repo {
interface ExposedRepo : Repo, FieldSet {
val database: Database
}
val selectAll: Transaction.() -> Query
get() = { (this@ExposedRepo as FieldSet).selectAll() }
}

View File

@@ -19,7 +19,7 @@ class ExposedStandardVersionsRepoProxy(
override val database: Database
) : StandardVersionsRepoProxy<Database>, Table("ExposedVersionsProxy"), ExposedRepo {
val tableNameColumn = text("tableName")
val tableVersionColumn = integer("tableName")
val tableVersionColumn = integer("tableVersion")
init {
initTable()

View File

@@ -7,10 +7,10 @@ import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.pagination.extractPagination
import dev.inmo.micro_utils.repos.ReadStandardCRUDRepo
import dev.inmo.micro_utils.repos.ktor.common.crud.*
import io.ktor.application.call
import io.ktor.http.ContentType
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.server.application.call
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer

View File

@@ -6,8 +6,8 @@ import dev.inmo.micro_utils.ktor.server.UnifiedRouter
import dev.inmo.micro_utils.ktor.server.standardKtorSerialFormatContentType
import dev.inmo.micro_utils.repos.StandardCRUDRepo
import io.ktor.http.ContentType
import io.ktor.routing.Route
import io.ktor.routing.route
import io.ktor.server.routing.Route
import io.ktor.server.routing.route
import kotlinx.serialization.KSerializer
fun <ObjectType, IdType, InputValue> Route.configureStandardCrudRepoRoutes(

View File

@@ -5,10 +5,9 @@ import dev.inmo.micro_utils.ktor.common.standardKtorSerialFormat
import dev.inmo.micro_utils.ktor.server.*
import dev.inmo.micro_utils.repos.WriteStandardCRUDRepo
import dev.inmo.micro_utils.repos.ktor.common.crud.*
import io.ktor.application.call
import io.ktor.http.ContentType
import io.ktor.routing.Route
import io.ktor.routing.post
import io.ktor.server.routing.Route
import io.ktor.server.routing.post
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.*

View File

@@ -6,8 +6,8 @@ import dev.inmo.micro_utils.ktor.server.UnifiedRouter
import dev.inmo.micro_utils.ktor.server.standardKtorSerialFormatContentType
import dev.inmo.micro_utils.repos.StandardKeyValueRepo
import io.ktor.http.ContentType
import io.ktor.routing.Route
import io.ktor.routing.route
import io.ktor.server.routing.Route
import io.ktor.server.routing.route
import kotlinx.serialization.KSerializer
fun <K, V> Route.configureStandardKeyValueRepoRoutes(
@@ -43,4 +43,4 @@ fun <K, V> Route.configureStandartKeyValueRepoRoutes(
valueNullableSerializer: KSerializer<V?>,
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
serialFormatContentType: ContentType = standardKtorSerialFormatContentType
) = configureStandardKeyValueRepoRoutes(baseSubpart, originalRepo, keySerializer, valueSerializer, valueNullableSerializer, UnifiedRouter(serialFormat, serialFormatContentType))
) = configureStandardKeyValueRepoRoutes(baseSubpart, originalRepo, keySerializer, valueSerializer, valueNullableSerializer, UnifiedRouter(serialFormat, serialFormatContentType))

View File

@@ -8,10 +8,10 @@ import dev.inmo.micro_utils.pagination.extractPagination
import dev.inmo.micro_utils.repos.ReadStandardKeyValueRepo
import dev.inmo.micro_utils.repos.ktor.common.key_value.*
import dev.inmo.micro_utils.repos.ktor.common.valueParameterName
import io.ktor.application.call
import io.ktor.http.ContentType
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.server.application.call
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer

View File

@@ -6,8 +6,8 @@ import dev.inmo.micro_utils.ktor.server.*
import dev.inmo.micro_utils.repos.WriteStandardKeyValueRepo
import dev.inmo.micro_utils.repos.ktor.common.key_value.*
import io.ktor.http.ContentType
import io.ktor.routing.Route
import io.ktor.routing.post
import io.ktor.server.routing.Route
import io.ktor.server.routing.post
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.*

View File

@@ -6,8 +6,8 @@ import dev.inmo.micro_utils.ktor.server.UnifiedRouter
import dev.inmo.micro_utils.ktor.server.standardKtorSerialFormatContentType
import dev.inmo.micro_utils.repos.OneToManyKeyValueRepo
import io.ktor.http.ContentType
import io.ktor.routing.Route
import io.ktor.routing.route
import io.ktor.server.routing.Route
import io.ktor.server.routing.route
import kotlinx.serialization.KSerializer
fun <Key, Value> Route.configureOneToManyKeyValueRepoRoutes(
@@ -32,4 +32,4 @@ fun <Key, Value> Route.configureOneToManyKeyValueRepoRoutes(
serialFormatContentType: ContentType = standardKtorSerialFormatContentType
) = configureOneToManyKeyValueRepoRoutes(
baseSubpart, originalRepo, keySerializer, valueSerializer, UnifiedRouter(serialFormat, serialFormatContentType)
)
)

View File

@@ -6,15 +6,14 @@ import dev.inmo.micro_utils.ktor.server.*
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.pagination.extractPagination
import dev.inmo.micro_utils.repos.ReadOneToManyKeyValueRepo
import dev.inmo.micro_utils.repos.ktor.common.*
import dev.inmo.micro_utils.repos.ktor.common.keyParameterName
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.*
import dev.inmo.micro_utils.repos.ktor.common.valueParameterName
import dev.inmo.micro_utils.repos.ktor.common.reversedParameterName
import io.ktor.application.call
import io.ktor.http.ContentType
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.server.application.call
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer

View File

@@ -5,10 +5,9 @@ import dev.inmo.micro_utils.ktor.common.standardKtorSerialFormat
import dev.inmo.micro_utils.ktor.server.*
import dev.inmo.micro_utils.repos.WriteOneToManyKeyValueRepo
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.*
import io.ktor.application.call
import io.ktor.http.ContentType
import io.ktor.routing.Route
import io.ktor.routing.post
import io.ktor.server.routing.Route
import io.ktor.server.routing.post
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.*