When working with services, or command line applications I have taken an approach of environment configuration. I can still remember being a system admin, and if you said environment variable that was heresey.

This changed with containers and zones. The environment variables changed from a global scope to a local scope. Where the local scope is now the container. I'll post next on how I use environment variables.

Old Syntax

data class APIConfig(
    override val ClusterPort: Int,
    override val ClusterManagerHostName: String,
): EnvironmentConfiguration, VertxClusterWithCassandraConfig, VertxWithKafkaConfig

val EnvironmentConfig = loadConfigurationFromEnvironment(PricingEngineAPIConfig::class)

The old way took a data class, which extended an interface. This was an explicit way of noting that the data class was populated by the environment. There are also several other extensions.

interface VertxClusterWithCassandraConfig {
    val CassandraHostName: String
    val CassandraKey: String
    val CassandraUser: String
    val CassandraPassword: String
}

The benefit of this was being able to sub type the config for helper methods.

suspend fun getCassandraConnection(config:VertxClusterWithCassandraConfig) {
   ....
}

The Problem

The primary problem with this was in the loadConfigurationFromEnvironment method. I wanted this to be a multi platform project, and it didn't allow for that. It made heavy use of reflection which is not available on all platforms.

inline fun <reified C : EnvironmentConfiguration> loadConfigurationFromEnvironment(clazz: KClass<C>): C {
    val cons = clazz.primaryConstructor!!
    val params = cons.parameters
    val values = params
        .map {
            try {
                it to when (it.type.javaType) {
                    Int::class.java -> System.getenv(it.name).toInt()
                    Long::class.java -> System.getenv(it.name).toLong()
                    Double::class.java -> System.getenv(it.name).toDouble()
                    String::class.java -> System.getenv(it.name).toString()
                    Boolean::class.java -> System.getenv(it.name)!!.toBoolean()
                    else -> System.getenv(it.name).toString()
                }
            } catch (e: Exception) {
                println("Failed to retrieve ${it.name} from environment, exiting. Required configuration is not present")
                exitProcess(1)
            }
        }
        .toMap()
    return cons.callBy(values)
}

Right off the bat, this is an inline function. So it will be copied through the compiled target. We then pull out the constructor, iterate over the parameters, and set them one by  It works but as noted the biggest problem is the use of reflection.

Delegate Properties

I started trying to migrate this several weeks ago. But I got stuck on the reflection. I went through a laundry list of ideas. None of which panned out, or would sacrifice the type safety I was looking for. One of the initial criteria was pulling a configuration property and ensuring it matches the anticipated type. A port is an Int.

object TestConfig {
    val testVariable: String by Environment("TESTVARIABLE")
    val camelCaseVariable: Int by Environment()
    val magicalBananaMan: String by Environment()
}

fun main() {
  println(TestConfig.testVariable)
}

The new method is an explicit delegate property. If you pass in a string it will look for that variable under that name. If not explicitly set it will default to the property name. It will attempt a cast of the value from the property type.

Switching from a data class to an object means that only a single instance can be present. The delegation also allows for it to be used under other classes.

Building a delegate is a pretty affair. As noted from the upstream documentation two simple methods are needed. They are getValue and setValue, we are only worried about the getValue implementation.

import kotlin.reflect.KProperty

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }
 
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name}' in $thisRef.")
    }
}

Platform Specific

JVM

For the JVM implementation we are using the standard System.getenv.

    operator fun getValue(thisRef: Any?, prop: KProperty<*>): T {
        val raw = (if (varName == null) System.getenv(prop.name) else System.getenv(varName))
            ?: throw EnvironmentVariableNotFound("Environment variable $varName / ${prop.name} could not be found.")
        return when (prop.returnType) {
            ::stringResponse.returnType -> raw
            ::intResponse.returnType -> raw.toInt()
            ::floatResponse.returnType -> raw.toFloat()
            ::doubleResponse.returnType -> raw.toDouble()
            ::bigDecimalResponse.returnType -> raw.toBigDecimal()
            ::longResponse.returnType -> raw.toLong()
            ::bigIntegerResponse.returnType -> raw.toBigInteger()
            else -> raw
        } as T
    }

We first check to see if a custom variable name is being provided. Then if no value can be found an exception is thrown.

Determining the cast type was the hard part, especially with type erasure. We don't want to veer to far outside of the delegate properties, as that is the extent of reflection across most platforms. We do have a property returnType, but that can't easily be stubbed to a ::class call. The easiest way was to build the anticipated returnType from a stub function. This feels sort of ugly but I found it on the official Kotlin discussions board.

This same scaffolding will be used on all platforms.

    private fun stringResponse(): String = TODO()
    private fun intResponse(): Int = TODO()
    private fun floatResponse(): Float = TODO()
    private fun doubleResponse(): Double = TODO()
    private fun bigDecimalResponse(): BigDecimal = TODO()
    private fun longResponse(): Long = TODO()
    private fun bigIntegerResponse(): BigInteger = TODO()

The above gives us the anticipated return types that we can compare off of. With clear set boundaries we can now cast the types properly.

JavaScript

NodeJS retrieves their environment variables via process. The difficulty comes in that we need to stub out the process interface as it's not provided by default. Another thanks to Kotlin Discussions.

external val process: Process

external interface Process {
    val env: Map<String, Any?>
}

The casting function is much easier, because JS has an in-built unsafecast.

    operator fun getValue(thisRef: Any?, prop: KProperty<*>): T {
        return if (varName != null) {
            process.env[varName].unsafeCast<T>()
        } else {
            process.env[prop.name].unsafeCast<T>()
        }
    }
Linux X64
   operator fun getValue(thisRef: Any?, prop: KProperty<*>): T = memScoped {
        val raw = (if (varName == null) getenv(prop.name) else getenv(varName))!!.toKString()
        return when (prop.returnType) {
            ::stringResponse.returnType -> raw
            ....
            else -> raw
        } as T
    }

For Linux X64 we use the posix getenv. This is comes back as a ByteVar CPointer, which we just cast to a KString. Then perform the same match.

Closing

The more I've written software, the more I've realized some times you just need to turn off your brain. This solution feels much simpler than my initial approach. Artifacts will be up shortly.