feat: add flag metadata (#459)

Signed-off-by: Kavindu Dodanduwa <kavindudodanduwa@gmail.com>
This commit is contained in:
Kavindu Dodanduwa 2023-06-06 18:44:49 -07:00 committed by GitHub
parent 5f173ff860
commit 3ed40a3887
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 275 additions and 12 deletions

View File

@ -7,23 +7,27 @@ import javax.annotation.Nullable;
/**
* Contains information about how the evaluation happened, including any resolved values.
*
* @param <T> the type of the flag being evaluated.
*/
@Data @Builder
@Data
@Builder
public class FlagEvaluationDetails<T> implements BaseEvaluation<T> {
private String flagKey;
private T value;
@Nullable private String variant;
@Nullable private String reason;
private ErrorCode errorCode;
@Nullable private String errorMessage;
@Builder.Default private FlagMetadata flagMetadata = FlagMetadata.builder().build();
/**
* Generate detail payload from the provider response.
*
* @param providerEval provider response
* @param flagKey key for the flag being evaluated
* @param <T> type of flag being returned
* @param flagKey key for the flag being evaluated
* @param <T> type of flag being returned
* @return detail payload
*/
public static <T> FlagEvaluationDetails<T> from(ProviderEvaluation<T> providerEval, String flagKey) {
@ -33,6 +37,7 @@ public class FlagEvaluationDetails<T> implements BaseEvaluation<T> {
.variant(providerEval.getVariant())
.reason(providerEval.getReason())
.errorCode(providerEval.getErrorCode())
.flagMetadata(providerEval.getFlagMetadata())
.build();
}
}

View File

@ -0,0 +1,188 @@
package dev.openfeature.sdk;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
/**
* Immutable Flag Metadata representation. Implementation is backed by a {@link Map} and immutability is provided
* through builder and accessors.
*/
@Slf4j
public class FlagMetadata {
private final Map<String, Object> metadata;
private FlagMetadata(Map<String, Object> metadata) {
this.metadata = metadata;
}
/**
* Retrieve a {@link String} value for the given key. A {@code null} value is returned if the key does not exist
* or if the value is of a different type.
*
* @param key flag metadata key to retrieve
*/
public String getString(final String key) {
return getValue(key, String.class);
}
/**
* Retrieve a {@link Integer} value for the given key. A {@code null} value is returned if the key does not exist
* or if the value is of a different type.
*
* @param key flag metadata key to retrieve
*/
public Integer getInteger(final String key) {
return getValue(key, Integer.class);
}
/**
* Retrieve a {@link Long} value for the given key. A {@code null} value is returned if the key does not exist
* or if the value is of a different type.
*
* @param key flag metadata key to retrieve
*/
public Long getLong(final String key) {
return getValue(key, Long.class);
}
/**
* Retrieve a {@link Float} value for the given key. A {@code null} value is returned if the key does not exist
* or if the value is of a different type.
*
* @param key flag metadata key to retrieve
*/
public Float getFloat(final String key) {
return getValue(key, Float.class);
}
/**
* Retrieve a {@link Double} value for the given key. A {@code null} value is returned if the key does not exist
* or if the value is of a different type.
*
* @param key flag metadata key to retrieve
*/
public Double getDouble(final String key) {
return getValue(key, Double.class);
}
/**
* Retrieve a {@link Boolean} value for the given key. A {@code null} value is returned if the key does not exist
* or if the value is of a different type.
*
* @param key flag metadata key to retrieve
*/
public Boolean getBoolean(final String key) {
return getValue(key, Boolean.class);
}
private <T> T getValue(final String key, final Class<T> type) {
final Object o = metadata.get(key);
if (o == null) {
log.debug("Metadata key " + key + "does not exist");
return null;
}
try {
return type.cast(o);
} catch (ClassCastException e) {
log.debug("Error retrieving value for key " + key, e);
return null;
}
}
/**
* Obtain a builder for {@link FlagMetadata}.
*/
public static FlagMetadataBuilder builder() {
return new FlagMetadataBuilder();
}
/**
* Immutable builder for {@link FlagMetadata}.
*/
public static class FlagMetadataBuilder {
private final Map<String, Object> metadata;
private FlagMetadataBuilder() {
metadata = new HashMap<>();
}
/**
* Add String value to the metadata.
*
* @param key flag metadata key to add
* @param value flag metadata value to add
*/
public FlagMetadataBuilder addString(final String key, final String value) {
metadata.put(key, value);
return this;
}
/**
* Add Integer value to the metadata.
*
* @param key flag metadata key to add
* @param value flag metadata value to add
*/
public FlagMetadataBuilder addInteger(final String key, final Integer value) {
metadata.put(key, value);
return this;
}
/**
* Add Long value to the metadata.
*
* @param key flag metadata key to add
* @param value flag metadata value to add
*/
public FlagMetadataBuilder addLong(final String key, final Long value) {
metadata.put(key, value);
return this;
}
/**
* Add Float value to the metadata.
*
* @param key flag metadata key to add
* @param value flag metadata value to add
*/
public FlagMetadataBuilder addFloat(final String key, final Float value) {
metadata.put(key, value);
return this;
}
/**
* Add Double value to the metadata.
*
* @param key flag metadata key to add
* @param value flag metadata value to add
*/
public FlagMetadataBuilder addDouble(final String key, final Double value) {
metadata.put(key, value);
return this;
}
/**
* Add Boolean value to the metadata.
*
* @param key flag metadata key to add
* @param value flag metadata value to add
*/
public FlagMetadataBuilder addBoolean(final String key, final Boolean value) {
metadata.put(key, value);
return this;
}
/**
* Retrieve {@link FlagMetadata} with provided key,value pairs.
*/
public FlagMetadata build() {
return new FlagMetadata(this.metadata);
}
}
}

View File

@ -13,4 +13,6 @@ public class ProviderEvaluation<T> implements BaseEvaluation<T> {
@Nullable private String reason;
ErrorCode errorCode;
@Nullable private String errorMessage;
@Builder.Default
private FlagMetadata flagMetadata = FlagMetadata.builder().build();
}

View File

@ -1,11 +1,14 @@
package dev.openfeature.sdk;
public class DoSomethingProvider implements FeatureProvider {
class DoSomethingProvider implements FeatureProvider {
static final String name = "Something";
// Flag evaluation metadata
static final FlagMetadata flagMetadata = FlagMetadata.builder().build();
public static final String name = "Something";
private EvaluationContext savedContext;
public EvaluationContext getMergedContext() {
EvaluationContext getMergedContext() {
return savedContext;
}
@ -18,13 +21,16 @@ public class DoSomethingProvider implements FeatureProvider {
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
savedContext = ctx;
return ProviderEvaluation.<Boolean>builder()
.value(!defaultValue).build();
.value(!defaultValue)
.flagMetadata(flagMetadata)
.build();
}
@Override
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
return ProviderEvaluation.<String>builder()
.value(new StringBuilder(defaultValue).reverse().toString())
.flagMetadata(flagMetadata)
.build();
}
@ -33,6 +39,7 @@ public class DoSomethingProvider implements FeatureProvider {
savedContext = ctx;
return ProviderEvaluation.<Integer>builder()
.value(defaultValue * 100)
.flagMetadata(flagMetadata)
.build();
}
@ -41,6 +48,7 @@ public class DoSomethingProvider implements FeatureProvider {
savedContext = ctx;
return ProviderEvaluation.<Double>builder()
.value(defaultValue * 100)
.flagMetadata(flagMetadata)
.build();
}
@ -49,6 +57,7 @@ public class DoSomethingProvider implements FeatureProvider {
savedContext = invocationContext;
return ProviderEvaluation.<Value>builder()
.value(null)
.flagMetadata(flagMetadata)
.build();
}
}

View File

@ -1,6 +1,8 @@
package dev.openfeature.sdk;
import static org.assertj.core.api.Assertions.*;
import static dev.openfeature.sdk.DoSomethingProvider.flagMetadata;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
@ -15,18 +17,18 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
import dev.openfeature.sdk.testutils.FeatureProviderTestUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import dev.openfeature.sdk.fixtures.HookFixtures;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.simplify4u.slf4jmock.LoggerMock;
import org.slf4j.Logger;
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
import dev.openfeature.sdk.fixtures.HookFixtures;
import dev.openfeature.sdk.testutils.FeatureProviderTestUtils;
class FlagEvaluationSpecTest implements HookFixtures {
private Logger logger;
@ -150,6 +152,7 @@ class FlagEvaluationSpecTest implements HookFixtures {
.flagKey(key)
.value(false)
.variant(null)
.flagMetadata(flagMetadata)
.build();
assertEquals(bd, c.getBooleanDetails(key, true));
assertEquals(bd, c.getBooleanDetails(key, true, new ImmutableContext()));
@ -159,6 +162,7 @@ class FlagEvaluationSpecTest implements HookFixtures {
.flagKey(key)
.value("tset")
.variant(null)
.flagMetadata(flagMetadata)
.build();
assertEquals(sd, c.getStringDetails(key, "test"));
assertEquals(sd, c.getStringDetails(key, "test", new ImmutableContext()));
@ -167,6 +171,7 @@ class FlagEvaluationSpecTest implements HookFixtures {
FlagEvaluationDetails<Integer> id = FlagEvaluationDetails.<Integer>builder()
.flagKey(key)
.value(400)
.flagMetadata(flagMetadata)
.build();
assertEquals(id, c.getIntegerDetails(key, 4));
assertEquals(id, c.getIntegerDetails(key, 4, new ImmutableContext()));
@ -175,6 +180,7 @@ class FlagEvaluationSpecTest implements HookFixtures {
FlagEvaluationDetails<Double> dd = FlagEvaluationDetails.<Double>builder()
.flagKey(key)
.value(40.0)
.flagMetadata(flagMetadata)
.build();
assertEquals(dd, c.getDoubleDetails(key, .4));
assertEquals(dd, c.getDoubleDetails(key, .4, new ImmutableContext()));

View File

@ -0,0 +1,53 @@
package dev.openfeature.sdk;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class FlagMetadataTest {
@Test
@DisplayName("Test metadata payload construction and retrieval")
public void builder_validation() {
// given
FlagMetadata flagMetadata = FlagMetadata.builder()
.addString("string", "string")
.addInteger("integer", 1)
.addLong("long", 1L)
.addFloat("float", 1.5f)
.addDouble("double", Double.MAX_VALUE)
.addBoolean("boolean", Boolean.FALSE)
.build();
// then
assertThat(flagMetadata.getString("string")).isEqualTo("string");
assertThat(flagMetadata.getInteger("integer")).isEqualTo(1);
assertThat(flagMetadata.getLong("long")).isEqualTo(1L);
assertThat(flagMetadata.getFloat("float")).isEqualTo(1.5f);
assertThat(flagMetadata.getDouble("double")).isEqualTo(Double.MAX_VALUE);
assertThat(flagMetadata.getBoolean("boolean")).isEqualTo(Boolean.FALSE);
}
@Test
@DisplayName("Value type mismatch returns a null")
public void value_type_validation() {
// given
FlagMetadata flagMetadata = FlagMetadata.builder()
.addString("string", "string")
.build();
// then
assertThat(flagMetadata.getBoolean("string")).isNull();
}
@Test
@DisplayName("A null is returned if key does not exist")
public void notfound_error_validation() {
// given
FlagMetadata flagMetadata = FlagMetadata.builder().build();
// then
assertThat(flagMetadata.getBoolean("string")).isNull();
}
}