ajalt / clikt

Multiplatform command line interface parsing for Kotlin
https://ajalt.github.io/clikt/
Apache License 2.0
2.51k stars 121 forks source link

Other ways to construct a CliktCommand object #493

Closed aoli-al closed 6 months ago

aoli-al commented 6 months ago

Hi, thanks for maintaining such a great package!

I have a small Configuration class is used across my application. I coupled this with ClicktCommand so that I can construct my Configuration object right from user commands.

class Configuration: CliktCommand() {
    val clazz by argument()
    val report by argument()
    val targetArgs by option("-a", "--args", help = "Arguments passed to target application").default("")
    val algorithm by option().groupChoice(
        "algo1" to Algo1(),
        "algo2" to Algo2()
    )
}

However, this becomes painful while writing unit tests. In order to construct a Configuration object, the only way is to call parse manually and pass strings. This is not ideal because I can't get many type information while writing this code. One potential solution is to also add setters for argument() and option() as well so that I can just create a val config = Configuration() and then config them manually in my unit test config.algorithm = Algo1(). Another solution is to use composition instead of inheritance. I saw several discussion here #371 and #453. It seems that this scenario is not discussed.

ajalt commented 6 months ago

Another solution is to use composition instead of inheritance.

What exactly would that solution look like?

aoli-al commented 6 months ago

Similar to this https://github.com/ajalt/clikt/issues/453#issuecomment-1718994678

If I can create a data object

data class Configuration(val @Option("clazz", type=String...) clazz: String, val: report: String, val targetArgs: AlgoOption) { // if we can configure the options through argument annotations. 
    init(args: CliKt) { // If CliKt can generate this constructor automatically...
        init(args.clazz, args.report, args.algo)
    }
}

and construct the data class through clikt in my main program

val config: Configuration = parser.parse(args)

Then I can just construct my Configuration object in unit tests freely.

val config = Configuration("", ...)
ajalt commented 6 months ago

Thanks for the example. There are lots of annotation based CLI libraries out there, and I touched on the disadvantages of them in the docs. They require reflection and code generation, they're less type safe, and are harder to customize than Clikt. But feel free to use one if you prefer; Clikt will never be an annotation based library.

It seems that the folks who have issues with Clikt's inheritance are trying to use a CliktCommand as their data model. Clikt is UI, and like all UI, coupling it to your data layer can make it harder to test. You probably wouldn't define your data model in HTML fields on DOM nodes, or on properties on an Android View.

It's easy to make a plain data class from a command:

fun MyCommand.toModel() = MyModel(
    foo=foo,
    bar=bar,
    //...
)

val model = MyCommand().apply{ main(argv) }.toModel()
aoli-al commented 6 months ago

I see, Thanks for your explanation. It makes sense to me.