diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b697700..65c636a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,6 +56,15 @@ This project targets Java 17 and above. - Add JavaDoc comments for public APIs - Format code consistently (use IDE auto-formatting) +### Null Safety + +This project enforces null safety at compile time using **[NullAway](https://github.com/uber/nullaway)** (an Error Prone checker) and **[JSpecify](https://jspecify.dev/)** annotations. + +- All packages are annotated `@NullMarked` — types are non-null by default +- Use `@Nullable` to explicitly mark parameters, fields, or return values that may be `null` +- The `./gradlew compileJava` build step will fail if NullAway detects any null-safety violations +- Do **not** suppress NullAway warnings with `@SuppressWarnings("NullAway")` unless there is a well-documented reason (e.g., a third-party API that returns nullable values without annotations) + ### Testing - All new features must include JUnit tests diff --git a/README.md b/README.md index 337f041..4927529 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ 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). -**Key Features:** Minimal syntax • TOON Encoding and Decoding • Tabular arrays for uniform data • Array length validation • Java 17 • full [Jackson Annotation](https://github.com/FasterXML/jackson-annotations) Support • Comprehensive test coverage. +**Key Features:** Minimal syntax • TOON Encoding and Decoding • Tabular arrays for uniform data • Array length validation • Java 17 • full [Jackson Annotation](https://github.com/FasterXML/jackson-annotations) Support • Null-safe by default (NullAway + JSpecify) • Comprehensive test coverage. ## Installation diff --git a/build.gradle b/build.gradle index d61a7a9..91b7edf 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'java' + id 'java-library' id 'maven-publish' id 'signing' id 'jacoco' @@ -9,6 +9,7 @@ plugins { id 'org.owasp.dependencycheck' version '12.2.2' id 'org.cyclonedx.bom' version '3.2.4' id 'info.solidsoft.pitest' version '1.19.0' + id 'net.ltgt.errorprone' version '5.1.0' } group = 'dev.toonformat' @@ -125,7 +126,7 @@ dependencyCheck { } tasks.cyclonedxBom { - projectType = 'library' + projectType = Component.Type.LIBRARY jsonOutput.set(file("build/reports/jtoon-bom.json")) xmlOutput.unsetConvention() } @@ -146,6 +147,17 @@ dependencies { implementation 'tools.jackson.core:jackson-databind:3.2.0' implementation 'tools.jackson.module:jackson-module-blackbird:3.2.0' compileOnly 'com.github.spotbugs:spotbugs-annotations:4.10.2' + + // NullAway + Error Prone for compile-time null safety + errorprone 'com.uber.nullaway:nullaway:0.13.7' + // Pin error_prone_core to 2.42.0 (Java 17 compatible; 2.50.0+ requires Java 21) + errorprone('com.google.errorprone:error_prone_core:2.42.0') { + version { + strictly '2.42.0' + } + } + api 'org.jspecify:jspecify:1.0.0' + testImplementation platform('org.junit:junit-bom:6.1.1') testImplementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' @@ -156,6 +168,29 @@ dependencies { testAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37' } +// ---- NullAway / Error Prone Configuration ---- +import net.ltgt.gradle.errorprone.CheckSeverity +import org.cyclonedx.model.Component + +tasks.withType(JavaCompile).configureEach { + options.errorprone { + // NullAway as ERROR-Level for main source set + check('NullAway', CheckSeverity.ERROR) + // Annotate Packages + option('NullAway:AnnotatedPackages', 'dev.toonformat') + // Generated-Code ignore (Jackson, etc.) + option('NullAway:TreatGeneratedAsUnannotated', 'true') + } +} + +// no NullAway for tests; exclude JMH-generated sources from ErrorProne +tasks.compileTestJava { + options.errorprone { + disable('NullAway') + excludedPaths = '.*/build/generated/.*' + } +} + // Check if running in CI or coverage is explicitly requested boolean isCi = System.getenv('CI') == 'true' boolean withCoverage = project.hasProperty('withCoverage') || isCi diff --git a/gradle/verification-metadata.dryrun.xml b/gradle/verification-metadata.dryrun.xml new file mode 100644 index 0000000..8a1a59f --- /dev/null +++ b/gradle/verification-metadata.dryrun.xml @@ -0,0 +1,2979 @@ + + + + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 074a9d9..c2b258b 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -127,9 +127,6 @@ - - - @@ -207,6 +204,22 @@ + + + + + + + + + + + + + + + + @@ -235,9 +248,6 @@ - - - @@ -246,9 +256,6 @@ - - - @@ -257,9 +264,6 @@ - - - @@ -300,6 +304,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -347,6 +385,14 @@ + + + + + + + + @@ -363,6 +409,14 @@ + + + + + + + + @@ -371,6 +425,22 @@ + + + + + + + + + + + + + + + + @@ -381,11 +451,29 @@ + + + + + + + + + + + + + + + + + + @@ -394,6 +482,14 @@ + + + + + + + + @@ -402,6 +498,11 @@ + + + + + @@ -410,6 +511,14 @@ + + + + + + + + @@ -423,6 +532,11 @@ + + + + + @@ -433,6 +547,11 @@ + + + + + @@ -467,6 +586,14 @@ + + + + + + + + @@ -475,6 +602,24 @@ + + + + + + + + + + + + + + + + + + @@ -549,6 +694,14 @@ + + + + + + + + @@ -565,14 +718,6 @@ - - - - - - - - @@ -629,6 +774,27 @@ + + + + + + + + + + + + + + + + + + + + + @@ -752,6 +918,19 @@ + + + + + + + + + + + + + @@ -895,11 +1074,6 @@ - - - - - @@ -1040,11 +1214,6 @@ - - - - - @@ -1300,9 +1469,6 @@ - - - @@ -1316,9 +1482,6 @@ - - - @@ -1606,9 +1769,6 @@ - - - @@ -1635,9 +1795,6 @@ - - - @@ -1646,8 +1803,21 @@ - - + + + + + + + + + + + + + + + @@ -1806,9 +1976,6 @@ - - - @@ -1977,9 +2144,6 @@ - - - @@ -2141,17 +2305,6 @@ - - - - - - - - - - - @@ -2160,17 +2313,6 @@ - - - - - - - - - - - @@ -2179,17 +2321,6 @@ - - - - - - - - - - - @@ -2198,17 +2329,6 @@ - - - - - - - - - - - @@ -2217,17 +2337,6 @@ - - - - - - - - - - - @@ -2236,17 +2345,6 @@ - - - - - - - - - - - @@ -2255,17 +2353,6 @@ - - - - - - - - - - - @@ -2312,9 +2399,6 @@ - - - @@ -2448,6 +2532,14 @@ + + + + + + + + @@ -2455,9 +2547,6 @@ - - - @@ -2670,9 +2759,6 @@ - - - @@ -2684,9 +2770,6 @@ - - - @@ -2713,9 +2796,6 @@ - - - @@ -2724,9 +2804,6 @@ - - - @@ -2735,9 +2812,6 @@ - - - diff --git a/src/main/java/dev/toonformat/jtoon/DecodeOptions.java b/src/main/java/dev/toonformat/jtoon/DecodeOptions.java index 89a808f..76b6ef6 100644 --- a/src/main/java/dev/toonformat/jtoon/DecodeOptions.java +++ b/src/main/java/dev/toonformat/jtoon/DecodeOptions.java @@ -80,7 +80,7 @@ public DecodeOptions() { if (indent > MAX_ALLOWED_INDENT) { throw new IllegalArgumentException("indent must be <= " + MAX_ALLOWED_INDENT + ", got: " + indent); } - delimiter = Objects.requireNonNull(delimiter, "delimiter cannot be null"); + Objects.requireNonNull(delimiter, "delimiter cannot be null"); if (maxDepth <= 0) { throw new IllegalArgumentException("maxDepth must be positive, got: " + maxDepth); } diff --git a/src/main/java/dev/toonformat/jtoon/EncodeOptions.java b/src/main/java/dev/toonformat/jtoon/EncodeOptions.java index 5fbd35c..a281c57 100644 --- a/src/main/java/dev/toonformat/jtoon/EncodeOptions.java +++ b/src/main/java/dev/toonformat/jtoon/EncodeOptions.java @@ -58,7 +58,7 @@ public EncodeOptions() { if (indent > MAX_ALLOWED_INDENT) { throw new IllegalArgumentException("indent must be <= " + MAX_ALLOWED_INDENT + ", got: " + indent); } - delimiter = Objects.requireNonNull(delimiter, "delimiter cannot be null"); + Objects.requireNonNull(delimiter, "delimiter cannot be null"); if (flattenDepth < 0) { throw new IllegalArgumentException("flattenDepth must be non-negative, got: " + flattenDepth); } diff --git a/src/main/java/dev/toonformat/jtoon/JToon.java b/src/main/java/dev/toonformat/jtoon/JToon.java index d0c2157..10c76a7 100644 --- a/src/main/java/dev/toonformat/jtoon/JToon.java +++ b/src/main/java/dev/toonformat/jtoon/JToon.java @@ -3,6 +3,7 @@ import dev.toonformat.jtoon.decoder.ValueDecoder; import dev.toonformat.jtoon.encoder.ValueEncoder; import dev.toonformat.jtoon.normalizer.JsonNormalizer; +import org.jspecify.annotations.Nullable; import tools.jackson.databind.JsonNode; import java.util.Objects; @@ -99,6 +100,7 @@ public static String encodeJson(final String json, final EncodeOptions options) * @throws IllegalArgumentException if strict mode is enabled and input is * invalid */ + @Nullable public static Object decode(final String toon) { return decode(toon, DecodeOptions.DEFAULT); } @@ -118,6 +120,7 @@ public static Object decode(final String toon) { * invalid * @throws NullPointerException if options is null */ + @Nullable public static Object decode(final String toon, final DecodeOptions options) { Objects.requireNonNull(options, "DecodeOptions cannot be null"); return ValueDecoder.decode(toon, options); diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java index 56bebb3..beec689 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java @@ -1,6 +1,7 @@ package dev.toonformat.jtoon.decoder; import dev.toonformat.jtoon.Delimiter; +import org.jspecify.annotations.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -104,7 +105,7 @@ static List parseArrayWithDelimiter(final String header, final int depth context.options.maxArraySize(), context.options.maxStringLength()); validateArrayLength(header, result.size(), context.options.maxArraySize()); context.currentLine++; - return result; + return Collections.unmodifiableList(result); } } @@ -123,18 +124,18 @@ static List parseArrayWithDelimiter(final String header, final int depth if (nextContent.startsWith(LIST_ITEM_PREFIX)) { context.currentLine--; - return parseListArray(depth, header, context); + return Collections.unmodifiableList(parseListArray(depth, header, context)); } else { context.currentLine++; final List result = parseArrayValues(nextContent, arrayDelimiter, context.options.maxArraySize(), context.options.maxStringLength()); validateArrayLength(header, result.size(), context.options.maxArraySize()); - return result; + return Collections.unmodifiableList(result); } } final List empty = new ArrayList<>(); validateArrayLength(header, 0, context.options.maxArraySize()); - return empty; + return Collections.unmodifiableList(empty); } if (context.options.strict()) { @@ -165,6 +166,7 @@ static void validateArrayLength(final String header, final int actualLength, fin * @param maxArraySize maximum allowed array size * @return extracted length from header, or null if not found */ + @Nullable private static Integer extractLengthFromHeader(final String header, final int maxArraySize) { final Matcher matcher = ARRAY_HEADER_PATTERN.matcher(header); if (matcher.find()) { diff --git a/src/main/java/dev/toonformat/jtoon/decoder/DecodeContext.java b/src/main/java/dev/toonformat/jtoon/decoder/DecodeContext.java index 4eea0e3..b10c06b 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/DecodeContext.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/DecodeContext.java @@ -32,6 +32,9 @@ public class DecodeContext { */ public DecodeContext() { this.currentDepth = 0; + this.lines = new String[0]; + this.options = DecodeOptions.DEFAULT; + this.delimiter = Delimiter.COMMA; } void incrementDepth() { diff --git a/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java b/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java index 64a3770..83f7c14 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java @@ -1,6 +1,7 @@ package dev.toonformat.jtoon.decoder; import dev.toonformat.jtoon.Delimiter; +import org.jspecify.annotations.Nullable; import java.util.List; import java.util.Map; import static dev.toonformat.jtoon.util.Constants.BACKSLASH; @@ -137,7 +138,7 @@ static int findNextNonBlankLine(final int startIndex, final DecodeContext contex * @param context decode an object to deal with lines, delimiter, and options * @throws IllegalArgumentException in case there's a expansion conflict */ - static void checkFinalValueConflict(final String finalSegment, final Object existing, + static void checkFinalValueConflict(final String finalSegment, @Nullable final Object existing, final Object value, final DecodeContext context) { if (existing != null && context.options.strict()) { // Check for conflicts in strict mode @@ -195,7 +196,7 @@ static void checkDuplicateKey(final Map map, final String key, f * @param context decode an object to deal with lines, delimiter and options * @return the depth of the next non-blank line, or null if none exists */ - static Integer findNextNonBlankLineDepth(final DecodeContext context) { + static @Nullable Integer findNextNonBlankLineDepth(final DecodeContext context) { int nextLineIdx = context.currentLine; while (nextLineIdx < context.lines.length && isBlankLine(context.lines[nextLineIdx])) { nextLineIdx++; diff --git a/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java index a1ad953..b8eb6f5 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java @@ -3,12 +3,12 @@ import dev.toonformat.jtoon.Delimiter; import dev.toonformat.jtoon.PathExpansion; import dev.toonformat.jtoon.util.StringEscaper; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.MatchResult; -import static dev.toonformat.jtoon.util.Constants.DOT; import static dev.toonformat.jtoon.util.Headers.KEYED_ARRAY_PATTERN; /** @@ -16,6 +16,8 @@ */ public final class KeyDecoder { + private static final char DOT = '.'; + private KeyDecoder() { throw new UnsupportedOperationException("Utility class cannot be instantiated"); } @@ -56,7 +58,7 @@ static void processKeyedArrayLine(final Map result, final String */ static void expandPathIntoMap(final Map current, final String dottedKey, final Object value, final DecodeContext context) { - final String[] segments = dottedKey.split("\\."); + final String[] segments = splitByDot(dottedKey); Map currentMap = current; // Navigate/create nested structure @@ -71,7 +73,9 @@ static void expandPathIntoMap(final Map current, final String do currentMap = nested; } else if (existing instanceof Map existingMap) { // Use existing nested object; map was created as LinkedHashMap - currentMap = (Map) existingMap; + @SuppressWarnings("unchecked") + final Map typedMap = (Map) existingMap; + currentMap = typedMap; } else { // Conflict: existing is not a Map if (context.options.strict()) { @@ -160,13 +164,13 @@ static boolean shouldExpandKey(final String key, final DecodeContext context) { return false; } // Check if a key contains dots and is a valid identifier pattern - if (!key.contains(DOT)) { + if (key.indexOf(DOT) < 0) { return false; } // Valid identifier: starts with a letter or underscore, followed by letters, // digits, underscores // Each segment must match this pattern - final String[] segments = key.split("\\."); + final String[] segments = splitByDot(key); for (String segment : segments) { if (!segment.matches("^[a-zA-Z_]\\w*$")) { return false; @@ -367,4 +371,24 @@ static boolean parseKeyValueField(final String fieldContent, final Map result = new ArrayList<>(); + int start = 0; + for (int i = 0; i < input.length(); i++) { + if (input.charAt(i) == DOT) { + result.add(input.substring(start, i)); + start = i + 1; + } + } + final String last = input.substring(start); + if (!last.isEmpty()) { + result.add(last); + } + return result.toArray(new String[0]); + } } diff --git a/src/main/java/dev/toonformat/jtoon/decoder/PrimitiveDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/PrimitiveDecoder.java index 40add48..fdba04a 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/PrimitiveDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/PrimitiveDecoder.java @@ -45,14 +45,17 @@ private PrimitiveDecoder() { * @return The parsed value as {@code Boolean}, {@code Long}, {@code Double}, * {@code String}, or {@code null} */ + @SuppressWarnings("NullAway") static Object parse(final String value) { return parse(value, Integer.MAX_VALUE); } + @SuppressWarnings("NullAway") static Object parse(final String value, final DecodeContext context) { return parse(value, context.options.maxStringLength()); } + @SuppressWarnings("NullAway") static Object parse(final String value, final int maxStringLength) { if (value == null || value.isEmpty()) { return ""; diff --git a/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java index 43d98fc..53562b0 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java @@ -70,7 +70,7 @@ public static List parseTabularArray(final String header, final int dept } ArrayDecoder.validateArrayLength(header, result.size(), context.options.maxArraySize()); - return result; + return Collections.unmodifiableList(result); } /** diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java index db33cc7..dbfccc3 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java @@ -2,6 +2,7 @@ import dev.toonformat.jtoon.DecodeOptions; import dev.toonformat.jtoon.util.ObjectMapperSingleton; +import org.jspecify.annotations.Nullable; import tools.jackson.databind.ObjectMapper; import java.util.LinkedHashMap; import java.util.regex.Matcher; @@ -45,6 +46,7 @@ private ValueDecoder() { * @throws IllegalArgumentException if strict mode is enabled and input is * invalid */ + @Nullable public static Object decode(final String toon, final DecodeOptions options) { try { return decodeInternal(toon, options); @@ -56,6 +58,7 @@ public static Object decode(final String toon, final DecodeOptions options) { } } + @Nullable private static Object decodeInternal(final String toon, final DecodeOptions options) { if (toon == null || toon.isBlank()) { return new LinkedHashMap<>(); diff --git a/src/main/java/dev/toonformat/jtoon/decoder/package-info.java b/src/main/java/dev/toonformat/jtoon/decoder/package-info.java index 5f2fed5..1f0778c 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/package-info.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/package-info.java @@ -210,4 +210,7 @@ * @see dev.toonformat.jtoon.JToon#decode(String) * @see dev.toonformat.jtoon.JToon#decodeToJson(String) */ +@NullMarked package dev.toonformat.jtoon.decoder; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java index c8767a4..166856f 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java @@ -1,6 +1,7 @@ package dev.toonformat.jtoon.encoder; import dev.toonformat.jtoon.EncodeOptions; +import org.jspecify.annotations.Nullable; import tools.jackson.databind.JsonNode; import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ObjectNode; @@ -28,7 +29,7 @@ private ArrayEncoder() { * @param depth Indentation depth * @param options Encoding options */ - public static void encodeArray(final String key, final ArrayNode value, + public static void encodeArray(@Nullable final String key, final ArrayNode value, final LineWriter writer, final int depth, final EncodeOptions options) { if (value.isEmpty()) { // Per spec §9.1: encoders SHOULD emit key: [] for empty arrays. @@ -158,7 +159,7 @@ public static boolean isArrayOfObjects(final JsonNode array) { /** * Encodes a primitive array inline: key[N]: v1,v2,v3. */ - private static void encodeInlinePrimitiveArray(final String prefix, final ArrayNode values, + private static void encodeInlinePrimitiveArray(@Nullable final String prefix, final ArrayNode values, final LineWriter writer, final int depth, final EncodeOptions options) { final String formatted = formatInlineArray(values, options.delimiter().toString(), prefix, options.lengthMarker()); @@ -175,7 +176,7 @@ private static void encodeInlinePrimitiveArray(final String prefix, final ArrayN * @return the formatted inline array string */ public static String formatInlineArray(final ArrayNode values, final String delimiter, - final String prefix, final boolean lengthMarker) { + @Nullable final String prefix, final boolean lengthMarker) { final String header = PrimitiveEncoder.formatHeader(values.size(), prefix, null, delimiter, lengthMarker); // Early return for empty arrays @@ -203,7 +204,7 @@ public static String formatInlineArray(final ArrayNode values, final String deli /** * Encodes an array of primitive arrays as list items. */ - private static void encodeArrayOfArraysAsListItems(final String prefix, final ArrayNode values, + private static void encodeArrayOfArraysAsListItems(@Nullable final String prefix, final ArrayNode values, final LineWriter writer, final int depth, final EncodeOptions options) { final String header = PrimitiveEncoder.formatHeader(values.size(), prefix, null, options.delimiter().toString(), options.lengthMarker()); @@ -221,7 +222,7 @@ private static void encodeArrayOfArraysAsListItems(final String prefix, final Ar /** * Encodes a mixed array (non-uniform) as list items. */ - private static void encodeMixedArrayAsListItems(final String prefix, + private static void encodeMixedArrayAsListItems(@Nullable final String prefix, final ArrayNode items, final LineWriter writer, final int depth, diff --git a/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java b/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java index aa16f13..d66e0fb 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java @@ -1,5 +1,6 @@ package dev.toonformat.jtoon.encoder; +import org.jspecify.annotations.Nullable; import tools.jackson.databind.JsonNode; import tools.jackson.databind.node.ObjectNode; import java.util.ArrayList; @@ -30,8 +31,8 @@ private Flatten() { * @param segmentCount number of folded segments */ public record FoldResult(String foldedKey, - JsonNode remainder, - JsonNode leafValue, + @Nullable JsonNode remainder, + @Nullable JsonNode leafValue, int segmentCount) { } @@ -42,7 +43,7 @@ public record FoldResult(String foldedKey, * @param tail the tail node (if any) * @param leafValue the leaf JsonValue */ - record ChainResult(List segments, JsonNode tail, JsonNode leafValue) { + record ChainResult(List segments, @Nullable JsonNode tail, @Nullable JsonNode leafValue) { } /** @@ -63,12 +64,13 @@ record ChainResult(List segments, JsonNode tail, JsonNode leafValue) { * @param remainingDepth the remaining depth of the object * @return a {@link FoldResult}, or null if folding is not possible */ + @Nullable public static FoldResult tryFoldKeyChain(final String key, - final JsonNode value, - final Set siblings, - final Set rootLiteralKeys, - final String pathPrefix, - final Integer remainingDepth) { + final JsonNode value, + final Set siblings, + @Nullable final Set rootLiteralKeys, + @Nullable final String pathPrefix, + final Integer remainingDepth) { // Must be an object to begin folding if (!value.isObject() || remainingDepth <= 1) { return null; diff --git a/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java b/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java index 403c28b..5978fa7 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java @@ -2,6 +2,7 @@ import java.util.Collection; import java.util.List; +import org.jspecify.annotations.Nullable; import static dev.toonformat.jtoon.util.Constants.COLON; import static dev.toonformat.jtoon.util.Constants.OPEN_BRACKET; import static dev.toonformat.jtoon.util.Constants.COMMA; @@ -30,8 +31,8 @@ private HeaderFormatter() { */ public record HeaderConfig( int length, - String key, - List fields, + @Nullable String key, + @Nullable List fields, String delimiter, boolean lengthMarker) { } @@ -65,15 +66,15 @@ static String format(final HeaderConfig config) { */ public static String format( final int length, - final String key, - final List fields, + @Nullable final String key, + @Nullable final List fields, final String delimiter, final boolean lengthMarker) { final HeaderConfig config = new HeaderConfig(length, key, fields, delimiter, lengthMarker); return format(config); } - private static void appendKeyIfPresent(final StringBuilder header, final String key) { + private static void appendKeyIfPresent(final StringBuilder header, @Nullable final String key) { if (key != null) { header.append(PrimitiveEncoder.encodeKey(key)); } @@ -103,7 +104,7 @@ private static void appendDelimiterIfNotDefault(final StringBuilder header, fina private static void appendFieldsIfPresent( final StringBuilder header, - final Collection fields, + @Nullable final Collection fields, final String delimiter) { if (fields == null || fields.isEmpty()) { return; diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java index c7ae2c5..00d5850 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java @@ -2,6 +2,7 @@ import dev.toonformat.jtoon.EncodeOptions; import dev.toonformat.jtoon.KeyFolding; +import org.jspecify.annotations.Nullable; import tools.jackson.databind.JsonNode; import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ObjectNode; @@ -35,13 +36,13 @@ private ObjectEncoder() { * @param blockedKeys contains only keys that have undergone a successful flattening */ public static void encodeObject(final ObjectNode value, - final LineWriter writer, - final int depth, - final EncodeOptions options, - final Set rootLiteralKeys, - final String pathPrefix, - final Integer remainingDepth, - final Set blockedKeys) { + final LineWriter writer, + final int depth, + final EncodeOptions options, + @Nullable final Set rootLiteralKeys, + @Nullable final String pathPrefix, + @Nullable final Integer remainingDepth, + @Nullable final Set blockedKeys) { final int effectiveFlattenDepth = remainingDepth != null ? remainingDepth : options.flattenDepth(); // Single-pass collection: gather sibling keys and optionally dotted keys at root level @@ -83,15 +84,15 @@ public static void encodeObject(final ObjectNode value, * @param blockedKeys contains only keys that have undergone a successful flattening */ public static void encodeKeyValuePair(final String key, - final JsonNode value, - final LineWriter writer, - final int depth, - final EncodeOptions options, - final Set siblings, - final Set rootLiteralKeys, - final String pathPrefix, - final Integer flattenDepth, - final Set blockedKeys + final JsonNode value, + final LineWriter writer, + final int depth, + final EncodeOptions options, + final Set siblings, + @Nullable final Set rootLiteralKeys, + @Nullable final String pathPrefix, + @Nullable final Integer flattenDepth, + @Nullable final Set blockedKeys ) { if (key == null) { return; @@ -151,15 +152,16 @@ public static void encodeKeyValuePair(final String key, * @param remainingDepth the depth that remind to the limit * @return EncodeOptions changes for Case 2 */ + @Nullable private static EncodeOptions flatten(final String key, - final Flatten.FoldResult foldResult, - final LineWriter writer, - final int depth, - final EncodeOptions options, - final Set rootLiteralKeys, - final String pathPrefix, - final Set blockedKeys, - final int remainingDepth) { + final Flatten.FoldResult foldResult, + final LineWriter writer, + final int depth, + final EncodeOptions options, + @Nullable final Set rootLiteralKeys, + @Nullable final String pathPrefix, + final Set blockedKeys, + final int remainingDepth) { final String foldedKey = foldResult.foldedKey(); EncodeOptions currentOptions = options; @@ -207,6 +209,10 @@ private static void handleFullyFoldedLeaf(final Flatten.FoldResult foldResult, final String encodedFoldedKey) { final JsonNode leaf = foldResult.leafValue(); + if (leaf == null) { + return; + } + // Primitive if (leaf.isValueNode()) { writer.push(depth, diff --git a/src/main/java/dev/toonformat/jtoon/encoder/PrimitiveEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/PrimitiveEncoder.java index 2c867ce..2685469 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/PrimitiveEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/PrimitiveEncoder.java @@ -2,6 +2,7 @@ import dev.toonformat.jtoon.util.StringEscaper; import dev.toonformat.jtoon.util.StringValidator; +import org.jspecify.annotations.Nullable; import tools.jackson.databind.JsonNode; import java.math.BigDecimal; import java.util.Collection; @@ -160,8 +161,8 @@ public static String joinEncodedValues(final Collection values, final */ public static String formatHeader( final int length, - final String key, - final List fields, + @Nullable final String key, + @Nullable final List fields, final String delimiter, final boolean lengthMarker) { return HeaderFormatter.format(length, key, fields, delimiter, lengthMarker); diff --git a/src/main/java/dev/toonformat/jtoon/encoder/TabularArrayEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/TabularArrayEncoder.java index efae136..22a04d7 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/TabularArrayEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/TabularArrayEncoder.java @@ -1,6 +1,7 @@ package dev.toonformat.jtoon.encoder; import dev.toonformat.jtoon.EncodeOptions; +import org.jspecify.annotations.Nullable; import tools.jackson.databind.JsonNode; import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ObjectNode; @@ -43,7 +44,7 @@ public static List detectTabularHeader(final ArrayNode rows) { } if (isTabularArray(rows, firstKeys)) { - return firstKeys; + return Collections.unmodifiableList(firstKeys); } return Collections.emptyList(); @@ -89,7 +90,7 @@ private static boolean isTabularArray(final Iterable rows, final List< * @param depth Indentation depth * @param options Encoding options */ - public static void encodeArrayOfObjectsAsTabular(final String prefix, final ArrayNode rows, + public static void encodeArrayOfObjectsAsTabular(@Nullable final String prefix, final ArrayNode rows, final List header, final LineWriter writer, final int depth, final EncodeOptions options) { final String headerStr = PrimitiveEncoder.formatHeader(rows.size(), prefix, header, diff --git a/src/main/java/dev/toonformat/jtoon/encoder/package-info.java b/src/main/java/dev/toonformat/jtoon/encoder/package-info.java index ab6022a..c4dcd8f 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/package-info.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/package-info.java @@ -149,5 +149,8 @@ * @since 0.1.0 * @see dev.toonformat.jtoon.EncodeOptions */ +@NullMarked package dev.toonformat.jtoon.encoder; +import org.jspecify.annotations.NullMarked; + diff --git a/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java b/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java index 58f092c..d35892a 100644 --- a/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java +++ b/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java @@ -1,6 +1,7 @@ package dev.toonformat.jtoon.normalizer; import dev.toonformat.jtoon.util.ObjectMapperSingleton; +import org.jspecify.annotations.Nullable; import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.node.ArrayNode; @@ -100,7 +101,7 @@ public static JsonNode parse(final String json) { * @return The normalized JsonNode * @throws IllegalArgumentException if nesting depth exceeds MAX_DEPTH or circular reference detected */ - public static JsonNode normalize(final Object value) { + public static JsonNode normalize(@Nullable final Object value) { final int currentDepth = DEPTH_COUNTER.get(); if (currentDepth > MAX_ALLOWED_NESTING_DEPTH) { DEPTH_COUNTER.remove(); @@ -114,7 +115,7 @@ public static JsonNode normalize(final Object value) { } } - private static JsonNode normalizeInternal(final Object value) { + private static JsonNode normalizeInternal(@Nullable final Object value) { if (value == null) { return NullNode.getInstance(); } else if (value instanceof JsonNode jsonNode) { @@ -155,6 +156,7 @@ private static JsonNode normalizeWithStrategy(final Object value) { * Attempts to normalize primitive types and their wrappers. * Returns null if the value is not a primitive type. */ + @Nullable private static JsonNode tryNormalizePrimitive(final Object value) { if (value instanceof String stringValue) { return StringNode.valueOf(stringValue); @@ -218,6 +220,7 @@ private static Optional tryConvertToLong(final Double value) { * Attempts to normalize BigInteger and BigDecimal. * Returns null if the value is not a big number type. */ + @Nullable private static JsonNode tryNormalizeBigNumber(final Object value) { if (value instanceof BigInteger bigInteger) { return normalizeBigInteger(bigInteger); @@ -243,6 +246,7 @@ private static JsonNode normalizeBigInteger(final BigInteger value) { * Attempts to normalize temporal types (date/time) to ISO strings. * Returns null if the value is not a temporal type. */ + @Nullable private static JsonNode tryNormalizeTemporal(final Object value) { if (value instanceof LocalDateTime ldt) { return formatTemporal(ldt, DateTimeFormatter.ISO_LOCAL_DATE_TIME); @@ -284,6 +288,7 @@ private static JsonNode formatTemporal(final T temporal, final DateTimeForma * Attempts to normalize collections (Collection and Map). * Returns null if the value is not a collection type. */ + @Nullable private static JsonNode tryNormalizeCollection(final Object value) { if (value instanceof Collection) { return normalizeCollection((Collection) value); diff --git a/src/main/java/dev/toonformat/jtoon/normalizer/package-info.java b/src/main/java/dev/toonformat/jtoon/normalizer/package-info.java index b08c5dd..71757a1 100644 --- a/src/main/java/dev/toonformat/jtoon/normalizer/package-info.java +++ b/src/main/java/dev/toonformat/jtoon/normalizer/package-info.java @@ -101,5 +101,8 @@ * @see tools.jackson.databind.JsonNode * @see tools.jackson.databind.ObjectMapper */ +@NullMarked package dev.toonformat.jtoon.normalizer; +import org.jspecify.annotations.NullMarked; + diff --git a/src/main/java/dev/toonformat/jtoon/package-info.java b/src/main/java/dev/toonformat/jtoon/package-info.java index 0f59f1b..b1602b3 100644 --- a/src/main/java/dev/toonformat/jtoon/package-info.java +++ b/src/main/java/dev/toonformat/jtoon/package-info.java @@ -208,4 +208,7 @@ * @since 0.1.0 * @version 0.1.0 */ +@NullMarked package dev.toonformat.jtoon; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/dev/toonformat/jtoon/util/Headers.java b/src/main/java/dev/toonformat/jtoon/util/Headers.java index 8deb6fa..0e55658 100644 --- a/src/main/java/dev/toonformat/jtoon/util/Headers.java +++ b/src/main/java/dev/toonformat/jtoon/util/Headers.java @@ -28,7 +28,7 @@ public final class Headers { * group(4)=optional field spec */ public static final Pattern KEYED_ARRAY_PATTERN = Pattern.compile( - "^(\"(?:[^\"\\\\]|\\\\.)*\"|[^\\[\\]]+)\\[(#?)\\d+([\\t|])?](\\{[^}]+})?:.*$"); + "^(\"(?:[^\"\\\\]|\\\\.)*+\"|[^\\[\\]]++)\\[(#?)\\d++([\\t|])?](\\{[^}]+})?+:.*+$"); private Headers() { throw new UnsupportedOperationException("Utility class cannot be instantiated"); diff --git a/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java b/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java index 3b41338..9e5bbcc 100644 --- a/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java +++ b/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java @@ -1,6 +1,7 @@ package dev.toonformat.jtoon.util; import com.fasterxml.jackson.annotation.JsonInclude; +import org.jspecify.annotations.Nullable; import tools.jackson.databind.MapperFeature; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; @@ -14,6 +15,7 @@ public final class ObjectMapperSingleton { /** * Holds the singleton ObjectMapper. */ + @Nullable private static volatile ObjectMapper instance; private ObjectMapperSingleton() { diff --git a/src/main/java/dev/toonformat/jtoon/util/StringValidator.java b/src/main/java/dev/toonformat/jtoon/util/StringValidator.java index c33987e..7c7e61b 100644 --- a/src/main/java/dev/toonformat/jtoon/util/StringValidator.java +++ b/src/main/java/dev/toonformat/jtoon/util/StringValidator.java @@ -48,24 +48,17 @@ public static boolean isSafeUnquoted(final String value, final String delimiter) for (int i = 0; i < len; i++) { final char c = value.charAt(i); switch (c) { - case ':': - case '"': - case '\\': - case '[': - case ']': - case '{': - case '}': - case '\n': - case '\r': - case '\t': + case ':', '"', '\\', '[', ']', '{', '}', '\n', '\r', '\t' -> { return false; - default: + } + default -> { if (c <= CONTROL_CHAR_MAX) { return false; } if (delimiter.length() == 1 && c == delimiter.charAt(0)) { return false; } + } } } diff --git a/src/main/java/dev/toonformat/jtoon/util/package-info.java b/src/main/java/dev/toonformat/jtoon/util/package-info.java index 3ce7b10..232ac1b 100644 --- a/src/main/java/dev/toonformat/jtoon/util/package-info.java +++ b/src/main/java/dev/toonformat/jtoon/util/package-info.java @@ -112,5 +112,8 @@ * * @since 0.1.0 */ +@NullMarked package dev.toonformat.jtoon.util; +import org.jspecify.annotations.NullMarked; + diff --git a/src/main/java/dev/toonformat/jtoon/validator/package-info.java b/src/main/java/dev/toonformat/jtoon/validator/package-info.java new file mode 100644 index 0000000..c934922 --- /dev/null +++ b/src/main/java/dev/toonformat/jtoon/validator/package-info.java @@ -0,0 +1,12 @@ +/** + * Input validation utilities for TOON data. + * + *

Provides validation logic for ensuring TOON-format data meets + * structural and content requirements before processing. + * + * @since 1.0.0 + */ +@NullMarked +package dev.toonformat.jtoon.validator; + +import org.jspecify.annotations.NullMarked; diff --git a/src/test/java/dev/toonformat/jtoon/JToonConcurrencyTest.java b/src/test/java/dev/toonformat/jtoon/JToonConcurrencyTest.java index 1592eea..067d30c 100644 --- a/src/test/java/dev/toonformat/jtoon/JToonConcurrencyTest.java +++ b/src/test/java/dev/toonformat/jtoon/JToonConcurrencyTest.java @@ -19,6 +19,7 @@ class JToonConcurrencyTest { private static final int AWAIT_TIMEOUT_SECONDS = 10; @Test + @SuppressWarnings("FutureReturnValueIgnored") void encodeDecodeStressTest() { final int threads = 8; final int tasksPerThread = 5_000; @@ -63,6 +64,7 @@ void encodeDecodeStressTest() { assertTrue(errors.isEmpty(), "Errors occurred in threads: " + errors); } + @SuppressWarnings("FutureReturnValueIgnored") void encodeDecodeJsonStressTest() { final int threads = 8; final int tasksPerThread = 5_000; diff --git a/src/test/java/dev/toonformat/jtoon/JToonFuzzTest.java b/src/test/java/dev/toonformat/jtoon/JToonFuzzTest.java index d5c9902..7c6a4e4 100644 --- a/src/test/java/dev/toonformat/jtoon/JToonFuzzTest.java +++ b/src/test/java/dev/toonformat/jtoon/JToonFuzzTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Arrays; import java.util.SplittableRandom; @@ -13,7 +14,7 @@ class JToonFuzzTest { private static final SplittableRandom RANDOM = new SplittableRandom(); private static final int FUZZ_ITERATIONS = 1_000; private static final int MAX_FUZZ_BYTE_LENGTH = 500; - private static final long FUZZ_TIMEOUT_MILLIS = 100; + private static final long FUZZ_TIMEOUT_MILLIS = 200; @Test @Tag("fuzz") @@ -42,7 +43,7 @@ void fuzzDoesNotHang() { for (int i = 0; i < FUZZ_ITERATIONS; i++) { final byte[] bytes = new byte[RANDOM.nextInt(MAX_FUZZ_BYTE_LENGTH)]; RANDOM.nextBytes(bytes); - final String input = new String(bytes); + final String input = new String(bytes, StandardCharsets.UTF_8); assertTimeoutPreemptively( Duration.ofMillis(FUZZ_TIMEOUT_MILLIS), diff --git a/src/test/java/dev/toonformat/jtoon/JToonJsonStringTest.java b/src/test/java/dev/toonformat/jtoon/JToonJsonStringTest.java index 9e836b1..d519a69 100644 --- a/src/test/java/dev/toonformat/jtoon/JToonJsonStringTest.java +++ b/src/test/java/dev/toonformat/jtoon/JToonJsonStringTest.java @@ -51,10 +51,10 @@ void encodesTabularArray() { final String result = JToon.encodeJson(json); // Then - final String expected = String.join("\n", - "items[2]{sku,qty,price}:", - " A1,2,9.99", - " B2,1,14.5"); + final String expected = """ + items[2]{sku,qty,price}: + A1,2,9.99 + B2,1,14.5"""; assertEquals(expected, result); } @@ -69,14 +69,14 @@ void encodesMixedArray() { final String result = JToon.encodeJson(json); // Then - final String expected = String.join("\n", - "[3]:", - " - summary", - " - id: 1", - " name: Ada", - " - [2]:", - " - id: 2", - " - status: draft"); + final String expected = """ + [3]: + - summary + - id: 1 + name: Ada + - [2]: + - id: 2 + - status: draft"""; assertEquals(expected, result); } @@ -93,11 +93,11 @@ void encodesWithCustomOptions() { final String result = JToon.encodeJson(json, options); // Then - final String expected = String.join("\n", - "tags[#3|]: reading|gaming|coding", - "items[#2|]{sku|qty|price}:", - " A1|2|9.99", - " B2|1|14.5"); + final String expected = """ + tags[#3|]: reading|gaming|coding + items[#2|]{sku|qty|price}: + A1|2|9.99 + B2|1|14.5"""; assertEquals(expected, result); } @@ -105,25 +105,26 @@ void encodesWithCustomOptions() { @DisplayName("supports custom options in flatten") void encodesWithCustomFlattingOptions() { // Given - final String json = "{\n" + - " \"a\": {\n" + - " \"b\": {\n" + - " \"c\": {\n" + - " \"d\": 1\n" + - " }\n" + - " }\n" + - " }\n" + - " }"; + final String json = """ + { + "a": { + "b": { + "c": { + "d": 1 + } + } + } + }"""; final EncodeOptions options = EncodeOptions.withFlattenDepth(2); // When final String result = JToon.encodeJson(json, options); // Then - final String expected = String.join("\n", - "a.b:", - " c:", - " d: 1"); + final String expected = """ + a.b: + c: + d: 1"""; assertEquals(expected, result); } } diff --git a/src/test/java/dev/toonformat/jtoon/JToonRaceConditionTest.java b/src/test/java/dev/toonformat/jtoon/JToonRaceConditionTest.java index 4509e7c..8ff95aa 100644 --- a/src/test/java/dev/toonformat/jtoon/JToonRaceConditionTest.java +++ b/src/test/java/dev/toonformat/jtoon/JToonRaceConditionTest.java @@ -31,7 +31,7 @@ void concurrentEncodeDecode() throws InterruptedException, ExecutionException { final Map metadata = new LinkedHashMap<>(); metadata.put("author", "dev"); metadata.put("stars", STARS_VALUE); - metadata.put("created", java.time.LocalDateTime.now()); + metadata.put("created", java.time.LocalDateTime.now(java.time.ZoneOffset.UTC)); input.put("metadata", metadata); final List> futures = new ArrayList<>(); diff --git a/src/test/java/dev/toonformat/jtoon/JToonTest.java b/src/test/java/dev/toonformat/jtoon/JToonTest.java index 315be69..d7a421e 100644 --- a/src/test/java/dev/toonformat/jtoon/JToonTest.java +++ b/src/test/java/dev/toonformat/jtoon/JToonTest.java @@ -24,7 +24,6 @@ class JToonTest { private static final double EXPECTED_ENCODE_PI = 3.14; - private static final double NEG_SEVEN = -7.0; private static final double EXPECTED_ENCODE_SMALL = 0.000001; private static final long EXPECTED_ENCODE_LARGE_LONG = 9007199254740991L; @@ -917,7 +916,7 @@ void noTrailingSpaces() { final String result = encode(obj); // Then - final String[] lines = result.split("\n"); + final String[] lines = result.lines().toArray(String[]::new); for (String line : lines) { assertFalse(line.matches(".* $"), "Line has trailing space: '" + line + "'"); } diff --git a/src/test/java/dev/toonformat/jtoon/conformance/ConformanceTest.java b/src/test/java/dev/toonformat/jtoon/conformance/ConformanceTest.java index 859cadc..c4cb8e9 100644 --- a/src/test/java/dev/toonformat/jtoon/conformance/ConformanceTest.java +++ b/src/test/java/dev/toonformat/jtoon/conformance/ConformanceTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.*; import java.io.File; import java.util.Arrays; +import java.util.Locale; import java.util.Objects; import java.util.stream.Stream; import dev.toonformat.jtoon.DecodeOptions; @@ -206,7 +207,7 @@ private DecodeOptions parseOptions(final JsonDecodeTestOptions options) { PathExpansion expandPaths = null; if (options.expandPaths() != null) { - expandPaths = switch (options.expandPaths().toLowerCase()) { + expandPaths = switch (options.expandPaths().toLowerCase(Locale.ROOT)) { case "safe" -> PathExpansion.SAFE; default -> PathExpansion.OFF; }; diff --git a/src/test/java/dev/toonformat/jtoon/decoder/ArrayDecoderTest.java b/src/test/java/dev/toonformat/jtoon/decoder/ArrayDecoderTest.java index 6b8e20f..c32d76b 100644 --- a/src/test/java/dev/toonformat/jtoon/decoder/ArrayDecoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/decoder/ArrayDecoderTest.java @@ -44,6 +44,20 @@ private static Object invokePrivateStatic(final String methodName, final Class result = ArrayDecoder.parseArray("[3]: 1,2,3", 0, context); + + // Then + final int extraElement = 4; + assertThrows(UnsupportedOperationException.class, () -> result.add(extraElement)); + } + @Test @DisplayName("Should parse TOON format numerical array to JSON") void parseNumericalPrimitiveArray() { diff --git a/src/test/java/dev/toonformat/jtoon/decoder/TabularArrayDecoderTest.java b/src/test/java/dev/toonformat/jtoon/decoder/TabularArrayDecoderTest.java index e553453..7ac78ec 100644 --- a/src/test/java/dev/toonformat/jtoon/decoder/TabularArrayDecoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/decoder/TabularArrayDecoderTest.java @@ -50,6 +50,21 @@ void throwsOnConstructor() throws NoSuchMethodException { assertEquals("Utility class cannot be instantiated", cause.getMessage()); } + @Test + @DisplayName("Parse tabular array and return unmodifiable list") + void parseTabularArrayReturnsUnmodifiableList() { + // Given + setUpContext("[2]{id,value}:\n 1,null\n 2,\"test\""); + + // When + final List result = TabularArrayDecoder.parseTabularArray( + "[2]{id,value}:\n 1,null\n 2,\"test\"", 0, + Delimiter.COMMA, context); + + // Then + assertThrows(UnsupportedOperationException.class, () -> result.add("x")); + } + @Test @DisplayName("Parse TOON format tabular array to JSON") void parseTabularArray() { diff --git a/src/test/java/dev/toonformat/jtoon/encoder/ArrayEncoderTest.java b/src/test/java/dev/toonformat/jtoon/encoder/ArrayEncoderTest.java index f6871aa..b899939 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/ArrayEncoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/ArrayEncoderTest.java @@ -62,10 +62,9 @@ void givenArrayOfArraysWithMixedInnerTypes_whenEncodePrivate_thenWritesExpected( method.invoke(null, "items", outer, writer, 0, options); // Then - final String expected = String.join("\n", - "items[3]:", - " - [2]: 1,2" - ); + final String expected = """ + items[3]: + - [2]: 1,2"""; assertEquals(expected, writer.toString()); } @@ -133,9 +132,11 @@ void encodeArrayWithAllPrimitivesArrayOfArrays() { // Then assertFalse(lineWriter.toString().isBlank()); - assertEquals(" \"\"[2]:\n" + - " - [3]: 1,2,3\n" + - " - [3]: 4,5,6", lineWriter.toString()); + @SuppressWarnings("StringConcatToTextBlock") + final String expected = " \"\"[2]:\n" + + " - [3]: 1,2,3\n" + + " - [3]: 4,5,6"; + assertEquals(expected, lineWriter.toString()); } @Test diff --git a/src/test/java/dev/toonformat/jtoon/encoder/ListItemEncoderTest.java b/src/test/java/dev/toonformat/jtoon/encoder/ListItemEncoderTest.java index 1404df5..018438f 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/ListItemEncoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/ListItemEncoderTest.java @@ -133,21 +133,22 @@ void usesTabularFormatForNestedUniformObjectArrays() { ArrayEncoder.encodeArray("items",node, writer, 0, opts); // Then - final String expected = String.join("\n", - "items[1]:", - " - users[2]{id,name}:", - " 1,Ada", - " 2,Bob", - " status: active"); + final String expected = """ + items[1]: + - users[2]{id,name}: + 1,Ada + 2,Bob + status: active"""; assertEquals(expected, writer.toString()); } @Test void usesListFormatForNestedObjectArraysWithMismatchedKeys() { // Given - final String json = "[\n" + - " { \"users\": [{ \"id\": 1, \"name\": \"Ada\" }, { \"id\": 2 }], \"status\": \"active\" }\n" + - " ]"; + final String json = """ + [ + { "users": [{ "id": 1, "name": "Ada" }, { "id": 2 }], "status": "active" } + ]"""; final ArrayNode node = (ArrayNode) new ObjectMapper().readTree(json); final EncodeOptions opts = EncodeOptions.DEFAULT; @@ -158,18 +159,17 @@ void usesListFormatForNestedObjectArraysWithMismatchedKeys() { // Then - final String expected = String.join("\n", - "items[1]:", - " - users[2]:", - " - id: 1", - " name: Ada", - " - id: 2", - " status: active"); + final String expected = """ + items[1]: + - users[2]: + - id: 1 + name: Ada + - id: 2 + status: active"""; assertEquals(expected, writer.toString()); } @Test - @DisplayName("given mixed-type array as first value when encoded then writes complex list format") void givenMixedTypeArrayAsFirstValue_whenEncoded_thenWritesComplexListFormat() { // Given final int firstValue = 10; @@ -193,11 +193,11 @@ void givenMixedTypeArrayAsFirstValue_whenEncoded_thenWritesComplexListFormat() { ListItemEncoder.encodeObjectAsListItem(obj, writer, 0, options); // Then - final String expected = String.join("\n", - "- mixed[3]:", - " - 1", - " - [2]: 10,11", - " - a: 5"); + final String expected = """ + - mixed[3]: + - 1 + - [2]: 10,11 + - a: 5"""; assertEquals(expected, writer.toString()); } } diff --git a/src/test/java/dev/toonformat/jtoon/encoder/ObjectEncoderTest.java b/src/test/java/dev/toonformat/jtoon/encoder/ObjectEncoderTest.java index ff7c8ce..2b592f5 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/ObjectEncoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/ObjectEncoderTest.java @@ -237,10 +237,9 @@ void givenFullyFoldedObjectLeaf_whenFlatten_thenWritesObjectAndReturnsNull() thr // Then assertNull(result); - final String expected = String.join("\n", - "user.info:", - " id: 1" - ); + final String expected = """ + user.info: + id: 1"""; assertEquals(expected, writer.toString()); assertTrue(blockedKeys.contains("user")); assertTrue(blockedKeys.contains("user.info")); @@ -596,11 +595,12 @@ void givenPartiallyFolded_whenFlatten_thenWriterReceivesFoldedKeyAndObjectIsEnco @Test void usesListFormatForObjectsContainingArraysOfArrays() { // Given - final String json = "{\n" + - " \"items\": [\n" + - " { \"matrix\": [[1, 2], [3, 4]], \"name\": \"grid\" }\n" + - " ]\n" + - " }"; + final String json = """ + { + "items": [ + { "matrix": [[1, 2], [3, 4]], "name": "grid" } + ] + }"""; final ObjectNode node = (ObjectNode) new ObjectMapper().readTree(json); final EncodeOptions options = EncodeOptions.withFlatten(true); @@ -611,23 +611,24 @@ void usesListFormatForObjectsContainingArraysOfArrays() { ObjectEncoder.encodeObject(node, writer, 0, options, siblings, null, null, new HashSet<>()); // Then - final String expected = String.join("\n", - "items[1]:", - " - matrix[2]:", - " - [2]: 1,2", - " - [2]: 3,4", - " name: grid"); + final String expected = """ + items[1]: + - matrix[2]: + - [2]: 1,2 + - [2]: 3,4 + name: grid"""; assertEquals(expected, writer.toString()); } @Test void testEncodeKeyValuePairWithAKey() { // Given - final String json = "{\n" + - " \"items\": [\n" + - " { \"matrix\": [[1, 2], [3, 4]], \"name\": \"grid\" }\n" + - " ]\n" + - " }"; + final String json = """ + { + "items": [ + { "matrix": [[1, 2], [3, 4]], "name": "grid" } + ] + }"""; final ObjectNode node = (ObjectNode) new ObjectMapper().readTree(json); final EncodeOptions options = EncodeOptions.withFlatten(true); @@ -639,24 +640,25 @@ void testEncodeKeyValuePairWithAKey() { siblings, null, null, REMAINING_DEPTH, new HashSet<>()); // Then - final String expected = String.join("\n", - "items:", - " items[1]:", - " - matrix[2]:", - " - [2]: 1,2", - " - [2]: 3,4", - " name: grid"); + final String expected = """ + items: + items[1]: + - matrix[2]: + - [2]: 1,2 + - [2]: 3,4 + name: grid"""; assertEquals(expected, writer.toString()); } @Test void testEncodeKeyValuePairWithANullKey() { // Given - final String json = "{\n" + - " \"items\": [\n" + - " { \"matrix\": [[1, 2], [3, 4]], \"name\": \"grid\" }\n" + - " ]\n" + - " }"; + final String json = """ + { + "items": [ + { "matrix": [[1, 2], [3, 4]], "name": "grid" } + ] + }"""; final ObjectNode node = (ObjectNode) new ObjectMapper().readTree(json); final EncodeOptions options = EncodeOptions.withFlatten(true); @@ -675,11 +677,12 @@ void testEncodeKeyValuePairWithANullKey() { @Test void testEncodeKeyValuePairWithNullFlattenDepth() { // Given - final String json = "{\n" + - " \"items\": [\n" + - " { \"matrix\": [[1, 2], [3, 4]], \"name\": \"grid\" }\n" + - " ]\n" + - " }"; + final String json = """ + { + "items": [ + { "matrix": [[1, 2], [3, 4]], "name": "grid" } + ] + }"""; final ObjectNode node = (ObjectNode) new ObjectMapper().readTree(json); final EncodeOptions options = EncodeOptions.withFlatten(true); @@ -691,13 +694,13 @@ void testEncodeKeyValuePairWithNullFlattenDepth() { null, null, null, new HashSet<>()); // Then - final String expected = String.join("\n", - "items:", - " items[1]:", - " - matrix[2]:", - " - [2]: 1,2", - " - [2]: 3,4", - " name: grid"); + final String expected = """ + items: + items[1]: + - matrix[2]: + - [2]: 1,2 + - [2]: 3,4 + name: grid"""; assertEquals(expected, writer.toString()); } @@ -724,11 +727,12 @@ void testEncodeKeyValuePairWithoutEmptySiblings() { @Test void testEncodeKeyValuePairWithKeyInBlockedKeysSet() { // Given - final String json = "{\n" + - " \"items\": [\n" + - " { \"matrix\": [[1, 2], [3, 4]], \"name\": \"grid\" }\n" + - " ]\n" + - " }"; + final String json = """ + { + "items": [ + { "matrix": [[1, 2], [3, 4]], "name": "grid" } + ] + }"""; final ObjectNode node = (ObjectNode) new ObjectMapper().readTree(json); final EncodeOptions options = EncodeOptions.withFlatten(true); @@ -741,24 +745,25 @@ void testEncodeKeyValuePairWithKeyInBlockedKeysSet() { siblings, null, null, REMAINING_DEPTH, blockedKeys); // Then - final String expected = String.join("\n", - "items:", - " items[1]:", - " - matrix[2]:", - " - [2]: 1,2", - " - [2]: 3,4", - " name: grid"); + final String expected = """ + items: + items[1]: + - matrix[2]: + - [2]: 1,2 + - [2]: 3,4 + name: grid"""; assertEquals(expected, writer.toString()); } @Test void testEncodeKeyValuePairWithoutFlattenWithAKey() { // Given - final String json = "{\n" + - " \"items\": [\n" + - " { \"matrix\": [[1, 2], [3, 4]], \"name\": \"grid\" }\n" + - " ]\n" + - " }"; + final String json = """ + { + "items": [ + { "matrix": [[1, 2], [3, 4]], "name": "grid" } + ] + }"""; final ObjectNode node = (ObjectNode) new ObjectMapper().readTree(json); final EncodeOptions options = EncodeOptions.withFlatten(false); @@ -770,13 +775,13 @@ void testEncodeKeyValuePairWithoutFlattenWithAKey() { siblings, null, null, REMAINING_DEPTH, new HashSet<>()); // Then - final String expected = String.join("\n", - "items:", - " items[1]:", - " - matrix[2]:", - " - [2]: 1,2", - " - [2]: 3,4", - " name: grid"); + final String expected = """ + items: + items[1]: + - matrix[2]: + - [2]: 1,2 + - [2]: 3,4 + name: grid"""; assertEquals(expected, writer.toString()); } @@ -800,20 +805,13 @@ void handleFullyFoldedLeafForObjectNodeAsLeaf() throws Exception { foldResult, writer, 2, options, "item"); // Then - final String expected = String.join("\n", " item:", - " id: 1"); + final String expected = " item:\n id: 1"; assertEquals(expected, writer.toString()); } @Test void handleFullyFoldedLeafForBokenNodeAsLeaf() throws Exception { // Given - abstract class A extends JsonNode { - protected A() { - } - } - - final ObjectNode objectLeaf = (ObjectNode) new ObjectMapper().readTree("{\"id\":1}"); final Flatten.FoldResult foldResult = new Flatten.FoldResult( "user.info", @@ -831,8 +829,7 @@ protected A() { foldResult, writer, 2, options, "item"); // Then - final String expected = String.join("\n", " item:", - " id: 1"); + final String expected = " item:\n id: 1"; assertEquals(expected, writer.toString()); } diff --git a/src/test/java/dev/toonformat/jtoon/encoder/TabularArrayEncoderTest.java b/src/test/java/dev/toonformat/jtoon/encoder/TabularArrayEncoderTest.java index 8a87ccb..0af4753 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/TabularArrayEncoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/TabularArrayEncoderTest.java @@ -169,10 +169,10 @@ void givenUniformObjects_whenEncodeArrayAsTabular_thenWritesHeaderAndRows() { TabularArrayEncoder.encodeArrayOfObjectsAsTabular("users", rows, header, writer, 0, options); // Then - final String expected = String.join("\n", - "users[2]{id,name}:", - " 1,Ada", - " 2,Bob"); + final String expected = """ + users[2]{id,name}: + 1,Ada + 2,Bob"""; assertEquals(expected, writer.toString()); } @@ -199,12 +199,32 @@ void givenHeaderAndRows_whenWriteTabularRows_thenWritesValuesWithIndent() { TabularArrayEncoder.writeTabularRows(rows, header, writer, 2, options); // Then - final String expected = String.join("\n", - " 10,20", - " 11,21"); + @SuppressWarnings("StringConcatToTextBlock") + final String expected = " 10,20\n" + + " 11,21"; assertEquals(expected, writer.toString()); } + @Test + void givenUniformObjects_whenDetectHeader_thenReturnsUnmodifiableList() { + // Given + final ObjectNode a = jsonNodeFactory.objectNode(); + a.put("id", 1); + a.put("name", "Ada"); + + final ObjectNode b = jsonNodeFactory.objectNode(); + b.put("id", 2); + b.put("name", "Bob"); + + final ArrayNode rows = jsonNodeFactory.arrayNode().add(a).add(b); + + // When + final List header = TabularArrayEncoder.detectTabularHeader(rows); + + // Then + assertThrows(UnsupportedOperationException.class, () -> header.add("extra")); + } + @Test void testDetectTabularHeaderWithEmptyRow() { // Given @@ -279,9 +299,9 @@ void testDetectTabularHeaderWithUnevenObjectInTheList() { TabularArrayEncoder.writeTabularRows(rows, header, writer, 2, options); // Then - final String expected = String.join("\n", - " 10,20", - " 11"); + @SuppressWarnings("StringConcatToTextBlock") + final String expected = " 10,20\n" + + " 11"; assertEquals(expected, writer.toString()); } @@ -308,8 +328,7 @@ void testDetectTabularHeaderWithUnevenObjectArrayMixInTheList() { TabularArrayEncoder.writeTabularRows(rows, header, writer, 2, options); // Then - final String expected = String.join("\n", - " 10,20"); + final String expected = " 10,20"; assertEquals(expected, writer.toString()); } } diff --git a/src/test/java/dev/toonformat/jtoon/encoder/ValueEncoderTest.java b/src/test/java/dev/toonformat/jtoon/encoder/ValueEncoderTest.java index f609ca5..d23ac32 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/ValueEncoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/ValueEncoderTest.java @@ -73,9 +73,9 @@ void givenObject_whenEncodeValue_thenWritesObjectLines() { final String result = ValueEncoder.encodeValue(obj, options); // Then - final String expected = String.join("\n", - "a: 1", - "b: x"); + final String expected = """ + a: 1 + b: x"""; assertEquals(expected, result); } } diff --git a/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java b/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java index 31a9e67..008268f 100644 --- a/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java +++ b/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java @@ -61,8 +61,6 @@ class JsonNormalizerTest { private static final long TEST_BIG_LONG = 123456789L; private static final int TEST_MAP_SIZE = 3; private static final int TEST_ARRAY_SIZE = 3; - private static final int TEST_MAX_DEPTH = 256; - private static final int DEEP_NESTING_DEPTH = 600; private static final double DOUBLE_DELTA = 0.001; private static final int TEST_AGE = 30; private static final int EXPECTED_SIZE_FOUR = 4; @@ -146,7 +144,7 @@ void testEmptyString() { @DisplayName("should normalize Boolean to BooleanNode") void testBoolean() { // Given - final JsonNode resultTrue = JsonNormalizer.normalize(Boolean.TRUE); + final JsonNode resultTrue = JsonNormalizer.normalize(true); // Then assertTrue(resultTrue.isBoolean()); assertTrue(resultTrue.asBoolean()); @@ -157,7 +155,7 @@ void testBoolean() { @DisplayName("should normalize Boolean to BooleanNode") void testBoolean2() { // Given - final JsonNode resultFalse = JsonNormalizer.normalize(Boolean.FALSE); + final JsonNode resultFalse = JsonNormalizer.normalize(false); // Then assertTrue(resultFalse.isBoolean()); assertFalse(resultFalse.asBoolean()); @@ -1471,6 +1469,7 @@ void givenGregorianCalendar_whenTryNormalizeTemporal_thenIsoStringNode() throws @Test @DisplayName("Given Date, When tryNormalizeTemporal is called, Then an ISO date StringNode is returned") + @SuppressWarnings("JavaUtilDate") void givenDate_whenTryNormalizeTemporal_thenIsoStringNode() throws Exception { // Given final Date input = new Date(1764362004); @@ -1729,7 +1728,7 @@ void NormalizeArray_thenNullNode() throws Exception { @Nested @DisplayName("NormalizePojo") class NormalizePojo { - class ExplodingPojo { + static class ExplodingPojo { public String getValue() { throw new IllegalStateException("Boom"); } diff --git a/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerThreadSafetyTest.java b/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerThreadSafetyTest.java index d355564..aea12d4 100644 --- a/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerThreadSafetyTest.java +++ b/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerThreadSafetyTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.*; import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.List; import java.util.Map; import java.util.UUID; @@ -23,7 +24,7 @@ void normalizeThreadSafety() { final String id = UUID.randomUUID().toString(); final Map input = Map.of( "id", id, - "timestamp", LocalDateTime.now(), + "timestamp", LocalDateTime.now(ZoneOffset.UTC), "tags", List.of("a", "b", "c"), "nested", Map.of("key", "value") );