From 3f0ef5715c6c3a92ea6a227020fa0c66ec12863e Mon Sep 17 00:00:00 2001 From: Jason <11360596+jpenilla@users.noreply.github.com> Date: Mon, 15 Nov 2021 16:35:21 -0800 Subject: [PATCH] `cloud-kotlin-coroutines` (#318) * Builder coroutine support * Move coroutines to version catalog * Add kdocs * Add note about simple coordinator * Update changelog --- CHANGELOG.md | 4 +- .../build.gradle.kts | 3 +- .../cloud-kotlin-coroutines/build.gradle.kts | 11 +++ .../src/main/descriptions.md | 11 +++ .../coroutines/SuspendingExecutionHandler.kt | 87 +++++++++++++++++++ .../extension/command-builder-extensions.kt | 47 ++++++++++ .../mutable-command-builder-extensions.kt | 49 +++++++++++ .../coroutines/SuspendingHandlerTest.kt | 75 ++++++++++++++++ gradle/libs.versions.yml | 13 +++ settings.gradle.kts | 1 + 10 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 cloud-kotlin/cloud-kotlin-coroutines/build.gradle.kts create mode 100644 cloud-kotlin/cloud-kotlin-coroutines/src/main/descriptions.md create mode 100644 cloud-kotlin/cloud-kotlin-coroutines/src/main/kotlin/cloud/commandframework/kotlin/coroutines/SuspendingExecutionHandler.kt create mode 100644 cloud-kotlin/cloud-kotlin-coroutines/src/main/kotlin/cloud/commandframework/kotlin/coroutines/extension/command-builder-extensions.kt create mode 100644 cloud-kotlin/cloud-kotlin-coroutines/src/main/kotlin/cloud/commandframework/kotlin/coroutines/extension/mutable-command-builder-extensions.kt create mode 100644 cloud-kotlin/cloud-kotlin-coroutines/src/test/kotlin/cloud/commandframework/kotlin/coroutines/SuspendingHandlerTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fabba9e..1681693d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.6.0] ### Added -- Kotlin: Support for suspending command functions using `AnnotationParser.installCoroutineSupport()` +- Kotlin: New module `cloud-kotlin-coroutines`: Support for suspending command handlers in builders and the Kotlin builder DSL +- Kotlin: New module `cloud-kotlin-coroutines-annotations`: Support for suspending annotated command functions using + `AnnotationParser.installCoroutineSupport()` - Flags can be bound to a permission - Paper: Implement KeyedWorldArgument for matching worlds by their namespaced key - Annotations: Parser parameter annotations are now also parsed for flags ([#315](https://github.com/Incendo/cloud/pull/315)) diff --git a/cloud-kotlin/cloud-kotlin-coroutines-annotations/build.gradle.kts b/cloud-kotlin/cloud-kotlin-coroutines-annotations/build.gradle.kts index 09eb3586..04b06b55 100644 --- a/cloud-kotlin/cloud-kotlin-coroutines-annotations/build.gradle.kts +++ b/cloud-kotlin/cloud-kotlin-coroutines-annotations/build.gradle.kts @@ -6,6 +6,5 @@ dependencies { api(project(":cloud-core")) api(project(":cloud-annotations")) api(kotlin("reflect")) - api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") - api("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.5.2") + api(libs.bundles.coroutines) } diff --git a/cloud-kotlin/cloud-kotlin-coroutines/build.gradle.kts b/cloud-kotlin/cloud-kotlin-coroutines/build.gradle.kts new file mode 100644 index 00000000..4ac8abdf --- /dev/null +++ b/cloud-kotlin/cloud-kotlin-coroutines/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("cloud.kotlin-conventions") +} + +dependencies { + api(project(":cloud-core")) + api(libs.bundles.coroutines) + + compileOnly(project(":cloud-kotlin-extensions")) + testImplementation(project(":cloud-kotlin-extensions")) +} diff --git a/cloud-kotlin/cloud-kotlin-coroutines/src/main/descriptions.md b/cloud-kotlin/cloud-kotlin-coroutines/src/main/descriptions.md new file mode 100644 index 00000000..43e6c363 --- /dev/null +++ b/cloud-kotlin/cloud-kotlin-coroutines/src/main/descriptions.md @@ -0,0 +1,11 @@ +# Module cloud-kotlin-coroutines + +Cloud extensions for Kotlin coroutine integration. + +# Package cloud.commandframework.kotlin.coroutines + +Cloud Kotlin coroutines classes and functions. + +# Package cloud.commandframework.kotlin.coroutines.extension + +Extension functions for existing Cloud types diff --git a/cloud-kotlin/cloud-kotlin-coroutines/src/main/kotlin/cloud/commandframework/kotlin/coroutines/SuspendingExecutionHandler.kt b/cloud-kotlin/cloud-kotlin-coroutines/src/main/kotlin/cloud/commandframework/kotlin/coroutines/SuspendingExecutionHandler.kt new file mode 100644 index 00000000..1d11e292 --- /dev/null +++ b/cloud-kotlin/cloud-kotlin-coroutines/src/main/kotlin/cloud/commandframework/kotlin/coroutines/SuspendingExecutionHandler.kt @@ -0,0 +1,87 @@ +// +// MIT License +// +// Copyright (c) 2021 Alexander Söderberg & Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.kotlin.coroutines + +import cloud.commandframework.context.CommandContext +import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator +import cloud.commandframework.execution.CommandExecutionCoordinator +import cloud.commandframework.execution.CommandExecutionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.future.future +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +/** + * Suspending version of [CommandExecutionHandler] for use with + * coroutines. + * + * NOTE: It is highly advised to not use [CommandExecutionCoordinator.SimpleCoordinator] together + * with coroutine support. Consider using [AsynchronousCommandExecutionCoordinator] instead. + * + * @param C command sender type + */ +public fun interface SuspendingExecutionHandler { + /** + * Handles command execution. + * + * @param commandContext command context + */ + public suspend operator fun invoke(commandContext: CommandContext) + + /** + * Create a new [CommandExecutionHandler] for use in building commands, + * backed by this [SuspendingExecutionHandler]. + * + * @param scope coroutine scope + * @param context coroutine context + * @return new [CommandExecutionHandler] + */ + public fun asCommandExecutionHandler( + scope: CoroutineScope = GlobalScope, + context: CoroutineContext = EmptyCoroutineContext, + ): CommandExecutionHandler = createCommandExecutionHandler(scope, context, this) + + public companion object { + /** + * Create a new [CommandExecutionHandler] for use in building commands, + * backed by the given [SuspendingExecutionHandler]. + * + * @param scope coroutine scope + * @param context coroutine context + * @param handler suspending handler + * @return new [CommandExecutionHandler] + */ + public fun createCommandExecutionHandler( + scope: CoroutineScope = GlobalScope, + context: CoroutineContext = EmptyCoroutineContext, + handler: SuspendingExecutionHandler, + ): CommandExecutionHandler = CommandExecutionHandler.FutureCommandExecutionHandler { ctx -> + scope.future(context) { + handler(ctx) + null + } + } + } +} diff --git a/cloud-kotlin/cloud-kotlin-coroutines/src/main/kotlin/cloud/commandframework/kotlin/coroutines/extension/command-builder-extensions.kt b/cloud-kotlin/cloud-kotlin-coroutines/src/main/kotlin/cloud/commandframework/kotlin/coroutines/extension/command-builder-extensions.kt new file mode 100644 index 00000000..d1667153 --- /dev/null +++ b/cloud-kotlin/cloud-kotlin-coroutines/src/main/kotlin/cloud/commandframework/kotlin/coroutines/extension/command-builder-extensions.kt @@ -0,0 +1,47 @@ +// +// MIT License +// +// Copyright (c) 2021 Alexander Söderberg & Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.kotlin.coroutines.extension + +import cloud.commandframework.Command +import cloud.commandframework.kotlin.coroutines.SuspendingExecutionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.GlobalScope +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +/** + * Specify a suspending command execution handler. + * + * @param scope coroutine scope + * @param context coroutine context + * @param handler suspending handler + * @return modified copy of this [Command.Builder] + * @see Command.Builder.handler + * @see SuspendingExecutionHandler + */ +public fun Command.Builder.suspendingHandler( + scope: CoroutineScope = GlobalScope, + context: CoroutineContext = EmptyCoroutineContext, + handler: SuspendingExecutionHandler, +): Command.Builder = handler(SuspendingExecutionHandler.createCommandExecutionHandler(scope, context, handler)) diff --git a/cloud-kotlin/cloud-kotlin-coroutines/src/main/kotlin/cloud/commandframework/kotlin/coroutines/extension/mutable-command-builder-extensions.kt b/cloud-kotlin/cloud-kotlin-coroutines/src/main/kotlin/cloud/commandframework/kotlin/coroutines/extension/mutable-command-builder-extensions.kt new file mode 100644 index 00000000..428f67fb --- /dev/null +++ b/cloud-kotlin/cloud-kotlin-coroutines/src/main/kotlin/cloud/commandframework/kotlin/coroutines/extension/mutable-command-builder-extensions.kt @@ -0,0 +1,49 @@ +// +// MIT License +// +// Copyright (c) 2021 Alexander Söderberg & Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.kotlin.coroutines.extension + +import cloud.commandframework.kotlin.MutableCommandBuilder +import cloud.commandframework.kotlin.coroutines.SuspendingExecutionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.GlobalScope +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +/** + * Specify a suspending command execution handler. + * + * @param scope coroutine scope + * @param context coroutine context + * @param handler suspending handler + * @return this [MutableCommandBuilder] + * @see MutableCommandBuilder.handler + * @see SuspendingExecutionHandler + */ +public fun MutableCommandBuilder.suspendingHandler( + scope: CoroutineScope = GlobalScope, + context: CoroutineContext = EmptyCoroutineContext, + handler: SuspendingExecutionHandler, +): MutableCommandBuilder = mutate { + it.suspendingHandler(scope, context, handler) +} diff --git a/cloud-kotlin/cloud-kotlin-coroutines/src/test/kotlin/cloud/commandframework/kotlin/coroutines/SuspendingHandlerTest.kt b/cloud-kotlin/cloud-kotlin-coroutines/src/test/kotlin/cloud/commandframework/kotlin/coroutines/SuspendingHandlerTest.kt new file mode 100644 index 00000000..98553d5b --- /dev/null +++ b/cloud-kotlin/cloud-kotlin-coroutines/src/test/kotlin/cloud/commandframework/kotlin/coroutines/SuspendingHandlerTest.kt @@ -0,0 +1,75 @@ +// +// MIT License +// +// Copyright (c) 2021 Alexander Söderberg & Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.kotlin.coroutines + +import cloud.commandframework.CommandManager +import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator +import cloud.commandframework.internal.CommandRegistrationHandler +import cloud.commandframework.kotlin.coroutines.extension.suspendingHandler +import cloud.commandframework.kotlin.extension.buildAndRegister +import cloud.commandframework.meta.CommandMeta +import cloud.commandframework.meta.SimpleCommandMeta +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Test +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +class SuspendingHandlerTest { + + companion object { + val executorService: ExecutorService = Executors.newSingleThreadExecutor() + } + + @Test + fun test(): Unit = runBlocking { + val manager = TestCommandManager() + + manager.buildAndRegister("suspend") { + suspendingHandler { + println("called from thread: ${Thread.currentThread().name}") + + someSuspendingFunction() + } + } + + manager.executeCommand(TestCommandSender(), "suspend").await() + } + + suspend fun someSuspendingFunction() {} + + private class TestCommandSender + + private class TestCommandManager : CommandManager( + AsynchronousCommandExecutionCoordinator.newBuilder() + .withExecutor(executorService) + .build(), + CommandRegistrationHandler.nullCommandRegistrationHandler() + ) { + + override fun hasPermission(sender: TestCommandSender, permission: String): Boolean = true + + override fun createDefaultCommandMeta(): CommandMeta = SimpleCommandMeta.empty() + } +} diff --git a/gradle/libs.versions.yml b/gradle/libs.versions.yml index 4759eee6..d5050554 100644 --- a/gradle/libs.versions.yml +++ b/gradle/libs.versions.yml @@ -11,6 +11,7 @@ plugins: versions: kotlin: &kotlin 1.5.31 dokka: *kotlin + coroutines: 1.5.2 checkerQual: 3.14.0 # build-logic @@ -26,6 +27,15 @@ dependencies: name: checker-qual version: { ref: checkerQual } + coroutinesCore: + group: org.jetbrains.kotlinx + name: kotlinx-coroutines-core + version: { ref: coroutines } + coroutinesJdk8: + group: org.jetbrains.kotlinx + name: kotlinx-coroutines-jdk8 + version: { ref: coroutines } + # build-logic indraCommon: group: net.kyori @@ -61,3 +71,6 @@ dependencies: version: { ref: ktlint } bundles: + coroutines: + - coroutinesCore + - coroutinesJdk8 diff --git a/settings.gradle.kts b/settings.gradle.kts index b6bc677c..a796b9d2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,6 +24,7 @@ include(":cloud-annotations") // Kotlin Extensions setupKotlinModule("cloud-kotlin-extensions") +setupKotlinModule("cloud-kotlin-coroutines") setupKotlinModule("cloud-kotlin-coroutines-annotations") // Discord Modules