Skip to content

Instantly share code, notes, and snippets.

@DRSchlaubi
Last active January 18, 2021 19:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DRSchlaubi/d74f0bb798f0da091cf0f9306b0883dd to your computer and use it in GitHub Desktop.
Save DRSchlaubi/d74f0bb798f0da091cf0f9306b0883dd to your computer and use it in GitHub Desktop.
Small helper class forenvironment variable based configs
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
/**
* Helper class that allows you to specify a [prefix] for your whole config.
*
* Is intended to be used via composition or via inheritance
*/
public open class EnvironmentConfig internal constructor(private val prefix: String) {
/**
* @see getEnv
*/
protected fun getEnv(default: String? = null): EnvironmentVariable<String> =
getEnv(prefix, default)
/**
* @see getEnv
*/
protected fun <T> getEnv(
default: T? = null,
transform: (String) -> T?
): EnvironmentVariable<T> =
getEnv(prefix, default, transform)
}
/**
* Returns a delegated environment variable prefixed by [prefix] that fallbacks to [default] if the found variable is empty or invalid
*/
private fun getEnv(
prefix: String? = null,
default: String? = null
): EnvironmentVariable<String> =
EnvironmentVariable(prefix, { it }, default)
/**
* Returns a delegated environment variable prefixed by [prefix] that fallbacks to [default] if the found variable is empty or invalid.
*
* The variable is transformed to [T] by [transform]
*/
private fun <T> getEnv(
prefix: String? = null,
default: T? = null,
transform: (String) -> T?
): EnvironmentVariable<T> =
EnvironmentVariable(prefix, transform, default)
/**
* Delegated property for a environment variable.
*
* @param prefix the prefix for the variable
* @param transform a transformer to map the value to another type
* @param default an optional default value
*
* @param T the type of the (transformed) variable
*
* @see getEnv
* @see Config
* @see ReadOnlyProperty
*/
@Suppress("LocalVariableName")
public sealed class EnvironmentVariable<T>(
private val prefix: String?,
protected val transform: (String) -> T?,
protected val default: T?,
) : ReadOnlyProperty<Any, T> {
/**
* Computes the name of the variable prefixed by [prefix].
*/
protected val KProperty<*>.prefixedName: String
get() = prefix?.let { it + name } ?: name
/**
* Makes this variable optional.
*
* @return a new [EnvironmentVariable] being optional
*/
public open fun optional(): EnvironmentVariable<T?> = Optional(prefix, transform, default)
/**
* Internal getter.
*/
protected fun <T> getEnv(
property: KProperty<*>,
default: T? = null,
transform: (String) -> T?
): T? = System.getenv(property.prefixedName)?.let(transform) ?: default
private class Required<T>(prefix: String?, transform: (String) -> T?, default: T?) :
EnvironmentVariable<T>(prefix, transform, default) {
@Volatile
private var _value: T? = null
private fun missing(name: String): Nothing = error("Missing env variable: $name")
@Suppress("UNCHECKED_CAST")
override fun getValue(thisRef: Any, property: KProperty<*>): T {
val _v1 = _value
if (_v1 != null) {
return _v1
}
return synchronized(this) {
val _v2 = _value
if (_v2 != null) {
_v2
} else {
val typedValue = getEnv(property, default, transform)
_value = typedValue
typedValue ?: missing(property.prefixedName)
}
}
}
}
private class Optional<T>(prefix: String?, transform: (String) -> T?, default: T?) :
EnvironmentVariable<T?>(prefix, transform, default) {
private object UNINITIALIZED
@Volatile
private var _value: Any? = UNINITIALIZED
override fun optional(): EnvironmentVariable<T?> = this
@Suppress("UNCHECKED_CAST")
override fun getValue(thisRef: Any, property: KProperty<*>): T {
val _v1 = _value
if (_v1 != UNINITIALIZED) {
return _v1 as T
}
return synchronized(this) {
val _v2 = _value
if (_v2 != UNINITIALIZED) {
_v2 as T
} else {
val typedValue = getEnv(property, default, transform)
_value = typedValue
typedValue as T
}
}
}
}
public companion object {
/**
* @see EnvironmentVariable
*/
public operator fun <T> invoke(
prefix: String?,
transform: (String) -> T?,
default: T?,
): EnvironmentVariable<T> = Required(prefix, transform, default)
}
}
object Config {
// => getenv("ENV_VAR")
val ENV_VAR: String by getEnv()
// => getenv("PREFIX_ENV_VAR")
val ENV_VAR2: String? by getEnv("PREFIX_").optional()
val TIMEOUT: Int by getEnv {
it.toIntOrNull() // an error is thrown when the type is invalid and there is no default }
}
val DEFAULTED_TIMEOUT by getEnv(default = 10) { it.toIntOrNull() }
}
object MyConfig : Config("SERVICE_") {
// => getenv("SERVICE_ENV_VAR")
val ENV_VAR: String by getEnv()
val TIMEOUT: Int by getEnv {
it.toIntOrNull() // an error is thrown when the type is invalid and there is no default }
}
val DEFAULTED_TIMEOUT: Int by getEnv(default = 10) { it.toIntOrNull() }
}
object ComposedConfig {
val config = Config("SERVICE_")
// => getenv("SERVICE_ENV_VAR")
val ENV_VAR: String by config.getEnv()
val TIMEOUT: Int by config.getEnv {
it.toIntOrNull() // an error is thrown when the type is invalid and there is no default }
}
val DEFAULTED_TIMEOUT: Int by config.getEnv(default = 10) { it.toIntOrNull() }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment