Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

增加一个有关 Ktor Client 的辅助工具模块 ktor-inputfile #805

Merged
merged 1 commit into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ openjdk-jmh-core = { group = "org.openjdk.jmh", name = "jmh-core", version.ref =
openjdk-jmh-generator-annprocess = { group = "org.openjdk.jmh", name = "jmh-generator-annprocess", version.ref = "openjdk-jmh" }

# ktor
ktor-io = { group = "io.ktor", name = "ktor-io", version.ref = "ktor" }
ktor-http = { group = "io.ktor", name = "ktor-http", version.ref = "ktor" }

ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" }
Expand All @@ -99,6 +100,7 @@ ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" }
ktor-client-js = { group = "io.ktor", name = "ktor-client-js", version.ref = "ktor" }
ktor-client-mock = { group = "io.ktor", name = "ktor-client-mock", version.ref = "ktor" }

# Lincheck
# https://kotlinlang.org/docs/lincheck-guide.html#associate-compiler-tasks
Expand Down
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ include(":simbot-commons:simbot-common-collection")
include(":simbot-commons:simbot-common-atomic")
include(":simbot-commons:simbot-common-apidefinition")
include(":simbot-commons:simbot-common-core")
include(":simbot-commons:simbot-common-ktor-inputfile")
include(":simbot-commons:simbot-common-suspend-runner")
include(":simbot-commons:simbot-common-stage-loop")
include(":simbot-api")
Expand Down
7 changes: 7 additions & 0 deletions simbot-commons/simbot-common-ktor-inputfile/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# simbot-common-ktor-inputfile

一个适用于 Ktor Client,可更简单的将一个本地文件作为 `Ktor` 的 `MultiPartFormDataContent` 的一部分。
提供一个类型 `InputFile`,以及相关的扩展方法、工厂方法,
比如在 JVM 下可以通过 `File` 和 `Path` 构建它。

在 JVM 中对 `ktor-client-core` 的依赖是**仅编译**的。
81 changes: 81 additions & 0 deletions simbot-commons/simbot-common-ktor-inputfile/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright (c) 2024. ForteScarlet.
*
* Project https://github.com/simple-robot/simpler-robot
* Email [email protected]
*
* This file is part of the Simple Robot Library (Alias: simple-robot, simbot, etc.).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Lesser GNU General Public License for more details.
*
* You should have received a copy of the Lesser GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/

import love.forte.gradle.common.core.project.setup
import love.forte.gradle.common.kotlin.multiplatform.applyTier1
import love.forte.gradle.common.kotlin.multiplatform.applyTier2
import love.forte.gradle.common.kotlin.multiplatform.applyTier3

plugins {
kotlin("multiplatform")
id("simbot.dokka-module-configuration")
}

setup(P.SimbotCommon)

configJavaCompileWithModule("simbot.common.ktor.inputfile")
apply(plugin = "simbot-multiplatform-maven-publish")

kotlin {
explicitApi()
applyDefaultHierarchyTemplate()

configKotlinJvm(JVMConstants.KT_JVM_TARGET_VALUE)

js(IR) {
configJs()
}


applyTier1()
applyTier2()
applyTier3(supportKtorClient = true)

sourceSets {
commonMain.dependencies {
compileOnly(libs.ktor.client.core)
}
commonTest {
dependencies {
implementation(kotlin("test"))
implementation(libs.kotlinx.coroutines.test)
implementation(libs.ktor.client.mock)
}
}
jvmTest {
dependencies {
implementation(libs.slf4j.api)
implementation(project(":simbot-logger-slf4j2-impl"))
}
}

jsMain.dependencies {
implementation(libs.ktor.client.core)
}

nativeMain.dependencies {
implementation(libs.ktor.client.core)
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright (c) 2024. ForteScarlet.
*
* Project https://github.com/simple-robot/simpler-robot
* Email [email protected]
*
* This file is part of the Simple Robot Library (Alias: simple-robot, simbot, etc.).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Lesser GNU General Public License for more details.
*
* You should have received a copy of the Lesser GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
@file:JvmName("InputFiles")
@file:JvmMultifileClass

package love.forte.simbot.common.ktor.inputfile

import io.ktor.client.request.forms.*
import io.ktor.http.*
import io.ktor.utils.io.core.*
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.jvm.JvmOverloads

/**
* An [InputFile](https://core.telegram.org/bots/api#inputfile).
*
* Tip: Create [InputFile] via static methods in `InputFiles` on JVM.
*
* @author ForteScarlet
*/
public interface InputFile {
/**
* Include this file to [FormBuilder].
*/
@Throws(Exception::class)
public fun includeTo(key: String, headers: Headers = Headers.Empty, formBuilder: FormBuilder)

/**
* To a [FormPart].
*/
@Throws(Exception::class)
public fun toFormPart(key: String, headers: Headers = Headers.Empty): FormPart<*>
}

/**
* Create an instance of [InputFile] from [ByteArray].
*/
@JvmName("of")
@JvmOverloads
public fun InputFile(bytes: ByteArray, defaultHeaders: Headers = Headers.Empty): InputFile =
ByteArrayInputFile(bytes, defaultHeaders)

private class ByteArrayInputFile(private val bytes: ByteArray, private val defaultHeaders: Headers) : InputFile {
override fun includeTo(key: String, headers: Headers, formBuilder: FormBuilder) {
formBuilder.append(key, bytes, defaultHeaders + headers)
}

override fun toFormPart(key: String, headers: Headers): FormPart<*> =
FormPart(key, bytes, defaultHeaders + headers)
}

/**
* Create an instance of [InputFile] from [InputProvider].
*/
@JvmName("of")
@JvmOverloads
public fun InputFile(input: InputProvider, defaultHeaders: Headers = Headers.Empty): InputFile =
InputProviderInputFile(input, defaultHeaders)

private class InputProviderInputFile(private val input: InputProvider, private val defaultHeaders: Headers) :
InputFile {
override fun includeTo(key: String, headers: Headers, formBuilder: FormBuilder) {
formBuilder.append(key, input, defaultHeaders + headers)
}

override fun toFormPart(key: String, headers: Headers): FormPart<*> =
FormPart(key, input, defaultHeaders + headers)
}

/**
* Create an instance of [InputFile] from [ChannelProvider].
*/
@JvmName("of")
@JvmOverloads
public fun InputFile(channel: ChannelProvider, defaultHeaders: Headers = Headers.Empty): InputFile =
ChannelProviderInputFile(channel, defaultHeaders)

private class ChannelProviderInputFile(private val channel: ChannelProvider, private val defaultHeaders: Headers) :
InputFile {
override fun includeTo(key: String, headers: Headers, formBuilder: FormBuilder) {
formBuilder.append(key, channel, defaultHeaders + headers)
}

override fun toFormPart(key: String, headers: Headers): FormPart<*> =
FormPart(key, channel, defaultHeaders + headers)
}

/**
* Create an instance of [InputFile] from [ByteReadPacket].
*/
@JvmName("of")
@JvmOverloads
public fun InputFile(byteReadPacket: ByteReadPacket, defaultHeaders: Headers = Headers.Empty): InputFile =
ByteReadPacketInputFile(byteReadPacket, defaultHeaders)

private class ByteReadPacketInputFile(private val byteReadPacket: ByteReadPacket, private val defaultHeaders: Headers) :
InputFile {
override fun includeTo(key: String, headers: Headers, formBuilder: FormBuilder) {
formBuilder.append(key, byteReadPacket, defaultHeaders + headers)
}

override fun toFormPart(key: String, headers: Headers): FormPart<*> =
FormPart(key, byteReadPacket, defaultHeaders + headers)
}

internal operator fun Headers.plus(other: Headers): Headers {
if (isEmpty()) return other
if (other.isEmpty()) return this

return headers {
appendAll(this@plus)
appendAll(other)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright (c) 2024. ForteScarlet.
*
* Project https://github.com/simple-robot/simpler-robot
* Email [email protected]
*
* This file is part of the Simple Robot Library (Alias: simple-robot, simbot, etc.).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Lesser GNU General Public License for more details.
*
* You should have received a copy of the Lesser GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/

package love.forte.simbot.common.ktor.inputfile

import io.ktor.client.*
import io.ktor.client.engine.mock.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.utils.io.*
import io.ktor.utils.io.core.*
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs


/**
*
* @author ForteScarlet
*/
class InputFileTests {
companion object {
private const val CLRF = "\r\n"

internal suspend inline fun assertInputFileByMockClient(
length: Int, assertText: String,
inputFile: InputFile,
) {
HttpClient(MockEngine { req ->
val body = req.body
assertIs<MultiPartFormDataContent>(body)
val boundary = body.boundary
val bodyText = body.toByteArray().decodeToString()

assertEquals(
"--$boundary$CLRF" +
"Content-Disposition: form-data; name=file$CLRF" +
"Content-Length: $length$CLRF" +
CLRF +
"$assertText$CLRF" +
"--$boundary--$CLRF",
bodyText
)

respondOk()
}).use {
it.post("") {
setBody(MultiPartFormDataContent(formData {
inputFile.includeTo("file", formBuilder = this)
}))
}
}
}
}

@Test
fun bytesInputFile() = runTest {
val realText = "Hello, World"
val bytes = realText.toByteArray()

assertInputFileByMockClient(bytes.size, realText, InputFile(bytes))
}

@Test
fun inputProviderInputFile() = runTest {
val realText = "Hello, World"
val bytes = realText.toByteArray()

assertInputFileByMockClient(bytes.size, realText, InputFile(InputProvider(bytes.size.toLong()) { ByteReadPacket(bytes) }))
}

@Test
fun channelProviderInputFile() = runTest {
val realText = "Hello, World"
val bytes = realText.toByteArray()

assertInputFileByMockClient(bytes.size, realText, InputFile(ChannelProvider(bytes.size.toLong()) { ByteReadChannel(realText) }))
}

@Test
fun byteReadPacketInputFile() = runTest {
val realText = "Hello, World"
val bytes = realText.toByteArray()

assertInputFileByMockClient(bytes.size, realText, InputFile(ByteReadPacket(bytes)))
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module simbot.common.ktor.inputfile {
requires kotlin.stdlib;
requires static io.ktor.http;
requires static io.ktor.client.core;
requires static io.ktor.utils;
requires static io.ktor.io;

exports love.forte.simbot.common.ktor.inputfile;
}
Loading
Loading