Compare commits

..

13 Commits
0.8.6 ... 0.8.8

18 changed files with 520 additions and 42 deletions

View File

@@ -11,9 +11,9 @@ jobs:
- uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Fix android 31.0.0 dx
- name: Fix android 32.0.0 dx
continue-on-error: true
run: cd /usr/local/lib/android/sdk/build-tools/31.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
- name: Build
run: ./gradlew dokkaHtml
- name: Publish KDocs

View File

@@ -9,9 +9,9 @@ jobs:
- uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Fix android 31.0.0 dx
- name: Fix android 32.0.0 dx
continue-on-error: true
run: cd /usr/local/lib/android/sdk/build-tools/31.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
- name: Rewrite version
run: |
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"

View File

@@ -1,5 +1,24 @@
# Changelog
## 0.8.8
* `Versions`:
* `AppCompat`: `1.3.1` -> `1.4.0`
* Android Compile SDK: `31.0.0` -> `32.0.0`
* `FSM`:
* `DefaultStatesMachine` now is extendable
* New type `UpdatableStatesMachine` with default realization`DefaultUpdatableStatesMachine`
## 0.8.7
* `Ktor`:
* `Client`:
* `UnifiedRequester` now have no private fields
* Add preview work with multipart
* `Server`
* `UnifiedRouter` now have no private fields
* Add preview work with multipart
## 0.8.6
* `Common`:

View File

@@ -23,11 +23,12 @@ value class FileName(val string: String) {
}
@PreviewFeature
expect class MPPFile
expect val MPPFile.filename: FileName
expect val MPPFile.filesize: Long
expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator
expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator
fun MPPFile.bytesSync() = bytesAllocatorSync()
suspend fun MPPFile.bytes() = bytesAllocator()

View File

@@ -21,8 +21,10 @@ import kotlinx.serialization.Serializable
*/
@Serializable
data class Optional<T> internal constructor(
internal val data: T?,
internal val dataPresented: Boolean
@Warning("It is unsafe to use this data directly")
val data: T?,
@Warning("It is unsafe to use this data directly")
val dataPresented: Boolean
) {
companion object {
/**
@@ -42,17 +44,31 @@ inline val <T> T.optional
/**
* Will call [block] when data presented ([Optional.dataPresented] == true)
*/
fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply {
inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply {
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) }
}
/**
* Will call [block] when data presented ([Optional.dataPresented] == true)
*/
inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run {
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null
}
/**
* Will call [block] when data absent ([Optional.dataPresented] == false)
*/
fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply {
inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply {
if (!dataPresented) { block() }
}
/**
* Will call [block] when data presented ([Optional.dataPresented] == true)
*/
inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run {
if (!dataPresented) { @Suppress("UNCHECKED_CAST") block() } else null
}
/**
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise
*/
@@ -67,9 +83,10 @@ fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = if (dataPresented) @Supp
/**
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
*/
fun <T> Optional<T>.dataOrElse(block: () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
inline fun <T> Optional<T>.dataOrElse(block: () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
/**
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
*/
@Deprecated("dataOrElse now is inline", ReplaceWith("dataOrElse", "dev.inmo.micro_utils.common.dataOrElse"))
suspend fun <T> Optional<T>.dataOrElseSuspendable(block: suspend () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()

View File

@@ -2,8 +2,7 @@ package dev.inmo.micro_utils.common
import org.khronos.webgl.ArrayBuffer
import org.w3c.dom.ErrorEvent
import org.w3c.files.File
import org.w3c.files.FileReader
import org.w3c.files.*
import kotlin.js.Promise
/**
@@ -24,6 +23,11 @@ fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure ->
reader.readAsArrayBuffer(this)
}
fun MPPFile.readBytes(): ByteArray {
val reader = FileReaderSync()
return reader.readAsArrayBuffer(this).toByteArray()
}
private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await()
/**
@@ -40,5 +44,11 @@ actual val MPPFile.filesize: Long
* @suppress
*/
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
get() = ::readBytes
/**
* @suppress
*/
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
get() = ::dirtyReadBytes

View File

@@ -22,6 +22,11 @@ actual val MPPFile.filesize: Long
/**
* @suppress
*/
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
get() = ::readBytes
/**
* @suppress
*/
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
get() = {
doInIO {

View File

@@ -10,6 +10,7 @@ kotlin {
sourceSets {
commonMain {
dependencies {
api project(":micro_utils.common")
api project(":micro_utils.coroutines")
}
}

View File

@@ -1,8 +1,11 @@
package dev.inmo.micro_utils.fsm.common
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.common.Optional
import dev.inmo.micro_utils.common.onPresented
import dev.inmo.micro_utils.coroutines.*
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* Default [StatesMachine] may [startChain] and use inside logic for handling [State]s. By default you may use
@@ -42,17 +45,53 @@ interface StatesMachine<T : State> : StatesHandler<T, T> {
/**
* Default realization of [StatesMachine]. It uses [statesManager] for incapsulation of [State]s storing and contexts
* resolving, and uses [launchStateHandling] for [State] handling
* resolving, and uses [launchStateHandling] for [State] handling.
*
* This class suppose to be extended in case you wish some custom behaviour inside of [launchStateHandling], for example
*/
class DefaultStatesMachine <T: State>(
private val statesManager: StatesManager<T>,
private val handlers: List<CheckableHandlerHolder<in T, T>>
open class DefaultStatesMachine <T: State>(
protected val statesManager: StatesManager<T>,
protected val handlers: List<CheckableHandlerHolder<in T, T>>,
) : StatesMachine<T> {
/**
* Will call [launchStateHandling] for state handling
*/
override suspend fun StatesMachine<in T>.handleState(state: T): T? = launchStateHandling(state, handlers)
/**
* This
*/
protected val statesJobs = mutableMapOf<T, Job>()
protected val statesJobsMutex = Mutex()
protected open suspend fun performUpdate(state: T) {
val newState = launchStateHandling(state, handlers)
if (newState != null) {
statesManager.update(state, newState)
} else {
statesManager.endChain(state)
}
}
open suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
statesJobsMutex.withLock {
statesJobs[actualState] ?.cancel()
statesJobs[actualState] = scope.launch {
performUpdate(actualState)
}.also { job ->
job.invokeOnCompletion { _ ->
scope.launch {
statesJobsMutex.withLock {
if (statesJobs[actualState] == job) {
statesJobs.remove(actualState)
}
}
}
}
}
}
}
/**
* Launch handling of states. On [statesManager] [StatesManager.onStartChain],
* [statesManager] [StatesManager.onChainStateUpdated] will be called lambda with performing of state. If
@@ -60,23 +99,15 @@ class DefaultStatesMachine <T: State>(
* [StatesManager.endChain].
*/
override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
val statePerformer: suspend (T) -> Unit = { state: T ->
val newState = launchStateHandling(state, handlers)
if (newState != null) {
statesManager.update(state, newState)
} else {
statesManager.endChain(state)
}
}
statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) {
launch { statePerformer(it) }
launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
}
statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) {
launch { statePerformer(it.second) }
launch { performStateUpdate(Optional.presented(it.first), it.second, scope.LinkedSupervisorScope()) }
}
statesManager.getActiveStates().forEach {
launch { statePerformer(it) }
launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
}
}

View File

@@ -0,0 +1,58 @@
package dev.inmo.micro_utils.fsm.common
import dev.inmo.micro_utils.common.*
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.withLock
/**
* This extender of [StatesMachine] interface declare one new function [updateChain]. Realizations of this interface
* must be able to perform update of chain in internal [StatesManager]
*/
interface UpdatableStatesMachine<T : State> : StatesMachine<T> {
/**
* Update chain with current state equal to [currentState] with [newState]. Behaviour of this update preforming
* in cases when [currentState] does not exist in [StatesManager] must be declared inside of realization of
* [StatesManager.update] function
*/
suspend fun updateChain(currentState: T, newState: T)
}
open class DefaultUpdatableStatesMachine<T : State>(
statesManager: StatesManager<T>,
handlers: List<CheckableHandlerHolder<in T, T>>,
) : DefaultStatesMachine<T>(
statesManager,
handlers
), UpdatableStatesMachine<T> {
protected val jobsStates = mutableMapOf<Job, T>()
override suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
statesJobsMutex.withLock {
if (previousState.dataOrNull() != actualState) {
statesJobs[actualState] ?.cancel()
}
val job = previousState.mapOnPresented {
statesJobs.remove(it)
} ?: scope.launch {
performUpdate(actualState)
}.also { job ->
job.invokeOnCompletion { _ ->
scope.launch {
statesJobsMutex.withLock {
statesJobs.remove(
jobsStates[job] ?: return@withLock
)
}
}
}
}
jobsStates.remove(job)
statesJobs[actualState] = job
jobsStates[job] = actualState
}
}
override suspend fun updateChain(currentState: T, newState: T) {
statesManager.update(currentState, newState)
}
}

View File

@@ -7,6 +7,12 @@ import kotlin.reflect.KClass
class FSMBuilder<T : State>(
var statesManager: StatesManager<T> = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()),
val fsmBuilder: (statesManager: StatesManager<T>, states: List<CheckableHandlerHolder<T, T>>) -> StatesMachine<T> = { statesManager, states ->
StatesMachine(
statesManager,
states
)
},
var defaultStateHandler: StatesHandler<T, T>? = StatesHandler { null }
) {
private var states = mutableListOf<CheckableHandlerHolder<T, T>>()
@@ -42,7 +48,7 @@ class FSMBuilder<T : State>(
add(filter, handler)
}
fun build() = StatesMachine(
fun build() = fsmBuilder(
statesManager,
states.toList().let { list ->
defaultStateHandler ?.let { list + it.holder { true } } ?: list

View File

@@ -24,12 +24,12 @@ uuidVersion=0.3.1
core_ktx_version=1.7.0
androidx_recycler_version=1.2.1
appcompat_version=1.3.1
appcompat_version=1.4.0
android_minSdkVersion=19
android_compileSdkVersion=31
android_buildToolsVersion=31.0.0
dexcount_version=3.0.0
android_compileSdkVersion=32
android_buildToolsVersion=32.0.0
dexcount_version=3.0.1
junit_version=4.12
test_ext_junit_version=1.1.2
espresso_core=3.3.0
@@ -45,5 +45,5 @@ dokka_version=1.5.31
# Project data
group=dev.inmo
version=0.8.6
android_code_version=86
version=0.8.8
android_code_version=88

View File

@@ -0,0 +1,6 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile
import io.ktor.client.request.forms.InputProvider
expect suspend fun MPPFile.inputProvider(): InputProvider

View File

@@ -1,16 +1,20 @@
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.*
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.http.*
import io.ktor.utils.io.core.ByteReadPacket
import kotlinx.serialization.*
typealias BodyPair<T> = Pair<SerializationStrategy<T>, T>
class UnifiedRequester(
private val client: HttpClient = HttpClient(),
private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
val client: HttpClient = HttpClient(),
val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
) {
suspend fun <ResultType> uniget(
url: String,
@@ -31,6 +35,54 @@ class UnifiedRequester(
resultDeserializer: DeserializationStrategy<ResultType>
) = client.unipost(url, bodyInfo, resultDeserializer, serialFormat)
suspend fun <ResultType> unimultipart(
url: String,
filename: String,
inputProvider: InputProvider,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
): ResultType = client.unimultipart(url, filename, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat)
suspend fun <BodyType, ResultType> unimultipart(
url: String,
filename: String,
inputProvider: InputProvider,
otherData: BodyPair<BodyType>,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
): ResultType = client.unimultipart(url, filename, otherData, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat)
suspend fun <ResultType> unimultipart(
url: String,
mppFile: MPPFile,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {}
): ResultType = client.unimultipart(
url, mppFile, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat
)
suspend fun <BodyType, ResultType> unimultipart(
url: String,
mppFile: MPPFile,
otherData: BodyPair<BodyType>,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {}
): ResultType = client.unimultipart(
url, mppFile, otherData, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat
)
fun <T> createStandardWebsocketFlow(
url: String,
checkReconnection: (Throwable?) -> Boolean = { true },
@@ -69,3 +121,124 @@ suspend fun <BodyType, ResultType> HttpClient.unipost(
}.let {
serialFormat.decodeDefault(resultDeserializer, it)
}
suspend fun <ResultType> HttpClient.unimultipart(
url: String,
filename: String,
inputProvider: InputProvider,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
): ResultType = submitFormWithBinaryData<StandardKtorSerialInputData>(
url,
formData = formData {
append(
"bytes",
inputProvider,
Headers.build {
append(HttpHeaders.ContentType, mimetype)
append(HttpHeaders.ContentDisposition, "filename=$filename")
dataHeadersBuilder()
}
)
additionalParametersBuilder()
}
) {
requestBuilder()
}.let { serialFormat.decodeDefault(resultDeserializer, it) }
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
url: String,
filename: String,
otherData: BodyPair<BodyType>,
inputProvider: InputProvider,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
): ResultType = unimultipart(
url,
filename,
inputProvider,
resultDeserializer,
mimetype,
additionalParametersBuilder = {
val serialized = serialFormat.encodeDefault(otherData.first, otherData.second)
append(
"data",
InputProvider(serialized.size.toLong()) {
ByteReadPacket(serialized)
},
Headers.build {
append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType)
append(HttpHeaders.ContentDisposition, "filename=data.bytes")
dataHeadersBuilder()
}
)
additionalParametersBuilder()
},
dataHeadersBuilder,
requestBuilder,
serialFormat
)
suspend fun <ResultType> HttpClient.unimultipart(
url: String,
mppFile: MPPFile,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
): ResultType = unimultipart(
url,
mppFile.filename.string,
mppFile.inputProvider(),
resultDeserializer,
mimetype,
additionalParametersBuilder,
dataHeadersBuilder,
requestBuilder,
serialFormat
)
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
url: String,
mppFile: MPPFile,
otherData: BodyPair<BodyType>,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
): ResultType = unimultipart(
url,
mppFile,
resultDeserializer,
mimetype,
additionalParametersBuilder = {
val serialized = serialFormat.encodeDefault(otherData.first, otherData.second)
append(
"data",
InputProvider(serialized.size.toLong()) {
ByteReadPacket(serialized)
},
Headers.build {
append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType)
append(HttpHeaders.ContentDisposition, "filename=data.bytes")
dataHeadersBuilder()
}
)
additionalParametersBuilder()
},
dataHeadersBuilder,
requestBuilder,
serialFormat
)

View File

@@ -0,0 +1,11 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.*
import io.ktor.client.request.forms.InputProvider
import io.ktor.utils.io.core.ByteReadPacket
actual suspend fun MPPFile.inputProvider(): InputProvider = bytes().let {
InputProvider(it.size.toLong()) {
ByteReadPacket(it)
}
}

View File

@@ -0,0 +1,9 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile
import io.ktor.client.request.forms.InputProvider
import io.ktor.utils.io.streams.asInput
actual suspend fun MPPFile.inputProvider(): InputProvider = InputProvider(length()) {
inputStream().asInput()
}

View File

@@ -10,6 +10,7 @@ kotlin {
sourceSets {
commonMain {
dependencies {
api internalProject("micro_utils.common")
api "org.jetbrains.kotlinx:kotlinx-serialization-cbor:$kotlin_serialisation_core_version"
api "com.soywiz.korlibs.klock:klock:$klockVersion"
}

View File

@@ -1,22 +1,31 @@
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.ContentType
import io.ktor.http.HttpStatusCode
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.util.pipeline.PipelineContext
import io.ktor.utils.io.core.*
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.*
import java.io.File
import java.io.File.createTempFile
class UnifiedRouter(
private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
private val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
) {
fun <T> Route.includeWebsocketHandling(
suburl: String,
@@ -104,6 +113,127 @@ suspend fun <T> ApplicationCall.uniload(
)
}
suspend fun ApplicationCall.uniloadMultipart(
onFormItem: (PartData.FormItem) -> Unit = {},
onCustomFileItem: (PartData.FileItem) -> Unit = {},
onBinaryContent: (PartData.BinaryItem) -> Unit = {}
) = safely {
val multipartData = receiveMultipart()
var resultInput: Input? = null
multipartData.forEachPart {
when (it) {
is PartData.FormItem -> onFormItem(it)
is PartData.FileItem -> {
when (it.name) {
"bytes" -> resultInput = it.provider()
else -> onCustomFileItem(it)
}
}
is PartData.BinaryItem -> onBinaryContent(it)
}
}
resultInput ?: error("Bytes has not been received")
}
suspend fun <T> ApplicationCall.uniloadMultipart(
deserializer: DeserializationStrategy<T>,
onFormItem: (PartData.FormItem) -> Unit = {},
onCustomFileItem: (PartData.FileItem) -> Unit = {},
onBinaryContent: (PartData.BinaryItem) -> Unit = {}
): Pair<Input, T> {
var data: Optional<T>? = null
val resultInput = uniloadMultipart(
onFormItem,
{
if (it.name == "data") {
data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional
} else {
onCustomFileItem(it)
}
},
onBinaryContent
)
val completeData = data ?: error("Data has not been received")
return resultInput to (completeData.dataOrNull().let { it as T })
}
suspend fun <T> ApplicationCall.uniloadMultipartFile(
deserializer: DeserializationStrategy<T>,
onFormItem: (PartData.FormItem) -> Unit = {},
onCustomFileItem: (PartData.FileItem) -> Unit = {},
onBinaryContent: (PartData.BinaryItem) -> Unit = {},
) = safely {
val multipartData = receiveMultipart()
var resultInput: MPPFile? = null
var data: Optional<T>? = null
multipartData.forEachPart {
when (it) {
is PartData.FormItem -> onFormItem(it)
is PartData.FileItem -> {
when (it.name) {
"bytes" -> {
val name = FileName(it.originalFileName ?: error("File name is unknown for default part"))
resultInput = MPPFile.createTempFile(
name.nameWithoutExtension,
".${name.extension}"
).apply {
outputStream().use { fileStream ->
it.provider().asStream().copyTo(fileStream)
}
}
}
"data" -> data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional
else -> onCustomFileItem(it)
}
}
is PartData.BinaryItem -> onBinaryContent(it)
}
}
val completeData = data ?: error("Data has not been received")
(resultInput ?: error("Bytes has not been received")) to (completeData.dataOrNull().let { it as T })
}
suspend fun ApplicationCall.uniloadMultipartFile(
onFormItem: (PartData.FormItem) -> Unit = {},
onCustomFileItem: (PartData.FileItem) -> Unit = {},
onBinaryContent: (PartData.BinaryItem) -> Unit = {},
) = safely {
val multipartData = receiveMultipart()
var resultInput: MPPFile? = null
multipartData.forEachPart {
when (it) {
is PartData.FormItem -> onFormItem(it)
is PartData.FileItem -> {
if (it.name == "bytes") {
val name = FileName(it.originalFileName ?: error("File name is unknown for default part"))
resultInput = MPPFile.createTempFile(
name.nameWithoutExtension,
".${name.extension}"
).apply {
outputStream().use { fileStream ->
it.provider().asStream().copyTo(fileStream)
}
}
} else {
onCustomFileItem(it)
}
}
is PartData.BinaryItem -> onBinaryContent(it)
}
}
resultInput ?: error("Bytes has not been received")
}
suspend fun ApplicationCall.getParameterOrSendError(
field: String
) = parameters[field].also {