From 5c56161b636e1911c99e4144975f3b35d1c96f81 Mon Sep 17 00:00:00 2001 From: AJ Alt Date: Sat, 20 Jul 2024 08:43:26 -0700 Subject: [PATCH] Add `associate` overloads (#533) --- CHANGELOG.md | 1 + .../ajalt/clikt/parameters/OptionTest.kt | 22 ++++++++++ clikt/api/clikt.api | 6 +++ .../clikt/parameters/options/TransformAll.kt | 41 ++++++++++++++++++- 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e187088..b2ed91a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Added `Context.exitProcess` which you can use to prevent the process from exiting during tests. - Added core module that supports watchOS, tvOS, and wasmWasi targets and has no dependencies. - Added more options to `CliktCommand.test` to control the terminal interactivity. ([#517](https://github.com/ajalt/clikt/pull/517)) +- Added `associate{}`, `associateBy{}`, and `associateWith{}' transforms for options that allow you to convert the keys and values of the map. ([#529](https://github.com/ajalt/clikt/pull/529)) ### Changed - In a subcommand with `argument().multiple()`, the behavior is now the same regardless of the value of `allowMultipleSubcommands`: if a token matches a subcommand name, it's now treated as a subcommand rather than a positional argument. diff --git a/clikt-mordant/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/OptionTest.kt b/clikt-mordant/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/OptionTest.kt index 4de265fb..48aa87b0 100644 --- a/clikt-mordant/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/OptionTest.kt +++ b/clikt-mordant/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/OptionTest.kt @@ -919,6 +919,28 @@ class OptionTest { C().parse(argv) } + @Test + @JsName("associate_conversion_options") + fun `associate conversion options`() = forAll( + row("", emptyMap(), emptyMap(), emptyMap()), + row( + "-Xa=1 -Yb=2 -Zc=3", mapOf("A" to 1), mapOf("B" to "2"), mapOf("c" to 3), + ) + ) { argv, ex, ey, ez -> + class C : TestCommand() { + val x by option("-X").associate { (k, v) -> k.uppercase() to v.toInt() } + val y by option("-Y").associateBy { it.uppercase() } + val z by option("-Z").associateWith { it.toInt() } + override fun run_() { + x shouldBe ex + y shouldBe ey + z shouldBe ez + } + } + C().parse(argv) + } + + @Test @JsName("customized_splitPair") fun `customized splitPair`() = forAll( diff --git a/clikt/api/clikt.api b/clikt/api/clikt.api index ceee6b3c..5e969665 100644 --- a/clikt/api/clikt.api +++ b/clikt/api/clikt.api @@ -1130,7 +1130,13 @@ public final class com/github/ajalt/clikt/parameters/options/OptionWithValues$De public final class com/github/ajalt/clikt/parameters/options/OptionWithValuesKt { public static final fun associate (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Ljava/lang/String;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues; + public static final fun associate (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues; public static synthetic fun associate$default (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Ljava/lang/String;ILjava/lang/Object;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues; + public static synthetic fun associate$default (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues; + public static final fun associateBy (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues; + public static synthetic fun associateBy$default (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues; + public static final fun associateWith (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues; + public static synthetic fun associateWith$default (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues; public static final fun convert (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Ljava/lang/String;Lcom/github/ajalt/clikt/completion/CompletionCandidates;Lkotlin/jvm/functions/Function2;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues; public static final fun convert (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Lkotlin/jvm/functions/Function1;Lcom/github/ajalt/clikt/completion/CompletionCandidates;Lkotlin/jvm/functions/Function2;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues; public static synthetic fun convert$default (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Ljava/lang/String;Lcom/github/ajalt/clikt/completion/CompletionCandidates;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues; diff --git a/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/options/TransformAll.kt b/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/options/TransformAll.kt index 6614dfec..1442c4ba 100644 --- a/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/options/TransformAll.kt +++ b/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/options/TransformAll.kt @@ -144,7 +144,8 @@ fun NullableOption.multiple( * val opt2: Set by option().int().split(",").default(emptyList()).unique() * ``` */ -fun OptionWithValues, EachT, ValueT>.unique(): OptionWithValues, EachT, ValueT> { +fun OptionWithValues, EachT, ValueT>.unique() +: OptionWithValues, EachT, ValueT> { return copy(transformValue, transformEach, { transformAll(it).toSet() }, defaultValidator()) } @@ -153,7 +154,8 @@ fun OptionWithValues, EachT, ValueT>.unique(): Option * * If the same key appears more than once, the last one will be added to the map. */ -fun OptionWithValues>, EachT, ValueT>.toMap(): OptionWithValues, EachT, ValueT> { +fun OptionWithValues>, EachT, ValueT>.toMap() +: OptionWithValues, EachT, ValueT> { return copy(transformValue, transformEach, { transformAll(it).toMap() }, defaultValidator()) } @@ -165,3 +167,38 @@ fun OptionWithValues>, EachT, ValueT>.toMa fun RawOption.associate(delimiter: String = "="): OptionWithValues, Pair, Pair> { return splitPair(delimiter).multiple().toMap() } + +/** + * Change this option to take multiple values, each split on a [delimiter] and converted with the + * [transform] function and converted to a map. + * + * This is shorthand for [splitPair], [convert], [multiple], and [toMap]. + */ +inline fun RawOption.associate( + delimiter: String = "=", + crossinline transform: (Pair) -> Pair, +): OptionWithValues, Pair, Pair> { + return splitPair(delimiter).convert { transform(it) }.multiple().toMap() +} + +/** + * Change this option to take multiple values, each split on a [delimiter], its first value + * converted with the [keySelector], and converted to a map. + */ +inline fun RawOption.associateBy( + delimiter: String = "=", + crossinline keySelector: (String) -> K, +): OptionWithValues, Pair, Pair> { + return associate(delimiter) { keySelector(it.first) to it.second } +} + +/** + * Change this option to take multiple values, each split on a [delimiter], its second value + * converted with the [valueSelector], and converted to a map. + */ +inline fun RawOption.associateWith( + delimiter: String = "=", + crossinline valueSelector: (String) -> V, +): OptionWithValues, Pair, Pair> { + return associate(delimiter) { it.first to valueSelector(it.second) } +}