Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Type-safe Kotlin DSL #544

Open
sya-ri opened this issue Apr 13, 2024 · 9 comments
Open

Type-safe Kotlin DSL #544

sya-ri opened this issue Apr 13, 2024 · 9 comments
Labels
enhancement New feature or request

Comments

@sya-ri
Copy link

sya-ri commented Apr 13, 2024

Description

The current Kotlin DSL requires casts, so it is not type-safe. We can't get the most out of Kotlin.

commandTree("sendmessageto") {
    playerArgument("player") {
        greedyStringArgument("msg") {
            anyExecutor { _, args ->
                val player: Player = args["player"] as Player
                val message: String = args["msg"] as String
                player.sendMessage(message)
            }
        }
    }
}
commandAPICommand("sendmessageto") {
    playerArgument("player")
    greedyStringArgument("msg")
    anyExecutor { _, args ->
        val player: Player = args["player"] as Player
        val message: String = args["msg"] as String
        player.sendMessage(message)
    }
}

from: Defining a simple message command

Expected code

commandTree("sendmessageto") {
    playerArgument("player") { getPlayer ->
        greedyStringArgument("msg") { getMessage ->
            anyExecutor { _, args ->
                val player = getPlayer(args) // Returns Player
                val message = getMessage(args) // Returns String
                player.sendMessage(message)
            }
        }
    }
}

I have confirmed that this code can actually be rewritten. You can try it out using this library: https://github.com/sya-ri/commandapi-kotlin-improved

In my library, the Kotlin DSL for CommandAPICommand is not supported, but we should be able to rewrite it similarly.

Extra details

If necessary, I would like to submit a PR for the changes made by my library 😉

@sya-ri sya-ri added the enhancement New feature or request label Apr 13, 2024
@DerEchtePilz
Copy link
Collaborator

Hmm, interesting.
If you choose to open a PR, please note that this should be an additional option, not a replacement.
Also, this should also be supported in the commandAPICommand structure without the need to redefine how that looks.

@sya-ri
Copy link
Author

sya-ri commented Apr 13, 2024

@DerEchtePilz Thank you for your interest.

We will need to make a breaking change to "optional".

Currently, specifying optional = true for argument makes it nullable, and specifying false makes it not.

For example:

commandTree("sendmessageto") {
    playerArgument("player", /* Actually: optional = false */) { getPlayer ->
        greedyStringArgument("msg", optional = true) { getMessage ->
            anyExecutor { _, args ->
                val player = getPlayer(args) // Expected: Player (not-null)
                val message = getMessage(args) // Expected: String? (nullable)
                player.sendMessage(message)
            }
        }
    }
}

However, depending on the value of the argument and changing the type is difficult and requires changing the method to call.

commandTree("sendmessageto") {
    playerArgument("player") { getPlayer ->
        greedyStringOptionalArgument("msg") { getMessage ->
            anyExecutor { _, args ->
                val player = getPlayer(args) // Returns Player (not-null)
                val message = getMessage(args) // Returns String? (nullable)
                player.sendMessage(message)
            }
        }
    }
}

I would like to discuss this 🙏

@DerEchtePilz
Copy link
Collaborator

DerEchtePilz commented Apr 13, 2024

@sya-ri
Yeah, adding the Optional part to the method doesn't work.
When adding optional arguments, it was discussed whether we want a separate method for optional arguments or if we want it as a parameter which by default is set to false.
We obviously settled on the second option and the reason was that doesn't require twice as many methods.
It would be required to keep the optional parameter.

On a different note, iirc @willkroboth currently is working on updating the way commands are registered and has also made changes to optional argument handling. During that, we also kinda decided that optional arguments don't really make sense in a CommandTree - you would rather handle it with branching which is why adding methods with the Optional part in the name is unncessary work. It's going to be removed anyway.

Currently, specifying optional = true for argument makes it nullable, and specifying false makes it not.

This will also never change. Optional arguments just have the risk of not being present which is why we return null if they aren't given.

@sya-ri
Copy link
Author

sya-ri commented Apr 13, 2024

We obviously settled on the second option and the reason was that doesn't require twice as many methods.

Yes, that's exactly right 😢

@Deprecated("This version has been deprecated since version 9.0.2", ReplaceWith("multiLiteralArgument(nodeName, listOf(literals))", "dev.jorel.commandapi.kotlindsl.*"), DeprecationLevel.WARNING)
inline fun CommandAPICommand.multiLiteralArgument(vararg literals: String, optional: Boolean = false, block: Argument<*>.() -> Unit = {}): CommandAPICommand = withArguments(MultiLiteralArgument(literals).setOptional(optional).apply(block))
@Deprecated("This method has been deprecated since version 9.1.0", ReplaceWith("multiLiteralArgument(nodeName, literals)", "dev.jorel.commandapi.kotlindsl.*"), DeprecationLevel.WARNING)
inline fun CommandAPICommand.multiLiteralArgument(nodeName: String, literals: List<String>, optional: Boolean = false, block: Argument<*>.() -> Unit = {}): CommandAPICommand = withArguments(MultiLiteralArgument(nodeName, literals).setOptional(optional).apply(block))

@Deprecated("This version has been deprecated since version 9.0.2", ReplaceWith("multiLiteralArgument(nodeName, listOf(literals))", "dev.jorel.commandapi.kotlindsl.*"), DeprecationLevel.WARNING)
inline fun CommandTree.multiLiteralArgument(vararg literals: String, optional: Boolean = false, block: Argument<*>.() -> Unit = {}): CommandTree= then(MultiLiteralArgument(literals).setOptional(optional).apply(block))
@Deprecated("This method has been deprecated since version 9.1.0", ReplaceWith("multiLiteralArgument(nodeName, literals)", "dev.jorel.commandapi.kotlindsl.*"), DeprecationLevel.WARNING)
inline fun CommandTree.multiLiteralArgument(nodeName: String, literals: List<String>, optional: Boolean = false, block: Argument<*>.() -> Unit = {}): CommandTree= then(MultiLiteralArgument(nodeName, literals).setOptional(optional).apply(block))

@Deprecated("This version has been deprecated since version 9.0.2", ReplaceWith("multiLiteralArgument(nodeName, listOf(literals))", "dev.jorel.commandapi.kotlindsl.*"), DeprecationLevel.WARNING)
inline fun Argument<*>.multiLiteralArgument(vararg literals: String, optional: Boolean = false, block: Argument<*>.() -> Unit = {}): Argument<*> = then(MultiLiteralArgument(literals).setOptional(optional).apply(block))
@Deprecated("This method has been deprecated since version 9.1.0", ReplaceWith("multiLiteralArgument(nodeName, literals)", "dev.jorel.commandapi.kotlindsl.*"), DeprecationLevel.WARNING)
inline fun Argument<*>.multiLiteralArgument(nodeName: String, literals: List<String>, optional: Boolean = false, block: Argument<*>.() -> Unit = {}): Argument<*> = then(MultiLiteralArgument(nodeName, literals).setOptional(optional).apply(block))

I would like to take this opportunity to remove the deprecated methods.
It's a bit strange to add optional methods to deprecated methods and then deprecate it.

@Deprecated("This method has been deprecated since version 9.1.0 as it is not needed anymore. See the documentation for more information", ReplaceWith(""), DeprecationLevel.WARNING)
inline fun commandAPICommand(name: String, predicate: Predicate<CommandSender>, command: CommandAPICommand.() -> Unit = {}) = CommandAPICommand(name).withRequirement(predicate).apply(command).register()

@Deprecated("This method has been deprecated since version 9.1.0 as it is not needed anymore. See the documentation for more information", ReplaceWith(""), DeprecationLevel.WARNING)
inline fun commandTree(name: String, predicate: Predicate<CommandSender>, tree: CommandTree.() -> Unit = {}) = CommandTree(name).withRequirement(predicate).apply(tree).register()

@Deprecated("This method has been deprecated since version 9.1.0 as it is not needed anymore. See the documentation for more information", ReplaceWith(""), DeprecationLevel.WARNING)
inline fun CommandTree.requirement(base: Argument<*>, predicate: Predicate<CommandSender>, block: Argument<*>.() -> Unit = {}): CommandTree = then(base.withRequirement(predicate).apply(block))
@Deprecated("This method has been deprecated since version 9.1.0 as it is not needed anymore. See the documentation for more information", ReplaceWith(""), DeprecationLevel.WARNING)
inline fun Argument<*>.requirement(base: Argument<*>, predicate: Predicate<CommandSender>, block: Argument<*>.() -> Unit = {}): Argument<*> = then(base.withRequirement(predicate).apply(block))

I would to leave deprecated methods that are not related to arguments.

What do you think?

@DerEchtePilz
Copy link
Collaborator

I hate that MultiLiteralArgument mess...
Luckily this is already resolved and I am removing deprecated methods in #493

A bit of history about that MultiLiteralArgument mess. I actually added the node name to MultiLiteralArguments but couldn't directly remove the existing methods obviously.
That's why I initially deprecated one method in the DSL and added the (then not deprecated) alternative.
I then fixed that mess and now there are two deprecated methods.

But yeah, those deprecations are already in the process of being removed, we just can't do it until 10.0.0

@sya-ri
Copy link
Author

sya-ri commented Apr 14, 2024

But yeah, those deprecations are already in the process of being removed, we just can't do it until 10.0.0

I see. Changes to this issue will also be merged in 10.0.0. Right?

@DerEchtePilz
Copy link
Collaborator

That solely depends on the nature of your PR and what it changes.

@sya-ri
Copy link
Author

sya-ri commented Apr 14, 2024

OK. I will create a PR as soon as possible 😉

@willkroboth
Copy link
Collaborator

willkroboth commented Apr 14, 2024

I wonder if this could be added for Java as well.

If I add this method to AbstractCommandTree and AbstractArgumentTree:

public <T, N extends AbstractArgument<T, ?, Argument, CommandSender>> Impl then(final N tree, BiConsumer<Function<CommandArguments, T>, N> consumer) {
	this.arguments.add(tree);
	consumer.accept(args -> args.getUnchecked(tree.getNodeName()), tree);
	return instance();
}

Then this command syntax becomes possible:

new CommandTree("sendMessageTo")
	.then(new PlayerArgument("player"), (getPlayer, playerArgument) -> playerArgument
		.then(new GreedyStringArgument("msg"), (getMessage, messageArgument) -> messageArgument
			.executes((sender, args) -> {
				Player player = getPlayer.apply(args);
				String message = getMessage.apply(args);

				player.sendMessage(message);
			})
		)
	)
	.register();

Java lambdas aren't as clean as Kotlin's, but it still hides the unsafe casting (calling CommandArguments#getUnchecked) from the developer's view.

With AbstractCommandAPICommand, if I add these methods:

public <T> Impl withArgument(AbstractArgument<T, ?, Argument, CommandSender> argument, BiConsumer<Function<CommandArguments, T>, Impl> consumer) {
	this.arguments.add((Argument) argument);
	consumer.accept(args -> args.getUnchecked(argument.getNodeName()), instance());
	return instance();
}

public <T> Impl withOptionalArgument(AbstractArgument<T, ?, Argument, CommandSender> argument, BiConsumer<OptionalArgumentProvider<T>, Impl> consumer) {
	argument.setOptional(true);
	this.arguments.add((Argument) argument);
	consumer.accept(new OptionalArgumentProvider<>(argument), instance());
	return instance();
}

I can make a command like this:

new CommandAPICommand("sayhi")
	.withArgument(new PlayerArgument("target"), (getTarget, command1) -> command1
	.withOptionalArgument(new GreedyStringArgument("message"), (getMessage, command2) -> command2 // This is a little annoying, where the lambda parameters must have different names --- Although command1 and command2 do refer to the same object (`new CommandAPICommand("sayhi")`), so maybe command2 can be removed
	.executes((sender, args) -> {
		// target is not only type-safe, but also guaranteed to be not null
		Player target = getTarget.apply(args);
		target.sendMessage("Hello!");

		Optional<String> message = getMessage.getOptional(args);
		if(message.isPresent()) {
			target.sendMessage(message.get());
		} else {
			target.sendMessage("How are you doing?");
		}
	})))
	.register();

For the optional argument here, I created an OptionalArgumentProvider class, which looks like this:

public class OptionalArgumentProvider<T> {
    private final AbstractArgument<T, ?, ?, ?> argument;

    public OptionalArgumentProvider(AbstractArgument<T, ?, ?, ?> argument) {
        this.argument = argument;
    }

    @Nullable
    public T get(CommandArguments args) {
        return args.getUnchecked(this.argument.getNodeName());
    }

    @NotNull
    public T getOrDefault(CommandArguments args, T defaultValue) {
        return args.getOrDefaultUnchecked(this.argument.getNodeName(), defaultValue);
    }

    @NotNull
    public Optional<T> getOptional(CommandArguments args) {
        return args.getOptionalUnchecked(this.argument.getNodeName());
    }
}

Just a way of giving more options than a basic Function<CommandArguments, T> for this case where there are multiple ways that the developer might choose to access the optional argument.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants