From 317ace4d99cdf94d6a7bfde77dc40dd7e9565693 Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Wed, 31 May 2023 11:34:01 -0700 Subject: [PATCH] flag metadata Signed-off-by: Kavindu Dodanduwa --- .../sdk/FlagEvaluationDetails.java | 11 +- .../dev/openfeature/sdk/FlagMetadata.java | 170 ++++++++++++++++++ .../openfeature/sdk/ProviderEvaluation.java | 2 + .../openfeature/sdk/DoSomethingProvider.java | 17 +- .../sdk/FlagEvaluationSpecTest.java | 39 ++-- .../dev/openfeature/sdk/FlagMetadataTest.java | 50 ++++++ 6 files changed, 261 insertions(+), 28 deletions(-) create mode 100644 src/main/java/dev/openfeature/sdk/FlagMetadata.java create mode 100644 src/test/java/dev/openfeature/sdk/FlagMetadataTest.java diff --git a/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java b/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java index 67ee853d1..7fccaff04 100644 --- a/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java +++ b/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java @@ -7,23 +7,25 @@ /** * Contains information about how the evaluation happened, including any resolved values. + * * @param the type of the flag being evaluated. */ -@Data @Builder -public class FlagEvaluationDetails implements BaseEvaluation { +@Data @Builder public class FlagEvaluationDetails implements BaseEvaluation { + 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 type of flag being returned + * @param flagKey key for the flag being evaluated + * @param type of flag being returned * @return detail payload */ public static FlagEvaluationDetails from(ProviderEvaluation providerEval, String flagKey) { @@ -33,6 +35,7 @@ public static FlagEvaluationDetails from(ProviderEvaluation providerEv .variant(providerEval.getVariant()) .reason(providerEval.getReason()) .errorCode(providerEval.getErrorCode()) + .flagMetadata(providerEval.getFlagMetadata()) .build(); } } diff --git a/src/main/java/dev/openfeature/sdk/FlagMetadata.java b/src/main/java/dev/openfeature/sdk/FlagMetadata.java new file mode 100644 index 000000000..9a84f03bc --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/FlagMetadata.java @@ -0,0 +1,170 @@ +package dev.openfeature.sdk; + +import dev.openfeature.sdk.exceptions.GeneralError; +import dev.openfeature.sdk.exceptions.ParseError; + +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. + */ +public class FlagMetadata { + private final Map metadata; + + private FlagMetadata(Map metadata) { + this.metadata = metadata; + } + + /** + * Retrieve a {@link String} value for the given key. If a value is not found, {@link GeneralError} will be thrown. + * If value exist but of another type, {@link ParseError} will be thrown. + * + * @param key flag metadata key to retrieve + */ + public String getString(final String key) { + return getValue(key, String.class); + } + + /** + * Retrieve an {@link Integer} value for the given key. + * If a value is not found, {@link GeneralError} will be thrown. + * If value exist but of another type, {@link ParseError} will be thrown. + * + * @param key flag metadata key to retrieve + */ + public Integer getInteger(final String key) { + return getValue(key, Integer.class); + } + + /** + * Retrieve an {@link Float} value for the given key. If a value is not found, {@link GeneralError} will be thrown. + * If value exist but of another type, {@link ParseError} will be thrown. + * + * @param key flag metadata key to retrieve + */ + public Float getFloat(final String key) { + return getValue(key, Float.class); + } + + /** + * Retrieve an {@link Double} value for the given key. + * If a value is not found, {@link GeneralError} will be thrown. + * If value exist but of another type, {@link ParseError} will be thrown. + * + * @param key flag metadata key to retrieve + */ + public Double getDouble(final String key) { + return getValue(key, Double.class); + } + + /** + * Retrieve an {@link Boolean} value for the given key. + * If a value is not found, {@link GeneralError} will be thrown. + * If value exist but of another type, {@link ParseError} will be thrown. + * + * @param key flag metadata key to retrieve + */ + public Boolean getBoolean(final String key) { + return getValue(key, Boolean.class); + } + + private T getValue(final String key, final Class type) { + final Object o = metadata.get(key); + + if (o == null) { + throw new GeneralError("key " + key + " does not exist in metadata"); + } + + try { + return type.cast(o); + } catch (ClassCastException e) { + throw new ParseError( + "wrong type for key " + key + + ". Expected" + type.getSimpleName() + "but got " + o.getClass().getSimpleName(), e); + } + } + + + /** + * 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 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 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); + } + + } +} diff --git a/src/main/java/dev/openfeature/sdk/ProviderEvaluation.java b/src/main/java/dev/openfeature/sdk/ProviderEvaluation.java index 9ba1ab9a1..ffa5c3ccc 100644 --- a/src/main/java/dev/openfeature/sdk/ProviderEvaluation.java +++ b/src/main/java/dev/openfeature/sdk/ProviderEvaluation.java @@ -13,4 +13,6 @@ public class ProviderEvaluation implements BaseEvaluation { @Nullable private String reason; ErrorCode errorCode; @Nullable private String errorMessage; + @Builder.Default + private FlagMetadata flagMetadata = FlagMetadata.builder().build(); } diff --git a/src/test/java/dev/openfeature/sdk/DoSomethingProvider.java b/src/test/java/dev/openfeature/sdk/DoSomethingProvider.java index d87fa3749..26e6737bb 100644 --- a/src/test/java/dev/openfeature/sdk/DoSomethingProvider.java +++ b/src/test/java/dev/openfeature/sdk/DoSomethingProvider.java @@ -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 Metadata getMetadata() { public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { savedContext = ctx; return ProviderEvaluation.builder() - .value(!defaultValue).build(); + .value(!defaultValue) + .flagMetadata(flagMetadata) + .build(); } @Override public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { return ProviderEvaluation.builder() .value(new StringBuilder(defaultValue).reverse().toString()) + .flagMetadata(flagMetadata) .build(); } @@ -33,6 +39,7 @@ public ProviderEvaluation getIntegerEvaluation(String key, Integer defa savedContext = ctx; return ProviderEvaluation.builder() .value(defaultValue * 100) + .flagMetadata(flagMetadata) .build(); } @@ -41,6 +48,7 @@ public ProviderEvaluation getDoubleEvaluation(String key, Double default savedContext = ctx; return ProviderEvaluation.builder() .value(defaultValue * 100) + .flagMetadata(flagMetadata) .build(); } @@ -49,6 +57,7 @@ public ProviderEvaluation getObjectEvaluation(String key, Value defaultVa savedContext = invocationContext; return ProviderEvaluation.builder() .value(null) + .flagMetadata(flagMetadata) .build(); } } diff --git a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java index e246c6d61..10cfbd33c 100644 --- a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java @@ -1,7 +1,20 @@ package dev.openfeature.sdk; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.InstanceOfAssertFactories.optional; +import dev.openfeature.sdk.exceptions.FlagNotFoundError; +import dev.openfeature.sdk.fixtures.HookFixtures; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.simplify4u.slf4jmock.LoggerMock; +import org.slf4j.Logger; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static dev.openfeature.sdk.DoSomethingProvider.flagMetadata; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; @@ -12,24 +25,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - - -import dev.openfeature.sdk.exceptions.FlagNotFoundError; -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; - class FlagEvaluationSpecTest implements HookFixtures { private Logger logger; @@ -154,6 +149,7 @@ private Client _client() { .flagKey(key) .value(false) .variant(null) + .flagMetadata(flagMetadata) .build(); assertEquals(bd, c.getBooleanDetails(key, true)); assertEquals(bd, c.getBooleanDetails(key, true, new ImmutableContext())); @@ -163,6 +159,7 @@ private Client _client() { .flagKey(key) .value("tset") .variant(null) + .flagMetadata(flagMetadata) .build(); assertEquals(sd, c.getStringDetails(key, "test")); assertEquals(sd, c.getStringDetails(key, "test", new ImmutableContext())); @@ -171,6 +168,7 @@ private Client _client() { FlagEvaluationDetails id = FlagEvaluationDetails.builder() .flagKey(key) .value(400) + .flagMetadata(flagMetadata) .build(); assertEquals(id, c.getIntegerDetails(key, 4)); assertEquals(id, c.getIntegerDetails(key, 4, new ImmutableContext())); @@ -179,6 +177,7 @@ private Client _client() { FlagEvaluationDetails dd = FlagEvaluationDetails.builder() .flagKey(key) .value(40.0) + .flagMetadata(flagMetadata) .build(); assertEquals(dd, c.getDoubleDetails(key, .4)); assertEquals(dd, c.getDoubleDetails(key, .4, new ImmutableContext())); diff --git a/src/test/java/dev/openfeature/sdk/FlagMetadataTest.java b/src/test/java/dev/openfeature/sdk/FlagMetadataTest.java new file mode 100644 index 000000000..97ec5a4c3 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/FlagMetadataTest.java @@ -0,0 +1,50 @@ +package dev.openfeature.sdk; + +import dev.openfeature.sdk.exceptions.GeneralError; +import dev.openfeature.sdk.exceptions.ParseError; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class FlagMetadataTest { + + @Test + public void builder_validation() { + // given + FlagMetadata flagMetadata = FlagMetadata.builder() + .addString("string", "string") + .addInteger("integer", 1) + .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.getFloat("float")).isEqualTo(1.5f); + assertThat(flagMetadata.getDouble("double")).isEqualTo(Double.MAX_VALUE); + assertThat(flagMetadata.getBoolean("boolean")).isEqualTo(Boolean.FALSE); + } + + @Test + public void parse_error_validation() { + // given + FlagMetadata flagMetadata = FlagMetadata.builder() + .addString("string", "string") + .build(); + + // then + assertThatThrownBy(() -> flagMetadata.getBoolean("string")).isInstanceOf(ParseError.class); + } + + @Test + public void notfound_error_validation() { + // given + FlagMetadata flagMetadata = FlagMetadata.builder().build(); + + // then + assertThatThrownBy(() -> flagMetadata.getBoolean("string")).isInstanceOf(GeneralError.class); + } +} \ No newline at end of file