From 82cb03d87f48deca8866a2554a3d88e609cbd5ba Mon Sep 17 00:00:00 2001 From: AndreasVolkmann Date: Sun, 15 Sep 2019 18:40:18 +0200 Subject: [PATCH 1/6] Replace deprecated base64 api in Encryption --- .../main/kotlin/com/apurebase/arkenv/feature/Encryption.kt | 6 +++--- .../kotlin/com/apurebase/arkenv/feature/EncryptionTest.kt | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/arkenv/src/main/kotlin/com/apurebase/arkenv/feature/Encryption.kt b/arkenv/src/main/kotlin/com/apurebase/arkenv/feature/Encryption.kt index 4be73c6..c319205 100644 --- a/arkenv/src/main/kotlin/com/apurebase/arkenv/feature/Encryption.kt +++ b/arkenv/src/main/kotlin/com/apurebase/arkenv/feature/Encryption.kt @@ -1,8 +1,8 @@ package com.apurebase.arkenv.feature import com.apurebase.arkenv.Arkenv +import java.util.* import javax.crypto.Cipher -import javax.xml.bind.DatatypeConverter /** * Supports decryption of encrypted values during the processing phase. @@ -29,8 +29,8 @@ class Encryption(private val cipher: Cipher) : ProcessorFeature { } } - private fun Cipher.decrypt(input: String): String = DatatypeConverter - .parseHexBinary(input) + private fun Cipher.decrypt(input: String): String = Base64.getDecoder() + .decode(input) .let(::doFinal) .let { String(it) } } diff --git a/arkenv/src/test/kotlin/com/apurebase/arkenv/feature/EncryptionTest.kt b/arkenv/src/test/kotlin/com/apurebase/arkenv/feature/EncryptionTest.kt index 5fbc626..69262ca 100644 --- a/arkenv/src/test/kotlin/com/apurebase/arkenv/feature/EncryptionTest.kt +++ b/arkenv/src/test/kotlin/com/apurebase/arkenv/feature/EncryptionTest.kt @@ -10,8 +10,8 @@ import strikt.assertions.isEqualTo import java.security.Key import java.security.KeyPair import java.security.KeyPairGenerator +import java.util.* import javax.crypto.Cipher -import javax.xml.bind.DatatypeConverter internal class EncryptionTest { @@ -50,6 +50,6 @@ internal class EncryptionTest { fun encrypt(input: String): String = encryptCipher .doFinal(input.toByteArray()) - .let { DatatypeConverter.printHexBinary(it) } + .let(Base64.getEncoder()::encodeToString) } } From 533a5f24a57452ce430c3e3f8dbc719b8ec978af Mon Sep 17 00:00:00 2001 From: AndreasVolkmann Date: Tue, 17 Sep 2019 20:26:22 +0200 Subject: [PATCH 2/6] Refactor mapping, add primitive array mappings --- .../com/apurebase/arkenv/ArgumentDelegate.kt | 28 ++++--- .../kotlin/com/apurebase/arkenv/Arkenv.kt | 2 +- .../com/apurebase/arkenv/ArkenvBuilder.kt | 4 +- .../apurebase/arkenv/ArkenvDelegateLoader.kt | 38 +++------ .../kotlin/com/apurebase/arkenv/ArkenvUtil.kt | 37 +++++++-- .../com/apurebase/arkenv/StringUtils.kt | 2 + .../apurebase/arkenv/ValidationException.kt | 2 +- .../com/apurebase/arkenv/MappingTests.kt | 81 +++++++++++++++++++ .../com/apurebase/arkenv/test/TestUtil.kt | 2 +- 9 files changed, 144 insertions(+), 52 deletions(-) create mode 100644 arkenv/src/test/kotlin/com/apurebase/arkenv/MappingTests.kt diff --git a/arkenv/src/main/kotlin/com/apurebase/arkenv/ArgumentDelegate.kt b/arkenv/src/main/kotlin/com/apurebase/arkenv/ArgumentDelegate.kt index 307638e..2ce82a2 100644 --- a/arkenv/src/main/kotlin/com/apurebase/arkenv/ArgumentDelegate.kt +++ b/arkenv/src/main/kotlin/com/apurebase/arkenv/ArgumentDelegate.kt @@ -3,17 +3,17 @@ package com.apurebase.arkenv import com.apurebase.arkenv.Argument.Validation import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty +import kotlin.reflect.jvm.jvmErasure /** * Delegate class for parsing arguments. */ class ArgumentDelegate internal constructor( - private val arkenv: Arkenv, val argument: Argument, - val property: KProperty<*>, - val isBoolean: Boolean, - private val mapping: (String) -> T -) : ReadOnlyProperty { + val property: KProperty<*> +) : ReadOnlyProperty { + + val isBoolean: Boolean = property.returnType.jvmErasure == Boolean::class @Suppress("UNCHECKED_CAST") internal var value: T = null as T @@ -40,10 +40,10 @@ class ArgumentDelegate internal constructor( else -> throw IllegalStateException("Attempted to set value to true but ${property.name} is not boolean") } - override operator fun getValue(thisRef: Any?, property: KProperty<*>): T { + override operator fun getValue(thisRef: Arkenv, property: KProperty<*>): T { if (!isSet) { - value = setValue(property) - checkNullable(property) + value = setValue(thisRef, property) + checkNullable(thisRef, property) if (value != null) checkValidation(argument.validation, value, property) isSet = true } @@ -60,7 +60,7 @@ class ArgumentDelegate internal constructor( } @Suppress("UNCHECKED_CAST") - private fun setValue(property: KProperty<*>): T { + private fun setValue(arkenv: Arkenv, property: KProperty<*>): T { val values = arkenv.parseDelegate(this, argument.names) return when { isBoolean -> mapBoolean(values) @@ -75,7 +75,9 @@ class ArgumentDelegate internal constructor( return map(input) } - private fun map(value: String): T = mapping(value) + private fun map(value: String): T = + argument.mapping?.invoke(value) + ?: mapDefault(property.name, value, property.returnType.jvmErasure) @Suppress("UNCHECKED_CAST") private fun mapBoolean(values: Collection): T { @@ -86,13 +88,13 @@ class ArgumentDelegate internal constructor( } as T } - private fun checkNullable(property: KProperty<*>) { + private fun checkNullable(arkenv: Arkenv, property: KProperty<*>) { val valuesAreNull = value == null && defaultValue == null - if (valuesAreNull && !isHelp() && !property.returnType.isMarkedNullable) { + if (valuesAreNull && !isHelp(arkenv) && !property.returnType.isMarkedNullable) { val nameInfo = if (argument.isMainArg) "Main argument" else argument.names.joinToString() throw IllegalArgumentException("No value passed for property ${property.name} ($nameInfo)") } } - private fun isHelp(): Boolean = argument.isHelp || arkenv.isHelp() + private fun isHelp(arkenv: Arkenv): Boolean = argument.isHelp || arkenv.isHelp() } diff --git a/arkenv/src/main/kotlin/com/apurebase/arkenv/Arkenv.kt b/arkenv/src/main/kotlin/com/apurebase/arkenv/Arkenv.kt index a952535..9ceab42 100644 --- a/arkenv/src/main/kotlin/com/apurebase/arkenv/Arkenv.kt +++ b/arkenv/src/main/kotlin/com/apurebase/arkenv/Arkenv.kt @@ -67,7 +67,7 @@ abstract class Arkenv( .append(doubleIndent) .append(delegate.property.name) .append(doubleIndent) - .append(delegate.getValue(this, delegate.property)) + .append(delegate.getValue(this@Arkenv, delegate.property)) .appendln() } }.toString() diff --git a/arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvBuilder.kt b/arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvBuilder.kt index ce56285..3599b09 100644 --- a/arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvBuilder.kt +++ b/arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvBuilder.kt @@ -47,9 +47,7 @@ class ArkenvBuilder { * Uninstalls the [feature] from [Arkenv] if installed. */ fun uninstall(feature: ArkenvFeature) { - features.removeIf { - feature.getKeyValPair().first == it.getKeyValPair().first - } + features.removeIf { feature.key == it.key } } init { diff --git a/arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvDelegateLoader.kt b/arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvDelegateLoader.kt index 02f644f..8780e06 100644 --- a/arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvDelegateLoader.kt +++ b/arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvDelegateLoader.kt @@ -1,40 +1,22 @@ package com.apurebase.arkenv import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.KClass import kotlin.reflect.KProperty class ArkenvDelegateLoader( private val argument: Argument, - private val kClass: KClass, private val arkenv: Arkenv ) { - operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): ReadOnlyProperty = createDelegate(prop) - - private fun createDelegate(prop: KProperty<*>): ArgumentDelegate = with(argument) { - names = (if (names.isEmpty()) listOf("--${prop.name.toSnakeCase()}") else names).let(::processNames) - - return ArgumentDelegate( - arkenv, - argument, - prop, - kClass == Boolean::class, - mapping ?: getMapping(prop) - ).also { arkenv.delegates.add(it) } + operator fun provideDelegate(thisRef: Arkenv, prop: KProperty<*>): ReadOnlyProperty { + argument.names = getNames(argument.names, prop.name) + return ArgumentDelegate(argument, prop) + .also { arkenv.delegates.add(it) } } - private fun processNames(names: List) = names.map { - if (!it.startsWith("-")) "--$it".mapRelaxed() - else it.mapRelaxed() - } - - @Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_ANY") - private fun getMapping(prop: KProperty<*>): (String) -> T = { value -> - when (kClass) { - Int::class -> value.toIntOrNull() - Long::class -> value.toLongOrNull() - String::class -> value - else -> throw IllegalArgumentException("${prop.name} ($kClass) is not supported") - } as T - } + private fun getNames(names: List, propName: String) = + names.ifEmpty { listOf("--${propName.toSnakeCase()}") } + .map { + if (!it.startsWith("-")) "--$it".mapRelaxed() + else it.mapRelaxed() + } } diff --git a/arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvUtil.kt b/arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvUtil.kt index 0cdff94..59c0d36 100644 --- a/arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvUtil.kt +++ b/arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvUtil.kt @@ -1,6 +1,7 @@ package com.apurebase.arkenv import com.apurebase.arkenv.feature.ArkenvFeature +import kotlin.reflect.KClass import kotlin.reflect.jvm.jvmName /** @@ -23,14 +24,14 @@ fun T.parse(args: Array) = apply { parseArguments(args) } * but the last supplied argument * @param configuration optional configuration of the argument's properties */ -inline fun Arkenv.argument( +inline fun Arkenv.argument( names: List, isMainArg: Boolean = false, configuration: Argument.() -> Unit = {} ): ArkenvDelegateLoader { val argument = Argument(names).apply(configuration) argument.isMainArg = isMainArg - return ArkenvDelegateLoader(argument, T::class, this) + return ArkenvDelegateLoader(argument, this) } /** @@ -38,7 +39,7 @@ inline fun Arkenv.argument( * @param names the names that the argument can be called with * @param configuration optional configuration of the argument's properties */ -inline fun Arkenv.argument( +inline fun Arkenv.argument( vararg names: String, configuration: Argument.() -> Unit = {} ): ArkenvDelegateLoader = argument(names.toList(), false, configuration) @@ -49,10 +50,10 @@ inline fun Arkenv.argument( * The main argument can't be passed through environment variables. * @param block the configuration that will be applied to the Argument */ -inline fun Arkenv.mainArgument(block: Argument.() -> Unit = {}): ArkenvDelegateLoader = +inline fun Arkenv.mainArgument(block: Argument.() -> Unit = {}): ArkenvDelegateLoader = argument(listOf(), true, block) -internal fun ArkenvFeature.getKeyValPair() = this::class.jvmName to this +internal val ArkenvFeature.key get() = this::class.jvmName internal fun Arkenv.isHelp(): Boolean = when { argList.isEmpty() && !delegates.first { it.argument.isHelp }.isSet -> false @@ -78,3 +79,29 @@ fun Arkenv.putAll(from: Map) = from.forEach { (k, v) -> set( */ operator fun Arkenv.get(key: String): String = getOrNull(key) ?: throw IllegalArgumentException("Arkenv does not contain a value for key '$key'") + +/** + * Maps the input [value] to an instance of [T] using [clazz] as a reference. + * @throws IllegalArgumentException if the mapping is not supported or didn't succeed + */ +@Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_ANY", "ComplexMethod", "LongMethod", "TooGenericExceptionCaught") +internal fun mapDefault(key: String, value: String, clazz: KClass<*>): T = try { + with(value) { + when (clazz) { + Int::class -> toIntOrNull() + Long::class -> toLongOrNull() + String::class -> value + IntArray::class -> split().map(String::toInt).toIntArray() + ShortArray::class -> split().map(String::toShort).toShortArray() + CharArray::class -> toCharArray() + LongArray::class -> split().map(String::toLong).toLongArray() + FloatArray::class -> split().map(String::toFloat).toFloatArray() + DoubleArray::class -> split().map(String::toDouble).toDoubleArray() + BooleanArray::class -> split().map(String::toBoolean).toBooleanArray() + ByteArray::class -> split().map(String::toByte).toByteArray() + else -> throw IllegalArgumentException("$key ($clazz) is not supported. Define a custom mapping.") + } as T + } +} catch (ex: RuntimeException) { + throw IllegalArgumentException("Could not parse $key - $value as $clazz", ex) +} diff --git a/arkenv/src/main/kotlin/com/apurebase/arkenv/StringUtils.kt b/arkenv/src/main/kotlin/com/apurebase/arkenv/StringUtils.kt index 7af8f98..c45fb9a 100644 --- a/arkenv/src/main/kotlin/com/apurebase/arkenv/StringUtils.kt +++ b/arkenv/src/main/kotlin/com/apurebase/arkenv/StringUtils.kt @@ -34,5 +34,7 @@ internal fun String.mapRelaxed(): String = if (isAdvancedName()) "--" + toSnakeCase() else this +internal fun String.split() = split(',') + internal const val DEPRECATED_GENERAL = "Will be removed in future major version" internal const val DEPRECATED_USE_FEATURE = "$DEPRECATED_GENERAL. Use Features instead." diff --git a/arkenv/src/main/kotlin/com/apurebase/arkenv/ValidationException.kt b/arkenv/src/main/kotlin/com/apurebase/arkenv/ValidationException.kt index 3a3e305..f3a19c6 100644 --- a/arkenv/src/main/kotlin/com/apurebase/arkenv/ValidationException.kt +++ b/arkenv/src/main/kotlin/com/apurebase/arkenv/ValidationException.kt @@ -5,6 +5,6 @@ import kotlin.reflect.KProperty /** * Unchecked exception thrown when validation of an [Argument] was unsuccessful. */ -class ValidationException(property: KProperty<*>, value: Any?, message: String) : IllegalArgumentException( +internal class ValidationException(property: KProperty<*>, value: Any?, message: String) : IllegalArgumentException( "Argument ${property.name} with value '$value' did not pass validation: '$message'" ) diff --git a/arkenv/src/test/kotlin/com/apurebase/arkenv/MappingTests.kt b/arkenv/src/test/kotlin/com/apurebase/arkenv/MappingTests.kt new file mode 100644 index 0000000..759af16 --- /dev/null +++ b/arkenv/src/test/kotlin/com/apurebase/arkenv/MappingTests.kt @@ -0,0 +1,81 @@ +package com.apurebase.arkenv + +import com.apurebase.arkenv.test.expectThat +import com.apurebase.arkenv.test.parse +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import strikt.assertions.isEqualTo + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class MappingTests { + + @Test fun `IntArray should map`() { + object : Arkenv() { + val array: IntArray by argument() + }.parse(argName, numericInput).array.expectThat { + isEqualTo(numericOutput.toIntArray()) + } + } + + @Test fun `CharArray should map`() { + object : Arkenv() { + val array: CharArray by argument() + }.parse(argName, "abc").array.expectThat { + isEqualTo(listOf('a', 'b', 'c').toCharArray()) + } + } + + @Test fun `ShortArray should map`() { + object : Arkenv() { + val array: ShortArray by argument() + }.parse(argName, numericInput).array.expectThat { + isEqualTo(numericOutput.map(Int::toShort).toShortArray()) + } + } + + @Test fun `LongArray should map`() { + object : Arkenv() { + val array: LongArray by argument() + }.parse(argName, numericInput).array.expectThat { + isEqualTo(numericOutput.map(Int::toLong).toLongArray()) + } + } + + @Test fun `FloatArray should map`() { + object : Arkenv() { + val array: FloatArray by argument() + }.parse(argName, floatingPointInput).array.expectThat { + isEqualTo(floatingPointOutput.map(Double::toFloat).toFloatArray()) + } + } + + @Test fun `DoubleArray should map`() { + object : Arkenv() { + val array: DoubleArray by argument() + }.parse(argName, floatingPointInput).array.expectThat { + isEqualTo(floatingPointOutput.toDoubleArray()) + } + } + + @Test fun `BooleanArray should map`() { + object : Arkenv() { + val array: BooleanArray by argument() + }.parse(argName, "True,False,true").array.expectThat { + isEqualTo(listOf(true, false, true).toBooleanArray()) + } + } + + @Test fun `ByteArray should map`() { + object : Arkenv() { + val array: ByteArray by argument() + }.parse("--$argName", "-1,2,3").array.expectThat { + isEqualTo(listOf(-1, 2, 3).map(Int::toByte).toByteArray()) + } + } + + private val floatingPointInput = "1.1,29.92,-387.9999" + private val floatingPointOutput = listOf(1.1, 29.92, -387.9999) + private val numericInput = "1,-29,387" + private val numericOutput = listOf(1, -29, 387) + private val argName = "ARRAY" +} diff --git a/arkenv/src/test/kotlin/com/apurebase/arkenv/test/TestUtil.kt b/arkenv/src/test/kotlin/com/apurebase/arkenv/test/TestUtil.kt index 2d64a8c..e3f8364 100644 --- a/arkenv/src/test/kotlin/com/apurebase/arkenv/test/TestUtil.kt +++ b/arkenv/src/test/kotlin/com/apurebase/arkenv/test/TestUtil.kt @@ -10,6 +10,6 @@ fun getTestResourcePath(name: String): String = File("src/test/resources/$name") fun T.parse(vararg arguments: String) = apply { parseArguments(arguments) } -fun getTestResource(name: String) = MockSystem::class.java.classLoader.getResource(name).readText() +fun getTestResource(name: String) = MockSystem::class.java.classLoader.getResource(name)!!.readText() const val DEPRECATED = "Property files are no longer required, can be null." From 624e520167eeb593ff013cbe2eeb12448dff7811 Mon Sep 17 00:00:00 2001 From: AndreasVolkmann Date: Thu, 19 Sep 2019 17:59:57 +0200 Subject: [PATCH 3/6] Added Char mapping and mapping docs --- .../kotlin/com/apurebase/arkenv/ArkenvUtil.kt | 1 + .../com/apurebase/arkenv/MappingTests.kt | 8 +++++ docs/features/mapping.md | 32 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 docs/features/mapping.md diff --git a/arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvUtil.kt b/arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvUtil.kt index 59c0d36..abd45a5 100644 --- a/arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvUtil.kt +++ b/arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvUtil.kt @@ -91,6 +91,7 @@ internal fun mapDefault(key: String, value: String, clazz: KClass<*>): T = t Int::class -> toIntOrNull() Long::class -> toLongOrNull() String::class -> value + Char::class -> firstOrNull() IntArray::class -> split().map(String::toInt).toIntArray() ShortArray::class -> split().map(String::toShort).toShortArray() CharArray::class -> toCharArray() diff --git a/arkenv/src/test/kotlin/com/apurebase/arkenv/MappingTests.kt b/arkenv/src/test/kotlin/com/apurebase/arkenv/MappingTests.kt index 759af16..8e09017 100644 --- a/arkenv/src/test/kotlin/com/apurebase/arkenv/MappingTests.kt +++ b/arkenv/src/test/kotlin/com/apurebase/arkenv/MappingTests.kt @@ -9,6 +9,14 @@ import strikt.assertions.isEqualTo @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class MappingTests { + @Test fun `char should map`() { + object : Arkenv() { + val char: Char by argument() + }.parse("CHAR", "OnlyTheFirstChar").char.expectThat { + isEqualTo('O') + } + } + @Test fun `IntArray should map`() { object : Arkenv() { val array: IntArray by argument() diff --git a/docs/features/mapping.md b/docs/features/mapping.md new file mode 100644 index 0000000..867fe1f --- /dev/null +++ b/docs/features/mapping.md @@ -0,0 +1,32 @@ +--- +layout: default +title: Mapping +parent: Features +nav_order: 13 +--- + +# Mapping + +The following mappings are supported by default: + +```kotlin +object Ark : Arkenv() { + val int: Int by argument() + val long: Long by argument() + val string: String by argument() + val char: Char by argument() + val intArray: IntArray by argument() + val shortArray: ShortArray by argument() + val charArray: CharArray by argument() + val longArray: LongArray by argument() + val floatArray: FloatArray by argument() + val doubleArray: DoubleArray by argument() + val booleanArray: BooleanArray by argument() + val byteArray: ByteArray by argument() +} +``` + +All array types can be defined as a comma-separated string. +The only exception being `CharArray`, which simply takes each input +character as a single array item. + From e2c88ac5bf72bd86ae71a303e07ebfaad23d5b6c Mon Sep 17 00:00:00 2001 From: AndreasVolkmann Date: Thu, 19 Sep 2019 18:48:50 +0200 Subject: [PATCH 4/6] Add String Collection mapping --- .../kotlin/com/apurebase/arkenv/ArkenvUtil.kt | 5 +-- .../apurebase/arkenv/ValidationException.kt | 19 ++++++++++- .../arkenv/feature/ProfileFeature.kt | 2 -- .../arkenv/feature/PropertyFeature.kt | 12 +++---- .../com/apurebase/arkenv/MappingTests.kt | 12 +++++++ docs/features/mapping.md | 32 +++++++++++-------- 6 files changed, 57 insertions(+), 25 deletions(-) diff --git a/arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvUtil.kt b/arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvUtil.kt index abd45a5..5201421 100644 --- a/arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvUtil.kt +++ b/arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvUtil.kt @@ -92,6 +92,7 @@ internal fun mapDefault(key: String, value: String, clazz: KClass<*>): T = t Long::class -> toLongOrNull() String::class -> value Char::class -> firstOrNull() + List::class, Collection::class -> split() IntArray::class -> split().map(String::toInt).toIntArray() ShortArray::class -> split().map(String::toShort).toShortArray() CharArray::class -> toCharArray() @@ -100,9 +101,9 @@ internal fun mapDefault(key: String, value: String, clazz: KClass<*>): T = t DoubleArray::class -> split().map(String::toDouble).toDoubleArray() BooleanArray::class -> split().map(String::toBoolean).toBooleanArray() ByteArray::class -> split().map(String::toByte).toByteArray() - else -> throw IllegalArgumentException("$key ($clazz) is not supported. Define a custom mapping.") + else -> throw UnsupportedMappingException(key, clazz) } as T } } catch (ex: RuntimeException) { - throw IllegalArgumentException("Could not parse $key - $value as $clazz", ex) + throw MappingException(key, value, clazz, ex) } diff --git a/arkenv/src/main/kotlin/com/apurebase/arkenv/ValidationException.kt b/arkenv/src/main/kotlin/com/apurebase/arkenv/ValidationException.kt index f3a19c6..6404f11 100644 --- a/arkenv/src/main/kotlin/com/apurebase/arkenv/ValidationException.kt +++ b/arkenv/src/main/kotlin/com/apurebase/arkenv/ValidationException.kt @@ -1,10 +1,27 @@ package com.apurebase.arkenv +import kotlin.reflect.KClass import kotlin.reflect.KProperty +internal open class ArkenvException(message: String, cause: Exception? = null) : RuntimeException(message, cause) + /** * Unchecked exception thrown when validation of an [Argument] was unsuccessful. */ -internal class ValidationException(property: KProperty<*>, value: Any?, message: String) : IllegalArgumentException( +internal class ValidationException(property: KProperty<*>, value: Any?, message: String) : ArkenvException( "Argument ${property.name} with value '$value' did not pass validation: '$message'" ) + +/** + * Unchecked exception thrown when no supported mapping exists for the given class. + */ +internal class UnsupportedMappingException(key: String, clazz: KClass<*>) : ArkenvException( + "Property '$key' of type '$clazz' is not supported. Define a custom mapping." +) + +/** + * Unchecked exception thrown when mapping was unsuccessful. + */ +internal class MappingException(key: String, value: String, clazz: KClass<*>, cause: Exception) : ArkenvException( + "Could not parse property '$key' with value '$value' as class '$clazz'", cause +) diff --git a/arkenv/src/main/kotlin/com/apurebase/arkenv/feature/ProfileFeature.kt b/arkenv/src/main/kotlin/com/apurebase/arkenv/feature/ProfileFeature.kt index 409d39f..106b50f 100644 --- a/arkenv/src/main/kotlin/com/apurebase/arkenv/feature/ProfileFeature.kt +++ b/arkenv/src/main/kotlin/com/apurebase/arkenv/feature/ProfileFeature.kt @@ -28,7 +28,6 @@ class ProfileFeature( internal val profiles: List by argument(name) { defaultValue = ::emptyList - mapping = { it.split(',') } } private val prefix: String by argument("--arkenv-profile-prefix") { @@ -36,7 +35,6 @@ class ProfileFeature( } private val location: Collection by argument("--arkenv-profile-location") { - mapping = { it.split(",").map(String::trim) } defaultValue = { locations } } diff --git a/arkenv/src/main/kotlin/com/apurebase/arkenv/feature/PropertyFeature.kt b/arkenv/src/main/kotlin/com/apurebase/arkenv/feature/PropertyFeature.kt index e015a37..e51a4d3 100644 --- a/arkenv/src/main/kotlin/com/apurebase/arkenv/feature/PropertyFeature.kt +++ b/arkenv/src/main/kotlin/com/apurebase/arkenv/feature/PropertyFeature.kt @@ -17,11 +17,9 @@ open class PropertyFeature( ) : ArkenvFeature, Arkenv("PropertyFeature") { protected open val extensions = listOf("properties") - private val defaultLocations = listOf("", "config/") - private val locations: Collection by argument("--arkenv-property-location") { - val combined = locations + defaultLocations - mapping = { it.split(',') + combined } - defaultValue = { combined } + private val defaultLocations = locations + listOf("", "config/") + private val extraLocations: List by argument("--arkenv-property-location") { + defaultValue = ::emptyList } override fun onLoad(arkenv: Arkenv) { @@ -42,7 +40,7 @@ open class PropertyFeature( protected open fun parse(stream: InputStream): Map = parseProperties(stream) private fun getStream(name: String): InputStream? { - locations + (extraLocations + defaultLocations) .map { fixLocation(it) + name } .forEach { val stream = getFileStream(it) ?: getResourceStream(it) @@ -51,7 +49,7 @@ open class PropertyFeature( return null } - private fun fixLocation(location: String) = // TODO test this + private fun fixLocation(location: String) = if (location.isNotBlank() && !location.endsWith('/')) "$location/" else location diff --git a/arkenv/src/test/kotlin/com/apurebase/arkenv/MappingTests.kt b/arkenv/src/test/kotlin/com/apurebase/arkenv/MappingTests.kt index 8e09017..7f02018 100644 --- a/arkenv/src/test/kotlin/com/apurebase/arkenv/MappingTests.kt +++ b/arkenv/src/test/kotlin/com/apurebase/arkenv/MappingTests.kt @@ -9,6 +9,18 @@ import strikt.assertions.isEqualTo @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class MappingTests { + @Test fun `list should map to List of String`() { + val input = "by,default" + val output = listOf("by", "default") + object: Arkenv() { + val list: List by argument() + val collection: Collection by argument() + }.parse("LIST", input, "COLLECTION", input).expectThat { + get { list }.isEqualTo(output) + get { collection }.isEqualTo(output) + } + } + @Test fun `char should map`() { object : Arkenv() { val char: Char by argument() diff --git a/docs/features/mapping.md b/docs/features/mapping.md index 867fe1f..8e8ecc1 100644 --- a/docs/features/mapping.md +++ b/docs/features/mapping.md @@ -11,22 +11,28 @@ The following mappings are supported by default: ```kotlin object Ark : Arkenv() { - val int: Int by argument() - val long: Long by argument() - val string: String by argument() - val char: Char by argument() - val intArray: IntArray by argument() - val shortArray: ShortArray by argument() - val charArray: CharArray by argument() - val longArray: LongArray by argument() - val floatArray: FloatArray by argument() - val doubleArray: DoubleArray by argument() - val booleanArray: BooleanArray by argument() - val byteArray: ByteArray by argument() + val int: Int by argument() + val long: Long by argument() + val string: String by argument() + val char: Char by argument() + val intArray: IntArray by argument() + val shortArray: ShortArray by argument() + val charArray: CharArray by argument() + val longArray: LongArray by argument() + val floatArray: FloatArray by argument() + val doubleArray: DoubleArray by argument() + val booleanArray: BooleanArray by argument() + val byteArray: ByteArray by argument() + + val stringList: List by argument() + val stringCollection: Collection by argument() } ``` -All array types can be defined as a comma-separated string. +All primitive array types can be defined as a comma-separated string. The only exception being `CharArray`, which simply takes each input character as a single array item. +For `List` and `Collection` the type parameter is assumed to be `String` +and will only work in this case. + From 52999ed67ac21fd2c9a33d004289b31ccdecaac1 Mon Sep 17 00:00:00 2001 From: AndreasVolkmann Date: Thu, 26 Sep 2019 18:37:20 +0200 Subject: [PATCH 5/6] Deprecate ProfilFeature constructor with name --- .../com/apurebase/arkenv/feature/ProfileFeature.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/arkenv/src/main/kotlin/com/apurebase/arkenv/feature/ProfileFeature.kt b/arkenv/src/main/kotlin/com/apurebase/arkenv/feature/ProfileFeature.kt index 106b50f..7fbd008 100644 --- a/arkenv/src/main/kotlin/com/apurebase/arkenv/feature/ProfileFeature.kt +++ b/arkenv/src/main/kotlin/com/apurebase/arkenv/feature/ProfileFeature.kt @@ -1,6 +1,7 @@ package com.apurebase.arkenv.feature import com.apurebase.arkenv.Arkenv +import com.apurebase.arkenv.DEPRECATED_GENERAL import com.apurebase.arkenv.argument import com.apurebase.arkenv.parse @@ -13,13 +14,21 @@ import com.apurebase.arkenv.parse * can be set via *ARKENV_PROFILE_LOCATION* * @param parsers additional providers for profile file parsing. By default supports the property format. */ -class ProfileFeature( +class ProfileFeature +@Deprecated(DEPRECATED_GENERAL) +constructor( name: String = "--arkenv-profile", prefix: String = "application", locations: Collection = listOf(), parsers: Collection = listOf() ) : ArkenvFeature, Arkenv("ProfileFeature") { + constructor( + prefix: String = "application", + locations: Collection = listOf(), + parsers: Collection = listOf() + ) : this("--arkenv-profile", prefix, locations, parsers) + private val parsers: MutableList = mutableListOf(::PropertyFeature) init { From 4f6ba2327b1d42560745119a055e39ded41f0d26 Mon Sep 17 00:00:00 2001 From: AndreasVolkmann Date: Thu, 26 Sep 2019 18:41:05 +0200 Subject: [PATCH 6/6] Fix ProfileFeature docs --- .../main/kotlin/com/apurebase/arkenv/feature/ProfileFeature.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arkenv/src/main/kotlin/com/apurebase/arkenv/feature/ProfileFeature.kt b/arkenv/src/main/kotlin/com/apurebase/arkenv/feature/ProfileFeature.kt index 7fbd008..72ff314 100644 --- a/arkenv/src/main/kotlin/com/apurebase/arkenv/feature/ProfileFeature.kt +++ b/arkenv/src/main/kotlin/com/apurebase/arkenv/feature/ProfileFeature.kt @@ -7,7 +7,7 @@ import com.apurebase.arkenv.parse /** * Feature for loading profile-based configuration. - * A list of active profiles can be configured via a custom [name] or the *ARKENV_PROFILE* argument. + * A list of active profiles can be configured via the *ARKENV_PROFILE* argument. * @param name overrides the default name of the profile argument, can be set via *ARKENV_PROFILE* * @param prefix the default prefix for any profile configuration files, can be set via *ARKENV_PROFILE_PREFIX* * @param locations defines the default list of locations in which to look for profile configuration files,