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/test/**/build/
kls_database.db
/notebook.ipynb
### IntelliJ IDEA ###
.idea/

View file

@ -16,6 +16,6 @@ suspend fun main() {
modules(generalModule)
koinModules(appModule)
koin(appModule)
}.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.module.BotModule
import kotlin.reflect.KFunction
import kotlinx.coroutines.runBlocking
import org.koin.core.module.Module as KoinModule
import org.koin.dsl.bind
@ -36,11 +35,11 @@ class BotBuilder internal constructor(
prefixes.forEach(bot::addPrefix)
}
fun earlyKoinModules(vararg modules: KoinModule) {
fun earlyKoin(vararg modules: KoinModule) {
earlyKoinModules.addAll(modules)
}
fun koinModules(vararg modules: KoinModule) {
fun koin(vararg modules: KoinModule) {
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.allowedMentions
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 org.koin.core.component.inject
@ -35,6 +36,16 @@ class Context(
*/
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 reply(content: String, mentionsAuthor: Boolean = false) = message.channel.createMessage {
@ -47,15 +58,4 @@ class Context(
}
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) {
val values = context.parseArguments()?.toMutableList() ?: return
run {
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.commands.Argument
import io.github.null2264.tsukumogami.core.commands.Arguments
import io.github.null2264.tsukumogami.core.commands.StringParser
import kotlin.reflect.KProperty
abstract class Converter<OutputType: Any?> {
@ -13,7 +14,7 @@ abstract class Converter<OutputType: Any?> {
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 {
return this.parsed

View file

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

View file

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

View file

@ -1,6 +1,7 @@
package io.github.null2264.tsukumogami.core.commands.converters.impl
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.exceptions.CommandException
import java.net.URI
@ -14,13 +15,15 @@ class URLConverter : Converter<URL>() {
override lateinit var parsed: URL
override suspend fun consume(context: Context, inputs: MutableList<String>): Boolean {
val input = inputs.getOrNull(0) ?: return false
override suspend fun consume(context: Context, parser: StringParser): Boolean {
if (!parser.hasNext()) return false
val input = parser.peekNext()
this.parsed = try {
URL(input)
} catch (e: Exception) {
throw CommandException("Failed to parse URL", e)
}
parser.next()
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.core.entity.User
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.exceptions.CommandException
import io.github.null2264.tsukumogami.core.ext.users
@ -16,27 +17,28 @@ class UserConverter : Converter<User>() {
override lateinit var parsed: User
override suspend fun consume(context: Context, inputs: MutableList<String>): Boolean {
val input = inputs.getOrNull(0) ?: throw CommandException("User ID is null")
override suspend fun consume(context: Context, parser: StringParser): Boolean {
if (!parser.hasNext()) throw CommandException("User is not specified")
val input = parser.peekNext()
if (input.equals("me", true)) {
val user = context.author
if (user != null) {
this.parsed = user
inputs.removeAt(0)
parser.next()
return true
}
}
if (input.equals("you", true)) {
this.parsed = context.bot.client.getSelf()
inputs.removeAt(0)
parser.next()
return true
}
this.parsed = context.findUser(input) ?:
throw CommandException("User not found")
inputs.removeAt(0)
parser.next()
return true
}