From 825c4d46637675bd2dc9e207cec6aacde1f89f60 Mon Sep 17 00:00:00 2001 From: Kevin Cianfarini <kevin.cianfarini@octoenergy.com> Date: Wed, 6 Mar 2024 08:34:42 -0500 Subject: [PATCH] Add POSIX environment variable integration (#48) --- .../environment-variable/build.gradle.kts | 26 ++++ .../environment-variable/gradle.properties | 3 + ...EnvironmentVariableFeatureFlagDataStore.kt | 63 +++++++++ .../monarch/environment/environment.kt | 3 + ...ronmentVariableFeatureFlagDataStoreTest.kt | 131 ++++++++++++++++++ .../monarch/environment/environment.jvm.kt | 5 + .../monarch/environment/environment.native.kt | 10 ++ settings.gradle.kts | 1 + 8 files changed, 242 insertions(+) create mode 100644 integrations/environment-variable/build.gradle.kts create mode 100644 integrations/environment-variable/gradle.properties create mode 100644 integrations/environment-variable/src/commonMain/kotlin/io/github/kevincianfarini/monarch/environment/EnvironmentVariableFeatureFlagDataStore.kt create mode 100644 integrations/environment-variable/src/commonMain/kotlin/io/github/kevincianfarini/monarch/environment/environment.kt create mode 100644 integrations/environment-variable/src/commonTest/kotlin/io/github/kevincianfarini/monarch/environment/EnvironmentVariableFeatureFlagDataStoreTest.kt create mode 100644 integrations/environment-variable/src/jvmMain/kotlin/io/github/kevincianfarini/monarch/environment/environment.jvm.kt create mode 100644 integrations/environment-variable/src/nativeMain/kotlin/io/github/kevincianfarini/monarch/environment/environment.native.kt diff --git a/integrations/environment-variable/build.gradle.kts b/integrations/environment-variable/build.gradle.kts new file mode 100644 index 0000000..0f9b629 --- /dev/null +++ b/integrations/environment-variable/build.gradle.kts @@ -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) + } + } +} \ No newline at end of file diff --git a/integrations/environment-variable/gradle.properties b/integrations/environment-variable/gradle.properties new file mode 100644 index 0000000..f54102e --- /dev/null +++ b/integrations/environment-variable/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=environment-variable-integration +POM_NAME=Monarch System Environment Variable Integration +POM_DESCRIPTION=Multiplatform integration with environment variables \ No newline at end of file diff --git a/integrations/environment-variable/src/commonMain/kotlin/io/github/kevincianfarini/monarch/environment/EnvironmentVariableFeatureFlagDataStore.kt b/integrations/environment-variable/src/commonMain/kotlin/io/github/kevincianfarini/monarch/environment/EnvironmentVariableFeatureFlagDataStore.kt new file mode 100644 index 0000000..225181b --- /dev/null +++ b/integrations/environment-variable/src/commonMain/kotlin/io/github/kevincianfarini/monarch/environment/EnvironmentVariableFeatureFlagDataStore.kt @@ -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." + } +} \ No newline at end of file diff --git a/integrations/environment-variable/src/commonMain/kotlin/io/github/kevincianfarini/monarch/environment/environment.kt b/integrations/environment-variable/src/commonMain/kotlin/io/github/kevincianfarini/monarch/environment/environment.kt new file mode 100644 index 0000000..f681d14 --- /dev/null +++ b/integrations/environment-variable/src/commonMain/kotlin/io/github/kevincianfarini/monarch/environment/environment.kt @@ -0,0 +1,3 @@ +package io.github.kevincianfarini.monarch.environment + +internal expect fun getSystemEnvVar(key: String): String? \ No newline at end of file diff --git a/integrations/environment-variable/src/commonTest/kotlin/io/github/kevincianfarini/monarch/environment/EnvironmentVariableFeatureFlagDataStoreTest.kt b/integrations/environment-variable/src/commonTest/kotlin/io/github/kevincianfarini/monarch/environment/EnvironmentVariableFeatureFlagDataStoreTest.kt new file mode 100644 index 0000000..98eeb78 --- /dev/null +++ b/integrations/environment-variable/src/commonTest/kotlin/io/github/kevincianfarini/monarch/environment/EnvironmentVariableFeatureFlagDataStoreTest.kt @@ -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()) + ) + } +} \ No newline at end of file diff --git a/integrations/environment-variable/src/jvmMain/kotlin/io/github/kevincianfarini/monarch/environment/environment.jvm.kt b/integrations/environment-variable/src/jvmMain/kotlin/io/github/kevincianfarini/monarch/environment/environment.jvm.kt new file mode 100644 index 0000000..1f720cf --- /dev/null +++ b/integrations/environment-variable/src/jvmMain/kotlin/io/github/kevincianfarini/monarch/environment/environment.jvm.kt @@ -0,0 +1,5 @@ +package io.github.kevincianfarini.monarch.environment + +internal actual fun getSystemEnvVar(key: String): String? { + return System.getenv(key) +} \ No newline at end of file diff --git a/integrations/environment-variable/src/nativeMain/kotlin/io/github/kevincianfarini/monarch/environment/environment.native.kt b/integrations/environment-variable/src/nativeMain/kotlin/io/github/kevincianfarini/monarch/environment/environment.native.kt new file mode 100644 index 0000000..1f6a125 --- /dev/null +++ b/integrations/environment-variable/src/nativeMain/kotlin/io/github/kevincianfarini/monarch/environment/environment.native.kt @@ -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() +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 1c9be91..b355e88 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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")