From 921229aa5b35345f55fdd46ca7a4792d76f5c7c6 Mon Sep 17 00:00:00 2001 From: Infendro Date: Wed, 7 Jan 2026 21:44:25 +0100 Subject: [PATCH] implement type-safe fallback commands --- README.md | 8 +- .../com/infendro/cli/command/Command.kt | 175 +++++++++++++----- .../cli/command/{ => argument}/Argument.kt | 18 +- .../cli/command/argument/ArgumentFactory.kt | 19 ++ .../infendro/cli/command/context/Context.kt | 75 ++++---- .../context/{Parser.kt => ContextParser.kt} | 91 +++++---- .../cli/command/help/DefaultHelpRenderer.kt | 6 +- .../cli/command/{ => option}/Option.kt | 21 +-- .../cli/command/option/OptionFactory.kt | 22 +++ .../cli/exception/build/BuildException.kt | 5 - .../cli/exception/{ => build}/CliException.kt | 2 +- .../cli/exception/build/DuplicateCommand.kt | 2 +- .../cli/exception/build/DuplicateExecute.kt | 2 +- .../cli/exception/build/DuplicateOption.kt | 2 +- .../cli/exception/build/InvalidArgument.kt | 2 +- .../exception/build/InvalidArgumentOrder.kt | 2 +- .../cli/exception/build/InvalidCommand.kt | 2 +- .../exception/build/InvalidCommandOrder.kt | 2 +- .../cli/exception/build/InvalidFallback.kt | 2 +- .../cli/exception/build/InvalidOption.kt | 2 +- .../cli/exception/build/InvalidRange.kt | 2 +- .../cli/exception/build/MissingOptionName.kt | 2 +- .../cli/exception/run/InvalidArgumentCount.kt | 4 +- .../cli/exception/run/InvalidOptionCount.kt | 4 +- .../cli/exception/run/InvalidValue.kt | 2 +- .../cli/exception/run/MalformedOption.kt | 2 +- .../cli/exception/run/MissingOptionValue.kt | 4 +- .../cli/exception/run/RunException.kt | 5 - .../cli/exception/run/RuntimeError.kt | 5 + .../cli/exception/run/UnexpectedArgument.kt | 2 +- .../cli/exception/run/UnknownCommand.kt | 2 +- .../cli/exception/run/UnknownOption.kt | 2 +- .../com/infendro/cli/parser/BooleanParser.kt | 18 +- .../com/infendro/cli/parser/DoubleParser.kt | 13 +- .../com/infendro/cli/parser/EnumParser.kt | 10 +- .../com/infendro/cli/parser/FloatParser.kt | 13 +- .../com/infendro/cli/parser/IntParser.kt | 13 +- .../com/infendro/cli/parser/LongParser.kt | 13 +- .../kotlin/com/infendro/cli/parser/Parser.kt | 20 +- .../com/infendro/cli/parser/StringParser.kt | 10 +- 40 files changed, 345 insertions(+), 261 deletions(-) rename src/commonMain/kotlin/com/infendro/cli/command/{ => argument}/Argument.kt (66%) create mode 100644 src/commonMain/kotlin/com/infendro/cli/command/argument/ArgumentFactory.kt rename src/commonMain/kotlin/com/infendro/cli/command/context/{Parser.kt => ContextParser.kt} (65%) rename src/commonMain/kotlin/com/infendro/cli/command/{ => option}/Option.kt (73%) create mode 100644 src/commonMain/kotlin/com/infendro/cli/command/option/OptionFactory.kt delete mode 100644 src/commonMain/kotlin/com/infendro/cli/exception/build/BuildException.kt rename src/commonMain/kotlin/com/infendro/cli/exception/{ => build}/CliException.kt (51%) delete mode 100644 src/commonMain/kotlin/com/infendro/cli/exception/run/RunException.kt create mode 100644 src/commonMain/kotlin/com/infendro/cli/exception/run/RuntimeError.kt diff --git a/README.md b/README.md index 5b5478f..f999f46 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,11 @@ fun main(args: Array) = cli("application") { // declare command "greet" command("greet") { // declare arguments - val speakersArg by Argument.string("speakers").variable(min = 1) + val speakersArg by argument.string("speakers").variable(min = 1) // declare options - val greetingOpt by Option.string("greeting", "greet", "g").orElse("Hello") - val nameOpt by Option.string("name", "n").orNull() + val greetingOpt by option.string("greeting", "greet", "g").orElse("Hello") + val nameOpt by option.string("name", "n").orNull() execute { // retrieve the arguments and options @@ -56,7 +56,7 @@ fun main(args: Array) = cli("application") { } // declare fallback command - fallback("value") { valueArg -> + fallback.string("value") { valueArg -> execute { // retrieve the value used for the fallback command val value by valueArg diff --git a/src/commonMain/kotlin/com/infendro/cli/command/Command.kt b/src/commonMain/kotlin/com/infendro/cli/command/Command.kt index cb9d0f8..b0ae597 100644 --- a/src/commonMain/kotlin/com/infendro/cli/command/Command.kt +++ b/src/commonMain/kotlin/com/infendro/cli/command/Command.kt @@ -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, val arguments: List>, val options: List>, @@ -31,60 +33,130 @@ class Command private constructor( get() = renderer.render(this) fun run(args: Array) { - 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, + arguments: List>, + options: List>, + execute: Context.() -> Unit, + renderer: HelpRenderer, + ) : Command(parent, name, commands, arguments, options, execute, renderer) { + class Builder( + name: String, + ) : Command.Builder(name) { + override fun create() = + Named(parent, name, commands, arguments, options, execute, renderer) + } + } + + class Fallback( + parent: Command?, + val key: Key, + val parser: Parser, + name: String, + commands: List, + arguments: List>, + options: List>, + execute: Context.() -> Unit, + renderer: HelpRenderer, + ) : Command(parent, name, commands, arguments, options, execute, renderer) { + class Builder( + val parser: Parser, + name: String, + ) : Command.Builder>(name) { + internal val key = Key() + + override fun create() = + Fallback(parent, key, parser, name, commands, arguments, options, execute, renderer) + } + + class Key + } + @Dsl - class Builder internal constructor( - private val level: Int, - private val name: String, - private val fallback: Boolean, - var renderer: HelpRenderer, + sealed class Builder( + protected val name: String, ) { - private var parent: Command? = null - private val builders = mutableListOf() - private val arguments = mutableListOf>() - private val options = mutableListOf>() - private lateinit var _execute: Context.() -> Unit + protected var parent: Command? = null + private val builders = mutableListOf>() + protected val commands = mutableListOf() + protected val arguments = mutableListOf>() + protected val options = mutableListOf>() + 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) -> Unit) = + fallback(StringParser, name, block) + + fun int(name: String, block: Builder<*>.(Fallback.Key) -> Unit) = + fallback(IntParser, name, block) + + fun long(name: String, block: Builder<*>.(Fallback.Key) -> Unit) = + fallback(LongParser, name, block) + + fun float(name: String, block: Builder<*>.(Fallback.Key) -> Unit) = + fallback(FloatParser, name, block) + + fun double(name: String, block: Builder<*>.(Fallback.Key) -> Unit) = + fallback(DoubleParser, name, block) + + fun boolean(name: String, block: Builder<*>.(Fallback.Key) -> Unit) = + fallback(BooleanParser, name, block) + + inline fun > enum(name: String, noinline block: Builder<*>.(Fallback.Key) -> Unit) = + fallback(enumParser(), name, block) } - fun argument(argument: Argument<*>) { + val fallback = FallbackFactory() + + fun fallback(parser: Parser, name: String, block: Fallback.Builder.(Fallback.Key) -> 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.provideDelegate(thisRef: Nothing?, property: KProperty<*>) = also(::argument) + operator fun > T.provideDelegate(thisRef: Nothing?, property: KProperty<*>) = also(::register) operator fun > 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.provideDelegate(thisRef: Nothing?, property: KProperty<*>) = also(::option) + operator fun > T.provideDelegate(thisRef: Nothing?, property: KProperty<*>) = also(::register) operator fun > 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() - 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() diff --git a/src/commonMain/kotlin/com/infendro/cli/command/Argument.kt b/src/commonMain/kotlin/com/infendro/cli/command/argument/Argument.kt similarity index 66% rename from src/commonMain/kotlin/com/infendro/cli/command/Argument.kt rename to src/commonMain/kotlin/com/infendro/cli/command/argument/Argument.kt index ea7d1fa..489d35c 100644 --- a/src/commonMain/kotlin/com/infendro/cli/command/Argument.kt +++ b/src/commonMain/kotlin/com/infendro/cli/command/argument/Argument.kt @@ -1,7 +1,7 @@ -package com.infendro.cli.command +package com.infendro.cli.command.argument import com.infendro.cli.exception.build.InvalidRange -import com.infendro.cli.parser.* +import com.infendro.cli.parser.Parser sealed class Argument( val parser: Parser, @@ -52,18 +52,4 @@ sealed class Argument( min: Int, max: Int?, ) : Argument(parser, name, min, max) - - companion object { - fun string(name: String) = argument(StringParser, name) - fun int(name: String) = argument(IntParser, name) - fun long(name: String) = argument(LongParser, name) - fun float(name: String) = argument(FloatParser, name) - fun double(name: String) = argument(DoubleParser, name) - fun boolean(name: String) = argument(BooleanParser, name) - - inline fun > enum(name: String) = argument(enumParser(), name) - } } - -fun argument(parser: Parser, name: String) = - Argument.Required(parser, name) diff --git a/src/commonMain/kotlin/com/infendro/cli/command/argument/ArgumentFactory.kt b/src/commonMain/kotlin/com/infendro/cli/command/argument/ArgumentFactory.kt new file mode 100644 index 0000000..fc73224 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/command/argument/ArgumentFactory.kt @@ -0,0 +1,19 @@ +package com.infendro.cli.command.argument + +import com.infendro.cli.parser.* + +object ArgumentFactory { + fun string(name: String) = argument(StringParser, name) + fun int(name: String) = argument(IntParser, name) + fun long(name: String) = argument(LongParser, name) + fun float(name: String) = argument(FloatParser, name) + fun double(name: String) = argument(DoubleParser, name) + fun boolean(name: String) = argument(BooleanParser, name) + + inline fun > enum(name: String) = argument(enumParser(), name) +} + +val argument = ArgumentFactory + +fun argument(parser: Parser, name: String) = + Argument.Required(parser, name) diff --git a/src/commonMain/kotlin/com/infendro/cli/command/context/Context.kt b/src/commonMain/kotlin/com/infendro/cli/command/context/Context.kt index bfc024a..5084cb0 100644 --- a/src/commonMain/kotlin/com/infendro/cli/command/context/Context.kt +++ b/src/commonMain/kotlin/com/infendro/cli/command/context/Context.kt @@ -1,65 +1,60 @@ package com.infendro.cli.command.context import com.infendro.cli.Dsl -import com.infendro.cli.command.Argument import com.infendro.cli.command.Command -import com.infendro.cli.command.Option +import com.infendro.cli.command.argument.Argument +import com.infendro.cli.command.option.Option import kotlin.reflect.KProperty @Dsl class Context internal constructor( val command: Command, - val commands: List, - val arguments: List, - val options: List, + private val commands: Map, Any>, + private val arguments: Map, List>, + private val options: Map, List>, ) { - class Cmd( - val name: String, - ) - - class Arg( - val value: Any, - ) - - class Opt( - val name: String, - val value: Any, - ) - internal fun execute() = (command.execute)() fun help() { - command.help?.let { println(it) } + command.help?.let { print(it) } } - operator fun Command.Key.getValue(thisRef: Any?, property: KProperty<*>): String = commands[index].name + @Suppress("UNCHECKED_CAST") + private val Command.Fallback.Key.value: T + get() = commands[this] as T - private val Argument.index: Int - get() = command.arguments.takeWhile { it != this }.sumOf { it.count } + operator fun Command.Fallback.Key.getValue(thisRef: Any?, property: KProperty<*>): T = + value @Suppress("UNCHECKED_CAST") - private val Argument.value: T? - get() = arguments.getOrNull(index)?.value as? T + private val Argument.values: List + get() = arguments[this] as List + + operator fun Argument.Required.getValue(thisRef: Any?, property: KProperty<*>): T = + values.first() + + operator fun Argument.OrElse.getValue(thisRef: Any?, property: KProperty<*>): T = + values.firstOrNull() ?: other + + operator fun Argument.OrNull.getValue(thisRef: Any?, property: KProperty<*>): T? = + values.firstOrNull() + + operator fun Argument.Variable.getValue(thisRef: Any?, property: KProperty<*>): List = + values @Suppress("UNCHECKED_CAST") - private val Argument.Variable.values: List - get() = arguments.drop(index).take(count).map { it.value as T } + private val Option.values: List + get() = options[this] as List - operator fun Argument.Required.getValue(thisRef: Any?, property: KProperty<*>): T = value!! - operator fun Argument.OrElse.getValue(thisRef: Any?, property: KProperty<*>): T = value ?: other - operator fun Argument.OrNull.getValue(thisRef: Any?, property: KProperty<*>): T? = value - operator fun Argument.Variable.getValue(thisRef: Any?, property: KProperty<*>): List = values + operator fun Option.Required.getValue(thisRef: Any?, property: KProperty<*>): T = + values.first() - @Suppress("UNCHECKED_CAST") - private val Option.value: T? - get() = options.firstOrNull { it.name in names }?.value as? T + operator fun Option.OrElse.getValue(thisRef: Any?, property: KProperty<*>): T = + values.firstOrNull() ?: other - @Suppress("UNCHECKED_CAST") - private val Option.Variable.values: List - get() = options.filter { it.name in names }.map { it.value as T } + operator fun Option.OrNull.getValue(thisRef: Any?, property: KProperty<*>): T? = + values.firstOrNull() - operator fun Option.Required.getValue(thisRef: Any?, property: KProperty<*>): T = value!! - operator fun Option.OrElse.getValue(thisRef: Any?, property: KProperty<*>): T = value ?: other - operator fun Option.OrNull.getValue(thisRef: Any?, property: KProperty<*>): T? = value - operator fun Option.Variable.getValue(thisRef: Any?, property: KProperty<*>): List = values + operator fun Option.Variable.getValue(thisRef: Any?, property: KProperty<*>): List = + values } diff --git a/src/commonMain/kotlin/com/infendro/cli/command/context/Parser.kt b/src/commonMain/kotlin/com/infendro/cli/command/context/ContextParser.kt similarity index 65% rename from src/commonMain/kotlin/com/infendro/cli/command/context/Parser.kt rename to src/commonMain/kotlin/com/infendro/cli/command/context/ContextParser.kt index 19cc5e9..07b6795 100644 --- a/src/commonMain/kotlin/com/infendro/cli/command/context/Parser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/command/context/ContextParser.kt @@ -1,24 +1,13 @@ package com.infendro.cli.command.context import com.infendro.cli.command.Command -import com.infendro.cli.command.context.Context.* import com.infendro.cli.exception.run.* +import com.infendro.cli.parser.Parser -internal class Parser( +internal class ContextParser( private val root: Command, private val args: Array, ) { - private var index = 0 - private val current: String - get() = args[index] - private val next: String? - get() = args.getOrNull(index) - - private fun hasNext() = index < args.size - private fun consume() { - index++ - } - sealed class Result { data class Success( val context: Context, @@ -30,40 +19,60 @@ internal class Parser( data class Failure( val command: Command, - val exception: RunException, + val error: RuntimeError, ) : Result() } - fun success(context: Context) = Result.Success(context) - fun help(command: Command) = Result.Help(command) - fun failure(command: Command, exception: RunException) = Result.Failure(command, exception) + private fun success(context: Context) = Result.Success(context) + private fun help(command: Command) = Result.Help(command) + private fun failure(command: Command, exception: RuntimeError) = Result.Failure(command, exception) + + private var index = 0 + private val current: String + get() = args[index] + private val next: String? + get() = args.getOrNull(index) + + private fun hasNext() = index < args.size + private fun consume() { + index++ + } fun parse(): Result { - var command = root - val cmd = mutableListOf() - val arg = mutableListOf() - val opt = mutableListOf() - // command + var command = root + val commands = mutableMapOf, Any>() + while (hasNext()) { if (current.startsWith("-")) break - val c = command.commands.firstOrNull { it.name == current || it.fallback } - if (c == null) { + val cmd = command.commands.firstOrNull { + (it is Command.Named && it.name == current) || it is Command.Fallback<*> + } + if (cmd == null) { when { command.arguments.isEmpty() -> return failure(command, UnknownCommand(current)) else -> break } } - command = c - cmd += Cmd(current) + command = cmd + if (cmd is Command.Fallback<*>) { + val value = when (val result = cmd.parser.parse(current)) { + is Parser.Result.Success<*> -> result.value + is Parser.Result.Failure<*> -> return failure(command, result.error) + } + commands[cmd.key] = value + } consume() } // arguments and options + val arguments = command.arguments.associateWith { mutableListOf() } + val options = command.options.associateWith { mutableListOf() } + var endOfOptions = false var argumentIndex = 0 var argumentCount = 0 @@ -78,8 +87,11 @@ internal class Parser( val argument = command.arguments.getOrNull(argumentIndex) ?: return failure(command, UnexpectedArgument()) - val value = argument.parser.parse(trimmed) - arg += Arg(value) + val value = when (val result = argument.parser.parse(trimmed)) { + is Parser.Result.Success<*> -> result.value + is Parser.Result.Failure<*> -> return failure(command, result.error) + } + arguments[argument]!! += value argumentCount++ if (argumentCount == argument.count) { @@ -124,11 +136,15 @@ internal class Parser( ?: return failure(command, UnknownOption(name)) val value = when { - text != null -> option.parser.parse(text) + text != null -> + when (val result = option.parser.parse(current)) { + is Parser.Result.Success<*> -> result.value + is Parser.Result.Failure<*> -> return failure(command, result.error) + } option.flag -> option.fallback!! else -> return failure(command, MissingOptionValue(option)) } - opt += Opt(name, value) + options[option]!! += value } } @@ -148,23 +164,18 @@ internal class Parser( } // validate arguments - val required = command.arguments.filter { it.required } - var consumed = 0 - for (argument in required) { - val needed = if (argument === required.last()) argument.min else argument.count - val delta = arg.size - consumed - if (delta < needed) return failure(command, InvalidArgumentCount(argument, delta)) - - consumed += needed + for (argument in command.arguments) { + val count = arguments[argument]!!.count() + if (count !in argument.min..argument.count) return failure(command, InvalidArgumentCount(argument, count)) } // validate options for (option in command.options) { - val count = opt.count { it.name in option.names } + val count = options[option]!!.count() if (count !in option.min..option.count) return failure(command, InvalidOptionCount(option, count)) } - val context = Context(command, cmd, arg, opt) + val context = Context(command, commands, arguments, options) return success(context) } } diff --git a/src/commonMain/kotlin/com/infendro/cli/command/help/DefaultHelpRenderer.kt b/src/commonMain/kotlin/com/infendro/cli/command/help/DefaultHelpRenderer.kt index 5c7379e..36bed08 100644 --- a/src/commonMain/kotlin/com/infendro/cli/command/help/DefaultHelpRenderer.kt +++ b/src/commonMain/kotlin/com/infendro/cli/command/help/DefaultHelpRenderer.kt @@ -1,8 +1,8 @@ package com.infendro.cli.command.help -import com.infendro.cli.command.Argument import com.infendro.cli.command.Command -import com.infendro.cli.command.Option +import com.infendro.cli.command.argument.Argument +import com.infendro.cli.command.option.Option object DefaultHelpRenderer : HelpRenderer { override fun render(command: Command) = buildString { @@ -26,7 +26,7 @@ object DefaultHelpRenderer : HelpRenderer { for (command in command.path) { append(" ") when { - command.fallback -> append("<${command.name}>") + command is Command.Fallback<*> -> append("<${command.name}>") else -> append(command.name) } } diff --git a/src/commonMain/kotlin/com/infendro/cli/command/Option.kt b/src/commonMain/kotlin/com/infendro/cli/command/option/Option.kt similarity index 73% rename from src/commonMain/kotlin/com/infendro/cli/command/Option.kt rename to src/commonMain/kotlin/com/infendro/cli/command/option/Option.kt index 5bc9d1c..ac3711a 100644 --- a/src/commonMain/kotlin/com/infendro/cli/command/Option.kt +++ b/src/commonMain/kotlin/com/infendro/cli/command/option/Option.kt @@ -1,8 +1,8 @@ -package com.infendro.cli.command +package com.infendro.cli.command.option import com.infendro.cli.exception.build.InvalidRange import com.infendro.cli.exception.build.MissingOptionName -import com.infendro.cli.parser.* +import com.infendro.cli.parser.Parser sealed class Option( val parser: Parser, @@ -90,21 +90,4 @@ sealed class Option( max: Int?, ) : this(parser, null, names, min, max) } - - companion object { - fun string(vararg names: String) = option(StringParser, *names) - fun int(vararg names: String) = option(IntParser, *names) - fun long(vararg names: String) = option(LongParser, *names) - fun float(vararg names: String) = option(FloatParser, *names) - fun double(vararg names: String) = option(DoubleParser, *names) - fun boolean(vararg names: String) = option(BooleanParser, true, *names) - - inline fun > enum(vararg names: String) = option(enumParser(), *names) - } } - -fun option(parser: Parser, vararg names: String) = - Option.Required(parser, names.asList()) - -fun option(parser: Parser, fallback: T, vararg names: String) = - Option.Required(parser, fallback, names.asList()) diff --git a/src/commonMain/kotlin/com/infendro/cli/command/option/OptionFactory.kt b/src/commonMain/kotlin/com/infendro/cli/command/option/OptionFactory.kt new file mode 100644 index 0000000..82ceaab --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/command/option/OptionFactory.kt @@ -0,0 +1,22 @@ +package com.infendro.cli.command.option + +import com.infendro.cli.parser.* + +object OptionFactory { + fun string(vararg names: String) = option(StringParser, *names) + fun int(vararg names: String) = option(IntParser, *names) + fun long(vararg names: String) = option(LongParser, *names) + fun float(vararg names: String) = option(FloatParser, *names) + fun double(vararg names: String) = option(DoubleParser, *names) + fun boolean(vararg names: String) = option(BooleanParser, true, *names) + + inline fun > enum(vararg names: String) = option(enumParser(), *names) +} + +val option = OptionFactory + +fun option(parser: Parser, vararg names: String) = + Option.Required(parser, names.asList()) + +fun option(parser: Parser, fallback: T, vararg names: String) = + Option.Required(parser, fallback, names.asList()) diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/BuildException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/BuildException.kt deleted file mode 100644 index 78c7397..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/BuildException.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.infendro.cli.exception.build - -import com.infendro.cli.exception.CliException - -abstract class BuildException : CliException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/CliException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/CliException.kt similarity index 51% rename from src/commonMain/kotlin/com/infendro/cli/exception/CliException.kt rename to src/commonMain/kotlin/com/infendro/cli/exception/build/CliException.kt index 913de16..1f666d7 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/CliException.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/CliException.kt @@ -1,3 +1,3 @@ -package com.infendro.cli.exception +package com.infendro.cli.exception.build abstract class CliException : Exception() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateCommand.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateCommand.kt index 5f02c1d..fbddcd7 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateCommand.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateCommand.kt @@ -2,4 +2,4 @@ package com.infendro.cli.exception.build class DuplicateCommand( val command: String, -) : BuildException() +) : CliException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateExecute.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateExecute.kt index 0a106cb..818a437 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateExecute.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateExecute.kt @@ -1,3 +1,3 @@ package com.infendro.cli.exception.build -class DuplicateExecute : BuildException() +class DuplicateExecute : CliException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateOption.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateOption.kt index fe0bc83..687e9ea 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateOption.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateOption.kt @@ -2,4 +2,4 @@ package com.infendro.cli.exception.build class DuplicateOption( val name: String, -) : BuildException() +) : CliException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidArgument.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidArgument.kt index cc59440..ad901b4 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidArgument.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidArgument.kt @@ -2,4 +2,4 @@ package com.infendro.cli.exception.build class InvalidArgument( val name: String, -) : BuildException() +) : CliException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidArgumentOrder.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidArgumentOrder.kt index c7628e6..6e2a498 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidArgumentOrder.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidArgumentOrder.kt @@ -1,3 +1,3 @@ package com.infendro.cli.exception.build -class InvalidArgumentOrder : BuildException() +class InvalidArgumentOrder : CliException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidCommand.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidCommand.kt index f869336..f49aacd 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidCommand.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidCommand.kt @@ -2,4 +2,4 @@ package com.infendro.cli.exception.build class InvalidCommand( val name: String, -) : BuildException() +) : CliException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidCommandOrder.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidCommandOrder.kt index 582268a..4c35cf4 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidCommandOrder.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidCommandOrder.kt @@ -1,3 +1,3 @@ package com.infendro.cli.exception.build -class InvalidCommandOrder : BuildException() +class InvalidCommandOrder : CliException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidFallback.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidFallback.kt index f510674..cc50110 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidFallback.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidFallback.kt @@ -1,3 +1,3 @@ package com.infendro.cli.exception.build -class InvalidFallback : BuildException() +class InvalidFallback : CliException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidOption.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidOption.kt index 87ee939..f336e4d 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidOption.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidOption.kt @@ -2,4 +2,4 @@ package com.infendro.cli.exception.build class InvalidOption( val name: String, -) : BuildException() +) : CliException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidRange.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidRange.kt index 989a180..23eec23 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidRange.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidRange.kt @@ -1,3 +1,3 @@ package com.infendro.cli.exception.build -class InvalidRange : BuildException() +class InvalidRange : CliException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/MissingOptionName.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/MissingOptionName.kt index 7f19fd2..8725a87 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/MissingOptionName.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/MissingOptionName.kt @@ -1,3 +1,3 @@ package com.infendro.cli.exception.build -class MissingOptionName : BuildException() +class MissingOptionName : CliException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidArgumentCount.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidArgumentCount.kt index 86848b4..661b4c6 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidArgumentCount.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidArgumentCount.kt @@ -1,11 +1,11 @@ package com.infendro.cli.exception.run -import com.infendro.cli.command.Argument +import com.infendro.cli.command.argument.Argument class InvalidArgumentCount( val argument: Argument<*>, val count: Int, -) : RunException() { +) : RuntimeError() { override val message: String get() { val expected = when { diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidOptionCount.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidOptionCount.kt index 0b6c7fd..8be30ba 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidOptionCount.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidOptionCount.kt @@ -1,11 +1,11 @@ package com.infendro.cli.exception.run -import com.infendro.cli.command.Option +import com.infendro.cli.command.option.Option class InvalidOptionCount( val option: Option<*>, val count: Int, -) : RunException() { +) : RuntimeError() { override val message: String get() { val expected = when { diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidValue.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidValue.kt index dd7fcf5..40285a0 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidValue.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidValue.kt @@ -5,7 +5,7 @@ import kotlin.reflect.KClass class InvalidValue( val value: String, val type: KClass<*>, -) : RunException() { +) : RuntimeError() { override val message: String get() = """"$value" cannot be converted to ${type.simpleName ?: "Unknown"}""" } diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/MalformedOption.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/MalformedOption.kt index 9ea3d06..b107348 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/MalformedOption.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/MalformedOption.kt @@ -2,7 +2,7 @@ package com.infendro.cli.exception.run class MalformedOption( val option: String, -) : RunException() { +) : RuntimeError() { override val message: String get() = """malformed option "$option"""" } diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingOptionValue.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingOptionValue.kt index 20c2a27..e7f6bb9 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingOptionValue.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingOptionValue.kt @@ -1,10 +1,10 @@ package com.infendro.cli.exception.run -import com.infendro.cli.command.Option +import com.infendro.cli.command.option.Option class MissingOptionValue( val option: Option<*>, -) : RunException() { +) : RuntimeError() { override val message: String get() = """no value provided for option "${option.name}"""" } diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/RunException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/RunException.kt deleted file mode 100644 index 5c8a626..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/RunException.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.infendro.cli.exception.run - -import com.infendro.cli.exception.CliException - -abstract class RunException : CliException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/RuntimeError.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/RuntimeError.kt new file mode 100644 index 0000000..d53e4c2 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/RuntimeError.kt @@ -0,0 +1,5 @@ +package com.infendro.cli.exception.run + +abstract class RuntimeError { + abstract val message: String +} diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/UnexpectedArgument.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/UnexpectedArgument.kt index bf63c2c..8570525 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/UnexpectedArgument.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/UnexpectedArgument.kt @@ -1,6 +1,6 @@ package com.infendro.cli.exception.run -class UnexpectedArgument : RunException() { +class UnexpectedArgument : RuntimeError() { override val message: String get() = """unexpected argument""" } diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownCommand.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownCommand.kt index 4fb1117..2474927 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownCommand.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownCommand.kt @@ -2,7 +2,7 @@ package com.infendro.cli.exception.run class UnknownCommand( val command: String, -) : RunException() { +) : RuntimeError() { override val message: String get() = """unknown command "$command"""" } diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownOption.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownOption.kt index 38c9a41..ce27c2b 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownOption.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownOption.kt @@ -2,7 +2,7 @@ package com.infendro.cli.exception.run class UnknownOption( val option: String, -) : RunException() { +) : RuntimeError() { override val message: String get() = """unknown option "$option"""" } diff --git a/src/commonMain/kotlin/com/infendro/cli/parser/BooleanParser.kt b/src/commonMain/kotlin/com/infendro/cli/parser/BooleanParser.kt index d699bf1..2389e45 100644 --- a/src/commonMain/kotlin/com/infendro/cli/parser/BooleanParser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/parser/BooleanParser.kt @@ -1,14 +1,10 @@ package com.infendro.cli.parser -import kotlin.reflect.KClass - -object BooleanParser : Parser { - override val type: KClass - get() = Boolean::class - - override fun parse(text: String): Boolean = when (text) { - "true", "t" -> true - "false", "f" -> false - else -> invalid(text) - } +object BooleanParser : Parser(Boolean::class) { + override fun parse(text: String) = + when (text) { + "true", "t" -> success(true) + "false", "f" -> success(false) + else -> failure(text) + } } diff --git a/src/commonMain/kotlin/com/infendro/cli/parser/DoubleParser.kt b/src/commonMain/kotlin/com/infendro/cli/parser/DoubleParser.kt index f0286c7..2d1215b 100644 --- a/src/commonMain/kotlin/com/infendro/cli/parser/DoubleParser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/parser/DoubleParser.kt @@ -1,11 +1,8 @@ package com.infendro.cli.parser -import kotlin.reflect.KClass - -object DoubleParser : Parser { - override val type: KClass - get() = Double::class - - override fun parse(text: String): Double = text.toDoubleOrNull() - ?: invalid(text) +object DoubleParser : Parser(Double::class) { + override fun parse(text: String) = + text.toDoubleOrNull() + ?.let { success(it) } + ?: failure(text) } diff --git a/src/commonMain/kotlin/com/infendro/cli/parser/EnumParser.kt b/src/commonMain/kotlin/com/infendro/cli/parser/EnumParser.kt index bf9448f..5ee0b74 100644 --- a/src/commonMain/kotlin/com/infendro/cli/parser/EnumParser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/parser/EnumParser.kt @@ -4,11 +4,13 @@ import kotlin.enums.enumEntries import kotlin.reflect.KClass class EnumParser>( - override val type: KClass, + type: KClass, private val values: List, -) : Parser { - override fun parse(text: String): T = values.firstOrNull { it.name == text } - ?: invalid(text) +) : Parser(type) { + override fun parse(text: String) = + values.firstOrNull { it.name == text } + ?.let { success(it) } + ?: failure(text) } inline fun > enumParser() = EnumParser(T::class, enumEntries()) diff --git a/src/commonMain/kotlin/com/infendro/cli/parser/FloatParser.kt b/src/commonMain/kotlin/com/infendro/cli/parser/FloatParser.kt index aa3c3e2..c41f620 100644 --- a/src/commonMain/kotlin/com/infendro/cli/parser/FloatParser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/parser/FloatParser.kt @@ -1,11 +1,8 @@ package com.infendro.cli.parser -import kotlin.reflect.KClass - -object FloatParser : Parser { - override val type: KClass - get() = Float::class - - override fun parse(text: String): Float = text.toFloatOrNull() - ?: invalid(text) +object FloatParser : Parser(Float::class) { + override fun parse(text: String) = + text.toFloatOrNull() + ?.let { success(it) } + ?: failure(text) } diff --git a/src/commonMain/kotlin/com/infendro/cli/parser/IntParser.kt b/src/commonMain/kotlin/com/infendro/cli/parser/IntParser.kt index 570c7bd..3bf23ef 100644 --- a/src/commonMain/kotlin/com/infendro/cli/parser/IntParser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/parser/IntParser.kt @@ -1,11 +1,8 @@ package com.infendro.cli.parser -import kotlin.reflect.KClass - -object IntParser : Parser { - override val type: KClass - get() = Int::class - - override fun parse(text: String): Int = text.toIntOrNull() - ?: invalid(text) +object IntParser : Parser(Int::class) { + override fun parse(text: String) = + text.toIntOrNull() + ?.let { success(it) } + ?: failure(text) } diff --git a/src/commonMain/kotlin/com/infendro/cli/parser/LongParser.kt b/src/commonMain/kotlin/com/infendro/cli/parser/LongParser.kt index 4e8319b..4f218aa 100644 --- a/src/commonMain/kotlin/com/infendro/cli/parser/LongParser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/parser/LongParser.kt @@ -1,11 +1,8 @@ package com.infendro.cli.parser -import kotlin.reflect.KClass - -object LongParser : Parser { - override val type: KClass - get() = Long::class - - override fun parse(text: String): Long = text.toLongOrNull() - ?: invalid(text) +object LongParser : Parser(Long::class) { + override fun parse(text: String) = + text.toLongOrNull() + ?.let { success(it) } + ?: failure(text) } diff --git a/src/commonMain/kotlin/com/infendro/cli/parser/Parser.kt b/src/commonMain/kotlin/com/infendro/cli/parser/Parser.kt index ee9dfa2..d2bfe1b 100644 --- a/src/commonMain/kotlin/com/infendro/cli/parser/Parser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/parser/Parser.kt @@ -1,12 +1,24 @@ package com.infendro.cli.parser import com.infendro.cli.exception.run.InvalidValue +import com.infendro.cli.exception.run.RuntimeError import kotlin.reflect.KClass -interface Parser { - val type: KClass +abstract class Parser( + val type: KClass, +) { + sealed class Result { + data class Success( + val value: T, + ) : Result() - fun parse(text: String): T + data class Failure( + val error: RuntimeError, + ) : Result() + } - fun invalid(text: String): Nothing = throw InvalidValue(text, type) + protected fun success(value: T) = Result.Success(value) + protected fun failure(text: String) = Result.Failure(InvalidValue(text, type)) + + abstract fun parse(text: String): Result } diff --git a/src/commonMain/kotlin/com/infendro/cli/parser/StringParser.kt b/src/commonMain/kotlin/com/infendro/cli/parser/StringParser.kt index be7f9b3..ebf2f24 100644 --- a/src/commonMain/kotlin/com/infendro/cli/parser/StringParser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/parser/StringParser.kt @@ -1,10 +1,6 @@ package com.infendro.cli.parser -import kotlin.reflect.KClass - -object StringParser : Parser { - override val type: KClass - get() = String::class - - override fun parse(text: String) = text +object StringParser : Parser(String::class) { + override fun parse(text: String) = + success(text) }