Skip to content

Commit

Permalink
Merge pull request #10 from aPureBase/v2.2.0
Browse files Browse the repository at this point in the history
v2.2.0
  • Loading branch information
jeggy authored Sep 28, 2019
2 parents 173f3d1 + 1153c3c commit bc238d7
Show file tree
Hide file tree
Showing 14 changed files with 242 additions and 68 deletions.
28 changes: 15 additions & 13 deletions arkenv/src/main/kotlin/com/apurebase/arkenv/ArgumentDelegate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<T : Any?> internal constructor(
private val arkenv: Arkenv,
val argument: Argument<T>,
val property: KProperty<*>,
val isBoolean: Boolean,
private val mapping: (String) -> T
) : ReadOnlyProperty<Any?, T> {
val property: KProperty<*>
) : ReadOnlyProperty<Arkenv, T> {

val isBoolean: Boolean = property.returnType.jvmErasure == Boolean::class

@Suppress("UNCHECKED_CAST")
internal var value: T = null as T
Expand All @@ -40,10 +40,10 @@ class ArgumentDelegate<T : Any?> 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
}
Expand All @@ -60,7 +60,7 @@ class ArgumentDelegate<T : Any?> 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)
Expand All @@ -75,7 +75,9 @@ class ArgumentDelegate<T : Any?> 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<String>): T {
Expand All @@ -86,13 +88,13 @@ class ArgumentDelegate<T : Any?> 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()
}
2 changes: 1 addition & 1 deletion arkenv/src/main/kotlin/com/apurebase/arkenv/Arkenv.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 1 addition & 3 deletions arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
38 changes: 10 additions & 28 deletions arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvDelegateLoader.kt
Original file line number Diff line number Diff line change
@@ -1,40 +1,22 @@
package com.apurebase.arkenv

import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KClass
import kotlin.reflect.KProperty

class ArkenvDelegateLoader<T : Any>(
private val argument: Argument<T>,
private val kClass: KClass<T>,
private val arkenv: Arkenv
) {
operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): ReadOnlyProperty<Any?, T> = createDelegate(prop)

private fun createDelegate(prop: KProperty<*>): ArgumentDelegate<T> = 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<Arkenv, T> {
argument.names = getNames(argument.names, prop.name)
return ArgumentDelegate(argument, prop)
.also { arkenv.delegates.add(it) }
}

private fun processNames(names: List<String>) = 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<String>, propName: String) =
names.ifEmpty { listOf("--${propName.toSnakeCase()}") }
.map {
if (!it.startsWith("-")) "--$it".mapRelaxed()
else it.mapRelaxed()
}
}
39 changes: 34 additions & 5 deletions arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvUtil.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.apurebase.arkenv

import com.apurebase.arkenv.feature.ArkenvFeature
import kotlin.reflect.KClass
import kotlin.reflect.jvm.jvmName

/**
Expand All @@ -23,22 +24,22 @@ fun <T : Arkenv> T.parse(args: Array<String>) = apply { parseArguments(args) }
* but the last supplied argument
* @param configuration optional configuration of the argument's properties
*/
inline fun <reified T : Any> Arkenv.argument(
inline fun <T : Any> Arkenv.argument(
names: List<String>,
isMainArg: Boolean = false,
configuration: Argument<T>.() -> Unit = {}
): ArkenvDelegateLoader<T> {
val argument = Argument<T>(names).apply(configuration)
argument.isMainArg = isMainArg
return ArkenvDelegateLoader(argument, T::class, this)
return ArkenvDelegateLoader(argument, this)
}

/**
* Defines an argument that can be parsed.
* @param names the names that the argument can be called with
* @param configuration optional configuration of the argument's properties
*/
inline fun <reified T : Any> Arkenv.argument(
inline fun <T : Any> Arkenv.argument(
vararg names: String,
configuration: Argument<T>.() -> Unit = {}
): ArkenvDelegateLoader<T> = argument(names.toList(), false, configuration)
Expand All @@ -49,10 +50,10 @@ inline fun <reified T : Any> 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 <reified T : Any> Arkenv.mainArgument(block: Argument<T>.() -> Unit = {}): ArkenvDelegateLoader<T> =
inline fun <T : Any> Arkenv.mainArgument(block: Argument<T>.() -> Unit = {}): ArkenvDelegateLoader<T> =
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
Expand All @@ -78,3 +79,31 @@ fun Arkenv.putAll(from: Map<out String, String>) = 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 <T> mapDefault(key: String, value: String, clazz: KClass<*>): T = try {
with(value) {
when (clazz) {
Int::class -> toIntOrNull()
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()
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 UnsupportedMappingException(key, clazz)
} as T
}
} catch (ex: RuntimeException) {
throw MappingException(key, value, clazz, ex)
}
2 changes: 2 additions & 0 deletions arkenv/src/main/kotlin/com/apurebase/arkenv/StringUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Original file line number Diff line number Diff line change
@@ -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.
*/
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
)
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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) }
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
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

/**
* 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,
* 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<String> = listOf(),
parsers: Collection<PropertyParser> = listOf()
) : ArkenvFeature, Arkenv("ProfileFeature") {

constructor(
prefix: String = "application",
locations: Collection<String> = listOf(),
parsers: Collection<PropertyParser> = listOf()
) : this("--arkenv-profile", prefix, locations, parsers)

private val parsers: MutableList<PropertyParser> = mutableListOf(::PropertyFeature)

init {
Expand All @@ -28,15 +37,13 @@ class ProfileFeature(

internal val profiles: List<String> by argument(name) {
defaultValue = ::emptyList
mapping = { it.split(',') }
}

private val prefix: String by argument("--arkenv-profile-prefix") {
defaultValue = { prefix }
}

private val location: Collection<String> by argument("--arkenv-profile-location") {
mapping = { it.split(",").map(String::trim) }
defaultValue = { locations }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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<String> by argument("--arkenv-property-location") {
defaultValue = ::emptyList
}

override fun onLoad(arkenv: Arkenv) {
Expand All @@ -42,7 +40,7 @@ open class PropertyFeature(
protected open fun parse(stream: InputStream): Map<String, String> = parseProperties(stream)

private fun getStream(name: String): InputStream? {
locations
(extraLocations + defaultLocations)
.map { fixLocation(it) + name }
.forEach {
val stream = getFileStream(it) ?: getResourceStream(it)
Expand All @@ -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

Expand Down
Loading

0 comments on commit bc238d7

Please sign in to comment.