Skip to content

chore: go feature flags: use sdk functionality #403

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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