refactor: Turn argument from mutable list to a custom string parser

class
This commit is contained in:
Ahmad Ansori Palembani 2025-06-08 19:42:44 +07:00
parent b46dc2f5eb
commit e01dafa69d
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
11 changed files with 116 additions and 36 deletions

1
.gitignore vendored
View file

@ -4,6 +4,7 @@ build/
!**/src/main/**/build/ !**/src/main/**/build/
!**/src/test/**/build/ !**/src/test/**/build/
kls_database.db kls_database.db
/notebook.ipynb
### IntelliJ IDEA ### ### IntelliJ IDEA ###
.idea/ .idea/

View file

@ -16,6 +16,6 @@ suspend fun main() {
modules(generalModule) modules(generalModule)
koinModules(appModule) koin(appModule)
}.start() }.start()
} }

View file

@ -8,7 +8,6 @@ import io.github.null2264.tsukumogami.core.ext.loadModule
import io.github.null2264.tsukumogami.core.koin.TsukumogamiKoinContext import io.github.null2264.tsukumogami.core.koin.TsukumogamiKoinContext
import io.github.null2264.tsukumogami.core.module.BotModule import io.github.null2264.tsukumogami.core.module.BotModule
import kotlin.reflect.KFunction import kotlin.reflect.KFunction
import kotlinx.coroutines.runBlocking
import org.koin.core.module.Module as KoinModule import org.koin.core.module.Module as KoinModule
import org.koin.dsl.bind import org.koin.dsl.bind
@ -36,11 +35,11 @@ class BotBuilder internal constructor(
prefixes.forEach(bot::addPrefix) prefixes.forEach(bot::addPrefix)
} }
fun earlyKoinModules(vararg modules: KoinModule) { fun earlyKoin(vararg modules: KoinModule) {
earlyKoinModules.addAll(modules) earlyKoinModules.addAll(modules)
} }
fun koinModules(vararg modules: KoinModule) { fun koin(vararg modules: KoinModule) {
koinModules.addAll(modules) koinModules.addAll(modules)
} }
} }

View file

@ -5,6 +5,7 @@ import dev.kord.core.entity.Message
import dev.kord.rest.builder.message.AllowedMentionsBuilder import dev.kord.rest.builder.message.AllowedMentionsBuilder
import dev.kord.rest.builder.message.allowedMentions import dev.kord.rest.builder.message.allowedMentions
import io.github.null2264.tsukumogami.core.commands.Command import io.github.null2264.tsukumogami.core.commands.Command
import io.github.null2264.tsukumogami.core.commands.StringParser
import io.github.null2264.tsukumogami.core.koin.TsukumogamiKoinComponent import io.github.null2264.tsukumogami.core.koin.TsukumogamiKoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -35,6 +36,16 @@ class Context(
*/ */
var command: Command? = null var command: Command? = null
val parser: StringParser by lazy {
StringParser(
if (prefix != null && invokedWith != null) {
message.content
.substringAfter("$prefix$invokedWith")
.trim()
} else ""
)
}
suspend fun send(content: String) = message.channel.createMessage(content) suspend fun send(content: String) = message.channel.createMessage(content)
suspend fun reply(content: String, mentionsAuthor: Boolean = false) = message.channel.createMessage { suspend fun reply(content: String, mentionsAuthor: Boolean = false) = message.channel.createMessage {
@ -47,15 +58,4 @@ class Context(
} }
suspend fun typing() = message.channel.type() suspend fun typing() = message.channel.type()
fun parseArguments(): MutableList<String>? =
if (prefix != null && invokedWith != null) {
message.content
.substringAfter("$prefix$invokedWith")
.trim()
.split(' ')
.toMutableList()
} else {
null
}
} }

View file

@ -17,11 +17,9 @@ abstract class Arguments {
} }
suspend fun parse(context: Context) { suspend fun parse(context: Context) {
val values = context.parseArguments()?.toMutableList() ?: return
run { run {
args.forEach { arg -> args.forEach { arg ->
arg.converter.consume(context, values) arg.converter.consume(context, context.parser)
} }
} }
} }

View file

@ -0,0 +1,72 @@
package io.github.null2264.tsukumogami.core.commands
class StringParser(private val string: String) {
private var currentIndex = 0
private var lastPeekIndex = 0
fun hasNext() = currentIndex < string.length
private fun parse(peek: Boolean, naive: Boolean = false): String {
check(hasNext())
val currentToken = StringBuilder()
var inQuotes = false
val currentIndexInitial = currentIndex
while (hasNext()) {
val char = string[currentIndex]
currentIndex++
when (char) {
'"' -> {
if (naive) {
currentToken.append(char)
continue
}
if (inQuotes) {
// Closing quote: add the accumulated token
break
} else {
if (currentToken.isNotEmpty()) {
// Did not start with quote, probably something like: hello"world"
currentToken.append(char)
continue
}
// Opening quote:
inQuotes = true
}
}
' ' -> {
if (inQuotes) {
// Space inside quotes: append it
currentToken.append(char)
} else {
// Space outside quotes: separator
if (currentToken.isNotEmpty()) {
break
}
}
}
else -> {
// Any other character: append it
currentToken.append(char)
}
}
}
//check(!inQuotes)
if (peek) {
lastPeekIndex = currentIndex
currentIndex = currentIndexInitial
}
return currentToken.toString()
}
fun parseNext(naive: Boolean = false): String = parse(false, naive)
fun peekNext(naive: Boolean = false): String = parse(true, naive)
fun next() {
currentIndex = lastPeekIndex
}
}

View file

@ -3,6 +3,7 @@ package io.github.null2264.tsukumogami.core.commands.converters
import io.github.null2264.tsukumogami.core.Context import io.github.null2264.tsukumogami.core.Context
import io.github.null2264.tsukumogami.core.commands.Argument import io.github.null2264.tsukumogami.core.commands.Argument
import io.github.null2264.tsukumogami.core.commands.Arguments import io.github.null2264.tsukumogami.core.commands.Arguments
import io.github.null2264.tsukumogami.core.commands.StringParser
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
abstract class Converter<OutputType: Any?> { abstract class Converter<OutputType: Any?> {
@ -13,7 +14,7 @@ abstract class Converter<OutputType: Any?> {
abstract var parsed: OutputType abstract var parsed: OutputType
abstract suspend fun consume(context: Context, inputs: MutableList<String>): Boolean abstract suspend fun consume(context: Context, parser: StringParser): Boolean
operator fun getValue(thisRef: Arguments, property: KProperty<*>): OutputType { operator fun getValue(thisRef: Arguments, property: KProperty<*>): OutputType {
return this.parsed return this.parsed

View file

@ -1,25 +1,26 @@
package io.github.null2264.tsukumogami.core.commands.converters.impl package io.github.null2264.tsukumogami.core.commands.converters.impl
import io.github.null2264.tsukumogami.core.Context import io.github.null2264.tsukumogami.core.Context
import io.github.null2264.tsukumogami.core.commands.StringParser
import io.github.null2264.tsukumogami.core.commands.converters.Converter import io.github.null2264.tsukumogami.core.commands.converters.Converter
class StringConverter : Converter<String>() { class StringConverter : Converter<String>() {
override var parsed: String = "" override var parsed: String = ""
override suspend fun consume(context: Context, inputs: MutableList<String>): Boolean { override suspend fun consume(context: Context, parser: StringParser): Boolean {
this.parsed = if (isGreedy && inputs.size > 1) run { if (!parser.hasNext()) return this.parsed.isEmpty()
val limit = inputs.size
this.parsed = if (isGreedy) run {
var buffer = "" var buffer = ""
var count = 0 var count = 0
for (i in (1..limit)) { while (parser.hasNext()) {
if (++count > 1) buffer += " " if (++count > 1) buffer += " "
if (count <= limit) { buffer += parser.parseNext(true)
buffer += inputs.removeFirstOrNull() ?: ""
} else break
} }
buffer buffer
} else inputs.removeFirstOrNull().orEmpty() } else parser.parseNext()
return this.parsed.isNotEmpty() return this.parsed.isNotEmpty()
} }
} }

View file

@ -1,6 +1,7 @@
package io.github.null2264.tsukumogami.core.commands.converters.impl package io.github.null2264.tsukumogami.core.commands.converters.impl
import io.github.null2264.tsukumogami.core.Context import io.github.null2264.tsukumogami.core.Context
import io.github.null2264.tsukumogami.core.commands.StringParser
import io.github.null2264.tsukumogami.core.commands.converters.Converter import io.github.null2264.tsukumogami.core.commands.converters.Converter
import io.github.null2264.tsukumogami.core.exceptions.CommandException import io.github.null2264.tsukumogami.core.exceptions.CommandException
import java.net.URI import java.net.URI
@ -13,13 +14,15 @@ class URIConverter : Converter<URI>() {
override lateinit var parsed: URI override lateinit var parsed: URI
override suspend fun consume(context: Context, inputs: MutableList<String>): Boolean { override suspend fun consume(context: Context, parser: StringParser): Boolean {
val input = inputs.getOrNull(0) ?: return false if (!parser.hasNext()) return false
val input = parser.peekNext()
this.parsed = try { this.parsed = try {
URI(input) URI(input)
} catch (e: Exception) { } catch (e: Exception) {
throw CommandException("Failed to parse URI", e) throw CommandException("Failed to parse URI", e)
} }
parser.next()
return true return true
} }
} }

View file

@ -1,6 +1,7 @@
package io.github.null2264.tsukumogami.core.commands.converters.impl package io.github.null2264.tsukumogami.core.commands.converters.impl
import io.github.null2264.tsukumogami.core.Context import io.github.null2264.tsukumogami.core.Context
import io.github.null2264.tsukumogami.core.commands.StringParser
import io.github.null2264.tsukumogami.core.commands.converters.Converter import io.github.null2264.tsukumogami.core.commands.converters.Converter
import io.github.null2264.tsukumogami.core.exceptions.CommandException import io.github.null2264.tsukumogami.core.exceptions.CommandException
import java.net.URI import java.net.URI
@ -14,13 +15,15 @@ class URLConverter : Converter<URL>() {
override lateinit var parsed: URL override lateinit var parsed: URL
override suspend fun consume(context: Context, inputs: MutableList<String>): Boolean { override suspend fun consume(context: Context, parser: StringParser): Boolean {
val input = inputs.getOrNull(0) ?: return false if (!parser.hasNext()) return false
val input = parser.peekNext()
this.parsed = try { this.parsed = try {
URL(input) URL(input)
} catch (e: Exception) { } catch (e: Exception) {
throw CommandException("Failed to parse URL", e) throw CommandException("Failed to parse URL", e)
} }
parser.next()
return true return true
} }
} }

View file

@ -3,6 +3,7 @@ package io.github.null2264.tsukumogami.core.commands.converters.impl
import dev.kord.common.entity.Snowflake import dev.kord.common.entity.Snowflake
import dev.kord.core.entity.User import dev.kord.core.entity.User
import io.github.null2264.tsukumogami.core.Context import io.github.null2264.tsukumogami.core.Context
import io.github.null2264.tsukumogami.core.commands.StringParser
import io.github.null2264.tsukumogami.core.commands.converters.Converter import io.github.null2264.tsukumogami.core.commands.converters.Converter
import io.github.null2264.tsukumogami.core.exceptions.CommandException import io.github.null2264.tsukumogami.core.exceptions.CommandException
import io.github.null2264.tsukumogami.core.ext.users import io.github.null2264.tsukumogami.core.ext.users
@ -16,27 +17,28 @@ class UserConverter : Converter<User>() {
override lateinit var parsed: User override lateinit var parsed: User
override suspend fun consume(context: Context, inputs: MutableList<String>): Boolean { override suspend fun consume(context: Context, parser: StringParser): Boolean {
val input = inputs.getOrNull(0) ?: throw CommandException("User ID is null") if (!parser.hasNext()) throw CommandException("User is not specified")
val input = parser.peekNext()
if (input.equals("me", true)) { if (input.equals("me", true)) {
val user = context.author val user = context.author
if (user != null) { if (user != null) {
this.parsed = user this.parsed = user
inputs.removeAt(0) parser.next()
return true return true
} }
} }
if (input.equals("you", true)) { if (input.equals("you", true)) {
this.parsed = context.bot.client.getSelf() this.parsed = context.bot.client.getSelf()
inputs.removeAt(0) parser.next()
return true return true
} }
this.parsed = context.findUser(input) ?: this.parsed = context.findUser(input) ?:
throw CommandException("User not found") throw CommandException("User not found")
inputs.removeAt(0) parser.next()
return true return true
} }