diff --git a/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java b/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java index 67ee853d1..24e6a3985 100644 --- a/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java +++ b/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java @@ -7,23 +7,27 @@ /** * Contains information about how the evaluation happened, including any resolved values. + * * @param the type of the flag being evaluated. */ -@Data @Builder +@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 +37,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..e9b6ccb6b --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/FlagMetadata.java @@ -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 metadata; + + private FlagMetadata(Map 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 getValue(final String key, final Class 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 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); + } + + } +} 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 e508bdd07..57f0c0454 100644 --- a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java @@ -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.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 @@ void getApiInstance() { .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 @@ void getApiInstance() { .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 @@ void getApiInstance() { 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())); @@ -175,6 +180,7 @@ void getApiInstance() { 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..b52eb6de7 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/FlagMetadataTest.java @@ -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(); + } +}