Skip to content

Commit 2e4b633

Browse files
committed
Add env-var provider
1 parent 84b9d4e commit 2e4b633

File tree

16 files changed

+1354
-2
lines changed

16 files changed

+1354
-2
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@
88
build
99

1010
# Ignore IntelliJ files
11-
.idea
11+
.idea
12+
13+
# Ignore Kotlin files
14+
.kotlin

gradle/libs.versions.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
[versions]
2+
kotlin = "2.1.21"
23
open-feature-kotlin-sdk = "0.4.1"
34

45
[libraries]
5-
open-feature-kotlin-sdk = { group="dev.openfeature", name="kotlin-sdk", version.ref="open-feature-kotlin-sdk" }
6+
openfeature-kotlin-sdk = { group="dev.openfeature", name="kotlin-sdk", version.ref="open-feature-kotlin-sdk" }
7+
kotlin-test = { group="org.jetbrains.kotlin", name="kotlin-test", version.ref="kotlin" }
8+
9+
[plugins]
10+
kotlin-multiplatform = { id="org.jetbrains.kotlin.multiplatform", version.ref="kotlin" }

kotlin-js-store/yarn.lock

Lines changed: 519 additions & 0 deletions
Large diffs are not rendered by default.

providers/env-var/README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Environment Variables Kotlin Provider
2+
3+
Environment Variables provider allows you to read feature flags from the [process's environment](https://en.wikipedia.org/wiki/Environment_variable).
4+
5+
## Installation
6+
7+
<!-- x-release-please-start-version -->
8+
9+
```xml
10+
<dependency>
11+
<groupId>dev.openfeature.kotlin.contrib.providers</groupId>
12+
<artifactId>env-var</artifactId>
13+
<version>0.1.0</version>
14+
</dependency>
15+
```
16+
17+
<!-- x-release-please-end-version -->
18+
19+
## Usage
20+
21+
To use the `EnvVarProvider` create an instance and use it as a provider:
22+
23+
```kotlin
24+
val provider = EnvVarProvider()
25+
OpenFeatureAPI.setProviderAndWait(provider)
26+
```
27+
28+
### Configuring different methods for fetching environment variables
29+
30+
This provider defines an `EnvironmentGateway` interface, which is used to access the actual environment variables.
31+
The method [`platformSpecificEnvironmentGateway`][platformSpecificEnvironmentGateway], which is implemented for each supported platform, returns a default implementation.
32+
33+
```kotlin
34+
val testFake = EnvironmentGateway { arg -> "true" } // always returns true
35+
36+
val provider = EnvVarProvider(testFake)
37+
OpenFeatureAPI.getInstance().setProvider(provider)
38+
```
39+
40+
### Key transformation
41+
42+
This provider supports transformation of keys to support different patterns used for naming feature flags and for
43+
naming environment variables, e.g. SCREAMING_SNAKE_CASE env variables vs. hyphen-case keys for feature flags.
44+
It supports chaining/combining different transformers incl. self-written ones by providing a transforming function in the constructor.
45+
Currently, the following transformations are supported out of the box:
46+
47+
- converting to lower case (e.g. `Feature.Flag` => `feature.flag`)
48+
- converting to UPPER CASE (e.g. `Feature.Flag` => `FEATURE.FLAG`)
49+
- converting hyphen-case to SCREAMING_SNAKE_CASE (e.g. `Feature-Flag` => `FEATURE_FLAG`)
50+
- convert to camelCase (e.g. `FEATURE_FLAG` => `featureFlag`)
51+
- replace '_' with '.' (e.g. `feature_flag` => `feature.flag`)
52+
- replace '.' with '_' (e.g. `feature.flag` => `feature_flag`)
53+
54+
**Examples:**
55+
56+
1. hyphen-case feature flag names to screaming snake-case environment variables:
57+
58+
```kotlin
59+
// Definition of the EnvVarProvider:
60+
val provider = EnvVarProvider(EnvironmentKeyTransformer.hyphenCaseToScreamingSnake())
61+
```
62+
63+
2. chained/composed transformations:
64+
65+
```kotlin
66+
// Definition of the EnvVarProvider:
67+
val keyTransformer = EnvironmentKeyTransformer
68+
.toLowerCaseTransformer()
69+
.andThen(EnvironmentKeyTransformer.replaceUnderscoreWithDotTransformer())
70+
71+
val provider = EnvVarProvider(keyTransformer)
72+
```
73+
74+
3. freely defined transformation function:
75+
76+
```kotlin
77+
// Definition of the EnvVarProvider:
78+
val keyTransformer = EnvironmentKeyTransformer { key -> key.substring(1) }
79+
val provider = EnvVarProvider(keyTransformer)
80+
```
81+
82+
<!-- links -->
83+
84+
[platformSpecificEnvironmentGateway]: src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/PlatformSpecificEnvironmentGateway.kt

providers/env-var/build.gradle.kts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2+
3+
plugins {
4+
alias(libs.plugins.kotlin.multiplatform)
5+
}
6+
7+
kotlin {
8+
jvm {
9+
compilations.all {
10+
compileTaskProvider.configure {
11+
compilerOptions {
12+
jvmTarget.set(JvmTarget.JVM_21)
13+
}
14+
}
15+
}
16+
}
17+
linuxX64 {}
18+
js {
19+
nodejs()
20+
}
21+
22+
sourceSets {
23+
commonMain.dependencies {
24+
api(libs.openfeature.kotlin.sdk)
25+
}
26+
commonTest.dependencies {
27+
implementation(libs.kotlin.test)
28+
}
29+
}
30+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package dev.openfeature.kotlin.contrib.providers.envvar
2+
3+
/**
4+
* Converts an underscore-separated string to camel case.
5+
*/
6+
internal fun String.camelCase(): String {
7+
// Split by underscores
8+
val words =
9+
split('_')
10+
.filter { it.isNotEmpty() } // Remove empty strings that might result from splitting
11+
12+
if (words.isEmpty()) {
13+
return ""
14+
}
15+
16+
// The first word is converted to lowercase
17+
val firstWord = words.first().lowercase()
18+
19+
// Subsequent words are capitalized (first letter uppercase, rest lowercase)
20+
val restOfWords =
21+
words.drop(1).joinToString("") { word ->
22+
word.lowercase().replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
23+
}
24+
25+
return firstWord + restOfWords
26+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package dev.openfeature.kotlin.contrib.providers.envvar
2+
3+
import dev.openfeature.sdk.EvaluationContext
4+
import dev.openfeature.sdk.FeatureProvider
5+
import dev.openfeature.sdk.Hook
6+
import dev.openfeature.sdk.ProviderEvaluation
7+
import dev.openfeature.sdk.ProviderMetadata
8+
import dev.openfeature.sdk.Reason
9+
import dev.openfeature.sdk.Value
10+
import dev.openfeature.sdk.exceptions.OpenFeatureError
11+
12+
/** EnvVarProvider is the Kotlin provider implementation for the environment variables. */
13+
class EnvVarProvider(
14+
private val environmentGateway: EnvironmentGateway = platformSpecificEnvironmentGateway(),
15+
private val keyTransformer: EnvironmentKeyTransformer = EnvironmentKeyTransformer.doNothing(),
16+
) : FeatureProvider {
17+
override val hooks: List<Hook<*>> = emptyList()
18+
override val metadata: ProviderMetadata
19+
get() = Metadata
20+
21+
override suspend fun initialize(initialContext: EvaluationContext?) {
22+
// Nothing to do here
23+
}
24+
25+
override fun shutdown() {
26+
// Nothing to do here
27+
}
28+
29+
override suspend fun onContextSet(
30+
oldContext: EvaluationContext?,
31+
newContext: EvaluationContext,
32+
) {
33+
// Nothing to do here
34+
}
35+
36+
override fun getBooleanEvaluation(
37+
key: String,
38+
defaultValue: Boolean,
39+
context: EvaluationContext?,
40+
): ProviderEvaluation<Boolean> = evaluateEnvironmentVariable(key, String::toBoolean)
41+
42+
override fun getDoubleEvaluation(
43+
key: String,
44+
defaultValue: Double,
45+
context: EvaluationContext?,
46+
): ProviderEvaluation<Double> = evaluateEnvironmentVariable(key, String::toDouble)
47+
48+
override fun getIntegerEvaluation(
49+
key: String,
50+
defaultValue: Int,
51+
context: EvaluationContext?,
52+
): ProviderEvaluation<Int> = evaluateEnvironmentVariable(key, String::toInt)
53+
54+
override fun getStringEvaluation(
55+
key: String,
56+
defaultValue: String,
57+
context: EvaluationContext?,
58+
): ProviderEvaluation<String> = evaluateEnvironmentVariable(key, { it })
59+
60+
override fun getObjectEvaluation(
61+
key: String,
62+
defaultValue: Value,
63+
context: EvaluationContext?,
64+
): ProviderEvaluation<Value> = throw OpenFeatureError.GeneralError("EnvVarProvider supports only primitives")
65+
66+
private fun <T> evaluateEnvironmentVariable(
67+
key: String,
68+
parse: (String) -> T,
69+
): ProviderEvaluation<T> {
70+
val value: String =
71+
environmentGateway.getEnvironmentVariable(keyTransformer.transformKey(key))
72+
?: throw OpenFeatureError.FlagNotFoundError(key)
73+
74+
try {
75+
return ProviderEvaluation(
76+
value = parse(value),
77+
reason = Reason.STATIC.toString(),
78+
)
79+
} catch (e: Exception) {
80+
throw OpenFeatureError.ParseError(e.message ?: "Unknown parsing error")
81+
}
82+
}
83+
84+
companion object {
85+
private val NAME: String = "Environment Variables Provider"
86+
}
87+
88+
private object Metadata : ProviderMetadata {
89+
override val name: String
90+
get() = NAME
91+
}
92+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package dev.openfeature.kotlin.contrib.providers.envvar
2+
3+
/**
4+
* This is an abstraction to fetch environment variables. It can be used to support
5+
* environment-specific access or provide additional functionality, like prefixes, casing and even
6+
* sources like spring configurations which come from different sources. Also, a test double could
7+
* implement this interface, making the tests independent of the actual environment.
8+
*/
9+
fun interface EnvironmentGateway {
10+
fun getEnvironmentVariable(key: String): String?
11+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package dev.openfeature.kotlin.contrib.providers.envvar
2+
3+
/**
4+
* This class provides a way to transform any given key to another value. This is helpful, if keys in the code have a
5+
* different representation as in the actual environment, e.g. SCREAMING_SNAKE_CASE env vars vs. hyphen-case keys
6+
* for feature flags.
7+
*
8+
*
9+
* This class also supports chaining/combining different transformers incl. self-written ones by providing
10+
* a transforming function in the constructor. <br></br>
11+
* Currently, the following transformations are supported out of the box:
12+
*
13+
* * [converting to lower case][.toLowerCaseTransformer]
14+
* * [converting to UPPER CASE][.toUpperCaseTransformer]
15+
* * [converting hyphen-case to SCREAMING_SNAKE_CASE][.hyphenCaseToScreamingSnake]
16+
* * [convert to camelCase][.toCamelCaseTransformer]
17+
* * [replace &#39;_&#39; with &#39;.&#39;][.replaceUnderscoreWithDotTransformer]
18+
* * [replace &#39;.&#39; with &#39;_&#39;][.replaceDotWithUnderscoreTransformer]
19+
*
20+
*
21+
*
22+
* **Examples:**
23+
*
24+
*
25+
* 1. hyphen-case feature flag names to screaming snake-case environment variables:
26+
* <pre>
27+
* `// Definition of the EnvVarProvider:
28+
* EnvironmentKeyTransformer transformer = EnvironmentKeyTransformer
29+
* .hyphenCaseToScreamingSnake();
30+
*
31+
* FeatureProvider provider = new EnvVarProvider(transformer);
32+
` *
33+
</pre> *
34+
* 2. chained/composed transformations:
35+
* <pre>
36+
* `// Definition of the EnvVarProvider:
37+
* EnvironmentKeyTransformer transformer = EnvironmentKeyTransformer
38+
* .toLowerCaseTransformer()
39+
* .andThen(EnvironmentKeyTransformer.replaceUnderscoreWithDotTransformer());
40+
*
41+
* FeatureProvider provider = new EnvVarProvider(transformer);
42+
` *
43+
</pre> *
44+
* 3. freely defined transformation function:
45+
* <pre>
46+
* `// Definition of the EnvVarProvider:
47+
* EnvironmentKeyTransformer transformer = new EnvironmentKeyTransformer(key -> "constant");
48+
*
49+
* FeatureProvider provider = new EnvVarProvider(keyTransformer);
50+
` *
51+
</pre> *
52+
*/
53+
fun interface EnvironmentKeyTransformer {
54+
fun transformKey(key: String): String
55+
56+
fun andThen(another: EnvironmentKeyTransformer): EnvironmentKeyTransformer =
57+
EnvironmentKeyTransformer { key ->
58+
another.transformKey(
59+
this.transformKey(key),
60+
)
61+
}
62+
63+
companion object {
64+
fun toLowerCaseTransformer(): EnvironmentKeyTransformer = EnvironmentKeyTransformer { key -> key.lowercase() }
65+
66+
fun toUpperCaseTransformer(): EnvironmentKeyTransformer = EnvironmentKeyTransformer { key -> key.uppercase() }
67+
68+
fun toCamelCaseTransformer(): EnvironmentKeyTransformer = EnvironmentKeyTransformer { key -> key.camelCase() }
69+
70+
fun replaceUnderscoreWithDotTransformer(): EnvironmentKeyTransformer = EnvironmentKeyTransformer { key -> key.replace('_', '.') }
71+
72+
fun replaceDotWithUnderscoreTransformer(): EnvironmentKeyTransformer = EnvironmentKeyTransformer { key -> key.replace('.', '_') }
73+
74+
fun hyphenCaseToScreamingSnake(): EnvironmentKeyTransformer =
75+
EnvironmentKeyTransformer { key ->
76+
key.replace('-', '_').uppercase()
77+
}
78+
79+
fun doNothing(): EnvironmentKeyTransformer = EnvironmentKeyTransformer { s -> s }
80+
}
81+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package dev.openfeature.kotlin.contrib.providers.envvar
2+
3+
internal expect fun platformSpecificEnvironmentGateway(): EnvironmentGateway

0 commit comments

Comments
 (0)