diff --git a/build.gradle b/build.gradle index 1560d1d..61f9ec9 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,7 @@ repositories { dependencies { implementation 'tools.jackson.core:jackson-databind:3.0.2' implementation 'tools.jackson.module:jackson-module-afterburner:3.0.2' + implementation 'tools.jackson.dataformat:jackson-dataformat-xml:3.0.2' testImplementation platform('org.junit:junit-bom:6.0.1') testImplementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' @@ -38,4 +39,4 @@ dependencies { test { useJUnitPlatform() -} \ No newline at end of file +} diff --git a/src/main/java/com/felipestanzani/jtoon/JToon.java b/src/main/java/com/felipestanzani/jtoon/JToon.java index a3eeb13..a9946a9 100644 --- a/src/main/java/com/felipestanzani/jtoon/JToon.java +++ b/src/main/java/com/felipestanzani/jtoon/JToon.java @@ -3,6 +3,7 @@ import com.felipestanzani.jtoon.decoder.ValueDecoder; import com.felipestanzani.jtoon.encoder.ValueEncoder; import com.felipestanzani.jtoon.normalizer.JsonNormalizer; +import com.felipestanzani.jtoon.normalizer.XmlNormalizer; import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; @@ -27,6 +28,9 @@ * // Encode a plain JSON string directly * String toon = JToon.encodeJson("{\"id\":123,\"name\":\"Ada\"}"); * + * // Encode a plain XML string directly + * String toon = JToon.encodeXml("123Ada"); + * * // Decode TOON back to Java objects * Object result = JToon.decode(toon); * @@ -110,6 +114,42 @@ public static String encodeJson(String json, EncodeOptions options) { return ValueEncoder.encodeValue(parsed, options); } + /** + * Encodes a plain XML string to TOON format using default options. + * + *

+ * This is a convenience overload that parses the XML string and encodes it + * without requiring callers to create a {@code JsonNode} or intermediate + * objects. + *

+ * + * @param xml The XML string to encode (must be valid XML) + * @return The TOON-formatted string + * @throws IllegalArgumentException if the input is not valid XML + */ + public static String encodeXml(String xml) { + return encodeXml(xml, EncodeOptions.DEFAULT); + } + + /** + * Encodes a plain XML string to TOON format using custom options. + * + *

+ * Parsing is delegated to + * {@link com.felipestanzani.jtoon.normalizer.XmlNormalizer#parse(String)} + * to maintain separation of concerns. + *

+ * + * @param xml The XML string to encode (must be valid XML) + * @param options Encoding options (indent, delimiter, length marker) + * @return The TOON-formatted string + * @throws IllegalArgumentException if the input is not valid XML + */ + public static String encodeXml(String xml, EncodeOptions options) { + JsonNode parsed = XmlNormalizer.parse(xml); + return ValueEncoder.encodeValue(parsed, options); + } + /** * Decodes a TOON-formatted string to Java objects using default options. * diff --git a/src/main/java/com/felipestanzani/jtoon/normalizer/XmlNormalizer.java b/src/main/java/com/felipestanzani/jtoon/normalizer/XmlNormalizer.java new file mode 100644 index 0000000..d0d3667 --- /dev/null +++ b/src/main/java/com/felipestanzani/jtoon/normalizer/XmlNormalizer.java @@ -0,0 +1,40 @@ +package com.felipestanzani.jtoon.normalizer; + +import tools.jackson.databind.JsonNode; +import tools.jackson.dataformat.xml.XmlMapper; + +/** + * Normalizes XML strings to Jackson JsonNode representation. + * Converts XML structure to JSON-compatible format for TOON encoding. + */ +public final class XmlNormalizer { + + private static final XmlMapper XML_MAPPER = XmlMapper.builder().build(); + + private XmlNormalizer() { + throw new UnsupportedOperationException("Utility class cannot be instantiated"); + } + + /** + * Parses an XML string into a JsonNode using the shared XmlMapper. + *

+ * This centralizes XML parsing concerns to keep the public API thin and + * maintain separation of responsibilities between parsing, normalization, + * and encoding. + *

+ * + * @param xml The XML string to parse (must be valid XML) + * @return Parsed JsonNode + * @throws IllegalArgumentException if the input is blank or not valid XML + */ + public static JsonNode parse(String xml) { + if (xml == null || xml.trim().isEmpty()) { + throw new IllegalArgumentException("Invalid XML"); + } + try { + return XML_MAPPER.readTree(xml); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid XML", e); + } + } +} diff --git a/src/test/java/com/felipestanzani/jtoon/JToonTest.java b/src/test/java/com/felipestanzani/jtoon/JToonTest.java index ab23250..29452f8 100644 --- a/src/test/java/com/felipestanzani/jtoon/JToonTest.java +++ b/src/test/java/com/felipestanzani/jtoon/JToonTest.java @@ -745,6 +745,94 @@ void noTrailingNewline() { } } + @Nested + @DisplayName("XML tests") + class XmlTests { + + @Test + @DisplayName("encodes XML with custom options") + void encodesXmlWithOptions() { + String xml = "123Ada"; + EncodeOptions options = new EncodeOptions(4, Delimiter.PIPE, true); + String result = JToon.encodeXml(xml, options); + // Basic check that custom options work + assertNotNull(result); + assertTrue(result.length() > 0); + } + + @Test + @DisplayName("throws exception for invalid XML") + void throwsForInvalidXml() { + String invalidXml = "123Ada"; + assertThrows(IllegalArgumentException.class, () -> JToon.encodeXml(invalidXml)); + } + + @Test + @DisplayName("throws exception for null XML") + void throwsForNullXml() { + assertThrows(IllegalArgumentException.class, () -> JToon.encodeXml(null)); + } + + @Test + @DisplayName("throws exception for empty XML") + void throwsForEmptyXml() { + assertThrows(IllegalArgumentException.class, () -> JToon.encodeXml("")); + } + + @Nested + @DisplayName("XML structures (positive test cases)") + class XmlStructuresPositive { + + @Test + @DisplayName("encodes XML successfully") + void encodesXmlSuccessfully() { + String xml = "John25"; + String result = JToon.encodeXml(xml); + assertNotNull(result); + assertTrue(result.length() > 0); + } + + @Test + @DisplayName("encodes complex XML successfully") + void encodesComplexXmlSuccessfully() { + String xml = "TechCorpAlice"; + String result = JToon.encodeXml(xml); + assertNotNull(result); + assertTrue(result.length() > 0); + } + } + + @Nested + @DisplayName("XML error handling (negative test cases)") + class XmlErrorHandling { + + @Test + @DisplayName("throws exception for invalid XML") + void throwsForInvalidXml() { + String invalidXml = "123Ada"; + assertThrows(IllegalArgumentException.class, () -> JToon.encodeXml(invalidXml)); + } + + @Test + @DisplayName("throws exception for null XML input") + void throwsForNullXml() { + assertThrows(IllegalArgumentException.class, () -> JToon.encodeXml(null)); + } + + @Test + @DisplayName("throws exception for empty XML string") + void throwsForEmptyXml() { + assertThrows(IllegalArgumentException.class, () -> JToon.encodeXml("")); + } + + @Test + @DisplayName("throws exception for whitespace-only XML") + void throwsForWhitespaceOnlyXml() { + assertThrows(IllegalArgumentException.class, () -> JToon.encodeXml(" ")); + } + } + } + @Nested @DisplayName("non-JSON-serializable values") class NonJson {