diff --git a/build.gradle b/build.gradle index ba74bd71..292e3604 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,8 @@ allprojects { license { header rootProject.file('HEADER') mapping 'java', 'DOUBLESLASH_STYLE' - includes(["**/*.java"]) + mapping 'kt', 'DOUBLESLASH_STYLE' + includes(["**/*.java", "**/*.kt"]) } build.dependsOn(checkstyleMain) diff --git a/cloud-kotlin-extensions/build.gradle b/cloud-kotlin-extensions/build.gradle new file mode 100644 index 00000000..4461cfaa --- /dev/null +++ b/cloud-kotlin-extensions/build.gradle @@ -0,0 +1,43 @@ +import org.jetbrains.dokka.gradle.DokkaTask + +plugins { + id 'org.jetbrains.kotlin.jvm' version '1.4.21' + id 'org.jetbrains.dokka' version '1.4.20' +} + +kotlin { + explicitApi() +} + +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} + +compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + api project(':cloud-core') + testImplementation "org.jetbrains.kotlin:kotlin-test-junit5" +} + +javadocJar { + from dokkaHtml +} + +tasks.withType(DokkaTask).configureEach { + dokkaSourceSets { + main { + includes.from(layout.projectDirectory.file("src/main/descriptions.md").toString()) + externalDocumentationLink { + url.set(new URL("https://javadoc.commandframework.cloud/")) //todo fix KDoc linking to JavaDoc + packageListUrl.set(new URL("https://javadoc.commandframework.cloud/allpackages-index.html")) + } + } + } +} diff --git a/cloud-kotlin-extensions/src/main/descriptions.md b/cloud-kotlin-extensions/src/main/descriptions.md new file mode 100644 index 00000000..9c565a37 --- /dev/null +++ b/cloud-kotlin-extensions/src/main/descriptions.md @@ -0,0 +1,11 @@ +# Module cloud-kotlin-extensions + +Extension functions and new classes that allow for using Cloud in a more idiomatic Kotlin style + +# Package cloud.commandframework.kotlin + +Cloud Kotlin specific classes + +# Package cloud.commandframework.kotlin.extension + +Extension functions for existing Cloud types diff --git a/cloud-kotlin-extensions/src/main/kotlin/cloud/commandframework/kotlin/MutableCommandBuilder.kt b/cloud-kotlin-extensions/src/main/kotlin/cloud/commandframework/kotlin/MutableCommandBuilder.kt new file mode 100644 index 00000000..dbbdc374 --- /dev/null +++ b/cloud-kotlin-extensions/src/main/kotlin/cloud/commandframework/kotlin/MutableCommandBuilder.kt @@ -0,0 +1,478 @@ +// +// MIT License +// +// Copyright (c) 2020 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 + +import cloud.commandframework.Command +import cloud.commandframework.CommandManager +import cloud.commandframework.Description +import cloud.commandframework.arguments.CommandArgument +import cloud.commandframework.execution.CommandExecutionHandler +import cloud.commandframework.kotlin.extension.command +import cloud.commandframework.kotlin.extension.senderType +import cloud.commandframework.meta.CommandMeta +import cloud.commandframework.permission.CommandPermission +import kotlin.reflect.KClass + +/** + * A mutable [Command.Builder] wrapper, providing functions to assist in creating commands using the Kotlin builder DSL style + * + * @since 1.3.0 + */ +public class MutableCommandBuilder { + private val commandManager: CommandManager + private var commandBuilder: Command.Builder + + /** + * Create a new [MutableCommandBuilder] + * + * @param name name for the root command node + * @param description description for the root command node + * @param aliases aliases for the root command node + * @param commandManager the command manager which will own this command + * @since 1.3.0 + */ + public constructor( + name: String, + description: Description = Description.empty(), + aliases: Array = emptyArray(), + commandManager: CommandManager + ) { + this.commandManager = commandManager + this.commandBuilder = commandManager.commandBuilder(name, description, *aliases) + } + + /** + * Create a new [MutableCommandBuilder] and invoke the provided receiver lambda on it + * + * @param name name for the root command node + * @param description description for the root command node + * @param aliases aliases for the root command node + * @param commandManager the command manager which will own this command + * @param lambda receiver lambda which will be invoked on the new builder + * @since 1.3.0 + */ + public constructor( + name: String, + description: Description = Description.empty(), + aliases: Array = emptyArray(), + commandManager: CommandManager, + lambda: MutableCommandBuilder.() -> Unit + ) : this(name, description, aliases, commandManager) { + lambda(this) + } + + private constructor( + commandManager: CommandManager, + commandBuilder: Command.Builder + ) { + this.commandManager = commandManager + this.commandBuilder = commandBuilder + } + + /** + * Build a [Command] from the current state of this builder + * + * @return built command + * @since 1.3.0 + */ + public fun build(): Command = + this.commandBuilder.build() + + /** + * Invoke the provided receiver lambda on this builder, then build a [Command] from the resulting state + * + * @param lambda receiver lambda which will be invoked on builder before building + * @return built command + * @since 1.3.0 + */ + public fun build( + lambda: MutableCommandBuilder.() -> Unit + ): Command { + lambda(this) + return this.commandBuilder.build() + } + + /** + * Modify this [MutableCommandBuilder]'s internal [Command.Builder] with a unary function + * + * @param mutator mutator function + * @return this mutable builder + * @since 1.3.0 + */ + public fun mutate( + mutator: (Command.Builder) -> Command.Builder + ): MutableCommandBuilder { + this.commandBuilder = mutator(this.commandBuilder) + return this + } + + private fun onlyMutate( + mutator: (Command.Builder) -> Command.Builder + ): Unit { + mutate(mutator) + } + + /** + * Make a new copy of this [MutableCommandBuilder] + * + * @return a copy of this mutable builder + * @since 1.3.0 + */ + public fun copy(): MutableCommandBuilder = + MutableCommandBuilder(this.commandManager, this.commandBuilder) + + /** + * Make a new copy of this [MutableCommandBuilder] and invoke the provided receiver lambda on it + * + * @param lambda receiver lambda which will be invoked on the new builder + * @return a copy of this mutable builder + * @since 1.3.0 + */ + public fun copy( + lambda: MutableCommandBuilder.() -> Unit + ): MutableCommandBuilder = + copy().apply { + lambda(this) + } + + /** + * Make a new copy of this [MutableCommandBuilder], append a literal, and invoke the provided receiver lambda on it + * + * @param literal name for the literal + * @param description description for the literal + * @param lambda receiver lambda which will be invoked on the new builder + * @return a copy of this mutable builder + * @since 1.3.0 + */ + public fun copy( + literal: String, + description: Description, + lambda: MutableCommandBuilder.() -> Unit + ): MutableCommandBuilder = + copy().apply { + literal(literal, description) + lambda(this) + } + + /** + * Make a new copy of this [MutableCommandBuilder], append a literal, and invoke the provided receiver lambda on it + * + * @param literal name for the literal + * @param lambda receiver lambda which will be invoked on the new builder + * @return a copy of this mutable builder + * @since 1.3.0 + */ + public fun copy( + literal: String, + lambda: MutableCommandBuilder.() -> Unit + ): MutableCommandBuilder = + copy().apply { + literal(literal) + lambda(this) + } + + /** + * Build and register this command with the owning command manager + * + * @return this mutable builder + * @see [CommandManager.command] + * @since 1.3.0 + */ + public fun register(): MutableCommandBuilder = + apply { + this.commandManager.command(this) + } + + /** + * Create a new copy of this mutable builder, act on it with a receiver lambda, and then register it with the owning + * command manager + * + * @param lambda receiver lambda which will be invoked on the new builder + * @return the new mutable builder + * @see [CommandManager.command] + * @since 1.3.0 + */ + public fun registerCopy( + lambda: MutableCommandBuilder.() -> Unit + ): MutableCommandBuilder = + copy(lambda).register() + + /** + * Create a new copy of this mutable builder, append a literal, act on it with a receiver lambda, and then register it with + * the owning + * command manager + * + * @param literal name for the literal + * @param lambda receiver lambda which will be invoked on the new builder + * @return the new mutable builder + * @see [CommandManager.command] + * @since 1.3.0 + */ + public fun registerCopy( + literal: String, + lambda: MutableCommandBuilder.() -> Unit + ): MutableCommandBuilder = + copy(literal, lambda).register() + + /** + * Create a new copy of this mutable builder, append a literal, act on it with a receiver lambda, and then register it with + * the owning + * command manager + * + * @param literal name for the literal + * @param description description for the literal + * @param lambda receiver lambda which will be invoked on the new builder + * @return the new mutable builder + * @see [CommandManager.command] + * @since 1.3.0 + */ + public fun registerCopy( + literal: String, + description: Description, + lambda: MutableCommandBuilder.() -> Unit + ): MutableCommandBuilder = + copy(literal, description, lambda).register() + + /** + * Set the value for a certain [CommandMeta.Key] in the command meta storage for this builder + * + * @param T value type + * @param key the key to set a value for + * @param value new value + * @return this mutable builder + * @since 1.3.0 + */ + public fun meta( + key: CommandMeta.Key, + value: T + ): MutableCommandBuilder = + mutate { it.meta(key, value) } + + /** + * Set the value for a certain [CommandMeta.Key] in the command meta storage for this builder + * + * @param T value type + * @param value new value + * @return this mutable builder + * @since 1.3.0 + */ + public infix fun CommandMeta.Key.to( + value: T + ): MutableCommandBuilder = + meta(this, value) + + /** + * Set the [CommandMeta.DESCRIPTION] meta for this command + * + * @param description command description + * @return this mutable builder + * @since 1.3.0 + */ + public fun commandDescription( + description: String + ): MutableCommandBuilder = + meta(CommandMeta.DESCRIPTION, description) + + /** + * Set the [CommandMeta.LONG_DESCRIPTION] meta for this command + * + * @param description command description + * @return this mutable builder + * @since 1.3.0 + */ + public fun longCommandDescription( + description: String + ): MutableCommandBuilder = + meta(CommandMeta.LONG_DESCRIPTION, description) + + /** + * Set the [CommandMeta.HIDDEN] meta for this command + * + * @param hidden whether this command should be hidden + * @return this mutable builder + * @since 1.3.0 + */ + public fun hidden( + hidden: Boolean = true + ): MutableCommandBuilder = + meta(CommandMeta.HIDDEN, hidden) + + /** + * Specify a required sender type + * + * @param T sender type + * @return this mutable builder + * @since 1.3.0 + */ + public inline fun senderType(): MutableCommandBuilder = + mutate { it.senderType(T::class) } + + /** + * Specify a required sender type + * + * @param type sender type + * @return this mutable builder + * @since 1.3.0 + */ + public fun senderType( + type: KClass + ): MutableCommandBuilder = + mutate { it.senderType(type) } + + /** + * Field to get and set the required sender type for this command builder + * + * @since 1.3.0 + */ + public var senderType: KClass? + get() = this.commandBuilder.senderType()?.kotlin + set(type) { + if (type == null) throw UnsupportedOperationException("Cannot set a null sender type") + onlyMutate { it.senderType(type) } + } + + /** + * Specify a required sender type + * + * @param type sender type + * @return this mutable builder + * @since 1.3.0 + */ + public fun senderType( + type: Class + ): MutableCommandBuilder = + mutate { it.senderType(type) } + + /** + * Specify a permission required to execute this command + * + * @param permission permission string + * @return this mutable builder + * @since 1.3.0 + */ + public fun permission( + permission: String + ): MutableCommandBuilder = + mutate { it.permission(permission) } + + /** + * Specify a permission required to execute this command + * + * @param permission command permission + * @return this mutable builder + * @since 1.3.0 + */ + public fun permission( + permission: CommandPermission + ): MutableCommandBuilder = + mutate { it.permission(permission) } + + /** + * Field to get and set the required permission for this command builder + * + * @since 1.3.0 + */ + public var permission: String + get() = this.commandBuilder.commandPermission().toString() + set(permission) = onlyMutate { it.permission(permission) } + + /** + * Field to get and set the required permission for this command builder + * + * @since 1.3.0 + */ + public var commandPermission: CommandPermission + get() = this.commandBuilder.commandPermission() + set(permission) = onlyMutate { it.permission(permission) } + + /** + * Add a new argument to this command + * + * @param argument argument to add + * @param description description of the argument + * @return this mutable builder + * @since 1.3.0 + */ + public fun argument( + argument: CommandArgument, + description: Description = Description.empty() + ): MutableCommandBuilder = + mutate { it.argument(argument, description) } + + /** + * Add a new argument to this command + * + * @param argument argument to add + * @param description description of the argument + * @return this mutable builder + * @since 1.3.0 + */ + public fun argument( + argument: CommandArgument.Builder, + description: Description = Description.empty() + ): MutableCommandBuilder = + mutate { it.argument(argument, description) } + + /** + * Add a new argument to this command + * + * @param description description of the argument + * @param argumentSupplier supplier of the argument to add + * @return this mutable builder + * @since 1.3.0 + */ + public fun argument( + description: Description = Description.empty(), + argumentSupplier: () -> CommandArgument + ): MutableCommandBuilder = + mutate { it.argument(argumentSupplier(), description) } + + /** + * Add a new literal argument to this command + * + * @param name main argument name + * @param description literal description + * @param aliases argument aliases + * @return this mutable builder + * @since 1.3.0 + */ + public fun literal( + name: String, + description: Description = Description.empty(), + vararg aliases: String + ): MutableCommandBuilder = + mutate { it.literal(name, description, *aliases) } + + /** + * Set the [CommandExecutionHandler] for this builder + * + * @param handler command execution handler + * @return this mutable builder + * @since 1.3.0 + */ + public fun handler( + handler: CommandExecutionHandler + ): MutableCommandBuilder = + mutate { it.handler(handler) } +} diff --git a/cloud-kotlin-extensions/src/main/kotlin/cloud/commandframework/kotlin/extension/CommandBuildingExtensions.kt b/cloud-kotlin-extensions/src/main/kotlin/cloud/commandframework/kotlin/extension/CommandBuildingExtensions.kt new file mode 100644 index 00000000..65991533 --- /dev/null +++ b/cloud-kotlin-extensions/src/main/kotlin/cloud/commandframework/kotlin/extension/CommandBuildingExtensions.kt @@ -0,0 +1,104 @@ +// +// MIT License +// +// Copyright (c) 2020 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.extension + +import cloud.commandframework.Command +import cloud.commandframework.CommandManager +import cloud.commandframework.Description +import cloud.commandframework.kotlin.MutableCommandBuilder +import kotlin.reflect.KClass + +/** + * Create a new [MutableCommandBuilder] and invoke the provided receiver lambda on it + * + * @param name name for the root command node + * @param description description for the root command node + * @param aliases aliases for the root command node + * @param lambda receiver lambda which will be invoked on the new builder + * @since 1.3.0 + */ +public fun CommandManager.commandBuilder( + name: String, + description: Description = Description.empty(), + aliases: Array = emptyArray(), + lambda: MutableCommandBuilder.() -> Unit +): MutableCommandBuilder = + MutableCommandBuilder(name, description, aliases, this, lambda) + +/** + * Create a new [MutableCommandBuilder] which will invoke the provided receiver lambda, and then register itself with the + * owning [CommandManager] + * + * @param name name for the root command node + * @param description description for the root command node + * @param aliases aliases for the root command node + * @param lambda receiver lambda which will be invoked on the new builder + * @since 1.3.0 + */ +public fun CommandManager.buildAndRegister( + name: String, + description: Description = Description.empty(), + aliases: Array = emptyArray(), + lambda: MutableCommandBuilder.() -> Unit +): MutableCommandBuilder = + commandBuilder(name, description, aliases, lambda).register() + +/** + * Build the provided [MutableCommandBuilder]s into [Command]s, and then register them with the command manager + * + * @param commands mutable command builder(s) to register + * @return the command manager + * @see [CommandManager.command] + * @since 1.3.0 + */ +public fun CommandManager.command( + vararg commands: MutableCommandBuilder +): CommandManager = + apply { + commands.forEach { command -> + this.command(command.build()) + } + } + +/** + * Specify a required sender type + * + * @param type required sender type + * @return New builder instance using the required sender type + * @since 1.3.0 + */ +public fun Command.Builder.senderType(type: KClass): Command.Builder = + senderType(type.java) + +/** + * Get a [Description], defaulting to [Description.empty] + * + * @param description description string + * @return the description + * @since 1.3.0 + */ +public fun description( + description: String = "" +): Description = + if (description.isEmpty()) Description.empty() else Description.of(description) diff --git a/cloud-kotlin-extensions/src/test/kotlin/cloud/commandframework/kotlin/CommandBuildingDSLTest.kt b/cloud-kotlin-extensions/src/test/kotlin/cloud/commandframework/kotlin/CommandBuildingDSLTest.kt new file mode 100644 index 00000000..3ea19986 --- /dev/null +++ b/cloud-kotlin-extensions/src/test/kotlin/cloud/commandframework/kotlin/CommandBuildingDSLTest.kt @@ -0,0 +1,116 @@ +// +// MIT License +// +// Copyright (c) 2020 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 + +import cloud.commandframework.CommandManager +import cloud.commandframework.arguments.standard.StringArgument +import cloud.commandframework.execution.CommandExecutionCoordinator +import cloud.commandframework.internal.CommandRegistrationHandler +import cloud.commandframework.kotlin.extension.buildAndRegister +import cloud.commandframework.kotlin.extension.command +import cloud.commandframework.kotlin.extension.commandBuilder +import cloud.commandframework.kotlin.extension.description +import cloud.commandframework.meta.CommandMeta +import cloud.commandframework.meta.SimpleCommandMeta +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class CommandBuildingDSLTest { + + @Test + fun testCommandDSL() { + val manager = TestCommandManager() + + manager.command( + manager.commandBuilder("kotlin", aliases = arrayOf("alias")) { + permission = "permission" + senderType() + + literal("dsl") + argument(description("An amazing command argument")) { + StringArgument.of("moment") + } + handler { + // ... + } + + manager.command(copy { + literal("bruh_moment") + handler { + // ... + } + }) + } + ) + + manager.buildAndRegister("is") { + commandDescription("Command description") + + registerCopy { + literal("this") + CommandMeta.DESCRIPTION to "Command description" + + registerCopy { + literal("going") + meta(CommandMeta.DESCRIPTION, "Command Description") + + registerCopy("too_far") { + // ? + } + } + } + } + + manager.executeCommand(SpecificCommandSender(), "kotlin dsl time") + manager.executeCommand(SpecificCommandSender(), "kotlin dsl time bruh_moment") + + Assertions.assertEquals( + manager.commandHelpHandler.allCommands.map { it.syntaxString }.sorted(), + setOf( + "kotlin dsl ", + "kotlin dsl bruh_moment", + "is", + "is this", + "is this going", + "is this going too_far", + ).sorted() + ) + } + + class TestCommandManager : CommandManager( + CommandExecutionCoordinator.simpleCoordinator(), CommandRegistrationHandler.nullCommandRegistrationHandler() + ) { + override fun createDefaultCommandMeta(): SimpleCommandMeta { + return SimpleCommandMeta.empty() + } + + override fun hasPermission(sender: TestCommandSender, permission: String): Boolean { + return !permission.equals("no", ignoreCase = true) + } + } + + open class TestCommandSender + class SpecificCommandSender : TestCommandSender() + +} diff --git a/settings.gradle b/settings.gradle index da2585bb..cf224c64 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,6 +6,8 @@ include(':cloud-annotations') include(':cloud-core') include(':cloud-services') include(':cloud-tasks') +// Kotlin +include(':cloud-kotlin-extensions') // //Discord Modules //