diff --git a/pom.xml b/pom.xml
index ada93b719..0a4dbfbfe 100644
--- a/pom.xml
+++ b/pom.xml
@@ -168,6 +168,31 @@
test
+
+ com.fasterxml.jackson.core
+ jackson-core
+ test
+
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ test
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ test
+
+
+
+ dev.cel
+ cel
+ 0.10.1
+ test
+
+
@@ -191,6 +216,14 @@
+
+ com.fasterxml.jackson
+ jackson-bom
+ 2.16.1
+ pom
+ import
+
+
io.cucumber
cucumber-bom
diff --git a/spec b/spec
index d4a9a9109..e33a15e92 160000
--- a/spec
+++ b/spec
@@ -1 +1 @@
-Subproject commit d4a9a910946eded57cf82d6fd4921785a5e64c2b
+Subproject commit e33a15e92bd0e45f0de087e7e55ee7e87f952c29
diff --git a/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java b/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java
index f2dc6b495..4422dc51f 100644
--- a/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java
+++ b/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java
@@ -20,4 +20,5 @@ public class Flag {
private String defaultVariant;
private ContextEvaluator contextEvaluator;
private ImmutableMetadata flagMetadata;
+ private boolean disabled;
}
diff --git a/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java
index 3be1b6316..1773ae8a8 100644
--- a/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java
+++ b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java
@@ -97,36 +97,37 @@ public void updateFlag(String flagKey, Flag> newFlag) {
@Override
public ProviderEvaluation getBooleanEvaluation(
String key, Boolean defaultValue, EvaluationContext evaluationContext) {
- return getEvaluation(key, evaluationContext, Boolean.class);
+ return getEvaluation(key, defaultValue, evaluationContext, Boolean.class);
}
@Override
public ProviderEvaluation getStringEvaluation(
String key, String defaultValue, EvaluationContext evaluationContext) {
- return getEvaluation(key, evaluationContext, String.class);
+ return getEvaluation(key, defaultValue, evaluationContext, String.class);
}
@Override
public ProviderEvaluation getIntegerEvaluation(
String key, Integer defaultValue, EvaluationContext evaluationContext) {
- return getEvaluation(key, evaluationContext, Integer.class);
+ return getEvaluation(key, defaultValue, evaluationContext, Integer.class);
}
@Override
public ProviderEvaluation getDoubleEvaluation(
String key, Double defaultValue, EvaluationContext evaluationContext) {
- return getEvaluation(key, evaluationContext, Double.class);
+ return getEvaluation(key, defaultValue, evaluationContext, Double.class);
}
@SneakyThrows
@Override
public ProviderEvaluation getObjectEvaluation(
String key, Value defaultValue, EvaluationContext evaluationContext) {
- return getEvaluation(key, evaluationContext, Value.class);
+ return getEvaluation(key, defaultValue, evaluationContext, Value.class);
}
private ProviderEvaluation getEvaluation(
- String key, EvaluationContext evaluationContext, Class> expectedType) throws OpenFeatureError {
+ String key, T defaultValue, EvaluationContext evaluationContext, Class> expectedType)
+ throws OpenFeatureError {
if (!ProviderState.READY.equals(state)) {
if (ProviderState.NOT_READY.equals(state)) {
throw new ProviderNotReadyError("provider not yet initialized");
@@ -138,11 +139,28 @@ private ProviderEvaluation getEvaluation(
}
Flag> flag = flags.get(key);
if (flag == null) {
- throw new FlagNotFoundError("flag " + key + "not found");
+ throw new FlagNotFoundError("flag " + key + " not found");
+ }
+ if (flag.isDisabled()) {
+ return ProviderEvaluation.builder()
+ .reason(Reason.DISABLED.name())
+ .value(defaultValue)
+ .flagMetadata(flag.getFlagMetadata())
+ .build();
}
T value;
+ Reason reason = Reason.STATIC;
if (flag.getContextEvaluator() != null) {
- value = (T) flag.getContextEvaluator().evaluate(flag, evaluationContext);
+ try {
+ value = (T) flag.getContextEvaluator().evaluate(flag, evaluationContext);
+ reason = Reason.TARGETING_MATCH;
+ } catch (Exception e) {
+ value = null;
+ }
+ if (value == null) {
+ value = (T) flag.getVariants().get(flag.getDefaultVariant());
+ reason = Reason.DEFAULT;
+ }
} else if (!expectedType.isInstance(flag.getVariants().get(flag.getDefaultVariant()))) {
throw new TypeMismatchError("flag " + key + "is not of expected type");
} else {
@@ -151,7 +169,7 @@ private ProviderEvaluation getEvaluation(
return ProviderEvaluation.builder()
.value(value)
.variant(flag.getDefaultVariant())
- .reason(Reason.STATIC.toString())
+ .reason(reason.toString())
.flagMetadata(flag.getFlagMetadata())
.build();
}
diff --git a/src/test/java/dev/openfeature/sdk/e2e/EvaluationTest.java b/src/test/java/dev/openfeature/sdk/e2e/GherkinSpecTest.java
similarity index 82%
rename from src/test/java/dev/openfeature/sdk/e2e/EvaluationTest.java
rename to src/test/java/dev/openfeature/sdk/e2e/GherkinSpecTest.java
index b7c834312..89c7161be 100644
--- a/src/test/java/dev/openfeature/sdk/e2e/EvaluationTest.java
+++ b/src/test/java/dev/openfeature/sdk/e2e/GherkinSpecTest.java
@@ -5,6 +5,7 @@
import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME;
import org.junit.platform.suite.api.ConfigurationParameter;
+import org.junit.platform.suite.api.ExcludeTags;
import org.junit.platform.suite.api.IncludeEngines;
import org.junit.platform.suite.api.SelectDirectories;
import org.junit.platform.suite.api.Suite;
@@ -15,4 +16,5 @@
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.sdk.e2e.steps")
@ConfigurationParameter(key = OBJECT_FACTORY_PROPERTY_NAME, value = "io.cucumber.picocontainer.PicoFactory")
-public class EvaluationTest {}
+@ExcludeTags({"deprecated", "reason-codes-cached", "async", "immutability", "evaluation-options"})
+public class GherkinSpecTest {}
diff --git a/src/test/java/dev/openfeature/sdk/e2e/Utils.java b/src/test/java/dev/openfeature/sdk/e2e/Utils.java
index 902ee11d0..565968c1c 100644
--- a/src/test/java/dev/openfeature/sdk/e2e/Utils.java
+++ b/src/test/java/dev/openfeature/sdk/e2e/Utils.java
@@ -1,9 +1,14 @@
package dev.openfeature.sdk.e2e;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import dev.openfeature.sdk.Value;
import java.util.Objects;
public final class Utils {
+ public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
private Utils() {}
public static Object convert(String value, String type) {
@@ -22,6 +27,12 @@ public static Object convert(String value, String type) {
return Double.parseDouble(value);
case "long":
return Long.parseLong(value);
+ case "object":
+ try {
+ return Value.objectToValue(OBJECT_MAPPER.readValue(value, Object.class));
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
}
throw new RuntimeException("Unknown config type: " + type);
}
diff --git a/src/test/java/dev/openfeature/sdk/e2e/steps/ContextSteps.java b/src/test/java/dev/openfeature/sdk/e2e/steps/ContextSteps.java
index ccb78e72a..ce9bb8b5f 100644
--- a/src/test/java/dev/openfeature/sdk/e2e/steps/ContextSteps.java
+++ b/src/test/java/dev/openfeature/sdk/e2e/steps/ContextSteps.java
@@ -8,11 +8,14 @@
import dev.openfeature.sdk.Hook;
import dev.openfeature.sdk.HookContext;
import dev.openfeature.sdk.ImmutableContext;
+import dev.openfeature.sdk.ImmutableStructure;
+import dev.openfeature.sdk.MutableContext;
import dev.openfeature.sdk.OpenFeatureAPI;
import dev.openfeature.sdk.ThreadLocalTransactionContextPropagator;
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.e2e.ContextStoringProvider;
import dev.openfeature.sdk.e2e.State;
+import dev.openfeature.sdk.e2e.Utils;
import io.cucumber.datatable.DataTable;
import io.cucumber.java.en.And;
import io.cucumber.java.en.Given;
@@ -101,4 +104,29 @@ public void contextEntriesForEachLevelFromAPILevelDownToTheLevelWithKeyAndValue(
}
}
}
+
+ @Given("a context containing a key {string} with null value")
+ public void a_context_containing_a_key_with_null_value(String key) {
+ a_context_containing_a_key_with_type_and_with_value(key, "String", null);
+ }
+
+ @Given("a context containing a key {string}, with type {string} and with value {string}")
+ public void a_context_containing_a_key_with_type_and_with_value(String key, String type, String value) {
+ Map map = state.context.asMap();
+ map.put(key, Value.objectToValue(Utils.convert(value, type)));
+ state.context = new MutableContext(state.context.getTargetingKey(), map);
+ }
+
+ @Given("a context containing a targeting key with value {string}")
+ public void a_context_containing_a_targeting_key_with_value(String string) {
+ state.context.setTargetingKey(string);
+ }
+
+ @Given("a context containing a nested property with outer key {string} and inner key {string}, with value {string}")
+ public void a_context_containing_a_nested_property_with_outer_key_and_inner_key_with_value(
+ String outer, String inner, String value) {
+ Map innerMap = new HashMap<>();
+ innerMap.put(inner, new Value(value));
+ state.context.add(outer, new ImmutableStructure(innerMap));
+ }
}
diff --git a/src/test/java/dev/openfeature/sdk/e2e/steps/FlagStepDefinitions.java b/src/test/java/dev/openfeature/sdk/e2e/steps/FlagStepDefinitions.java
index 390e067f3..dccdbf9af 100644
--- a/src/test/java/dev/openfeature/sdk/e2e/steps/FlagStepDefinitions.java
+++ b/src/test/java/dev/openfeature/sdk/e2e/steps/FlagStepDefinitions.java
@@ -2,6 +2,7 @@
import static org.assertj.core.api.Assertions.assertThat;
+import dev.openfeature.sdk.ErrorCode;
import dev.openfeature.sdk.FlagEvaluationDetails;
import dev.openfeature.sdk.ImmutableMetadata;
import dev.openfeature.sdk.Value;
@@ -23,7 +24,7 @@ public FlagStepDefinitions(State state) {
this.state = state;
}
- @Given("a {}-flag with key {string} and a default value {string}")
+ @Given("a {}-flag with key {string} and a fallback value {string}")
public void givenAFlag(String type, String name, String defaultValue) {
state.flag = new Flag(type, name, Utils.convert(defaultValue, type));
}
@@ -60,7 +61,20 @@ public void the_flag_was_evaluated_with_details() {
@Then("the resolved details value should be {string}")
public void the_resolved_details_value_should_be(String value) {
- assertThat(state.evaluation.getValue()).isEqualTo(Utils.convert(value, state.flag.type));
+ Object evaluationValue = state.evaluation.getValue();
+ if (state.flag.type.equalsIgnoreCase("object")) {
+ assertThat(((Value) evaluationValue).asStructure().asObjectMap())
+ .isEqualTo(((Value) Utils.convert(value, state.flag.type))
+ .asStructure()
+ .asObjectMap());
+ } else {
+ assertThat(evaluationValue).isEqualTo(Utils.convert(value, state.flag.type));
+ }
+ }
+
+ @Then("the flag key should be {string}")
+ public void the_flag_key_should_be(String key) {
+ assertThat(state.evaluation.getFlagKey()).isEqualTo(key);
}
@Then("the reason should be {string}")
@@ -73,6 +87,20 @@ public void the_variant_should_be(String variant) {
assertThat(state.evaluation.getVariant()).isEqualTo(variant);
}
+ @Then("the error-code should be {string}")
+ public void the_error_code_should_be(String errorCode) {
+ if (errorCode.isEmpty()) {
+ assertThat(state.evaluation.getErrorCode()).isNull();
+ } else {
+ assertThat(state.evaluation.getErrorCode()).isEqualTo(ErrorCode.valueOf(errorCode));
+ }
+ }
+
+ @Then("the error message should contain {string}")
+ public void the_error_message_should_contain(String messageSubstring) {
+ assertThat(state.evaluation.getErrorMessage()).contains(messageSubstring);
+ }
+
@Then("the resolved metadata value \"{}\" with type \"{}\" should be \"{}\"")
public void theResolvedMetadataValueShouldBe(String key, String type, String value)
throws NoSuchFieldException, IllegalAccessException {
diff --git a/src/test/java/dev/openfeature/sdk/e2e/steps/ProviderSteps.java b/src/test/java/dev/openfeature/sdk/e2e/steps/ProviderSteps.java
index 82cdb2e79..d9dde3c2b 100644
--- a/src/test/java/dev/openfeature/sdk/e2e/steps/ProviderSteps.java
+++ b/src/test/java/dev/openfeature/sdk/e2e/steps/ProviderSteps.java
@@ -1,13 +1,32 @@
package dev.openfeature.sdk.e2e.steps;
import static dev.openfeature.sdk.testutils.TestFlagsUtils.buildFlags;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+import dev.openfeature.sdk.Client;
+import dev.openfeature.sdk.ErrorCode;
+import dev.openfeature.sdk.EventProvider;
+import dev.openfeature.sdk.FeatureProvider;
import dev.openfeature.sdk.OpenFeatureAPI;
+import dev.openfeature.sdk.ProviderEvaluation;
+import dev.openfeature.sdk.ProviderEventDetails;
+import dev.openfeature.sdk.ProviderState;
+import dev.openfeature.sdk.Reason;
+import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.e2e.State;
+import dev.openfeature.sdk.exceptions.FatalError;
import dev.openfeature.sdk.providers.memory.Flag;
import dev.openfeature.sdk.providers.memory.InMemoryProvider;
import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
import java.util.Map;
+import org.awaitility.Awaitility;
public class ProviderSteps {
private final State state;
@@ -16,11 +35,128 @@ public ProviderSteps(State state) {
this.state = state;
}
- @Given("a stable provider")
- public void aStableProvider() {
+ @Given("a {} provider")
+ public void a_provider_with_status(String providerType) throws Exception {
+ // Normalize input to handle both single word and quoted strings
+ String normalizedType =
+ providerType.toLowerCase().replaceAll("[\"\\s]+", " ").trim();
+
+ switch (normalizedType) {
+ case "not ready":
+ setupMockProvider(ErrorCode.PROVIDER_NOT_READY, "Provider in not ready state", ProviderState.NOT_READY);
+ break;
+ case "stable":
+ case "ready":
+ setupStableProvider();
+ break;
+ case "fatal":
+ setupMockProvider(ErrorCode.PROVIDER_FATAL, "Provider in fatal state", ProviderState.FATAL);
+ break;
+ case "error":
+ setupMockProvider(ErrorCode.GENERAL, "Provider in error state", ProviderState.ERROR);
+ break;
+ case "stale":
+ setupMockProvider(null, null, ProviderState.STALE);
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported provider type: " + providerType);
+ }
+ }
+
+ // ===============================
+ // Provider Status Assertion Steps
+ // ===============================
+
+ @Then("the provider status should be {string}")
+ public void the_provider_status_should_be(String expectedStatus) {
+ ProviderState actualStatus = state.client.getProviderState();
+ ProviderState expected = ProviderState.valueOf(expectedStatus);
+ assertThat(actualStatus).isEqualTo(expected);
+ }
+
+ // ===============================
+ // Helper Methods
+ // ===============================
+
+ private void setupStableProvider() throws Exception {
Map> flags = buildFlags();
InMemoryProvider provider = new InMemoryProvider(flags);
OpenFeatureAPI.getInstance().setProviderAndWait(provider);
state.client = OpenFeatureAPI.getInstance().getClient();
}
+
+ private void setupMockProvider(ErrorCode errorCode, String errorMessage, ProviderState providerState)
+ throws Exception {
+ EventProvider mockProvider = spy(EventProvider.class);
+
+ switch (providerState) {
+ case NOT_READY:
+ doAnswer(invocationOnMock -> {
+ while (true) {}
+ })
+ .when(mockProvider)
+ .initialize(any());
+ break;
+ case FATAL:
+ doThrow(new FatalError(errorMessage)).when(mockProvider).initialize(any());
+ break;
+ }
+ // Configure all evaluation methods with a single helper
+ configureMockEvaluations(mockProvider, errorCode, errorMessage);
+
+ OpenFeatureAPI.getInstance().setProvider(providerState.name(), mockProvider);
+ Client client = OpenFeatureAPI.getInstance().getClient(providerState.name());
+ state.client = client;
+
+ ProviderEventDetails details =
+ ProviderEventDetails.builder().errorCode(errorCode).build();
+ switch (providerState) {
+ case FATAL:
+ case ERROR:
+ mockProvider.emitProviderReady(details);
+ mockProvider.emitProviderError(details);
+ break;
+ case STALE:
+ mockProvider.emitProviderReady(details);
+ mockProvider.emitProviderStale(details);
+ break;
+ default:
+ }
+ Awaitility.await().until(() -> {
+ ProviderState providerState1 = client.getProviderState();
+ return providerState1 == providerState;
+ });
+ }
+
+ private void configureMockEvaluations(FeatureProvider mockProvider, ErrorCode errorCode, String errorMessage) {
+ // Configure Boolean evaluation
+ when(mockProvider.getBooleanEvaluation(anyString(), any(Boolean.class), any()))
+ .thenAnswer(invocation -> createProviderEvaluation(invocation.getArgument(1), errorCode, errorMessage));
+
+ // Configure String evaluation
+ when(mockProvider.getStringEvaluation(anyString(), any(String.class), any()))
+ .thenAnswer(invocation -> createProviderEvaluation(invocation.getArgument(1), errorCode, errorMessage));
+
+ // Configure Integer evaluation
+ when(mockProvider.getIntegerEvaluation(anyString(), any(Integer.class), any()))
+ .thenAnswer(invocation -> createProviderEvaluation(invocation.getArgument(1), errorCode, errorMessage));
+
+ // Configure Double evaluation
+ when(mockProvider.getDoubleEvaluation(anyString(), any(Double.class), any()))
+ .thenAnswer(invocation -> createProviderEvaluation(invocation.getArgument(1), errorCode, errorMessage));
+
+ // Configure Object evaluation
+ when(mockProvider.getObjectEvaluation(anyString(), any(Value.class), any()))
+ .thenAnswer(invocation -> createProviderEvaluation(invocation.getArgument(1), errorCode, errorMessage));
+ }
+
+ private ProviderEvaluation createProviderEvaluation(
+ T defaultValue, ErrorCode errorCode, String errorMessage) {
+ return ProviderEvaluation.builder()
+ .value(defaultValue)
+ .errorCode(errorCode)
+ .errorMessage(errorMessage)
+ .reason(Reason.ERROR.toString())
+ .build();
+ }
}
diff --git a/src/test/java/dev/openfeature/sdk/e2e/steps/StepDefinitions.java b/src/test/java/dev/openfeature/sdk/e2e/steps/StepDefinitions.java
index 924c9d59e..c31e9eb7e 100644
--- a/src/test/java/dev/openfeature/sdk/e2e/steps/StepDefinitions.java
+++ b/src/test/java/dev/openfeature/sdk/e2e/steps/StepDefinitions.java
@@ -21,6 +21,7 @@
import java.util.Map;
import lombok.SneakyThrows;
+@Deprecated
public class StepDefinitions {
private static Client client;
diff --git a/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java b/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java
index c1767ff6f..7c45166f9 100644
--- a/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java
+++ b/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java
@@ -1,18 +1,28 @@
package dev.openfeature.sdk.testutils;
-import static dev.openfeature.sdk.Structure.mapToStructure;
+import static dev.openfeature.sdk.e2e.Utils.OBJECT_MAPPER;
-import com.google.common.collect.ImmutableMap;
+import com.fasterxml.jackson.core.StreamReadFeature;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
import dev.openfeature.sdk.ImmutableMetadata;
-import dev.openfeature.sdk.Value;
+import dev.openfeature.sdk.providers.memory.ContextEvaluator;
import dev.openfeature.sdk.providers.memory.Flag;
-import java.util.HashMap;
+import dev.openfeature.sdk.testutils.jackson.ContextEvaluatorDeserializer;
+import dev.openfeature.sdk.testutils.jackson.ImmutableMetadataDeserializer;
+import dev.openfeature.sdk.testutils.jackson.InMemoryFlagMixin;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.Collections;
import java.util.Map;
import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
/**
* Test flags utils.
*/
+@Slf4j
@UtilityClass
public class TestFlagsUtils {
@@ -25,87 +35,37 @@ public class TestFlagsUtils {
public static final String WRONG_FLAG_KEY = "wrong-flag";
public static final String METADATA_FLAG_KEY = "metadata-flag";
+ private static Map> flags;
/**
* Building flags for testing purposes.
*
* @return map of flags
*/
- public static Map> buildFlags() {
- Map> flags = new HashMap<>();
- flags.put(
- BOOLEAN_FLAG_KEY,
- Flag.builder()
- .variant("on", true)
- .variant("off", false)
- .defaultVariant("on")
- .build());
- flags.put(
- STRING_FLAG_KEY,
- Flag.builder()
- .variant("greeting", "hi")
- .variant("parting", "bye")
- .defaultVariant("greeting")
- .build());
- flags.put(
- INT_FLAG_KEY,
- Flag.builder()
- .variant("one", 1)
- .variant("ten", 10)
- .defaultVariant("ten")
- .build());
- flags.put(
- FLOAT_FLAG_KEY,
- Flag.builder()
- .variant("tenth", 0.1)
- .variant("half", 0.5)
- .defaultVariant("half")
- .build());
- flags.put(
- OBJECT_FLAG_KEY,
- Flag.builder()
- .variant("empty", new HashMap<>())
- .variant(
- "template",
- new Value(mapToStructure(ImmutableMap.of(
- "showImages", new Value(true),
- "title", new Value("Check out these pics!"),
- "imagesPerPage", new Value(100)))))
- .defaultVariant("template")
- .build());
- flags.put(
- CONTEXT_AWARE_FLAG_KEY,
- Flag.builder()
- .variant("internal", "INTERNAL")
- .variant("external", "EXTERNAL")
- .defaultVariant("external")
- .contextEvaluator((flag, evaluationContext) -> {
- if (new Value(false).equals(evaluationContext.getValue("customer"))) {
- return (String) flag.getVariants().get("internal");
- } else {
- return (String) flag.getVariants().get(flag.getDefaultVariant());
- }
- })
- .build());
- flags.put(
- WRONG_FLAG_KEY,
- Flag.builder()
- .variant("one", "uno")
- .variant("two", "dos")
- .defaultVariant("one")
- .build());
- flags.put(
- METADATA_FLAG_KEY,
- Flag.builder()
- .variant("on", true)
- .variant("off", false)
- .defaultVariant("on")
- .flagMetadata(ImmutableMetadata.builder()
- .addString("string", "1.0.2")
- .addInteger("integer", 2)
- .addBoolean("boolean", true)
- .addDouble("float", 0.1d)
- .build())
- .build());
+ public static synchronized Map> buildFlags() {
+ if (flags == null) {
+ ObjectMapper objectMapper = OBJECT_MAPPER;
+ objectMapper.configure(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION.mappedFeature(), true);
+ objectMapper.addMixIn(Flag.class, InMemoryFlagMixin.class);
+ objectMapper.addMixIn(Flag.FlagBuilder.class, InMemoryFlagMixin.FlagBuilderMixin.class);
+
+ SimpleModule module = new SimpleModule();
+ module.addDeserializer(ImmutableMetadata.class, new ImmutableMetadataDeserializer());
+ module.addDeserializer(ContextEvaluator.class, new ContextEvaluatorDeserializer());
+ objectMapper.registerModule(module);
+
+ Map> flagsJson;
+ try {
+ flagsJson = objectMapper.readValue(
+ Paths.get("spec/specification/assets/gherkin/test-flags.json")
+ .toFile(),
+ new TypeReference<>() {});
+
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ flags = Collections.unmodifiableMap(flagsJson);
+ }
+
return flags;
}
}
diff --git a/src/test/java/dev/openfeature/sdk/testutils/jackson/CelContextEvaluator.java b/src/test/java/dev/openfeature/sdk/testutils/jackson/CelContextEvaluator.java
new file mode 100644
index 000000000..6ca3875ef
--- /dev/null
+++ b/src/test/java/dev/openfeature/sdk/testutils/jackson/CelContextEvaluator.java
@@ -0,0 +1,61 @@
+package dev.openfeature.sdk.testutils.jackson;
+
+import dev.cel.common.types.SimpleType;
+import dev.cel.compiler.CelCompiler;
+import dev.cel.compiler.CelCompilerFactory;
+import dev.cel.runtime.CelRuntime;
+import dev.cel.runtime.CelRuntimeFactory;
+import dev.openfeature.sdk.EvaluationContext;
+import dev.openfeature.sdk.providers.memory.ContextEvaluator;
+import dev.openfeature.sdk.providers.memory.Flag;
+import java.util.HashMap;
+import java.util.Map;
+
+public class CelContextEvaluator implements ContextEvaluator {
+ private final CelRuntime.Program program;
+
+ public CelContextEvaluator(String expression) {
+ try {
+ CelRuntime celRuntime =
+ CelRuntimeFactory.standardCelRuntimeBuilder().build();
+ CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder()
+ .addVar("customer", SimpleType.BOOL)
+ .addVar("email", SimpleType.STRING)
+ .addVar("age", SimpleType.INT)
+ .addVar("dummy", SimpleType.STRING)
+ .setResultType(SimpleType.STRING)
+ // Add other variables you expect
+ .build();
+
+ var ast = celCompiler.compile(expression).getAst();
+ this.program = celRuntime.createProgram(ast);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to compile CEL expression: " + expression, e);
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public T evaluate(Flag flag, EvaluationContext evaluationContext) {
+ try {
+ Map objectMap = new HashMap<>();
+ // Provide defaults for all declared variables to prevent runtime errors.
+ objectMap.put("email", "");
+ objectMap.put("customer", true);
+ objectMap.put("age", 0);
+ objectMap.put("dummy", "");
+
+ if (evaluationContext != null) {
+ // Evaluate with context, overriding defaults.
+ objectMap.putAll(evaluationContext.asObjectMap());
+ }
+
+ Object result = program.eval(objectMap);
+
+ String stringResult = (String) result;
+ return (T) flag.getVariants().get(stringResult);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/test/java/dev/openfeature/sdk/testutils/jackson/ContextEvaluatorDeserializer.java b/src/test/java/dev/openfeature/sdk/testutils/jackson/ContextEvaluatorDeserializer.java
new file mode 100644
index 000000000..e348fc8c5
--- /dev/null
+++ b/src/test/java/dev/openfeature/sdk/testutils/jackson/ContextEvaluatorDeserializer.java
@@ -0,0 +1,25 @@
+package dev.openfeature.sdk.testutils.jackson;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+import dev.openfeature.sdk.providers.memory.ContextEvaluator;
+import java.io.IOException;
+
+public class ContextEvaluatorDeserializer extends JsonDeserializer> {
+ @Override
+ public ContextEvaluator> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+ JsonNode node = p.getCodec().readTree(p);
+
+ if (node.isTextual()) {
+ return new CelContextEvaluator<>(node.asText());
+ }
+
+ if (node.isObject() && node.has("expression")) {
+ return new CelContextEvaluator<>(node.get("expression").asText());
+ }
+
+ return null;
+ }
+}
diff --git a/src/test/java/dev/openfeature/sdk/testutils/jackson/ImmutableMetadataDeserializer.java b/src/test/java/dev/openfeature/sdk/testutils/jackson/ImmutableMetadataDeserializer.java
new file mode 100644
index 000000000..09f7c6f24
--- /dev/null
+++ b/src/test/java/dev/openfeature/sdk/testutils/jackson/ImmutableMetadataDeserializer.java
@@ -0,0 +1,41 @@
+package dev.openfeature.sdk.testutils.jackson;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import dev.openfeature.sdk.ImmutableMetadata;
+import java.io.IOException;
+import java.util.Map;
+
+public class ImmutableMetadataDeserializer extends JsonDeserializer {
+ @Override
+ public ImmutableMetadata deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+ Map properties = p.readValueAs(new TypeReference