Skip to content

Commit db21c78

Browse files
committed
feat(kotlin): add SuspendingArgumentParser/SuggestionProvider
1 parent 3480d6c commit db21c78

4 files changed

Lines changed: 312 additions & 4 deletions

File tree

cloud-core/src/main/java/cloud/commandframework/arguments/parser/ArgumentParser.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,6 @@ public interface ArgumentParser<C, T> extends SuggestionProvider<C> {
9292
* parsing is. Particular care should be taken when parsing for suggestions, as the parsing
9393
* method is then likely to be called once for every character written by the command sender.
9494
* <p>
95-
* This method should never throw any exceptions under normal circumstances. Instead, if the
96-
* parsing for some reason cannot be done successfully {@link ArgumentParseResult#failure(Throwable)}
97-
* should be returned. This then wraps any exception that should be forwarded to the command sender.
98-
* <p>
9995
* The parser is assumed to be completely stateless and should not store any information about
10096
* the command sender or the command context. Instead, information should be stored in the
10197
* {@link CommandContext}.
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//
2+
// MIT License
3+
//
4+
// Copyright (c) 2022 Alexander Söderberg & Contributors
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in all
14+
// copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
// SOFTWARE.
23+
//
24+
package cloud.commandframework.kotlin.coroutines
25+
26+
import cloud.commandframework.CommandManager
27+
import cloud.commandframework.arguments.parser.ArgumentParser
28+
import cloud.commandframework.arguments.parser.ParserDescriptor
29+
import cloud.commandframework.arguments.suggestion.SuggestionFactory
30+
import cloud.commandframework.context.CommandContext
31+
import cloud.commandframework.context.CommandInput
32+
import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator
33+
import cloud.commandframework.execution.CommandExecutionCoordinator
34+
import kotlinx.coroutines.CoroutineScope
35+
import kotlinx.coroutines.GlobalScope
36+
import kotlinx.coroutines.future.future
37+
import kotlin.coroutines.CoroutineContext
38+
import kotlin.coroutines.EmptyCoroutineContext
39+
40+
/**
41+
* Suspending version of [ArgumentParser] for use with coroutines.
42+
*
43+
* NOTE: It is highly advised to not use [CommandExecutionCoordinator.SimpleCoordinator] together
44+
* with coroutine support. Consider using [AsynchronousCommandExecutionCoordinator] instead.
45+
*
46+
* @param C command sender type.
47+
*/
48+
public fun interface SuspendingArgumentParser<C : Any, T : Any> {
49+
/**
50+
* Returns the result of parsing the given [commandInput].
51+
*
52+
* This method may be called when a command chain is being parsed for execution
53+
* (using [CommandManager.executeCommand])
54+
* or when a command is being parsed to provide context for suggestions
55+
* (using [SuggestionFactory.suggest]).
56+
* It is possible to use [CommandContext.isSuggestions] to see what the purpose of the
57+
* parsing is. Particular care should be taken when parsing for suggestions, as the parsing
58+
* method is then likely to be called once for every character written by the command sender.
59+
*
60+
* The parser is assumed to be completely stateless and should not store any information about
61+
* the command sender or the command context. Instead, information should be stored in the
62+
* [CommandContext].
63+
*
64+
* @param commandContext Command context
65+
* @param commandInput Command Input
66+
* @return the result
67+
*/
68+
public suspend operator fun invoke(commandContext: CommandContext<C>, commandInput: CommandInput): T
69+
70+
/**
71+
* Creates a new [ArgumentParser] backed by this [SuspendingArgumentParser].
72+
*
73+
* @param scope coroutine scope
74+
* @param context coroutine context
75+
* @return new [ArgumentParser]
76+
*/
77+
public fun asArgumentParser(
78+
scope: CoroutineScope = GlobalScope,
79+
context: CoroutineContext = EmptyCoroutineContext
80+
): ArgumentParser<C, T> = createArgumentParser(scope, context, this)
81+
82+
public companion object {
83+
/**
84+
* Creates a new [ArgumentParser] backed by the given [SuspendingArgumentParser].
85+
*
86+
* @param scope coroutine scope
87+
* @param context coroutine context
88+
* @param parser suspending parser
89+
* @return new [ArgumentParser]
90+
*/
91+
public fun <C : Any, T : Any> createArgumentParser(
92+
scope: CoroutineScope = GlobalScope,
93+
context: CoroutineContext = EmptyCoroutineContext,
94+
parser: SuspendingArgumentParser<C, T>
95+
): ArgumentParser<C, T> = ArgumentParser.FutureArgumentParser { ctx, commandInput ->
96+
scope.future(context) {
97+
parser(ctx, commandInput)
98+
}
99+
}
100+
}
101+
}
102+
103+
/**
104+
* Creates a new [ParserDescriptor] backed by this [SuspendingArgumentParser].
105+
*
106+
* @param scope coroutine scope
107+
* @param context coroutine context
108+
* @return the descriptor
109+
*/
110+
public inline fun <C : Any, reified T : Any> SuspendingArgumentParser<C, T>.asParserDescriptor(
111+
scope: CoroutineScope = GlobalScope,
112+
context: CoroutineContext = EmptyCoroutineContext
113+
): ParserDescriptor<C, T> = ParserDescriptor.of(this.asArgumentParser(scope, context), T::class.java)
114+
115+
/**
116+
* Creates a suspending argument parser backed by the given [parser].
117+
*
118+
* @param scope coroutine scope
119+
* @param context coroutine context
120+
* @return the descriptor
121+
*/
122+
public suspend inline fun <C : Any, reified T : Any> suspendingArgumentParser(
123+
scope: CoroutineScope = GlobalScope,
124+
context: CoroutineContext = EmptyCoroutineContext,
125+
crossinline parser: suspend (CommandContext<C>, CommandInput) -> T
126+
): ParserDescriptor<C, T> = SuspendingArgumentParser<C, T> { commandContext, commandInput ->
127+
parser(commandContext, commandInput)
128+
}.asParserDescriptor(scope, context)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//
2+
// MIT License
3+
//
4+
// Copyright (c) 2022 Alexander Söderberg & Contributors
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in all
14+
// copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
// SOFTWARE.
23+
//
24+
package cloud.commandframework.kotlin.coroutines
25+
26+
import cloud.commandframework.arguments.suggestion.Suggestion
27+
import cloud.commandframework.arguments.suggestion.SuggestionProvider
28+
import cloud.commandframework.context.CommandContext
29+
import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator
30+
import cloud.commandframework.execution.CommandExecutionCoordinator
31+
import kotlinx.coroutines.CoroutineScope
32+
import kotlinx.coroutines.GlobalScope
33+
import kotlinx.coroutines.future.future
34+
import kotlin.coroutines.CoroutineContext
35+
import kotlin.coroutines.EmptyCoroutineContext
36+
37+
/**
38+
* Suspending version of [SuggestionProvider] for use with coroutines.
39+
*
40+
* NOTE: It is highly advised to not use [CommandExecutionCoordinator.SimpleCoordinator] together
41+
* with coroutine support. Consider using [AsynchronousCommandExecutionCoordinator] instead.
42+
*
43+
* @param C command sender type.
44+
*/
45+
public fun interface SuspendingSuggestionProvider<C : Any> {
46+
/**
47+
* Returns the suggestions for the given [input].
48+
*
49+
* @param context the context of the suggestion lookup
50+
* @param input the current input
51+
* @return the suggestions
52+
*/
53+
public suspend operator fun invoke(context: CommandContext<C>, input: String): Iterable<Suggestion>
54+
55+
/**
56+
* Creates a new [SuggestionProvider] backed by this [SuspendingExecutionHandler].
57+
*
58+
* @param scope coroutine scope
59+
* @param context coroutine context
60+
* @return new [SuggestionProvider]
61+
*/
62+
public fun asSuggestionProvider(
63+
scope: CoroutineScope = GlobalScope,
64+
context: CoroutineContext = EmptyCoroutineContext
65+
): SuggestionProvider<C> = createSuggestionProvider(scope, context, this)
66+
67+
public companion object {
68+
/**
69+
* Creates a new [SuggestionProvider] backed by the given [SuspendingSuggestionProvider].
70+
*
71+
* @param scope coroutine scope
72+
* @param context coroutine context
73+
* @param provider suspending provider
74+
* @return new [SuggestionProvider]
75+
*/
76+
public fun <C : Any> createSuggestionProvider(
77+
scope: CoroutineScope = GlobalScope,
78+
context: CoroutineContext = EmptyCoroutineContext,
79+
provider: SuspendingSuggestionProvider<C>
80+
): SuggestionProvider<C> = SuggestionProvider.FutureSuggestionProvider { ctx, input ->
81+
scope.future(context) {
82+
provider(ctx, input).toList()
83+
}
84+
}
85+
}
86+
}
87+
88+
/**
89+
* Creates a suspending suggestion provider backed by the given [provider].
90+
*
91+
* @param scope coroutine scope
92+
* @param context coroutine context
93+
* @return the provider
94+
*/
95+
public suspend inline fun <C : Any> suspendingSuggestionProvider(
96+
scope: CoroutineScope = GlobalScope,
97+
context: CoroutineContext = EmptyCoroutineContext,
98+
crossinline provider: suspend (CommandContext<C>, String) -> Iterable<Suggestion>
99+
): SuggestionProvider<C> = SuspendingSuggestionProvider<C> { commandContext, input ->
100+
provider(commandContext, input)
101+
}.asSuggestionProvider(scope, context)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//
2+
// MIT License
3+
//
4+
// Copyright (c) 2022 Alexander Söderberg & Contributors
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in all
14+
// copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
// SOFTWARE.
23+
//
24+
package cloud.commandframework.kotlin.coroutines
25+
26+
import cloud.commandframework.CommandManager
27+
import cloud.commandframework.arguments.suggestion.Suggestion
28+
import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator
29+
import cloud.commandframework.internal.CommandRegistrationHandler
30+
import cloud.commandframework.kotlin.extension.buildAndRegister
31+
import com.google.common.truth.Truth.assertThat
32+
import kotlinx.coroutines.delay
33+
import kotlinx.coroutines.future.await
34+
import kotlinx.coroutines.runBlocking
35+
import org.junit.jupiter.api.Test
36+
import java.util.concurrent.ExecutorService
37+
import java.util.concurrent.Executors
38+
39+
class SuspendingArgumentParserTest {
40+
41+
companion object {
42+
val executorService: ExecutorService = Executors.newSingleThreadExecutor()
43+
}
44+
45+
@Test
46+
fun test(): Unit = runBlocking {
47+
val suspendingParser = suspendingArgumentParser<TestCommandSender, Int> { _, commandInput ->
48+
delay(1L)
49+
commandInput.readInteger()
50+
}
51+
val suspendingSuggestionProvider = suspendingSuggestionProvider<TestCommandSender> { _, _ ->
52+
delay(1L)
53+
(1..3).asSequence().map(Number::toString).map(Suggestion::simple).asIterable()
54+
}
55+
56+
val manager = TestCommandManager()
57+
58+
manager.buildAndRegister("test") {
59+
required("int", suspendingParser) {
60+
suggestionProvider(suspendingSuggestionProvider)
61+
}
62+
}
63+
64+
manager.executeCommand(TestCommandSender(), "test 123").await()
65+
assertThat(manager.suggestionFactory().suggest(TestCommandSender(), "test ").await()).containsExactly(
66+
Suggestion.simple("1"),
67+
Suggestion.simple("2"),
68+
Suggestion.simple("3")
69+
)
70+
}
71+
72+
private class TestCommandSender
73+
74+
private class TestCommandManager : CommandManager<TestCommandSender>(
75+
AsynchronousCommandExecutionCoordinator.builder<TestCommandSender>()
76+
.withExecutor(SuspendingHandlerTest.executorService)
77+
.build(),
78+
CommandRegistrationHandler.nullCommandRegistrationHandler()
79+
) {
80+
81+
override fun hasPermission(sender: TestCommandSender, permission: String): Boolean = true
82+
}
83+
}

0 commit comments

Comments
 (0)