diff --git a/src/main/java/cloud/eppo/BaseEppoClient.java b/src/main/java/cloud/eppo/BaseEppoClient.java index 8e2fb8f..92bd89a 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,9 @@ 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 +428,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 +474,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 aee8bae..3436527 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 f2830ad..791e624 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()); + } }