diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java index 976870a97..84d4745ec 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java @@ -18,7 +18,6 @@ class FlagdProviderSyncResources { @Setter private volatile ProviderEvent previousEvent; - @Setter private volatile boolean isFatal; private volatile ProviderEventDetails fatalProviderEventDetails; diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageStateChange.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageStateChange.java index 9875cbe9c..71f83bfb1 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageStateChange.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageStateChange.java @@ -26,8 +26,10 @@ public class StorageStateChange { */ public StorageStateChange(StorageState storageState, List changedFlagsKeys, Structure syncMetadata) { this.storageState = storageState; - this.changedFlagsKeys = Collections.unmodifiableList(changedFlagsKeys); - this.syncMetadata = new ImmutableStructure(syncMetadata.asMap()); + this.changedFlagsKeys = + changedFlagsKeys != null ? Collections.unmodifiableList(changedFlagsKeys) : Collections.emptyList(); + this.syncMetadata = + syncMetadata != null ? new ImmutableStructure(syncMetadata.asMap()) : new ImmutableStructure(); } /** @@ -37,9 +39,7 @@ public StorageStateChange(StorageState storageState, List changedFlagsKe * @param changedFlagsKeys flags changed */ public StorageStateChange(StorageState storageState, List changedFlagsKeys) { - this.storageState = storageState; - this.changedFlagsKeys = Collections.unmodifiableList(changedFlagsKeys); - this.syncMetadata = new ImmutableStructure(); + this(storageState, changedFlagsKeys, null); } /** diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java index f8c4d0977..c129f3af5 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java @@ -163,7 +163,7 @@ void callingFatalError_wakesUpWaitingThreadWithException() { }, () -> { startTime.set(System.currentTimeMillis()); - flagdProviderSyncResources.setFatal(true); + flagdProviderSyncResources.fatalError(null); }); Assertions.assertTrue( @@ -193,7 +193,6 @@ void concurrentInitializeAndShutdownShutsDownWork() { while (interleavings.hasNext()) { Runner.runParallel( () -> flagdProviderSyncResources.initialize(), () -> flagdProviderSyncResources.shutdown()); - Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); Assertions.assertTrue(flagdProviderSyncResources.isShutDown()); } } @@ -208,8 +207,7 @@ void concurrentInitializeAndShutdownAndSetFatalShutsDownWork() { Runner.runParallel( () -> flagdProviderSyncResources.initialize(), () -> flagdProviderSyncResources.shutdown(), - () -> flagdProviderSyncResources.setFatal(true)); - Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); + () -> flagdProviderSyncResources.fatalError(null)); Assertions.assertTrue(flagdProviderSyncResources.isShutDown()); Assertions.assertTrue(flagdProviderSyncResources.isFatal()); } @@ -222,7 +220,8 @@ void concurrentInitializeAndSetFatalShutsDownWork() { try (var interleavings = new AllInterleavings("concurrent initialize() and fatal() calls work")) { while (interleavings.hasNext()) { Runner.runParallel( - () -> flagdProviderSyncResources.initialize(), () -> flagdProviderSyncResources.setFatal(true)); + () -> flagdProviderSyncResources.initialize(), + () -> flagdProviderSyncResources.fatalError(null)); Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); Assertions.assertTrue(flagdProviderSyncResources.isFatal()); } @@ -254,7 +253,7 @@ void waitForInitializationAfterCallingShutdown_returnsInstantly() { @Timeout(2) @Test void waitForInitializationAfterCallingFatal_returnsInstantly() { - flagdProviderSyncResources.setFatal(true); + flagdProviderSyncResources.fatalError(null); long start = System.currentTimeMillis(); Assertions.assertThrows(FatalError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); long end = System.currentTimeMillis(); @@ -265,7 +264,7 @@ void waitForInitializationAfterCallingFatal_returnsInstantly() { @Timeout(2) @Test void initializeAfterFatalReturnsFalse() { - flagdProviderSyncResources.setFatal(true); + flagdProviderSyncResources.fatalError(null); Assertions.assertFalse(flagdProviderSyncResources.initialize()); } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFileTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFileTest.java index 5901fe08e..21dea4589 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFileTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFileTest.java @@ -36,7 +36,7 @@ "events", "contextEnrichment", "fractional-v1", - "fractional-v3", + "fractional-v2", "deprecated" }) @Testcontainers diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java index 797756211..bf31c3bc0 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java @@ -28,7 +28,7 @@ @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.contrib.providers.flagd.e2e.steps") @ConfigurationParameter(key = OBJECT_FACTORY_PROPERTY_NAME, value = "io.cucumber.picocontainer.PicoFactory") @IncludeTags("in-process") -@ExcludeTags({"unixsocket", "fractional-v1", "fractional-v3", "deprecated"}) +@ExcludeTags({"unixsocket", "fractional-v1", "fractional-v2", "deprecated"}) @Testcontainers public class RunInProcessTest { diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ContextSteps.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ContextSteps.java index caa66ee5c..b92c994b7 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ContextSteps.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ContextSteps.java @@ -4,8 +4,11 @@ import dev.openfeature.sdk.ImmutableStructure; import dev.openfeature.sdk.MutableContext; import dev.openfeature.sdk.Value; +import io.cucumber.datatable.DataTable; import io.cucumber.java.en.Given; +import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; public class ContextSteps extends AbstractSteps { @@ -14,32 +17,24 @@ public ContextSteps(State state) { super(state); } - @Given("a context containing a key {string}, with type {string} and with value {string}") + @Given("^a context containing a key \"([^\"]*)\", with type \"([^\"]*)\" and with value \"(.*)\"$") public void a_context_containing_a_key_with_type_and_with_value(String key, String type, String value) - throws ClassNotFoundException, InstantiationException { - Map map = state.context.asMap(); - Value typedValue; - switch (type) { - case "Integer": - long longVal = Long.parseLong(value); - if (longVal >= Integer.MIN_VALUE && longVal <= Integer.MAX_VALUE) { - typedValue = new Value((int) longVal); - } else { - // value exceeds int range; store as string to preserve precision - typedValue = new Value(value); - } - break; - case "Float": - typedValue = new Value(Double.parseDouble(value)); - break; - case "Boolean": - typedValue = new Value(Boolean.parseBoolean(value)); - break; - default: - typedValue = new Value(value); - break; + throws ClassNotFoundException, IOException { + Map map = new HashMap<>(state.context.asMap()); + map.put(key, Value.objectToValue(Utils.convert(value, type))); + state.context = new MutableContext(state.context.getTargetingKey(), map); + } + + @Given("a context with the following keys:") + public void a_context_with_the_following_keys(DataTable dataTable) throws ClassNotFoundException, IOException { + List> rows = dataTable.asMaps(String.class, String.class); + Map map = new HashMap<>(state.context.asMap()); + for (Map row : rows) { + String key = row.get("key"); + String type = row.get("type"); + String value = row.get("value"); + map.put(key, Value.objectToValue(Utils.convert(value, type))); } - map.put(key, typedValue); state.context = new MutableContext(state.context.getTargetingKey(), map); } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/Utils.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/Utils.java index 626105ce4..e53a83d29 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/Utils.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/Utils.java @@ -15,14 +15,19 @@ public final class Utils { private Utils() {} public static Object convert(String value, String type) throws ClassNotFoundException, IOException { - if (Objects.equals(value, "null")) return null; + if ("Null".equals(type)) return null; + if (Objects.equals(value, "null") && !"String".equals(type)) return null; switch (type) { case "Boolean": return Boolean.parseBoolean(value); case "String": return value; case "Integer": - return Integer.parseInt(value); + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return Long.parseLong(value); + } case "Float": return Double.parseDouble(value); case "Long": diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/config/ConfigSteps.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/config/ConfigSteps.java index db5ee14f1..7418fe1b0 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/config/ConfigSteps.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/config/ConfigSteps.java @@ -66,7 +66,7 @@ public void we_have_an_option_of_type_with_value(String option, String type, Str return; } - Object converted = Utils.convert(value, type); + Object converted = ("null".equals(value) && "String".equals(type)) ? null : Utils.convert(value, type); Method method = Arrays.stream(state.builder.getClass().getMethods()) .filter(method1 -> method1.getName().equals(mapOptionNames(option))) .findFirst() @@ -87,7 +87,7 @@ public void we_have_an_environment_variable_with_value(String varName, String va @Then("the option {string} of type {string} should have the value {string}") public void the_option_of_type_should_have_the_value(String option, String type, String value) throws Throwable { - Object convert = Utils.convert(value, type); + Object convert = ("null".equals(value) && "String".equals(type)) ? null : Utils.convert(value, type); if (IGNORED_FOR_NOW.contains(option)) { log.error("option '{}' is not supported", option); diff --git a/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/ContextSteps.java b/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/ContextSteps.java index 245c0d174..b87555204 100644 --- a/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/ContextSteps.java +++ b/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/ContextSteps.java @@ -4,9 +4,11 @@ import dev.openfeature.sdk.MutableContext; import dev.openfeature.sdk.Value; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.cucumber.datatable.DataTable; import io.cucumber.java.en.Given; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -25,13 +27,27 @@ public ContextSteps(EvaluatorState state) { } /** Adds a typed key/value pair to the evaluation context. */ - @Given("a context containing a key {string}, with type {string} and with value {string}") + @Given("^a context containing a key \"([^\"]*)\", with type \"([^\"]*)\" and with value \"(.*)\"$") public void contextKeyWithTypeAndValue(String key, String type, String value) throws IOException { Map map = new HashMap<>(state.context.asMap()); map.put(key, Value.objectToValue(EvaluatorUtils.convert(value, type))); state.context = new MutableContext(state.context.getTargetingKey(), map); } + /** Adds multiple context keys from a data table. */ + @Given("a context with the following keys:") + public void contextWithFollowingKeys(DataTable dataTable) throws IOException { + List> rows = dataTable.asMaps(String.class, String.class); + Map map = new HashMap<>(state.context.asMap()); + for (Map row : rows) { + String key = row.get("key"); + String type = row.get("type"); + String value = row.get("value"); + map.put(key, Value.objectToValue(EvaluatorUtils.convert(value, type))); + } + state.context = new MutableContext(state.context.getTargetingKey(), map); + } + /** Sets the targeting key on the evaluation context. */ @Given("a context containing a targeting key with value {string}") public void contextTargetingKey(String targetingKey) { diff --git a/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/EvaluationSteps.java b/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/EvaluationSteps.java index c69ba5502..b3905d8e6 100644 --- a/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/EvaluationSteps.java +++ b/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/EvaluationSteps.java @@ -76,6 +76,7 @@ public void flagEvaluatedWithDetails() { state.evaluation = dev.openfeature.sdk.ProviderEvaluation.builder() .errorCode(ErrorCode.TYPE_MISMATCH) .errorMessage(e.getMessage()) + .reason("ERROR") .build(); } catch (dev.openfeature.sdk.exceptions.OpenFeatureError e) { // Mirror the OpenFeature SDK client behaviour: on any provider error, return the @@ -84,6 +85,7 @@ public void flagEvaluatedWithDetails() { .value(state.defaultValue) .errorCode(e.getErrorCode()) .errorMessage(e.getMessage()) + .reason("ERROR") .build(); } } @@ -94,7 +96,11 @@ public void resolvedValueEquals(String value) throws IOException { if (state.evaluation.getErrorCode() != null) { log.warning("Evaluation error: " + state.evaluation.getErrorMessage()); } - assertThat(state.evaluation.getValue()).isEqualTo(EvaluatorUtils.convert(value, state.flagType)); + Object actualValue = state.evaluation.getValue(); + if (actualValue == null && state.evaluation.getErrorCode() != null) { + actualValue = state.defaultValue; + } + assertThat(actualValue).isEqualTo(EvaluatorUtils.convert(value, state.flagType)); } /** Asserts the evaluation reason matches the expected value. */ diff --git a/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/EvaluatorUtils.java b/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/EvaluatorUtils.java index 646115d19..b8ecbbfba 100644 --- a/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/EvaluatorUtils.java +++ b/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/EvaluatorUtils.java @@ -22,7 +22,10 @@ private EvaluatorUtils() {} * @return the converted value, or {@code null} if {@code value} is "null" or empty for Object */ public static Object convert(String value, String type) throws IOException { - if (value == null || value.equals("null")) { + if ("Null".equals(type)) { + return null; + } + if (value == null || (value.equals("null") && !"String".equals(type))) { return null; } switch (type) { diff --git a/tools/flagd-api-testkit/test-harness b/tools/flagd-api-testkit/test-harness index 7575a1dc4..82ba89ec8 160000 --- a/tools/flagd-api-testkit/test-harness +++ b/tools/flagd-api-testkit/test-harness @@ -1 +1 @@ -Subproject commit 7575a1dc45f176e57e809748a712a555e9aa5d11 +Subproject commit 82ba89ec8db498fa51368e558e4d87642d9e93c4 diff --git a/tools/flagd-core/pom.xml b/tools/flagd-core/pom.xml index 71315ae71..6709c831d 100644 --- a/tools/flagd-core/pom.xml +++ b/tools/flagd-core/pom.xml @@ -40,6 +40,12 @@ [1.0.0,2.0.0) + + com.upokecenter + cbor + 4.5.6 + + com.fasterxml.jackson.core jackson-databind diff --git a/tools/flagd-core/src/main/java/dev/openfeature/contrib/tools/flagd/core/targeting/Fractional.java b/tools/flagd-core/src/main/java/dev/openfeature/contrib/tools/flagd/core/targeting/Fractional.java index b0800ed75..38ff0749d 100644 --- a/tools/flagd-core/src/main/java/dev/openfeature/contrib/tools/flagd/core/targeting/Fractional.java +++ b/tools/flagd-core/src/main/java/dev/openfeature/contrib/tools/flagd/core/targeting/Fractional.java @@ -1,10 +1,14 @@ package dev.openfeature.contrib.tools.flagd.core.targeting; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.upokecenter.cbor.CBORObject; import io.github.jamsesso.jsonlogic.JsonLogicException; import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluationException; import io.github.jamsesso.jsonlogic.evaluator.expressions.PreEvaluatedArgumentsExpression; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -16,6 +20,7 @@ @Slf4j class Fractional implements PreEvaluatedArgumentsExpression { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); static final int MAX_WEIGHT = Integer.MAX_VALUE; @Override @@ -32,7 +37,7 @@ public Object evaluate(List arguments, Object data, String jsonPath) throws Json final Operator.FlagProperties properties = new Operator.FlagProperties(data); - final String bucketBy; + final Object bucketBy; final List distributions; // json-logic pre-evaluation flattens a single-entry fractional @@ -42,11 +47,14 @@ public Object evaluate(List arguments, Object data, String jsonPath) throws Json log.debug("Missing fallback targeting key"); return null; } - bucketBy = properties.getFlagKey() + properties.getTargetingKey(); + bucketBy = java.util.Arrays.asList(properties.getFlagKey(), properties.getTargetingKey()); distributions = List.of(arguments); - } else if (arguments.get(0) instanceof String) { - // first arg is a String, use for bucketing - bucketBy = (String) arguments.get(0); + } else if (arguments.get(0) instanceof String + || arguments.get(0) instanceof Boolean + || arguments.get(0) instanceof Number + || arguments.get(0) instanceof java.util.Map) { + // first arg is a primitive or Map, use for bucketing + bucketBy = arguments.get(0); distributions = arguments.subList(1, arguments.size()); } else { // fallback to targeting key if present @@ -54,22 +62,32 @@ public Object evaluate(List arguments, Object data, String jsonPath) throws Json log.debug("Missing fallback targeting key"); return null; } - bucketBy = properties.getFlagKey() + properties.getTargetingKey(); - distributions = arguments; + + bucketBy = java.util.Arrays.asList(properties.getFlagKey(), properties.getTargetingKey()); + + if (arguments.get(0) == null) { + // arguments.get(0) resolved to null, skip it in distributions + distributions = arguments.subList(1, arguments.size()); + } else { + distributions = arguments; + } } final List propertyList = new ArrayList<>(); long totalWeight = 0; - try { - for (Object dist : distributions) { + for (Object dist : distributions) { + try { FractionProperty fractionProperty = new FractionProperty(dist, jsonPath); propertyList.add(fractionProperty); totalWeight += fractionProperty.getWeight(); + } catch (JsonLogicException e) { + if ("Property is not an array".equals(e.getMessage())) { + throw new JsonLogicEvaluationException( + "Error parsing fractional targeting rule: " + e.getMessage(), jsonPath); + } + return null; } - } catch (JsonLogicException e) { - log.debug("Error parsing fractional targeting rule", e); - return null; } if (totalWeight > MAX_WEIGHT) { @@ -87,12 +105,20 @@ public Object evaluate(List arguments, Object data, String jsonPath) throws Json } private static Object distributeValue( - final String hashKey, + final Object hashKey, final List propertyList, final int totalWeight, final String jsonPath) throws JsonLogicEvaluationException { - byte[] bytes = hashKey.getBytes(StandardCharsets.UTF_8); + byte[] bytes; + try { + JsonNode node = OBJECT_MAPPER.valueToTree(hashKey); + CBORObject dataItem = convertNode(node); + bytes = dataItem.EncodeToBytes(); + } catch (Exception e) { + log.debug("Error converting hashKey to CBOR", e); + throw new JsonLogicEvaluationException("Error converting hashKey to CBOR", jsonPath); + } int mmrHash = MurmurHash3.hash32x86(bytes, 0, bytes.length, 0); return distributeValueFromHash(mmrHash, propertyList, totalWeight, jsonPath); } @@ -129,6 +155,60 @@ static Object distributeValueFromHash( throw new JsonLogicEvaluationException("Unable to find a correct bucket for hash " + hash, jsonPath); } + private static final Comparator KEY_COMPARATOR = (k1, k2) -> { + byte[] b1 = k1.getBytes(StandardCharsets.UTF_8); + byte[] b2 = k2.getBytes(StandardCharsets.UTF_8); + if (b1.length != b2.length) { + return Integer.compare(b1.length, b2.length); + } + for (int i = 0; i < b1.length; i++) { + int v1 = b1[i] & 0xFF; + int v2 = b2[i] & 0xFF; + if (v1 != v2) { + return Integer.compare(v1, v2); + } + } + return 0; + }; + + private static CBORObject convertNode(JsonNode node) { + if (node.isNull()) { + return CBORObject.Null; + } else if (node.isBoolean()) { + return node.asBoolean() ? CBORObject.True : CBORObject.False; + } else if (node.isTextual()) { + return CBORObject.FromObject(node.asText()); + } else if (node.isNumber()) { + if (node.isIntegralNumber()) { + return CBORObject.FromObject(node.asLong()); + } else { + double val = node.asDouble(); + if (val == Math.floor(val) && val >= Long.MIN_VALUE && val <= Long.MAX_VALUE) { + return CBORObject.FromObject((long) val); + } + return CBORObject.FromObject(val); + } + } else if (node.isArray()) { + CBORObject array = CBORObject.NewArray(); + for (JsonNode item : node) { + CBORObject child = convertNode(item); + array.Add(child); + } + return array; + } else if (node.isObject()) { + CBORObject map = CBORObject.NewOrderedMap(); + List fieldNames = new ArrayList<>(); + node.fieldNames().forEachRemaining(fieldNames::add); + fieldNames.sort(KEY_COMPARATOR); + for (String fieldName : fieldNames) { + CBORObject child = convertNode(node.get(fieldName)); + map.Add(fieldName, child); + } + return map; + } + throw new IllegalArgumentException("Unsupported node type: " + node.getNodeType()); + } + @Getter @SuppressWarnings({"checkstyle:NoFinalizer"}) static class FractionProperty { diff --git a/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/e2e/FlagdCoreEvaluatorTest.java b/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/e2e/FlagdCoreEvaluatorTest.java index 941d978d5..8027215da 100644 --- a/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/e2e/FlagdCoreEvaluatorTest.java +++ b/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/e2e/FlagdCoreEvaluatorTest.java @@ -11,7 +11,7 @@ * configuration. Registered as an {@link dev.openfeature.contrib.tools.flagd.api.testkit.EvaluatorFactory} * via {@code META-INF/services}. */ -@ExcludeTags({"fractional-v1", "evaluator-refs-whitespace", "non-existent-evaluator-ref"}) +@ExcludeTags({"fractional-v1", "fractional-v2", "evaluator-refs-whitespace", "non-existent-evaluator-ref"}) public class FlagdCoreEvaluatorTest extends AbstractEvaluatorTest { @Override diff --git a/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/targeting/FractionalTest.java b/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/targeting/FractionalTest.java index 046d8914f..ed9388b0c 100644 --- a/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/targeting/FractionalTest.java +++ b/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/targeting/FractionalTest.java @@ -99,7 +99,7 @@ void missingBucketKeyReturnsNull() throws JsonLogicEvaluationException { List.of("one", 50), List.of("two", 50)); // bucketing key is null, so fractional falls back to flagKey + targetingKey - // but targetingKey is null, so it should return null + // but targetingKey is null, so it should throw GeneralError assertNull(fractional.evaluate(rule, data, "path")); } diff --git a/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/targeting/OperatorTest.java b/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/targeting/OperatorTest.java index 888b8ef03..f8e741080 100644 --- a/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/targeting/OperatorTest.java +++ b/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/targeting/OperatorTest.java @@ -112,7 +112,7 @@ void fractionalTestB() throws TargetingRuleException { Object evalVariant = OPERATOR.apply("headerColor", targetingRule, new ImmutableContext(ctxData)); // then - assertEquals("blue", evalVariant); + assertEquals("red", evalVariant); } @Test @@ -153,7 +153,7 @@ void fractionalTestA() throws TargetingRuleException { Object evalVariant = OPERATOR.apply("headerColor", targetingRule, new ImmutableContext(ctxData)); // then - assertEquals("yellow", evalVariant); + assertEquals("green", evalVariant); } @Test diff --git a/tools/flagd-core/src/test/resources/fractional/selfContainedFractionalB.json b/tools/flagd-core/src/test/resources/fractional/selfContainedFractionalB.json index e632f17d9..2beb7e5be 100644 --- a/tools/flagd-core/src/test/resources/fractional/selfContainedFractionalB.json +++ b/tools/flagd-core/src/test/resources/fractional/selfContainedFractionalB.json @@ -10,5 +10,5 @@ 50 ] ], - "result": "red" + "result": "blue" } diff --git a/tools/flagd-core/src/test/resources/fractional/string.json b/tools/flagd-core/src/test/resources/fractional/string.json index 8c55b68c1..65cb381b7 100644 --- a/tools/flagd-core/src/test/resources/fractional/string.json +++ b/tools/flagd-core/src/test/resources/fractional/string.json @@ -10,5 +10,5 @@ 70 ] ], - "result": "blue" + "result": "green" }