implement type-safe fallback commands

This commit is contained in:
2026-01-07 21:44:25 +01:00
parent ecd255479a
commit 921229aa5b
40 changed files with 345 additions and 261 deletions

View File

@@ -1,20 +1,22 @@
package com.infendro.cli.command
import com.infendro.cli.Dsl
import com.infendro.cli.command.argument.Argument
import com.infendro.cli.command.context.Context
import com.infendro.cli.command.context.Parser
import com.infendro.cli.command.context.ContextParser
import com.infendro.cli.command.help.DefaultHelpRenderer
import com.infendro.cli.command.help.HelpRenderer
import com.infendro.cli.command.option.Option
import com.infendro.cli.exception.build.*
import com.infendro.cli.parser.*
import com.infendro.cli.util.Regex
import com.infendro.cli.util.Regex.ARGUMENT
import com.infendro.cli.util.Regex.COMMAND
import com.infendro.cli.util.Regex.OPTION
import kotlin.reflect.KProperty
class Command private constructor(
sealed class Command private constructor(
val parent: Command?,
val name: String,
val fallback: Boolean,
val commands: List<Command>,
val arguments: List<Argument<*>>,
val options: List<Option<*>>,
@@ -31,60 +33,130 @@ class Command private constructor(
get() = renderer.render(this)
fun run(args: Array<String>) {
when (val result = Parser(this, args).parse()) {
is Parser.Result.Success -> result.context.execute()
is Parser.Result.Help -> result.command.help?.let { println(it) }
is Parser.Result.Failure -> {
println("error: ${result.exception.message}")
result.command.help?.let { println("\n$it") }
when (val result = ContextParser(this, args).parse()) {
is ContextParser.Result.Success -> result.context.execute()
is ContextParser.Result.Help -> result.command.help?.let { print(it) }
is ContextParser.Result.Failure -> {
println("error: ${result.error.message}")
result.command.help?.let {
println()
print(it)
}
}
}
}
class Named(
parent: Command?,
name: String,
commands: List<Command>,
arguments: List<Argument<*>>,
options: List<Option<*>>,
execute: Context.() -> Unit,
renderer: HelpRenderer,
) : Command(parent, name, commands, arguments, options, execute, renderer) {
class Builder(
name: String,
) : Command.Builder<Named>(name) {
override fun create() =
Named(parent, name, commands, arguments, options, execute, renderer)
}
}
class Fallback<T : Any>(
parent: Command?,
val key: Key<T>,
val parser: Parser<T>,
name: String,
commands: List<Command>,
arguments: List<Argument<*>>,
options: List<Option<*>>,
execute: Context.() -> Unit,
renderer: HelpRenderer,
) : Command(parent, name, commands, arguments, options, execute, renderer) {
class Builder<T : Any>(
val parser: Parser<T>,
name: String,
) : Command.Builder<Fallback<T>>(name) {
internal val key = Key<T>()
override fun create() =
Fallback(parent, key, parser, name, commands, arguments, options, execute, renderer)
}
class Key<T>
}
@Dsl
class Builder internal constructor(
private val level: Int,
private val name: String,
private val fallback: Boolean,
var renderer: HelpRenderer,
sealed class Builder<COMMAND : Command>(
protected val name: String,
) {
private var parent: Command? = null
private val builders = mutableListOf<Builder>()
private val arguments = mutableListOf<Argument<*>>()
private val options = mutableListOf<Option<*>>()
private lateinit var _execute: Context.() -> Unit
protected var parent: Command? = null
private val builders = mutableListOf<Builder<*>>()
protected val commands = mutableListOf<Command>()
protected val arguments = mutableListOf<Argument<*>>()
protected val options = mutableListOf<Option<*>>()
private var _execute: (Context.() -> Unit)? = null
private var _renderer: HelpRenderer? = null
private val key: Key
get() = Key(level)
protected val execute: Context.() -> Unit
get() = _execute!!
protected val renderer: HelpRenderer
get() = _renderer!!
fun command(name: String, block: Builder.() -> Unit) {
fun command(name: String, block: Builder<*>.() -> Unit) {
validateCommand(name)
builders += Builder(level + 1, name, fallback = false, renderer).apply(block)
builders += Named.Builder(name).apply(block)
}
fun fallback(name: String, block: Builder.(Key) -> Unit) {
validateCommand(name)
builders += Builder(level + 1, name, fallback = true, renderer).apply { block(key) }
inner class FallbackFactory {
fun string(name: String, block: Builder<*>.(Fallback.Key<String>) -> Unit) =
fallback(StringParser, name, block)
fun int(name: String, block: Builder<*>.(Fallback.Key<Int>) -> Unit) =
fallback(IntParser, name, block)
fun long(name: String, block: Builder<*>.(Fallback.Key<Long>) -> Unit) =
fallback(LongParser, name, block)
fun float(name: String, block: Builder<*>.(Fallback.Key<Float>) -> Unit) =
fallback(FloatParser, name, block)
fun double(name: String, block: Builder<*>.(Fallback.Key<Double>) -> Unit) =
fallback(DoubleParser, name, block)
fun boolean(name: String, block: Builder<*>.(Fallback.Key<Boolean>) -> Unit) =
fallback(BooleanParser, name, block)
inline fun <reified T : Enum<T>> enum(name: String, noinline block: Builder<*>.(Fallback.Key<T>) -> Unit) =
fallback(enumParser<T>(), name, block)
}
fun argument(argument: Argument<*>) {
val fallback = FallbackFactory()
fun <T : Any> fallback(parser: Parser<T>, name: String, block: Fallback.Builder<T>.(Fallback.Key<T>) -> Unit) {
validateCommand(name)
builders += Fallback.Builder(parser, name).apply { block(key) }
}
fun register(argument: Argument<*>) {
validateArgument(argument)
arguments += argument
}
fun arguments(vararg arguments: Argument<*>) = arguments.forEach(::argument)
fun register(vararg arguments: Argument<*>) = arguments.forEach(::register)
operator fun <T : Argument<*>> T.provideDelegate(thisRef: Nothing?, property: KProperty<*>) = also(::argument)
operator fun <T : Argument<*>> T.provideDelegate(thisRef: Nothing?, property: KProperty<*>) = also(::register)
operator fun <T : Argument<*>> T.getValue(thisRef: Nothing?, property: KProperty<*>) = this
fun option(option: Option<*>) {
fun register(option: Option<*>) {
validateOption(option)
options += option
}
fun options(vararg options: Option<*>) = options.forEach(::option)
fun register(vararg options: Option<*>) = options.forEach(::register)
operator fun <T : Option<*>> T.provideDelegate(thisRef: Nothing?, property: KProperty<*>) = also(::option)
operator fun <T : Option<*>> T.provideDelegate(thisRef: Nothing?, property: KProperty<*>) = also(::register)
operator fun <T : Option<*>> T.getValue(thisRef: Nothing?, property: KProperty<*>) = this
fun execute(block: Context.() -> Unit) {
@@ -92,28 +164,35 @@ class Command private constructor(
_execute = block
}
fun build(): Command {
if (!::_execute.isInitialized)
_execute = {}
fun renderer(renderer: HelpRenderer) {
validateRenderer()
_renderer = renderer
}
internal fun build(): Command {
if (_execute == null) _execute = {}
if (_renderer == null) _renderer = DefaultHelpRenderer
validate()
val command = create()
val commands = mutableListOf<Command>()
val command = Command(parent, name, fallback, commands, arguments, options, _execute, renderer)
for (builder in builders) {
builder.parent = command
commands += builder.build()
}
return command
}
internal abstract fun create(): COMMAND
private fun validateCommand(name: String) {
if (builders.isNotEmpty()) {
val last = builders.last()
if (last.fallback) throw InvalidCommandOrder()
if (last is Fallback.Builder<*>) throw InvalidCommandOrder()
}
when {
!name.matches(COMMAND) -> throw InvalidCommand(name)
!name.matches(Regex.COMMAND) -> throw InvalidCommand(name)
builders.any { it.name == name } -> throw DuplicateCommand(name)
}
}
@@ -139,18 +218,18 @@ class Command private constructor(
}
private fun validateExecute() {
if (::_execute.isInitialized) throw DuplicateExecute()
if (_execute != null) throw DuplicateExecute()
}
private fun validateRenderer() {
if (_renderer != null) throw Exception() //TODO
}
private fun validate() {
if (builders.any { it.fallback } && arguments.isNotEmpty()) throw InvalidFallback()
if (builders.any { it is Fallback.Builder<*> } && arguments.isNotEmpty()) throw InvalidFallback()
}
}
class Key internal constructor(
internal val index: Int,
)
}
fun cli(name: String, block: Command.Builder.() -> Unit) =
Command.Builder(-1, name, fallback = false, DefaultHelpRenderer).apply(block).build()
fun cli(name: String, block: Command.Builder<*>.() -> Unit) =
Command.Named.Builder(name).apply(block).build()