Skip to content

Commit 8d80135

Browse files
lopitzbeeme1mr
andauthored
feat: provide key transformations (#288)
Signed-off-by: Lars Opitz <[email protected]> Co-authored-by: Michael Beemer <[email protected]>
1 parent ad5127e commit 8d80135

File tree

6 files changed

+362
-10
lines changed

6 files changed

+362
-10
lines changed

providers/env-var/README.md

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,70 @@ Environment Variables provider allows you to read feature flags from the [proces
1818

1919
## Usage
2020

21-
`EnvVarProvider` doesn't have any options.
22-
Just create an instance and use it as a provider:
21+
To use the `EnvVarProvider` create an instance and use it as a provider:
2322

2423
```java
25-
EnvVarProvider provider = new EnvVarProvider();
26-
OpenFeatureAPI.getInstance().setProvider(provider);
24+
EnvVarProvider provider = new EnvVarProvider();
25+
OpenFeatureAPI.getInstance().setProvider(provider);
2726
```
27+
28+
### Configuring different methods of fetching environment variables
29+
30+
This provider defines an `EnvironmentGateway` interface, which is used to access the actual environment variables.
31+
The class [OS][os-class] is implementing this interface and creates the actual connection between provider
32+
and environment variables. For testing or in case accessing the environment variables is more complex than supported
33+
by Java's `java.lang.System` class, the implementation can be switched accordingly by passing it into the constructor
34+
of the provider.
35+
36+
```java
37+
EnvironmentGateway testFake = arg -> "true"; //always returns true
38+
39+
EnvVarProvider provider = new EnvVarProvider(testFake);
40+
OpenFeatureAPI.getInstance().setProvider(provider);
41+
```
42+
43+
### Key Transformation
44+
45+
This provider supports transformation of keys to support different patterns used for naming feature flags and for
46+
naming environment variables, e.g. SCREAMING_SNAKE_CASE env variables vs. hyphen-case keys for feature flags.
47+
It supports chaining/combining different transformers incl. self-written ones by providing a transforming function in the constructor.
48+
Currently, the following transformations are supported out of the box:
49+
50+
- converting to lower case (e.g. `Feature.Flag` => `feature.flag`)
51+
- converting to UPPER CASE (e.g. `Feature.Flag` => `FEATURE.FLAG`)
52+
- converting hyphen-case to SCREAMING_SNAKE_CASE (e.g. `Feature-Flag` => `FEATURE_FLAG`)
53+
- convert to camelCase (e.g. `FEATURE_FLAG` => `featureFlag`)
54+
- replace '_' with '.' (e.g. `feature_flag` => `feature.flag`)
55+
- replace '.' with '_' (e.g. `feature.flag` => `feature_flag`)
56+
57+
**Examples:**
58+
59+
1. hyphen-case feature flag names to screaming snake-case environment variables:
60+
61+
```java
62+
// Definition of the EnvVarProvider:
63+
FeatureProvider provider = new EnvVarProvider(EnvironmentKeyTransformer.hyphenCaseToScreamingSnake());
64+
```
65+
66+
2. chained/composed transformations:
67+
68+
```java
69+
// Definition of the EnvVarProvider:
70+
EnvironmentKeyTransformer keyTransformer = EnvironmentKeyTransformer
71+
.toLowerCaseTransformer()
72+
.andThen(EnvironmentKeyTransformer.replaceUnderscoreWithDotTransformer());
73+
74+
FeatureProvider provider = new EnvVarProvider(keyTransformer);
75+
```
76+
77+
3. freely defined transformation function:
78+
79+
```java
80+
// Definition of the EnvVarProvider:
81+
EnvironmentKeyTransformer keyTransformer = new EnvironmentKeyTransformer(key -> key.substring(1));
82+
FeatureProvider provider = new EnvVarProvider(keyTransformer);
83+
```
84+
85+
<!-- links -->
86+
87+
[os-class]: src/main/java/dev/openfeature/contrib/providers/envvar/OS.java

providers/env-var/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,12 @@
2121
<name>Siarhei Krukau</name>
2222
</developer>
2323
</developers>
24+
25+
<dependencies>
26+
<dependency>
27+
<groupId>org.apache.commons</groupId>
28+
<artifactId>commons-lang3</artifactId>
29+
<version>3.12.0</version>
30+
</dependency>
31+
</dependencies>
2432
</project>

providers/env-var/src/main/java/dev/openfeature/contrib/providers/envvar/EnvVarProvider.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,23 @@ public final class EnvVarProvider implements FeatureProvider {
1414
private static final String NAME = "Environment Variables Provider";
1515

1616
private final EnvironmentGateway environmentGateway;
17+
private final EnvironmentKeyTransformer keyTransformer;
1718

1819
public EnvVarProvider() {
19-
this.environmentGateway = new OS();
20+
this(new OS(), EnvironmentKeyTransformer.doNothing());
2021
}
2122

2223
public EnvVarProvider(EnvironmentGateway environmentGateway) {
24+
this(environmentGateway, EnvironmentKeyTransformer.doNothing());
25+
}
26+
27+
public EnvVarProvider(EnvironmentKeyTransformer keyTransformer) {
28+
this(new OS(), keyTransformer);
29+
}
30+
31+
public EnvVarProvider(EnvironmentGateway environmentGateway, EnvironmentKeyTransformer keyTransformer) {
2332
this.environmentGateway = environmentGateway;
33+
this.keyTransformer = keyTransformer;
2434
}
2535

2636
@Override
@@ -57,7 +67,7 @@ private <T> ProviderEvaluation<T> evaluateEnvironmentVariable(
5767
String key,
5868
Function<String, T> parse
5969
) {
60-
final String value = environmentGateway.getEnvironmentVariable(key);
70+
final String value = environmentGateway.getEnvironmentVariable(keyTransformer.transformKey(key));
6171

6272
if (value == null) {
6373
throw new FlagNotFoundError();
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package dev.openfeature.contrib.providers.envvar;
2+
3+
import java.util.function.*;
4+
5+
import lombok.RequiredArgsConstructor;
6+
import org.apache.commons.lang3.StringUtils;
7+
import org.apache.commons.text.CaseUtils;
8+
9+
/**
10+
* This class provides a way to transform any given key to another value. This is helpful, if keys in the code have a
11+
* different representation as in the actual environment, e.g. SCREAMING_SNAKE_CASE env vars vs. hyphen-case keys
12+
* for feature flags.
13+
*
14+
* <p>This class also supports chaining/combining different transformers incl. self-written ones by providing
15+
* a transforming function in the constructor. <br>
16+
* Currently, the following transformations are supported out of the box:
17+
* <ul>
18+
* <li>{@link #toLowerCaseTransformer() converting to lower case}</li>
19+
* <li>{@link #toUpperCaseTransformer() converting to UPPER CASE}</li>
20+
* <li>{@link #hyphenCaseToScreamingSnake() converting hyphen-case to SCREAMING_SNAKE_CASE}</li>
21+
* <li>{@link #toCamelCaseTransformer() convert to camelCase}</li>
22+
* <li>{@link #replaceUnderscoreWithDotTransformer() replace '_' with '.'}</li>
23+
* <li>{@link #replaceDotWithUnderscoreTransformer() replace '.' with '_'}</li>
24+
* </ul>
25+
*
26+
* <p><strong>Examples:</strong>
27+
*
28+
* <p>1. hyphen-case feature flag names to screaming snake-case environment variables:
29+
* <pre>
30+
* {@code
31+
* // Definition of the EnvVarProvider:
32+
* EnvironmentKeyTransformer transformer = EnvironmentKeyTransformer
33+
* .hyphenCaseToScreamingSnake();
34+
*
35+
* FeatureProvider provider = new EnvVarProvider(transformer);
36+
* }
37+
* </pre>
38+
* 2. chained/composed transformations:
39+
* <pre>
40+
* {@code
41+
* // Definition of the EnvVarProvider:
42+
* EnvironmentKeyTransformer transformer = EnvironmentKeyTransformer
43+
* .toLowerCaseTransformer()
44+
* .andThen(EnvironmentKeyTransformer.replaceUnderscoreWithDotTransformer());
45+
*
46+
* FeatureProvider provider = new EnvVarProvider(transformer);
47+
* }
48+
* </pre>
49+
* 3. freely defined transformation function:
50+
* <pre>
51+
* {@code
52+
*
53+
* // Definition of the EnvVarProvider:
54+
* EnvironmentKeyTransformer transformer = new EnvironmentKeyTransformer(key -> "constant");
55+
*
56+
* FeatureProvider provider = new EnvVarProvider(keyTransformer);
57+
* }
58+
* </pre>
59+
*/
60+
@RequiredArgsConstructor
61+
public class EnvironmentKeyTransformer {
62+
63+
private static final UnaryOperator<String> TO_LOWER_CASE = StringUtils::lowerCase;
64+
private static final UnaryOperator<String> TO_UPPER_CASE = StringUtils::upperCase;
65+
private static final UnaryOperator<String> TO_CAMEL_CASE = s -> CaseUtils.toCamelCase(s, false, '_');
66+
private static final UnaryOperator<String> REPLACE_DOT_WITH_UNDERSCORE = s -> StringUtils.replaceChars(s, ".", "_");
67+
private static final UnaryOperator<String> REPLACE_UNDERSCORE_WITH_DOT = s -> StringUtils.replaceChars(s, "_", ".");
68+
private static final UnaryOperator<String> REPLACE_HYPHEN_WITH_UNDERSCORE =
69+
s -> StringUtils.replaceChars(s, "-", "_");
70+
71+
private final Function<String, String> transformation;
72+
73+
public String transformKey(String key) {
74+
return transformation.apply(key);
75+
}
76+
77+
public EnvironmentKeyTransformer andThen(EnvironmentKeyTransformer another) {
78+
return new EnvironmentKeyTransformer(transformation.andThen(another::transformKey));
79+
}
80+
81+
public static EnvironmentKeyTransformer toLowerCaseTransformer() {
82+
return new EnvironmentKeyTransformer(TO_LOWER_CASE);
83+
}
84+
85+
public static EnvironmentKeyTransformer toUpperCaseTransformer() {
86+
return new EnvironmentKeyTransformer(TO_UPPER_CASE);
87+
}
88+
89+
public static EnvironmentKeyTransformer toCamelCaseTransformer() {
90+
return new EnvironmentKeyTransformer(TO_CAMEL_CASE);
91+
}
92+
93+
public static EnvironmentKeyTransformer replaceUnderscoreWithDotTransformer() {
94+
return new EnvironmentKeyTransformer(REPLACE_UNDERSCORE_WITH_DOT);
95+
}
96+
97+
public static EnvironmentKeyTransformer replaceDotWithUnderscoreTransformer() {
98+
return new EnvironmentKeyTransformer(REPLACE_DOT_WITH_UNDERSCORE);
99+
}
100+
101+
public static EnvironmentKeyTransformer hyphenCaseToScreamingSnake() {
102+
return new EnvironmentKeyTransformer(REPLACE_HYPHEN_WITH_UNDERSCORE)
103+
.andThen(EnvironmentKeyTransformer.toUpperCaseTransformer());
104+
}
105+
106+
public static EnvironmentKeyTransformer doNothing() {
107+
return new EnvironmentKeyTransformer(s -> s);
108+
}
109+
}

providers/env-var/src/test/java/dev/openfeature/contrib/providers/envvar/EnvVarProviderTest.java

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@
44
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
55
import dev.openfeature.sdk.exceptions.ParseError;
66
import dev.openfeature.sdk.exceptions.ValueNotConvertableError;
7-
import org.junit.jupiter.api.DynamicTest;
8-
import org.junit.jupiter.api.Test;
9-
import org.junit.jupiter.api.TestFactory;
7+
import org.junit.jupiter.api.*;
108

11-
import java.util.Arrays;
9+
import java.util.*;
1210
import java.util.function.Consumer;
1311
import java.util.function.Function;
1412

13+
import static org.assertj.core.api.Assertions.assertThat;
1514
import static org.junit.jupiter.api.Assertions.assertEquals;
1615
import static org.junit.jupiter.api.Assertions.assertThrows;
1716

@@ -134,6 +133,33 @@ Iterable<DynamicTest> shouldThrowOnUnparseableValues() {
134133
);
135134
}
136135

136+
@Test
137+
@DisplayName("should transform key if configured")
138+
void shouldTransformKeyIfConfigured() {
139+
String key = "key.transformed";
140+
String expected = "key_transformed";
141+
142+
EnvironmentKeyTransformer transformer = EnvironmentKeyTransformer.replaceDotWithUnderscoreTransformer();
143+
EnvironmentGateway gateway = s -> s;
144+
EnvVarProvider provider = new EnvVarProvider(gateway, transformer);
145+
146+
String environmentVariableValue = provider.getStringEvaluation(key, "failed", null).getValue();
147+
148+
assertThat(environmentVariableValue).isEqualTo(expected);
149+
}
150+
151+
@Test
152+
@DisplayName("should use passed-in EnvironmentGateway")
153+
void shouldUsePassedInEnvironmentGateway() {
154+
EnvironmentGateway testFake = s -> "true";
155+
156+
EnvVarProvider provider = new EnvVarProvider(testFake);
157+
158+
boolean actual = provider.getBooleanEvaluation("any key", false, null).getValue();
159+
160+
assertThat(actual).isTrue();
161+
}
162+
137163
private <T> DynamicTest evaluationTest(
138164
String testName,
139165
String variableName,

0 commit comments

Comments
 (0)