Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc5786951d | ||
|
|
de79b6bcd5 |
@@ -4,7 +4,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "org.kargs"
|
group = "org.kargs"
|
||||||
version = "1.0.3"
|
version = "1.0.4"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
@@ -12,8 +12,8 @@ repositories {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
implementation(kotlin("stdlib"))
|
// implementation(kotlin("stdlib"))
|
||||||
implementation(kotlin("reflect"))
|
// implementation(kotlin("reflect"))
|
||||||
|
|
||||||
// JUnit 5 testing dependencies
|
// JUnit 5 testing dependencies
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0")
|
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0")
|
||||||
@@ -68,3 +68,4 @@ mavenPublishing {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package org.kargs
|
package org.kargs
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sealed class representing different argument types with conversion and validation logic
|
* Sealed class representing different argument types with conversion and validation logic
|
||||||
*/
|
*/
|
||||||
@@ -75,6 +77,51 @@ sealed class ArgType<T>(val typeName: String) {
|
|||||||
override fun getValidationDescription(): String = "one of: ${choices.joinToString(", ")}"
|
override fun getValidationDescription(): String = "one of: ${choices.joinToString(", ")}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional value type - can be used as flag or with value
|
||||||
|
*/
|
||||||
|
class OptionalValue(val defaultWhenPresent: String = "true") : ArgType<String>("OptionalValue") {
|
||||||
|
override fun convert(value: String): String = value
|
||||||
|
|
||||||
|
override fun getValidationDescription(): String = "optional value or flag"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File path type with existence and permision validation
|
||||||
|
*/
|
||||||
|
class FilePath(
|
||||||
|
val mustExist: Boolean = false,
|
||||||
|
val mustBeFile: Boolean = false,
|
||||||
|
val mustBeDirectory: Boolean = false,
|
||||||
|
val mustBeReadable: Boolean = false,
|
||||||
|
val mustBeWritable: Boolean = false
|
||||||
|
) : ArgType<File>("File") {
|
||||||
|
override fun convert(value: String): File = File(value)
|
||||||
|
|
||||||
|
override fun validate(value: File): Boolean {
|
||||||
|
return when {
|
||||||
|
mustExist && !value.exists() -> false
|
||||||
|
mustBeFile && !value.isFile -> false
|
||||||
|
mustBeDirectory && !value.isDirectory -> false
|
||||||
|
mustBeReadable && !value.canRead() -> false
|
||||||
|
mustBeWritable && !value.canWrite() -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getValidationDescription(): String {
|
||||||
|
val conditions = mutableListOf<String>()
|
||||||
|
if (mustExist) conditions.add("must exist")
|
||||||
|
if (mustBeFile) conditions.add("must be a file")
|
||||||
|
if (mustBeDirectory) conditions.add("must be a directory")
|
||||||
|
if (mustBeReadable) conditions.add("must be readable")
|
||||||
|
if (mustBeWritable) conditions.add("must be writable")
|
||||||
|
|
||||||
|
return if (conditions.isEmpty()) "file path" else "file path (${conditions.joinToString(", ")})"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val String: ArgType<kotlin.String> = StringType
|
val String: ArgType<kotlin.String> = StringType
|
||||||
val Int: ArgType<kotlin.Int> = IntType
|
val Int: ArgType<kotlin.Int> = IntType
|
||||||
@@ -90,5 +137,35 @@ sealed class ArgType<T>(val typeName: String) {
|
|||||||
* Create a choice type from valid options
|
* Create a choice type from valid options
|
||||||
*/
|
*/
|
||||||
fun choice(vararg options: kotlin.String) = Choice(options.toList())
|
fun choice(vararg options: kotlin.String) = Choice(options.toList())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an optional value or flag
|
||||||
|
*/
|
||||||
|
fun optionalValue(defaultWhenPresent: String = "true") = OptionalValue(defaultWhenPresent)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File that must exist and be a regular file
|
||||||
|
*/
|
||||||
|
fun existingFile() = FilePath(mustExist = true, mustBeFile = true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directory that must exist
|
||||||
|
*/
|
||||||
|
fun existingDirectory() = FilePath(mustExist = true, mustBeDirectory = true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File that must exist and be readable
|
||||||
|
*/
|
||||||
|
fun readableFile() = FilePath(mustExist = true, mustBeFile = true, mustBeReadable = true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File that must be writable (can be created if doesn't exist)
|
||||||
|
*/
|
||||||
|
fun writableFile() = FilePath(mustBeWritable = true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any file path (no validation)
|
||||||
|
*/
|
||||||
|
fun filePath() = FilePath()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
37
src/main/kotlin/org/kargs/OptionalOption.kt
Normal file
37
src/main/kotlin/org/kargs/OptionalOption.kt
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package org.kargs
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option that can be used as a flag or with a value
|
||||||
|
*/
|
||||||
|
class OptionalOption(
|
||||||
|
val longName: String,
|
||||||
|
val shortName: String? = null,
|
||||||
|
description: String? = null,
|
||||||
|
private val defaultWhenPresent: String = "true"
|
||||||
|
) : KargsProperty<String>(description) {
|
||||||
|
private var wasExplicitlySet = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
require(longName.isNotBlank()) { "Long name cannot be blank" }
|
||||||
|
require(!longName.startsWith("-")) { "Long name should not start with dashes" }
|
||||||
|
shortName?.let {
|
||||||
|
require(it.length == 1) { "Short name must be exactly one character" }
|
||||||
|
require(!it.startsWith("-")) { "Short name should not start with dashes" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseValue(str: String) {
|
||||||
|
value = str
|
||||||
|
wasExplicitlySet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set as flag (no value provided)
|
||||||
|
*/
|
||||||
|
fun setAsFlag() {
|
||||||
|
value = defaultWhenPresent
|
||||||
|
wasExplicitlySet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isSet(): Boolean = wasExplicitlySet
|
||||||
|
}
|
||||||
@@ -25,6 +25,10 @@ class Parser(
|
|||||||
* @param args Array of command-line arguments
|
* @param args Array of command-line arguments
|
||||||
*/
|
*/
|
||||||
fun parse(args: Array<String>) {
|
fun parse(args: Array<String>) {
|
||||||
|
if (handleVersionFlag(args)) {
|
||||||
|
return // Exit after showing version
|
||||||
|
}
|
||||||
|
|
||||||
if (args.isEmpty()) {
|
if (args.isEmpty()) {
|
||||||
if (config.helpOnEmpty) {
|
if (config.helpOnEmpty) {
|
||||||
printGlobalHelp()
|
printGlobalHelp()
|
||||||
@@ -108,6 +112,7 @@ class Parser(
|
|||||||
private fun parseLongOption(cmd: Subcommand, key: String, args: Array<String>, index: Int): Int {
|
private fun parseLongOption(cmd: Subcommand, key: String, args: Array<String>, index: Int): Int {
|
||||||
val option = cmd.options.firstOrNull { it.longName == key }
|
val option = cmd.options.firstOrNull { it.longName == key }
|
||||||
val flag = cmd.flags.firstOrNull { it.longName == key }
|
val flag = cmd.flags.firstOrNull { it.longName == key }
|
||||||
|
val optionalOption = cmd.optionalOptions.firstOrNull { it.longName == key }
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
option != null -> {
|
option != null -> {
|
||||||
@@ -122,6 +127,18 @@ class Parser(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
optionalOption != null -> {
|
||||||
|
// Check if next arg exists and doesn't start with -
|
||||||
|
if (index + 1 < args.size && !args[index + 1].startsWith("-")) {
|
||||||
|
// Has value
|
||||||
|
optionalOption.parseValue(args[index + 1])
|
||||||
|
index + 1
|
||||||
|
} else {
|
||||||
|
// Used as flag
|
||||||
|
optionalOption.setAsFlag()
|
||||||
|
index
|
||||||
|
}
|
||||||
|
}
|
||||||
flag != null -> {
|
flag != null -> {
|
||||||
flag.setFlag()
|
flag.setFlag()
|
||||||
index
|
index
|
||||||
@@ -241,7 +258,8 @@ class Parser(
|
|||||||
* Print global help menu
|
* Print global help menu
|
||||||
*/
|
*/
|
||||||
private fun printGlobalHelp() {
|
private fun printGlobalHelp() {
|
||||||
println(colorize("Usage: $programName <command> [options]", Color.BOLD))
|
val versionInfo = config.programVersion?.let { " (v$it)" } ?: ""
|
||||||
|
println(colorize("Usage: $programName$versionInfo <command> [options]", Color.BOLD))
|
||||||
println()
|
println()
|
||||||
println(colorize("Commands:", Color.BOLD))
|
println(colorize("Commands:", Color.BOLD))
|
||||||
commands.forEach { cmd ->
|
commands.forEach { cmd ->
|
||||||
@@ -308,6 +326,31 @@ class Parser(
|
|||||||
// Exit gracefully instead of throwing
|
// Exit gracefully instead of throwing
|
||||||
// kotlin.system.exitProcess(1)
|
// kotlin.system.exitProcess(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if version flag is present and handle it
|
||||||
|
*/
|
||||||
|
private fun handleVersionFlag(args: Array<String>): Boolean {
|
||||||
|
if (config.programVersion == null) return false
|
||||||
|
|
||||||
|
val versionFlags = listOf("--${config.versionLongName}", "-${config.versionShortName}")
|
||||||
|
|
||||||
|
if (args.any { it in versionFlags }) {
|
||||||
|
printVersion()
|
||||||
|
return true // Indicates version was shown, should exit
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print version information
|
||||||
|
*/
|
||||||
|
private fun printVersion() {
|
||||||
|
config.programVersion?.let { version ->
|
||||||
|
println("$programName version $version")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ data class ParserConfig(
|
|||||||
val helpOnEmpty: Boolean = true, // Show help when no args provided
|
val helpOnEmpty: Boolean = true, // Show help when no args provided
|
||||||
val caseSensitive: Boolean = true,
|
val caseSensitive: Boolean = true,
|
||||||
val allowAbbreviations: Boolean = false, // Allow partial option matching
|
val allowAbbreviations: Boolean = false, // Allow partial option matching
|
||||||
val programVersion: String? = null // Adds --version and -v flag if set
|
val programVersion: String? = null, // Adds --version and -v flag if set
|
||||||
|
|
||||||
|
// Optional customize version flag names
|
||||||
|
val versionLongName: String = "version",
|
||||||
|
val versionShortName: String = "V" // Capital V to avoid conflicts with -v (verbose)
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val DEFAULT = ParserConfig()
|
val DEFAULT = ParserConfig()
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ abstract class Subcommand(
|
|||||||
private val _options = mutableListOf<Option<*>>()
|
private val _options = mutableListOf<Option<*>>()
|
||||||
private val _flags = mutableListOf<Flag>()
|
private val _flags = mutableListOf<Flag>()
|
||||||
private val _arguments = mutableListOf<Argument<*>>()
|
private val _arguments = mutableListOf<Argument<*>>()
|
||||||
|
private val _optionalOptions = mutableListOf<OptionalOption>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
require(name.isNotBlank()) { "Subcommand name cannot be blank" }
|
require(name.isNotBlank()) { "Subcommand name cannot be blank" }
|
||||||
@@ -30,6 +31,7 @@ abstract class Subcommand(
|
|||||||
is Option<*> -> _options += prop
|
is Option<*> -> _options += prop
|
||||||
is Flag -> _flags += prop
|
is Flag -> _flags += prop
|
||||||
is Argument<*> -> _arguments += prop
|
is Argument<*> -> _arguments += prop
|
||||||
|
is OptionalOption -> _optionalOptions += prop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +39,7 @@ abstract class Subcommand(
|
|||||||
val options: List<Option<*>> get() = _options
|
val options: List<Option<*>> get() = _options
|
||||||
val flags: List<Flag> get() = _flags
|
val flags: List<Flag> get() = _flags
|
||||||
val arguments: List<Argument<*>> get() = _arguments
|
val arguments: List<Argument<*>> get() = _arguments
|
||||||
|
val optionalOptions: List<OptionalOption> get() = _optionalOptions
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute this subcommand - must be implemented by subclasses
|
* Execute this subcommand - must be implemented by subclasses
|
||||||
@@ -70,6 +73,18 @@ abstract class Subcommand(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (optionalOptions.isNotEmpty()) {
|
||||||
|
println()
|
||||||
|
println("Optional Value Options:")
|
||||||
|
optionalOptions.forEach { option ->
|
||||||
|
val shortName = option.shortName?.let { "-$it, " } ?: " "
|
||||||
|
println(" $shortName--${option.longName} [value]")
|
||||||
|
option.description?.let { desc ->
|
||||||
|
println(" $desc (can be used as flag or with value)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (flags.isNotEmpty()) {
|
if (flags.isNotEmpty()) {
|
||||||
println()
|
println()
|
||||||
println("Flags:")
|
println("Flags:")
|
||||||
@@ -117,12 +132,14 @@ abstract class Subcommand(
|
|||||||
*/
|
*/
|
||||||
private fun getTypeInfo(type: ArgType<*>): String {
|
private fun getTypeInfo(type: ArgType<*>): String {
|
||||||
return when (type) {
|
return when (type) {
|
||||||
is ArgType.StringType -> ""
|
is ArgType.StringType -> " <string>"
|
||||||
is ArgType.IntType -> " <int>"
|
is ArgType.IntType -> " <int>"
|
||||||
is ArgType.DoubleType -> " <double>"
|
is ArgType.DoubleType -> " <double>"
|
||||||
is ArgType.BooleanType -> " <bool>"
|
is ArgType.BooleanType -> " <bool>"
|
||||||
is ArgType.IntRange -> " <${type.min}-${type.max}>"
|
is ArgType.IntRange -> " <${type.min}-${type.max}>"
|
||||||
is ArgType.Choice -> " <${type.choices.joinToString("|")}>"
|
is ArgType.Choice -> " <${type.choices.joinToString("|")}>"
|
||||||
|
is ArgType.OptionalValue -> " [string]"
|
||||||
|
is ArgType.FilePath -> " <path>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import org.junit.jupiter.params.provider.ValueSource
|
|||||||
import org.kargs.ArgType
|
import org.kargs.ArgType
|
||||||
import org.kargs.ArgumentParseException
|
import org.kargs.ArgumentParseException
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.io.path.createTempFile
|
||||||
|
import kotlin.io.path.createTempDirectory
|
||||||
|
|
||||||
class ArgTypeTest {
|
class ArgTypeTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `String type converts correctly`() {
|
fun `String type converts correctly`() {
|
||||||
val type = ArgType.String
|
val type = ArgType.String
|
||||||
@@ -17,7 +19,7 @@ class ArgTypeTest {
|
|||||||
assertEquals("", type.convert(""))
|
assertEquals("", type.convert(""))
|
||||||
assertEquals("123", type.convert("123"))
|
assertEquals("123", type.convert("123"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Int type converts valid integers`() {
|
fun `Int type converts valid integers`() {
|
||||||
val type = ArgType.Int
|
val type = ArgType.Int
|
||||||
@@ -25,7 +27,7 @@ class ArgTypeTest {
|
|||||||
assertEquals(-10, type.convert("-10"))
|
assertEquals(-10, type.convert("-10"))
|
||||||
assertEquals(0, type.convert("0"))
|
assertEquals(0, type.convert("0"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Int type throws on invalid input`() {
|
fun `Int type throws on invalid input`() {
|
||||||
val type = ArgType.Int
|
val type = ArgType.Int
|
||||||
@@ -33,28 +35,28 @@ class ArgTypeTest {
|
|||||||
assertThrows<ArgumentParseException> { type.convert("12.5") }
|
assertThrows<ArgumentParseException> { type.convert("12.5") }
|
||||||
assertThrows<ArgumentParseException> { type.convert("") }
|
assertThrows<ArgumentParseException> { type.convert("") }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@ValueSource(strings = ["true", "TRUE", "yes", "YES", "1", "on", "ON"])
|
@ValueSource(strings = ["true", "TRUE", "yes", "YES", "1", "on", "ON"])
|
||||||
fun `Boolean type converts true values`(input: String) {
|
fun `Boolean type converts true values`(input: String) {
|
||||||
val type = ArgType.Boolean
|
val type = ArgType.Boolean
|
||||||
assertEquals(true, type.convert(input))
|
assertEquals(true, type.convert(input))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@ValueSource(strings = ["false", "FALSE", "no", "NO", "0", "off", "OFF"])
|
@ValueSource(strings = ["false", "FALSE", "no", "NO", "0", "off", "OFF"])
|
||||||
fun `Boolean type converts false values`(input: String) {
|
fun `Boolean type converts false values`(input: String) {
|
||||||
val type = ArgType.Boolean
|
val type = ArgType.Boolean
|
||||||
assertEquals(false, type.convert(input))
|
assertEquals(false, type.convert(input))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Boolean type throws on invalid input`() {
|
fun `Boolean type throws on invalid input`() {
|
||||||
val type = ArgType.Boolean
|
val type = ArgType.Boolean
|
||||||
assertThrows<ArgumentParseException> { type.convert("maybe") }
|
assertThrows<ArgumentParseException> { type.convert("maybe") }
|
||||||
assertThrows<ArgumentParseException> { type.convert("2") }
|
assertThrows<ArgumentParseException> { type.convert("2") }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Double type converts correctly`() {
|
fun `Double type converts correctly`() {
|
||||||
val type = ArgType.Double
|
val type = ArgType.Double
|
||||||
@@ -62,46 +64,233 @@ class ArgTypeTest {
|
|||||||
assertEquals(-2.5, type.convert("-2.5"))
|
assertEquals(-2.5, type.convert("-2.5"))
|
||||||
assertEquals(42.0, type.convert("42"))
|
assertEquals(42.0, type.convert("42"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Double type throws on invalid input`() {
|
fun `Double type throws on invalid input`() {
|
||||||
val type = ArgType.Double
|
val type = ArgType.Double
|
||||||
assertThrows<ArgumentParseException> { type.convert("not-a-number") }
|
assertThrows<ArgumentParseException> { type.convert("not-a-number") }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `IntRange validates bounds`() {
|
fun `IntRange validates bounds`() {
|
||||||
val type = ArgType.intRange(1, 10)
|
val type = ArgType.intRange(1, 10)
|
||||||
assertEquals(5, type.convert("5"))
|
assertEquals(5, type.convert("5"))
|
||||||
assertEquals(1, type.convert("1"))
|
assertEquals(1, type.convert("1"))
|
||||||
assertEquals(10, type.convert("10"))
|
assertEquals(10, type.convert("10"))
|
||||||
|
|
||||||
assertThrows<ArgumentParseException> { type.convert("0") }
|
assertThrows<ArgumentParseException> { type.convert("0") }
|
||||||
assertThrows<ArgumentParseException> { type.convert("11") }
|
assertThrows<ArgumentParseException> { type.convert("11") }
|
||||||
assertThrows<ArgumentParseException> { type.convert("-5") }
|
assertThrows<ArgumentParseException> { type.convert("-5") }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Choice validates options`() {
|
fun `Choice validates options`() {
|
||||||
val type = ArgType.choice("red", "green", "blue")
|
val type = ArgType.choice("red", "green", "blue")
|
||||||
assertEquals("red", type.convert("red"))
|
assertEquals("red", type.convert("red"))
|
||||||
assertEquals("green", type.convert("green"))
|
assertEquals("green", type.convert("green"))
|
||||||
assertEquals("blue", type.convert("blue"))
|
assertEquals("blue", type.convert("blue"))
|
||||||
|
|
||||||
assertThrows<ArgumentParseException> { type.convert("yellow") }
|
assertThrows<ArgumentParseException> { type.convert("yellow") }
|
||||||
assertThrows<ArgumentParseException> { type.convert("RED") } // case sensitive
|
assertThrows<ArgumentParseException> { type.convert("RED") } // case sensitive
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `IntRange provides correct validation description`() {
|
fun `IntRange provides correct validation description`() {
|
||||||
val type = ArgType.intRange(5, 15)
|
val type = ArgType.intRange(5, 15)
|
||||||
assertEquals("integer between 5 and 15", type.getValidationDescription())
|
assertEquals("integer between 5 and 15", type.getValidationDescription())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Choice provides correct validation description`() {
|
fun `Choice provides correct validation description`() {
|
||||||
val type = ArgType.choice("apple", "banana", "cherry")
|
val type = ArgType.choice("apple", "banana", "cherry")
|
||||||
assertEquals("one of: apple, banana, cherry", type.getValidationDescription())
|
assertEquals("one of: apple, banana, cherry", type.getValidationDescription())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `FilePath converts string to File`() {
|
||||||
|
val type = ArgType.filePath()
|
||||||
|
val result = type.convert("/path/to/file.txt")
|
||||||
|
assertEquals("/path/to/file.txt", result.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `FilePath with no constraints allows any path`() {
|
||||||
|
val type = ArgType.filePath()
|
||||||
|
// Should not throw for non-existent files
|
||||||
|
val result = type.convert("/non/existent/path")
|
||||||
|
assertEquals("/non/existent/path", result.path)
|
||||||
|
assertEquals(true, type.validate(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `existingFile validates file exists and is file`() {
|
||||||
|
val type = ArgType.existingFile()
|
||||||
|
|
||||||
|
// Create a temporary file for testing
|
||||||
|
val tempFile = kotlin.io.path.createTempFile().toFile()
|
||||||
|
tempFile.writeText("test content")
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Should work with existing file
|
||||||
|
val result = type.convert(tempFile.absolutePath)
|
||||||
|
assertEquals(true, type.validate(result))
|
||||||
|
|
||||||
|
// Should fail with non-existent file
|
||||||
|
val nonExistent = type.convert("/non/existent/file.txt")
|
||||||
|
assertEquals(false, type.validate(nonExistent))
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
tempFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `existingFile fails validation on directory`() {
|
||||||
|
val type = ArgType.existingFile()
|
||||||
|
|
||||||
|
// Create a temporary directory
|
||||||
|
val tempDir = kotlin.io.path.createTempDirectory().toFile()
|
||||||
|
|
||||||
|
try {
|
||||||
|
val result = type.convert(tempDir.absolutePath)
|
||||||
|
assertEquals(false, type.validate(result)) // Should fail because it's a directory, not a file
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
tempDir.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `existingDirectory validates directory exists and is directory`() {
|
||||||
|
val type = ArgType.existingDirectory()
|
||||||
|
|
||||||
|
// Create a temporary directory
|
||||||
|
val tempDir = kotlin.io.path.createTempDirectory().toFile()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Should work with existing directory
|
||||||
|
val result = type.convert(tempDir.absolutePath)
|
||||||
|
assertEquals(true, type.validate(result))
|
||||||
|
|
||||||
|
// Should fail with non-existent directory
|
||||||
|
val nonExistent = type.convert("/non/existent/directory")
|
||||||
|
assertEquals(false, type.validate(nonExistent))
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
tempDir.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `existingDirectory fails validation on file`() {
|
||||||
|
val type = ArgType.existingDirectory()
|
||||||
|
|
||||||
|
// Create a temporary file
|
||||||
|
val tempFile = kotlin.io.path.createTempFile().toFile()
|
||||||
|
tempFile.writeText("test")
|
||||||
|
|
||||||
|
try {
|
||||||
|
val result = type.convert(tempFile.absolutePath)
|
||||||
|
assertEquals(false, type.validate(result)) // Should fail because it's a file, not a directory
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
tempFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `readableFile validates file is readable`() {
|
||||||
|
val type = ArgType.readableFile()
|
||||||
|
|
||||||
|
// Create a temporary file
|
||||||
|
val tempFile = kotlin.io.path.createTempFile().toFile()
|
||||||
|
tempFile.writeText("test content")
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Should work with readable file
|
||||||
|
val result = type.convert(tempFile.absolutePath)
|
||||||
|
assertEquals(true, type.validate(result))
|
||||||
|
|
||||||
|
// Make file unreadable (this might not work on all systems)
|
||||||
|
tempFile.setReadable(false)
|
||||||
|
assertEquals(false, type.validate(result))
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
tempFile.setReadable(true) // Restore permissions
|
||||||
|
tempFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `writableFile validates file is writable`() {
|
||||||
|
val type = ArgType.writableFile()
|
||||||
|
|
||||||
|
// Create a temporary file
|
||||||
|
val tempFile = kotlin.io.path.createTempFile().toFile()
|
||||||
|
tempFile.writeText("test content")
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Should work with writable file
|
||||||
|
val result = type.convert(tempFile.absolutePath)
|
||||||
|
assertEquals(true, type.validate(result))
|
||||||
|
|
||||||
|
// Make file read-only (this might not work on all systems)
|
||||||
|
tempFile.setWritable(false)
|
||||||
|
assertEquals(false, type.validate(result))
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
tempFile.setWritable(true) // Restore permissions
|
||||||
|
tempFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `FilePath validation description includes constraints`() {
|
||||||
|
assertEquals("file path", ArgType.filePath().getValidationDescription())
|
||||||
|
assertEquals("file path (must exist, must be a file)", ArgType.existingFile().getValidationDescription())
|
||||||
|
assertEquals("file path (must exist, must be a directory)", ArgType.existingDirectory().getValidationDescription())
|
||||||
|
assertEquals("file path (must exist, must be a file, must be readable)", ArgType.readableFile().getValidationDescription())
|
||||||
|
assertEquals("file path (must be writable)", ArgType.writableFile().getValidationDescription())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `FilePath with multiple constraints`() {
|
||||||
|
val type = ArgType.FilePath(
|
||||||
|
mustExist = true,
|
||||||
|
mustBeFile = true,
|
||||||
|
mustBeReadable = true,
|
||||||
|
mustBeWritable = true
|
||||||
|
)
|
||||||
|
|
||||||
|
val tempFile = kotlin.io.path.createTempFile().toFile()
|
||||||
|
tempFile.writeText("test")
|
||||||
|
|
||||||
|
try {
|
||||||
|
val result = type.convert(tempFile.absolutePath)
|
||||||
|
assertEquals(true, type.validate(result))
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"file path (must exist, must be a file, must be readable, must be writable)",
|
||||||
|
type.getValidationDescription()
|
||||||
|
)
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
tempFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `FilePath handles non-existent parent directories`() {
|
||||||
|
val type = ArgType.writableFile()
|
||||||
|
|
||||||
|
// Path with non-existent parent directory
|
||||||
|
val result = type.convert("/non/existent/parent/file.txt")
|
||||||
|
|
||||||
|
// Should convert fine (validation happens separately)
|
||||||
|
assertEquals("/non/existent/parent/file.txt", result.path)
|
||||||
|
|
||||||
|
// Validation should fail because parent doesn't exist
|
||||||
|
assertEquals(false, type.validate(result))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user