diff --git a/README.md b/README.md index 0455b95..2755017 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ [![Build](https://github.com/felipestanzani/jtoon/actions/workflows/build.yml/badge.svg)](https://github.com/felipestanzani/jtoon/actions/workflows/build.yml) [![Release](https://github.com/felipestanzani/jtoon/actions/workflows/release.yml/badge.svg)](https://github.com/felipestanzani/jtoon/actions/workflows/release.yml) [![Maven Central](https://img.shields.io/maven-central/v/com.felipestanzani/jtoon.svg)](https://central.sonatype.com/artifact/com.felipestanzani/jtoon) -![Coverage](.github/badges/jacoco.svg) **Token-Oriented Object Notation** is a compact, human-readable format designed for passing structured data to Large Language Models with significantly reduced token usage. @@ -67,7 +66,7 @@ JToon is available on Maven Central. Add it to your project using your preferred ```gradle dependencies { - implementation 'com.felipestanzani:jtoon:0.1.3' + implementation 'com.felipestanzani:jtoon:0.1.2' } ``` @@ -75,7 +74,7 @@ dependencies { ```kotlin dependencies { - implementation("com.felipestanzani:jtoon:0.1.3") + implementation("com.felipestanzani:jtoon:0.1.2") } ``` @@ -85,7 +84,7 @@ dependencies { com.felipestanzani jtoon - 0.1.3 + 0.1.2 ``` @@ -196,6 +195,10 @@ Number normalization examples: ### `JToon.encodeJson(String json, EncodeOptions options): String` +### `JToon.encodeXml(String xml): String` + +### `JToon.encodeXml(String xml, EncodeOptions options): String` + Converts any Java object or JSON-string to TOON format. **Parameters:** @@ -210,6 +213,10 @@ For `encodeJson` overloads: - `json` – A valid JSON string to be parsed and encoded. Invalid or blank JSON throws `IllegalArgumentException`. +For `encodeXml` overloads: + +- `xml` – A valid XML string to be parsed and encoded. Invalid or blank XML throws `IllegalArgumentException`. + **Returns:** A TOON-formatted string with no trailing newline or spaces. @@ -262,6 +269,58 @@ user: tags[2]: reading,gaming ``` +#### Encode XML + +```java +String xml = "John25"; +System.out.println(JToon.encodeXml(xml)); +``` + +Output: + +``` +user: + name: John + age: 25 +``` + +#### XML to TOON Conversion Use Cases + +XML to TOON conversion is particularly useful in scenarios where: + +- Legacy System Integration**: Converting XML APIs or data feeds from older systems to TOON for efficient LLM processing +- Configuration Files**: Transforming XML configuration files to TOON format for AI-assisted configuration analysis +- Data Exchange**: Converting XML data exchange formats to TOON for reduced token usage in AI conversations +- Log Analysis**: Processing XML formatted logs and converting them to TOON for AI-powered log analysis +- Web Services**: Converting SOAP XML responses or REST XML payloads to TOON for AI interpretation + +For example, converting a complex XML document: +```xml + + TechCorp + + + Engineering + 50 + + + Marketing + 20 + + + +``` + +To TOON: +``` +company: + name: TechCorp + departments[2]{name,employees}: + Engineering,50 + Marketing,20 +``` +This conversion provides significant token savings while maintaining the hierarchical structure of the original XML. + #### Delimiter Options The `delimiter` option allows you to choose between comma (default), tab, or pipe delimiters for array values and tabular rows. Alternative delimiters can provide additional token savings in specific contexts. diff --git a/build.gradle b/build.gradle index a3980c1..f91ce63 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,7 @@ jacoco { 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' @@ -75,4 +76,4 @@ jacocoTestCoverageVerification { } } } -check.dependsOn jacocoTestReport \ No newline at end of file +check.dependsOn jacocoTestReport diff --git a/src/main/java/com/felipestanzani/jtoon/JToon.java b/src/main/java/com/felipestanzani/jtoon/JToon.java index ff44767..379d180 100644 --- a/src/main/java/com/felipestanzani/jtoon/JToon.java +++ b/src/main/java/com/felipestanzani/jtoon/JToon.java @@ -3,8 +3,40 @@ 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; +/** + * Main API for encoding and decoding JToon format. + * + *

+ * JToon is a structured text format that represents JSON-like data in a more + * human-readable way, with support for tabular arrays and inline formatting. + *

+ * + *

Usage Examples:

+ * + *
{@code
+ * // Encode a Java object with default options
+ * String toon = JToon.encode(myObject);
+ *
+ * // Encode with custom options
+ * EncodeOptions options = new EncodeOptions(4, Delimiter.PIPE, true);
+ * String toon = JToon.encode(myObject, options);
+ *
+ * // 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);
+ *
+ * // Decode TOON directly to JSON string
+ * String json = JToon.decodeToJson(toon);
+ * }
+ */ public final class JToon { private JToon() { @@ -79,6 +111,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 d59a874..f585176 100644 --- a/src/test/java/com/felipestanzani/jtoon/JToonTest.java +++ b/src/test/java/com/felipestanzani/jtoon/JToonTest.java @@ -747,6 +747,115 @@ 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); + assertEquals("id: \"123\"\nname: Ada", result); + } + + @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); + assertEquals("name: John\nage: \"25\"", result); + } + + @Test + @DisplayName("encodes complex XML successfully") + void encodesComplexXmlSuccessfully() { + String xml = "TechCorpAlice"; + String result = JToon.encodeXml(xml); + assertEquals("name: TechCorp\nemployees:\n employee:\n name: Alice", result); + } + + @Test + @DisplayName("encodes XML with attributes") + void encodesXmlWithAttributes() { + String xml = "Johnjohn@example.com"; + String result = JToon.encodeXml(xml); + assertEquals("id: \"123\"\nactive: \"true\"\nname: John\nemail: john@example.com", result); + } + + @Test + @DisplayName("encodes deeply nested XML with arrays") + void encodesDeeplyNestedXmlWithArrays() { + String xml = "TechCorpEngineeringAliceDeveloperBobManagerMarketingCarolDirector"; + String result = JToon.encodeXml(xml); + assertEquals("name: TechCorp\ndepartments:\n department[2]:\n - name: Engineering\n employees:\n employee[2]{name,role}:\n Alice,Developer\n Bob,Manager\n - name: Marketing\n employees:\n employee:\n name: Carol\n role: Director", result); + } + + @Test + @DisplayName("encodes XML with mixed content and attributes") + void encodesXmlWithMixedContentAndAttributes() { + String xml = "The Great NovelJane DoeWelcome to the storyThe plot thickens"; + String result = JToon.encodeXml(xml); + String expected = "isbn: 978-3-16-148410-0\ncategory: fiction\ntitle: The Great Novel\nauthor:\n status: bestselling\n \"\": Jane Doe\nchapters:\n chapter[2]{number,title,\"\"}:\n \"1\",Introduction,Welcome to the story\n \"2\",Development,The plot thickens"; + assertEquals(expected, result); + } + } + + @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 {