Skip to content

feat: [go-feature-flag] Support flag metadata #367

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 8 commits into from
Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package dev.openfeature.contrib.providers.gofeatureflag;

import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
Expand All @@ -12,35 +9,23 @@
import dev.openfeature.contrib.providers.gofeatureflag.bean.GoFeatureFlagUser;
import dev.openfeature.contrib.providers.gofeatureflag.exception.InvalidEndpoint;
import dev.openfeature.contrib.providers.gofeatureflag.exception.InvalidOptions;
import dev.openfeature.sdk.ErrorCode;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.FeatureProvider;
import dev.openfeature.sdk.Hook;
import dev.openfeature.sdk.Metadata;
import dev.openfeature.sdk.MutableStructure;
import dev.openfeature.sdk.ProviderEvaluation;
import dev.openfeature.sdk.Reason;
import dev.openfeature.sdk.Structure;
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.*;
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
import dev.openfeature.sdk.exceptions.GeneralError;
import dev.openfeature.sdk.exceptions.OpenFeatureError;
import dev.openfeature.sdk.exceptions.TypeMismatchError;
import okhttp3.ConnectionPool;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.*;

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

import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;

/**
* GoFeatureFlagProvider is the JAVA provider implementation for the feature flag solution GO Feature Flag.
*/
Expand Down Expand Up @@ -226,14 +211,15 @@ private <T> ProviderEvaluation<T> resolveEvaluationGoFeatureFlagProxy(

if (flagValue.getClass() != expectedType) {
throw new TypeMismatchError("Flag value " + key + " had unexpected type "
+ flagValue.getClass() + ", expected " + expectedType + ".");
+ flagValue.getClass() + ", expected " + expectedType + ".");
}

return ProviderEvaluation.<T>builder()
.errorCode(mapErrorCode(goffResp.getErrorCode()))
.reason(goffResp.getReason())
.value(flagValue)
.variant(goffResp.getVariationType())
.flagMetadata(this.convertFlagMetadata(goffResp.getMetadata()))
.build();

}
Expand All @@ -256,6 +242,32 @@ private ErrorCode mapErrorCode(String errorCode) {
}
}

/**
* convertFlagMetadata is converting the flagMetadata object received from the server
* to an ImmutableMetadata format known by Open Feature.
*
* @param flagMetadata - metadata received from the server
* @return a converted metadata object.
*/
private ImmutableMetadata convertFlagMetadata(Map<String, Object> flagMetadata) {
ImmutableMetadata.ImmutableMetadataBuilder builder = ImmutableMetadata.builder();
flagMetadata.forEach((k, v) -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're facing NullPointerException on introducing the flag sdk into our server. The metadata should be optional.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am seeing this as well

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (v instanceof Long) {
builder.addLong(k, (Long) v);
} else if (v instanceof Integer) {
builder.addInteger(k, (Integer) v);
} else if (v instanceof Float) {
builder.addFloat(k, (Float) v);
} else if (v instanceof Double) {
builder.addDouble(k, (Double) v);
} else if (v instanceof Boolean) {
builder.addBoolean(k, (Boolean) v);
} else {
builder.addString(k, v.toString());
}
});
return builder.build();
}

/**
* convertValue is converting the object return by the proxy response in the right type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import lombok.Setter;
import lombok.ToString;

import java.util.Map;

/**
* GoFeatureFlagResponse is the response returned by the relay proxy.
*/
Expand All @@ -18,6 +20,7 @@ public class GoFeatureFlagResponse {
private String reason;
private String errorCode;
private Object value;
private Map<String, Object> metadata;
}


Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,14 @@
import java.util.Arrays;
import java.util.List;

import dev.openfeature.sdk.*;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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.MutableContext;
import dev.openfeature.sdk.MutableStructure;
import dev.openfeature.sdk.ProviderEvaluation;
import dev.openfeature.sdk.Reason;
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
import dev.openfeature.sdk.exceptions.GeneralError;
import dev.openfeature.sdk.exceptions.TypeMismatchError;
Expand Down Expand Up @@ -56,6 +52,12 @@ public MockResponse dispatch(RecordedRequest request) {
private HttpUrl baseUrl;
private MutableContext evaluationContext;

private final static ImmutableMetadata defaultMetadata =
ImmutableMetadata.builder()
.addString("pr_link", "https://github.com/thomaspoignant/go-feature-flag/pull/916")
.addInteger("version", 1)
.build();

@BeforeEach
void beforeEach() throws IOException {
this.server = new MockWebServer();
Expand Down Expand Up @@ -153,6 +155,9 @@ void should_resolve_a_valid_boolean_flag_with_TARGETING_MATCH_reason() throws In
assertNull(res.getErrorCode());
assertEquals(Reason.TARGETING_MATCH.toString(), res.getReason());
assertEquals("True", res.getVariant());
assertAll("Test flagMetadata",
() -> assertEquals(defaultMetadata.getInteger("version"), res.getFlagMetadata().getInteger("version")),
() -> assertEquals(defaultMetadata.getString("pr_link"), res.getFlagMetadata().getString("pr_link")));
}

@Test
Expand All @@ -163,6 +168,9 @@ void should_return_custom_reason_if_returned_by_relay_proxy() throws InvalidOpti
assertNull(res.getErrorCode());
assertEquals("CUSTOM_REASON", res.getReason());
assertEquals("True", res.getVariant());
assertAll("Test flagMetadata",
() -> assertEquals(defaultMetadata.getInteger("version"), res.getFlagMetadata().getInteger("version")),
() -> assertEquals(defaultMetadata.getString("pr_link"), res.getFlagMetadata().getString("pr_link")));
}

@Test
Expand All @@ -187,6 +195,9 @@ void should_resolve_a_valid_string_flag_with_TARGETING_MATCH_reason() throws Inv
assertNull(res.getErrorCode());
assertEquals(Reason.TARGETING_MATCH.toString(), res.getReason());
assertEquals("True", res.getVariant());
assertAll("Test flagMetadata",
() -> assertEquals(defaultMetadata.getInteger("version"), res.getFlagMetadata().getInteger("version")),
() -> assertEquals(defaultMetadata.getString("pr_link"), res.getFlagMetadata().getString("pr_link")));
}

@Test
Expand All @@ -211,6 +222,9 @@ void should_resolve_a_valid_integer_flag_with_TARGETING_MATCH_reason() throws In
assertNull(res.getErrorCode());
assertEquals(Reason.TARGETING_MATCH.toString(), res.getReason());
assertEquals("True", res.getVariant());
assertAll("Test flagMetadata",
() -> assertEquals(defaultMetadata.getInteger("version"), res.getFlagMetadata().getInteger("version")),
() -> assertEquals(defaultMetadata.getString("pr_link"), res.getFlagMetadata().getString("pr_link")));
}

@Test
Expand All @@ -235,6 +249,9 @@ void should_resolve_a_valid_double_flag_with_TARGETING_MATCH_reason() throws Inv
assertNull(res.getErrorCode());
assertEquals(Reason.TARGETING_MATCH.toString(), res.getReason());
assertEquals("True", res.getVariant());
assertAll("Test flagMetadata",
() -> assertEquals(defaultMetadata.getInteger("version"), res.getFlagMetadata().getInteger("version")),
() -> assertEquals(defaultMetadata.getString("pr_link"), res.getFlagMetadata().getString("pr_link")));
}

@Test
Expand All @@ -254,6 +271,9 @@ void should_resolve_a_valid_value_flag_with_TARGETING_MATCH_reason() throws Inva
assertNull(res.getErrorCode());
assertEquals(Reason.TARGETING_MATCH.toString(), res.getReason());
assertEquals("True", res.getVariant());
assertAll("Test flagMetadata",
() -> assertEquals(defaultMetadata.getInteger("version"), res.getFlagMetadata().getInteger("version")),
() -> assertEquals(defaultMetadata.getString("pr_link"), res.getFlagMetadata().getString("pr_link")));
}

@Test
Expand All @@ -265,6 +285,9 @@ void should_wrap_into_value_if_wrong_type() throws InvalidOptions {
assertNull(res.getErrorCode());
assertEquals(Reason.TARGETING_MATCH.toString(), res.getReason());
assertEquals("True", res.getVariant());
assertAll("Test flagMetadata",
() -> assertEquals(defaultMetadata.getInteger("version"), res.getFlagMetadata().getInteger("version")),
() -> assertEquals(defaultMetadata.getString("pr_link"), res.getFlagMetadata().getString("pr_link")));
}

@Test
Expand Down Expand Up @@ -296,6 +319,9 @@ void should_throw_an_error_if_no_targeting_key() throws InvalidOptions {
assertNull(res.getErrorCode());
assertEquals(Reason.TARGETING_MATCH.toString(), res.getReason());
assertEquals("True", res.getVariant());
assertAll("Test flagMetadata",
() -> assertEquals(defaultMetadata.getInteger("version"), res.getFlagMetadata().getInteger("version")),
() -> assertEquals(defaultMetadata.getString("pr_link"), res.getFlagMetadata().getString("pr_link")));
}

@Test
Expand All @@ -306,6 +332,9 @@ void should_not_fail_if_receive_an_unknown_field_in_response() throws InvalidOpt
assertNull(res.getErrorCode());
assertEquals(Reason.TARGETING_MATCH.toString(), res.getReason());
assertEquals("True", res.getVariant());
assertAll("Test flagMetadata",
() -> assertEquals(defaultMetadata.getInteger("version"), res.getFlagMetadata().getInteger("version")),
() -> assertEquals(defaultMetadata.getString("pr_link"), res.getFlagMetadata().getString("pr_link")));
}

private String readMockResponse(String filename) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@
"version": 0,
"reason": "TARGETING_MATCH",
"errorCode": "",
"value": true
"value": true,
"metadata": {
"pr_link": "https://github.com/thomaspoignant/go-feature-flag/pull/916",
"version": 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@
"version": 0,
"reason": "DISABLED",
"errorCode": "",
"value": true
"value": true,
"metadata": {
"pr_link": "https://github.com/thomaspoignant/go-feature-flag/pull/916",
"version": 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@
"version": 0,
"reason": "TARGETING_MATCH",
"errorCode": "",
"value": 100.25
"value": 100.25,
"metadata": {
"pr_link": "https://github.com/thomaspoignant/go-feature-flag/pull/916",
"version": 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@
"version": 0,
"reason": "ERROR",
"errorCode": "FLAG_NOT_FOUND",
"value": "false"
"value": "false",
"metadata": {
"pr_link": "https://github.com/thomaspoignant/go-feature-flag/pull/916",
"version": 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@
"version": 0,
"reason": "TARGETING_MATCH",
"errorCode": "",
"value": 100
"value": 100,
"metadata": {
"pr_link": "https://github.com/thomaspoignant/go-feature-flag/pull/916",
"version": 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,9 @@
"test2",
"false",
"test3"
]
],
"metadata": {
"pr_link": "https://github.com/thomaspoignant/go-feature-flag/pull/916",
"version": 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,9 @@
"test3": 123.3,
"test4": 1,
"test5": null
},
"metadata": {
"pr_link": "https://github.com/thomaspoignant/go-feature-flag/pull/916",
"version": 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@
"version": 0,
"reason": "TARGETING_MATCH",
"errorCode": "",
"value": "CC0000"
"value": "CC0000",
"metadata": {
"pr_link": "https://github.com/thomaspoignant/go-feature-flag/pull/916",
"version": 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,9 @@
"errorCode": "",
"value": true,
"unknown_field": true,
"unknown_field2": "unknown_field2"
"unknown_field2": "unknown_field2",
"metadata": {
"pr_link": "https://github.com/thomaspoignant/go-feature-flag/pull/916",
"version": 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@
"version": 0,
"reason": "CUSTOM_REASON",
"errorCode": "",
"value": true
"value": true,
"metadata": {
"pr_link": "https://github.com/thomaspoignant/go-feature-flag/pull/916",
"version": 1
}
}