partially implemented ktor utils

This commit is contained in:
000Sanya 2020-09-22 12:20:22 +10:00
parent b84367f47c
commit 1dcf17a35d
38 changed files with 541 additions and 0 deletions

View File

@ -7,6 +7,8 @@ kotlin_exposed_version=0.27.1
ktor_version=1.4.0
klockVersion=1.7.3
gradle_bintray_plugin_version=1.8.5
uuidVersion=0.2.2

17
ktor/client/build.gradle Normal file
View File

@ -0,0 +1,17 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
}
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api internalProject("micro_utils.ktor.common")
api "io.ktor:ktor-client:$ktor_version"
}
}
}
}

View File

@ -0,0 +1,81 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.ktor.common.asCorrectWebSocketUrl
import dev.inmo.micro_utils.ktor.common.standardKtorSerialFormat
import io.ktor.client.HttpClient
import io.ktor.client.features.websocket.ws
import io.ktor.http.cio.websocket.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.serialization.DeserializationStrategy
/**
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
* connection. Must return true in case if must be reconnected. By default always reconnecting
*/
inline fun <T> HttpClient.createStandardWebsocketFlow(
url: String,
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
crossinline conversation: suspend (ByteArray) -> T
): Flow<T> {
val correctedUrl = url.asCorrectWebSocketUrl
return channelFlow {
val producerScope = this@channelFlow
do {
val reconnect = try {
safely(
{
throw it
}
) {
ws(
correctedUrl
) {
while (true) {
when (val received = incoming.receive()) {
is Frame.Binary -> producerScope.send(
conversation(received.readBytes())
)
else -> {
producerScope.close()
return@ws
}
}
}
}
}
checkReconnection(null)
} catch (e: Throwable) {
checkReconnection(e).also {
if (!it) {
producerScope.close(e)
}
}
}
} while (reconnect)
if (!producerScope.isClosedForSend) {
safely(
{ /* do nothing */ }
) {
producerScope.close()
}
}
}
}
/**
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
* connection. Must return true in case if must be reconnected. By default always reconnecting
*/
inline fun <T> HttpClient.createStandardWebsocketFlow(
url: String,
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
deserializer: DeserializationStrategy<T>
) = createStandardWebsocketFlow(
url,
checkReconnection
) {
standardKtorSerialFormat.decodeFromByteArray(deserializer, it)
}

View File

@ -0,0 +1,33 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.ktor.common.standardKtorSerialFormat
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.request.post
import kotlinx.serialization.*
typealias BodyPair<T> = Pair<SerializationStrategy<T>, T>
suspend fun <ResultType> HttpClient.uniget(
url: String,
resultDeserializer: DeserializationStrategy<ResultType>
) = get<ByteArray>(
url
).let {
standardKtorSerialFormat.decodeFromByteArray(resultDeserializer, it)
}
fun <T> SerializationStrategy<T>.encodeUrlQueryValue(value: T) = standardKtorSerialFormat.encodeToHexString(
this,
value
)
suspend fun <BodyType, ResultType> HttpClient.unipost(
url: String,
bodyInfo: BodyPair<BodyType>,
resultDeserializer: DeserializationStrategy<ResultType>
) = post<ByteArray>(url) {
body = standardKtorSerialFormat.encodeToByteArray(bodyInfo.first, bodyInfo.second)
}.let {
standardKtorSerialFormat.decodeFromByteArray(resultDeserializer, it)
}

16
ktor/common/build.gradle Normal file
View File

@ -0,0 +1,16 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
}
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api "com.soywiz.korlibs.klock:klock:$klockVersion"
}
}
}
}

View File

@ -0,0 +1,23 @@
package dev.inmo.micro_utils.ktor.common
fun buildStandardUrl(
basePart: String,
subpart: String,
parameters: QueryParams = emptyMap()
) = "$basePart/$subpart".includeQueryParams(
parameters
)
fun buildStandardUrl(
basePart: String,
subpart: String,
parameters: List<QueryParam>
) = "$basePart/$subpart".includeQueryParams(
parameters
)
fun buildStandardUrl(
basePart: String,
subpart: String,
vararg parameters: QueryParam
) = buildStandardUrl(basePart, subpart, parameters.toList())

View File

@ -0,0 +1,6 @@
package dev.inmo.micro_utils.ktor.common
import kotlinx.serialization.builtins.SetSerializer
import kotlinx.serialization.builtins.serializer
val setIdsSerializer = SetSerializer(String.serializer())

View File

@ -0,0 +1,3 @@
package dev.inmo.micro_utils.ktor.common
object CorrectCloseException : Exception()

View File

@ -0,0 +1,14 @@
package dev.inmo.micro_utils.ktor.common
private val schemaRegex = Regex("[^:]*//")
val String.asCorrectWebSocketUrl: String
get() = if (startsWith("ws")) {
this
} else {
if (contains("://")) {
replace(schemaRegex, "ws://")
} else {
"ws://$this"
}
}

View File

@ -0,0 +1,17 @@
package dev.inmo.micro_utils.ktor.common
import com.soywiz.klock.DateTime
typealias FromToDateTime = Pair<DateTime?, DateTime?>
val FromToDateTime.asFromToUrlPart: QueryParams
get() = mapOf(
"from" to first ?.unixMillis ?.toString(),
"to" to second ?.unixMillis ?.toString()
)
val QueryParams.extractFromToDateTime: FromToDateTime
get() = FromToDateTime(
get("from") ?.toDoubleOrNull() ?.let { DateTime(it) },
get("to") ?.toDoubleOrNull() ?.let { DateTime(it) }
)

View File

@ -0,0 +1,25 @@
package dev.inmo.micro_utils.ktor.common
typealias QueryParam = Pair<String, String?>
typealias QueryParams = Map<String, String?>
val QueryParams.asUrlQuery: String
get() = keys.joinToString("&") { "${it}${get(it) ?.let { value -> "=$value" }}" }
val List<QueryParam>.asUrlQuery: String
get() = joinToString("&") { (key, value) -> "${key}${value ?.let { _ -> "=$value" }}" }
fun String.includeQueryParams(
queryParams: QueryParams
): String = "$this${if (contains("?")) "&" else "?"}${queryParams.asUrlQuery}"
fun String.includeQueryParams(
queryParams: List<QueryParam>
): String = "$this${if (contains("?")) "&" else "?"}${queryParams.asUrlQuery}"
val String.parseUrlQuery: QueryParams
get() = split("&").map {
it.split("=").let { pair ->
pair.first() to pair.getOrNull(1)
}
}.toMap()

View File

@ -0,0 +1,5 @@
package dev.inmo.micro_utils.ktor.common
import kotlinx.serialization.cbor.Cbor
val standardKtorSerialFormat = Cbor

View File

@ -0,0 +1,7 @@
package dev.inmo.micro_utils.ktor.common
const val clientWebsocketHelloMessage = "Start getting of updates"
const val serverWebsocketHelloMessage = "Accepted"
const val serverWebsocketNewMessageMessage = "NewMessage"
const val websocketFinalizationMessage = "Final"

24
ktor/server/build.gradle Normal file
View File

@ -0,0 +1,24 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
}
apply from: "$mppJavaProjectPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api internalProject("micro_utils.ktor.common")
}
}
jvmMain {
dependencies {
api "io.ktor:ktor-server:$ktor_version"
api "io.ktor:ktor-server-host-common:$ktor_version"
api "io.ktor:ktor-websockets:$ktor_version"
}
}
}
}

View File

@ -0,0 +1,43 @@
package dev.inmo.micro_utils.ktor.server
import dev.inmo.micro_utils.ktor.common.CorrectCloseException
import dev.inmo.micro_utils.ktor.common.standardKtorSerialFormat
import io.ktor.http.cio.websocket.*
import io.ktor.routing.Route
import io.ktor.websocket.webSocket
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.serialization.SerializationStrategy
private suspend fun DefaultWebSocketSession.checkReceivedAndCloseIfExists() {
if (incoming.poll() != null) {
close()
throw CorrectCloseException
}
}
fun <T> Route.includeWebsocketHandling(
suburl: String,
flow: Flow<T>,
converter: (T) -> ByteArray
) {
webSocket(suburl) {
safely {
flow.collect {
checkReceivedAndCloseIfExists()
send(converter(it))
}
}
}
}
fun <T> Route.includeWebsocketHandling(
suburl: String,
flow: Flow<T>,
serializer: SerializationStrategy<T>
) = includeWebsocketHandling(
suburl,
flow
) {
standardKtorSerialFormat.encodeToByteArray(serializer, it)
}

View File

@ -0,0 +1,11 @@
package dev.inmo.micro_utils.ktor.server
import com.soywiz.klock.DateTime
import dev.inmo.micro_utils.ktor.common.FromToDateTime
import io.ktor.http.Parameters
val Parameters.extractFromToDateTime: FromToDateTime
get() = FromToDateTime(
get("from") ?.toDoubleOrNull() ?.let { DateTime(it) },
get("to") ?.toDoubleOrNull() ?.let { DateTime(it) }
)

View File

@ -0,0 +1,65 @@
package dev.inmo.micro_utils.ktor.server
import dev.inmo.micro_utils.ktor.common.standardKtorSerialFormat
import io.ktor.application.ApplicationCall
import io.ktor.http.HttpStatusCode
import io.ktor.response.respond
import io.ktor.response.respondBytes
import io.ktor.util.toByteArray
import kotlinx.serialization.*
suspend fun <T> ApplicationCall.unianswer(
answerSerializer: SerializationStrategy<T>,
answer: T
) {
respondBytes(
standardKtorSerialFormat.encodeToByteArray(answerSerializer, answer),
standardKtorSerialFormatContentType
)
}
suspend fun <T> ApplicationCall.uniload(
deserializer: DeserializationStrategy<T>
) = standardKtorSerialFormat.decodeFromByteArray(
deserializer,
request.receiveChannel().toByteArray()
)
suspend fun ApplicationCall.getParameterOrSendError(
field: String
) = parameters[field].also {
if (it == null) {
respond(HttpStatusCode.BadRequest, "Request must contains $field")
}
}
fun ApplicationCall.getQueryParameter(
field: String
) = request.queryParameters[field]
suspend fun ApplicationCall.getQueryParameterOrSendError(
field: String
) = getQueryParameter(field).also {
if (it == null) {
respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
}
}
fun <T> ApplicationCall.decodeUrlQueryValue(
field: String,
deserializer: DeserializationStrategy<T>
) = getQueryParameter(field) ?.let {
standardKtorSerialFormat.decodeFromHexString(
deserializer,
it
)
}
suspend fun <T> ApplicationCall.decodeUrlQueryValueOrSendError(
field: String,
deserializer: DeserializationStrategy<T>
) = decodeUrlQueryValue(field, deserializer).also {
if (it == null) {
respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
}
}

View File

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

View File

@ -0,0 +1,27 @@
package dev.inmo.micro_utils.ktor.server.configurators
import io.ktor.application.*
import io.ktor.routing.Route
import io.ktor.routing.Routing
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
class ApplicationRoutingConfigurator(
private val elements: List<@Contextual Element>
) : KtorApplicationConfigurator {
interface Element { operator fun Route.invoke() }
override fun Application.configure() {
try {
feature(Routing)
} catch (e: IllegalStateException) {
install(Routing) {
elements.forEach {
it.apply { invoke() }
}
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
package dev.inmo.micro_utils.ktor.server
import io.ktor.http.ContentType
val standardKtorSerialFormatContentType = ContentType.Application.Cbor

View File

@ -10,6 +10,8 @@ kotlin {
commonMain {
dependencies {
api internalProject("micro_utils.repos.ktor.common")
api internalProject("micro_utils.pagination.ktor.common")
api internalProject("micro_utils.ktor.client")
api "io.ktor:ktor-client:$ktor_version"
}
}

View File

@ -1,7 +1,11 @@
package dev.inmo.micro_utils.repos.ktor.client.crud
import dev.inmo.micro_utils.ktor.client.encodeUrlQueryValue
import dev.inmo.micro_utils.ktor.client.uniget
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.pagination.asUrlQueryParts
import dev.inmo.micro_utils.repos.ReadStandardCRUDRepo
import dev.inmo.micro_utils.repos.ktor.common.crud.*
import io.ktor.client.HttpClient

View File

@ -1,5 +1,9 @@
package dev.inmo.micro_utils.repos.ktor.client.crud
import dev.inmo.micro_utils.ktor.client.BodyPair
import dev.inmo.micro_utils.ktor.client.createStandardWebsocketFlow
import dev.inmo.micro_utils.ktor.client.unipost
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
import dev.inmo.micro_utils.repos.UpdatedValuePair
import dev.inmo.micro_utils.repos.WriteStandardCRUDRepo
import dev.inmo.micro_utils.repos.ktor.common.crud.*

View File

@ -1,7 +1,11 @@
package dev.inmo.micro_utils.repos.ktor.client.key_value
import dev.inmo.micro_utils.ktor.client.encodeUrlQueryValue
import dev.inmo.micro_utils.ktor.client.uniget
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.pagination.asUrlQueryParts
import dev.inmo.micro_utils.repos.StandardReadKeyValueRepo
import dev.inmo.micro_utils.repos.ktor.common.key_value.*
import io.ktor.client.*

View File

@ -1,5 +1,9 @@
package dev.inmo.micro_utils.repos.ktor.client.key_value
import dev.inmo.micro_utils.ktor.client.BodyPair
import dev.inmo.micro_utils.ktor.client.createStandardWebsocketFlow
import dev.inmo.micro_utils.ktor.client.unipost
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
import dev.inmo.micro_utils.repos.StandardWriteKeyValueRepo
import dev.inmo.micro_utils.repos.ktor.common.key_value.*
import io.ktor.client.*

View File

@ -1,7 +1,11 @@
package dev.inmo.micro_utils.repos.ktor.client.one_to_many
import dev.inmo.micro_utils.ktor.client.encodeUrlQueryValue
import dev.inmo.micro_utils.ktor.client.uniget
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.pagination.asUrlQueryParts
import dev.inmo.micro_utils.repos.OneToManyReadKeyValueRepo
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.*
import io.ktor.client.HttpClient

View File

@ -1,5 +1,8 @@
package dev.inmo.micro_utils.repos.ktor.client.one_to_many
import dev.inmo.micro_utils.ktor.client.BodyPair
import dev.inmo.micro_utils.ktor.client.unipost
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
import dev.inmo.micro_utils.repos.OneToManyWriteKeyValueRepo
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.*
import io.ktor.client.HttpClient

View File

@ -15,6 +15,8 @@ kotlin {
jvmMain {
dependencies {
api internalProject("micro_utils.pagination.ktor.server")
api internalProject("micro_utils.ktor.server")
api "io.ktor:ktor-server:$ktor_version"
api "io.ktor:ktor-server-host-common:$ktor_version"
}

View File

@ -1,9 +1,12 @@
package dev.inmo.micro_utils.repos.ktor.server.crud
import dev.inmo.micro_utils.ktor.server.decodeUrlQueryValueOrSendError
import dev.inmo.micro_utils.ktor.server.unianswer
import dev.inmo.micro_utils.repos.ktor.common.crud.containsRouting
import dev.inmo.micro_utils.repos.ktor.common.crud.getByIdRouting
import dev.inmo.micro_utils.repos.ktor.common.crud.getByPaginationRouting
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.pagination.extractPagination
import dev.inmo.micro_utils.repos.ReadStandardCRUDRepo
import io.ktor.application.call
import io.ktor.routing.Route

View File

@ -1,5 +1,8 @@
package dev.inmo.micro_utils.repos.ktor.server.crud
import dev.inmo.micro_utils.ktor.server.includeWebsocketHandling
import dev.inmo.micro_utils.ktor.server.unianswer
import dev.inmo.micro_utils.ktor.server.uniload
import dev.inmo.micro_utils.repos.WriteStandardCRUDRepo
import dev.inmo.micro_utils.repos.ktor.common.crud.*
import io.ktor.application.call

View File

@ -1,5 +1,9 @@
package dev.inmo.micro_utils.repos.ktor.server.key_value
import dev.inmo.micro_utils.ktor.server.decodeUrlQueryValueOrSendError
import dev.inmo.micro_utils.ktor.server.unianswer
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.pagination.extractPagination
import dev.inmo.micro_utils.repos.StandardReadKeyValueRepo
import dev.inmo.micro_utils.repos.ktor.common.key_value.*
import io.ktor.application.*

View File

@ -1,5 +1,7 @@
package dev.inmo.micro_utils.repos.ktor.server.key_value
import dev.inmo.micro_utils.ktor.server.includeWebsocketHandling
import dev.inmo.micro_utils.ktor.server.uniload
import dev.inmo.micro_utils.repos.StandardWriteKeyValueRepo
import dev.inmo.micro_utils.repos.ktor.common.key_value.*
import io.ktor.application.*

View File

@ -1,6 +1,10 @@
package dev.inmo.micro_utils.repos.ktor.server.one_to_many
import dev.inmo.micro_utils.ktor.server.decodeUrlQueryValue
import dev.inmo.micro_utils.ktor.server.decodeUrlQueryValueOrSendError
import dev.inmo.micro_utils.ktor.server.unianswer
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.pagination.extractPagination
import dev.inmo.micro_utils.repos.OneToManyReadKeyValueRepo
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.*
import io.ktor.application.call

View File

@ -1,5 +1,7 @@
package dev.inmo.micro_utils.repos.ktor.server.one_to_many
import dev.inmo.micro_utils.ktor.server.unianswer
import dev.inmo.micro_utils.ktor.server.uniload
import dev.inmo.micro_utils.repos.OneToManyWriteKeyValueRepo
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.*
import io.ktor.application.call

View File

@ -10,6 +10,9 @@ String[] includes = [
":repos:ktor:client",
":repos:ktor:common",
":repos:ktor:server",
":ktor:server",
":ktor:common",
":ktor:client",
]