From d1c6802e0bc91d32e4ce25dbdeb80013f1f12b94 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Tue, 9 Dec 2025 10:45:53 -0500 Subject: [PATCH 1/5] ability to unwrap Eppo values to a given type --- src/main/java/cloud/eppo/api/EppoValue.java | 34 +++++ .../java/cloud/eppo/api/EppoValueTest.java | 123 ++++++++++++++++++ 2 files changed, 157 insertions(+) diff --git a/src/main/java/cloud/eppo/api/EppoValue.java b/src/main/java/cloud/eppo/api/EppoValue.java index aee8bae5..1ff12ec6 100644 --- a/src/main/java/cloud/eppo/api/EppoValue.java +++ b/src/main/java/cloud/eppo/api/EppoValue.java @@ -1,6 +1,9 @@ package cloud.eppo.api; import cloud.eppo.ufc.dto.EppoValueType; +import cloud.eppo.ufc.dto.VariationType; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Iterator; import java.util.List; import java.util.Objects; @@ -96,6 +99,37 @@ public EppoValueType getType() { return type; } + /** + * Unwraps an EppoValue to the appropriate Java type based on the variation type. + * + * @param value the EppoValue to unwrap + * @param expectedType the expected variation type + * @param the target type + * @return the unwrapped value + */ + @SuppressWarnings("unchecked") + public static T unwrap(EppoValue value, VariationType expectedType) { + switch (expectedType) { + case BOOLEAN: + return (T) Boolean.valueOf(value.booleanValue()); + case INTEGER: + return (T) Integer.valueOf(Double.valueOf(value.doubleValue()).intValue()); + case NUMERIC: + return (T) Double.valueOf(value.doubleValue()); + case STRING: + return (T) value.stringValue(); + case JSON: + String jsonString = value.stringValue(); + try { + ObjectMapper mapper = new ObjectMapper(); + return (T) mapper.readTree(jsonString); + } catch (JsonProcessingException e) { + return null; + } + } + throw new IllegalArgumentException("Unknown variation type: " + expectedType); + } + @Override public String toString() { switch (this.type) { diff --git a/src/test/java/cloud/eppo/api/EppoValueTest.java b/src/test/java/cloud/eppo/api/EppoValueTest.java index f2830ad2..c9b633ca 100644 --- a/src/test/java/cloud/eppo/api/EppoValueTest.java +++ b/src/test/java/cloud/eppo/api/EppoValueTest.java @@ -1,7 +1,11 @@ package cloud.eppo.api; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import cloud.eppo.ufc.dto.VariationType; +import com.fasterxml.jackson.databind.JsonNode; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -65,4 +69,123 @@ public void testToStringConsistencyAcrossTypes() { EppoValue arrayValue = EppoValue.valueOf(array); assertEquals("test1, test2", arrayValue.toString()); } + + @Test + public void testUnwrapBoolean() { + EppoValue boolValue = EppoValue.valueOf(true); + Boolean result = EppoValue.unwrap(boolValue, VariationType.BOOLEAN); + assertEquals(Boolean.TRUE, result); + + EppoValue falseValue = EppoValue.valueOf(false); + Boolean falseResult = EppoValue.unwrap(falseValue, VariationType.BOOLEAN); + assertEquals(Boolean.FALSE, falseResult); + } + + @Test + public void testUnwrapInteger() { + EppoValue numValue = EppoValue.valueOf(42.0); + Integer result = EppoValue.unwrap(numValue, VariationType.INTEGER); + assertEquals(Integer.valueOf(42), result); + + EppoValue negativeValue = EppoValue.valueOf(-17.0); + Integer negativeResult = EppoValue.unwrap(negativeValue, VariationType.INTEGER); + assertEquals(Integer.valueOf(-17), negativeResult); + } + + @Test + public void testUnwrapNumeric() { + EppoValue numValue = EppoValue.valueOf(123.456); + Double result = EppoValue.unwrap(numValue, VariationType.NUMERIC); + assertEquals(Double.valueOf(123.456), result); + + EppoValue intValue = EppoValue.valueOf(100.0); + Double intResult = EppoValue.unwrap(intValue, VariationType.NUMERIC); + assertEquals(Double.valueOf(100.0), intResult); + } + + @Test + public void testUnwrapString() { + EppoValue strValue = EppoValue.valueOf("hello world"); + String result = EppoValue.unwrap(strValue, VariationType.STRING); + assertEquals("hello world", result); + + EppoValue emptyValue = EppoValue.valueOf(""); + String emptyResult = EppoValue.unwrap(emptyValue, VariationType.STRING); + assertEquals("", emptyResult); + } + + @Test + public void testUnwrapJsonValid() { + String jsonString = "{\"foo\":\"bar\",\"count\":42}"; + EppoValue jsonValue = EppoValue.valueOf(jsonString); + JsonNode result = EppoValue.unwrap(jsonValue, VariationType.JSON); + + assertTrue(result.isObject()); + assertEquals("bar", result.get("foo").asText()); + assertEquals(42, result.get("count").asInt()); + } + + @Test + public void testUnwrapJsonArray() { + String jsonArrayString = "[1,2,3,4,5]"; + EppoValue jsonValue = EppoValue.valueOf(jsonArrayString); + JsonNode result = EppoValue.unwrap(jsonValue, VariationType.JSON); + + assertTrue(result.isArray()); + assertEquals(5, result.size()); + assertEquals(1, result.get(0).asInt()); + assertEquals(5, result.get(4).asInt()); + } + + @Test + public void testUnwrapJsonWithSpecialCharacters() { + String jsonString = "{\"a\":\"kümmert\",\"b\":\"schön\"}"; + EppoValue jsonValue = EppoValue.valueOf(jsonString); + JsonNode result = EppoValue.unwrap(jsonValue, VariationType.JSON); + + assertTrue(result.isObject()); + assertEquals("kümmert", result.get("a").asText()); + assertEquals("schön", result.get("b").asText()); + } + + @Test + public void testUnwrapJsonWithEmojis() { + String jsonString = "{\"a\":\"🤗\",\"b\":\"🌸\"}"; + EppoValue jsonValue = EppoValue.valueOf(jsonString); + JsonNode result = EppoValue.unwrap(jsonValue, VariationType.JSON); + + assertTrue(result.isObject()); + assertEquals("🤗", result.get("a").asText()); + assertEquals("🌸", result.get("b").asText()); + } + + @Test + public void testUnwrapJsonWithWhitespace() { + String jsonString = "{ \"key\": \"value\", \"number\": 123 }"; + EppoValue jsonValue = EppoValue.valueOf(jsonString); + JsonNode result = EppoValue.unwrap(jsonValue, VariationType.JSON); + + assertTrue(result.isObject()); + assertEquals("value", result.get("key").asText()); + assertEquals(123, result.get("number").asInt()); + } + + @Test + public void testUnwrapJsonInvalid() { + String invalidJson = "not valid json {"; + EppoValue jsonValue = EppoValue.valueOf(invalidJson); + JsonNode result = EppoValue.unwrap(jsonValue, VariationType.JSON); + + assertNull(result, "Invalid JSON should return null"); + } + + @Test + public void testUnwrapJsonEmpty() { + String emptyJson = "{}"; + EppoValue jsonValue = EppoValue.valueOf(emptyJson); + JsonNode result = EppoValue.unwrap(jsonValue, VariationType.JSON); + + assertTrue(result.isObject()); + assertEquals(0, result.size()); + } } From 27b9986d1794b868f931a853a87200215caab8e4 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Wed, 10 Dec 2025 08:45:50 -0500 Subject: [PATCH 2/5] make unwrapping an instance method --- src/main/java/cloud/eppo/api/EppoValue.java | 17 +++++------ .../java/cloud/eppo/api/EppoValueTest.java | 30 +++++++++---------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/main/java/cloud/eppo/api/EppoValue.java b/src/main/java/cloud/eppo/api/EppoValue.java index 1ff12ec6..c3355d9d 100644 --- a/src/main/java/cloud/eppo/api/EppoValue.java +++ b/src/main/java/cloud/eppo/api/EppoValue.java @@ -100,26 +100,25 @@ public EppoValueType getType() { } /** - * Unwraps an EppoValue to the appropriate Java type based on the variation type. + * Unwraps this EppoValue to the appropriate Java type based on the variation type. * - * @param value the EppoValue to unwrap * @param expectedType the expected variation type - * @param the target type + * @param the target type (Boolean, Integer, Double, String, or JsonNode) * @return the unwrapped value */ @SuppressWarnings("unchecked") - public static T unwrap(EppoValue value, VariationType expectedType) { + public T unwrap(VariationType expectedType) { switch (expectedType) { case BOOLEAN: - return (T) Boolean.valueOf(value.booleanValue()); + return (T) Boolean.valueOf(booleanValue()); case INTEGER: - return (T) Integer.valueOf(Double.valueOf(value.doubleValue()).intValue()); + return (T) Integer.valueOf(Double.valueOf(doubleValue()).intValue()); case NUMERIC: - return (T) Double.valueOf(value.doubleValue()); + return (T) Double.valueOf(doubleValue()); case STRING: - return (T) value.stringValue(); + return (T) stringValue(); case JSON: - String jsonString = value.stringValue(); + String jsonString = stringValue(); try { ObjectMapper mapper = new ObjectMapper(); return (T) mapper.readTree(jsonString); diff --git a/src/test/java/cloud/eppo/api/EppoValueTest.java b/src/test/java/cloud/eppo/api/EppoValueTest.java index c9b633ca..791e624a 100644 --- a/src/test/java/cloud/eppo/api/EppoValueTest.java +++ b/src/test/java/cloud/eppo/api/EppoValueTest.java @@ -73,44 +73,44 @@ public void testToStringConsistencyAcrossTypes() { @Test public void testUnwrapBoolean() { EppoValue boolValue = EppoValue.valueOf(true); - Boolean result = EppoValue.unwrap(boolValue, VariationType.BOOLEAN); + Boolean result = boolValue.unwrap(VariationType.BOOLEAN); assertEquals(Boolean.TRUE, result); EppoValue falseValue = EppoValue.valueOf(false); - Boolean falseResult = EppoValue.unwrap(falseValue, VariationType.BOOLEAN); + Boolean falseResult = falseValue.unwrap(VariationType.BOOLEAN); assertEquals(Boolean.FALSE, falseResult); } @Test public void testUnwrapInteger() { EppoValue numValue = EppoValue.valueOf(42.0); - Integer result = EppoValue.unwrap(numValue, VariationType.INTEGER); + Integer result = numValue.unwrap(VariationType.INTEGER); assertEquals(Integer.valueOf(42), result); EppoValue negativeValue = EppoValue.valueOf(-17.0); - Integer negativeResult = EppoValue.unwrap(negativeValue, VariationType.INTEGER); + Integer negativeResult = negativeValue.unwrap(VariationType.INTEGER); assertEquals(Integer.valueOf(-17), negativeResult); } @Test public void testUnwrapNumeric() { EppoValue numValue = EppoValue.valueOf(123.456); - Double result = EppoValue.unwrap(numValue, VariationType.NUMERIC); + Double result = numValue.unwrap(VariationType.NUMERIC); assertEquals(Double.valueOf(123.456), result); EppoValue intValue = EppoValue.valueOf(100.0); - Double intResult = EppoValue.unwrap(intValue, VariationType.NUMERIC); + Double intResult = intValue.unwrap(VariationType.NUMERIC); assertEquals(Double.valueOf(100.0), intResult); } @Test public void testUnwrapString() { EppoValue strValue = EppoValue.valueOf("hello world"); - String result = EppoValue.unwrap(strValue, VariationType.STRING); + String result = strValue.unwrap(VariationType.STRING); assertEquals("hello world", result); EppoValue emptyValue = EppoValue.valueOf(""); - String emptyResult = EppoValue.unwrap(emptyValue, VariationType.STRING); + String emptyResult = emptyValue.unwrap(VariationType.STRING); assertEquals("", emptyResult); } @@ -118,7 +118,7 @@ public void testUnwrapString() { public void testUnwrapJsonValid() { String jsonString = "{\"foo\":\"bar\",\"count\":42}"; EppoValue jsonValue = EppoValue.valueOf(jsonString); - JsonNode result = EppoValue.unwrap(jsonValue, VariationType.JSON); + JsonNode result = jsonValue.unwrap(VariationType.JSON); assertTrue(result.isObject()); assertEquals("bar", result.get("foo").asText()); @@ -129,7 +129,7 @@ public void testUnwrapJsonValid() { public void testUnwrapJsonArray() { String jsonArrayString = "[1,2,3,4,5]"; EppoValue jsonValue = EppoValue.valueOf(jsonArrayString); - JsonNode result = EppoValue.unwrap(jsonValue, VariationType.JSON); + JsonNode result = jsonValue.unwrap(VariationType.JSON); assertTrue(result.isArray()); assertEquals(5, result.size()); @@ -141,7 +141,7 @@ public void testUnwrapJsonArray() { public void testUnwrapJsonWithSpecialCharacters() { String jsonString = "{\"a\":\"kümmert\",\"b\":\"schön\"}"; EppoValue jsonValue = EppoValue.valueOf(jsonString); - JsonNode result = EppoValue.unwrap(jsonValue, VariationType.JSON); + JsonNode result = jsonValue.unwrap(VariationType.JSON); assertTrue(result.isObject()); assertEquals("kümmert", result.get("a").asText()); @@ -152,7 +152,7 @@ public void testUnwrapJsonWithSpecialCharacters() { public void testUnwrapJsonWithEmojis() { String jsonString = "{\"a\":\"🤗\",\"b\":\"🌸\"}"; EppoValue jsonValue = EppoValue.valueOf(jsonString); - JsonNode result = EppoValue.unwrap(jsonValue, VariationType.JSON); + JsonNode result = jsonValue.unwrap(VariationType.JSON); assertTrue(result.isObject()); assertEquals("🤗", result.get("a").asText()); @@ -163,7 +163,7 @@ public void testUnwrapJsonWithEmojis() { public void testUnwrapJsonWithWhitespace() { String jsonString = "{ \"key\": \"value\", \"number\": 123 }"; EppoValue jsonValue = EppoValue.valueOf(jsonString); - JsonNode result = EppoValue.unwrap(jsonValue, VariationType.JSON); + JsonNode result = jsonValue.unwrap(VariationType.JSON); assertTrue(result.isObject()); assertEquals("value", result.get("key").asText()); @@ -174,7 +174,7 @@ public void testUnwrapJsonWithWhitespace() { public void testUnwrapJsonInvalid() { String invalidJson = "not valid json {"; EppoValue jsonValue = EppoValue.valueOf(invalidJson); - JsonNode result = EppoValue.unwrap(jsonValue, VariationType.JSON); + JsonNode result = jsonValue.unwrap(VariationType.JSON); assertNull(result, "Invalid JSON should return null"); } @@ -183,7 +183,7 @@ public void testUnwrapJsonInvalid() { public void testUnwrapJsonEmpty() { String emptyJson = "{}"; EppoValue jsonValue = EppoValue.valueOf(emptyJson); - JsonNode result = EppoValue.unwrap(jsonValue, VariationType.JSON); + JsonNode result = jsonValue.unwrap(VariationType.JSON); assertTrue(result.isObject()); assertEquals(0, result.size()); From 12a6ecd1a0390d66aa2d20b2f390090269284703 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Tue, 9 Dec 2025 19:23:17 -0500 Subject: [PATCH 3/5] Have Flag Evaluator calculate details --- src/main/java/cloud/eppo/BaseEppoClient.java | 25 +- .../eppo/DetailedFlagEvaluationResult.java | 153 ++++++++++ src/main/java/cloud/eppo/FlagEvaluator.java | 263 ++++++++++++++---- .../java/cloud/eppo/FlagEvaluatorTest.java | 143 +++++++--- 4 files changed, 489 insertions(+), 95 deletions(-) create mode 100644 src/main/java/cloud/eppo/DetailedFlagEvaluationResult.java diff --git a/src/main/java/cloud/eppo/BaseEppoClient.java b/src/main/java/cloud/eppo/BaseEppoClient.java index 6a206c0e..d465c436 100644 --- a/src/main/java/cloud/eppo/BaseEppoClient.java +++ b/src/main/java/cloud/eppo/BaseEppoClient.java @@ -223,11 +223,20 @@ protected EppoValue getTypedAssignment( return defaultValue; } - FlagEvaluationResult evaluationResult = - FlagEvaluator.evaluateFlag( - flag, flagKey, subjectKey, subjectAttributes, config.isConfigObfuscated()); + // Evaluate flag with details + DetailedFlagEvaluationResult detailedResult = + FlagEvaluator.evaluateFlagWithDetails( + flag, + flagKey, + subjectKey, + subjectAttributes, + config.isConfigObfuscated(), + config.getEnvironmentName(), + config.getConfigFetchedAt(), + config.getConfigPublishedAt()); + EppoValue assignedValue = - evaluationResult.getVariation() != null ? evaluationResult.getVariation().getValue() : null; + detailedResult.getVariation() != null ? detailedResult.getVariation().getValue() : null; if (assignedValue != null && !valueTypeMatchesExpected(expectedType, assignedValue)) { log.warn( @@ -238,17 +247,17 @@ protected EppoValue getTypedAssignment( return defaultValue; } - if (assignedValue != null && assignmentLogger != null && evaluationResult.doLog()) { + if (assignedValue != null && assignmentLogger != null && detailedResult.doLog()) { try { - String allocationKey = evaluationResult.getAllocationKey(); + String allocationKey = detailedResult.getAllocationKey(); String experimentKey = flagKey + '-' + allocationKey; // Our experiment key is derived by hyphenating the flag key and // allocation key - String variationKey = evaluationResult.getVariation().getKey(); - Map extraLogging = evaluationResult.getExtraLogging(); + String variationKey = detailedResult.getVariation().getKey(); + Map extraLogging = detailedResult.getExtraLogging(); Map metaData = buildLogMetaData(config.isConfigObfuscated()); Assignment assignment = diff --git a/src/main/java/cloud/eppo/DetailedFlagEvaluationResult.java b/src/main/java/cloud/eppo/DetailedFlagEvaluationResult.java new file mode 100644 index 00000000..0cfb9c87 --- /dev/null +++ b/src/main/java/cloud/eppo/DetailedFlagEvaluationResult.java @@ -0,0 +1,153 @@ +package cloud.eppo; + +import cloud.eppo.api.*; +import cloud.eppo.ufc.dto.Variation; +import java.util.Date; +import java.util.Map; + +/** + * Extended flag evaluation result that includes detailed evaluation information for debugging and + * understanding flag assignments. + */ +public class DetailedFlagEvaluationResult extends FlagEvaluationResult { + private final EvaluationDetails evaluationDetails; + + public DetailedFlagEvaluationResult( + String flagKey, + String subjectKey, + Attributes subjectAttributes, + String allocationKey, + Variation variation, + Map extraLogging, + boolean doLog, + EvaluationDetails evaluationDetails) { + super(flagKey, subjectKey, subjectAttributes, allocationKey, variation, extraLogging, doLog); + this.evaluationDetails = evaluationDetails; + } + + public EvaluationDetails getEvaluationDetails() { + return evaluationDetails; + } + + /** Builder to construct detailed evaluation results during flag evaluation. */ + public static class Builder { + private String flagKey; + private String subjectKey; + private Attributes subjectAttributes; + private String allocationKey; + private Variation variation; + private Map extraLogging; + private boolean doLog; + + // Delegate to EvaluationDetails.Builder for evaluation details + private final EvaluationDetails.Builder detailsBuilder = EvaluationDetails.builder(); + + public Builder flagKey(String flagKey) { + this.flagKey = flagKey; + return this; + } + + public Builder subjectKey(String subjectKey) { + this.subjectKey = subjectKey; + return this; + } + + public Builder subjectAttributes(Attributes subjectAttributes) { + this.subjectAttributes = subjectAttributes; + return this; + } + + public Builder allocationKey(String allocationKey) { + this.allocationKey = allocationKey; + return this; + } + + public Builder variation(Variation variation) { + this.variation = variation; + return this; + } + + public Builder extraLogging(Map extraLogging) { + this.extraLogging = extraLogging; + return this; + } + + public Builder doLog(boolean doLog) { + this.doLog = doLog; + return this; + } + + public Builder environmentName(String environmentName) { + detailsBuilder.environmentName(environmentName); + return this; + } + + public Builder flagEvaluationCode(FlagEvaluationCode code) { + detailsBuilder.flagEvaluationCode(code); + return this; + } + + public Builder flagEvaluationDescription(String description) { + detailsBuilder.flagEvaluationDescription(description); + return this; + } + + public Builder banditKey(String banditKey) { + detailsBuilder.banditKey(banditKey); + return this; + } + + public Builder banditAction(String banditAction) { + detailsBuilder.banditAction(banditAction); + return this; + } + + public Builder matchedRule(MatchedRule matchedRule) { + detailsBuilder.matchedRule(matchedRule); + return this; + } + + public Builder matchedAllocation(AllocationDetails matchedAllocation) { + detailsBuilder.matchedAllocation(matchedAllocation); + return this; + } + + public Builder addUnmatchedAllocation(AllocationDetails allocation) { + detailsBuilder.addUnmatchedAllocation(allocation); + return this; + } + + public Builder addUnevaluatedAllocation(AllocationDetails allocation) { + detailsBuilder.addUnevaluatedAllocation(allocation); + return this; + } + + public Builder configFetchedAt(Date configFetchedAt) { + detailsBuilder.configFetchedAt(configFetchedAt); + return this; + } + + public Builder configPublishedAt(Date configPublishedAt) { + detailsBuilder.configPublishedAt(configPublishedAt); + return this; + } + + public DetailedFlagEvaluationResult build() { + // Set variation details before building + if (variation != null) { + detailsBuilder.variationKey(variation.getKey()); + detailsBuilder.variationValue(variation.getValue()); + } + + return new DetailedFlagEvaluationResult( + flagKey, + subjectKey, + subjectAttributes, + allocationKey, + variation, + extraLogging, + doLog, + detailsBuilder.build()); + } + } +} diff --git a/src/main/java/cloud/eppo/FlagEvaluator.java b/src/main/java/cloud/eppo/FlagEvaluator.java index 0a2e78f7..015c05b8 100644 --- a/src/main/java/cloud/eppo/FlagEvaluator.java +++ b/src/main/java/cloud/eppo/FlagEvaluator.java @@ -1,98 +1,168 @@ package cloud.eppo; import static cloud.eppo.Utils.base64Decode; +import static cloud.eppo.Utils.getMD5Hex; import static cloud.eppo.Utils.getShard; +import cloud.eppo.api.AllocationDetails; +import cloud.eppo.api.AllocationEvaluationCode; import cloud.eppo.api.Attributes; import cloud.eppo.api.EppoValue; +import cloud.eppo.api.FlagEvaluationCode; +import cloud.eppo.api.MatchedRule; +import cloud.eppo.api.RuleCondition; import cloud.eppo.model.ShardRange; import cloud.eppo.ufc.dto.Allocation; import cloud.eppo.ufc.dto.FlagConfig; import cloud.eppo.ufc.dto.Shard; import cloud.eppo.ufc.dto.Split; +import cloud.eppo.ufc.dto.TargetingRule; import cloud.eppo.ufc.dto.Variation; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; public class FlagEvaluator { - public static FlagEvaluationResult evaluateFlag( + /** + * Evaluates a flag and returns detailed evaluation information including allocation statuses, + * matched rules, and evaluation codes. This is useful for debugging and understanding why a + * particular variation was assigned. + */ + public static DetailedFlagEvaluationResult evaluateFlagWithDetails( FlagConfig flag, String flagKey, String subjectKey, Attributes subjectAttributes, - boolean isConfigObfuscated) { + boolean isConfigObfuscated, + String environmentName, + Date configFetchedAt, + Date configPublishedAt) { Date now = new Date(); - Variation variation = null; - String allocationKey = null; - Map extraLogging = new HashMap<>(); - boolean doLog = false; + DetailedFlagEvaluationResult.Builder builder = + new DetailedFlagEvaluationResult.Builder() + .flagKey(flagKey) + .subjectKey(subjectKey) + .subjectAttributes(subjectAttributes) + .extraLogging(new HashMap<>()) + .environmentName(environmentName != null ? environmentName : "Unknown") + .configFetchedAt(configFetchedAt) + .configPublishedAt(configPublishedAt); + + // Handle disabled flag + if (!flag.isEnabled()) { + builder + .doLog(false) + .flagEvaluationCode(FlagEvaluationCode.FLAG_UNRECOGNIZED_OR_DISABLED) + .flagEvaluationDescription("Unrecognized or disabled flag: " + flagKey); + + // All allocations are unevaluated for disabled flags + if (flag.getAllocations() != null) { + for (int i = 0; i < flag.getAllocations().size(); i++) { + Allocation allocation = flag.getAllocations().get(i); + String allocationKey = + isConfigObfuscated ? base64Decode(allocation.getKey()) : allocation.getKey(); + builder.addUnevaluatedAllocation( + new AllocationDetails(allocationKey, AllocationEvaluationCode.UNEVALUATED, i + 1)); + } + } + + return builder.build(); + } - // If flag is disabled; use an empty list of allocations so that the empty result is returned - // Note: this is a safety check; disabled flags should be filtered upstream List allocationsToConsider = - flag.isEnabled() && flag.getAllocations() != null - ? flag.getAllocations() - : new LinkedList<>(); + flag.getAllocations() != null ? flag.getAllocations() : new LinkedList<>(); + + int allocationPosition = 0; + boolean foundMatch = false; for (Allocation allocation : allocationsToConsider) { + allocationPosition++; + String allocationKey = allocation.getKey(); + String deobfuscatedAllocationKey = + isConfigObfuscated ? base64Decode(allocationKey) : allocationKey; + + // Check if allocation is time-bound and not yet active if (allocation.getStartAt() != null && allocation.getStartAt().after(now)) { - // Allocation not yet active + builder.addUnmatchedAllocation( + new AllocationDetails( + deobfuscatedAllocationKey, + AllocationEvaluationCode.BEFORE_START_TIME, + allocationPosition)); continue; } + + // Check if allocation is time-bound and no longer active if (allocation.getEndAt() != null && allocation.getEndAt().before(now)) { - // Allocation no longer active + builder.addUnmatchedAllocation( + new AllocationDetails( + deobfuscatedAllocationKey, + AllocationEvaluationCode.AFTER_END_TIME, + allocationPosition)); continue; } - // For convenience, we will automatically include the subject key as the "id" attribute if - // none is provided + // For convenience, automatically include subject key as "id" attribute if not provided Attributes subjectAttributesToEvaluate = new Attributes(subjectAttributes); if (!subjectAttributesToEvaluate.containsKey("id")) { subjectAttributesToEvaluate.put("id", subjectKey); } - if (allocation.getRules() != null - && !allocation.getRules().isEmpty() - && RuleEvaluator.findMatchingRule( - subjectAttributesToEvaluate, allocation.getRules(), isConfigObfuscated) - == null) { - // Rules are defined, but none match - continue; + // Check rules + TargetingRule matchedTargetingRule = null; + if (allocation.getRules() != null && !allocation.getRules().isEmpty()) { + matchedTargetingRule = + RuleEvaluator.findMatchingRule( + subjectAttributesToEvaluate, allocation.getRules(), isConfigObfuscated); + + if (matchedTargetingRule == null) { + // Rules are defined but none match + builder.addUnmatchedAllocation( + new AllocationDetails( + deobfuscatedAllocationKey, + AllocationEvaluationCode.FAILING_RULE, + allocationPosition)); + continue; + } } - // This allocation has matched; find variation + // This allocation has matched rules; find variation in splits + Variation variation = null; + Map extraLogging = new HashMap<>(); + Split matchedSplit = null; + for (Split split : allocation.getSplits()) { if (allShardsMatch(split, subjectKey, flag.getTotalShards(), isConfigObfuscated)) { - // Variation and extra logging is determined by the relevant split variation = flag.getVariations().get(split.getVariationKey()); if (variation == null) { throw new RuntimeException("Unknown split variation key: " + split.getVariationKey()); } extraLogging = split.getExtraLogging(); + matchedSplit = split; break; } } - if (variation != null) { - // We only evaluate the first relevant allocation - allocationKey = allocation.getKey(); - // doLog is determined by the allocation - doLog = allocation.doLog(); - break; + if (variation == null) { + // Rules matched but subject doesn't fall in traffic split + builder.addUnmatchedAllocation( + new AllocationDetails( + deobfuscatedAllocationKey, + AllocationEvaluationCode.TRAFFIC_EXPOSURE_MISS, + allocationPosition)); + continue; } - } - if (isConfigObfuscated) { - // Need to unobfuscate for the returned evaluation result - if (allocationKey != null) { - allocationKey = base64Decode(allocationKey); - } - if (variation != null) { + foundMatch = true; + + // Deobfuscate if needed + if (isConfigObfuscated) { + allocationKey = deobfuscatedAllocationKey; String key = base64Decode(variation.getKey()); EppoValue decodedValue = EppoValue.nullValue(); if (!variation.getValue().isNull()) { @@ -116,27 +186,116 @@ public static FlagEvaluationResult evaluateFlag( } } variation = new Variation(key, decodedValue); - } - // Deobfuscate extraLogging if present - if (extraLogging != null && !extraLogging.isEmpty()) { - Map deobfuscatedExtraLogging = new HashMap<>(); - for (Map.Entry entry : extraLogging.entrySet()) { - try { - String deobfuscatedKey = base64Decode(entry.getKey()); - String deobfuscatedValue = base64Decode(entry.getValue()); - deobfuscatedExtraLogging.put(deobfuscatedKey, deobfuscatedValue); - } catch (Exception e) { - // If deobfuscation fails, keep the original key-value pair - deobfuscatedExtraLogging.put(entry.getKey(), entry.getValue()); + // Deobfuscate extraLogging if present + if (extraLogging != null && !extraLogging.isEmpty()) { + Map deobfuscatedExtraLogging = new HashMap<>(); + for (Map.Entry entry : extraLogging.entrySet()) { + try { + String deobfuscatedKey = base64Decode(entry.getKey()); + String deobfuscatedValue = base64Decode(entry.getValue()); + deobfuscatedExtraLogging.put(deobfuscatedKey, deobfuscatedValue); + } catch (Exception e) { + // If deobfuscation fails, keep the original key-value pair + deobfuscatedExtraLogging.put(entry.getKey(), entry.getValue()); + } } + extraLogging = deobfuscatedExtraLogging; + } + } + + // Build matched rule details if applicable + MatchedRule matchedRule = null; + if (matchedTargetingRule != null) { + Set conditions = + matchedTargetingRule.getConditions().stream() + .map( + tc -> { + // Deobfuscate attribute name if config is obfuscated + String attribute = tc.getAttribute(); + if (isConfigObfuscated) { + // Find the original attribute name by matching the MD5 hash + for (Map.Entry entry : + subjectAttributesToEvaluate.entrySet()) { + if (getMD5Hex(entry.getKey()).equals(attribute)) { + attribute = entry.getKey(); + break; + } + } + } + + // Condition values are already handled by RuleEvaluator during evaluation + // For display purposes, we keep the raw value + return new RuleCondition(attribute, tc.getOperator().value, tc.getValue()); + }) + .collect(Collectors.toSet()); + matchedRule = new MatchedRule(conditions); + } + + // Determine evaluation description + String description; + if (matchedRule != null) { + // Check if we need to include traffic assignment details + // Include traffic details if there are multiple splits OR multiple shards + boolean hasMultipleSplits = allocation.getSplits().size() > 1; + boolean hasMultipleShards = + matchedSplit.getShards() != null && matchedSplit.getShards().size() > 1; + + if (hasMultipleSplits || hasMultipleShards) { + description = + String.format( + "Supplied attributes match rules defined in allocation \"%s\" and %s belongs to the range of traffic assigned to \"%s\".", + allocationKey, subjectKey, variation.getKey()); + } else { + description = + String.format( + "Supplied attributes match rules defined in allocation \"%s\".", allocationKey); } - extraLogging = deobfuscatedExtraLogging; + } else { + description = + String.format( + "%s belongs to the range of traffic assigned to \"%s\" defined in allocation \"%s\".", + subjectKey, variation.getKey(), allocationKey); + } + + // TODO check type + + builder + .allocationKey(allocationKey) + .variation(variation) + .extraLogging(extraLogging) + .doLog(allocation.doLog()) + .flagEvaluationCode(FlagEvaluationCode.MATCH) + .flagEvaluationDescription(description) + .matchedRule(matchedRule) + .matchedAllocation( + new AllocationDetails( + allocationKey, AllocationEvaluationCode.MATCH, allocationPosition)); + + // Mark remaining allocations as unevaluated + for (int i = allocationPosition; i < allocationsToConsider.size(); i++) { + Allocation unevaluatedAllocation = allocationsToConsider.get(i); + String unevaluatedKey = + isConfigObfuscated + ? base64Decode(unevaluatedAllocation.getKey()) + : unevaluatedAllocation.getKey(); + builder.addUnevaluatedAllocation( + new AllocationDetails(unevaluatedKey, AllocationEvaluationCode.UNEVALUATED, i + 1)); } + + break; + } + + // If no match was found, return default with appropriate code + if (!foundMatch) { + builder + .doLog(false) + .flagEvaluationCode(FlagEvaluationCode.DEFAULT_ALLOCATION_NULL) + .flagEvaluationDescription( + "No allocations matched. Falling back to \"Default Allocation\", serving NULL"); } - return new FlagEvaluationResult( - flagKey, subjectKey, subjectAttributes, allocationKey, variation, extraLogging, doLog); + return builder.build(); } private static boolean allShardsMatch( diff --git a/src/test/java/cloud/eppo/FlagEvaluatorTest.java b/src/test/java/cloud/eppo/FlagEvaluatorTest.java index c4c7c212..3f895414 100644 --- a/src/test/java/cloud/eppo/FlagEvaluatorTest.java +++ b/src/test/java/cloud/eppo/FlagEvaluatorTest.java @@ -6,6 +6,7 @@ import cloud.eppo.api.Attributes; import cloud.eppo.api.EppoValue; +import cloud.eppo.api.EvaluationDetails; import cloud.eppo.model.ShardRange; import cloud.eppo.ufc.dto.Allocation; import cloud.eppo.ufc.dto.FlagConfig; @@ -37,8 +38,22 @@ public void testDisabledFlag() { List splits = createSplits("a", shards); List allocations = createAllocations("allocation", splits); FlagConfig flag = createFlag("flag", false, variations, allocations); - FlagEvaluationResult result = - FlagEvaluator.evaluateFlag(flag, "flag", "subjectKey", new Attributes(), false); + + // Create test metadata values + String testEnvironmentName = "Production"; + Date testConfigFetchedAt = new Date(1672531200000L); // Jan 1, 2023 + Date testConfigPublishedAt = new Date(1672444800000L); // Dec 31, 2022 + + DetailedFlagEvaluationResult result = + FlagEvaluator.evaluateFlagWithDetails( + flag, + "flag", + "subjectKey", + new Attributes(), + false, + testEnvironmentName, + testConfigFetchedAt, + testConfigPublishedAt); assertEquals(flag.getKey(), result.getFlagKey()); assertEquals("subjectKey", result.getSubjectKey()); @@ -46,14 +61,22 @@ public void testDisabledFlag() { assertNull(result.getAllocationKey()); assertNull(result.getVariation()); assertFalse(result.doLog()); + + // Verify configuration metadata flows through to evaluation details + EvaluationDetails details = result.getEvaluationDetails(); + assertNotNull(details); + assertEquals("Production", details.getEnvironmentName()); + assertEquals(testConfigFetchedAt, details.getConfigFetchedAt()); + assertEquals(testConfigPublishedAt, details.getConfigPublishedAt()); } @Test public void testNoAllocations() { Map variations = createVariations("a"); FlagConfig flag = createFlag("flag", true, variations, null); - FlagEvaluationResult result = - FlagEvaluator.evaluateFlag(flag, "flag", "subjectKey", new Attributes(), false); + DetailedFlagEvaluationResult result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subjectKey", new Attributes(), false, null, null, null); assertEquals(flag.getKey(), result.getFlagKey()); assertEquals("subjectKey", result.getSubjectKey()); @@ -70,8 +93,22 @@ public void testSimpleFlag() { List splits = createSplits("a", shards); List allocations = createAllocations("allocation", splits); FlagConfig flag = createFlag("flag", true, variations, allocations); - FlagEvaluationResult result = - FlagEvaluator.evaluateFlag(flag, "flag", "subjectKey", new Attributes(), false); + + // Create test metadata values + String testEnvironmentName = "Staging"; + Date testConfigFetchedAt = new Date(1672617600000L); // Jan 2, 2023 + Date testConfigPublishedAt = new Date(1672531200000L); // Jan 1, 2023 + + DetailedFlagEvaluationResult result = + FlagEvaluator.evaluateFlagWithDetails( + flag, + "flag", + "subjectKey", + new Attributes(), + false, + testEnvironmentName, + testConfigFetchedAt, + testConfigPublishedAt); assertEquals(flag.getKey(), result.getFlagKey()); assertEquals("subjectKey", result.getSubjectKey()); @@ -79,6 +116,13 @@ public void testSimpleFlag() { assertEquals("allocation", result.getAllocationKey()); assertEquals("A", result.getVariation().getValue().stringValue()); assertTrue(result.doLog()); + + // Verify configuration metadata flows through to evaluation details + EvaluationDetails details = result.getEvaluationDetails(); + assertNotNull(details); + assertEquals("Staging", details.getEnvironmentName()); + assertEquals(testConfigFetchedAt, details.getConfigFetchedAt()); + assertEquals(testConfigPublishedAt, details.getConfigPublishedAt()); } @Test @@ -97,16 +141,21 @@ public void testIDTargetingCondition() { // Check that subjectKey is evaluated as the "id" attribute - FlagEvaluationResult result = - FlagEvaluator.evaluateFlag(flag, "flag", "alice", new Attributes(), false); + DetailedFlagEvaluationResult result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "alice", new Attributes(), false, null, null, null); assertEquals("A", result.getVariation().getValue().stringValue()); - result = FlagEvaluator.evaluateFlag(flag, "flag", "bob", new Attributes(), false); + result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "bob", new Attributes(), false, null, null, null); assertEquals("A", result.getVariation().getValue().stringValue()); - result = FlagEvaluator.evaluateFlag(flag, "flag", "charlie", new Attributes(), false); + result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "charlie", new Attributes(), false, null, null, null); assertNull(result.getVariation()); @@ -114,14 +163,18 @@ public void testIDTargetingCondition() { Attributes aliceAttributes = new Attributes(); aliceAttributes.put("id", "charlie"); - result = FlagEvaluator.evaluateFlag(flag, "flag", "alice", aliceAttributes, false); + result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "alice", aliceAttributes, false, null, null, null); assertNull(result.getVariation()); Attributes charlieAttributes = new Attributes(); charlieAttributes.put("id", "alice"); - result = FlagEvaluator.evaluateFlag(flag, "flag", "charlie", charlieAttributes, false); + result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "charlie", charlieAttributes, false, null, null, null); assertEquals("A", result.getVariation().getValue().stringValue()); } @@ -133,8 +186,9 @@ public void testCatchAllAllocation() { List allocations = createAllocations("default", splits); FlagConfig flag = createFlag("key", true, variations, allocations); - FlagEvaluationResult result = - FlagEvaluator.evaluateFlag(flag, "flag", "subjectKey", new Attributes(), false); + DetailedFlagEvaluationResult result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subjectKey", new Attributes(), false, null, null, null); assertEquals("default", result.getAllocationKey()); assertEquals("A", result.getVariation().getValue().stringValue()); @@ -155,16 +209,21 @@ public void testMultipleAllocations() { Attributes matchingEmailAttributes = new Attributes(); matchingEmailAttributes.put("email", "eppo@example.com"); - FlagEvaluationResult result = - FlagEvaluator.evaluateFlag(flag, "flag", "subjectKey", matchingEmailAttributes, false); + DetailedFlagEvaluationResult result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subjectKey", matchingEmailAttributes, false, null, null, null); assertEquals("B", result.getVariation().getValue().stringValue()); Attributes unknownEmailAttributes = new Attributes(); unknownEmailAttributes.put("email", "eppo@test.com"); - result = FlagEvaluator.evaluateFlag(flag, "flag", "subjectKey", unknownEmailAttributes, false); + result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subjectKey", unknownEmailAttributes, false, null, null, null); assertEquals("A", result.getVariation().getValue().stringValue()); - result = FlagEvaluator.evaluateFlag(flag, "flag", "subjectKey", new Attributes(), false); + result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subjectKey", new Attributes(), false, null, null, null); assertEquals("A", result.getVariation().getValue().stringValue()); } @@ -188,16 +247,21 @@ public void testVariationShardRanges() { FlagConfig flag = createFlag("key", true, variations, allocations); - FlagEvaluationResult result = - FlagEvaluator.evaluateFlag(flag, "flag", "subject4", new Attributes(), false); + DetailedFlagEvaluationResult result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subject4", new Attributes(), false, null, null, null); assertEquals("A", result.getVariation().getValue().stringValue()); - result = FlagEvaluator.evaluateFlag(flag, "flag", "subject13", new Attributes(), false); + result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subject13", new Attributes(), false, null, null, null); assertEquals("B", result.getVariation().getValue().stringValue()); - result = FlagEvaluator.evaluateFlag(flag, "flag", "subject14", new Attributes(), false); + result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subject14", new Attributes(), false, null, null, null); assertEquals("C", result.getVariation().getValue().stringValue()); } @@ -219,8 +283,9 @@ public void testAllocationStartAndEndAt() { allocation.setStartAt(startAt); allocation.setEndAt(endAt); - FlagEvaluationResult result = - FlagEvaluator.evaluateFlag(flag, "flag", "subject", new Attributes(), false); + DetailedFlagEvaluationResult result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subject", new Attributes(), false, null, null, null); assertEquals("A", result.getVariation().getValue().stringValue()); assertTrue(result.doLog()); @@ -229,7 +294,9 @@ public void testAllocationStartAndEndAt() { allocation.setStartAt(new Date(now.getTime() + oneDayInMilliseconds)); allocation.setEndAt(new Date(now.getTime() + 2 * oneDayInMilliseconds)); - result = FlagEvaluator.evaluateFlag(flag, "flag", "subject", new Attributes(), false); + result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subject", new Attributes(), false, null, null, null); assertNull(result.getVariation()); assertFalse(result.doLog()); @@ -238,7 +305,9 @@ public void testAllocationStartAndEndAt() { allocation.setStartAt(new Date(now.getTime() - 2 * oneDayInMilliseconds)); allocation.setEndAt(new Date(now.getTime() - oneDayInMilliseconds)); - result = FlagEvaluator.evaluateFlag(flag, "flag", "subject", new Attributes(), false); + result = + FlagEvaluator.evaluateFlagWithDetails( + flag, "flag", "subject", new Attributes(), false, null, null, null); assertNull(result.getVariation()); assertFalse(result.doLog()); @@ -332,9 +401,9 @@ public void testObfuscated() { flag.getVariationType(), encodedVariations, encodedAllocations); - FlagEvaluationResult result = - FlagEvaluator.evaluateFlag( - obfuscatedFlag, "flag", "subjectKey", matchingEmailAttributes, true); + DetailedFlagEvaluationResult result = + FlagEvaluator.evaluateFlagWithDetails( + obfuscatedFlag, "flag", "subjectKey", matchingEmailAttributes, true, null, null, null); // Expect an unobfuscated evaluation result assertEquals("flag", result.getFlagKey()); @@ -347,12 +416,13 @@ public void testObfuscated() { Attributes unknownEmailAttributes = new Attributes(); unknownEmailAttributes.put("email", "eppo@test.com"); result = - FlagEvaluator.evaluateFlag( - obfuscatedFlag, "flag", "subjectKey", unknownEmailAttributes, true); + FlagEvaluator.evaluateFlagWithDetails( + obfuscatedFlag, "flag", "subjectKey", unknownEmailAttributes, true, null, null, null); assertEquals("A", result.getVariation().getValue().stringValue()); result = - FlagEvaluator.evaluateFlag(obfuscatedFlag, "flag", "subjectKey", new Attributes(), true); + FlagEvaluator.evaluateFlagWithDetails( + obfuscatedFlag, "flag", "subjectKey", new Attributes(), true, null, null, null); assertEquals("A", result.getVariation().getValue().stringValue()); } @@ -423,8 +493,9 @@ public void testObfuscatedExtraLogging() { encodedAllocations); // Test with obfuscated config - FlagEvaluationResult result = - FlagEvaluator.evaluateFlag(obfuscatedFlag, "flag", "subject", new Attributes(), true); + DetailedFlagEvaluationResult result = + FlagEvaluator.evaluateFlagWithDetails( + obfuscatedFlag, "flag", "subject", new Attributes(), true, null, null, null); // Verify that extraLogging is deobfuscated Map extraLogging = result.getExtraLogging(); @@ -434,7 +505,9 @@ public void testObfuscatedExtraLogging() { assertEquals(2, extraLogging.size()); // Test with non-obfuscated config to ensure no deobfuscation happens - result = FlagEvaluator.evaluateFlag(obfuscatedFlag, "flag", "subject", new Attributes(), false); + result = + FlagEvaluator.evaluateFlagWithDetails( + obfuscatedFlag, "flag", "subject", new Attributes(), false, null, null, null); // Verify that extraLogging remains obfuscated extraLogging = result.getExtraLogging(); From 30e0c467cc4cf9c862255bb395520ef61f1c97f4 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Tue, 9 Dec 2025 20:02:58 -0500 Subject: [PATCH 4/5] increase profiling test max allowed time --- src/test/java/cloud/eppo/ProfileBaseEppoClientTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/cloud/eppo/ProfileBaseEppoClientTest.java b/src/test/java/cloud/eppo/ProfileBaseEppoClientTest.java index 3dd24802..3787a7dc 100644 --- a/src/test/java/cloud/eppo/ProfileBaseEppoClientTest.java +++ b/src/test/java/cloud/eppo/ProfileBaseEppoClientTest.java @@ -94,9 +94,9 @@ public void testGetStringAssignmentPerformance() { assertEquals(0.12, variationCounts.get("yellow").doubleValue() / numIterations, 0.02); // Seeing ~48,000,000 - ~54,000,000 for 10k iterations on a M2 Macbook Pro; let's fail if more - // than 150,000,000; giving a generous allowance for slower systems (like GitHub) but will still - // catch if things slow down considerably - long maxAllowedTime = 15000 * numIterations; + // than 200,000,000; giving a generous allowance for slower systems (like GitHub) but will still + // catch if things slow down considerably (>4x) + long maxAllowedTime = 20000 * numIterations; assertTrue( elapsedTime < maxAllowedTime, "Cpu time of " + elapsedTime + " is more than the " + maxAllowedTime + " allowed"); From 1ee86b668e611d8f861063daa9791a12722c7aed Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Wed, 10 Dec 2025 08:57:39 -0500 Subject: [PATCH 5/5] rollback EppoValue unwrapping --- src/main/java/cloud/eppo/api/EppoValue.java | 33 ----- .../java/cloud/eppo/api/EppoValueTest.java | 123 ------------------ 2 files changed, 156 deletions(-) diff --git a/src/main/java/cloud/eppo/api/EppoValue.java b/src/main/java/cloud/eppo/api/EppoValue.java index c3355d9d..aee8bae5 100644 --- a/src/main/java/cloud/eppo/api/EppoValue.java +++ b/src/main/java/cloud/eppo/api/EppoValue.java @@ -1,9 +1,6 @@ package cloud.eppo.api; import cloud.eppo.ufc.dto.EppoValueType; -import cloud.eppo.ufc.dto.VariationType; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Iterator; import java.util.List; import java.util.Objects; @@ -99,36 +96,6 @@ public EppoValueType getType() { return type; } - /** - * Unwraps this EppoValue to the appropriate Java type based on the variation type. - * - * @param expectedType the expected variation type - * @param the target type (Boolean, Integer, Double, String, or JsonNode) - * @return the unwrapped value - */ - @SuppressWarnings("unchecked") - public T unwrap(VariationType expectedType) { - switch (expectedType) { - case BOOLEAN: - return (T) Boolean.valueOf(booleanValue()); - case INTEGER: - return (T) Integer.valueOf(Double.valueOf(doubleValue()).intValue()); - case NUMERIC: - return (T) Double.valueOf(doubleValue()); - case STRING: - return (T) stringValue(); - case JSON: - String jsonString = stringValue(); - try { - ObjectMapper mapper = new ObjectMapper(); - return (T) mapper.readTree(jsonString); - } catch (JsonProcessingException e) { - return null; - } - } - throw new IllegalArgumentException("Unknown variation type: " + expectedType); - } - @Override public String toString() { switch (this.type) { diff --git a/src/test/java/cloud/eppo/api/EppoValueTest.java b/src/test/java/cloud/eppo/api/EppoValueTest.java index 791e624a..f2830ad2 100644 --- a/src/test/java/cloud/eppo/api/EppoValueTest.java +++ b/src/test/java/cloud/eppo/api/EppoValueTest.java @@ -1,11 +1,7 @@ package cloud.eppo.api; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import cloud.eppo.ufc.dto.VariationType; -import com.fasterxml.jackson.databind.JsonNode; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -69,123 +65,4 @@ public void testToStringConsistencyAcrossTypes() { EppoValue arrayValue = EppoValue.valueOf(array); assertEquals("test1, test2", arrayValue.toString()); } - - @Test - public void testUnwrapBoolean() { - EppoValue boolValue = EppoValue.valueOf(true); - Boolean result = boolValue.unwrap(VariationType.BOOLEAN); - assertEquals(Boolean.TRUE, result); - - EppoValue falseValue = EppoValue.valueOf(false); - Boolean falseResult = falseValue.unwrap(VariationType.BOOLEAN); - assertEquals(Boolean.FALSE, falseResult); - } - - @Test - public void testUnwrapInteger() { - EppoValue numValue = EppoValue.valueOf(42.0); - Integer result = numValue.unwrap(VariationType.INTEGER); - assertEquals(Integer.valueOf(42), result); - - EppoValue negativeValue = EppoValue.valueOf(-17.0); - Integer negativeResult = negativeValue.unwrap(VariationType.INTEGER); - assertEquals(Integer.valueOf(-17), negativeResult); - } - - @Test - public void testUnwrapNumeric() { - EppoValue numValue = EppoValue.valueOf(123.456); - Double result = numValue.unwrap(VariationType.NUMERIC); - assertEquals(Double.valueOf(123.456), result); - - EppoValue intValue = EppoValue.valueOf(100.0); - Double intResult = intValue.unwrap(VariationType.NUMERIC); - assertEquals(Double.valueOf(100.0), intResult); - } - - @Test - public void testUnwrapString() { - EppoValue strValue = EppoValue.valueOf("hello world"); - String result = strValue.unwrap(VariationType.STRING); - assertEquals("hello world", result); - - EppoValue emptyValue = EppoValue.valueOf(""); - String emptyResult = emptyValue.unwrap(VariationType.STRING); - assertEquals("", emptyResult); - } - - @Test - public void testUnwrapJsonValid() { - String jsonString = "{\"foo\":\"bar\",\"count\":42}"; - EppoValue jsonValue = EppoValue.valueOf(jsonString); - JsonNode result = jsonValue.unwrap(VariationType.JSON); - - assertTrue(result.isObject()); - assertEquals("bar", result.get("foo").asText()); - assertEquals(42, result.get("count").asInt()); - } - - @Test - public void testUnwrapJsonArray() { - String jsonArrayString = "[1,2,3,4,5]"; - EppoValue jsonValue = EppoValue.valueOf(jsonArrayString); - JsonNode result = jsonValue.unwrap(VariationType.JSON); - - assertTrue(result.isArray()); - assertEquals(5, result.size()); - assertEquals(1, result.get(0).asInt()); - assertEquals(5, result.get(4).asInt()); - } - - @Test - public void testUnwrapJsonWithSpecialCharacters() { - String jsonString = "{\"a\":\"kümmert\",\"b\":\"schön\"}"; - EppoValue jsonValue = EppoValue.valueOf(jsonString); - JsonNode result = jsonValue.unwrap(VariationType.JSON); - - assertTrue(result.isObject()); - assertEquals("kümmert", result.get("a").asText()); - assertEquals("schön", result.get("b").asText()); - } - - @Test - public void testUnwrapJsonWithEmojis() { - String jsonString = "{\"a\":\"🤗\",\"b\":\"🌸\"}"; - EppoValue jsonValue = EppoValue.valueOf(jsonString); - JsonNode result = jsonValue.unwrap(VariationType.JSON); - - assertTrue(result.isObject()); - assertEquals("🤗", result.get("a").asText()); - assertEquals("🌸", result.get("b").asText()); - } - - @Test - public void testUnwrapJsonWithWhitespace() { - String jsonString = "{ \"key\": \"value\", \"number\": 123 }"; - EppoValue jsonValue = EppoValue.valueOf(jsonString); - JsonNode result = jsonValue.unwrap(VariationType.JSON); - - assertTrue(result.isObject()); - assertEquals("value", result.get("key").asText()); - assertEquals(123, result.get("number").asInt()); - } - - @Test - public void testUnwrapJsonInvalid() { - String invalidJson = "not valid json {"; - EppoValue jsonValue = EppoValue.valueOf(invalidJson); - JsonNode result = jsonValue.unwrap(VariationType.JSON); - - assertNull(result, "Invalid JSON should return null"); - } - - @Test - public void testUnwrapJsonEmpty() { - String emptyJson = "{}"; - EppoValue jsonValue = EppoValue.valueOf(emptyJson); - JsonNode result = jsonValue.unwrap(VariationType.JSON); - - assertTrue(result.isObject()); - assertEquals(0, result.size()); - } }