Skip to content

Commit

Permalink
chore: go feature flags: use sdk functionality (open-feature#403)
Browse files Browse the repository at this point in the history
Signed-off-by: liran2000 <liran2000@gmail.com>
  • Loading branch information
liran2000 authored and Kavindu-Dodan committed Sep 6, 2023
1 parent a8c91f6 commit d805b93
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 81 deletions.
14 changes: 14 additions & 0 deletions providers/go-feature-flag/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,21 @@ FeatureProvider provider = new GoFeatureFlagProvider(
.endpoint("https://my-gofeatureflag-instance.org")
.timeout(1000)
.build());

OpenFeatureAPI.getInstance().setProviderAndWait(providerName);

// ...

Client client = OpenFeatureAPI.getInstance().getClient(providerName);

// targetingKey is mandatory for each evaluation
EvaluationContext evaluationContext = new ImmutableContext(targetingKey);

booleanFlagEvaluationDetails = client.getBooleanDetails("feature_flag1", false, evaluationContext);
value = booleanFlagEvaluationDetails.getValue();

// ...

provider.shutdown();
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,14 @@
import dev.openfeature.sdk.Hook;
import dev.openfeature.sdk.ImmutableMetadata;
import dev.openfeature.sdk.Metadata;
import dev.openfeature.sdk.MutableStructure;
import dev.openfeature.sdk.ProviderEvaluation;
import dev.openfeature.sdk.ProviderState;
import dev.openfeature.sdk.Reason;
import dev.openfeature.sdk.Structure;
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
import dev.openfeature.sdk.exceptions.GeneralError;
import dev.openfeature.sdk.exceptions.OpenFeatureError;
import dev.openfeature.sdk.exceptions.ProviderNotReadyError;
import dev.openfeature.sdk.exceptions.TypeMismatchError;
import lombok.AccessLevel;
import lombok.Getter;
Expand All @@ -47,13 +46,12 @@

import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static dev.openfeature.sdk.Value.objectToValue;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
Expand Down Expand Up @@ -262,15 +260,15 @@ private <T> ProviderEvaluation<T> getEvaluation(
GoFeatureFlagUser user = GoFeatureFlagUser.fromEvaluationContext(evaluationContext);
try {
if (!ProviderState.READY.equals(state)) {
ErrorCode errorCode = ErrorCode.PROVIDER_NOT_READY;
if (ProviderState.ERROR.equals(state)) {
errorCode = ErrorCode.GENERAL;
if (ProviderState.NOT_READY.equals(state)) {

/*
should be handled by the SDK framework, ErrorCode.PROVIDER_NOT_READY and default value
should be returned when evaluated via the client.
*/
throw new ProviderNotReadyError("provider not initialized yet");
}
return ProviderEvaluation.<T>builder()
.errorCode(errorCode)
.reason(errorCode.name())
.value(defaultValue)
.build();
throw new GeneralError("unknown error, provider state: " + state);
}

if (cache == null) {
Expand Down Expand Up @@ -476,52 +474,6 @@ private <T> T convertValue(Object value, Class<?> expectedType) {
return (T) objectToValue(value);
}

/**
* objectToValue is wrapping an object into a Value.
*
* @param object the object you want to wrap
* @return the wrapped object
*/
private Value objectToValue(Object object) {
if (object instanceof Value) {
return (Value) object;
} else if (object == null) {
return null;
} else if (object instanceof String) {
return new Value((String) object);
} else if (object instanceof Boolean) {
return new Value((Boolean) object);
} else if (object instanceof Integer) {
return new Value((Integer) object);
} else if (object instanceof Double) {
return new Value((Double) object);
} else if (object instanceof Structure) {
return new Value((Structure) object);
} else if (object instanceof List) {
// need to translate each elem in list to a value
return new Value(((List<Object>) object).stream().map(this::objectToValue).collect(Collectors.toList()));
} else if (object instanceof Instant) {
return new Value((Instant) object);
} else if (object instanceof Map) {
return new Value(mapToStructure((Map<String, Object>) object));
} else {
throw new ClassCastException("Could not cast Object to Value");
}
}

/**
* mapToStructure transform a map coming from a JSON Object to a Structure type.
*
* @param map - JSON object return by the API
* @return a Structure object in the SDK format
*/
private Structure mapToStructure(Map<String, Object> map) {
return new MutableStructure(
map.entrySet().stream()
.filter(e -> e.getValue() != null)
.collect(Collectors.toMap(Map.Entry::getKey, e -> objectToValue(e.getValue()))));
}

/**
* publishEvents is calling the GO Feature Flag data/collector api to store the flag usage for analytics.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package dev.openfeature.contrib.providers.gofeatureflag.bean;

import dev.openfeature.contrib.providers.gofeatureflag.exception.InvalidTargetingKey;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.exceptions.TargetingKeyMissingError;
import lombok.Builder;
import lombok.Getter;

Expand All @@ -29,7 +29,7 @@ public class GoFeatureFlagUser {
public static GoFeatureFlagUser fromEvaluationContext(EvaluationContext ctx) {
String key = ctx.getTargetingKey();
if (key == null || "".equals(key)) {
throw new InvalidTargetingKey();
throw new TargetingKeyMissingError();
}
Value anonymousValue = ctx.getValue(anonymousFieldName);
if (anonymousValue == null) {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@
import java.util.List;

import com.google.common.cache.CacheBuilder;
import dev.openfeature.sdk.Client;
import dev.openfeature.sdk.ErrorCode;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.FlagEvaluationDetails;
import dev.openfeature.sdk.ImmutableContext;
import dev.openfeature.sdk.OpenFeatureAPI;
import dev.openfeature.sdk.ProviderState;
import dev.openfeature.sdk.exceptions.ProviderNotReadyError;
import dev.openfeature.sdk.exceptions.TargetingKeyMissingError;
import org.jetbrains.annotations.NotNull;
import dev.openfeature.sdk.ImmutableMetadata;
import dev.openfeature.sdk.MutableContext;
Expand All @@ -24,7 +31,6 @@

import dev.openfeature.contrib.providers.gofeatureflag.exception.InvalidEndpoint;
import dev.openfeature.contrib.providers.gofeatureflag.exception.InvalidOptions;
import dev.openfeature.contrib.providers.gofeatureflag.exception.InvalidTargetingKey;
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
import dev.openfeature.sdk.exceptions.GeneralError;
import dev.openfeature.sdk.exceptions.TypeMismatchError;
Expand All @@ -35,6 +41,7 @@
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import static dev.openfeature.contrib.providers.gofeatureflag.GoFeatureFlagProvider.CACHED_REASON;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
Expand Down Expand Up @@ -157,10 +164,51 @@ void constructor_options_valid_endpoint() {
@SneakyThrows
@Test
void should_return_not_ready_if_not_initialized() {
GoFeatureFlagProvider g = new GoFeatureFlagProvider(GoFeatureFlagProviderOptions.builder().endpoint(this.baseUrl.toString()).timeout(1000).build()){
@Override
public void initialize(EvaluationContext evaluationContext) throws Exception {

// make the provider not initialized for this test
Thread.sleep(3000);
}
};

/*
ErrorCode.PROVIDER_NOT_READY and default value should be returned when evaluated via the client,
see next step in this test.
*/
assertThrows(ProviderNotReadyError.class, ()-> g.getBooleanEvaluation("bool_targeting_match", false, this.evaluationContext));

String providerName = "shouldReturnNotReadyIfNotInitialized";
OpenFeatureAPI.getInstance().setProvider(providerName, g);
assertThat(OpenFeatureAPI.getInstance().getProvider(providerName).getState()).isEqualTo(ProviderState.NOT_READY);
Client client = OpenFeatureAPI.getInstance().getClient(providerName);
FlagEvaluationDetails<Boolean> booleanFlagEvaluationDetails = client.getBooleanDetails("return_error_when_not_initialized", false, new ImmutableContext("targetingKey"));
assertEquals(ErrorCode.PROVIDER_NOT_READY, booleanFlagEvaluationDetails.getErrorCode());
assertEquals(Boolean.FALSE, booleanFlagEvaluationDetails.getValue());
}

@SneakyThrows
@Test
void client_test() {
GoFeatureFlagProvider g = new GoFeatureFlagProvider(GoFeatureFlagProviderOptions.builder().endpoint(this.baseUrl.toString()).timeout(1000).build());
assertEquals(ErrorCode.PROVIDER_NOT_READY, g.getBooleanEvaluation("fail_not_initialized", false, this.evaluationContext).getErrorCode());

String providerName = "clientTest";
OpenFeatureAPI.getInstance().setProviderAndWait(providerName, g);

Client client = OpenFeatureAPI.getInstance().getClient(providerName);
Boolean value = client.getBooleanValue("bool_targeting_match",
false);
assertEquals(Boolean.FALSE, value, "should evaluate to default value without context");
FlagEvaluationDetails<Boolean> booleanFlagEvaluationDetails = client.getBooleanDetails("bool_targeting_match",
false, new ImmutableContext());
assertEquals(Boolean.FALSE, booleanFlagEvaluationDetails.getValue(), "should evaluate to default value with empty context");
assertEquals(ErrorCode.TARGETING_KEY_MISSING, booleanFlagEvaluationDetails.getErrorCode(), "should evaluate to default value with empty context");
booleanFlagEvaluationDetails = client.getBooleanDetails("bool_targeting_match", false, new ImmutableContext("targetingKey"));
assertEquals(Boolean.TRUE, booleanFlagEvaluationDetails.getValue(), "should evaluate with a valid context");
}


@SneakyThrows
@Test
void should_throw_an_error_if_endpoint_not_available() {
Expand Down Expand Up @@ -461,15 +509,15 @@ void should_use_object_default_value_if_the_flag_is_disabled() {

@SneakyThrows
@Test
void should_resolve_a_valid_value_flag_with_a_list() {
void should_throw_an_error_if_no_targeting_key() {
GoFeatureFlagProvider g = new GoFeatureFlagProvider(GoFeatureFlagProviderOptions.builder().endpoint(this.baseUrl.toString()).timeout(1000).build());
g.initialize(new ImmutableContext());
assertThrows(InvalidTargetingKey.class, () -> g.getObjectEvaluation("list_key", null, new MutableContext()));
assertThrows(TargetingKeyMissingError.class, () -> g.getObjectEvaluation("list_key", null, new MutableContext()));
}

@SneakyThrows
@Test
void should_throw_an_error_if_no_targeting_key() {
void should_resolve_a_valid_value_flag_with_a_list() {
GoFeatureFlagProvider g = new GoFeatureFlagProvider(GoFeatureFlagProviderOptions.builder().endpoint(this.baseUrl.toString()).timeout(1000).build());
g.initialize(new ImmutableContext());
ProviderEvaluation<Value> res = g.getObjectEvaluation("list_key", null, this.evaluationContext);
Expand Down

0 comments on commit d805b93

Please sign in to comment.