Skip to content
Open
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
67 changes: 63 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -67,15 +66,15 @@ 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'
}
```

**Gradle (Kotlin DSL):**

```kotlin
dependencies {
implementation("com.felipestanzani:jtoon:0.1.3")
implementation("com.felipestanzani:jtoon:0.1.2")
}
```

Expand All @@ -85,7 +84,7 @@ dependencies {
<dependency>
<groupId>com.felipestanzani</groupId>
<artifactId>jtoon</artifactId>
<version>0.1.3</version>
<version>0.1.2</version>
</dependency>
```

Expand Down Expand Up @@ -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:**
Expand All @@ -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.
Expand Down Expand Up @@ -262,6 +269,58 @@ user:
tags[2]: reading,gaming
```

#### Encode XML

```java
String xml = "<user><name>John</name><age>25</age></user>";
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
<company>
<name>TechCorp</name>
<departments>
<department>
<name>Engineering</name>
<employees>50</employees>
</department>
<department>
<name>Marketing</name>
<employees>20</employees>
</department>
</departments>
</company>
```

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.
Expand Down
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -75,4 +76,4 @@ jacocoTestCoverageVerification {
}
}
}
check.dependsOn jacocoTestReport
check.dependsOn jacocoTestReport
68 changes: 68 additions & 0 deletions src/main/java/com/felipestanzani/jtoon/JToon.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>
* 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.
* </p>
*
* <h2>Usage Examples:</h2>
*
* <pre>{@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("<user><id>123</id><name>Ada</name></user>");
*
* // Decode TOON back to Java objects
* Object result = JToon.decode(toon);
*
* // Decode TOON directly to JSON string
* String json = JToon.decodeToJson(toon);
* }</pre>
*/
public final class JToon {

private JToon() {
Expand Down Expand Up @@ -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.
*
* <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);
}
}
}
109 changes: 109 additions & 0 deletions src/test/java/com/felipestanzani/jtoon/JToonTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,115 @@ 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);
assertEquals("id: \"123\"\nname: Ada", result);
}

@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);
assertEquals("name: John\nage: \"25\"", result);
}

@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);
assertEquals("name: TechCorp\nemployees:\n employee:\n name: Alice", result);
}

@Test
@DisplayName("encodes XML with attributes")
void encodesXmlWithAttributes() {
String xml = "<user id=\"123\" active=\"true\"><name>John</name><email>[email protected]</email></user>";
String result = JToon.encodeXml(xml);
assertEquals("id: \"123\"\nactive: \"true\"\nname: John\nemail: [email protected]", result);
}

@Test
@DisplayName("encodes deeply nested XML with arrays")
void encodesDeeplyNestedXmlWithArrays() {
String xml = "<company><name>TechCorp</name><departments><department><name>Engineering</name><employees><employee><name>Alice</name><role>Developer</role></employee><employee><name>Bob</name><role>Manager</role></employee></employees></department><department><name>Marketing</name><employees><employee><name>Carol</name><role>Director</role></employee></employees></department></departments></company>";
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 = "<book isbn=\"978-3-16-148410-0\" category=\"fiction\"><title>The Great Novel</title><author status=\"bestselling\">Jane Doe</author><chapters><chapter number=\"1\" title=\"Introduction\">Welcome to the story</chapter><chapter number=\"2\" title=\"Development\">The plot thickens</chapter></chapters></book>";
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 = "<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
Loading