From 405c9938867f750a566f58ed5a80e1d0126acee9 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Wed, 10 Dec 2025 09:59:17 -0500 Subject: [PATCH 1/4] roll back profiling increase --- 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 3787a7dc..3dd24802 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 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; + // 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; assertTrue( elapsedTime < maxAllowedTime, "Cpu time of " + elapsedTime + " is more than the " + maxAllowedTime + " allowed"); From 90bc73c7456d4aadc4680a0f19f1ec2563aae02c Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Wed, 10 Dec 2025 10:09:46 -0500 Subject: [PATCH 2/4] bring back unwrap after chunking order change and rebase --- src/main/java/cloud/eppo/BaseEppoClient.java | 22 +--- src/main/java/cloud/eppo/api/EppoValue.java | 35 ++++- .../java/cloud/eppo/api/EppoValueTest.java | 123 ++++++++++++++++++ 3 files changed, 160 insertions(+), 20 deletions(-) diff --git a/src/main/java/cloud/eppo/BaseEppoClient.java b/src/main/java/cloud/eppo/BaseEppoClient.java index 8e2fb8fd..3189afbc 100644 --- a/src/main/java/cloud/eppo/BaseEppoClient.java +++ b/src/main/java/cloud/eppo/BaseEppoClient.java @@ -11,10 +11,7 @@ import cloud.eppo.logging.BanditAssignment; import cloud.eppo.logging.BanditLogger; import cloud.eppo.ufc.dto.*; -import cloud.eppo.ufc.dto.adapters.EppoModule; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import java.util.HashMap; import java.util.Map; import java.util.Timer; @@ -27,12 +24,7 @@ public class BaseEppoClient { private static final Logger log = LoggerFactory.getLogger(BaseEppoClient.class); - private final ObjectMapper mapper = - new ObjectMapper() - .registerModule(EppoModule.eppoModule()); // TODO: is this the best place for this? - protected final ConfigurationRequestor requestor; - private final IConfigurationStore configurationStore; private final AssignmentLogger assignmentLogger; private final BanditLogger banditLogger; @@ -311,8 +303,8 @@ private boolean valueTypeMatchesExpected(VariationType expectedType, EppoValue v case JSON: typeMatch = value.isString() - // Eppo leaves JSON as a JSON string; to verify it's valid we attempt to parse - && parseJsonString(value.stringValue()) != null; + // Eppo leaves JSON as a JSON string; to verify it's valid we attempt to parse (via unwrapping) + && value.unwrap(VariationType.JSON) != null; break; default: throw new IllegalArgumentException("Unexpected type for type checking: " + expectedType); @@ -435,7 +427,7 @@ public JsonNode getJSONAssignment( subjectAttributes, EppoValue.valueOf(defaultValue.toString()), VariationType.JSON); - return parseJsonString(value.stringValue()); + return value.unwrap(VariationType.JSON); } catch (Exception e) { return throwIfNotGraceful(e, defaultValue); } @@ -481,14 +473,6 @@ public String getJSONStringAssignment(String flagKey, String subjectKey, String return this.getJSONStringAssignment(flagKey, subjectKey, new Attributes(), defaultValue); } - private JsonNode parseJsonString(String jsonString) { - try { - return mapper.readTree(jsonString); - } catch (JsonProcessingException e) { - return null; - } - } - public BanditResult getBanditAction( String flagKey, String subjectKey, diff --git a/src/main/java/cloud/eppo/api/EppoValue.java b/src/main/java/cloud/eppo/api/EppoValue.java index aee8bae5..34365276 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,36 @@ 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) { @@ -106,7 +139,7 @@ public String toString() { case STRING: return this.stringValue; case ARRAY_OF_STRING: - // Android21 back-compatability + // Android21 back-compatibility return joinStringArray(this.stringArrayValue); case NULL: return ""; diff --git a/src/test/java/cloud/eppo/api/EppoValueTest.java b/src/test/java/cloud/eppo/api/EppoValueTest.java index f2830ad2..791e624a 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 = 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()); + } } From 7b3d458e06c0363ef5b041cd2ff446849270b151 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Wed, 10 Dec 2025 10:22:55 -0500 Subject: [PATCH 3/4] don't rollback profile change --- 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 53024b8d77ff22b677115088e4364d8ce77062d9 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Wed, 10 Dec 2025 10:24:44 -0500 Subject: [PATCH 4/4] appease linter --- src/main/java/cloud/eppo/BaseEppoClient.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/cloud/eppo/BaseEppoClient.java b/src/main/java/cloud/eppo/BaseEppoClient.java index 3189afbc..92bd89ac 100644 --- a/src/main/java/cloud/eppo/BaseEppoClient.java +++ b/src/main/java/cloud/eppo/BaseEppoClient.java @@ -303,7 +303,8 @@ private boolean valueTypeMatchesExpected(VariationType expectedType, EppoValue v case JSON: typeMatch = value.isString() - // Eppo leaves JSON as a JSON string; to verify it's valid we attempt to parse (via unwrapping) + // Eppo leaves JSON as a JSON string; to verify it's valid we attempt to parse (via + // unwrapping) && value.unwrap(VariationType.JSON) != null; break; default: