Skip to content

Commit

Permalink
Merge pull request #15 from aPureBase/v3.0.0
Browse files Browse the repository at this point in the history
V3.0.0
  • Loading branch information
jeggy authored Oct 21, 2019
2 parents b30e88e + 45e9875 commit 01d129c
Show file tree
Hide file tree
Showing 38 changed files with 358 additions and 300 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@

# 3.0.0
* #5 The profile feature is now installed by default
* #17 Enable access to the active profiles

# 2.2.0
* #6 Add default mappings for primitive arrays, List<String>, and Collection<String>
* #1 Fix encryption api only works on java < 9

# 2.1.0
> Published 2019-09-02
Expand Down
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ Type-safe Kotlin configuration parser `by` delegates.
Supports the most common external configuration sources, including:
* Command line
* Environment Variables
* Properties & Spring-like profiles
* Dot env (.env) files
* Properties, Yaml & Spring-like profiles
* `.env` files


### 📦 Installation
Add jcenter to your repositories. Then you can add Arkenv in Gradle:
Add jcenter to your repositories and add Arkenv in Gradle:

```groovy
repositories { jcenter() }
Expand Down Expand Up @@ -51,14 +51,15 @@ class Arguments : Arkenv() {
```
If you don't specify any names for the argument, it will use the property's name.

In the case of `country`, you can parse it like this:
* From command line with `--country world`
* As an environment variable `COUNTRY=world`
In the case of `nullInt`, you can parse it like this:
* From command line with `--null-int world`
* As an environment variable `NULL_INT=world`

By default, Arkenv supports parsing command line arguments and environment variables.
Read more about other features and their configuration [here](https://apurebase.gitlab.io/arkenv/features/features/).
By default, Arkenv supports parsing command line arguments,
environment variables, and profiles.

You can also find more examples and guides in [the documentation](https://apurebase.gitlab.io/arkenv/guides/guides/)
To get started, we recommend reading about [the basics](https://apurebase.gitlab.io/arkenv/guides/the-basics)
for a quick tour of what's included.


### 📃 Documentation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,21 @@ class YamlFeature(

override val extensions: List<String> = listOf("yml", "yaml")

private val map = mutableMapOf<String, String>()

override fun parse(stream: InputStream): Map<String, String> {
Yaml().load<Map<String, Any?>>(stream)?.map { (key, value) -> parse(key, value) }
return getAll()
return map
}

override fun finally(arkenv: Arkenv) = clear()
override fun finally(arkenv: Arkenv) = map.clear()

@Suppress("UNCHECKED_CAST")
private fun parse(key: String, value: Any?) {
when (value) {
is Map<*, *> -> (value as? Map<String, Any?>)?.forEach { (k, v) -> parse("${key}_$k", v) }
is ArrayList<*> -> this[key] = value.joinToString(",")
else -> this[key] = value.toString()
is ArrayList<*> -> map[key] = value.joinToString(",")
else -> map[key] = value.toString()
}
}
}
18 changes: 0 additions & 18 deletions arkenv/src/main/kotlin/com/apurebase/arkenv/Argument.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,6 @@ class Argument<T : Any?>(var names: List<String>) {
*/
var mapping: ((String) -> T)? = null

/**
* Whether this [Argument] should consider environment variables when parsing
*/
@Deprecated(DEPRECATED_GENERAL)
var withEnv: Boolean = true

/**
* A prefix that is applied to the environment variable names when parsing
*/
@Deprecated(DEPRECATED_USE_FEATURE)
var envPrefix: String? = null

/**
* A custom name for the environment variable parsing
*/
@Deprecated(DEPRECATED_GENERAL)
var envVariable: String? = null

/**
* Whether this [Argument] is a main argument
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,10 @@ class ArgumentDelegate<T : Any?> internal constructor(

private fun checkNullable(arkenv: Arkenv, property: KProperty<*>) {
val valuesAreNull = value == null && defaultValue == null
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)")
}
if (valuesAreNull && !isHelp(arkenv) && !property.returnType.isMarkedNullable) throw MissingArgumentException(
property.name,
info = if (argument.isMainArg) "Main argument" else argument.names.joinToString()
)
}

private fun isHelp(arkenv: Arkenv): Boolean = argument.isHelp || arkenv.isHelp()
Expand Down
22 changes: 4 additions & 18 deletions arkenv/src/main/kotlin/com/apurebase/arkenv/Arkenv.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ abstract class Arkenv(
internal val configuration: ArkenvBuilder = ArkenvBuilder()
) {

@Deprecated(DEPRECATED_GENERAL)
constructor(programName: String = "Arkenv", configuration: (ArkenvBuilder.() -> Unit))
: this(programName, configureArkenv(configuration))

internal val argList = mutableListOf<String>()
private val keyValue = mutableMapOf<String, String>()
internal val delegates = mutableListOf<ArgumentDelegate<*>>()
Expand All @@ -30,7 +26,6 @@ abstract class Arkenv(
internal fun parseArguments(args: Array<out String>) {
if (configuration.clearInputBeforeParse) clear()
argList.addAll(args)
onParse(args)
configuration.features.forEach { it.onLoad(this) }
process()
parse()
Expand All @@ -46,14 +41,6 @@ abstract class Arkenv(
keyValue.clear()
}

@Deprecated(DEPRECATED_USE_FEATURE)
open fun onParse(args: Array<out String>) {
}

@Deprecated(DEPRECATED_USE_FEATURE)
open fun onParseArgument(name: String, argument: Argument<*>, value: Any?) {
}

override fun toString(): String = StringBuilder().apply {
val indent = " "
val doubleIndent = indent + indent
Expand Down Expand Up @@ -94,8 +81,8 @@ abstract class Arkenv(
* @return The value for the [key] or null if not found
*/
fun getOrNull(key: String): String? {
val result = keyValue[key.toSnakeCase()]
return result ?: EnvironmentVariableFeature.getEnv(key, false)
val formattedKey = key.toSnakeCase()
return keyValue[formattedKey] ?: findFeature<EnvironmentVariableFeature>()?.getEnv(formattedKey, false)
}

/**
Expand All @@ -108,7 +95,7 @@ abstract class Arkenv(
.mapNotNull { it.onParse(this, delegate) }
.map { processValue("", it) }
return if (onParseValues.isNotEmpty()) onParseValues
else names.mapNotNull(::getOrNull)
else names.filterNot(String::isSimpleName).mapNotNull(::getOrNull)
}

private fun parse() = delegates
Expand All @@ -118,7 +105,6 @@ abstract class Arkenv(
feature.configure(it.argument)
}
it.reset()
val value = it.getValue(this, it.property)
onParseArgument(it.property.name, it.argument, value)
it.getValue(this, it.property)
}
}
26 changes: 19 additions & 7 deletions arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvBuilder.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package com.apurebase.arkenv

import com.apurebase.arkenv.feature.ArkenvFeature
import com.apurebase.arkenv.feature.EnvironmentVariableFeature
import com.apurebase.arkenv.feature.PlaceholderParser
import com.apurebase.arkenv.feature.ProcessorFeature
import com.apurebase.arkenv.feature.*
import com.apurebase.arkenv.feature.cli.CliFeature

/**
* [Arkenv] configuration builder which controls features and other settings.
* @param installAdvancedFeatures whether to install the profile and placeholder feature.
*/
class ArkenvBuilder {
class ArkenvBuilder(installAdvancedFeatures: Boolean = true) {

/**
* Whether data should be cleared before parsing.
Expand All @@ -26,9 +24,18 @@ class ArkenvBuilder {

/**
* Installs the [feature] into [Arkenv].
* If the feature is already installed, it will be replaced, retaining its order.
* @param feature the feature to install.
*/
fun install(feature: ArkenvFeature) {
features.add(feature)
var index: Int? = null
features.forEachIndexed { i, arkenvFeature ->
if (arkenvFeature.key == feature.key) index = i
}
index?.let { i ->
uninstall(feature)
features.add(i, feature)
} ?: features.add(feature)
}

/**
Expand All @@ -45,6 +52,7 @@ class ArkenvBuilder {

/**
* Uninstalls the [feature] from [Arkenv] if installed.
* @param feature the feature to uninstall.
*/
fun uninstall(feature: ArkenvFeature) {
features.removeIf { feature.key == it.key }
Expand All @@ -53,11 +61,15 @@ class ArkenvBuilder {
init {
install(CliFeature())
install(EnvironmentVariableFeature())
install(PlaceholderParser())
if (installAdvancedFeatures) {
install(ProfileFeature())
install(PlaceholderParser())
}
}
}

/**
* Configure [Arkenv] settings.
* @param block Arkenv configuration logic.
*/
inline fun configureArkenv(block: (ArkenvBuilder.() -> Unit)) = ArkenvBuilder().apply(block)
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,17 @@ internal class UnsupportedMappingException(key: String, clazz: KClass<*>) : Arke
internal class MappingException(key: String, value: String, clazz: KClass<*>, cause: Exception) : ArkenvException(
"Could not parse property '$key' with value '$value' as class '$clazz'", cause
)

/**
* Unchecked exception thrown when no value can be found for the given name.
*/
internal class MissingArgumentException(name: String, info: String): ArkenvException(
"No value passed for property $name ($info)"
)

/**
* Unchecked exception thrown when the requested feature could not be found.
*/
internal class FeatureNotFoundException(featureName: String?) : ArkenvException(
"Feature $featureName could not be found. Make sure it was installed correctly."
)
7 changes: 5 additions & 2 deletions arkenv/src/main/kotlin/com/apurebase/arkenv/ArkenvUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ internal inline fun <reified T : ArkenvFeature> Arkenv.findFeature(): T? {
return configuration.features.find { it is T } as T?
}

internal inline fun <reified T : ArkenvFeature> Arkenv.getFeature(): T =
findFeature() ?: throw FeatureNotFoundException(T::class.simpleName)

/**
* Inserts all key-value pairs in [from] to [Arkenv], overwriting already existing keys.
* Applies all [ProcessorFeature]s to the value.
Expand All @@ -75,10 +78,10 @@ fun Arkenv.putAll(from: Map<out String, String>) = from.forEach { (k, v) -> set(
* All parsed but not declared arguments are available.
* @param key the non-case-sensitive name of the argument
* @return The value for the [key]
* @throws IllegalArgumentException when the key can not be found
* @throws MissingArgumentException when the key can not be found
*/
operator fun Arkenv.get(key: String): String =
getOrNull(key) ?: throw IllegalArgumentException("Arkenv does not contain a value for key '$key'")
getOrNull(key) ?: throw MissingArgumentException("Arkenv does not contain a value for key '$key'", "")

/**
* Maps the input [value] to an instance of [T] using [clazz] as a reference.
Expand Down
3 changes: 0 additions & 3 deletions arkenv/src/main/kotlin/com/apurebase/arkenv/StringUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,3 @@ internal fun String.mapRelaxed(): String =
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
Expand Up @@ -26,33 +26,20 @@ class EnvironmentVariableFeature(

override fun onParse(arkenv: Arkenv, delegate: ArgumentDelegate<*>): String? = with(delegate) {
val envSecrets = enableEnvSecrets || arkenv.getOrNull("ARKENV_ENV_SECRETS") != null
if (argument.withEnv) {
val setEnvPrefix = argument.envPrefix ?: envPrefix ?: arkenv.getOrNull("ARKENV_ENV_PREFIX") ?: ""
getEnvValue(argument, envSecrets, setEnvPrefix)
} else null
val setEnvPrefix = envPrefix ?: arkenv.getOrNull("ARKENV_ENV_PREFIX") ?: ""
getEnvValue(argument, envSecrets, setEnvPrefix)
}

@Deprecated(DEPRECATED_GENERAL)
override fun configure(argument: Argument<*>) {
argument.withEnv = true
argument.envPrefix = envPrefix
}

private fun getEnvValue(argument: Argument<*>, enableEnvSecrets: Boolean, prefix: String): String? {
// If an envVariable is defined we'll pick this as highest order value
argument.envVariable?.let {
val definedEnvValue = getEnv(it, enableEnvSecrets)
if (!definedEnvValue.isNullOrEmpty()) return definedEnvValue
}

// Loop over all argument names and pick the first one that matches
return argument.names
/**
* Loop over all argument names and pick the first one that matches
*/
private fun getEnvValue(argument: Argument<*>, enableEnvSecrets: Boolean, prefix: String): String? =
argument.names
.asSequence()
.filter(String::isAdvancedName)
.map { parseArgumentName(it, prefix) }
.mapNotNull { getEnv(it, enableEnvSecrets) }
.firstOrNull()
}

private fun parseArgumentName(name: String, prefix: String): String =
prefix.toSnakeCase().ensureEndsWith('_') + name.toSnakeCase()
Expand All @@ -65,22 +52,20 @@ class EnvironmentVariableFeature(
.inputStream()
.use(PropertyFeature.Companion::parseProperties)

companion object {
internal fun getEnv(name: String, enableEnvSecrets: Boolean): String? =
System.getenv(name)
?: getEnvSecret(name, enableEnvSecrets)
?: getKebabCase(name)
?: getCamelCase(name)
internal fun getEnv(name: String, enableEnvSecrets: Boolean): String? =
System.getenv(name)
?: getEnvSecret(name, enableEnvSecrets)
?: getKebabCase(name)
?: getCamelCase(name)

private fun getKebabCase(name: String) = System.getenv(name.replace('_', '-').toLowerCase())
private fun getKebabCase(name: String) = System.getenv(name.replace('_', '-').toLowerCase())

private fun getCamelCase(name: String): String? = System.getenv(
name.toLowerCase().split('_').joinToString("", transform = String::capitalize).decapitalize()
)
private fun getCamelCase(name: String): String? = System.getenv(
name.toLowerCase().split('_').joinToString("", transform = String::capitalize).decapitalize()
)

private fun getEnvSecret(lookup: String, enableEnvSecrets: Boolean): String? = when {
enableEnvSecrets -> System.getenv("${lookup}_FILE")?.let(::File)?.readText()
else -> null
}
private fun getEnvSecret(lookup: String, enableEnvSecrets: Boolean): String? = when {
enableEnvSecrets -> System.getenv("${lookup}_FILE")?.let(::File)?.readText()
else -> null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.apurebase.arkenv.feature

import com.apurebase.arkenv.ArgumentDelegate
import com.apurebase.arkenv.Arkenv
import com.apurebase.arkenv.feature.EnvironmentVariableFeature.Companion.getEnv
import com.apurebase.arkenv.MissingArgumentException
import com.apurebase.arkenv.toSnakeCase

/**
Expand Down Expand Up @@ -44,9 +44,8 @@ internal class PlaceholderParser : ProcessorFeature {
private fun findPlaceholderReplacement(placeholder: String): String =
findReplacementInDelegates(arkenv.delegates, placeholder)
?: arkenv.getOrNull(placeholder)
?: getEnv(placeholder, false)
?: findReplacementInArgs(arkenv.argList, placeholder)
?: throw IllegalArgumentException("Cannot find value for placeholder $placeholder")
?: throw MissingArgumentException(placeholder, "Cannot find value for placeholder")

private fun findReplacementInDelegates(delegates: Collection<ArgumentDelegate<*>>, placeholder: String): String? =
delegates
Expand Down
Loading

0 comments on commit 01d129c

Please sign in to comment.