Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ 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'
}

test {
useJUnitPlatform()
}
}
40 changes: 40 additions & 0 deletions src/main/java/com/felipestanzani/jtoon/JToon.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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("<user><id>123</id><name>Ada</name></user>");
*
* // Decode TOON back to Java objects
* Object result = JToon.decode(toon);
*
Expand Down Expand Up @@ -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.
*
* <p>
* This is a convenience overload that parses the XML string and encodes it
* without requiring callers to create a {@code JsonNode} or intermediate
* objects.
* </p>
*
* @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.
*
* <p>
* Parsing is delegated to
* {@link com.felipestanzani.jtoon.normalizer.XmlNormalizer#parse(String)}
* to maintain separation of concerns.
* </p>
*
* @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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* This centralizes XML parsing concerns to keep the public API thin and
* maintain separation of responsibilities between parsing, normalization,
* and encoding.
* </p>
*
* @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);
}
}
}
88 changes: 88 additions & 0 deletions src/test/java/com/felipestanzani/jtoon/JToonTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,94 @@ void noTrailingNewline() {
}
}

@Nested
@DisplayName("XML tests")
class XmlTests {

@Test
@DisplayName("encodes XML with custom options")
void encodesXmlWithOptions() {
String xml = "<user><id>123</id><name>Ada</name></user>";
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 = "<user><id>123</id><name>Ada</name>";
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 = "<user><name>John</name><age>25</age></user>";
String result = JToon.encodeXml(xml);
assertNotNull(result);
assertTrue(result.length() > 0);
}

@Test
@DisplayName("encodes complex XML successfully")
void encodesComplexXmlSuccessfully() {
String xml = "<company><name>TechCorp</name><employees><employee><name>Alice</name></employee></employees></company>";
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 = "<user><id>123</id><name>Ada</name>";
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 {
Expand Down