diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b8ffb6..9976324 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -106,11 +106,8 @@ This project uses [Gradle Dependency Verification](https://docs.gradle.org/curre When you **add or update dependencies**, you must regenerate the verification metadata before pushing: ```bash -# Regenerate verification metadata (includes all configurations) -./gradlew build --write-verification-metadata sha256 - -# If you also use CycloneDX SBOM generation, update that too -./gradlew cyclonedxBom --write-verification-metadata sha256 +# Regenerate verification metadata (includes build and CycloneDX SBOM configurations) +./gradlew --write-verification-metadata sha256 build cyclonedxBom -x test ``` Commit the updated `gradle/verification-metadata.xml` with your dependency changes. The CI build will **fail** if checksums are missing. diff --git a/README.md b/README.md index 93c1d03..d1bda16 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Release](https://github.com/toon-format/toon-java/actions/workflows/release.yml/badge.svg)](https://github.com/toon-format/toon-java/actions/workflows/release.yml) [![Maven Central](https://img.shields.io/maven-central/v/dev.toonformat/jtoon.svg)](https://central.sonatype.com/artifact/dev.toonformat/jtoon) ![Coverage](.github/badges/jacoco.svg) -[![SPEC v3.3](https://img.shields.io/badge/spec-v3.3-fef3c0?labelColor=1b1b1f)](https://github.com/toon-format/spec) +[![SPEC v3.3.2](https://img.shields.io/badge/spec-v3.3.2-fef3c0?labelColor=1b1b1f)](https://github.com/toon-format/spec) [![License: MIT](https://img.shields.io/badge/license-MIT-fef3c0?labelColor=1b1b1f)](./LICENSE) Compact, human-readable serialization format for LLM contexts with **30-60% token reduction** vs JSON. Combines YAML-like indentation with CSV-like tabular arrays. Working towards full compatibility with the [official TOON specification](https://github.com/toon-format/spec). @@ -351,12 +351,25 @@ Object result2 = JToon.decode(invalidToon, lenient); **CI/CD:** GitHub Actions • Java 17 • Coverage enforcement • PR coverage comments +## Development + +```bash +# Build (includes tests, checks, coverage) +./gradlew build + +# Run tests only +./gradlew test + +# Update dependency verification metadata (required when adding/updating dependencies) +./gradlew --write-verification-metadata sha256 build cyclonedxBom -x test +``` + +See [CONTRIBUTING.md](CONTRIBUTING.md) for full development guidelines. + ## Project Status This project is 100% compliant with TOON specification. Release conformance enforced on CI/CD. -See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines. - ## Documentation - [📘 Full Documentation](docs/) - Extended guides and references diff --git a/build.gradle b/build.gradle index 9fc8575..bd93fec 100644 --- a/build.gradle +++ b/build.gradle @@ -3,11 +3,11 @@ plugins { id 'maven-publish' id 'signing' id 'jacoco' - id 'com.github.spotbugs' version '6.5.8' + id 'com.github.spotbugs' version '6.5.9' id 'pmd' id 'checkstyle' id 'org.owasp.dependencycheck' version '12.2.2' - id 'org.cyclonedx.bom' version '3.2.4' + id 'org.cyclonedx.bom' version '3.3.0' id 'info.solidsoft.pitest' version '1.19.0' id 'net.ltgt.errorprone' version '5.1.0' } @@ -146,7 +146,7 @@ tasks.checkstyleTest { dependencies { implementation 'tools.jackson.core:jackson-databind:3.2.1' implementation 'tools.jackson.module:jackson-module-blackbird:3.2.1' - compileOnly 'com.github.spotbugs:spotbugs-annotations:4.10.2' + compileOnly 'com.github.spotbugs:spotbugs-annotations:4.10.3' // NullAway + Error Prone for compile-time null safety errorprone 'com.uber.nullaway:nullaway:0.13.7' @@ -158,7 +158,7 @@ dependencies { } api 'org.jspecify:jspecify:1.0.0' - testImplementation platform('org.junit:junit-bom:6.1.1') + testImplementation platform('org.junit:junit-bom:6.1.2') testImplementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testImplementation 'org.awaitility:awaitility:4.3.0' diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 391025f..3191973 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -247,6 +247,11 @@ + + + + + @@ -269,6 +274,17 @@ + + + + + + + + + + + @@ -288,6 +304,14 @@ + + + + + + + + @@ -1110,6 +1134,11 @@ + + + + + @@ -1563,6 +1592,11 @@ + + + + + @@ -1571,6 +1605,14 @@ + + + + + + + + @@ -1579,6 +1621,14 @@ + + + + + + + + @@ -1587,6 +1637,14 @@ + + + + + + + + @@ -1595,6 +1653,14 @@ + + + + + + + + @@ -1603,6 +1669,14 @@ + + + + + + + + @@ -1618,6 +1692,11 @@ + + + + + @@ -1626,6 +1705,14 @@ + + + + + + + + @@ -1634,6 +1721,14 @@ + + + + + + + + @@ -1642,6 +1737,14 @@ + + + + + + + + @@ -1650,6 +1753,14 @@ + + + + + + + + @@ -1658,6 +1769,14 @@ + + + + + + + + @@ -1879,6 +1998,11 @@ + + + + + @@ -1897,6 +2021,14 @@ + + + + + + + + @@ -1979,6 +2111,14 @@ + + + + + + + + @@ -2003,11 +2143,24 @@ + + + + + + + + + + + + + @@ -2310,6 +2463,14 @@ + + + + + + + + @@ -2350,6 +2511,14 @@ + + + + + + + + @@ -2361,6 +2530,17 @@ + + + + + + + + + + + @@ -2372,6 +2552,17 @@ + + + + + + + + + + + @@ -2383,6 +2574,17 @@ + + + + + + + + + + + @@ -2394,6 +2596,17 @@ + + + + + + + + + + + @@ -2405,6 +2618,17 @@ + + + + + + + + + + + @@ -2416,6 +2640,17 @@ + + + + + + + + + + + @@ -2427,6 +2662,17 @@ + + + + + + + + + + + diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java index dbfccc3..3d63321 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java @@ -102,9 +102,15 @@ private static Object decodeInternal(final String toon, final DecodeOptions opti } // Handle keyed arrays: items[2]{id,name}: + // Only match if the key part (before the bracket) doesn't contain a colon, + // because a colon indicates a key-value pair (e.g. 'a: "[2]: x"') final Matcher keyedArray = KEYED_ARRAY_PATTERN.matcher(line); if (keyedArray.matches()) { - return KeyDecoder.parseKeyedArrayValue(keyedArray, line, depth, context); + final String keyPart = keyedArray.group(1); + final int colonInKey = DecodeHelper.findUnquotedColon(keyPart); + if (colonInKey <= 0) { + return KeyDecoder.parseKeyedArrayValue(keyedArray, line, depth, context); + } } // Handle key-value pairs: name: Ada final int colonIdx = DecodeHelper.findUnquotedColon(line); diff --git a/src/test/resources/conformance/decode/arrays-nested.json b/src/test/resources/conformance/decode/arrays-nested.json index fad16f6..f66e6de 100644 --- a/src/test/resources/conformance/decode/arrays-nested.json +++ b/src/test/resources/conformance/decode/arrays-nested.json @@ -81,8 +81,7 @@ } ] }, - "specSection": "10", - "note": "Single-field list-item object: only the tabular array, no sibling fields" + "specSection": "10" }, { "name": "parses objects containing arrays (including empty arrays) in list format", diff --git a/src/test/resources/conformance/decode/arrays-primitive.json b/src/test/resources/conformance/decode/arrays-primitive.json index 7e948ba..63ff03f 100644 --- a/src/test/resources/conformance/decode/arrays-primitive.json +++ b/src/test/resources/conformance/decode/arrays-primitive.json @@ -107,6 +107,14 @@ }, "specSection": "9.1" }, + { + "name": "parses quoted key containing a colon with inline array", + "input": "\"a:b\"[2]: 1,2", + "expected": { + "a:b": [1, 2] + }, + "specSection": "9.1" + }, { "name": "parses quoted key with empty array", "input": "\"x-custom\"[0]:", diff --git a/src/test/resources/conformance/decode/arrays-tabular.json b/src/test/resources/conformance/decode/arrays-tabular.json index 5adb1cc..636e2bc 100644 --- a/src/test/resources/conformance/decode/arrays-tabular.json +++ b/src/test/resources/conformance/decode/arrays-tabular.json @@ -80,6 +80,19 @@ "count": 2 }, "specSection": "9.3" + }, + { + "name": "treats a key-value line at header depth whose value contains the active delimiter as end of rows, not a row", + "input": "t[2]{a,b}:\n 1,2\n 3,4\nx: 3,4", + "expected": { + "t": [ + { "a": 1, "b": 2 }, + { "a": 3, "b": 4 } + ], + "x": "3,4" + }, + "specSection": "9.3", + "note": "The dedent to the header's depth closes the rows (§8); the line is a key-value pair, and the whole post-colon token is the value, non-numeric so a string (§11.2)" } ] } diff --git a/src/test/resources/conformance/decode/delimiters.json b/src/test/resources/conformance/decode/delimiters.json index 71191fd..021505b 100644 --- a/src/test/resources/conformance/decode/delimiters.json +++ b/src/test/resources/conformance/decode/delimiters.json @@ -159,7 +159,7 @@ "note": "a,b" }, "specSection": "11", - "note": "Object values don't require comma quoting regardless of delimiter" + "note": "Object field values are parsed as the whole post-colon token, never split on a delimiter (§11.2)" }, { "name": "object values in list items follow document delimiter", diff --git a/src/test/resources/conformance/decode/numbers.json b/src/test/resources/conformance/decode/numbers.json index a4d4e3c..d15cdac 100644 --- a/src/test/resources/conformance/decode/numbers.json +++ b/src/test/resources/conformance/decode/numbers.json @@ -86,8 +86,7 @@ "expected": { "value": 5 }, - "specSection": "4", - "note": "Exponent +00 results in the integer 5" + "specSection": "4" }, { "name": "parses zero with exponent as number", diff --git a/src/test/resources/conformance/decode/objects.json b/src/test/resources/conformance/decode/objects.json index 47a1b75..68b249e 100644 --- a/src/test/resources/conformance/decode/objects.json +++ b/src/test/resources/conformance/decode/objects.json @@ -122,6 +122,50 @@ }, "specSection": "8" }, + { + "name": "parses quoted object value shaped like an inline array header", + "input": "a: \"[2]: x\"", + "expected": { + "a": "[2]: x" + }, + "specSection": "8", + "note": "Quoted value is opaque (§4); the bracket-and-colon shape inside the quotes is data, not an array header (§6)" + }, + { + "name": "parses quoted object value shaped like a count-matching inline array header", + "input": "a: \"[1]: x\"", + "expected": { + "a": "[1]: x" + }, + "specSection": "8", + "note": "Quoting makes the value opaque (§4), so it is never parsed as a header – the matching count is irrelevant because a quoted scalar is never a header candidate" + }, + { + "name": "parses unquoted value shaped like an inline array header after the key", + "input": "key: foo [2]: bar", + "expected": { + "key": "foo [2]: bar" + }, + "specSection": "6", + "note": "An unquoted key cannot contain a colon, so the first colon ends the key; the bracket-and-colon shape in the remainder is value text, not an array header" + }, + { + "name": "parses unquoted value shaped like a tabular array header after the key", + "input": "a: b [2]{x,y}: 1,2", + "expected": { + "a": "b [2]{x,y}: 1,2" + }, + "specSection": "6", + "note": "An unquoted key cannot contain a colon, so the first colon ends the key; the tabular-header shape in the remainder is value text, not an array header" + }, + { + "name": "parses quoted object value with bracket text and a later colon", + "input": "content: \"accept (process.argv[2]) and greet: name\"", + "expected": { + "content": "accept (process.argv[2]) and greet: name" + }, + "specSection": "8" + }, { "name": "parses quoted key with colon", "input": "\"order:id\": 7", @@ -286,6 +330,14 @@ }, "specSection": "8" }, + { + "name": "unescapes tab in key immediately followed by a colon", + "input": "\"\\t:x\": v", + "expected": { + "\t:x": "v" + }, + "specSection": "8" + }, { "name": "unescapes quotes in key", "input": "\"he said \\\"hi\\\"\": 1", diff --git a/src/test/resources/conformance/decode/validation-errors.json b/src/test/resources/conformance/decode/validation-errors.json index c525b46..a50b28b 100644 --- a/src/test/resources/conformance/decode/validation-errors.json +++ b/src/test/resources/conformance/decode/validation-errors.json @@ -185,6 +185,42 @@ "note": "[-1] is not a non-negative integer length; decoders MUST NOT interpret it as a bracket segment", "minSpecVersion": "3.2" }, + { + "name": "throws on decimal bracket length in strict mode", + "input": "x[3.7]: a,b,c", + "expected": null, + "shouldError": true, + "options": { + "strict": true + }, + "specSection": "6", + "note": "[3.7] is not an integer length (§6 length is DIGIT-only); decoders MUST NOT interpret it as a bracket segment", + "minSpecVersion": "3.2" + }, + { + "name": "throws on bracket length with plus sign in strict mode", + "input": "x[+3]: a,b,c", + "expected": null, + "shouldError": true, + "options": { + "strict": true + }, + "specSection": "6", + "note": "[+3] carries a sign and is not a non-negative integer length; decoders MUST NOT interpret it as a bracket segment", + "minSpecVersion": "3.2" + }, + { + "name": "throws on bracket length in exponent form in strict mode", + "input": "x[1e1]: 1,2,3,4,5,6,7,8,9,10", + "expected": null, + "shouldError": true, + "options": { + "strict": true + }, + "specSection": "6", + "note": "[1e1] is exponent notation, not a DIGIT-only integer length; decoders MUST NOT interpret it as a bracket segment", + "minSpecVersion": "3.2" + }, { "name": "throws on whitespace between bracket segment and colon in strict mode", "input": "items[2] :\n 1,2", diff --git a/src/test/resources/conformance/decode/whitespace.json b/src/test/resources/conformance/decode/whitespace.json index 7584e13..f2c51d2 100644 --- a/src/test/resources/conformance/decode/whitespace.json +++ b/src/test/resources/conformance/decode/whitespace.json @@ -37,8 +37,7 @@ { "id": 2, "name": "Bob" } ] }, - "specSection": "12", - "note": "Values in tabular rows are trimmed" + "specSection": "12" }, { "name": "tolerates spaces around delimiters with quoted values", diff --git a/src/test/resources/conformance/encode/arrays-objects.json b/src/test/resources/conformance/encode/arrays-objects.json index 9646619..2688b85 100644 --- a/src/test/resources/conformance/encode/arrays-objects.json +++ b/src/test/resources/conformance/encode/arrays-objects.json @@ -59,7 +59,7 @@ }, "expected": "items[1]:\n - users[2]{id,name}:\n 1,Ada\n 2,Bob\n status: active", "specSection": "10", - "note": "YAML-style encoding for list-item objects with tabular array as first field" + "note": "Tabular header on hyphen line with rows at depth +2 and sibling fields at depth +1 (§10)" }, { "name": "uses list format for nested object arrays with mismatched keys", diff --git a/src/test/resources/conformance/encode/key-folding.json b/src/test/resources/conformance/encode/key-folding.json index 367732a..a96b34f 100644 --- a/src/test/resources/conformance/encode/key-folding.json +++ b/src/test/resources/conformance/encode/key-folding.json @@ -133,7 +133,7 @@ "flattenDepth": 0 }, "specSection": "13.4", - "note": "flattenDepth=0 disables all folding" + "note": "flattenDepth below 2 has no practical effect, leaving standard nesting (§13.4)" }, { "name": "encodes standard nesting with keyFolding=off (baseline)", diff --git a/src/test/resources/conformance/encode/primitives.json b/src/test/resources/conformance/encode/primitives.json index da3b19b..373f536 100644 --- a/src/test/resources/conformance/encode/primitives.json +++ b/src/test/resources/conformance/encode/primitives.json @@ -63,7 +63,7 @@ "input": "05", "expected": "\"05\"", "specSection": "7.2", - "note": "Leading zeros make it non-numeric" + "note": "Matches the numeric-like pattern (05 is a listed §7.2 example), so it MUST be quoted" }, { "name": "escapes newline in string", @@ -198,8 +198,7 @@ "name": "encodes negative zero as zero", "input": -0, "expected": "0", - "specSection": "2", - "note": "Negative zero normalizes to zero" + "specSection": "2" }, { "name": "encodes scientific notation as decimal", diff --git a/src/test/resources/conformance/encode/whitespace.json b/src/test/resources/conformance/encode/whitespace.json index e370d59..27bb3a0 100644 --- a/src/test/resources/conformance/encode/whitespace.json +++ b/src/test/resources/conformance/encode/whitespace.json @@ -9,8 +9,7 @@ "id": 123 }, "expected": "id: 123", - "specSection": "12", - "note": "Output should not end with newline character" + "specSection": "12" }, { "name": "maintains proper indentation for nested structures", @@ -37,8 +36,7 @@ "specSection": "12", "options": { "indent": 4 - }, - "note": "4-space indentation for nested objects when indent option is set to 4" + } } ] }