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
return declaredMethod.invoke(null, args);
}
+ @Test
+ @DisplayName("Should parse array and return unmodifiable list")
+ void parseArrayReturnsUnmodifiableList() {
+ // Given
+ setUpContext("[3]: 1,2,3");
+
+ // When
+ final List