diff --git a/TestLiteralEval.java b/TestLiteralEval.java index 97d80f3..5e36d32 100644 --- a/TestLiteralEval.java +++ b/TestLiteralEval.java @@ -37,6 +37,11 @@ public static void main(String[] args) { testUnterminatedDict(); testUnterminatedTuple(); testNonStringDictKey(); + testJsonKeywordTrue(); + testJsonKeywordFalse(); + testJsonKeywordNull(); + testJsonMixedList(); + testJsonRoundTrip(); System.out.println("\n=== Results: " + passed + " passed, " + failed + " failed out of " + (passed + failed) + " tests ==="); if (failed > 0) { @@ -267,4 +272,42 @@ static void testNonStringDictKey() { check("numeric dict key throws", true, e.getMessage().contains("Dict keys must be non-null strings")); } } + + // --- JSON keyword interop tests --- + + static void testJsonKeywordTrue() { + Object result = concoredocker.literalEval("true"); + check("json true -> Boolean.TRUE", Boolean.TRUE, result); + } + + static void testJsonKeywordFalse() { + Object result = concoredocker.literalEval("false"); + check("json false -> Boolean.FALSE", Boolean.FALSE, result); + } + + static void testJsonKeywordNull() { + Object result = concoredocker.literalEval("null"); + check("json null -> null", null, result); + } + + static void testJsonMixedList() { + // Python sends [0.0, true, null] via json.dumps — Java must parse it + @SuppressWarnings("unchecked") + List result = (List) concoredocker.literalEval("[0.0, true, null]"); + check("json list[0] simtime", 0.0, result.get(0)); + check("json list[1] true", Boolean.TRUE, result.get(1)); + check("json list[2] null", null, result.get(2)); + } + + static void testJsonRoundTrip() { + // JSON-style payload: true/false/null and double-quoted strings + String jsonPayload = "[0.0, 1.5, true, false, null, \"hello\"]"; + @SuppressWarnings("unchecked") + List result = (List) concoredocker.literalEval(jsonPayload); + check("json round-trip double", 1.5, result.get(1)); + check("json round-trip true", Boolean.TRUE, result.get(2)); + check("json round-trip false", Boolean.FALSE, result.get(3)); + check("json round-trip null", null, result.get(4)); + check("json round-trip string", "hello", result.get(5)); + } } diff --git a/concoredocker.java b/concoredocker.java index 1e66bca..80baf5b 100644 --- a/concoredocker.java +++ b/concoredocker.java @@ -281,6 +281,60 @@ private static String toPythonLiteral(Object obj) { return obj.toString(); } + /** + * Escapes a Java string so it can be safely embedded in a JSON double-quoted string. + * Escapes backslash, double quote, newline, carriage return, and tab. + */ + private static String escapeJsonString(String s) { + StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '\\': sb.append("\\\\"); break; + case '"': sb.append("\\\""); break; + case '\n': sb.append("\\n"); break; + case '\r': sb.append("\\r"); break; + case '\t': sb.append("\\t"); break; + default: sb.append(c); break; + } + } + return sb.toString(); + } + + /** + * Converts a Java object to its JSON string representation. + * true/false/null instead of True/False/None; strings double-quoted. + */ + private static String toJsonLiteral(Object obj) { + if (obj == null) return "null"; + if (obj instanceof Boolean) return ((Boolean) obj) ? "true" : "false"; + if (obj instanceof String) return "\"" + escapeJsonString((String) obj) + "\""; + if (obj instanceof Number) return obj.toString(); + if (obj instanceof List) { + List list = (List) obj; + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < list.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(toJsonLiteral(list.get(i))); + } + sb.append("]"); + return sb.toString(); + } + if (obj instanceof Map) { + Map map = (Map) obj; + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (Map.Entry entry : map.entrySet()) { + if (!first) sb.append(", "); + sb.append(toJsonLiteral(entry.getKey())).append(": ").append(toJsonLiteral(entry.getValue())); + first = false; + } + sb.append("}"); + return sb.toString(); + } + return obj.toString(); + } + /** * Writes data to a port file. * Prepends simtime+delta to the value list, then serializes to Python-literal format. @@ -437,10 +491,10 @@ public static void write(String portName, String name, Object val, int delta) { if (val instanceof List) { List listVal = (List) val; StringBuilder sb = new StringBuilder("["); - sb.append(toPythonLiteral(simtime + delta)); + sb.append(toJsonLiteral(simtime + delta)); for (Object o : listVal) { sb.append(", "); - sb.append(toPythonLiteral(o)); + sb.append(toJsonLiteral(o)); } sb.append("]"); payload = sb.toString(); @@ -750,9 +804,9 @@ Object parseKeyword() { } String word = input.substring(start, pos); switch (word) { - case "True": return Boolean.TRUE; - case "False": return Boolean.FALSE; - case "None": return null; + case "True": case "true": return Boolean.TRUE; + case "False": case "false": return Boolean.FALSE; + case "None": case "null": return null; default: throw new IllegalArgumentException("Unknown keyword: '" + word + "' at position " + start); } }