Skip to content
This repository has been archived by the owner on Mar 21, 2024. It is now read-only.

Commit

Permalink
Merge pull request #3 from The-ProxyFox-Group/dev/2.0
Browse files Browse the repository at this point in the history
ProxyFox Command 2.0
  • Loading branch information
Oliver-makes-code authored Jul 16, 2023
2 parents ac40838 + fc356e2 commit 546d7e1
Show file tree
Hide file tree
Showing 23 changed files with 481 additions and 512 deletions.
8 changes: 6 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -17,6 +18,9 @@ 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<KotlinCompile> {
Expand Down
22 changes: 22 additions & 0 deletions src/main/kotlin/dev/proxyfox/command/Annotations.kt
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 0 additions & 2 deletions src/main/kotlin/dev/proxyfox/command/CommandContext.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package dev.proxyfox.command

import dev.proxyfox.command.menu.CommandMenu

public abstract class CommandContext<T> {
/**
* The command trigger for the context
Expand Down
158 changes: 158 additions & 0 deletions src/main/kotlin/dev/proxyfox/command/CommandDecoder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
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, public val reason: String? = null) : Exception("Cannot decode command at $idx${reason?.let {": $it" } ?: ""}")

public inline fun <reified T> decode(cursor: StringCursor, context: CommandContext<Any>, serializer: KSerializer<T>): T =
CommandDecoder(cursor, context).decodeSerializableValue(serializer)
public inline fun <reified T> decode(cursor: StringCursor, context: CommandContext<Any>): T =
decode(cursor, context, serializer())

@OptIn(ExperimentalSerializationApi::class)
public class CommandDecoder(public val cursor: StringCursor, public val context: CommandContext<Any>) : 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 CommandDecodingException(cursor.index, reason)

override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
if (elementsCount == descriptor.elementsCount) return CompositeDecoder.DECODE_DONE
return elementsCount++
}

override fun decodeString(): String {
if (cursor.end) fails()
val string = cursor.extractString(true)
cursor.inc()
return string
}

override fun decodeChar(): Char {
cursor.checkout()
val string = decodeString()
if (string.length != 1) {
cursor.rollback()
fails()
}
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()
fails()
}
cursor.commit()
return string == "true"
}

override fun decodeLong(): Long {
cursor.checkout()
val num = cursor.extractString(false).toLongOrNull()
cursor.inc()
if (num == null) {
cursor.rollback()
fails()
}
cursor.commit()
return num
}

override fun decodeInt(): Int {
cursor.checkout()
val num = cursor.extractString(false).toIntOrNull()
cursor.inc()
if (num == null) {
cursor.rollback()
fails()
}
cursor.commit()
return num
}

override fun decodeShort(): Short {
cursor.checkout()
val num = cursor.extractString(false).toShortOrNull()
cursor.inc()
if (num == null) {
cursor.rollback()
fails()
}
cursor.commit()
return num
}

override fun decodeByte(): Byte {
cursor.checkout()
val num = cursor.extractString(false).toByteOrNull()
cursor.inc()
if (num == null) {
cursor.rollback()
fails()
}
cursor.commit()
return num
}

override fun decodeDouble(): Double {
cursor.checkout()
val num = cursor.extractString(false).toDoubleOrNull()
cursor.inc()
if (num == null) {
cursor.rollback()
fails()
}
cursor.commit()
return num
}

override fun decodeFloat(): Float {
cursor.checkout()
val num = cursor.extractString(false).toFloatOrNull()
cursor.inc()
if (num == null) {
cursor.rollback()
fails()
}
cursor.commit()
return num
}

override fun decodeEnum(enumDescriptor: SerialDescriptor): Int {
cursor.checkout()
val idx = enumDescriptor.getElementIndex(decodeString())
if (idx == UNKNOWN_NAME) {
cursor.rollback()
fails()
}
cursor.commit()
return idx
}

override fun decodeNotNullMark(): Boolean {
return false
}

override fun <T : Any> decodeNullableSerializableValue(deserializer: DeserializationStrategy<T?>): T? {
return try {
deserializer.deserialize(this)
} catch (err: CommandDecodingException) {
null
}
}

override fun decodeSequentially(): Boolean = true
}
85 changes: 27 additions & 58 deletions src/main/kotlin/dev/proxyfox/command/CommandParser.kt
Original file line number Diff line number Diff line change
@@ -1,68 +1,37 @@
package dev.proxyfox.command

import dev.proxyfox.command.node.CommandNode
import dev.proxyfox.command.node.builtin.LiteralNode
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import kotlin.reflect.KFunction

public class CommandParser<T, C: CommandContext<T>>: NodeHolder<T, C>() {
public suspend fun parse(ctx: C): Boolean? {
val literals = ArrayList<LiteralNode<T, C>>()
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
public class CommandParser<T, C: CommandContext<T>> {
public suspend fun parse(ctx: C): Either<Unit, List<ParseError>> {
val errors = ArrayList<ParseError>()

for (function in functionMembers) {
val result = function.parseFunc(ctx)
if (result.isLeft())
return Unit.left()
errors.add(result.getOrNull()!!)
}
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
}

private suspend fun tryParseNode(cursor: StringCursor, node: CommandNode<T, C>, 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<LiteralNode<T, C>>()
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
for (clazz in classMembers) {
val result = clazz.parse(ctx)
if (result.isLeft())
return Unit.left()
errors.addAll(result.getOrNull()!!)
}
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
errors.sortBy { -it.ordinal }
return errors.right()
}

private fun getLevenshtein(test: String, literals: ArrayList<LiteralNode<T, C>>): Pair<String, Int>? {
var closest: Pair<String, Int>? = 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<Any>()
private val functionMembers = ArrayList<KFunction<Any>>()

public operator fun plusAssign(value: Any) {
if (value is KFunction<*>)
functionMembers.add(value as KFunction<Any>)
else classMembers.add(value)
}
}
14 changes: 0 additions & 14 deletions src/main/kotlin/dev/proxyfox/command/NodeHolder.kt

This file was deleted.

Loading

0 comments on commit 546d7e1

Please sign in to comment.