Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5519a6d
use JDK 17 in CI/CD
jenspapenhagen Feb 12, 2026
78de567
remove leading space in .gitattributes
jenspapenhagen Feb 12, 2026
fe951bf
remove .idea folder
jenspapenhagen Feb 12, 2026
281a699
install spotbugs
jenspapenhagen Feb 12, 2026
35cddf6
adding pmd
jenspapenhagen Feb 12, 2026
71f8a6c
running PMD and fix the warnnings
jenspapenhagen Feb 12, 2026
feeec03
install checkstyle
jenspapenhagen Feb 12, 2026
a8a266a
run checkstyle and fix warnings
jenspapenhagen Feb 12, 2026
ce9fd32
cleanup warnings
jenspapenhagen Feb 12, 2026
536b8c2
adding pitest
jenspapenhagen Feb 12, 2026
f177c3c
update versions numbers
jenspapenhagen Feb 12, 2026
4994a4d
adding JHM
jenspapenhagen Feb 12, 2026
5caf07b
adding test
jenspapenhagen Feb 12, 2026
f67531f
update pmd rules and spotbugs rules
jenspapenhagen Feb 12, 2026
9c8be17
fix javaDoc
jenspapenhagen Feb 12, 2026
0012704
clean up rules
jenspapenhagen Feb 12, 2026
ebfdf2a
remove warnings for pmd test
jenspapenhagen Feb 12, 2026
e802e92
adding final to public methods calls
jenspapenhagen Feb 12, 2026
c5e78cd
cleanup run with spotbugs
jenspapenhagen Feb 13, 2026
5ffca6f
cleanup run with spotbugs part 2
jenspapenhagen Feb 13, 2026
be6e71f
adding exclude rule for pmd-test
jenspapenhagen Feb 13, 2026
c6087bb
no warnings for checkstyle
jenspapenhagen Feb 13, 2026
da4ff14
performents testing
jenspapenhagen Feb 15, 2026
38a119c
adding test
jenspapenhagen Feb 15, 2026
17ffd61
cleanup
jenspapenhagen Feb 15, 2026
b7d8c8a
harding
jenspapenhagen Feb 15, 2026
39906ce
Merge branch 'main' into cleanup
jenspapenhagen Feb 20, 2026
dfd889d
fix PMD warnings
jenspapenhagen Feb 20, 2026
a9b5f4b
update PMD to 7.21.0 adn rerun
jenspapenhagen Feb 20, 2026
062d23c
update PMD to 7.21.0 adn rerun
jenspapenhagen Feb 20, 2026
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
35 changes: 28 additions & 7 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ pitest {
spotbugs {
toolVersion = '4.9.8'
excludeFilter = file('spotbugs-exclude.xml')
effort = "max"
reportLevel = "low"
effort = com.github.spotbugs.snom.Effort.valueOf("MAX")
reportLevel = com.github.spotbugs.snom.Confidence.valueOf("LOW")
reportsDir = layout.buildDirectory.dir('spotbugs')
}

Expand Down Expand Up @@ -84,7 +84,7 @@ tasks.spotbugsTest {
}

pmd {
toolVersion = '7.0.0'
toolVersion = '7.21.0'
ruleSetFiles = files('pmd-rules.xml')
ruleSets = [] // Disable default rulesets, use custom file only
consoleOutput = true
Expand Down Expand Up @@ -128,7 +128,7 @@ tasks.checkstyleTest {

dependencies {
implementation 'tools.jackson.core:jackson-databind:3.0.4'
implementation 'tools.jackson.module:jackson-module-afterburner:3.0.4'
implementation 'tools.jackson.module:jackson-module-blackbird:3.0.4'
compileOnly 'com.github.spotbugs:spotbugs-annotations:4.9.8'
testImplementation platform('org.junit:junit-bom:6.0.2')
testImplementation 'org.junit.jupiter:junit-jupiter'
Expand All @@ -140,12 +140,26 @@ dependencies {
testAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37'
}

// Check if running in CI or coverage is explicitly requested
boolean isCi = System.getenv('CI') == 'true'
boolean withCoverage = project.hasProperty('withCoverage') || isCi

test {
useJUnitPlatform()
finalizedBy jacocoTestReport // report is always generated after tests run
// Only run jacoco coverage in CI or when explicitly requested
if (withCoverage) {
finalizedBy jacocoTestReport
jacoco {
enabled = true
}
} else {
// Completely disable jacoco for local development
jacoco {
enabled = false
}
}
}


jacocoTestReport {
dependsOn test
reports {
Expand All @@ -156,6 +170,8 @@ jacocoTestReport {
}

jacocoTestCoverageVerification {
// Only enforce coverage thresholds in CI
onlyIf { isCi }
violationRules {
rule {
enabled = true
Expand All @@ -174,6 +190,11 @@ jacocoTestCoverageVerification {
}
}
}

// Only add coverage verification to check task in CI
if (isCi) {
check.dependsOn jacocoTestCoverageVerification
}
check.dependsOn jacocoTestReport
check.dependsOn spotbugsMain
check.dependsOn pmdMain
Expand Down Expand Up @@ -203,7 +224,7 @@ tasks.register('jmh', JavaExec) {
group = 'verification'
description = 'Run JMH benchmarks'
classpath = configurations.testRuntimeClasspath + sourceSets.test.runtimeClasspath
mainClass = 'dev.toonformat.jtoon.JToonBenchmark'
mainClass = findProperty('benchmarkClass') ?: 'dev.toonformat.jtoon.JToonBenchmark'
workingDir = projectDir
doFirst {
file('build/jmh-results').mkdirs()
Expand Down
14 changes: 11 additions & 3 deletions pmd-rules-test.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
<!-- Best Practices -->
<rule ref="category/java/bestpractices.xml">
<exclude name="GuardLogStatement" />
<exclude name="JUnitAssertionsShouldIncludeMessage" />
<exclude name="JUnitTestContainsTooManyAsserts" />
<exclude name="JUnitTestsShouldIncludeAssert" />
<exclude name="JUnitUseExpected" />
<exclude name="LiteralsFirstInComparisons" />
<exclude name="JUnit5TestShouldBePackagePrivate" />
<exclude name="LooseCoupling" />
<exclude name="RelianceOnDefaultCharset" />
<exclude name="UnnecessaryVarargsArrayCreation" />
<exclude name="UnitTestAssertionsShouldIncludeMessage" />
<exclude name="UnitTestContainsTooManyAsserts" />
<exclude name="UnitTestShouldIncludeAssert" />
</rule>

<!-- Code Style -->
Expand All @@ -28,6 +30,7 @@
<exclude name="ConfusingTernary" />
<exclude name="ControlStatementBraces" />
<exclude name="FieldNamingConventions" />
<exclude name="LambdaCanBeMethodReference" />
<exclude name="LinguisticNaming" />
<exclude name="LocalVariableCouldBeFinal" />
<exclude name="LongVariable" />
Expand Down Expand Up @@ -70,6 +73,7 @@
<exclude name="LoosePackageCoupling" />
<exclude name="NPathComplexity" />
<exclude name="NcssCount" />
<exclude name="PublicMemberInNonPublicType" />
<exclude name="SignatureDeclareThrowsException" />
<exclude name="SimplifiedTernary" />
<exclude name="SimplifyBooleanReturns" />
Expand All @@ -81,6 +85,7 @@

<!-- Error Prone -->
<rule ref="category/java/errorprone.xml">
<exclude name="AvoidCatchingGenericException" />
<exclude name="AvoidDuplicateLiterals" />
<exclude name="AvoidFieldNameMatchingMethodName" />
<exclude name="AvoidLiteralsInIfCondition" />
Expand All @@ -102,6 +107,8 @@
<exclude name="MissingSerialVersionUID" />
<exclude name="NullAssignment" />
<exclude name="ProperCloneImplementation" />
<exclude name="ReplaceJavaUtilCalendar" />
<exclude name="ReplaceJavaUtilDate" />
<exclude name="ReturnFromFinallyBlock" />
<exclude name="SimpleDateFormatNeedsLocale" />
<exclude name="SingletonClassReturningNewInstance" />
Expand All @@ -120,6 +127,7 @@
<exclude name="InefficientEmptyStringCheck" />
<exclude name="InsufficientStringBufferDeclaration" />
<exclude name="OptimizableToArrayCall" />
<exclude name="TooFewBranchesForSwitch" />
<exclude name="UseArrayListInsteadOfVector" />
<exclude name="UseStringBufferForStringAppends" />
</rule>
Expand Down
8 changes: 4 additions & 4 deletions pmd-rules.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@
<!-- Best Practices -->
<rule ref="category/java/bestpractices.xml">
<exclude name="GuardLogStatement" />
<exclude name="JUnitAssertionsShouldIncludeMessage" />
<exclude name="JUnitTestContainsTooManyAsserts" />
<exclude name="JUnitTestsShouldIncludeAssert" />
<exclude name="ForLoopCanBeForeach" />
<exclude name="UnusedPrivateMethod" />
<exclude name="JUnitUseExpected" />
</rule>

Expand All @@ -40,13 +39,13 @@
<!-- Design -->
<rule ref="category/java/design.xml">
<exclude name="AbstractClassWithoutAnyMethod" />
<exclude name="AvoidCatchingGenericException" />
<exclude name="AvoidDeeplyNestedIfStmts" />
<exclude name="CognitiveComplexity" />
<exclude name="CollapsibleIfStatements" />
<exclude name="CyclomaticComplexity" />
<exclude name="DataClass" />
<exclude name="ExcessiveImports" />
<exclude name="ExcessiveParameterList" />
<exclude name="ExcessivePublicCount" />
<exclude name="GodClass" />
<exclude name="ImmutableField" />
Expand All @@ -64,6 +63,7 @@

<!-- Error Prone -->
<rule ref="category/java/errorprone.xml">
<exclude name="AvoidCatchingGenericException" />
<exclude name="AvoidLiteralsInIfCondition" />
<exclude name="AvoidMultipleUnaryOperators" />
<exclude name="CheckSkipResult" />
Expand Down
11 changes: 11 additions & 0 deletions spotbugs-exclude.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@
<Class name="~.*JToonBenchmark.*" />
</Match>

<!-- Exclude JMH generated classes -->
<Match>
<Class name="~.*jmh_generated.*" />
</Match>

<!-- Exclude NM_CONFUSING for JMH benchmark methods -->
<Match>
<Class name="~.*Benchmark.*" />
<Bug pattern="NM_CONFUSING" />
</Match>

<!-- Exclude generated classes -->
<Match>
<Class name="~.*\.generated\..*" />
Expand Down
13 changes: 10 additions & 3 deletions src/main/java/dev/toonformat/jtoon/JToon.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import dev.toonformat.jtoon.encoder.ValueEncoder;
import dev.toonformat.jtoon.normalizer.JsonNormalizer;
import tools.jackson.databind.JsonNode;
import java.util.Objects;

/**
* Main entry point for encoding and decoding TOON (Token-Oriented Object Notation) format.
Expand Down Expand Up @@ -41,8 +42,10 @@ public static String encode(final Object input) {
* @param input The object to encode (can be null)
* @param options Encoding options (indent, delimiter, length marker)
* @return The JToon-formatted string
* @throws NullPointerException if options is null
*/
public static String encode(final Object input, final EncodeOptions options) {
Objects.requireNonNull(options, "EncodeOptions cannot be null");
final JsonNode normalizedValue = JsonNormalizer.normalize(input);
return ValueEncoder.encodeValue(normalizedValue, options);
}
Expand All @@ -68,17 +71,17 @@ public static String encodeJson(final String json) {
* Encodes a plain JSON string to TOON format using custom options.
*
* <p>
* Parsing is delegated to
* {@link JsonNormalizer#parse(String)}
* to maintain separation of concerns.
* Parses the JSON string to a tree structure, then encodes to TOON format.
* </p>
*
* @param json The JSON string to encode (must be valid JSON)
* @param options Encoding options (indent, delimiter, length marker)
* @return The TOON-formatted string
* @throws IllegalArgumentException if the input is not valid JSON
* @throws NullPointerException if options is null
*/
public static String encodeJson(final String json, final EncodeOptions options) {
Objects.requireNonNull(options, "EncodeOptions cannot be null");
final JsonNode parsed = JsonNormalizer.parse(json);
return ValueEncoder.encodeValue(parsed, options);
}
Expand Down Expand Up @@ -113,8 +116,10 @@ public static Object decode(final String toon) {
* @return Parsed object (Map, List, primitive, or null)
* @throws IllegalArgumentException if strict mode is enabled and input is
* invalid
* @throws NullPointerException if options is null
*/
public static Object decode(final String toon, final DecodeOptions options) {
Objects.requireNonNull(options, "DecodeOptions cannot be null");
return ValueDecoder.decode(toon, options);
}

Expand Down Expand Up @@ -150,8 +155,10 @@ public static String decodeToJson(final String toon) {
* @return JSON string representation
* @throws IllegalArgumentException if strict mode is enabled and input is
* invalid
* @throws NullPointerException if options is null
*/
public static String decodeToJson(final String toon, final DecodeOptions options) {
Objects.requireNonNull(options, "DecodeOptions cannot be null");
return ValueDecoder.decodeToJson(toon, options);
}
}
64 changes: 45 additions & 19 deletions src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.ObjectNode;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.StreamSupport;
import static dev.toonformat.jtoon.util.Constants.LIST_ITEM_PREFIX;
import static dev.toonformat.jtoon.util.Constants.SPACE;

Expand Down Expand Up @@ -39,26 +37,47 @@ public static void encodeArray(final String key, final ArrayNode value,
return;
}

// Primitive array
if (isArrayOfPrimitives(value)) {
final int size = value.size();
boolean allPrimitives = true;
boolean allArrays = true;
boolean allObjects = true;

for (int i = 0; i < size; i++) {
final JsonNode item = value.get(i);
if (!item.isValueNode()) {
allPrimitives = false;
}
if (!item.isArray()) {
allArrays = false;
}
if (!item.isObject()) {
allObjects = false;
}
if (!allPrimitives && !allArrays && !allObjects) {
break;
}
}

if (allPrimitives) {
encodeInlinePrimitiveArray(key, value, writer, depth, options);
return;
}

// Array of arrays (all primitives)
if (isArrayOfArrays(value)) {
final boolean allPrimitiveArrays = StreamSupport.stream(value.spliterator(), false)
.filter(JsonNode::isArray)
.allMatch(ArrayEncoder::isArrayOfPrimitives);

if (allArrays) {
boolean allPrimitiveArrays = true;
for (int i = 0; i < size; i++) {
if (!isArrayOfPrimitives(value.get(i))) {
allPrimitiveArrays = false;
break;
}
}
if (allPrimitiveArrays) {
encodeArrayOfArraysAsListItems(key, value, writer, depth, options);
return;
}
}

// Array of objects
if (isArrayOfObjects(value)) {
if (allObjects) {
final List<String> header = TabularArrayEncoder.detectTabularHeader(value);
if (!header.isEmpty()) {
TabularArrayEncoder.encodeArrayOfObjectsAsTabular(key, value, header, writer, depth, options);
Expand All @@ -68,7 +87,6 @@ public static void encodeArray(final String key, final ArrayNode value,
return;
}

// Mixed array: fallback to expanded format
encodeMixedArrayAsListItems(key, value, writer, depth, options);
}

Expand Down Expand Up @@ -147,17 +165,25 @@ private static void encodeInlinePrimitiveArray(final String prefix, final ArrayN
*/
public static String formatInlineArray(final ArrayNode values, final String delimiter,
final String prefix, final boolean lengthMarker) {
final List<JsonNode> valueList = new ArrayList<>();
values.forEach(valueList::add);

final String header = PrimitiveEncoder.formatHeader(values.size(), prefix, null, delimiter, lengthMarker);
final String joinedValue = PrimitiveEncoder.joinEncodedValues(valueList, delimiter);

// Only add space if there are values
// Early return for empty arrays
if (values.isEmpty()) {
return header;
}
return header + SPACE + joinedValue;

// Build joined values directly without intermediate collection
final StringBuilder joinedValues = new StringBuilder(128);
boolean first = true;
for (final JsonNode value : values) {
if (!first) {
joinedValues.append(delimiter);
}
first = false;
joinedValues.append(PrimitiveEncoder.encodePrimitive(value, delimiter));
}

return header + SPACE + joinedValues;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/dev/toonformat/jtoon/encoder/Flatten.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public record FoldResult(String foldedKey,
* @param tail the tail node (if any)
* @param leafValue the leaf JsonValue
*/
private record ChainResult(List<String> segments, JsonNode tail, JsonNode leafValue) {
record ChainResult(List<String> segments, JsonNode tail, JsonNode leafValue) {
}

/**
Expand Down Expand Up @@ -136,7 +136,7 @@ public static FoldResult tryFoldKeyChain(final String key,
* @param maxDepth maximum number of allowed segments
* @return a {@link ChainResult} containing segments, tail, and leafValue
*/
private static ChainResult collectSingleKeyChain(final String startKey,
static ChainResult collectSingleKeyChain(final String startKey,
final JsonNode startValue,
final int maxDepth) {
// normalize absolute key to its local segment
Expand Down
Loading
Loading