improve exceptions

This commit is contained in:
2026-01-07 22:00:32 +01:00
parent 921229aa5b
commit 2a9f086c9d
43 changed files with 137 additions and 92 deletions

View File

@@ -7,11 +7,9 @@ import com.infendro.cli.command.context.ContextParser
import com.infendro.cli.command.help.DefaultHelpRenderer import com.infendro.cli.command.help.DefaultHelpRenderer
import com.infendro.cli.command.help.HelpRenderer import com.infendro.cli.command.help.HelpRenderer
import com.infendro.cli.command.option.Option import com.infendro.cli.command.option.Option
import com.infendro.cli.exception.build.* import com.infendro.cli.error.build.*
import com.infendro.cli.parser.* import com.infendro.cli.parser.*
import com.infendro.cli.util.Regex import com.infendro.cli.util.Regex
import com.infendro.cli.util.Regex.ARGUMENT
import com.infendro.cli.util.Regex.OPTION
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
sealed class Command private constructor( sealed class Command private constructor(
@@ -205,15 +203,11 @@ sealed class Command private constructor(
argument.required && last.optional -> throw InvalidArgumentOrder() argument.required && last.optional -> throw InvalidArgumentOrder()
} }
} }
if (!argument.name.matches(ARGUMENT)) throw InvalidArgument(argument.name)
} }
private fun validateOption(option: Option<*>) { private fun validateOption(option: Option<*>) {
for (name in option.names) { for (name in option.names) {
when { if (options.any { name in it.names }) throw DuplicateOption(name)
!name.matches(OPTION) -> throw InvalidOption(name)
options.any { name in it.names } -> throw DuplicateOption(name)
}
} }
} }
@@ -222,7 +216,7 @@ sealed class Command private constructor(
} }
private fun validateRenderer() { private fun validateRenderer() {
if (_renderer != null) throw Exception() //TODO if (_renderer != null) throw DuplicateRenderer()
} }
private fun validate() { private fun validate() {

View File

@@ -1,7 +1,9 @@
package com.infendro.cli.command.argument package com.infendro.cli.command.argument
import com.infendro.cli.exception.build.InvalidRange import com.infendro.cli.error.build.InvalidArgument
import com.infendro.cli.error.build.InvalidRange
import com.infendro.cli.parser.Parser import com.infendro.cli.parser.Parser
import com.infendro.cli.util.Regex.ARGUMENT
sealed class Argument<T : Any>( sealed class Argument<T : Any>(
val parser: Parser<T>, val parser: Parser<T>,
@@ -10,7 +12,10 @@ sealed class Argument<T : Any>(
val max: Int?, val max: Int?,
) { ) {
init { init {
if (min !in 0..count || max == 0) throw InvalidRange() when {
!name.matches(ARGUMENT) -> throw InvalidArgument(name)
min !in 0..count || max == 0 -> throw InvalidRange()
}
} }
val required: Boolean val required: Boolean

View File

@@ -1,7 +1,7 @@
package com.infendro.cli.command.context package com.infendro.cli.command.context
import com.infendro.cli.command.Command import com.infendro.cli.command.Command
import com.infendro.cli.exception.run.* import com.infendro.cli.error.run.*
import com.infendro.cli.parser.Parser import com.infendro.cli.parser.Parser
internal class ContextParser( internal class ContextParser(
@@ -19,13 +19,13 @@ internal class ContextParser(
data class Failure( data class Failure(
val command: Command, val command: Command,
val error: RuntimeError, val error: RunError,
) : Result() ) : Result()
} }
private fun success(context: Context) = Result.Success(context) private fun success(context: Context) = Result.Success(context)
private fun help(command: Command) = Result.Help(command) private fun help(command: Command) = Result.Help(command)
private fun failure(command: Command, exception: RuntimeError) = Result.Failure(command, exception) private fun failure(command: Command, exception: RunError) = Result.Failure(command, exception)
private var index = 0 private var index = 0
private val current: String private val current: String

View File

@@ -75,7 +75,11 @@ object DefaultHelpRenderer : HelpRenderer {
private fun StringBuilder.commands(commands: List<Command>) { private fun StringBuilder.commands(commands: List<Command>) {
appendLine("Commands:") appendLine("Commands:")
for (command in commands) { for (command in commands) {
appendLine(" * ${command.name}") val name = when {
command is Command.Fallback<*> -> "<${command.name}>"
else -> command.name
}
appendLine(" * $name")
} }
} }

View File

@@ -1,8 +1,10 @@
package com.infendro.cli.command.option package com.infendro.cli.command.option
import com.infendro.cli.exception.build.InvalidRange import com.infendro.cli.error.build.InvalidOption
import com.infendro.cli.exception.build.MissingOptionName import com.infendro.cli.error.build.InvalidRange
import com.infendro.cli.error.build.MissingOptionName
import com.infendro.cli.parser.Parser import com.infendro.cli.parser.Parser
import com.infendro.cli.util.Regex.OPTION
sealed class Option<T : Any>( sealed class Option<T : Any>(
val parser: Parser<T>, val parser: Parser<T>,
@@ -33,6 +35,7 @@ sealed class Option<T : Any>(
init { init {
when { when {
names.isEmpty() -> throw MissingOptionName() names.isEmpty() -> throw MissingOptionName()
names.any { !it.matches(OPTION) } -> throw InvalidOption(name)
min !in 0..count || max == 0 -> throw InvalidRange() min !in 0..count || max == 0 -> throw InvalidRange()
} }
} }

View File

@@ -8,7 +8,7 @@ object OptionFactory {
fun long(vararg names: String) = option(LongParser, *names) fun long(vararg names: String) = option(LongParser, *names)
fun float(vararg names: String) = option(FloatParser, *names) fun float(vararg names: String) = option(FloatParser, *names)
fun double(vararg names: String) = option(DoubleParser, *names) fun double(vararg names: String) = option(DoubleParser, *names)
fun boolean(vararg names: String) = option(BooleanParser, true, *names) fun boolean(vararg names: String) = option(BooleanParser, fallback = true, *names)
inline fun <reified T : Enum<T>> enum(vararg names: String) = option(enumParser<T>(), *names) inline fun <reified T : Enum<T>> enum(vararg names: String) = option(enumParser<T>(), *names)
} }

View File

@@ -0,0 +1,3 @@
package com.infendro.cli.error.build
abstract class BuildException : Error()

View File

@@ -0,0 +1,8 @@
package com.infendro.cli.error.build
class DuplicateCommand(
val command: String,
) : BuildException() {
override val message: String
get() = """duplicate command "$command""""
}

View File

@@ -0,0 +1,6 @@
package com.infendro.cli.error.build
class DuplicateExecute : BuildException() {
override val message: String
get() = """duplicate execute block"""
}

View File

@@ -0,0 +1,8 @@
package com.infendro.cli.error.build
class DuplicateOption(
val name: String,
) : BuildException() {
override val message: String
get() = """duplicate option "$name""""
}

View File

@@ -0,0 +1,6 @@
package com.infendro.cli.error.build
class DuplicateRenderer : BuildException() {
override val message: String
get() = """duplicate renderer"""
}

View File

@@ -0,0 +1,8 @@
package com.infendro.cli.error.build
class InvalidArgument(
val name: String,
) : BuildException() {
override val message: String
get() = """invalid argument name "$name""""
}

View File

@@ -0,0 +1,6 @@
package com.infendro.cli.error.build
class InvalidArgumentOrder : BuildException() {
override val message: String
get() = """invalid argument order"""
}

View File

@@ -0,0 +1,8 @@
package com.infendro.cli.error.build
class InvalidCommand(
val name: String,
) : BuildException() {
override val message: String
get() = """invalid command name "$name""""
}

View File

@@ -0,0 +1,6 @@
package com.infendro.cli.error.build
class InvalidCommandOrder : BuildException() {
override val message: String
get() = """invalid command order"""
}

View File

@@ -0,0 +1,6 @@
package com.infendro.cli.error.build
class InvalidFallback : BuildException() {
override val message: String
get() = """invalid fallback"""
}

View File

@@ -0,0 +1,8 @@
package com.infendro.cli.error.build
class InvalidOption(
val name: String,
) : BuildException() {
override val message: String
get() = """invalid option name "$name""""
}

View File

@@ -0,0 +1,6 @@
package com.infendro.cli.error.build
class InvalidRange : BuildException() {
override val message: String
get() = """invalid range"""
}

View File

@@ -0,0 +1,6 @@
package com.infendro.cli.error.build
class MissingOptionName : BuildException() {
override val message: String
get() = """missing option name"""
}

View File

@@ -1,11 +1,11 @@
package com.infendro.cli.exception.run package com.infendro.cli.error.run
import com.infendro.cli.command.argument.Argument import com.infendro.cli.command.argument.Argument
class InvalidArgumentCount( class InvalidArgumentCount(
val argument: Argument<*>, val argument: Argument<*>,
val count: Int, val count: Int,
) : RuntimeError() { ) : RunError() {
override val message: String override val message: String
get() { get() {
val expected = when { val expected = when {

View File

@@ -1,11 +1,11 @@
package com.infendro.cli.exception.run package com.infendro.cli.error.run
import com.infendro.cli.command.option.Option import com.infendro.cli.command.option.Option
class InvalidOptionCount( class InvalidOptionCount(
val option: Option<*>, val option: Option<*>,
val count: Int, val count: Int,
) : RuntimeError() { ) : RunError() {
override val message: String override val message: String
get() { get() {
val expected = when { val expected = when {

View File

@@ -1,11 +1,11 @@
package com.infendro.cli.exception.run package com.infendro.cli.error.run
import kotlin.reflect.KClass import kotlin.reflect.KClass
class InvalidValue( class InvalidValue(
val value: String, val value: String,
val type: KClass<*>, val type: KClass<*>,
) : RuntimeError() { ) : RunError() {
override val message: String override val message: String
get() = """"$value" cannot be converted to ${type.simpleName ?: "Unknown"}""" get() = """"$value" cannot be converted to ${type.simpleName ?: "Unknown"}"""
} }

View File

@@ -1,8 +1,8 @@
package com.infendro.cli.exception.run package com.infendro.cli.error.run
class MalformedOption( class MalformedOption(
val option: String, val option: String,
) : RuntimeError() { ) : RunError() {
override val message: String override val message: String
get() = """malformed option "$option"""" get() = """malformed option "$option""""
} }

View File

@@ -1,10 +1,10 @@
package com.infendro.cli.exception.run package com.infendro.cli.error.run
import com.infendro.cli.command.option.Option import com.infendro.cli.command.option.Option
class MissingOptionValue( class MissingOptionValue(
val option: Option<*>, val option: Option<*>,
) : RuntimeError() { ) : RunError() {
override val message: String override val message: String
get() = """no value provided for option "${option.name}"""" get() = """no value provided for option "${option.name}""""
} }

View File

@@ -0,0 +1,5 @@
package com.infendro.cli.error.run
abstract class RunError {
abstract val message: String
}

View File

@@ -0,0 +1,6 @@
package com.infendro.cli.error.run
class UnexpectedArgument : RunError() {
override val message: String
get() = """unexpected argument"""
}

View File

@@ -1,8 +1,8 @@
package com.infendro.cli.exception.run package com.infendro.cli.error.run
class UnknownCommand( class UnknownCommand(
val command: String, val command: String,
) : RuntimeError() { ) : RunError() {
override val message: String override val message: String
get() = """unknown command "$command"""" get() = """unknown command "$command""""
} }

View File

@@ -1,8 +1,8 @@
package com.infendro.cli.exception.run package com.infendro.cli.error.run
class UnknownOption( class UnknownOption(
val option: String, val option: String,
) : RuntimeError() { ) : RunError() {
override val message: String override val message: String
get() = """unknown option "$option"""" get() = """unknown option "$option""""
} }

View File

@@ -1,3 +0,0 @@
package com.infendro.cli.exception.build
abstract class CliException : Exception()

View File

@@ -1,5 +0,0 @@
package com.infendro.cli.exception.build
class DuplicateCommand(
val command: String,
) : CliException()

View File

@@ -1,3 +0,0 @@
package com.infendro.cli.exception.build
class DuplicateExecute : CliException()

View File

@@ -1,5 +0,0 @@
package com.infendro.cli.exception.build
class DuplicateOption(
val name: String,
) : CliException()

View File

@@ -1,5 +0,0 @@
package com.infendro.cli.exception.build
class InvalidArgument(
val name: String,
) : CliException()

View File

@@ -1,3 +0,0 @@
package com.infendro.cli.exception.build
class InvalidArgumentOrder : CliException()

View File

@@ -1,5 +0,0 @@
package com.infendro.cli.exception.build
class InvalidCommand(
val name: String,
) : CliException()

View File

@@ -1,3 +0,0 @@
package com.infendro.cli.exception.build
class InvalidCommandOrder : CliException()

View File

@@ -1,3 +0,0 @@
package com.infendro.cli.exception.build
class InvalidFallback : CliException()

View File

@@ -1,5 +0,0 @@
package com.infendro.cli.exception.build
class InvalidOption(
val name: String,
) : CliException()

View File

@@ -1,3 +0,0 @@
package com.infendro.cli.exception.build
class InvalidRange : CliException()

View File

@@ -1,3 +0,0 @@
package com.infendro.cli.exception.build
class MissingOptionName : CliException()

View File

@@ -1,5 +0,0 @@
package com.infendro.cli.exception.run
abstract class RuntimeError {
abstract val message: String
}

View File

@@ -1,6 +0,0 @@
package com.infendro.cli.exception.run
class UnexpectedArgument : RuntimeError() {
override val message: String
get() = """unexpected argument"""
}

View File

@@ -1,7 +1,7 @@
package com.infendro.cli.parser package com.infendro.cli.parser
import com.infendro.cli.exception.run.InvalidValue import com.infendro.cli.error.run.InvalidValue
import com.infendro.cli.exception.run.RuntimeError import com.infendro.cli.error.run.RunError
import kotlin.reflect.KClass import kotlin.reflect.KClass
abstract class Parser<T : Any>( abstract class Parser<T : Any>(
@@ -13,7 +13,7 @@ abstract class Parser<T : Any>(
) : Result<T>() ) : Result<T>()
data class Failure<T : Any>( data class Failure<T : Any>(
val error: RuntimeError, val error: RunError,
) : Result<T>() ) : Result<T>()
} }