Skip to content

Commit 6b1efc8

Browse files
committed
feat: openfeature first draft
1 parent 36ab385 commit 6b1efc8

6 files changed

Lines changed: 261 additions & 20 deletions

File tree

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@
130130
<dependency>
131131
<groupId>dev.openfeature</groupId>
132132
<artifactId>sdk</artifactId>
133-
<version>1.10.0</version>
133+
<version>1.16.0</version>
134134
</dependency>
135135
<dependency>
136136
<groupId>ch.qos.logback</groupId>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package fr.maif.openfeatures;
2+
3+
import java.util.Optional;
4+
5+
import dev.openfeature.sdk.EvaluationContext;
6+
import dev.openfeature.sdk.Value;
7+
8+
public class IzanamiEvaluationContext {
9+
public static String IZANAMI_CONTEXT = "context";
10+
public final String user;
11+
public final String context;
12+
13+
private IzanamiEvaluationContext(String user, String context) {
14+
this.user = user;
15+
this.context = context;
16+
}
17+
18+
public static IzanamiEvaluationContext fromContext(EvaluationContext evaluationContext) {
19+
String user = evaluationContext.getTargetingKey();
20+
String context = Optional.ofNullable(evaluationContext.getValue(IZANAMI_CONTEXT)).map(Value::asString).orElse(null);
21+
22+
return new IzanamiEvaluationContext(user, context);
23+
24+
}
25+
26+
public static IzanamiEvaluationContextBuilder newBuilder() {
27+
return new IzanamiEvaluationContextBuilder();
28+
}
29+
30+
public static IzanamiEvaluationContextBuilder toBuilder(IzanamiEvaluationContext builder) {
31+
return new IzanamiEvaluationContextBuilder(builder.user, builder.context);
32+
}
33+
34+
public static class IzanamiEvaluationContextBuilder {
35+
private String user;
36+
private String context;
37+
38+
public IzanamiEvaluationContextBuilder() {}
39+
40+
public IzanamiEvaluationContextBuilder(String user, String context) {
41+
this.user = user;
42+
this.context = context;
43+
}
44+
45+
public IzanamiEvaluationContextBuilder withUser(String user) {
46+
this.user = user;
47+
return this;
48+
}
49+
50+
public IzanamiEvaluationContextBuilder withContext(String context) {
51+
this.context = context;
52+
return this;
53+
}
54+
55+
public IzanamiEvaluationContext build() {
56+
return new IzanamiEvaluationContext(user, context);
57+
}
58+
}
59+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package fr.maif.openfeatures;
2+
3+
import static dev.openfeature.sdk.Structure.mapToStructure;
4+
import static fr.maif.FeatureClientErrorStrategy.defaultValueStrategy;
5+
6+
import java.math.BigDecimal;
7+
import java.time.Instant;
8+
import java.util.List;
9+
import java.util.Map;
10+
import java.util.Optional;
11+
import java.util.stream.Collectors;
12+
13+
import com.fasterxml.jackson.core.JsonProcessingException;
14+
import com.fasterxml.jackson.databind.ObjectMapper;
15+
import com.fasterxml.jackson.databind.node.ObjectNode;
16+
17+
import dev.openfeature.sdk.EvaluationContext;
18+
import dev.openfeature.sdk.FeatureProvider;
19+
import dev.openfeature.sdk.Metadata;
20+
import dev.openfeature.sdk.ProviderEvaluation;
21+
import dev.openfeature.sdk.Structure;
22+
import dev.openfeature.sdk.Value;
23+
import dev.openfeature.sdk.exceptions.TypeMismatchError;
24+
import fr.maif.IzanamiClient;
25+
import fr.maif.requests.SingleFeatureRequest;
26+
27+
public class IzanamiOpenFeatureProvider implements FeatureProvider {
28+
private final IzanamiClient izanamiClient;
29+
private final ObjectMapper mapper = new ObjectMapper();
30+
31+
public IzanamiOpenFeatureProvider(IzanamiClient izanamiClient) {
32+
this.izanamiClient = izanamiClient;
33+
}
34+
35+
@Override
36+
public Metadata getMetadata() {
37+
return () -> "Izanami provider";
38+
}
39+
40+
@Override
41+
public void initialize(EvaluationContext evaluationContext) throws Exception {}
42+
43+
@Override
44+
public void shutdown() {
45+
izanamiClient.close().join();
46+
}
47+
48+
@Override
49+
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
50+
var izanamiContext = IzanamiEvaluationContext.fromContext(ctx);
51+
var result = izanamiClient.booleanValue(SingleFeatureRequest.newSingleFeatureRequest(key)
52+
.withContext(izanamiContext.context)
53+
.withUser(izanamiContext.user)
54+
.withErrorStrategy(defaultValueStrategy(defaultValue, null, null))
55+
).join();
56+
return ProviderEvaluation.<Boolean>builder().value(result).build();
57+
}
58+
59+
@Override
60+
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
61+
var izanamiContext = IzanamiEvaluationContext.fromContext(ctx);
62+
var result = izanamiClient.stringValue(SingleFeatureRequest.newSingleFeatureRequest(key)
63+
.withContext(izanamiContext.context)
64+
.withUser(izanamiContext.user)
65+
.withErrorStrategy(defaultValueStrategy(false, defaultValue, null))
66+
).join();
67+
return ProviderEvaluation.<String>builder().value(result).build();
68+
}
69+
70+
@Override
71+
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
72+
var izanamiContext = IzanamiEvaluationContext.fromContext(ctx);
73+
var result = izanamiClient.numberValue(SingleFeatureRequest.newSingleFeatureRequest(key)
74+
.withContext(izanamiContext.context)
75+
.withUser(izanamiContext.user)
76+
.withErrorStrategy(defaultValueStrategy(false, null, BigDecimal.valueOf(defaultValue)))
77+
).join();
78+
return ProviderEvaluation.<Integer>builder().value(result.intValue()).build();
79+
}
80+
81+
@Override
82+
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
83+
var izanamiContext = IzanamiEvaluationContext.fromContext(ctx);
84+
var result = izanamiClient.numberValue(SingleFeatureRequest.newSingleFeatureRequest(key)
85+
.withContext(izanamiContext.context)
86+
.withUser(izanamiContext.user)
87+
.withErrorStrategy(defaultValueStrategy(false, null, BigDecimal.valueOf(defaultValue)))
88+
).join();
89+
return ProviderEvaluation.<Double>builder().value(result.doubleValue()).build();
90+
}
91+
92+
@Override
93+
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
94+
var izanamiContext = IzanamiEvaluationContext.fromContext(ctx);
95+
var result = izanamiClient.stringValue(SingleFeatureRequest.newSingleFeatureRequest(key)
96+
.withContext(izanamiContext.context)
97+
.withUser(izanamiContext.user)
98+
.withErrorStrategy(
99+
defaultValueStrategy(
100+
defaultValue.asBoolean(),
101+
defaultValue.asString(),
102+
Optional.ofNullable(defaultValue.asDouble()).map(BigDecimal::valueOf)
103+
.or(() -> Optional.ofNullable(defaultValue.asInteger()).map(BigDecimal::valueOf))
104+
.orElse(null)
105+
)
106+
)
107+
).join();
108+
try {
109+
Object tree = mapper.readValue(result, Object.class);
110+
return ProviderEvaluation.<Value>builder().value(objectToValue(tree)).build();
111+
} catch (JsonProcessingException e) {
112+
throw new TypeMismatchError("Flag " + key + " evaluation returned invalid json.");
113+
}
114+
}
115+
116+
117+
private Value objectToValue(Object object) {
118+
if (object instanceof Value) {
119+
return (Value) object;
120+
} else if (object == null) {
121+
return null;
122+
} else if (object instanceof String) {
123+
return new Value((String) object);
124+
} else if (object instanceof Boolean) {
125+
return new Value((Boolean) object);
126+
} else if (object instanceof Integer) {
127+
return new Value((Integer) object);
128+
} else if (object instanceof Double) {
129+
return new Value((Double) object);
130+
} else if (object instanceof Structure) {
131+
return new Value((Structure) object);
132+
} else if (object instanceof List) {
133+
// need to translate each elem in list to a value
134+
return new Value(
135+
((List<Object>) object).stream().map(this::objectToValue).collect(Collectors.toList()));
136+
} else if (object instanceof Instant) {
137+
return new Value((Instant) object);
138+
} else if (object instanceof Map) {
139+
return new Value(mapToStructure((Map<String, Object>) object));
140+
} else if (object instanceof ObjectNode) {
141+
ObjectNode objectNode = (ObjectNode) object;
142+
return objectToValue(new ObjectMapper().convertValue(objectNode, Object.class));
143+
} else {
144+
throw new TypeMismatchError("Flag value " + object + " had unexpected type " + object.getClass() + ".");
145+
}
146+
}
147+
148+
149+
}

src/test/java/fr/maif/IzanamiClientTest.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,21 @@
22

33
import com.github.tomakehurst.wiremock.WireMockServer;
44
import com.github.tomakehurst.wiremock.client.WireMock;
5+
6+
import dev.openfeature.sdk.EvaluationContext;
7+
import dev.openfeature.sdk.ImmutableContext;
8+
import dev.openfeature.sdk.Value;
59
import fr.maif.errors.IzanamiException;
610
import fr.maif.features.values.BooleanCastStrategy;
11+
import fr.maif.openfeatures.IzanamiOpenFeatureProvider;
712
import fr.maif.requests.IzanamiConnectionInformation;
813
import fr.maif.requests.SpecificFeatureRequest;
914
import org.junit.jupiter.api.*;
1015

1116
import java.math.BigDecimal;
1217
import java.time.Duration;
1318
import java.util.List;
19+
import java.util.Map;
1420
import java.util.concurrent.CompletableFuture;
1521
import java.util.concurrent.CompletionException;
1622
import java.util.concurrent.atomic.AtomicBoolean;
@@ -1843,4 +1849,38 @@ public void boolean_cast_hierarchy_should_be_applied_correctly() {
18431849
var result3 = client.featureValues(newFeatureRequest().withFeature(SpecificFeatureRequest.feature(id1).withBooleanCastStrategy(BooleanCastStrategy.STRICT))).join();
18441850
assertThatThrownBy(() -> result3.booleanValue(id1)).isInstanceOf(IzanamiException.class);
18451851
}
1852+
1853+
@Test
1854+
public void open_feature_client_should_work_for_boolean() {
1855+
String id1 = "ae5dd05d-4e90-4ce7-bee7-3751750fdeaa";
1856+
var featureStub1 = Mocks.feature("bar1", true);
1857+
var response = newResponse().withFeature(id1, featureStub1);
1858+
String clientId = "THIS_IS_NOT_A_REAL_DATA_PLEASE_DONT_FILE_AN_ISSUE_ABOUT_THIS";
1859+
String clientSecret = "THIS_IS_NOT_A_REAL_SECRET_PLEASE_DONT_FILE_AN_ISSUE_ABOUT_THIS";
1860+
String url = "/api/v2/features";
1861+
1862+
mockServer.stubFor(WireMock.get(WireMock.urlPathEqualTo(url))
1863+
.withQueryParam("conditions", equalTo("true"))
1864+
.withQueryParam("features", equalTo(id1))
1865+
.withHeader("Izanami-Client-Id", equalTo(clientId))
1866+
.withHeader("Izanami-Client-Secret", equalTo(clientSecret))
1867+
.willReturn(WireMock.ok().withHeader("Content-Type", "application/json")
1868+
.withBody(response.toJson())
1869+
)
1870+
);
1871+
1872+
var client = IzanamiClient
1873+
.newBuilder(
1874+
connectionInformation()
1875+
.withUrl("http://localhost:9999/api")
1876+
.withClientId(clientId)
1877+
.withClientSecret(clientSecret)
1878+
)
1879+
.build();
1880+
1881+
IzanamiOpenFeatureProvider openFeatureProvider = new IzanamiOpenFeatureProvider(client);
1882+
1883+
var result = openFeatureProvider.getBooleanEvaluation(id1, false, new ImmutableContext());
1884+
assertThat(result.getValue()).isTrue();
1885+
}
18461886
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package fr.maif;
2+
3+
public class OpenFeatureClientTest {
4+
}

src/test/java/fr/maif/Sandbox.java

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
package fr.maif;
22

3-
import com.fasterxml.jackson.core.JsonProcessingException;
4-
import com.fasterxml.jackson.databind.JsonNode;
5-
import com.fasterxml.jackson.databind.node.ObjectNode;
6-
import fr.maif.features.Feature;
73
import fr.maif.http.ResponseUtils;
84
import fr.maif.requests.FeatureRequest;
9-
import org.junit.jupiter.api.Test;
105
import org.slf4j.Logger;
116
import org.slf4j.LoggerFactory;
127

@@ -30,11 +25,9 @@
3025
public class Sandbox {
3126
//@Test
3227
public void foo() throws URISyntaxException, IOException, InterruptedException {
33-
String clientId = "test_H7mD7i0gV8MtJgXY";
34-
String clientSecret = "aVUm9dXMp26D3TIlebmwAe5we44kDR0tm83mfwL5zBo09Wy15Bg122IizWZQPOBm";
35-
String f1 = "d64dccb4-06e9-4b99-b857-684f44cdd584";
36-
String f2 = "08bd325a-7132-460a-a397-2ca0c7d09a3d";
37-
String f3 = "legyacy";
28+
String clientId = "tenant_ArfEn0nS2O8uK6La";
29+
String clientSecret = "v34NojGKFd2IuPSPhTHyM00VZiJD6yNBJKqZvAiVz3Z4TAzeNmbHOjJnqEspeF0x";
30+
String f1 = "e4b0d23e-89da-4ebf-a906-8388ab625816";
3831

3932
var client = IzanamiClient.newBuilder(
4033
connectionInformation()
@@ -46,21 +39,17 @@ public void foo() throws URISyntaxException, IOException, InterruptedException {
4639
.shouldUseServerSentEvent(true)
4740
.withServerSentEventKeepAliveInterval(Duration.ofSeconds(3L))
4841
.build()
49-
).withPreloadedFeatures(f3)
50-
.withCallTimeout(Duration.ofSeconds(10L))
42+
)
5143
.build();
5244

5345
client.isLoaded().join();
5446
System.out.println("Client is loaded");
5547

56-
var result = client.checkFeatureActivations(FeatureRequest.newFeatureRequest().withFeatures(f3)).join();
48+
var result = client.booleanValue(FeatureRequest.newSingleFeatureRequest(f1)).join();
49+
var result2 = client.booleanValue(FeatureRequest.newSingleFeatureRequest(f1).withContext("dev/bar")).join();
50+
var result3 = client.booleanValue(FeatureRequest.newSingleFeatureRequest(f1)).join();
5751

58-
var strResult = ResponseUtils.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result);
59-
System.out.println(strResult);
60-
61-
while(true) {
62-
Thread.sleep(1000);
63-
}
52+
System.out.println(result3);
6453
}
6554

6655

0 commit comments

Comments
 (0)