Skip to content

Commit

Permalink
Add POSIX environment variable integration (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
kevincianfarini authored Mar 6, 2024
1 parent f44af02 commit 825c4d4
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 0 deletions.
26 changes: 26 additions & 0 deletions integrations/environment-variable/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
plugins {
alias(libs.plugins.dokka)
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.publish)
}

kotlin {

explicitApi()

jvm()
linuxArm64()
linuxX64()
macosArm64()
macosX64()
mingwX64()

sourceSets {
commonMain.dependencies {
api(project(":core"))
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
}
}
3 changes: 3 additions & 0 deletions integrations/environment-variable/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
POM_ARTIFACT_ID=environment-variable-integration
POM_NAME=Monarch System Environment Variable Integration
POM_DESCRIPTION=Multiplatform integration with environment variables
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.github.kevincianfarini.monarch.environment

import io.github.kevincianfarini.monarch.FeatureFlagDataStore

/**
* A [FeatureFlagDataStore] implementation that provides values from environment variables.
*/
public class EnvironmentVariableFeatureFlagDataStore internal constructor(
private val strictlyTyped: Boolean = true,
private val getEnvironmentVariable: (String) -> String?,
) : FeatureFlagDataStore {

/**
* Create an [EnvironmentVariableFeatureFlagDataStore] which reads from the environment.
* If [strictlyTyped] is true, this store will throw exceptions when the raw string value
* of the environment variable cannot be coerced to a specific type. Otherwise, this store
* will return the default value.
*/
public constructor(strictlyTyped: Boolean = true) : this(strictlyTyped, ::getSystemEnvVar)

override fun getBoolean(key: String, default: Boolean): Boolean {
val env = getEnvironmentVariable(key)
val boolean = if (strictlyTyped) env?.toBooleanStrict() else env?.toBooleanStrictOrNull()
return boolean ?: default
}

override fun getString(key: String, default: String): String {
return getEnvironmentVariable(key) ?: default
}

override fun getDouble(key: String, default: Double): Double {
val env = getEnvironmentVariable(key)
val double = if (strictlyTyped) env?.toDouble() else env?.toDoubleOrNull()
return double ?: default
}

override fun getLong(key: String, default: Long): Long {
val env = getEnvironmentVariable(key)
val long = if (strictlyTyped) env?.toLong() else env?.toLongOrNull()
return long ?: default
}

override fun getByteArray(key: String, default: ByteArray): ByteArray {
val env = getEnvironmentVariable(key)
val bytes = if (strictlyTyped) env?.decodeHexToByteArray() else env?.decodeHexToByteArrayOrNull()
return bytes ?: default
}
}

private fun String.decodeHexToByteArrayOrNull(): ByteArray? = takeIf { it.length % 2 == 0 }?.let { string ->
ByteArray(string.length / 2) { index ->
val startIndex = index * 2
val endIndex = startIndex + 1
val byteString = string.substring(startIndex, endIndex + 1)
byteString.toByte(16)
}
}

private fun String.decodeHexToByteArray(): ByteArray {
return requireNotNull(decodeHexToByteArrayOrNull()) {
"The input string $this is not a valid hex string."
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.github.kevincianfarini.monarch.environment

internal expect fun getSystemEnvVar(key: String): String?
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package io.github.kevincianfarini.monarch.environment

import kotlin.test.*

class EnvironmentVariableFeatureFlagDataStoreTest {

@Test
fun strictly_typed_boolean_returns_value() {
val store = EnvironmentVariableFeatureFlagDataStore(strictlyTyped = true) { "true" }
assertTrue(store.getBoolean(key = "key", default = false))
}

@Test
fun strictly_typed_boolean_fails() {
val store = EnvironmentVariableFeatureFlagDataStore(strictlyTyped = true) { "tRuE" }
assertFailsWith<IllegalArgumentException> {
store.getBoolean(key = "key", default = false)
}
}

@Test
fun loosely_typed_boolean_returns_default_on_failure() {
val store = EnvironmentVariableFeatureFlagDataStore(strictlyTyped = false) { "tRuE" }
assertFalse(store.getBoolean(key = "key", default = false))
}

@Test
fun no_underlying_value_boolean_returns_default() {
val store = EnvironmentVariableFeatureFlagDataStore(strictlyTyped = false) { null }
assertFalse(store.getBoolean(key = "key", default = false))
}

@Test
fun string_returns_environment_variable() {
val store = EnvironmentVariableFeatureFlagDataStore(strictlyTyped = true) { "some string" }
assertEquals("some string", store.getString(key = "key", default = "default"))
}

@Test
fun string_returns_default_when_no_environment_variable() {
val store = EnvironmentVariableFeatureFlagDataStore(strictlyTyped = true) { null }
assertEquals("default", store.getString(key = "key", default = "default"))
}

@Test
fun strictly_typed_double_returns_value() {
val store = EnvironmentVariableFeatureFlagDataStore(strictlyTyped = true) { "1.2" }
assertEquals(1.2, store.getDouble(key = "key", default = 0.0))
}

@Test
fun strictly_typed_double_fails() {
val store = EnvironmentVariableFeatureFlagDataStore(strictlyTyped = true) { "Not a number" }
assertFailsWith<NumberFormatException> {
store.getDouble(key = "key", default = 0.0)
}
}

@Test
fun loosely_typed_double_returns_default_on_failure() {
val store = EnvironmentVariableFeatureFlagDataStore(strictlyTyped = false) { "Not a number" }
assertEquals(0.0, store.getDouble(key = "key", default = 0.0))
}

@Test
fun no_underlying_value_double_returns_default() {
val store = EnvironmentVariableFeatureFlagDataStore(strictlyTyped = true) { null }
assertEquals(0.0, store.getDouble(key = "key", default = 0.0))
}

@Test
fun strictly_typed_long_returns_value() {
val store = EnvironmentVariableFeatureFlagDataStore(strictlyTyped = true) { "1000" }
assertEquals(1000, store.getLong(key = "key", default = 0))
}

@Test
fun strictly_typed_long_fails() {
val store = EnvironmentVariableFeatureFlagDataStore(strictlyTyped = true) { "Not a number" }
assertFailsWith<NumberFormatException> {
store.getLong(key = "key", default = 0)
}
}

@Test
fun loosely_typed_long_returns_default_on_failure() {
val store = EnvironmentVariableFeatureFlagDataStore(strictlyTyped = false) { "Not a number" }
assertEquals(0, store.getLong(key = "key", default = 0))
}

@Test
fun no_underlying_value_long_returns_default() {
val store = EnvironmentVariableFeatureFlagDataStore(strictlyTyped = true) { null }
assertEquals(0, store.getLong(key = "key", default = 0))
}

@Test
fun strictly_typed_byte_array_returns_value() {
val store = EnvironmentVariableFeatureFlagDataStore(strictlyTyped = true) { "0f00" }
assertContentEquals(
byteArrayOf(0x0f, 0x00),
store.getByteArray(key = "key", default = byteArrayOf())
)
}

@Test
fun strictly_typed_byte_array_fails() {
val store = EnvironmentVariableFeatureFlagDataStore(strictlyTyped = true) { "0f0" }
assertFailsWith<IllegalArgumentException> {
store.getByteArray(key = "key", default = byteArrayOf())
}
}

@Test
fun loosely_typed_byte_array_returns_default_on_failure() {
val store = EnvironmentVariableFeatureFlagDataStore(strictlyTyped = false) { "0f0" }
assertContentEquals(
byteArrayOf(),
store.getByteArray(key = "key", default = byteArrayOf())
)
}

@Test
fun no_underlying_value_byte_array_returns_default() {
val store = EnvironmentVariableFeatureFlagDataStore(strictlyTyped = true) { null }
assertContentEquals(
byteArrayOf(),
store.getByteArray(key = "key", default = byteArrayOf())
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.github.kevincianfarini.monarch.environment

internal actual fun getSystemEnvVar(key: String): String? {
return System.getenv(key)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.github.kevincianfarini.monarch.environment

import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.toKString
import platform.posix.getenv

@OptIn(ExperimentalForeignApi::class)
internal actual fun getSystemEnvVar(key: String): String? {
return getenv(key)?.toKString()
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ rootProject.name = "monarch"
include(":compose")
include(":core")
include(":integrations")
include(":integrations:environment-variable")
include(":integrations:launch-darkly")
include(":mixins")
include(":mixins:kotlinx-serialization-json")
Expand Down

0 comments on commit 825c4d4

Please sign in to comment.