From 7cfcf2124562b49bcb47b6a8017c0e611b6299d3 Mon Sep 17 00:00:00 2001 From: Octal Date: Sat, 15 Jul 2023 18:18:44 -0500 Subject: [PATCH 1/6] Initial 2.0 code --- build.gradle.kts | 7 +- .../dev/proxyfox/command/Annotations.kt | 22 +++ .../dev/proxyfox/command/CommandContext.kt | 2 - .../dev/proxyfox/command/CommandDecoder.kt | 156 ++++++++++++++++++ .../dev/proxyfox/command/CommandParser.kt | 76 ++------- .../kotlin/dev/proxyfox/command/NodeHolder.kt | 14 -- .../kotlin/dev/proxyfox/command/ParseUtil.kt | 60 +++++++ .../dev/proxyfox/command/StringCursor.kt | 4 +- src/main/kotlin/dev/proxyfox/command/Util.kt | 5 - .../dev/proxyfox/command/node/CommandNode.kt | 34 ---- .../dev/proxyfox/command/node/Priority.kt | 23 --- .../command/node/builtin/GreedyNode.kt | 31 ---- .../proxyfox/command/node/builtin/IntNode.kt | 32 ---- .../command/node/builtin/LiteralNode.kt | 54 ------ .../command/node/builtin/StringListNode.kt | 35 ---- .../command/node/builtin/StringNode.kt | 29 ---- .../proxyfox/command/node/builtin/UnixNode.kt | 50 ------ .../kotlin/dev/proxyfox/command/test/Main.kt | 145 +++------------- .../proxyfox/command/test/ZeroWidthNode.kt | 25 --- 19 files changed, 287 insertions(+), 517 deletions(-) create mode 100644 src/main/kotlin/dev/proxyfox/command/Annotations.kt create mode 100644 src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt delete mode 100644 src/main/kotlin/dev/proxyfox/command/NodeHolder.kt create mode 100644 src/main/kotlin/dev/proxyfox/command/ParseUtil.kt delete mode 100644 src/main/kotlin/dev/proxyfox/command/node/CommandNode.kt delete mode 100644 src/main/kotlin/dev/proxyfox/command/node/Priority.kt delete mode 100644 src/main/kotlin/dev/proxyfox/command/node/builtin/GreedyNode.kt delete mode 100644 src/main/kotlin/dev/proxyfox/command/node/builtin/IntNode.kt delete mode 100644 src/main/kotlin/dev/proxyfox/command/node/builtin/LiteralNode.kt delete mode 100644 src/main/kotlin/dev/proxyfox/command/node/builtin/StringListNode.kt delete mode 100644 src/main/kotlin/dev/proxyfox/command/node/builtin/StringNode.kt delete mode 100644 src/main/kotlin/dev/proxyfox/command/node/builtin/UnixNode.kt delete mode 100644 src/test/kotlin/dev/proxyfox/command/test/ZeroWidthNode.kt diff --git a/build.gradle.kts b/build.gradle.kts index 3673672..d81c28f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,12 +1,13 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - kotlin("jvm") version "1.8.10" + kotlin("jvm") version "1.9.0" + kotlin("plugin.serialization") version "1.9.0" `maven-publish` } group = "dev.proxyfox" -version = "1.8" +version = "2.0" repositories { mavenCentral() @@ -17,6 +18,8 @@ kotlin { } dependencies { + implementation(kotlin("reflect")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1") } tasks.withType { diff --git a/src/main/kotlin/dev/proxyfox/command/Annotations.kt b/src/main/kotlin/dev/proxyfox/command/Annotations.kt new file mode 100644 index 0000000..0e2d73d --- /dev/null +++ b/src/main/kotlin/dev/proxyfox/command/Annotations.kt @@ -0,0 +1,22 @@ +package dev.proxyfox.command + +@Target(AnnotationTarget.FUNCTION) +public annotation class Command + +@Target(AnnotationTarget.CLASS, AnnotationTarget.VALUE_PARAMETER) +public annotation class LiteralArgument(public vararg val values: String) + +@Target(AnnotationTarget.VALUE_PARAMETER) +public annotation class Context + +public fun LiteralArgument.getLiteral(cursor: StringCursor): String? { + cursor.checkout() + val string = cursor.extractString(false) + cursor.inc() + if (values.contains(string.lowercase())) { + cursor.commit() + return string.lowercase() + } + cursor.rollback() + return null +} \ No newline at end of file diff --git a/src/main/kotlin/dev/proxyfox/command/CommandContext.kt b/src/main/kotlin/dev/proxyfox/command/CommandContext.kt index b969ddd..0e42985 100644 --- a/src/main/kotlin/dev/proxyfox/command/CommandContext.kt +++ b/src/main/kotlin/dev/proxyfox/command/CommandContext.kt @@ -1,7 +1,5 @@ package dev.proxyfox.command -import dev.proxyfox.command.menu.CommandMenu - public abstract class CommandContext { /** * The command trigger for the context diff --git a/src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt b/src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt new file mode 100644 index 0000000..765e9d2 --- /dev/null +++ b/src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt @@ -0,0 +1,156 @@ +package dev.proxyfox.command + +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.AbstractDecoder +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.encoding.CompositeDecoder.Companion.UNKNOWN_NAME +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.modules.SerializersModule + +public class CommandDecodingException(idx: Int) : Exception("Cannot decode command at $idx") + +public inline fun decode(cursor: StringCursor, context: CommandContext, serializer: KSerializer): T = + CommandDecoder(cursor, context).decodeSerializableValue(serializer) +public inline fun decode(cursor: StringCursor, context: CommandContext): T = + decode(cursor, context, serializer()) + +public class CommandDecoder(private val cursor: StringCursor, public val context: CommandContext) : AbstractDecoder() { + private var elementsCount = 0 + + override val serializersModule: SerializersModule = EmptySerializersModule() + + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + if (elementsCount == descriptor.elementsCount) return CompositeDecoder.DECODE_DONE + return elementsCount++ + } + + override fun decodeString(): String { + if (cursor.end) { + throw CommandDecodingException(cursor.index) + } + val string = cursor.extractString(true) + cursor.inc() + return string + } + + override fun decodeChar(): Char { + cursor.checkout() + val string = decodeString() + if (string.length != 1) { + cursor.rollback() + throw CommandDecodingException(cursor.index) + } + cursor.commit() + return string[0] + } + + override fun decodeBoolean(): Boolean { + cursor.checkout() + val string = cursor.extractString(false).lowercase() + cursor.inc() + if (string != "true" && string != "false"){ + cursor.rollback() + throw CommandDecodingException(cursor.index) + } + cursor.commit() + return string == "true" + } + + override fun decodeLong(): Long { + cursor.checkout() + val num = cursor.extractString(false).toLongOrNull() + cursor.inc() + if (num == null) { + cursor.rollback() + throw CommandDecodingException(cursor.index) + } + cursor.commit() + return num + } + + override fun decodeInt(): Int { + cursor.checkout() + val num = cursor.extractString(false).toIntOrNull() + cursor.inc() + if (num == null) { + cursor.rollback() + throw CommandDecodingException(cursor.index) + } + cursor.commit() + return num + } + + override fun decodeShort(): Short { + cursor.checkout() + val num = cursor.extractString(false).toShortOrNull() + cursor.inc() + if (num == null) { + cursor.rollback() + throw CommandDecodingException(cursor.index) + } + cursor.commit() + return num + } + + override fun decodeByte(): Byte { + cursor.checkout() + val num = cursor.extractString(false).toByteOrNull() + cursor.inc() + if (num == null) { + cursor.rollback() + throw CommandDecodingException(cursor.index) + } + cursor.commit() + return num + } + + override fun decodeDouble(): Double { + cursor.checkout() + val num = cursor.extractString(false).toDoubleOrNull() + cursor.inc() + if (num == null) { + cursor.rollback() + throw CommandDecodingException(cursor.index) + } + cursor.commit() + return num + } + + override fun decodeFloat(): Float { + cursor.checkout() + val num = cursor.extractString(false).toFloatOrNull() + cursor.inc() + if (num == null) { + cursor.rollback() + throw CommandDecodingException(cursor.index) + } + cursor.commit() + return num + } + + override fun decodeEnum(enumDescriptor: SerialDescriptor): Int { + cursor.checkout() + val idx = enumDescriptor.getElementIndex(decodeString()) + if (idx == UNKNOWN_NAME) { + cursor.rollback() + throw CommandDecodingException(cursor.index) + } + cursor.commit() + return idx + } + + override fun decodeNotNullMark(): Boolean { + return false + } + + override fun decodeNullableSerializableValue(deserializer: DeserializationStrategy): T? { + return try { + deserializer.deserialize(this) + } catch (err: CommandDecodingException) { + null + } + } + + override fun decodeSequentially(): Boolean = true +} diff --git a/src/main/kotlin/dev/proxyfox/command/CommandParser.kt b/src/main/kotlin/dev/proxyfox/command/CommandParser.kt index 79fce08..047777c 100644 --- a/src/main/kotlin/dev/proxyfox/command/CommandParser.kt +++ b/src/main/kotlin/dev/proxyfox/command/CommandParser.kt @@ -1,68 +1,26 @@ package dev.proxyfox.command -import dev.proxyfox.command.node.CommandNode -import dev.proxyfox.command.node.builtin.LiteralNode +import kotlin.reflect.KFunction -public class CommandParser>: NodeHolder() { - public suspend fun parse(ctx: C): Boolean? { - val literals = ArrayList>() - val cursor = StringCursor(ctx.command) - for (node in nodes) { - if (node is LiteralNode) literals.add(node) - cursor.checkout() - val parsed = tryParseNode(cursor, node, ctx) - if (parsed == null) { - cursor.rollback() - continue - } - cursor.commit() - return parsed - } - val test = ctx.command.split(" ")[0] - val closest = getLevenshtein(test, literals) ?: return null - if (closest.second == 0) return null - ctx.respondFailure("Command `$test` not found. Did you mean `${closest.first}`? Closeness: ${closest.second}") - return false - } +public class CommandParser> { + public suspend fun parse(ctx: C): Boolean { + for (function in functionMembers) + if (function.parseFunc(ctx)) + return true + + for (clazz in classMembers) + if (clazz.parse(ctx)) + return true - private suspend fun tryParseNode(cursor: StringCursor, node: CommandNode, ctx: C): Boolean? { - // Try parsing the node - val parsed = node.parse(cursor, ctx) - // Return if parsing failed or there's no string left to consume - if (!parsed) return null - // Iterate through sub nodes and try parsing them - val literals = ArrayList>() - for (subNode in node.nodes) { - if (subNode is LiteralNode) literals.add(subNode) - cursor.checkout() - val parsed = tryParseNode(cursor, subNode, ctx) - if (parsed == null) { - cursor.rollback() - continue - } - cursor.commit() - return parsed - } - if (cursor.end) return node.execute(ctx) - val test = cursor.extractString(allowQuotes = false) - val closest = getLevenshtein(test, literals) ?: return node.execute(ctx) - if (closest.second == 0) return node.execute(ctx) - ctx.respondFailure("Command `$test` not found. Did you mean `${closest.first}`?") return false } - private fun getLevenshtein(test: String, literals: ArrayList>): Pair? { - var closest: Pair? = null - for (literal in literals) { - for (str in literal.literals) { - val dist = test.levenshtein(str) - if (closest == null) { - closest = Pair(str, dist) - } else if (closest.second > dist) { - closest = Pair(str, dist) - } - } - } - return closest + private val classMembers = ArrayList() + private val functionMembers = ArrayList>() + + public operator fun plusAssign(value: Any) { + if (value is KFunction<*>) + functionMembers.add(value as KFunction) + else classMembers.add(value) } } diff --git a/src/main/kotlin/dev/proxyfox/command/NodeHolder.kt b/src/main/kotlin/dev/proxyfox/command/NodeHolder.kt deleted file mode 100644 index 9b68108..0000000 --- a/src/main/kotlin/dev/proxyfox/command/NodeHolder.kt +++ /dev/null @@ -1,14 +0,0 @@ -package dev.proxyfox.command - -import dev.proxyfox.command.node.CommandNode - -public open class NodeHolder> { - public val nodes: ArrayList> = arrayListOf() - - public fun > addNode(node: N) { - nodes.add(node) - nodes.sortBy { - it.priority.value - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/proxyfox/command/ParseUtil.kt b/src/main/kotlin/dev/proxyfox/command/ParseUtil.kt new file mode 100644 index 0000000..fd40f4a --- /dev/null +++ b/src/main/kotlin/dev/proxyfox/command/ParseUtil.kt @@ -0,0 +1,60 @@ +package dev.proxyfox.command + +import kotlinx.serialization.serializer +import kotlin.reflect.* +import kotlin.reflect.full.callSuspendBy +import kotlin.reflect.full.functions + +public suspend fun C.parse(context: CommandContext): Boolean { + return this::class.functions.firstNotNullOfOrNull { + it.parseFunc(context, this) + } ?: false +} + +public suspend fun , R> F.parseFunc(context: CommandContext, base: Any? = null): Boolean { + annotations.find { it is Command } ?: return false + + val cursor = StringCursor(context.command) + + val map = HashMap() + + if (base != null) { + val arg = base::class.annotations.find { it is LiteralArgument } as? LiteralArgument + if (arg != null && arg.getLiteral(cursor) == null) { + return false + } + } + + for (parameter in parameters) { + if (parameter == parameters[0] && base != null) { + map[parameter] = base + continue + } + if (parameter.annotations.find { it is Context } != null) { + map[parameter] = context + continue + } + val literal = parameter.annotations.find { it is LiteralArgument } as? LiteralArgument? + if (literal != null) { + literal.getLiteral(cursor) ?: return false + map[parameter] = Unit + continue + } + val value = try { + decode(cursor, context as CommandContext, serializer(parameter.type)) + } catch (err: CommandDecodingException) { + if (parameter.isOptional || parameter.type.isMarkedNullable) + null + else return false + } + map[parameter] = value + if (value != null) + context[parameter.name!!] = value + } + + if (isSuspend) + callSuspendBy(map) + else callBy(map) + + return true +} diff --git a/src/main/kotlin/dev/proxyfox/command/StringCursor.kt b/src/main/kotlin/dev/proxyfox/command/StringCursor.kt index 28c0685..705158f 100644 --- a/src/main/kotlin/dev/proxyfox/command/StringCursor.kt +++ b/src/main/kotlin/dev/proxyfox/command/StringCursor.kt @@ -4,7 +4,8 @@ import java.util.ArrayList import kotlin.math.abs public class StringCursor(public val command: String) { - private var index = 0 + public var index: Int = 0 + private set public val beginning: Boolean get() = index <= 0 public val end: Boolean get() = index >= command.length @@ -36,7 +37,6 @@ public class StringCursor(public val command: String) { return this } - @OptIn(ExperimentalStdlibApi::class) public operator fun plusAssign(amount: Int) { for (i in 0.. 0) inc() diff --git a/src/main/kotlin/dev/proxyfox/command/Util.kt b/src/main/kotlin/dev/proxyfox/command/Util.kt index 59e0fc0..900e4a5 100644 --- a/src/main/kotlin/dev/proxyfox/command/Util.kt +++ b/src/main/kotlin/dev/proxyfox/command/Util.kt @@ -2,15 +2,10 @@ package dev.proxyfox.command import dev.proxyfox.command.menu.CommandMenu import dev.proxyfox.command.menu.CommandScreen -import dev.proxyfox.command.node.CommandNode import kotlin.math.min public typealias Executor = suspend CommandContext.() -> Boolean -public typealias NodeActionParam = suspend CommandNode.(ParamGetter) -> Unit - -public typealias NodeAction = suspend CommandNode.() -> Unit - public typealias ParamGetter = suspend CommandContext.() -> V public typealias MenuBuilder = suspend CommandMenu.() -> Unit diff --git a/src/main/kotlin/dev/proxyfox/command/node/CommandNode.kt b/src/main/kotlin/dev/proxyfox/command/node/CommandNode.kt deleted file mode 100644 index dd5bd5b..0000000 --- a/src/main/kotlin/dev/proxyfox/command/node/CommandNode.kt +++ /dev/null @@ -1,34 +0,0 @@ -package dev.proxyfox.command.node - -import dev.proxyfox.command.CommandContext -import dev.proxyfox.command.Executor -import dev.proxyfox.command.NodeHolder -import dev.proxyfox.command.StringCursor - -public abstract class CommandNode>: NodeHolder() { - /** - * The priority of the node - * */ - public abstract val priority: Priority - - public abstract val name: String - - private var executor: Executor? = null - - /** - * Parses the node - * @param cursor The cursor, at the current index - * @param ctx The command context - * @return Whether the parsing succeeded - * */ - public abstract fun parse(cursor: StringCursor, ctx: C): Boolean - - public fun executes(executor: Executor) { - this.executor = executor - } - - public suspend fun execute(ctx: C): Boolean? { - val ex = executor ?: return null - return ctx.ex() - } -} diff --git a/src/main/kotlin/dev/proxyfox/command/node/Priority.kt b/src/main/kotlin/dev/proxyfox/command/node/Priority.kt deleted file mode 100644 index 7e07135..0000000 --- a/src/main/kotlin/dev/proxyfox/command/node/Priority.kt +++ /dev/null @@ -1,23 +0,0 @@ -package dev.proxyfox.command.node - -public enum class Priority(public val value: Int) { - /** - * Static string, never changes - * */ - STATIC(0), - - /** - * Limited charset - * */ - SEMI_STATIC(1), - - /** - * Non-limited charset - * */ - VARIABLE(2), - - /** - * Takes up rest of command - * */ - GREEDY(3) -} diff --git a/src/main/kotlin/dev/proxyfox/command/node/builtin/GreedyNode.kt b/src/main/kotlin/dev/proxyfox/command/node/builtin/GreedyNode.kt deleted file mode 100644 index 4563004..0000000 --- a/src/main/kotlin/dev/proxyfox/command/node/builtin/GreedyNode.kt +++ /dev/null @@ -1,31 +0,0 @@ -package dev.proxyfox.command.node.builtin - -import dev.proxyfox.command.CommandContext -import dev.proxyfox.command.NodeActionParam -import dev.proxyfox.command.NodeHolder -import dev.proxyfox.command.StringCursor -import dev.proxyfox.command.node.CommandNode -import dev.proxyfox.command.node.Priority -import java.lang.NullPointerException - -public class GreedyNode>(override val name: String): CommandNode() { - override val priority: Priority = Priority.GREEDY - - override fun parse(cursor: StringCursor, ctx: C): Boolean { - if (cursor.end) return false - ctx[name] = cursor.seekToEnd() - return true - } -} - -public suspend fun > NodeHolder.greedy( - name: String, - action: NodeActionParam -): CommandNode { - val node = GreedyNode(name) - node.action { - this[name] ?: throw NullPointerException("Parameter $name for $command is null!") - } - addNode(node) - return node -} diff --git a/src/main/kotlin/dev/proxyfox/command/node/builtin/IntNode.kt b/src/main/kotlin/dev/proxyfox/command/node/builtin/IntNode.kt deleted file mode 100644 index 059cba4..0000000 --- a/src/main/kotlin/dev/proxyfox/command/node/builtin/IntNode.kt +++ /dev/null @@ -1,32 +0,0 @@ -package dev.proxyfox.command.node.builtin - -import dev.proxyfox.command.CommandContext -import dev.proxyfox.command.NodeActionParam -import dev.proxyfox.command.NodeHolder -import dev.proxyfox.command.StringCursor -import dev.proxyfox.command.node.CommandNode -import dev.proxyfox.command.node.Priority -import java.lang.NullPointerException - -public class IntNode>(override val name: String): CommandNode() { - override val priority: Priority = Priority.SEMI_STATIC - - override fun parse(cursor: StringCursor, ctx: C): Boolean { - if (cursor.end) return false - ctx[name] = cursor.extractString(true).toULongOrNull() ?: return false - cursor.inc() - return true - } -} - -public suspend fun > NodeHolder.int( - name: String, - action: NodeActionParam -): CommandNode { - val node = IntNode(name) - node.action { - this[name] ?: throw NullPointerException("Parameter $name for $command is null!") - } - addNode(node) - return node -} diff --git a/src/main/kotlin/dev/proxyfox/command/node/builtin/LiteralNode.kt b/src/main/kotlin/dev/proxyfox/command/node/builtin/LiteralNode.kt deleted file mode 100644 index 7c84b77..0000000 --- a/src/main/kotlin/dev/proxyfox/command/node/builtin/LiteralNode.kt +++ /dev/null @@ -1,54 +0,0 @@ -package dev.proxyfox.command.node.builtin - -import dev.proxyfox.command.CommandContext -import dev.proxyfox.command.NodeAction -import dev.proxyfox.command.node.CommandNode -import dev.proxyfox.command.node.Priority -import dev.proxyfox.command.NodeHolder -import dev.proxyfox.command.StringCursor - - -public class LiteralNode>(public val literals: Array): CommandNode() { - init { - for (literal in literals) - if (literal.contains(" ")) - throw IllegalArgumentException("Literal `$literal` contains a space!") - } - - override val priority: Priority = Priority.STATIC - override val name: String = "" - - override fun parse(cursor: StringCursor, ctx: C): Boolean { - val str = cursor.extractString(false) - cursor.inc() - for (literal in literals) { - if (literal.lowercase() == str.lowercase()) return true - } - return false - } -} - -public suspend fun > NodeHolder.literal( - vararg names: String, - action: NodeAction -): CommandNode { - val node = LiteralNode(names) - node.action() - addNode(node) - return node -} - -public suspend fun > NodeHolder.unixLiteral( - vararg names: String, - action: NodeAction -): CommandNode { - val newNames = ArrayList() - names.forEach { - newNames.add("-$it") - newNames.add("--$it") - } - val node = LiteralNode(newNames.toTypedArray()) - node.action() - addNode(node) - return node -} diff --git a/src/main/kotlin/dev/proxyfox/command/node/builtin/StringListNode.kt b/src/main/kotlin/dev/proxyfox/command/node/builtin/StringListNode.kt deleted file mode 100644 index 6b60bc6..0000000 --- a/src/main/kotlin/dev/proxyfox/command/node/builtin/StringListNode.kt +++ /dev/null @@ -1,35 +0,0 @@ -package dev.proxyfox.command.node.builtin - -import dev.proxyfox.command.CommandContext -import dev.proxyfox.command.NodeActionParam -import dev.proxyfox.command.NodeHolder -import dev.proxyfox.command.StringCursor -import dev.proxyfox.command.node.CommandNode -import dev.proxyfox.command.node.Priority - -public class StringListNode>(override val name: String): CommandNode() { - override val priority: Priority = Priority.GREEDY - - override fun parse(cursor: StringCursor, ctx: C): Boolean { - if (cursor.end) return false - val arr = ArrayList() - while (!cursor.end) { - arr += cursor.extractString(true) - cursor.inc() - } - ctx[name] = arr - return true - } -} - -public suspend fun > NodeHolder.stringList( - name: String, - action: NodeActionParam> -): CommandNode { - val node = StringListNode(name) - node.action { - this[name] ?: throw NullPointerException("Parameter $name for $command is null!") - } - addNode(node) - return node -} diff --git a/src/main/kotlin/dev/proxyfox/command/node/builtin/StringNode.kt b/src/main/kotlin/dev/proxyfox/command/node/builtin/StringNode.kt deleted file mode 100644 index f0a3c3e..0000000 --- a/src/main/kotlin/dev/proxyfox/command/node/builtin/StringNode.kt +++ /dev/null @@ -1,29 +0,0 @@ -package dev.proxyfox.command.node.builtin - -import dev.proxyfox.command.* -import dev.proxyfox.command.node.CommandNode -import dev.proxyfox.command.node.Priority -import java.lang.NullPointerException - -public class StringNode>(override val name: String): CommandNode() { - override val priority: Priority = Priority.VARIABLE - - override fun parse(cursor: StringCursor, ctx: C): Boolean { - if (cursor.end) return false - ctx[name] = cursor.extractString(true) - cursor.inc() - return true - } -} - -public suspend fun > NodeHolder.string( - name: String, - action: NodeActionParam -): CommandNode { - val node = StringNode(name) - node.action { - this[name] ?: throw NullPointerException("Parameter $name for $command is null!") - } - addNode(node) - return node -} diff --git a/src/main/kotlin/dev/proxyfox/command/node/builtin/UnixNode.kt b/src/main/kotlin/dev/proxyfox/command/node/builtin/UnixNode.kt deleted file mode 100644 index 31fea37..0000000 --- a/src/main/kotlin/dev/proxyfox/command/node/builtin/UnixNode.kt +++ /dev/null @@ -1,50 +0,0 @@ -package dev.proxyfox.command.node.builtin - -import dev.proxyfox.command.CommandContext -import dev.proxyfox.command.NodeActionParam -import dev.proxyfox.command.NodeHolder -import dev.proxyfox.command.StringCursor -import dev.proxyfox.command.node.CommandNode -import dev.proxyfox.command.node.Priority -import java.lang.NullPointerException - -public class UnixNode>(override val name: String): CommandNode() { - override val priority: Priority = Priority.SEMI_STATIC - - override fun parse(cursor: StringCursor, ctx: C): Boolean { - if (cursor.end) return false - val arr = ArrayList() - - while (!cursor.end) { - cursor.checkout() - val str = cursor.extractString(false) - arr += if (str.startsWith("--")) { - str.substring(2) - } else if (str.startsWith("-")) { - str.substring(1) - } else { - cursor.rollback() - break - } - cursor.commit() - cursor.inc() - } - - if (arr.isEmpty()) return false - ctx[name] = arr - cursor.inc() - return true - } -} - -public suspend fun > NodeHolder.unix( - name: String, - action: NodeActionParam> -): CommandNode { - val node = UnixNode(name) - node.action { - this[name] ?: throw NullPointerException("Parameter $name for $command is null!") - } - addNode(node) - return node -} diff --git a/src/test/kotlin/dev/proxyfox/command/test/Main.kt b/src/test/kotlin/dev/proxyfox/command/test/Main.kt index 1170527..d709893 100644 --- a/src/test/kotlin/dev/proxyfox/command/test/Main.kt +++ b/src/test/kotlin/dev/proxyfox/command/test/Main.kt @@ -1,126 +1,31 @@ package dev.proxyfox.command.test -import dev.proxyfox.command.CommandParser -import dev.proxyfox.command.node.builtin.* +import dev.proxyfox.command.* +import kotlinx.serialization.Serializable + +@Serializable +data class Test( + val test: String, + val uwu: Int?, + val owo: TestInner +) + +@Serializable +data class TestInner( + val nya: String, +) + +@LiteralArgument("nya") +object TestCommandGroup { + @Command + suspend fun test(@Context ctx: CommandContext, @LiteralArgument("owo", "uwu") owo: Unit, uwu: Test) { + println(uwu) + println(ctx.command) + } +} suspend fun main() { val parser = CommandParser() - - parser.literal("test") { - executes { - respondPlain("It didn't work.") - false - } - literal("int") { - int("someInt") { - executes { - respondSuccess("${it()}") - true - } - } - } - - literal("string") { - string("someString") { - executes { - respondSuccess(it()) - true - } - } - } - - literal("stringlist") { - stringList("someStringList") { - executes { - for (i in it()) { - respondSuccess(i) - } - true - } - } - } - - literal("greedy") { - greedy("someGreedy") { - executes { - respondSuccess(it()) - true - } - } - } - - literal("unix") { - unix("someUnix") { - executes { - for (i in it()) { - respondSuccess(i) - } - true - } - } - } - - literal("unixliteral") { - unixLiteral("test") { - executes { - respondSuccess("It worked!") - true - } - } - } - - literal("zw") { - zw("test") { - executes { - respondSuccess("It worked!") - true - } - literal("owo") { - executes { - respondSuccess("Ran in zw!") - true - } - } - } - } - - literal("menu") { - executes { - menu { - val other = "other" { - button("someOtherButton") { - println("Yay!") - close() - } - } - default("owo") { - button("someButton") { - println("Button pressed!") - setScreen(other) - } - } - } - true - } - } - } - println("testing unixLiteral") - parser.parse(StringContext("test unixliteral -test")) - println("testing int") - parser.parse(StringContext("test int 15")) - println("testing string") - parser.parse(StringContext("test string owo")) - println("testing unix") - parser.parse(StringContext("test unix --owo -uwu --nya")) - println("testing stringList") - parser.parse(StringContext("test stringlist owo uwu")) - println("testing greedy") - parser.parse(StringContext("test greedy owo owo uwu")) - println("testing zero width") - parser.parse(StringContext("test zw")) - parser.parse(StringContext("test zw owo")) - println("testing menu") - parser.parse(StringContext("test menu")) - println("testing levenshtein distance") - parser.parse(StringContext("twst")) + parser += TestCommandGroup + parser.parse(StringContext("nya owo uwu 15 nya")) } diff --git a/src/test/kotlin/dev/proxyfox/command/test/ZeroWidthNode.kt b/src/test/kotlin/dev/proxyfox/command/test/ZeroWidthNode.kt deleted file mode 100644 index f75a4fe..0000000 --- a/src/test/kotlin/dev/proxyfox/command/test/ZeroWidthNode.kt +++ /dev/null @@ -1,25 +0,0 @@ -package dev.proxyfox.command.test - -import dev.proxyfox.command.CommandContext -import dev.proxyfox.command.NodeAction -import dev.proxyfox.command.NodeHolder -import dev.proxyfox.command.StringCursor -import dev.proxyfox.command.node.CommandNode -import dev.proxyfox.command.node.Priority - -class ZeroWidthNode>(override val name: String) : CommandNode() { - override val priority: Priority = Priority.SEMI_STATIC - override fun parse(cursor: StringCursor, ctx: C): Boolean { - return true - } -} - -public suspend fun > NodeHolder.zw( - name: String, - action: NodeAction -): CommandNode { - val node = ZeroWidthNode(name) - node.action() - addNode(node) - return node -} From 13e7c2ed0baf37a0ec7119cee54a7f20a2b0abcd Mon Sep 17 00:00:00 2001 From: Octal Date: Sat, 15 Jul 2023 19:11:18 -0500 Subject: [PATCH 2/6] Add back GreedyString, StringList, and UnixList --- .../dev/proxyfox/command/CommandDecoder.kt | 2 +- .../command/types/CommandSerializer.kt | 17 ++++++++ .../proxyfox/command/types/GreedyString.kt | 26 +++++++++++++ .../dev/proxyfox/command/types/StringList.kt | 31 +++++++++++++++ .../dev/proxyfox/command/types/UnixList.kt | 39 +++++++++++++++++++ .../kotlin/dev/proxyfox/command/test/Main.kt | 18 +++++++-- 6 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/dev/proxyfox/command/types/CommandSerializer.kt create mode 100644 src/main/kotlin/dev/proxyfox/command/types/GreedyString.kt create mode 100644 src/main/kotlin/dev/proxyfox/command/types/StringList.kt create mode 100644 src/main/kotlin/dev/proxyfox/command/types/UnixList.kt diff --git a/src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt b/src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt index 765e9d2..17fb5d6 100644 --- a/src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt +++ b/src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt @@ -15,7 +15,7 @@ public inline fun decode(cursor: StringCursor, context: CommandConte public inline fun decode(cursor: StringCursor, context: CommandContext): T = decode(cursor, context, serializer()) -public class CommandDecoder(private val cursor: StringCursor, public val context: CommandContext) : AbstractDecoder() { +public class CommandDecoder(public val cursor: StringCursor, public val context: CommandContext) : AbstractDecoder() { private var elementsCount = 0 override val serializersModule: SerializersModule = EmptySerializersModule() diff --git a/src/main/kotlin/dev/proxyfox/command/types/CommandSerializer.kt b/src/main/kotlin/dev/proxyfox/command/types/CommandSerializer.kt new file mode 100644 index 0000000..ddd8be6 --- /dev/null +++ b/src/main/kotlin/dev/proxyfox/command/types/CommandSerializer.kt @@ -0,0 +1,17 @@ +package dev.proxyfox.command.types + +import dev.proxyfox.command.CommandDecoder +import kotlinx.serialization.KSerializer +import kotlinx.serialization.encoding.Decoder + +public interface CommandSerializer : KSerializer { + public fun decodeCommand(decoder: CommandDecoder): T + + public fun decodeRegular(decoder: Decoder): T + + override fun deserialize(decoder: Decoder): T { + return if (decoder is CommandDecoder) + decodeCommand(decoder) + else decodeRegular(decoder) + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/proxyfox/command/types/GreedyString.kt b/src/main/kotlin/dev/proxyfox/command/types/GreedyString.kt new file mode 100644 index 0000000..9b681e5 --- /dev/null +++ b/src/main/kotlin/dev/proxyfox/command/types/GreedyString.kt @@ -0,0 +1,26 @@ +package dev.proxyfox.command.types + +import dev.proxyfox.command.CommandDecoder +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@JvmInline +@Serializable(with = GreedyStringSerializer::class) +public value class GreedyString(public val value: String) + +private class GreedyStringSerializer : CommandSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("GreedyString", PrimitiveKind.STRING) + + override fun decodeCommand(decoder: CommandDecoder): GreedyString = GreedyString(decoder.cursor.seekToEnd()) + + override fun decodeRegular(decoder: Decoder): GreedyString = GreedyString(decoder.decodeString()) + + override fun serialize(encoder: Encoder, value: GreedyString) { + encoder.encodeString(value.value) + } +} diff --git a/src/main/kotlin/dev/proxyfox/command/types/StringList.kt b/src/main/kotlin/dev/proxyfox/command/types/StringList.kt new file mode 100644 index 0000000..cb870e9 --- /dev/null +++ b/src/main/kotlin/dev/proxyfox/command/types/StringList.kt @@ -0,0 +1,31 @@ +package dev.proxyfox.command.types + +import dev.proxyfox.command.CommandDecoder +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.serializer + +@JvmInline +@Serializable(with = StringListSerializer::class) +public value class StringList(public val list: List) + +private class StringListSerializer : CommandSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("StringList", PrimitiveKind.STRING) + + override fun decodeCommand(decoder: CommandDecoder): StringList { + val list = ArrayList() + while (!decoder.cursor.end) + list.add(decoder.decodeString()) + return StringList(list) + } + + override fun decodeRegular(decoder: Decoder): StringList = StringList(decoder.decodeSerializableValue(serializer>())) + + override fun serialize(encoder: Encoder, value: StringList) { + encoder.encodeSerializableValue(serializer>(), value.list) + } +} diff --git a/src/main/kotlin/dev/proxyfox/command/types/UnixList.kt b/src/main/kotlin/dev/proxyfox/command/types/UnixList.kt new file mode 100644 index 0000000..72a8a4d --- /dev/null +++ b/src/main/kotlin/dev/proxyfox/command/types/UnixList.kt @@ -0,0 +1,39 @@ +package dev.proxyfox.command.types + +import dev.proxyfox.command.CommandDecoder +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.serializer + +@JvmInline +@Serializable(with = UnixListSerializer::class) +public value class UnixList(public val list: List) + +private class UnixListSerializer : CommandSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UnixList", PrimitiveKind.STRING) + + override fun decodeCommand(decoder: CommandDecoder): UnixList { + val list = ArrayList() + while (!decoder.cursor.end) { + decoder.cursor.checkout() + val string = decoder.decodeString() + if (!string.startsWith("-")) { + decoder.cursor.rollback() + break + } + list.add(string.replace(Regex("--?"), "")) + decoder.cursor.commit() + } + return UnixList(list) + } + + override fun decodeRegular(decoder: Decoder): UnixList = UnixList(decoder.decodeSerializableValue(serializer>())) + + override fun serialize(encoder: Encoder, value: UnixList) { + encoder.encodeSerializableValue(serializer>(), value.list) + } +} diff --git a/src/test/kotlin/dev/proxyfox/command/test/Main.kt b/src/test/kotlin/dev/proxyfox/command/test/Main.kt index d709893..af94959 100644 --- a/src/test/kotlin/dev/proxyfox/command/test/Main.kt +++ b/src/test/kotlin/dev/proxyfox/command/test/Main.kt @@ -1,6 +1,8 @@ package dev.proxyfox.command.test import dev.proxyfox.command.* +import dev.proxyfox.command.types.GreedyString +import dev.proxyfox.command.types.UnixList import kotlinx.serialization.Serializable @Serializable @@ -12,20 +14,28 @@ data class Test( @Serializable data class TestInner( - val nya: String, + val nya: UnixList, ) @LiteralArgument("nya") object TestCommandGroup { @Command - suspend fun test(@Context ctx: CommandContext, @LiteralArgument("owo", "uwu") owo: Unit, uwu: Test) { + suspend fun test( + @Context ctx: CommandContext, + @LiteralArgument("owo", "uwu") _literal: Unit, + owo: String, + uwu: UnixList, + nya: GreedyString + ) { + println("Input: ${ctx.command}") + println(owo) println(uwu) - println(ctx.command) + println(nya) } } suspend fun main() { val parser = CommandParser() parser += TestCommandGroup - parser.parse(StringContext("nya owo uwu 15 nya")) + parser.parse(StringContext("nya owo uwu -awoo --arf 15 nya")) } From 88ea1d47a25db42f11f597309f9e30f5ae68ee4c Mon Sep 17 00:00:00 2001 From: Octal Date: Sat, 15 Jul 2023 20:20:27 -0500 Subject: [PATCH 3/6] Keep track of errors --- build.gradle.kts | 1 + .../dev/proxyfox/command/CommandDecoder.kt | 24 +++---- .../dev/proxyfox/command/CommandParser.kt | 27 +++++--- .../kotlin/dev/proxyfox/command/ParseUtil.kt | 50 ++++++++++---- .../proxyfox/command/types/GreedyString.kt | 6 +- .../dev/proxyfox/command/types/StringList.kt | 6 +- .../dev/proxyfox/command/types/UnixList.kt | 6 +- .../kotlin/dev/proxyfox/command/test/Main.kt | 66 ++++++++++++------- 8 files changed, 129 insertions(+), 57 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index d81c28f..f2a4656 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,6 +20,7 @@ kotlin { dependencies { implementation(kotlin("reflect")) implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1") + implementation("io.arrow-kt:arrow-core:1.2.0") } tasks.withType { diff --git a/src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt b/src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt index 17fb5d6..db6dd43 100644 --- a/src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt +++ b/src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt @@ -20,15 +20,15 @@ public class CommandDecoder(public val cursor: StringCursor, public val context: override val serializersModule: SerializersModule = EmptySerializersModule() + public fun fails(): Nothing = throw CommandDecodingException(cursor.index) + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { if (elementsCount == descriptor.elementsCount) return CompositeDecoder.DECODE_DONE return elementsCount++ } override fun decodeString(): String { - if (cursor.end) { - throw CommandDecodingException(cursor.index) - } + if (cursor.end) fails() val string = cursor.extractString(true) cursor.inc() return string @@ -39,7 +39,7 @@ public class CommandDecoder(public val cursor: StringCursor, public val context: val string = decodeString() if (string.length != 1) { cursor.rollback() - throw CommandDecodingException(cursor.index) + fails() } cursor.commit() return string[0] @@ -51,7 +51,7 @@ public class CommandDecoder(public val cursor: StringCursor, public val context: cursor.inc() if (string != "true" && string != "false"){ cursor.rollback() - throw CommandDecodingException(cursor.index) + fails() } cursor.commit() return string == "true" @@ -63,7 +63,7 @@ public class CommandDecoder(public val cursor: StringCursor, public val context: cursor.inc() if (num == null) { cursor.rollback() - throw CommandDecodingException(cursor.index) + fails() } cursor.commit() return num @@ -75,7 +75,7 @@ public class CommandDecoder(public val cursor: StringCursor, public val context: cursor.inc() if (num == null) { cursor.rollback() - throw CommandDecodingException(cursor.index) + fails() } cursor.commit() return num @@ -87,7 +87,7 @@ public class CommandDecoder(public val cursor: StringCursor, public val context: cursor.inc() if (num == null) { cursor.rollback() - throw CommandDecodingException(cursor.index) + fails() } cursor.commit() return num @@ -99,7 +99,7 @@ public class CommandDecoder(public val cursor: StringCursor, public val context: cursor.inc() if (num == null) { cursor.rollback() - throw CommandDecodingException(cursor.index) + fails() } cursor.commit() return num @@ -111,7 +111,7 @@ public class CommandDecoder(public val cursor: StringCursor, public val context: cursor.inc() if (num == null) { cursor.rollback() - throw CommandDecodingException(cursor.index) + fails() } cursor.commit() return num @@ -123,7 +123,7 @@ public class CommandDecoder(public val cursor: StringCursor, public val context: cursor.inc() if (num == null) { cursor.rollback() - throw CommandDecodingException(cursor.index) + fails() } cursor.commit() return num @@ -134,7 +134,7 @@ public class CommandDecoder(public val cursor: StringCursor, public val context: val idx = enumDescriptor.getElementIndex(decodeString()) if (idx == UNKNOWN_NAME) { cursor.rollback() - throw CommandDecodingException(cursor.index) + fails() } cursor.commit() return idx diff --git a/src/main/kotlin/dev/proxyfox/command/CommandParser.kt b/src/main/kotlin/dev/proxyfox/command/CommandParser.kt index 047777c..859529e 100644 --- a/src/main/kotlin/dev/proxyfox/command/CommandParser.kt +++ b/src/main/kotlin/dev/proxyfox/command/CommandParser.kt @@ -1,18 +1,29 @@ package dev.proxyfox.command +import arrow.core.Either +import arrow.core.left +import arrow.core.right import kotlin.reflect.KFunction public class CommandParser> { - public suspend fun parse(ctx: C): Boolean { - for (function in functionMembers) - if (function.parseFunc(ctx)) - return true + public suspend fun parse(ctx: C): Either> { + val errors = ArrayList() - for (clazz in classMembers) - if (clazz.parse(ctx)) - return true + for (function in functionMembers) { + val result = function.parseFunc(ctx) + if (result.isLeft()) + return Unit.left() + errors.add(result.getOrNull()!!) + } - return false + for (clazz in classMembers) { + val result = clazz.parse(ctx) + if (result.isLeft()) + return Unit.left() + errors.addAll(result.getOrNull()!!) + } + errors.sortBy { -it.ordinal } + return errors.right() } private val classMembers = ArrayList() diff --git a/src/main/kotlin/dev/proxyfox/command/ParseUtil.kt b/src/main/kotlin/dev/proxyfox/command/ParseUtil.kt index fd40f4a..635ce95 100644 --- a/src/main/kotlin/dev/proxyfox/command/ParseUtil.kt +++ b/src/main/kotlin/dev/proxyfox/command/ParseUtil.kt @@ -1,18 +1,43 @@ package dev.proxyfox.command +import arrow.core.Either +import arrow.core.left +import arrow.core.right import kotlinx.serialization.serializer import kotlin.reflect.* import kotlin.reflect.full.callSuspendBy import kotlin.reflect.full.functions -public suspend fun C.parse(context: CommandContext): Boolean { - return this::class.functions.firstNotNullOfOrNull { - it.parseFunc(context, this) - } ?: false +public class ParseError( + public val function: KFunction, + public val ordinal: Int, + public val parameter: KParameter?, +) { + override fun toString(): String = + if (parameter != null) + "${function.name}[$ordinal]:${parameter.name}" + else "${function.name}[$ordinal]" + + public companion object { + public const val NOT_COMMAND: Int = -2 + public const val ROOT_LITERAL_NOT_MATCH: Int = -1 + } +} + +public suspend fun C.parse(context: CommandContext): Either> { + val functions = this::class.functions + val errors = ArrayList() + for (function in functions) { + val result = function.parseFunc(context, this) + if (result.isLeft()) + return Unit.left() + errors.add(result.getOrNull()!!) + } + return errors.right() } -public suspend fun , R> F.parseFunc(context: CommandContext, base: Any? = null): Boolean { - annotations.find { it is Command } ?: return false +public suspend fun , R> F.parseFunc(context: CommandContext, base: Any? = null): Either { + annotations.find { it is Command } ?: return ParseError(this as KFunction, ParseError.NOT_COMMAND, null).right() val cursor = StringCursor(context.command) @@ -21,12 +46,13 @@ public suspend fun , R> F.parseFunc(context: CommandContext, ParseError.ROOT_LITERAL_NOT_MATCH, null).right() } } - for (parameter in parameters) { - if (parameter == parameters[0] && base != null) { + for (i in parameters.indices) { + val parameter = parameters[i] + if (i == 0 && base != null) { map[parameter] = base continue } @@ -36,7 +62,7 @@ public suspend fun , R> F.parseFunc(context: CommandContext, i, parameter).right() map[parameter] = Unit continue } @@ -45,7 +71,7 @@ public suspend fun , R> F.parseFunc(context: CommandContext, i, parameter).right() } map[parameter] = value if (value != null) @@ -56,5 +82,5 @@ public suspend fun , R> F.parseFunc(context: CommandContext { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("GreedyString", PrimitiveKind.STRING) diff --git a/src/main/kotlin/dev/proxyfox/command/types/StringList.kt b/src/main/kotlin/dev/proxyfox/command/types/StringList.kt index cb870e9..b613db0 100644 --- a/src/main/kotlin/dev/proxyfox/command/types/StringList.kt +++ b/src/main/kotlin/dev/proxyfox/command/types/StringList.kt @@ -11,7 +11,11 @@ import kotlinx.serialization.serializer @JvmInline @Serializable(with = StringListSerializer::class) -public value class StringList(public val list: List) +public value class StringList(public val list: List) { + override fun toString(): String { + return list.toString() + } +} private class StringListSerializer : CommandSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("StringList", PrimitiveKind.STRING) diff --git a/src/main/kotlin/dev/proxyfox/command/types/UnixList.kt b/src/main/kotlin/dev/proxyfox/command/types/UnixList.kt index 72a8a4d..ddd68bd 100644 --- a/src/main/kotlin/dev/proxyfox/command/types/UnixList.kt +++ b/src/main/kotlin/dev/proxyfox/command/types/UnixList.kt @@ -11,7 +11,11 @@ import kotlinx.serialization.serializer @JvmInline @Serializable(with = UnixListSerializer::class) -public value class UnixList(public val list: List) +public value class UnixList(public val list: List) { + override fun toString(): String { + return list.toString() + } +} private class UnixListSerializer : CommandSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UnixList", PrimitiveKind.STRING) diff --git a/src/test/kotlin/dev/proxyfox/command/test/Main.kt b/src/test/kotlin/dev/proxyfox/command/test/Main.kt index af94959..edd580e 100644 --- a/src/test/kotlin/dev/proxyfox/command/test/Main.kt +++ b/src/test/kotlin/dev/proxyfox/command/test/Main.kt @@ -1,41 +1,63 @@ package dev.proxyfox.command.test import dev.proxyfox.command.* +import dev.proxyfox.command.types.CommandSerializer import dev.proxyfox.command.types.GreedyString import dev.proxyfox.command.types.UnixList import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder -@Serializable -data class Test( - val test: String, - val uwu: Int?, - val owo: TestInner -) +@JvmInline +@Serializable(with = ValidatedStringSerializer::class) +value class ValidatedString(val value: String) { + fun validate(): Boolean { + if (value.length > 5) return false + return value.isNotEmpty() + } +} + +class ValidatedStringSerializer : CommandSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ValidatedString", PrimitiveKind.STRING) + + override fun decodeCommand(decoder: CommandDecoder): ValidatedString { + decoder.cursor.checkout() + val value = ValidatedString(decoder.decodeString()) + if (!value.validate()) { + decoder.cursor.rollback() + decoder.fails() + } + decoder.cursor.commit() + return value + } + + override fun decodeRegular(decoder: Decoder): ValidatedString = ValidatedString(decoder.decodeString()) -@Serializable -data class TestInner( - val nya: UnixList, -) + override fun serialize(encoder: Encoder, value: ValidatedString) { + encoder.encodeString(value.value) + } +} -@LiteralArgument("nya") -object TestCommandGroup { +@LiteralArgument("member", "mem", "m") +object MemberCommands { @Command suspend fun test( @Context ctx: CommandContext, - @LiteralArgument("owo", "uwu") _literal: Unit, - owo: String, - uwu: UnixList, - nya: GreedyString + member: ValidatedString, + @LiteralArgument("name", "n") literal_name: Unit, + name: String ) { - println("Input: ${ctx.command}") - println(owo) - println(uwu) - println(nya) + println(member) + println(name) } } suspend fun main() { val parser = CommandParser() - parser += TestCommandGroup - parser.parse(StringContext("nya owo uwu -awoo --arf 15 nya")) + parser += MemberCommands + println(parser.parse(StringContext("m owo n uwu"))) + println(parser.parse(StringContext("m owowowo n uwu"))) } From beb464fd116f7b961a9503a8b8bc2beaa2283ba8 Mon Sep 17 00:00:00 2001 From: Octal Date: Sat, 15 Jul 2023 20:29:50 -0500 Subject: [PATCH 4/6] CommandValidationException --- .../kotlin/dev/proxyfox/command/CommandDecoder.kt | 2 ++ src/main/kotlin/dev/proxyfox/command/ParseUtil.kt | 15 +++++++++------ src/test/kotlin/dev/proxyfox/command/test/Main.kt | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt b/src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt index db6dd43..725941a 100644 --- a/src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt +++ b/src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt @@ -9,6 +9,7 @@ import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.modules.SerializersModule public class CommandDecodingException(idx: Int) : Exception("Cannot decode command at $idx") +public class CommandValidationException(public val idx: Int, public val reason: String) : Exception("Cannot decode command at $idx: $reason") public inline fun decode(cursor: StringCursor, context: CommandContext, serializer: KSerializer): T = CommandDecoder(cursor, context).decodeSerializableValue(serializer) @@ -21,6 +22,7 @@ public class CommandDecoder(public val cursor: StringCursor, public val context: override val serializersModule: SerializersModule = EmptySerializersModule() public fun fails(): Nothing = throw CommandDecodingException(cursor.index) + public fun fails(reason: String): Nothing = throw CommandValidationException(cursor.index, reason) override fun decodeElementIndex(descriptor: SerialDescriptor): Int { if (elementsCount == descriptor.elementsCount) return CompositeDecoder.DECODE_DONE diff --git a/src/main/kotlin/dev/proxyfox/command/ParseUtil.kt b/src/main/kotlin/dev/proxyfox/command/ParseUtil.kt index 635ce95..0459c0c 100644 --- a/src/main/kotlin/dev/proxyfox/command/ParseUtil.kt +++ b/src/main/kotlin/dev/proxyfox/command/ParseUtil.kt @@ -8,14 +8,15 @@ import kotlin.reflect.* import kotlin.reflect.full.callSuspendBy import kotlin.reflect.full.functions -public class ParseError( +public data class ParseError( public val function: KFunction, public val ordinal: Int, public val parameter: KParameter?, + public val validationException: CommandValidationException? ) { override fun toString(): String = if (parameter != null) - "${function.name}[$ordinal]:${parameter.name}" + "${function.name}[$ordinal]:${parameter.name}${validationException?.let { " -> ${it.reason}" } ?: ""}" else "${function.name}[$ordinal]" public companion object { @@ -37,7 +38,7 @@ public suspend fun C.parse(context: CommandContext): Either, R> F.parseFunc(context: CommandContext, base: Any? = null): Either { - annotations.find { it is Command } ?: return ParseError(this as KFunction, ParseError.NOT_COMMAND, null).right() + annotations.find { it is Command } ?: return ParseError(this as KFunction, ParseError.NOT_COMMAND, null, null).right() val cursor = StringCursor(context.command) @@ -46,7 +47,7 @@ public suspend fun , R> F.parseFunc(context: CommandContext, ParseError.ROOT_LITERAL_NOT_MATCH, null).right() + return ParseError(this as KFunction, ParseError.ROOT_LITERAL_NOT_MATCH, null, null).right() } } @@ -62,7 +63,7 @@ public suspend fun , R> F.parseFunc(context: CommandContext, i, parameter).right() + literal.getLiteral(cursor) ?: return ParseError(this as KFunction, i, parameter, null).right() map[parameter] = Unit continue } @@ -71,7 +72,9 @@ public suspend fun , R> F.parseFunc(context: CommandContext, i, parameter).right() + else return ParseError(this as KFunction, i, parameter, null).right() + } catch (err: CommandValidationException) { + return ParseError(this as KFunction, i, parameter, err).right() } map[parameter] = value if (value != null) diff --git a/src/test/kotlin/dev/proxyfox/command/test/Main.kt b/src/test/kotlin/dev/proxyfox/command/test/Main.kt index edd580e..2e19f4a 100644 --- a/src/test/kotlin/dev/proxyfox/command/test/Main.kt +++ b/src/test/kotlin/dev/proxyfox/command/test/Main.kt @@ -28,7 +28,7 @@ class ValidatedStringSerializer : CommandSerializer { val value = ValidatedString(decoder.decodeString()) if (!value.validate()) { decoder.cursor.rollback() - decoder.fails() + decoder.fails("string not within 0-5 characters") } decoder.cursor.commit() return value From 0f88b8b4fa1ce48da9fa33933ffb8053fd2278cc Mon Sep 17 00:00:00 2001 From: Octal Date: Sat, 15 Jul 2023 20:32:55 -0500 Subject: [PATCH 5/6] Include root literal because you can't find it elsewhere --- .../kotlin/dev/proxyfox/command/ParseUtil.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/dev/proxyfox/command/ParseUtil.kt b/src/main/kotlin/dev/proxyfox/command/ParseUtil.kt index 0459c0c..e597d30 100644 --- a/src/main/kotlin/dev/proxyfox/command/ParseUtil.kt +++ b/src/main/kotlin/dev/proxyfox/command/ParseUtil.kt @@ -12,6 +12,7 @@ public data class ParseError( public val function: KFunction, public val ordinal: Int, public val parameter: KParameter?, + public val rootliteral: LiteralArgument?, public val validationException: CommandValidationException? ) { override fun toString(): String = @@ -38,16 +39,18 @@ public suspend fun C.parse(context: CommandContext): Either, R> F.parseFunc(context: CommandContext, base: Any? = null): Either { - annotations.find { it is Command } ?: return ParseError(this as KFunction, ParseError.NOT_COMMAND, null, null).right() + annotations.find { it is Command } ?: return ParseError(this as KFunction, ParseError.NOT_COMMAND, null, null, null).right() val cursor = StringCursor(context.command) val map = HashMap() + var rootLiteral: LiteralArgument? = null + if (base != null) { - val arg = base::class.annotations.find { it is LiteralArgument } as? LiteralArgument - if (arg != null && arg.getLiteral(cursor) == null) { - return ParseError(this as KFunction, ParseError.ROOT_LITERAL_NOT_MATCH, null, null).right() + rootLiteral = base::class.annotations.find { it is LiteralArgument } as? LiteralArgument + if (rootLiteral != null && rootLiteral.getLiteral(cursor) == null) { + return ParseError(this as KFunction, ParseError.ROOT_LITERAL_NOT_MATCH, null, rootLiteral, null).right() } } @@ -63,7 +66,7 @@ public suspend fun , R> F.parseFunc(context: CommandContext, i, parameter, null).right() + literal.getLiteral(cursor) ?: return ParseError(this as KFunction, i, parameter, rootLiteral, null).right() map[parameter] = Unit continue } @@ -72,9 +75,9 @@ public suspend fun , R> F.parseFunc(context: CommandContext, i, parameter, null).right() + else return ParseError(this as KFunction, i, parameter, rootLiteral, null).right() } catch (err: CommandValidationException) { - return ParseError(this as KFunction, i, parameter, err).right() + return ParseError(this as KFunction, i, parameter, rootLiteral, err).right() } map[parameter] = value if (value != null) From fc356e25ffc59493f3ed2ff3eceef05dca6b6115 Mon Sep 17 00:00:00 2001 From: Octal Date: Sat, 15 Jul 2023 20:55:50 -0500 Subject: [PATCH 6/6] get rid of CommandValidationException and misc changes --- .../kotlin/dev/proxyfox/command/CommandDecoder.kt | 6 +++--- src/main/kotlin/dev/proxyfox/command/ParseUtil.kt | 12 ++++-------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt b/src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt index 725941a..0eccf35 100644 --- a/src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt +++ b/src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt @@ -8,21 +8,21 @@ import kotlinx.serialization.encoding.CompositeDecoder.Companion.UNKNOWN_NAME import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.modules.SerializersModule -public class CommandDecodingException(idx: Int) : Exception("Cannot decode command at $idx") -public class CommandValidationException(public val idx: Int, public val reason: String) : Exception("Cannot decode command at $idx: $reason") +public class CommandDecodingException(idx: Int, public val reason: String? = null) : Exception("Cannot decode command at $idx${reason?.let {": $it" } ?: ""}") public inline fun decode(cursor: StringCursor, context: CommandContext, serializer: KSerializer): T = CommandDecoder(cursor, context).decodeSerializableValue(serializer) public inline fun decode(cursor: StringCursor, context: CommandContext): T = decode(cursor, context, serializer()) +@OptIn(ExperimentalSerializationApi::class) public class CommandDecoder(public val cursor: StringCursor, public val context: CommandContext) : AbstractDecoder() { private var elementsCount = 0 override val serializersModule: SerializersModule = EmptySerializersModule() public fun fails(): Nothing = throw CommandDecodingException(cursor.index) - public fun fails(reason: String): Nothing = throw CommandValidationException(cursor.index, reason) + public fun fails(reason: String): Nothing = throw CommandDecodingException(cursor.index, reason) override fun decodeElementIndex(descriptor: SerialDescriptor): Int { if (elementsCount == descriptor.elementsCount) return CompositeDecoder.DECODE_DONE diff --git a/src/main/kotlin/dev/proxyfox/command/ParseUtil.kt b/src/main/kotlin/dev/proxyfox/command/ParseUtil.kt index e597d30..51487b0 100644 --- a/src/main/kotlin/dev/proxyfox/command/ParseUtil.kt +++ b/src/main/kotlin/dev/proxyfox/command/ParseUtil.kt @@ -12,13 +12,11 @@ public data class ParseError( public val function: KFunction, public val ordinal: Int, public val parameter: KParameter?, - public val rootliteral: LiteralArgument?, - public val validationException: CommandValidationException? + public val rootLiteral: LiteralArgument?, + public val exception: CommandDecodingException? ) { override fun toString(): String = - if (parameter != null) - "${function.name}[$ordinal]:${parameter.name}${validationException?.let { " -> ${it.reason}" } ?: ""}" - else "${function.name}[$ordinal]" + "${rootLiteral?.values?.let { "$it " } ?: ""}${function.name}[$ordinal]${parameter?.name.let {": $it"} ?: ""}${exception?.let { " -> ${it.reason}" } ?: ""}" public companion object { public const val NOT_COMMAND: Int = -2 @@ -75,9 +73,7 @@ public suspend fun , R> F.parseFunc(context: CommandContext, i, parameter, rootLiteral, null).right() - } catch (err: CommandValidationException) { - return ParseError(this as KFunction, i, parameter, rootLiteral, err).right() + else return ParseError(this as KFunction, i, parameter, rootLiteral, err).right() } map[parameter] = value if (value != null)