diff --git a/.changelog/v4.0.0-beta2.md b/.changelog/v4.0.0-beta2.md new file mode 100644 index 000000000..e69de29bb diff --git a/.github/workflows/publish-v4-kdoc.yml b/.github/workflows/publish-v4-kdoc.yml new file mode 100644 index 000000000..40c723ffd --- /dev/null +++ b/.github/workflows/publish-v4-kdoc.yml @@ -0,0 +1,59 @@ +name: Publish V4 KDoc +on: + workflow_dispatch: + + +env: + IS_CI: true + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} + GPG_SECRET_KEY: ${{ secrets.GPG_SECRET_KEY }} + GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + GRADLE_OPTS: "-XX:MaxMetaspaceSize=2g -Dfile.encoding=UTF-8" + +jobs: + deploy-doc: + name: Deploy V4 API Doc + runs-on: macos-latest + steps: + # 检出仓库代码 + - name: Check out repo + uses: actions/checkout@v4 + with: + persist-credentials: false + fetch-depth: 0 + + # setup Java + - name: Setup java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: 21 + cache: 'gradle' + + # setup Gradle + - name: Gradle generate documentation + uses: gradle/gradle-build-action@v3 + with: + gradle-version: 8.5 + arguments: | + dokkaHtmlMultiModule + --info + --warning-mode all + -x test + --build-cache + -Porg.gradle.jvmargs="-Xms4G -XX:MaxMetaspaceSize=2G -Dfile.encoding=UTF-8" + -Porg.gradle.daemon=false + env: + SIMBOT_IS_SNAPSHOT: false + + # https://github.com/marketplace/actions/github-pages-action + - name: Push to doc repository + uses: peaceiris/actions-gh-pages@v3 + with: + personal_token: ${{ secrets.PUSH_TOKEN }} + external_repository: simple-robot-library/simbot3-api-docs + publish_branch: kdoc-deploy/main-v4 + publish_dir: ./build/dokka/html + destination_dir: main-v4 diff --git a/.github/workflows/publish-v4-snapshot.yml b/.github/workflows/publish-v4-snapshot.yml index 81c373caa..3015cad60 100644 --- a/.github/workflows/publish-v4-snapshot.yml +++ b/.github/workflows/publish-v4-snapshot.yml @@ -60,6 +60,14 @@ jobs: gradle-version: 8.5 arguments: assemble allTests --info --warning-mode all --build-cache -Porg.gradle.daemon=false + - name: Upload test reports + uses: actions/upload-artifact@v4 + if: ${{ always() }} + with: + name: test-reports-${{ matrix.os }} + path: '**/build/reports/tests' + retention-days: 7 + - name: Gradle publish snapshot uses: gradle/gradle-build-action@v3 with: diff --git a/.github/workflows/qodana_code_quality.yml b/.github/workflows/qodana_code_quality.yml index 663ed90f8..eb07fc4be 100644 --- a/.github/workflows/qodana_code_quality.yml +++ b/.github/workflows/qodana_code_quality.yml @@ -4,7 +4,7 @@ on: pull_request: push: branches: - - v3-dev + - v4-dev jobs: qodana: @@ -18,8 +18,29 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit fetch-depth: 0 # a full history is required for pull request analysis +# - uses: actions/setup-java@v4 +# with: +# distribution: 'zulu' +# java-version: 21 +# cache: 'gradle' +# +# - name: Run Kover for report +# uses: gradle/gradle-build-action@v3 +# with: +# gradle-version: 8.5 +# arguments: | +# koverHtmlReport +# --info +# --warning-mode all +# +# - name: Archive coverage data # Archive data for using by Qodana +# uses: actions/upload-artifact@v4 +# with: +# name: coverage-reports +# path: 'build/reports/kover/**' + - name: 'Qodana Scan' - uses: JetBrains/qodana-action@v2023.2 + uses: JetBrains/qodana-action@main env: QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} # read the steps about it below GITHUB_TOKEN: ${{ secrets.FORLIY_ACCESS_TOKEN }} diff --git a/.github/workflows/test-v4-branch.yml b/.github/workflows/test-v4-branch.yml index 709d01006..3a29ce064 100644 --- a/.github/workflows/test-v4-branch.yml +++ b/.github/workflows/test-v4-branch.yml @@ -39,5 +39,5 @@ jobs: if: ${{ always() }} with: name: test-reports-${{ matrix.os }} - path: build/test-reports + path: '**/build/reports/tests' retention-days: 7 diff --git a/.simbot/dokka-assets/logo-icon.svg b/.simbot/dokka-assets/logo-icon.svg index 03a946a33..4cb362534 100644 --- a/.simbot/dokka-assets/logo-icon.svg +++ b/.simbot/dokka-assets/logo-icon.svg @@ -1,1205 +1,78 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.simbot/jetbrains.png b/.simbot/jetbrains.png deleted file mode 100644 index ccceb9584..000000000 Binary files a/.simbot/jetbrains.png and /dev/null differ diff --git a/.simbot/jetbrains.svg b/.simbot/jetbrains.svg new file mode 100644 index 000000000..ba0ad3a24 --- /dev/null +++ b/.simbot/jetbrains.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.simbot/logo-dark.svg b/.simbot/logo-dark.svg new file mode 100644 index 000000000..4cb362534 --- /dev/null +++ b/.simbot/logo-dark.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.simbot/logo.png b/.simbot/logo.png deleted file mode 100644 index 931c4551c..000000000 Binary files a/.simbot/logo.png and /dev/null differ diff --git a/.simbot/logo.svg b/.simbot/logo.svg new file mode 100644 index 000000000..f61f12d47 --- /dev/null +++ b/.simbot/logo.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d489050f..47b46488e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# v4.0.0-beta2 + +> Release & Pull Notes: [v4.0.0-beta2](https://github.com/simple-robot/simpler-robot/releases/tag/v4.0.0-beta2) + +- feat(ID): 提供更多与 ID 相关的API ([`72719264`](https://github.com/simple-robot/simpler-robot/commit/72719264)) +- fix: 修复ContinuousSession测试问题 ([`3dd75162`](https://github.com/simple-robot/simpler-robot/commit/3dd75162)) +- fix: 修复ContinuousSession测试超时问题 ([`6086471d`](https://github.com/simple-robot/simpler-robot/commit/6086471d)) +- deps: 更新Kotlin到1.9.23 ([`a93c8d0f`](https://github.com/simple-robot/simpler-robot/commit/a93c8d0f)) +- module: 增加一个模块 common-ktor-inputfile 用来提供更简单的向 Ktor 提供表单文件信息的类型 `InputFile` ([`7df76e50`](https://github.com/simple-robot/simpler-robot/commit/7df76e50)) +- build(deps): bump dokka from 1.9.10 to 1.9.20 ([`12648620`](https://github.com/simple-robot/simpler-robot/commit/12648620)) + # v4.0.0-beta1 > Release & Pull Notes: [v4.0.0-beta1](https://github.com/simple-robot/simpler-robot/releases/tag/v4.0.0-beta1) diff --git a/README.md b/README.md index df79df3cb..254f7b0cd 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,16 @@
- logo -

- - Simple Robot - -

- + + + + + simbot logo + + +

+ - Simple Robot - +

+ ~ simbot v4 ~
@@ -47,32 +53,76 @@ ## 简介 -**`Simple Robot`** v4 是一个基于 **KMP** 的多平台 Bot 风格高性能异步事件调度框架(下文简称simbot), -提供统一的异步API和易用的风格设计,可以协助你更快速高效的编写 Bot 风格的事件调度应用。 -目前主要应用于对接各种类型的 Bot 应用平台/框架,并提供部分组件库实现。 - -**`simbot4`** 通过 [Kotlin](https://kotlinlang.org/) 语言开发、 -基于 [KMP](https://kotlinlang.org/docs/multiplatform.html) 支持多平台, -并兼容Java(**jdk11+**)等JVM平台语言, -且提供大量 Java 友好 API 和 Spring Boot starter,协助你快速开发。 - -本仓库为 simbot v3、v4 版本的仓库,提供 simbot 标准API的定义、simbot 核心库的实现以及 Spring Boot starter 的实现等核心、基础的内容。 - -> [!tip] -> 前往 [GitHub 组织首页](https://github.com/simple-robot/) 了解更多有关组件、文档、社群等相关信息! - -## 文档 - -simbot4应用手册: [simbot.forte.love][doc-homepage] +**`Simple Robot`** (v4) 是一个基于[Kotlin协程](https://github.com/Kotlin/kotlinx.coroutines) +的[Kotlin多平台](https://kotlinlang.org/docs/multiplatform.html) +**Bot风格**高性能异步事件调度框架(下文简称simbot), +异步高效、Java友好~ + +simbot提供统一的异步API和易用的风格设计,可以协助你更快速高效的编写**Bot风格**的事件调度应用。 +主要应用于对接各种类型的Bot应用平台/框架,并提供部分组件库实现。 + +simbot的**平台功能**由组件驱动,安装不同的组件库来获得不同的功能支持。 + +举个例子,在simbot中使用KOOK和QQ频道: + +```Kotlin +suspend fun main() { + val application = launchSimpleApplication { + // 安装KOOK和QQ频道组件库 + useKook() + useQQGuild() + } + + application.kookBots { + // ... 注册kook bot,并在此之后可处理到kook的相关事件 + register(...) { ... }.start() + } + application.qqGuildBots { + // ... 注册QQ频道bot,并在此之后可处理到QQ频道的相关事件 + register(...) { ... }.start() + } + + // 注册各种事件处理器 + application.listeners { + // 注册一个事件处理器 + // ChatChannelMessageEvent 是由simbot API定义的泛用类型,代表所有子频道消息事件 + // 其中就包括QQ频道的公域消息事件, 或者KOOK的频道消息事件 + listen { + println("context: $this") + println("context.event: $event") + + // 返回事件处理结果 + EventResult.empty() + } + + // 再注册一个事件处理器 + // 明确监听QQ频道的公域消息事件 + // 使用 process 不需要返回值 + process { + println("context: $this") + println("context.event: $event") + } + + // 再注册一个事件处理器 + // 明确监听KOOK的频道消息事件 + // 使用 process 不需要返回值 + process { + println("context: $this") + println("context.event: $event") + } + } +} +``` -> [!note] -> 也包括历届版本的文档地址引导 +## 文档与引导 -文档引导站&API Doc: [docs.simbot.forte.love](https://docs.simbot.forte.love) +- [组织首页](https://github.com/simple-robot/) 了解更多有关组件、文档、以及社群等相关信息! +- [社群](https://simbot.forte.love/communities.html) 文档中也有提供社群信息喔 +- [应用手册][doc-homepage] +- [文档引导站&API文档](https://docs.simbot.forte.love) -## V3 -切换分支到 [v3-dev](https://github.com/simple-robot/simpler-robot/tree/v3-dev) 来查看 simbot3 的开发分支。 +> 切换分支到 [v3-dev](https://github.com/simple-robot/simpler-robot/tree/v3-dev) 可查看 simbot v3 的历史分支。 ## 协助我们 为我们点亮一个 **✨star🌟** 便是能够给予我们继续走下去的最大动力与支持! @@ -96,7 +146,7 @@ simbot4应用手册: [simbot.forte.love][doc-homepage] ## 特别鸣谢 -jetbrains +jetbrains 感谢 [Jetbrains][jetbrains] 为团队提供的免费授权,也希望大家能够支持 [Jetbrains][jetbrains] 及其产品,支持正版。 diff --git a/README_en.md b/README_en.md index 4524150ea..03a2582c7 100644 --- a/README_en.md +++ b/README_en.md @@ -1,11 +1,17 @@
- logo -

- - Simple Robot - -

- - ~ simbot v4 ~ + + + + + simbot logo + + +

+ - Simple Robot - +

+ + ~ simbot v4 ~
@@ -113,7 +119,7 @@ Lighting up a **✨star🌟** for us is the greatest motivation and support we c ## Special thanks -jetbrains +jetbrains [jetbrains]: https://www.jetbrains.com/?from=simpler-robot diff --git a/benchmarks/v4-v3-core-benchmark/build.gradle.kts b/benchmarks/v4-v3-core-benchmark/build.gradle.kts new file mode 100644 index 000000000..1f6b3c281 --- /dev/null +++ b/benchmarks/v4-v3-core-benchmark/build.gradle.kts @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * Project https://github.com/simple-robot/simpler-robot + * Email ForteScarlet@163.com + * + * 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 . + * + */ + +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") +} + +setup(P.SimbotBenchmark) + +kotlin { + explicitApi() + applyDefaultHierarchyTemplate() + + configKotlinJvm(JVMConstants.KT_JVM_TARGET_VALUE) + + js(IR) { + configJs() + } + + applyTier1() + applyTier2() + applyTier3() + + sourceSets { + commonMain { + dependencies { + // jvm compile only + api(project(":simbot-api")) + api(project(":simbot-cores:simbot-core")) + } + } + commonTest { + dependencies { + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.test) + implementation(kotlin("test")) + implementation(libs.kotlinx.serialization.json) + } + } + } +} + diff --git a/build.gradle.kts b/build.gradle.kts index f4c70c6ec..6fe6fe92e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ * Project https://github.com/simple-robot/simpler-robot * Email ForteScarlet@163.com * - * This file is part of the Simple Robot Library. + * 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 @@ -30,6 +30,10 @@ plugins { id("io.gitlab.arturbosch.detekt") id("simbot.nexus-publish") id("simbot.changelog-generator") + + // https://www.jetbrains.com/help/qodana/code-coverage.html + // https://github.com/Kotlin/kotlinx-kover + id("org.jetbrains.kotlinx.kover") version "0.7.6" } setup(P.Simbot) @@ -48,6 +52,8 @@ repositories { mavenLocal() } +val root = project + subprojects { repositories { mavenCentral() @@ -79,7 +85,10 @@ subprojects { logger.info("Enable K2 for {}", this) } } + + applyKover(root) } + } fun Project.applyDetekt() { @@ -96,6 +105,18 @@ fun Project.applyDetekt() { // } } +fun Project.applyKover(rp: Project) { + val hasKt = (plugins.hasPlugin("org.jetbrains.kotlin.jvm") + || plugins.hasPlugin("org.jetbrains.kotlin.multiplatform")) + + if (hasKt) { + apply(plugin = "org.jetbrains.kotlinx.kover") + rp.dependencies { + kover(project(path)) + } + } +} + idea { module { isDownloadSources = true diff --git a/buildSrc/src/main/kotlin/P.kt b/buildSrc/src/main/kotlin/P.kt index d990699f7..e6d24e07c 100644 --- a/buildSrc/src/main/kotlin/P.kt +++ b/buildSrc/src/main/kotlin/P.kt @@ -57,6 +57,7 @@ sealed class P(override val group: String) : ProjectDetail() { const val GROUP_GRADLE = "love.forte.simbot.gradle" const val GROUP_QUANTCAT = "love.forte.simbot.quantcat" const val GROUP_EXTENSION = "love.forte.simbot.extension" + const val GROUP_BENCHMARK = "love.forte.simbot.benchmark" // const val COMPONENT_GROUP = "love.forte.simbot.component" const val DESCRIPTION = "Simple Robot,一个通用的bot风格事件调度框架,以灵活的统一标准来编写bot应用。" @@ -78,12 +79,13 @@ sealed class P(override val group: String) : ProjectDetail() { object SimbotGradle : P(GROUP_GRADLE) object SimbotQuantcat : P(GROUP_QUANTCAT) object SimbotExtension : P(GROUP_EXTENSION) + object SimbotBenchmark : P(GROUP_BENCHMARK) final override val version: Version val versionWithoutSnapshot: Version init { - val mainVersion = version(4, 0, 0) - version("beta1") + val mainVersion = version(4, 0, 0) - version("beta2") fun initVersionWithoutSnapshot(status: Version?): Version = if (status == null) { mainVersion diff --git a/gradle.properties b/gradle.properties index 266460df6..6a98d39ae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ # Project https://github.com/simple-robot/simpler-robot # Email ForteScarlet@163.com # -# This file is part of the Simple Robot Library. +# 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 @@ -47,5 +47,7 @@ org.gradle.caching=true #kotlin.experimental.tryK2=true -#http.proxyHost=localhost -#http.proxyPort=7790 +#systemProp.http.proxyHost=localhost +#systemProp.http.proxyPort=7790 +#systemProp.https.proxyHost=localhost +#systemProp.https.proxyPort=7790 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8abd6b06f..ff1e35234 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -kotlin = "1.9.22" -dokka = "1.9.10" +kotlin = "1.9.23" +dokka = "1.9.20" kotlinx-coroutines = "1.8.0" kotlinx-serialization = "1.6.3" spring-boot-v2 = "2.7.18" @@ -8,9 +8,11 @@ spring-boot-v3 = "3.2.1" openjdk-jmh = "1.36" ktor = "2.3.8" slf4j = "2.0.12" -ksp = "1.9.22-1.0.17" +ksp = "1.9.23-1.0.19" # https://square.github.io/kotlinpoet/ kotlinPoet = "1.16.0" +# https://github.com/Kotlin/kotlinx-benchmark +kotlinxBenchmark = "0.4.10" reactor = "3.6.2" # simbots suspendTransform = "0.7.0-beta1" @@ -91,6 +93,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" } @@ -99,6 +102,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 @@ -126,8 +130,13 @@ gradle-common-publication = { group = "love.forte.gradle.common", name = "gradle ## https://mockk.io/ mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } +# benchmark +kotlinx-benchmark-runtime = { group = "org.jetbrains.kotlinx", name = "kotlinx-benchmark-runtime", version.ref = "kotlinxBenchmark" } + [plugins] ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +kotlinxBenchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "kotlinxBenchmark" } +kotlinAllOpen = { id = "org.jetbrains.kotlin.plugin.allopen", version.ref = "kotlin" } [bundles] gradle-common = ["gradle-common-core", "gradle-common-multiplatform", "gradle-common-publication"] diff --git a/settings.gradle.kts b/settings.gradle.kts index 58b957337..77725d77a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,10 +33,13 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" } +// benchmarks +// include(":benchmarks:v4-v3-core-benchmark") + // internals include(":internal-processors:interface-uml-processor") - +// gradle include(":simbot-gradles:simbot-gradle-suspendtransforms") include(":simbot-commons:simbot-common-annotations") @@ -44,6 +47,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") diff --git a/simbot-commons/simbot-common-core/src/commonMain/kotlin/love/forte/simbot/common/id/ID.kt b/simbot-commons/simbot-common-core/src/commonMain/kotlin/love/forte/simbot/common/id/ID.kt index 3de51baaa..bd5dcef87 100644 --- a/simbot-commons/simbot-common-core/src/commonMain/kotlin/love/forte/simbot/common/id/ID.kt +++ b/simbot-commons/simbot-common-core/src/commonMain/kotlin/love/forte/simbot/common/id/ID.kt @@ -4,7 +4,7 @@ * Project https://github.com/simple-robot/simpler-robot * Email ForteScarlet@163.com * - * This file is part of the Simple Robot Library. + * 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 @@ -21,20 +21,26 @@ * */ +@file:JvmName("Identifies") +@file:JvmMultifileClass + package love.forte.simbot.common.id -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable +import kotlinx.serialization.* import kotlinx.serialization.builtins.serializer import kotlinx.serialization.descriptors.* -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.encoding.* +import love.forte.simbot.common.id.IntID.Companion.ID +import love.forte.simbot.common.id.LongID.Companion.ID import love.forte.simbot.common.id.StringID.Companion.ID +import love.forte.simbot.common.id.UIntID.Companion.ID +import love.forte.simbot.common.id.ULongID.Companion.ID import love.forte.simbot.common.id.UUID.Companion.UUID import kotlin.concurrent.Volatile import kotlin.experimental.and import kotlin.experimental.or import kotlin.js.JsName +import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName import kotlin.jvm.JvmOverloads import kotlin.jvm.JvmStatic @@ -55,6 +61,43 @@ import kotlin.random.Random * * 它们可以粗略的被归类为字符串类型( [UUID] 的字面值表现为字符串)和数字类型。 * + * ## 构造 + * + * 在 Kotlin 中,直接使用各类型的伴生对象提供的扩展属性构建即可: + * + * ```Kotlin + * 100.ID + * 100L.ID + * 100u.ID + * "100".ID + * UUID.random() + * ``` + * + * 在 Java 中,可以使用 `Identifies` 中提供的构造方法,它们通常被命名为 `of` 或以 `of` 为开头: + * + * ```Java + * Identifies.of(100); + * Identifies.of(100L); + * Identifies.of("100"); + * Identifies.ofULong(100L); + * Identifies.ofULong("100"); + * Identifies.uuid(); + * ``` + * + * 也可以使用具体类型的伴生对象所提供的静态API: + * + * ```Java + * IntID.valueOf(100); + * LongID.valueOf(100L); + * UIntID.valueOf(100); + * ULongID.valueOf(100L); + * StringID.valueOf("100"); + * UUID.random(); + * ``` + * + * 这些伴生对象提供的静态API与 `Identifies` 中的内容相比缺少了一些辅助性的API, + * 例如使用字符串构建无符号ID `ULongID`。 + * * ## 序列化 * * 所有**具体的**ID类型都是可序列化的,它们都会通过 Kotlinx serialization @@ -506,6 +549,59 @@ public class UUID private constructor( } } + /** + * 直接将 [UUID] 作为一个具有两个 [Long] 的结构体进行序列化的序列化器。 + */ + public object StructureSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UUID") { + element("mostSignificantBits", Long.serializer().descriptor) + element("leastSignificantBits", Long.serializer().descriptor) + } + + @OptIn(ExperimentalSerializationApi::class) + override fun deserialize(decoder: Decoder): UUID { + return decoder.decodeStructure(descriptor) { + var m = false + var l = false + var mostSignificantBits: Long = 0 + var leastSignificantBits: Long = 0 + + while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> { + m = true + mostSignificantBits = decodeLongElement(descriptor, index) + } + + 1 -> { + l = true + leastSignificantBits = decodeLongElement(descriptor, index) + } + + CompositeDecoder.DECODE_DONE -> break + else -> error("Unexpected index: $index") + } + } + if (!m || !l) { + val properties = buildList { + if (!m) add("mostSignificantBits") + if (!l) add("leastSignificantBits") + } + throw MissingFieldException(properties, "love.forte.simbot.common.id.UUID") + } + + UUID(mostSignificantBits, leastSignificantBits) + } + } + + override fun serialize(encoder: Encoder, value: UUID) { + encoder.encodeStructure(descriptor) { + encodeLongElement(descriptor, 0, value.mostSignificantBits) + encodeLongElement(descriptor, 1, value.leastSignificantBits) + } + } + } + } /** @@ -549,11 +645,23 @@ public sealed class NumericalID : ID() { */ public abstract fun toLong(): Long + /** + * 将数字值转化为 [ULong]。 + */ + public open fun toULong(): ULong = + toLong().toULong() + /** * 将数字值转化为 [Int]。类似于 [Number.toInt] */ public abstract fun toInt(): Int + /** + * 将数字值转化为 [UInt]。 + */ + public open fun toUInt(): UInt = + toInt().toUInt() + /** * 将数字值转化为 [Short]。类似于 [Number.toShort] */ @@ -587,7 +695,7 @@ public sealed class NumericalID : ID() { * - [IntID] 32位有符号整型 * - [LongID] 64位有符号整型 * - * 如果想要作为ID的数字已经超过64位有符号 ([LongID]) 锁能表示的最大数字, + * 如果想要作为ID的数字已经超过64位有符号 ([LongID]) 所能表示的最大数字, * 那么建议使用其他类型来表示,例如 [ULongID] 或 [StringID] * * @@ -602,7 +710,7 @@ public sealed class SignedNumericID : NumericalID() * - [UIntID] 32位无符号整型 * - [ULongID] 64位无符号整型 * - * 如果想要作为ID的数字已经超过64位无符号 ([ULongID]) 锁能表示的最大数字, + * 如果想要作为ID的数字已经超过64位无符号 ([ULongID]) 所能表示的最大数字, * 那么建议使用其他类型来表示,例如 [StringID] * * 在 Java 中对无符号数字的操作需要有些注意的地方。 @@ -686,6 +794,7 @@ public class UIntID private constructor(@get:JvmName("getValue") public val valu override fun toFloat(): Float = value.toFloat() override fun toLong(): Long = value.toLong() override fun toInt(): Int = value.toInt() + override fun toUInt(): UInt = value override fun toShort(): Short = value.toShort() override fun toByte(): Byte = value.toByte() override fun toString(): String = value.toString() @@ -799,6 +908,7 @@ public class ULongID private constructor(@get:JvmName("getValue") public val val override fun toDouble(): Double = value.toDouble() override fun toFloat(): Float = value.toFloat() override fun toLong(): Long = value.toLong() + override fun toULong(): ULong = value override fun toInt(): Int = value.toInt() override fun toShort(): Short = value.toShort() override fun toByte(): Byte = value.toByte() @@ -914,3 +1024,119 @@ private inline fun T.idExactEq(other: Any?, orElse: T.(T) -> Bo */ public inline val ID.literal: String get() = toString() + +/** + * 尝试将 [this] 转化为 [Int]。 + * 如果为 [NumericalID] 则直接使用 [NumericalID.toInt], + * 否则使用 [notNumerical] 转化。默认会尝试使用 [literal.toIntOrNull()][String.toIntOrNull]。 + */ +@JvmOverloads +@JvmName("toIntOrNull") +public inline fun ID.toIntOrNull(notNumerical: ID.() -> Int? = { literal.toIntOrNull() }): Int? = + (this as? NumericalID)?.toInt() ?: notNumerical() + +/** + * 尝试将 [this] 转化为 [Int]。 + * 如果为 [NumericalID] 则直接使用 [NumericalID.toInt], + * 否则使用 [notNumerical] 转化。默认会尝试使用 [literal.toInt()][String.toInt]。 + */ +@JvmOverloads +@JvmName("toInt") +public inline fun ID.toInt(notNumerical: ID.() -> Int = { literal.toInt() }): Int = + (this as? NumericalID)?.toInt() ?: notNumerical() + +/** + * 尝试将 [this] 转化为 [Long]。 + * 如果为 [NumericalID] 则直接使用 [NumericalID.toLong], + * 否则使用 [notNumerical] 转化。默认会尝试使用 [literal.toLongOrNull()][String.toLongOrNull]。 + */ +@JvmOverloads +@JvmName("toLongOrNull") +public inline fun ID.toLongOrNull(notNumerical: ID.() -> Long? = { literal.toLongOrNull() }): Long? = + (this as? NumericalID)?.toLong() ?: notNumerical() + +/** + * 尝试将 [this] 转化为 [Long]。 + * 如果为 [NumericalID] 则直接使用 [NumericalID.toLong], + * 否则使用 [notNumerical] 转化。默认会尝试使用 [literal.toLong()][String.toLong]。 + */ +@JvmOverloads +@JvmName("toLong") +public inline fun ID.toLong(notNumerical: ID.() -> Long = { literal.toLong() }): Long = + (this as? NumericalID)?.toLong() ?: notNumerical() + +/** + * 尝试将 [this] 转化为 [UInt]。 + * 如果为 [NumericalID] 则直接使用 [NumericalID.toInt].[toUInt][Int.toUInt], + * 否则使用 [notNumerical] 转化。默认会尝试使用 [literal.toUIntOrNull()][String.toIntOrNull]。 + */ +@JvmOverloads +@JvmName("toUIntOrNull") +public inline fun ID.toUIntOrNull(notNumerical: ID.() -> UInt? = { literal.toUIntOrNull() }): UInt? = + (this as? NumericalID)?.toInt()?.toUInt() ?: notNumerical() + +/** + * 尝试将 [this] 转化为 [UInt]。 + * 如果为 [NumericalID] 则直接使用 [NumericalID.toInt].[toUInt][Int.toUInt], + * 否则使用 [notNumerical] 转化。默认会尝试使用 [literal.toUInt()][String.toInt]。 + */ +@JvmOverloads +@JvmName("toUInt") +public inline fun ID.toUInt(notNumerical: ID.() -> UInt = { literal.toUInt() }): UInt = + (this as? NumericalID)?.toInt()?.toUInt() ?: notNumerical() + +/** + * 尝试将 [this] 转化为 [ULong]。 + * 如果为 [NumericalID] 则直接使用 [NumericalID.toLong].[toULong][Long.toULong], + * 否则使用 [notNumerical] 转化。默认会尝试使用 [literal.toULongOrNull()][String.toLongOrNull]。 + */ +@JvmOverloads +@JvmName("toULongOrNull") +public inline fun ID.toULongOrNull(notNumerical: ID.() -> ULong? = { literal.toULongOrNull() }): ULong? = + (this as? NumericalID)?.toLong()?.toULong() ?: notNumerical() + +/** + * 尝试将 [this] 转化为 [ULong]。 + * 如果为 [NumericalID] 则直接使用 [NumericalID.toLong].[toULong][Long.toULong], + * 否则使用 [notNumerical] 转化。默认会尝试使用 [literal.toULong()][String.toLong]。 + */ +@JvmOverloads +@JvmName("toULong") +public inline fun ID.toULong(notNumerical: ID.() -> ULong = { literal.toULong() }): ULong = + (this as? NumericalID)?.toLong()?.toULong() ?: notNumerical() + +/** + * 尝试将 [this] 转为 [IntID] 类型。 + * 如果不是数字ID,则会使用 [notNumerical] 获取结果。默认使用 [String.toInt]。 + */ +@JvmOverloads +@JvmName("toIntID") +public inline fun ID.toIntID(notNumerical: ID.() -> IntID = { literal.toInt().ID }): IntID = + this as? IntID ?: (this as? NumericalID)?.toInt()?.ID ?: notNumerical() + +/** + * 尝试将 [this] 转为 [UIntID] 类型。 + * 如果不是数字ID,则会使用 [notNumerical] 获取结果。默认使用 [String.toUInt]。 + */ +@JvmOverloads +@JvmName("toUIntID") +public inline fun ID.toUIntID(notNumerical: ID.() -> UIntID = { literal.toUInt().ID }): UIntID = + this as? UIntID ?: (this as? NumericalID)?.toUInt()?.ID ?: notNumerical() + +/** + * 尝试将 [this] 转为 [LongID] 类型。 + * 如果不是数字ID,则会使用 [notNumerical] 获取结果。默认使用 [String.toLong]。 + */ +@JvmOverloads +@JvmName("toLongID") +public inline fun ID.toLongID(notNumerical: ID.() -> LongID = { literal.toLong().ID }): LongID = + this as? LongID ?: (this as? NumericalID)?.toLong()?.ID ?: notNumerical() + +/** + * 尝试将 [this] 转为 [ULongID] 类型。 + * 如果不是数字ID,则会使用 [notNumerical] 获取结果。默认使用 [String.toULong]。 + */ +@JvmOverloads +@JvmName("toULongID") +public inline fun ID.toULongID(notNumerical: ID.() -> ULongID = { literal.toULong().ID }): ULongID = + this as? ULongID ?: (this as? NumericalID)?.toULong()?.ID ?: notNumerical() diff --git a/simbot-commons/simbot-common-core/src/commonMain/kotlin/love/forte/simbot/common/id/IDValueOf.kt b/simbot-commons/simbot-common-core/src/commonMain/kotlin/love/forte/simbot/common/id/IDValueOf.kt new file mode 100644 index 000000000..8ed4841f3 --- /dev/null +++ b/simbot-commons/simbot-common-core/src/commonMain/kotlin/love/forte/simbot/common/id/IDValueOf.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * Project https://github.com/simple-robot/simpler-robot + * Email ForteScarlet@163.com + * + * 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 . + * + */ + +@file:JvmName("Identifies") +@file:JvmMultifileClass + +package love.forte.simbot.common.id + +import love.forte.simbot.common.id.ULongID.Companion.ID +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName + + +/** + * 构建一个 [ULongID]. + * @see ULong.ID + */ +@JvmName("ofULong") +public fun uLongIDOf(value: ULong): ULongID = value.ID + diff --git a/simbot-commons/simbot-common-core/src/commonTest/kotlin/love/forte/simbot/common/id/IDTests.kt b/simbot-commons/simbot-common-core/src/commonTest/kotlin/love/forte/simbot/common/id/IDTests.kt index e830f869f..8c2c3f6fd 100644 --- a/simbot-commons/simbot-common-core/src/commonTest/kotlin/love/forte/simbot/common/id/IDTests.kt +++ b/simbot-commons/simbot-common-core/src/commonTest/kotlin/love/forte/simbot/common/id/IDTests.kt @@ -4,7 +4,7 @@ * Project https://github.com/simple-robot/simpler-robot * Email ForteScarlet@163.com * - * This file is part of the Simple Robot Library. + * 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 @@ -23,6 +23,8 @@ package love.forte.simbot.common.id +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.MissingFieldException import kotlinx.serialization.json.Json import love.forte.simbot.common.id.IntID.Companion.ID import love.forte.simbot.common.id.LongID.Companion.ID @@ -33,9 +35,7 @@ import love.forte.simbot.common.id.UUID.Companion.UUID import kotlin.random.Random import kotlin.random.nextUInt import kotlin.random.nextULong -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue +import kotlin.test.* class IDTests { @@ -72,7 +72,57 @@ class IDTests { assertEquals(json.encodeToString(LongID.serializer(), l.ID), l.toString()) assertEquals(json.encodeToString(UIntID.serializer(), ui.ID), ui.toString()) assertEquals(json.encodeToString(ULongID.serializer(), ul.ID), ul.toString()) + } + @Test + fun numericalIDTransformTests() { + with(1.ID) { + assertEquals(1L, toLong()) + assertEquals(1L.ID, toLongID()) + assertSame(this, toIntID()) + } + with(1L.ID) { + assertEquals(1, toInt()) + assertSame(this, toLongID()) + assertEquals(1.ID, toIntID()) + } + with("1".ID) { + assertEquals(1, toInt()) + assertEquals(1L, toLong()) + assertEquals(1L.ID, toLongID()) + assertEquals(1.ID, toIntID()) + } + with(1u.ID) { + assertEquals(1u, toUInt()) + val ul: ULong = 1u + assertEquals(ul.ID, toULongID()) + assertEquals(1.ID, toIntID()) + assertSame(this, toUIntID()) + } + with(uLongIDOf(1u)) { + assertEquals(1, toInt()) + assertEquals(1L.ID, toLongID()) + assertSame(this, toULongID()) + assertEquals(1.ID, toIntID()) + } } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun uuidSerializerTests() { + val mv = 111L + val lv = 222L + val uuid = UUID.from(mv, lv) + val jsonString = Json.encodeToString(UUID.StructureSerializer, uuid) + assertEquals("""{"mostSignificantBits":$mv,"leastSignificantBits":$lv}""", jsonString) + val decodedUUID = Json.decodeFromString(UUID.StructureSerializer, """{"mostSignificantBits":$mv,"leastSignificantBits":$lv}""") + assertEquals(uuid, decodedUUID) + + val err = assertFails { + Json.decodeFromString(UUID.StructureSerializer, """{"leastSignificantBits":$lv}""") + } + assertIs(err) + err.printStackTrace() + } } diff --git a/simbot-commons/simbot-common-core/src/jvmMain/kotlin/love/forte/simbot/common/id/ID.jvm.kt b/simbot-commons/simbot-common-core/src/jvmMain/kotlin/love/forte/simbot/common/id/ID.jvm.kt new file mode 100644 index 000000000..2418d0ddd --- /dev/null +++ b/simbot-commons/simbot-common-core/src/jvmMain/kotlin/love/forte/simbot/common/id/ID.jvm.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2023-2024. ForteScarlet. + * + * Project https://github.com/simple-robot/simpler-robot + * Email ForteScarlet@163.com + * + * 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 . + * + */ + +@file:JvmName("Identifies") +@file:JvmMultifileClass + +package love.forte.simbot.common.id + +import love.forte.simbot.common.id.IntID.Companion.ID +import love.forte.simbot.common.id.LongID.Companion.ID +import love.forte.simbot.common.id.StringID.Companion.ID +import love.forte.simbot.common.id.UIntID.Companion.ID +import love.forte.simbot.common.id.ULongID.Companion.ID +import love.forte.simbot.common.id.UUID.Companion.UUID +import kotlin.random.Random +import kotlin.random.asKotlinRandom + +/** + * 一个设计为仅供 Java 用户使用的API + */ +@Retention(AnnotationRetention.BINARY) +@RequiresOptIn( + message = "一个设计为仅供 Java 用户使用的ID API", + level = RequiresOptIn.Level.ERROR +) +@MustBeDocumented +public annotation class ID4J + +/** + * + * @see Long.ID + */ +@JvmName("of") +@ID4J +public fun longIDOf(value: Long): LongID = value.ID + +/** + * @throws NumberFormatException if the string is not a valid representation of a number. + * @see ULong.ID + */ +@JvmName("ofULong") +@ID4J +public fun uLongIDOf(value: String): ULongID = value.toULong().ID + +/** + * + * @see IntID.ID + */ +@JvmName("of") +@ID4J +public fun intIDOf(value: Int): IntID = value.ID + +/** + * + * @see UInt.ID + */ +@JvmName("ofUInt") +@ID4J +public fun uIntIDOf(value: UInt): UIntID = value.ID + +/** + * + * @throws NumberFormatException if the string is not a valid representation of a number. + * @see UInt.ID + */ +@JvmName("ofUInt") +@ID4J +public fun uIntIDOf(value: String): UIntID = value.toUInt().ID + +/** + * + * @see String.ID + */ +@JvmName("of") +@ID4J +public fun stringIDOf(value: String): StringID = value.ID + +/** + * + * @see CharSequence.ID + */ +@JvmName("of") +@ID4J +public fun stringIDOf(value: CharSequence): StringID = value.ID + +/** + * + * @see UUID.random + */ +@JvmName("uuid") +@JvmOverloads +@ID4J +public fun uuidOf(random: Random? = null): UUID = + if (random == null) UUID.random() else UUID.random(random) + +/** + * @see java.util.UUID.simbotUUID + */ +@JvmName("uuid") +@ID4J +public fun uuidOf(javaUuid: java.util.UUID): UUID = javaUuid.simbotUUID + +/** + * @see java.util.UUID.simbotUUID + */ +@JvmName("uuid") +@ID4J +public fun uuidOf(javaRandom: java.util.Random?): UUID = + uuidOf(javaRandom?.asKotlinRandom()) + diff --git a/simbot-commons/simbot-common-core/src/jvmMain/kotlin/love/forte/simbot/common/id/Identifies.kt b/simbot-commons/simbot-common-core/src/jvmMain/kotlin/love/forte/simbot/common/id/Identifies.kt deleted file mode 100644 index 52d830ab3..000000000 --- a/simbot-commons/simbot-common-core/src/jvmMain/kotlin/love/forte/simbot/common/id/Identifies.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2023-2024. ForteScarlet. - * - * Project https://github.com/simple-robot/simpler-robot - * Email ForteScarlet@163.com - * - * This file is part of the Simple Robot Library. - * - * 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 . - * - */ -package love.forte.simbot.common.id - -import love.forte.simbot.common.id.IntID.Companion.ID -import love.forte.simbot.common.id.LongID.Companion.ID -import love.forte.simbot.common.id.StringID.Companion.ID -import love.forte.simbot.common.id.UIntID.Companion.ID -import love.forte.simbot.common.id.ULongID.Companion.ID -import java.util.* -import kotlin.random.asKotlinRandom - -/** - * 提供给 Java 的 [ID] 辅助工具类。 - * - * Kotlin 可以选择直接使用各类型的伴生对象中的扩展函数,例如 [LongID.ID]。 - */ -public object Identifies { - - /** - * @see Long.ID - */ - @JvmStatic - public fun of(value: Long): LongID = value.ID - - /** - * @see ULong.ID - */ - @JvmStatic - @JvmName("ofULong") - public fun ofULong(value: ULong): ULongID = value.ID - - /** - * @throws NumberFormatException if the string is not a valid representation of a number. - * @see ULong.ID - */ - @JvmStatic - @JvmName("ofULong") - public fun ofULong(value: String): ULongID = value.toULong().ID - - /** - * @see IntID.ID - */ - @JvmStatic - public fun of(value: Int): IntID = value.ID - - /** - * @see UInt.ID - */ - @JvmStatic - @JvmName("ofUInt") - public fun ofUInt(value: UInt): UIntID = value.ID - - /** - * @throws NumberFormatException if the string is not a valid representation of a number. - * @see UInt.ID - */ - @JvmStatic - @JvmName("ofUInt") - public fun ofUInt(value: String): UIntID = value.toUInt().ID - - /** - * @see String.ID - */ - @JvmStatic - public fun of(value: String): StringID = value.ID - - /** - * @see CharSequence.ID - */ - @JvmStatic - public fun of(value: CharSequence): StringID = value.ID - - /** - * @see UUID.random - */ - @JvmStatic - @JvmOverloads - public fun uuid(random: Random? = null): UUID = - if (random == null) UUID.random() else UUID.random(random.asKotlinRandom()) - - /** - * @see UUID.random - */ - @JvmStatic - public fun uuid(random: kotlin.random.Random): UUID = UUID.random(random) - - /** - * @see java.util.UUID.simbotUUID - */ - @JvmStatic - public fun uuid(javaUuid: java.util.UUID): UUID = javaUuid.simbotUUID -} - diff --git a/simbot-commons/simbot-common-ktor-inputfile/README.md b/simbot-commons/simbot-common-ktor-inputfile/README.md new file mode 100644 index 000000000..f07826af4 --- /dev/null +++ b/simbot-commons/simbot-common-ktor-inputfile/README.md @@ -0,0 +1,7 @@ +# Module simbot-common-ktor-inputfile + +一个适用于 Ktor Client,可更简单的将一个本地文件作为 `Ktor` 的 `MultiPartFormDataContent` 的一部分。 +提供一个类型 `InputFile`,以及相关的扩展方法、工厂方法, +比如在 JVM 下可以通过 `File` 和 `Path` 构建它。 + +在 JVM 中对 `ktor-client-core` 的依赖是**仅编译**的。 diff --git a/simbot-commons/simbot-common-ktor-inputfile/build.gradle.kts b/simbot-commons/simbot-common-ktor-inputfile/build.gradle.kts new file mode 100644 index 000000000..95895f179 --- /dev/null +++ b/simbot-commons/simbot-common-ktor-inputfile/build.gradle.kts @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * Project https://github.com/simple-robot/simpler-robot + * Email ForteScarlet@163.com + * + * 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 . + * + */ + +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) + } + } +} + diff --git a/simbot-commons/simbot-common-ktor-inputfile/src/commonMain/kotlin/love/forte/simbot/common/ktor/inputfile/InputFile.kt b/simbot-commons/simbot-common-ktor-inputfile/src/commonMain/kotlin/love/forte/simbot/common/ktor/inputfile/InputFile.kt new file mode 100644 index 000000000..6839e41e5 --- /dev/null +++ b/simbot-commons/simbot-common-ktor-inputfile/src/commonMain/kotlin/love/forte/simbot/common/ktor/inputfile/InputFile.kt @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * Project https://github.com/simple-robot/simpler-robot + * Email ForteScarlet@163.com + * + * 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 . + * + */ +@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 + +/** + * 一个可以向 Ktor 表单请求中输入的文件信息。 + * + * 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) + } +} diff --git a/simbot-commons/simbot-common-ktor-inputfile/src/commonTest/kotlin/love/forte/simbot/common/ktor/inputfile/InputFileTests.kt b/simbot-commons/simbot-common-ktor-inputfile/src/commonTest/kotlin/love/forte/simbot/common/ktor/inputfile/InputFileTests.kt new file mode 100644 index 000000000..b7439d0c1 --- /dev/null +++ b/simbot-commons/simbot-common-ktor-inputfile/src/commonTest/kotlin/love/forte/simbot/common/ktor/inputfile/InputFileTests.kt @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * Project https://github.com/simple-robot/simpler-robot + * Email ForteScarlet@163.com + * + * 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 . + * + */ + +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(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))) + } + +} diff --git a/simbot-commons/simbot-common-ktor-inputfile/src/jvmMain/java/module-info.java b/simbot-commons/simbot-common-ktor-inputfile/src/jvmMain/java/module-info.java new file mode 100644 index 000000000..8f5035fdc --- /dev/null +++ b/simbot-commons/simbot-common-ktor-inputfile/src/jvmMain/java/module-info.java @@ -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; +} diff --git a/simbot-commons/simbot-common-ktor-inputfile/src/jvmMain/kotlin/love/forte/simbot/common/ktor/inputfile/InputFile.jvm.kt b/simbot-commons/simbot-common-ktor-inputfile/src/jvmMain/kotlin/love/forte/simbot/common/ktor/inputfile/InputFile.jvm.kt new file mode 100644 index 000000000..83604687c --- /dev/null +++ b/simbot-commons/simbot-common-ktor-inputfile/src/jvmMain/kotlin/love/forte/simbot/common/ktor/inputfile/InputFile.jvm.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * Project https://github.com/simple-robot/simpler-robot + * Email ForteScarlet@163.com + * + * 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 . + * + */ + +@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.util.cio.* +import io.ktor.utils.io.* +import io.ktor.utils.io.core.* +import io.ktor.utils.io.nio.* +import java.io.File +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.fileSize + +/** + * Create an instance of [InputFile] from [File]. + * + * This [InputFile] Will use [File.length] in [InputFile.includeTo], [InputFile.toFormPart], + * and use [File.readChannel] in [ChannelProvider]. + * + */ +@JvmName("of") +@JvmOverloads +public fun InputFile(file: File, defaultHeaders: Headers = Headers.Empty): InputFile = + FileInputFile(file, defaultHeaders) + +private class FileInputFile(private val file: File, private val defaultHeaders: Headers) : InputFile { + override fun includeTo(key: String, headers: Headers, formBuilder: FormBuilder) { + formBuilder.append(key, pathChannelProvider(), defaultHeaders + headers) + } + + override fun toFormPart(key: String, headers: Headers): FormPart<*> = + FormPart(key, pathChannelProvider(), defaultHeaders + headers) + + private fun pathChannelProvider() = ChannelProvider(file.length()) { file.readChannel() } +} + +/** + * Create an instance of [InputFile] from [Path]. + * + * This [InputFile] will use [Path.fileSize] in [InputFile.includeTo], [InputFile.toFormPart], + * and use [Files.newByteChannel], [asInput] in [ChannelProvider]. + * + */ +@JvmName("of") +@JvmOverloads +public fun InputFile(path: Path, defaultHeaders: Headers = Headers.Empty): InputFile = + PathInputFile(path, defaultHeaders) + +private class PathInputFile(private val path: Path, private val defaultHeaders: Headers) : InputFile { + + @Throws(IOException::class) + override fun includeTo(key: String, headers: Headers, formBuilder: FormBuilder) { + formBuilder.append(key, pathInput(), defaultHeaders + headers) + } + + @Throws(IOException::class) + override fun toFormPart(key: String, headers: Headers): FormPart<*> = + FormPart(key, pathInput(), defaultHeaders + headers) + + @Throws(IOException::class) + private fun pathInput(): InputProvider = InputProvider(path.fileSize()) { Files.newByteChannel(path).asInput() } +} diff --git a/simbot-commons/simbot-common-ktor-inputfile/src/jvmTest/kotlin/love/forte/simbot/common/ktor/inputfile/InputFileTests.jvm.kt b/simbot-commons/simbot-common-ktor-inputfile/src/jvmTest/kotlin/love/forte/simbot/common/ktor/inputfile/InputFileTests.jvm.kt new file mode 100644 index 000000000..c6b0a4d56 --- /dev/null +++ b/simbot-commons/simbot-common-ktor-inputfile/src/jvmTest/kotlin/love/forte/simbot/common/ktor/inputfile/InputFileTests.jvm.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * Project https://github.com/simple-robot/simpler-robot + * Email ForteScarlet@163.com + * + * 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 . + * + */ + +package love.forte.simbot.common.ktor.inputfile + +import io.ktor.utils.io.core.* +import kotlinx.coroutines.test.runTest +import love.forte.simbot.common.ktor.inputfile.InputFileTests.Companion.assertInputFileByMockClient +import java.io.File +import kotlin.test.Test + + +/** + * + * @author ForteScarlet + */ +class InputFileJvmTests { + + @Test + fun fileInputFile() = runTest { + val realText = "Hello, World" + val file = File.createTempFile("InputFileJvmTests-PREFIX", "InputFileJvmTests-SUFFIX") + .also { it.deleteOnExit() } + file.appendText(realText) + + val bytes = realText.toByteArray() + + assertInputFileByMockClient(bytes.size, realText, InputFile(file)) + } + + @Test + fun pathInputFile() = runTest { + val realText = "Hello, World" + val file = File.createTempFile("InputFileJvmTests-PREFIX", "InputFileJvmTests-SUFFIX") + .also { it.deleteOnExit() } + file.appendText(realText) + val bytes = realText.toByteArray() + + assertInputFileByMockClient(bytes.size, realText, InputFile(file.toPath())) + } + +} diff --git a/simbot-cores/simbot-core/src/jvmTest/java/JAppTests.java b/simbot-cores/simbot-core/src/jvmTest/java/JAppTests.java deleted file mode 100644 index a1ec88afb..000000000 --- a/simbot-cores/simbot-core/src/jvmTest/java/JAppTests.java +++ /dev/null @@ -1,42 +0,0 @@ -///* -// * Copyright (c) 2024. ForteScarlet. -// * -// * Project https://github.com/simple-robot/simpler-robot -// * Email ForteScarlet@163.com -// * -// * This file is part of the Simple Robot Library. -// * -// * 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 . -// * -// */ -// -//import love.forte.simbot.application.Applications; -//import love.forte.simbot.core.application.Simple; -//import org.junit.jupiter.api.Test; -// -///** -// * @author ForteScarlet -// */ -//public class JAppTests { -// -// @Test -// public void app() { -// Applications.launchApplicationAsync(Simple.INSTANCE, configurer -> { -// configurer.config(c -> { -// -// }); -// }).asFuture().join(); -// } -// -//} diff --git a/simbot-extensions/simbot-extension-continuous-session/src/commonMain/kotlin/love/forte/simbot/extension/continuous/session/AbstractContinuousSessionContext.kt b/simbot-extensions/simbot-extension-continuous-session/src/commonMain/kotlin/love/forte/simbot/extension/continuous/session/AbstractContinuousSessionContext.kt index 9786e5873..5a50e8e24 100644 --- a/simbot-extensions/simbot-extension-continuous-session/src/commonMain/kotlin/love/forte/simbot/extension/continuous/session/AbstractContinuousSessionContext.kt +++ b/simbot-extensions/simbot-extension-continuous-session/src/commonMain/kotlin/love/forte/simbot/extension/continuous/session/AbstractContinuousSessionContext.kt @@ -27,6 +27,7 @@ package love.forte.simbot.extension.continuous.session import kotlinx.coroutines.* +import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ClosedReceiveChannelException import kotlinx.coroutines.channels.ClosedSendChannelException @@ -65,7 +66,7 @@ public abstract class AbstractContinuousSessionContext(coroutineContext: C return when (strategy) { FAILURE -> { sessions.computeValue(key) { k, old -> - if (old != null) throw IllegalStateException("Session with key $key already exists") + if (old != null && old.isActive) throw IllegalStateException("Session with key $key already exists") computeSession(k, inSession) }!! @@ -103,15 +104,21 @@ private class SimpleContinuousSessionContext(coroutineContext: CoroutineCo private val parentJob = coroutineContext[Job] override fun computeSession(key: Any, inSession: InSession): SimpleSessionImpl { - val job = Job(parentJob) - val channel = Channel>() + val job = SupervisorJob(parentJob) + val channel = Channel>( + capacity = Channel.RENDEZVOUS, + onBufferOverflow = BufferOverflow.SUSPEND, + onUndeliveredElement = { (value, c) -> + c.resumeWithException(SessionPushOnFailureException("Undelivered value: $value")) + }) + val session = SimpleSessionImpl(key, job, channel, subScope) job.invokeOnCompletion { sessions.removeValue(key) { session } } job.invokeOnCompletion { - channel.close(it) + channel.cancel(it?.let { e -> CancellationException(e.message, e) }) } launchScope.launch { @@ -156,38 +163,51 @@ private class SimpleSessionImpl( job.join() } - override suspend fun push(value: T): R = suspendCancellableCoroutine { continuation -> - val data = SessionData(value, continuation) - launchScope.launch { - kotlin.runCatching { - channel.send(data) - }.onFailure { e -> - when (e) { - is ClosedSendChannelException -> { - data.continuation.resumeWithException( - SessionPushOnFailureException("Push to session channel (key=$key) failed: ${e.message}", e) - ) - } - - is ClosedReceiveChannelException -> { - data.continuation.resumeWithException( - SessionPushOnFailureException("Push to session channel (key=$key) failed: ${e.message}", e) - ) - } + override suspend fun push(value: T): R { + checkJob() - is CancellationException -> { - data.continuation.cancel( - CancellationException( - "Push to session channel (key=$key) failed: ${e.message}", - e.cause?.let { SessionPushOnFailureException(e.message, it) } + return suspendCancellableCoroutine { continuation -> + val data = SessionData(value, continuation) + launchScope.launch { + kotlin.runCatching { + channel.send(data) + }.onFailure { e -> + when (e) { + is ClosedSendChannelException -> { + data.continuation.resumeWithException( + SessionPushOnFailureException( + "Push to session channel (key=$key) failed: ${e.message}", + e + ) ) - ) - } - - else -> { - data.continuation.resumeWithException( - SessionPushOnFailureException("Push to session channel (key=$key) failed: ${e.message}", e) - ) + } + + is ClosedReceiveChannelException -> { + data.continuation.resumeWithException( + SessionPushOnFailureException( + "Push to session channel (key=$key) failed: ${e.message}", + e + ) + ) + } + + is CancellationException -> { + data.continuation.cancel( + CancellationException( + "Push to session channel (key=$key) failed: ${e.message}", + e.cause?.let { SessionPushOnFailureException(e.message, it) } + ) + ) + } + + else -> { + data.continuation.resumeWithException( + SessionPushOnFailureException( + "Push to session channel (key=$key) failed: ${e.message}", + e + ) + ) + } } } } @@ -232,7 +252,13 @@ private class SimpleSessionImpl( val (value, continuation) = receive() val handle = - job.invokeOnCompletion { cause -> continuation.resumeWithException(SessionCompletedWithoutResumeException(cause)) } + job.invokeOnCompletion { cause -> + continuation.resumeWithException( + SessionCompletedWithoutResumeException( + cause + ) + ) + } return createSimpleSessionContinuation(value, continuation, handle) } diff --git a/simbot-extensions/simbot-extension-continuous-session/src/commonMain/kotlin/love/forte/simbot/extension/continuous/session/ContinuousSessionContext.kt b/simbot-extensions/simbot-extension-continuous-session/src/commonMain/kotlin/love/forte/simbot/extension/continuous/session/ContinuousSessionContext.kt index f17011edd..a8b732a67 100644 --- a/simbot-extensions/simbot-extension-continuous-session/src/commonMain/kotlin/love/forte/simbot/extension/continuous/session/ContinuousSessionContext.kt +++ b/simbot-extensions/simbot-extension-continuous-session/src/commonMain/kotlin/love/forte/simbot/extension/continuous/session/ContinuousSessionContext.kt @@ -168,7 +168,9 @@ public interface ContinuousSessionContext { */ public enum class ConflictStrategy { /** - * 如果已经存在相同 `key` 的值,抛出异常 [ConflictSessionKeyException]。 + * 如果已经存在相同 `key` + * 的值且其仍处于活跃状态, + * 抛出异常 [ConflictSessionKeyException]。 * */ FAILURE, diff --git a/simbot-extensions/simbot-extension-continuous-session/src/commonTest/kotlin/ContinuousSessionTest.kt b/simbot-extensions/simbot-extension-continuous-session/src/commonTest/kotlin/ContinuousSessionTest.kt index 5cc1d520a..7c6664fe1 100644 --- a/simbot-extensions/simbot-extension-continuous-session/src/commonTest/kotlin/ContinuousSessionTest.kt +++ b/simbot-extensions/simbot-extension-continuous-session/src/commonTest/kotlin/ContinuousSessionTest.kt @@ -24,8 +24,11 @@ import kotlinx.coroutines.* import kotlinx.coroutines.test.runTest import love.forte.simbot.common.coroutines.IOOrDefault -import love.forte.simbot.extension.continuous.session.* +import love.forte.simbot.extension.continuous.session.ContinuousSessionContext import love.forte.simbot.extension.continuous.session.ContinuousSessionContext.ConflictStrategy.EXISTING +import love.forte.simbot.extension.continuous.session.InSession +import love.forte.simbot.extension.continuous.session.SessionAwaitOnFailureException +import love.forte.simbot.extension.continuous.session.SessionCompletedWithoutResumeException import kotlin.test.* import kotlin.time.Duration.Companion.milliseconds @@ -50,11 +53,10 @@ class ContinuousSessionTest { assertEquals("2", session.push(2)) assertEquals("3", session.push(3)) - val ex = assertFails { + // SessionPushFailed or Can + assertFails { session.push(4) } - ex.printStackTrace() - assertIs(ex) session.join() assertTrue(session.isCompleted) assertFalse(session.isCancelled) diff --git a/website b/website index 427f25380..cc00b6cd1 160000 --- a/website +++ b/website @@ -1 +1 @@ -Subproject commit 427f2538019cadf2fdc9168f1d1d3bfff6807307 +Subproject commit cc00b6cd17b192f0aad9c5588a845f567fda16c5