diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java index e3580c0..e68c1db 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java @@ -1,5 +1,7 @@ package dev.toonformat.jtoon.decoder; +import dev.toonformat.jtoon.Delimiter; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -11,7 +13,7 @@ /** * Handles decoding of TOON arrays to JSON format. */ -public class ArrayDecoder { +public final class ArrayDecoder { private ArrayDecoder() { throw new UnsupportedOperationException("Utility class cannot be instantiated"); @@ -26,8 +28,8 @@ private ArrayDecoder() { * @param context decode an object to deal with lines, delimiter and options * @return parsed array with delimiter */ - protected static List parseArray(String header, int depth, DecodeContext context) { - String arrayDelimiter = extractDelimiterFromHeader(header, context); + static List parseArray(String header, int depth, DecodeContext context) { + Delimiter arrayDelimiter = extractDelimiterFromHeader(header, context); return parseArrayWithDelimiter(header, depth, arrayDelimiter, context); } @@ -40,15 +42,16 @@ protected static List parseArray(String header, int depth, DecodeContext * @param context decode an object to deal with lines, delimiter and options * @return extracted delimiter from header */ - protected static String extractDelimiterFromHeader(String header, DecodeContext context) { + static Delimiter extractDelimiterFromHeader(String header, DecodeContext context) { Matcher matcher = ARRAY_HEADER_PATTERN.matcher(header); - if (matcher.find() && matcher.groupCount() == 3) { + if (matcher.find()) { String delimiter = matcher.group(3); if (delimiter != null) { if ("\t".equals(delimiter)) { - return "\t"; - } else if ("|".equals(delimiter)) { - return "|"; + return Delimiter.TAB; + } + if ("|".equals(delimiter)) { + return Delimiter.PIPE; } } } @@ -67,7 +70,7 @@ protected static String extractDelimiterFromHeader(String header, DecodeContext * @param context decode an object to deal with lines, delimiter and options * @return parsed array */ - protected static List parseArrayWithDelimiter(String header, int depth, String arrayDelimiter, DecodeContext context) { + static List parseArrayWithDelimiter(String header, int depth, Delimiter arrayDelimiter, DecodeContext context) { Matcher tabularMatcher = TABULAR_HEADER_PATTERN.matcher(header); Matcher arrayMatcher = ARRAY_HEADER_PATTERN.matcher(header); @@ -130,7 +133,7 @@ protected static List parseArrayWithDelimiter(String header, int depth, * @param header header * @param actualLength actual length */ - protected static void validateArrayLength(String header, int actualLength) { + static void validateArrayLength(String header, int actualLength) { Integer declaredLength = extractLengthFromHeader(header); if (declaredLength != null && declaredLength != actualLength) { throw new IllegalArgumentException( @@ -147,12 +150,8 @@ protected static void validateArrayLength(String header, int actualLength) { */ private static Integer extractLengthFromHeader(String header) { Matcher matcher = ARRAY_HEADER_PATTERN.matcher(header); - if (matcher.find() && matcher.groupCount() > 2) { - try { - return Integer.parseInt(matcher.group(2)); - } catch (NumberFormatException e) { - return null; - } + if (matcher.find()) { + return Integer.parseInt(matcher.group(2)); } return null; } @@ -164,7 +163,7 @@ private static Integer extractLengthFromHeader(String header) { * @param arrayDelimiter array delimiter * @return parsed array values */ - protected static List parseArrayValues(String values, String arrayDelimiter) { + static List parseArrayValues(String values, Delimiter arrayDelimiter) { List result = new ArrayList<>(); List rawValues = parseDelimitedValues(values, arrayDelimiter); for (String value : rawValues) { @@ -181,12 +180,12 @@ protected static List parseArrayValues(String values, String arrayDelimi * @param arrayDelimiter array delimiter * @return parsed delimited values */ - protected static List parseDelimitedValues(String input, String arrayDelimiter) { + static List parseDelimitedValues(String input, Delimiter arrayDelimiter) { List result = new ArrayList<>(); StringBuilder stringBuilder = new StringBuilder(); boolean inQuotes = false; boolean escaped = false; - char delimiterChar = arrayDelimiter.charAt(0); + char delimiterChar = arrayDelimiter.toString().charAt(0); int i = 0; while (i < input.length()) { @@ -220,7 +219,7 @@ protected static List parseDelimitedValues(String input, String arrayDel } // Add final value - if (!stringBuilder.isEmpty() || input.endsWith(arrayDelimiter)) { + if (!stringBuilder.isEmpty() || input.endsWith(arrayDelimiter.toString())) { result.add(stringBuilder.toString().trim()); } diff --git a/src/main/java/dev/toonformat/jtoon/decoder/DecodeContext.java b/src/main/java/dev/toonformat/jtoon/decoder/DecodeContext.java index 50697c7..f319efe 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/DecodeContext.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/DecodeContext.java @@ -1,6 +1,7 @@ package dev.toonformat.jtoon.decoder; import dev.toonformat.jtoon.DecodeOptions; +import dev.toonformat.jtoon.Delimiter; /** * Deals with the main attributes used to decode TOON to JSON format @@ -18,7 +19,7 @@ public class DecodeContext { /** * Delimiter used to split array elements. */ - protected String delimiter; + protected Delimiter delimiter; /** * Current line being decoded. */ diff --git a/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java b/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java index f3e8834..75c6c99 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java @@ -6,7 +6,7 @@ /** * Handles indentation, depth, conflicts, and validation for other decode classes. */ -public class DecodeHelper { +public final class DecodeHelper { private DecodeHelper() { throw new UnsupportedOperationException("Utility class cannot be instantiated"); @@ -76,7 +76,7 @@ private static int computeLeadingSpaces(String line, DecodeContext context) { * @param line the line string to parse * @return true or false depending on if the line is blank or not */ - protected static boolean isBlankLine(String line) { + static boolean isBlankLine(String line) { return line.trim().isEmpty(); } @@ -87,7 +87,7 @@ protected static boolean isBlankLine(String line) { * @param content the content string to parse * @return the unquoted colon */ - protected static int findUnquotedColon(String content) { + static int findUnquotedColon(String content) { boolean inQuotes = false; boolean escaped = false; @@ -115,7 +115,7 @@ protected static int findUnquotedColon(String content) { * @param context decode an object to deal with lines, delimiter, and options * @return index aiming for the next non-blank line */ - protected static int findNextNonBlankLine(int startIndex, DecodeContext context) { + static int findNextNonBlankLine(int startIndex, DecodeContext context) { int index = startIndex; while (index < context.lines.length && isBlankLine(context.lines[index])) { index++; @@ -132,7 +132,7 @@ protected static int findNextNonBlankLine(int startIndex, DecodeContext context) * @param context decode an object to deal with lines, delimiter, and options * @throws IllegalArgumentException in case there's a expansion conflict */ - protected static void checkFinalValueConflict(String finalSegment, Object existing, Object value, DecodeContext context) { + static void checkFinalValueConflict(String finalSegment, Object existing, Object value, DecodeContext context) { if (existing != null && context.options.strict()) { // Check for conflicts in strict mode if (existing instanceof Map && !(value instanceof Map)) { @@ -157,7 +157,7 @@ protected static void checkFinalValueConflict(String finalSegment, Object existi * @param value present value in a map * @param context decode an object to deal with lines, delimiter, and options */ - protected static void checkPathExpansionConflict(Map map, String key, Object value, DecodeContext context) { + static void checkPathExpansionConflict(Map map, String key, Object value, DecodeContext context) { if (!context.options.strict()) { return; } @@ -172,7 +172,7 @@ protected static void checkPathExpansionConflict(Map map, String * @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 */ - protected static Integer findNextNonBlankLineDepth(DecodeContext context) { + static Integer findNextNonBlankLineDepth(DecodeContext context) { int nextLineIdx = context.currentLine; while (nextLineIdx < context.lines.length && isBlankLine(context.lines[nextLineIdx])) { nextLineIdx++; @@ -191,7 +191,7 @@ protected static Integer findNextNonBlankLineDepth(DecodeContext context) { * @param context decode an object to deal with lines, delimiter, and options * @throws IllegalArgumentException in case the next depth is equal to 0 */ - protected static void validateNoMultiplePrimitivesAtRoot(DecodeContext context) { + static void validateNoMultiplePrimitivesAtRoot(DecodeContext context) { int lineIndex = context.currentLine; while (lineIndex < context.lines.length && isBlankLine(context.lines[lineIndex])) { lineIndex++; diff --git a/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java index d5de8e7..5b80d4d 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java @@ -1,5 +1,6 @@ package dev.toonformat.jtoon.decoder; +import dev.toonformat.jtoon.Delimiter; import dev.toonformat.jtoon.PathExpansion; import dev.toonformat.jtoon.util.StringEscaper; @@ -13,9 +14,11 @@ /** * Handles decoding of key values/arrays to JSON format. */ -public class KeyDecoder { +public final class KeyDecoder { - private KeyDecoder() {throw new UnsupportedOperationException("Utility class cannot be instantiated");} + private KeyDecoder() { + throw new UnsupportedOperationException("Utility class cannot be instantiated"); + } /** * Processes a keyed array line (e.g., "key[3]: value"). @@ -26,8 +29,8 @@ public class KeyDecoder { * @param parentDepth parent depth of keyed array line * @param context decode an object to deal with lines, delimiter and options */ - protected static void processKeyedArrayLine(Map result, String content, String originalKey, - int parentDepth, DecodeContext context) { + static void processKeyedArrayLine(Map result, String content, String originalKey, + int parentDepth, DecodeContext context) { String key = StringEscaper.unescape(originalKey); String arrayHeader = content.substring(originalKey.length()); List arrayValue = ArrayDecoder.parseArray(arrayHeader, parentDepth + 1, context); @@ -50,7 +53,7 @@ protected static void processKeyedArrayLine(Map result, String c * @param value value * @param context decode an object to deal with lines, delimiter and options */ - protected static void expandPathIntoMap(Map current, String dottedKey, Object value, DecodeContext context) { + static void expandPathIntoMap(Map current, String dottedKey, Object value, DecodeContext context) { String[] segments = dottedKey.split("\\."); // Navigate/create nested structure @@ -73,7 +76,7 @@ protected static void expandPathIntoMap(Map current, String dott if (context.options.strict()) { throw new IllegalArgumentException( String.format("Path expansion conflict: %s is %s, cannot expand to object", - segment, existing.getClass().getSimpleName())); + segment, existing.getClass().getSimpleName())); } // LWW: overwrite with new nested object Map nested = new LinkedHashMap<>(); @@ -101,7 +104,7 @@ protected static void expandPathIntoMap(Map current, String dott * @param depth the depth of the value line * @param context decode an object to deal with lines, delimiter and options */ - protected static void processKeyValueLine(Map result, String content, int depth, DecodeContext context) { + static void processKeyValueLine(Map result, String content, int depth, DecodeContext context) { int colonIdx = DecodeHelper.findUnquotedColon(content); if (colonIdx > 0) { @@ -127,8 +130,8 @@ protected static void processKeyValueLine(Map result, String con * @param depth the depth of the value pair * @param context decode an object to deal with lines, delimiter and options */ - protected static void parseKeyValuePairIntoMap(Map map, String key, String value, - int depth, DecodeContext context) { + static void parseKeyValuePairIntoMap(Map map, String key, String value, + int depth, DecodeContext context) { String unescapedKey = StringEscaper.unescape(key); Object parsedValue = parseKeyValue(value, depth, context); @@ -144,7 +147,7 @@ protected static void parseKeyValuePairIntoMap(Map map, String k * @param context decode an object to deal with lines, delimiter and options * @return true if a key should be expanded or false if not */ - protected static boolean shouldExpandKey(String key, DecodeContext context) { + static boolean shouldExpandKey(String key, DecodeContext context) { if (context.options.expandPaths() != PathExpansion.SAFE) { return false; } @@ -238,8 +241,8 @@ private static void putKeyValueIntoMap(Map map, String originalK * @param context decode an object to deal with lines, delimiter, and options * @return parsed a key-value pair */ - protected static Object parseKeyValuePair(String key, String value, int depth, boolean parseRootFields, - DecodeContext context) { + static Object parseKeyValuePair(String key, String value, int depth, boolean parseRootFields, + DecodeContext context) { Map obj = new LinkedHashMap<>(); parseKeyValuePairIntoMap(obj, key, value, depth, context); @@ -258,7 +261,7 @@ protected static Object parseKeyValuePair(String key, String value, int depth, b * @param context decode an object to deal with lines, delimiter, and options * @return parsed keyed array value */ - protected static Object parseKeyedArrayValue(Matcher keyedArray, String content, int depth, DecodeContext context) { + static Object parseKeyedArrayValue(Matcher keyedArray, String content, int depth, DecodeContext context) { String originalKey = keyedArray.group(1).trim(); String key = StringEscaper.unescape(originalKey); String arrayHeader = content.substring(keyedArray.group(1).length()); @@ -292,7 +295,7 @@ protected static Object parseKeyedArrayValue(Matcher keyedArray, String content, * @param context decode an object to deal with lines, delimiter and options * @return true if the field was processed as a keyed array, false otherwise */ - protected static boolean parseKeyedArrayField(String fieldContent, Map item, int depth, DecodeContext context) { + static boolean parseKeyedArrayField(String fieldContent, Map item, int depth, DecodeContext context) { Matcher keyedArray = KEYED_ARRAY_PATTERN.matcher(fieldContent); if (!keyedArray.matches()) { return false; @@ -303,7 +306,7 @@ protected static boolean parseKeyedArrayField(String fieldContent, Map item, int depth, DecodeContext context) { + static boolean parseKeyValueField(String fieldContent, Map item, int depth, DecodeContext context) { int colonIdx = DecodeHelper.findUnquotedColon(fieldContent); if (colonIdx <= 0) { return false; diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java index 603409f..1b77479 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java @@ -1,5 +1,6 @@ package dev.toonformat.jtoon.decoder; +import dev.toonformat.jtoon.Delimiter; import dev.toonformat.jtoon.util.StringEscaper; import java.util.LinkedHashMap; @@ -12,7 +13,7 @@ /** * Handles decoding of TOON list item to JSON format. */ -public class ListItemDecoder { +public final class ListItemDecoder { private ListItemDecoder() {throw new UnsupportedOperationException("Utility class cannot be instantiated");} @@ -67,7 +68,7 @@ public static Object parseListItem(String content, int depth, DecodeContext cont // Check for standalone array (e.g., "[2]: 1,2") if (itemContent.startsWith("[")) { // For nested arrays in list items, default to comma delimiter if not specified - String nestedArrayDelimiter = ArrayDecoder.extractDelimiterFromHeader(itemContent, context); + Delimiter nestedArrayDelimiter = ArrayDecoder.extractDelimiterFromHeader(itemContent, context); // parseArrayWithDelimiter handles currentLine increment internally // For inline arrays, it increments. For multi-line arrays, parseListArray // handles it. @@ -85,7 +86,7 @@ public static Object parseListItem(String content, int depth, DecodeContext cont String arrayHeader = itemContent.substring(keyedArray.group(1).length()); // For nested arrays in list items, default to comma delimiter if not specified - String nestedArrayDelimiter = ArrayDecoder.extractDelimiterFromHeader(arrayHeader, context); + Delimiter nestedArrayDelimiter = ArrayDecoder.extractDelimiterFromHeader(arrayHeader, context); List arrayValue = ArrayDecoder.parseArrayWithDelimiter(arrayHeader, depth + 2, nestedArrayDelimiter, context); Map item = new LinkedHashMap<>(); diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java index 46c718b..c1f23a1 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java @@ -11,7 +11,7 @@ /** * Handles decoding of TOON objects to JSON format. */ -public class ObjectDecoder { +public final class ObjectDecoder { private ObjectDecoder() { throw new UnsupportedOperationException("Utility class cannot be instantiated"); @@ -24,7 +24,7 @@ private ObjectDecoder() { * @param context decode an object to deal with lines, delimiter and options * @return parsed nested object */ - protected static Map parseNestedObject(int parentDepth, DecodeContext context) { + static Map parseNestedObject(int parentDepth, DecodeContext context) { Map result = new LinkedHashMap<>(); while (context.currentLine < context.lines.length) { @@ -75,7 +75,7 @@ private static void processDirectChildLine(Map result, String li * @param depth the depth of the object field * @param context decode an object to deal with lines, delimiter and options */ - protected static void parseRootObjectFields(Map obj, int depth, DecodeContext context) { + static void parseRootObjectFields(Map obj, int depth, DecodeContext context) { while (context.currentLine < context.lines.length) { String line = context.lines[context.currentLine]; int lineDepth = DecodeHelper.getDepth(line, context); @@ -144,7 +144,7 @@ private static void processRootKeyedArrayLine(Map objectMap, Str * @param context decode an object to deal with lines, delimiter and options * @return the parsed scalar value */ - protected static Object parseBareScalarValue(String content, int depth, DecodeContext context) { + static Object parseBareScalarValue(String content, int depth, DecodeContext context) { Object result = PrimitiveDecoder.parse(content); context.currentLine++; @@ -164,7 +164,7 @@ protected static Object parseBareScalarValue(String content, int depth, DecodeCo * @param context decode an object to deal with lines, delimiter and options * @return the parsed value (Map, List, or primitive) */ - protected static Object parseFieldValue(String fieldValue, int fieldDepth, DecodeContext context) { + static Object parseFieldValue(String fieldValue, int fieldDepth, DecodeContext context) { // Check if the next line is nested if (context.currentLine + 1 < context.lines.length) { int nextDepth = DecodeHelper.getDepth(context.lines[context.currentLine + 1], context); @@ -204,7 +204,7 @@ protected static Object parseFieldValue(String fieldValue, int fieldDepth, Decod * @param context decode an object to deal with lines, delimiter and options * @return the parsed value (Map, List, or primitive) */ - protected static Object parseObjectItemValue(String value, int depth, DecodeContext context) { + static Object parseObjectItemValue(String value, int depth, DecodeContext context) { boolean isEmpty = value.trim().isEmpty(); // Find the next non-blank line and its depth diff --git a/src/main/java/dev/toonformat/jtoon/decoder/PrimitiveDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/PrimitiveDecoder.java index bdee6de..4654326 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/PrimitiveDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/PrimitiveDecoder.java @@ -28,7 +28,7 @@ * parse("") β†’ "" (empty string) * } */ -final class PrimitiveDecoder { +public final class PrimitiveDecoder { private PrimitiveDecoder() { throw new UnsupportedOperationException("Utility class cannot be instantiated"); @@ -71,9 +71,7 @@ static Object parse(String value) { // Check for leading zeros (treat as string, except for "0", "-0", "0.0", etc.) String trimmed = value.trim(); - if (trimmed.length() > 1 - && trimmed.matches("^-?0+[0-7].*") //octal number - && !trimmed.matches("^-?0+(\\.0+)?([eE][+-]?\\d+)?$")) { + if (trimmed.length() > 1 && trimmed.matches("^-?0+[0-7].*")) { return value; } diff --git a/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java index 80d6d93..e4aeece 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java @@ -1,5 +1,6 @@ package dev.toonformat.jtoon.decoder; +import dev.toonformat.jtoon.Delimiter; import dev.toonformat.jtoon.util.StringEscaper; import java.util.ArrayList; @@ -14,7 +15,7 @@ /** * Handles decoding of tabular arrays to JSON format. */ -public class TabularArrayDecoder { +public final class TabularArrayDecoder { private TabularArrayDecoder() { throw new UnsupportedOperationException("Utility class cannot be instantiated"); @@ -31,7 +32,7 @@ private TabularArrayDecoder() { * @param context decode an object to deal with lines, delimiter and options * @return tabular array converted to JSON format */ - public static List parseTabularArray(String header, int depth, String arrayDelimiter, DecodeContext context) { + public static List parseTabularArray(String header, int depth, Delimiter arrayDelimiter, DecodeContext context) { Matcher matcher = TABULAR_HEADER_PATTERN.matcher(header); if (!matcher.find()) { return Collections.emptyList(); @@ -71,7 +72,7 @@ public static List parseTabularArray(String header, int depth, String ar * @param context decode an object to deal with lines, delimiter and options * @return list of keys */ - private static List parseTabularKeys(String keysStr, String arrayDelimiter, DecodeContext context) { + private static List parseTabularKeys(String keysStr, Delimiter arrayDelimiter, DecodeContext context) { // Validate delimiter mismatch between bracket and brace fields if (context.options.strict()) { validateKeysDelimiter(keysStr, arrayDelimiter); @@ -91,8 +92,8 @@ private static List parseTabularKeys(String keysStr, String arrayDelimit * @param keysStr the string representation of keys * @param expectedDelimiter the expected delimiter used in the array */ - private static void validateKeysDelimiter(String keysStr, String expectedDelimiter) { - char expectedChar = expectedDelimiter.charAt(0); + private static void validateKeysDelimiter(String keysStr, Delimiter expectedDelimiter) { + char expectedChar = expectedDelimiter.toString().charAt(0); boolean inQuotes = false; boolean escaped = false; @@ -141,7 +142,7 @@ private static void checkDelimiterMismatch(char expectedChar, char actualChar) { * @param context decode an object to deal with lines, delimiter and options * @return true if parsing should continue, false if an array should terminate */ - private static boolean processTabularArrayLine(int expectedRowDepth, List keys, String arrayDelimiter, + private static boolean processTabularArrayLine(int expectedRowDepth, List keys, Delimiter arrayDelimiter, List result, DecodeContext context) { String line = context.lines[context.currentLine]; @@ -236,7 +237,7 @@ private static boolean shouldTerminateTabularArray(String line, int lineDepth, i * @return true if a line was processed and the currentLine should be incremented, false otherwise. */ private static boolean processTabularRow(String line, int lineDepth, int expectedRowDepth, List keys, - String arrayDelimiter, List result, DecodeContext context) { + Delimiter arrayDelimiter, List result, DecodeContext context) { if (lineDepth == expectedRowDepth) { String rowContent = line.substring(expectedRowDepth * context.options.indent()); Map row = parseTabularRow(rowContent, keys, arrayDelimiter, context); @@ -260,7 +261,7 @@ private static boolean processTabularRow(String line, int lineDepth, int expecte * @param context decode an object to deal with lines, delimiter and options * @return a Map containing the parsed row values */ - private static Map parseTabularRow(String rowContent, List keys, String arrayDelimiter, DecodeContext context) { + private static Map parseTabularRow(String rowContent, List keys, Delimiter arrayDelimiter, DecodeContext context) { Map row = new LinkedHashMap<>(); List values = ArrayDecoder.parseArrayValues(rowContent, arrayDelimiter); diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java index 9652d26..fe3158e 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java @@ -64,7 +64,7 @@ public static Object decode(String toon, DecodeOptions options) { final DecodeContext context = new DecodeContext(); context.lines = processed.split("\r?\n", -1); context.options = options; - context.delimiter = options.delimiter().toString(); + context.delimiter = options.delimiter(); int lineIndex = context.currentLine; String line = context.lines[lineIndex]; @@ -77,28 +77,26 @@ public static Object decode(String toon, DecodeOptions options) { return new LinkedHashMap<>(); } - String content = depth == 0 ? line : line.substring(depth * context.options.indent()); - // Handle standalone arrays: [2]: - if (!content.isEmpty() && content.charAt(0) == '[') { - return ArrayDecoder.parseArray(content, depth, context); + if (!line.isEmpty() && line.charAt(0) == '[') { + return ArrayDecoder.parseArray(line, depth, context); } // Handle keyed arrays: items[2]{id,name}: - Matcher keyedArray = KEYED_ARRAY_PATTERN.matcher(content); + Matcher keyedArray = KEYED_ARRAY_PATTERN.matcher(line); if (keyedArray.matches()) { - return KeyDecoder.parseKeyedArrayValue(keyedArray, content, depth, context); + return KeyDecoder.parseKeyedArrayValue(keyedArray, line, depth, context); } // Handle key-value pairs: name: Ada - int colonIdx = DecodeHelper.findUnquotedColon(content); + int colonIdx = DecodeHelper.findUnquotedColon(line); if (colonIdx > 0) { - String key = content.substring(0, colonIdx).trim(); - String value = content.substring(colonIdx + 1).trim(); + String key = line.substring(0, colonIdx).trim(); + String value = line.substring(colonIdx + 1).trim(); return KeyDecoder.parseKeyValuePair(key, value, depth, depth == 0, context); } // Bare scalar value - return ObjectDecoder.parseBareScalarValue(content, depth, context); + return ObjectDecoder.parseBareScalarValue(line, depth, context); } /** diff --git a/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java b/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java index 6719091..9e16e4d 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java @@ -13,7 +13,7 @@ /** * Recursively flattens a JSON object or array into a single-level object. */ -public class Flatten { +public final class Flatten { private Flatten() { throw new UnsupportedOperationException("Utility class cannot be instantiated"); diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java index e36231f..59775be 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java @@ -110,9 +110,11 @@ public static void encodeKeyValuePair(String key, if (value.isValueNode()) { writer.push(depth, encodedKey + COLON + SPACE + PrimitiveEncoder.encodePrimitive(value, options.delimiter().toString())); - } else if (value.isArray()) { + } + if (value.isArray()) { ArrayEncoder.encodeArray(key, (ArrayNode) value, writer, depth, options); - } else if (value.isObject()) { + } + if (value.isObject()) { ObjectNode objValue = (ObjectNode) value; writer.push(depth, encodedKey + COLON); if (!objValue.isEmpty()) { diff --git a/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java b/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java index bdac3c9..52581f5 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 tools.jackson.core.JacksonException; import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.node.ArrayNode; @@ -175,9 +176,7 @@ private static Optional tryConvertToLong(Double value) { return Optional.empty(); } long longVal = value.longValue(); - return longVal == value - ? Optional.of(LongNode.valueOf(longVal)) - : Optional.empty(); + return Optional.of(LongNode.valueOf(longVal)); } /** @@ -283,7 +282,7 @@ private static ObjectNode normalizeMap(Map map) { private static JsonNode tryNormalizePojo(Object value) { try { return MAPPER.valueToTree(value); - } catch (IllegalArgumentException e) { + } catch (Exception e) { return NullNode.getInstance(); } } diff --git a/src/test/java/dev/toonformat/jtoon/DecodeOptionsTest.java b/src/test/java/dev/toonformat/jtoon/DecodeOptionsTest.java index 1d3d498..ff8bdf3 100644 --- a/src/test/java/dev/toonformat/jtoon/DecodeOptionsTest.java +++ b/src/test/java/dev/toonformat/jtoon/DecodeOptionsTest.java @@ -20,7 +20,10 @@ class DefaultOptions { @Test @DisplayName("should have correct default values") void testDefaultValues() { + // Given DecodeOptions options = DecodeOptions.DEFAULT; + + // Then assertEquals(2, options.indent()); assertEquals(Delimiter.COMMA, options.delimiter()); assertTrue(options.strict()); @@ -29,7 +32,10 @@ void testDefaultValues() { @Test @DisplayName("should create options with no-arg constructor") void testNoArgConstructor() { + // Given DecodeOptions options = new DecodeOptions(); + + // Then assertEquals(2, options.indent()); assertEquals(Delimiter.COMMA, options.delimiter()); assertTrue(options.strict()); @@ -43,7 +49,10 @@ class FactoryMethods { @Test @DisplayName("withIndent should create options with custom indent") void testWithIndent() { + // Given DecodeOptions options = DecodeOptions.withIndent(4); + + // Then assertEquals(4, options.indent()); assertEquals(Delimiter.COMMA, options.delimiter()); assertTrue(options.strict()); @@ -52,7 +61,10 @@ void testWithIndent() { @Test @DisplayName("withDelimiter should create options with custom delimiter") void testWithDelimiter() { + // Given DecodeOptions options = DecodeOptions.withDelimiter(Delimiter.PIPE); + + // Then assertEquals(2, options.indent()); assertEquals(Delimiter.PIPE, options.delimiter()); assertTrue(options.strict()); @@ -61,7 +73,10 @@ void testWithDelimiter() { @Test @DisplayName("withStrict should create options with custom strict mode") void testWithStrict() { + // Given DecodeOptions options = DecodeOptions.withStrict(false); + + // Then assertEquals(2, options.indent()); assertEquals(Delimiter.COMMA, options.delimiter()); assertFalse(options.strict()); @@ -75,7 +90,10 @@ class CustomOptions { @Test @DisplayName("should create options with all custom values") void testAllCustomValues() { + // Given DecodeOptions options = new DecodeOptions(4, Delimiter.TAB, false, PathExpansion.OFF); + + // Then assertEquals(4, options.indent()); assertEquals(Delimiter.TAB, options.delimiter()); assertFalse(options.strict()); @@ -84,6 +102,7 @@ void testAllCustomValues() { @Test @DisplayName("should support all delimiter types") void testAllDelimiters() { + // Then assertEquals(Delimiter.COMMA, new DecodeOptions(2, Delimiter.COMMA, true, PathExpansion.OFF).delimiter()); assertEquals(Delimiter.TAB, new DecodeOptions(2, Delimiter.TAB, true, PathExpansion.OFF).delimiter()); assertEquals(Delimiter.PIPE, new DecodeOptions(2, Delimiter.PIPE, true, PathExpansion.OFF).delimiter()); @@ -97,8 +116,11 @@ class RecordBehavior { @Test @DisplayName("should be equal when values are equal") void testEquality() { + // Given DecodeOptions options1 = new DecodeOptions(2, Delimiter.COMMA, true, PathExpansion.OFF); DecodeOptions options2 = new DecodeOptions(2, Delimiter.COMMA, true, PathExpansion.OFF); + + // Then assertEquals(options1, options2); assertEquals(options1.hashCode(), options2.hashCode()); } @@ -106,11 +128,13 @@ void testEquality() { @Test @DisplayName("should not be equal when values differ") void testInequality() { + // Given DecodeOptions options1 = new DecodeOptions(2, Delimiter.COMMA, true, PathExpansion.OFF); DecodeOptions options2 = new DecodeOptions(4, Delimiter.COMMA, true, PathExpansion.OFF); DecodeOptions options3 = new DecodeOptions(2, Delimiter.PIPE, true, PathExpansion.OFF); DecodeOptions options4 = new DecodeOptions(2, Delimiter.COMMA, false, PathExpansion.OFF); + // Then assertNotEquals(options1, options2); assertNotEquals(options1, options3); assertNotEquals(options1, options4); @@ -119,8 +143,13 @@ void testInequality() { @Test @DisplayName("should have meaningful toString") void testToString() { + // Given DecodeOptions options = new DecodeOptions(4, Delimiter.TAB, false, PathExpansion.OFF); + + // When String str = options.toString(); + + // Then assertTrue(str.contains("4"), "ToString should contain indent value: " + str); assertTrue(str.contains("TAB") || str.contains("delimiter="), "ToString should contain delimiter: " + str); assertTrue(str.contains("false") || str.contains("strict="), "ToString should contain strict value: " + str); diff --git a/src/test/java/dev/toonformat/jtoon/EncodeOptionsTest.java b/src/test/java/dev/toonformat/jtoon/EncodeOptionsTest.java index d947ce7..d8c7c8c 100644 --- a/src/test/java/dev/toonformat/jtoon/EncodeOptionsTest.java +++ b/src/test/java/dev/toonformat/jtoon/EncodeOptionsTest.java @@ -101,6 +101,22 @@ void givenFlattenFlag_whenUsingWithFlatten_thenOnlyFlattenIsModified() { assertEquals(Integer.MAX_VALUE, opts.flattenDepth()); } + @Test + void givenNegativeFlattenFlag_whenUsingWithFlatten_thenOnlyFlattenIsModified() { + // Given + boolean flatten = false; + + // When + EncodeOptions opts = EncodeOptions.withFlatten(flatten); + + // Then + assertEquals(2, opts.indent()); + assertEquals(Delimiter.COMMA, opts.delimiter()); + assertFalse(opts.lengthMarker()); + assertEquals(KeyFolding.OFF, opts.flatten()); + assertEquals(Integer.MAX_VALUE, opts.flattenDepth()); + } + @Test void givenFlattenDepth_whenUsingWithFlattenDepth_thenFlattenDepthIsSetAndFlattenIsTrue() { // Given diff --git a/src/test/java/dev/toonformat/jtoon/JToonDecodeTest.java b/src/test/java/dev/toonformat/jtoon/JToonDecodeTest.java index 598bd4b..391753c 100644 --- a/src/test/java/dev/toonformat/jtoon/JToonDecodeTest.java +++ b/src/test/java/dev/toonformat/jtoon/JToonDecodeTest.java @@ -21,7 +21,10 @@ class Primitives { @Test @DisplayName("should decode null") void testNull() { + // Given Object result = JToon.decode("value: null"); + + // Then assertInstanceOf(Map.class, result); @SuppressWarnings("unchecked") Map map = (Map) result; @@ -31,12 +34,22 @@ void testNull() { @Test @DisplayName("should decode booleans") void testBooleans() { + // Given Object result1 = JToon.decode("active: true"); + + // Then @SuppressWarnings("unchecked") Map map1 = (Map) result1; assertEquals(true, map1.get("active")); + } + @Test + @DisplayName("should decode booleans") + void testBooleans2() { + // Given Object result2 = JToon.decode("active: false"); + + // Then @SuppressWarnings("unchecked") Map map2 = (Map) result2; assertEquals(false, map2.get("active")); @@ -45,7 +58,10 @@ void testBooleans() { @Test @DisplayName("should decode integers") void testIntegers() { + // Given Object result = JToon.decode("count: 42"); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; assertEquals(42L, map.get("count")); @@ -54,7 +70,10 @@ void testIntegers() { @Test @DisplayName("should decode floating point numbers") void testFloatingPoint() { + // Given Object result = JToon.decode("price: 3.14"); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; assertEquals(3.14, (Double) map.get("price"), 0.0001); @@ -63,7 +82,10 @@ void testFloatingPoint() { @Test @DisplayName("should decode unquoted strings") void testUnquotedStrings() { + // Given Object result = JToon.decode("name: Ada"); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; assertEquals("Ada", map.get("name")); @@ -72,7 +94,10 @@ void testUnquotedStrings() { @Test @DisplayName("should decode quoted strings") void testQuotedStrings() { + // Given Object result = JToon.decode("note: \"hello, world\""); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; assertEquals("hello, world", map.get("note")); @@ -81,7 +106,10 @@ void testQuotedStrings() { @Test @DisplayName("should decode strings with escape sequences") void testEscapedStrings() { + // Given Object result = JToon.decode("text: \"line1\\nline2\""); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; assertEquals("line1\nline2", map.get("text")); @@ -95,12 +123,17 @@ class SimpleObjects { @Test @DisplayName("should decode simple object") void testSimpleObject() { + // Given String toon = """ - id: 123 - name: Ada - active: true - """; + id: 123 + name: Ada + active: true + """; + + // When Object result = JToon.decode(toon); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; assertEquals(123L, map.get("id")); @@ -111,8 +144,13 @@ void testSimpleObject() { @Test @DisplayName("should decode object with quoted keys") void testQuotedKeys() { + // Given String toon = "\"full name\": Alice"; + + // When Object result = JToon.decode(toon); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; assertEquals("Alice", map.get("full name")); @@ -121,8 +159,13 @@ void testQuotedKeys() { @Test @DisplayName("should decode object with special character keys") void testSpecialCharacterKeys() { + // Given String toon = "\"order:id\": 42"; + + // When Object result = JToon.decode(toon); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; assertEquals(42L, map.get("order:id")); @@ -136,12 +179,17 @@ class NestedObjects { @Test @DisplayName("should decode nested object") void testNestedObject() { + // Given String toon = """ - user: - id: 123 - name: Ada - """; + user: + id: 123 + name: Ada + """; + + // When Object result = JToon.decode(toon); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; @SuppressWarnings("unchecked") @@ -153,14 +201,19 @@ void testNestedObject() { @Test @DisplayName("should decode deeply nested object") void testDeeplyNestedObject() { + // Given String toon = """ - user: - id: 123 - contact: - email: ada@example.com - phone: "555-1234" - """; + user: + id: 123 + contact: + email: ada@example.com + phone: "555-1234" + """; + + // When Object result = JToon.decode(toon); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; @SuppressWarnings("unchecked") @@ -179,8 +232,13 @@ class PrimitiveArrays { @Test @DisplayName("should decode inline primitive array") void testInlinePrimitiveArray() { + // Given String toon = "tags[3]: reading,gaming,coding"; + + // When Object result = JToon.decode(toon); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; @SuppressWarnings("unchecked") @@ -194,11 +252,16 @@ void testInlinePrimitiveArray() { @Test @DisplayName("should decode multiline primitive array") void testMultilinePrimitiveArray() { + // Given String toon = """ - tags[3]: - reading,gaming,coding - """; + tags[3]: + reading,gaming,coding + """; + + // When Object result = JToon.decode(toon); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; @SuppressWarnings("unchecked") @@ -212,8 +275,13 @@ void testMultilinePrimitiveArray() { @Test @DisplayName("should decode array with mixed primitives") void testMixedPrimitiveArray() { + // Given String toon = "values[4]: 42,3.14,\"true\",null"; + + // When Object result = JToon.decode(toon); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; @SuppressWarnings("unchecked") @@ -227,8 +295,13 @@ void testMixedPrimitiveArray() { @Test @DisplayName("should decode empty array") void testEmptyArray() { + // Given String toon = "items[0]:"; + + // When Object result = JToon.decode(toon); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; @SuppressWarnings("unchecked") @@ -244,12 +317,17 @@ class TabularArrays { @Test @DisplayName("should decode tabular array") void testTabularArray() { + // Given String toon = """ - users[2]{id,name,role}: - 1,Alice,admin - 2,Bob,user - """; + users[2]{id,name,role}: + 1,Alice,admin + 2,Bob,user + """; + + // When Object result = JToon.decode(toon); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; @SuppressWarnings("unchecked") @@ -272,12 +350,17 @@ void testTabularArray() { @Test @DisplayName("should decode tabular array with mixed types") void testTabularArrayMixedTypes() { + // Given String toon = """ - items[2]{sku,qty,price}: - A1,2,9.99 - B2,1,14.5 - """; + items[2]{sku,qty,price}: + A1,2,9.99 + B2,1,14.5 + """; + + // When Object result = JToon.decode(toon); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; @SuppressWarnings("unchecked") @@ -293,12 +376,17 @@ void testTabularArrayMixedTypes() { @Test @DisplayName("should decode tabular array with quoted values") void testTabularArrayQuotedValues() { + // Given String toon = """ - items[2]{id,name}: - 1,"First Item" - 2,"Second, Item" - """; + items[2]{id,name}: + 1,"First Item" + 2,"Second, Item" + """; + + // When Object result = JToon.decode(toon); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; @SuppressWarnings("unchecked") @@ -317,12 +405,17 @@ class ListArrays { @Test @DisplayName("should decode list array with simple items") void testSimpleListArray() { + // Given String toon = """ - items[2]: - - first - - second - """; + items[2]: + - first + - second + """; + + // When Object result = JToon.decode(toon); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; @SuppressWarnings("unchecked") @@ -334,14 +427,19 @@ void testSimpleListArray() { @Test @DisplayName("should decode list array with object items") void testListArrayWithObjects() { + // Given String toon = """ - items[2]: - - id: 1 - name: First - - id: 2 - name: Second - """; + items[2]: + - id: 1 + name: First + - id: 2 + name: Second + """; + + // When Object result = JToon.decode(toon); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; @SuppressWarnings("unchecked") @@ -366,8 +464,13 @@ class DelimiterSupport { @Test @DisplayName("should decode comma-delimited array") void testCommaDelimiter() { + // Given String toon = "tags[3]: a,b,c"; + + // When Object result = JToon.decode(toon); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; @SuppressWarnings("unchecked") @@ -378,9 +481,15 @@ void testCommaDelimiter() { @Test @DisplayName("should decode tab-delimited array") void testTabDelimiter() { + // Given String toon = "tags[3\t]:\ta\tb\tc"; DecodeOptions options = DecodeOptions.withDelimiter(Delimiter.TAB); + + + // When Object result = JToon.decode(toon, options); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; @SuppressWarnings("unchecked") @@ -394,9 +503,14 @@ void testTabDelimiter() { @Test @DisplayName("should decode pipe-delimited array") void testPipeDelimiter() { + // Given String toon = "tags[3|]: a|b|c"; DecodeOptions options = DecodeOptions.withDelimiter(Delimiter.PIPE); + + // When Object result = JToon.decode(toon, options); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; @SuppressWarnings("unchecked") @@ -412,13 +526,18 @@ class ComplexStructures { @Test @DisplayName("should decode object with nested arrays") void testObjectWithNestedArrays() { + // Given String toon = """ - user: - id: 123 - name: Ada - tags[2]: dev,admin - """; + user: + id: 123 + name: Ada + tags[2]: dev,admin + """; + + // When Object result = JToon.decode(toon); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; @SuppressWarnings("unchecked") @@ -431,12 +550,17 @@ void testObjectWithNestedArrays() { @Test @DisplayName("should decode array of nested objects") void testArrayOfNestedObjects() { + // Given String toon = """ - users[2]{id,name}: - 1,Alice - 2,Bob - """; + users[2]{id,name}: + 1,Alice + 2,Bob + """; + + // When Object result = JToon.decode(toon); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; @SuppressWarnings("unchecked") @@ -447,13 +571,18 @@ void testArrayOfNestedObjects() { @Test @DisplayName("should decode mixed content at root level") void testMixedRootContent() { + // Given String toon = """ - id: 123 - name: Ada - tags[2]: dev,admin - active: true - """; + id: 123 + name: Ada + tags[2]: dev,admin + active: true + """; + + // When Object result = JToon.decode(toon); + + // Then @SuppressWarnings("unchecked") Map map = (Map) result; assertEquals(123L, map.get("id")); @@ -472,6 +601,7 @@ class ErrorHandling { @Test @DisplayName("should handle empty input") void testEmptyInput() { + // Then assertEquals(Collections.emptyMap(), JToon.decode("")); assertEquals(Collections.emptyMap(), JToon.decode(" ")); assertEquals(Collections.emptyMap(), JToon.decode(null)); @@ -480,17 +610,27 @@ void testEmptyInput() { @Test @DisplayName("should throw in strict mode for invalid array header") void testStrictModeError() { + // Given String toon = "[invalid]"; // Invalid array header format + + // When DecodeOptions options = DecodeOptions.withStrict(true); + + // Then assertThrows(IllegalArgumentException.class, () -> JToon.decode(toon, options)); } @Test @DisplayName("should return null in lenient mode for invalid array header") void testLenientMode() { + // Given String toon = "[invalid]"; // Invalid array header format DecodeOptions options = DecodeOptions.withStrict(false); - var result = JToon.decode(toon, options); + + // When + Object result = JToon.decode(toon, options); + + // Then assertEquals(Collections.emptyList(), result); } } @@ -502,11 +642,16 @@ class DecodeToJson { @Test @DisplayName("should decode to JSON string") void testDecodeToJson() { + // Given String toon = """ - id: 123 - name: Ada - """; + id: 123 + name: Ada + """; + + // When String json = JToon.decodeToJson(toon); + + // Then assertNotNull(json); assertTrue(json.contains("123")); assertTrue(json.contains("Ada")); @@ -515,12 +660,17 @@ void testDecodeToJson() { @Test @DisplayName("should decode complex structure to JSON") void testComplexDecodeToJson() { + // Given String toon = """ - users[2]{id,name}: - 1,Alice - 2,Bob - """; + users[2]{id,name}: + 1,Alice + 2,Bob + """; + + // When String json = JToon.decodeToJson(toon); + + // Then assertNotNull(json); assertTrue(json.contains("users")); assertTrue(json.contains("Alice")); @@ -530,51 +680,56 @@ void testComplexDecodeToJson() { @Test @DisplayName("should decode very complex structure to JSON, with empty Lists") void testVeryComplexDecodeToJson() { + // Given String toon = """ - [2]: - - name: Person.java - absolutePath: /Users/samples/petclinic/model/Person.java - types[1]: - - name: Person - lineNumber: 29 - fields[2]{name,lineNumber}: - firstName,33 - lastName,37 - members[2]: - - name: getFirstName - readFields[1]{name,lineNumber}: - firstName,40 - calledMethods[0]: - writtenFields[0]: - lineNumber: 39 - signature: getFirstName() - - name: setFirstName - readFields[0]: - calledMethods[0]: - writtenFields[1]{name,lineNumber}: - firstName,44 - lineNumber: 43 - signature: setFirstName(java.lang.String) - - name: NamedEntity.java - absolutePath: /Users/samples/petclinic/model/NamedEntity.java - types[1]: - - name: NamedEntity - lineNumber: 32 - fields[1]{name,lineNumber}: - name,36 - members[1]: - - name: toString - readFields[3]{name,lineNumber}: - address,154 - telephone,156 - city,155 - calledMethods[1]{name,lineNumber,signature}: - getFirstName,153,getFirstName - writtenFields[0]: - lineNumber: 47 - signature: toString() - """; + [2]: + - name: Person.java + absolutePath: /Users/samples/petclinic/model/Person.java + types[1]: + - name: Person + lineNumber: 29 + fields[2]{name,lineNumber}: + firstName,33 + lastName,37 + members[2]: + - name: getFirstName + readFields[1]{name,lineNumber}: + firstName,40 + calledMethods[0]: + writtenFields[0]: + lineNumber: 39 + signature: getFirstName() + - name: setFirstName + readFields[0]: + calledMethods[0]: + writtenFields[1]{name,lineNumber}: + firstName,44 + lineNumber: 43 + signature: setFirstName(java.lang.String) + - name: NamedEntity.java + absolutePath: /Users/samples/petclinic/model/NamedEntity.java + types[1]: + - name: NamedEntity + lineNumber: 32 + fields[1]{name,lineNumber}: + name,36 + members[1]: + - name: toString + readFields[3]{name,lineNumber}: + address,154 + telephone,156 + city,155 + calledMethods[1]{name,lineNumber,signature}: + getFirstName,153,getFirstName + writtenFields[0]: + lineNumber: 47 + signature: toString() + """; + + // When String json = JToon.decodeToJson(toon); + + // Then assertNotNull(json); assertTrue(json.contains("petclinic")); } diff --git a/src/test/java/dev/toonformat/jtoon/JToonJsonStringTest.java b/src/test/java/dev/toonformat/jtoon/JToonJsonStringTest.java index b247a8e..73e3eba 100644 --- a/src/test/java/dev/toonformat/jtoon/JToonJsonStringTest.java +++ b/src/test/java/dev/toonformat/jtoon/JToonJsonStringTest.java @@ -18,24 +18,39 @@ class HappyPaths { @Test @DisplayName("encodes simple object") void encodesSimpleObject() { + // Given String json = "{\"id\":123,\"name\":\"Ada\"}"; + + // When String result = JToon.encodeJson(json); + + // Then assertEquals("id: 123\nname: Ada", result); } @Test @DisplayName("encodes primitive array inline") void encodesPrimitiveArray() { + // Given String json = "{\"tags\":[\"admin\",\"ops\",\"dev\"]}"; + + // When String result = JToon.encodeJson(json); + + // Then assertEquals("tags[3]: admin,ops,dev", result); } @Test @DisplayName("encodes uniform objects as tabular array") void encodesTabularArray() { + // Given String json = "{\"items\":[{\"sku\":\"A1\",\"qty\":2,\"price\":9.99},{\"sku\":\"B2\",\"qty\":1,\"price\":14.5}]}"; + + // When String result = JToon.encodeJson(json); + + // Then String expected = String.join("\n", "items[2]{sku,qty,price}:", " A1,2,9.99", @@ -46,8 +61,13 @@ void encodesTabularArray() { @Test @DisplayName("encodes root-level array mixing primitive, object, and array of objects in list format") void encodesMixedArray() { + // Given String json = "[\"summary\", { \"id\": 1, \"name\": \"Ada\" }, [{ \"id\": 2 }, { \"status\": \"draft\" }]]"; + + // When String result = JToon.encodeJson(json); + + // Then String expected = String.join("\n", "[3]:", " - summary", @@ -62,22 +82,26 @@ void encodesMixedArray() { @Test @DisplayName("supports custom options with pipe delimiter and length marker") void encodesWithCustomOptions() { + // Given String json = "{\"tags\":[\"reading\",\"gaming\",\"coding\"],\"items\":[{\"sku\":\"A1\",\"qty\":2,\"price\":9.99},{\"sku\":\"B2\",\"qty\":1,\"price\":14.5}]}"; EncodeOptions options = new EncodeOptions(2, Delimiter.PIPE, true, KeyFolding.OFF, Integer.MAX_VALUE); + + // When String result = JToon.encodeJson(json, options); + // Then String expected = String.join("\n", "tags[#3|]: reading|gaming|coding", "items[#2|]{sku|qty|price}:", " A1|2|9.99", " B2|1|14.5"); - assertEquals(expected, result); } @Test @DisplayName("supports custom options in flatten") void encodesWithCustomFlattingOptions() { + // Given String json = "{\n" + " \"a\": {\n" + " \"b\": {\n" + @@ -88,13 +112,15 @@ void encodesWithCustomFlattingOptions() { " }\n" + " }"; EncodeOptions options = EncodeOptions.withFlattenDepth(2); + + // When String result = JToon.encodeJson(json, options); + // Then String expected = String.join("\n", "a.b:", " c:", " d: 1"); - assertEquals(expected, result); } } @@ -106,12 +132,14 @@ class Errors { @Test @DisplayName("throws on invalid JSON") void throwsOnInvalidJson() { + // Then assertThrows(IllegalArgumentException.class, () -> JToon.encodeJson("{invalid}")); } @Test @DisplayName("throws on blank JSON") void throwsOnBlankJson() { + // Then assertThrows(IllegalArgumentException.class, () -> JToon.encodeJson(" \n \t ")); } } diff --git a/src/test/java/dev/toonformat/jtoon/JToonTest.java b/src/test/java/dev/toonformat/jtoon/JToonTest.java index 36c8377..f6ad301 100644 --- a/src/test/java/dev/toonformat/jtoon/JToonTest.java +++ b/src/test/java/dev/toonformat/jtoon/JToonTest.java @@ -57,6 +57,7 @@ class Primitives { @Test @DisplayName("encodes safe strings without quotes") void encodesSafeStrings() { + // Then assertEquals("hello", encode("hello")); assertEquals("Ada_99", encode("Ada_99")); } @@ -64,12 +65,14 @@ void encodesSafeStrings() { @Test @DisplayName("quotes empty string") void quotesEmptyString() { + // Then assertEquals("\"\"", encode("")); } @Test @DisplayName("quotes strings that look like booleans or numbers") void quotesAmbiguousStrings() { + // Then assertEquals("\"true\"", encode("true")); assertEquals("\"false\"", encode("false")); assertEquals("\"null\"", encode("null")); @@ -82,6 +85,7 @@ void quotesAmbiguousStrings() { @Test @DisplayName("escapes control characters in strings") void escapesControlChars() { + // Then assertEquals("\"line1\\nline2\"", encode("line1\nline2")); assertEquals("\"tab\\there\"", encode("tab\there")); assertEquals("\"return\\rcarriage\"", encode("return\rcarriage")); @@ -91,6 +95,7 @@ void escapesControlChars() { @Test @DisplayName("quotes strings with structural characters") void quotesStructuralChars() { + // Then assertEquals("\"[3]: x,y\"", encode("[3]: x,y")); assertEquals("\"- item\"", encode("- item")); assertEquals("\"[test]\"", encode("[test]")); @@ -100,6 +105,7 @@ void quotesStructuralChars() { @Test @DisplayName("handles Unicode and emoji") void handlesUnicodeAndEmoji() { + // Then assertEquals("cafΓ©", encode("cafΓ©")); assertEquals("δ½ ε₯½", encode("δ½ ε₯½")); assertEquals("πŸš€", encode("πŸš€")); @@ -109,6 +115,7 @@ void handlesUnicodeAndEmoji() { @Test @DisplayName("encodes numbers") void encodesNumbers() { + // Then assertEquals("42", encode(42)); assertEquals("3.14", encode(3.14)); assertEquals("-7", encode(-7)); @@ -118,6 +125,7 @@ void encodesNumbers() { @Test @DisplayName("handles special numeric values") void handlesSpecialNumericValues() { + // Then assertEquals("0", encode(-0.0)); assertEquals("1000000", encode(1e6)); assertEquals("0.000001", encode(1e-6)); @@ -129,6 +137,7 @@ void handlesSpecialNumericValues() { @Test @DisplayName("encodes booleans") void encodesBooleans() { + // Then assertEquals("true", encode(true)); assertEquals("false", encode(false)); } @@ -136,6 +145,7 @@ void encodesBooleans() { @Test @DisplayName("encodes null") void encodesNull() { + // Then assertEquals("null", encode(null)); } } @@ -147,29 +157,37 @@ class SimpleObjects { @Test @DisplayName("preserves key order in objects") void preservesKeyOrder() { + // Given Map obj = obj( "id", 123, "name", "Ada", "active", true); + + // Then assertEquals("id: 123\nname: Ada\nactive: true", encode(obj)); } @Test @DisplayName("encodes null values in objects") void encodesNullValues() { + // Given Map obj = obj("id", 123, "value", null); + + // Then assertEquals("id: 123\nvalue: null", encode(obj)); } @Test @DisplayName("encodes empty objects as empty string") void encodesEmptyObjects() { + // Then assertEquals("", encode(Map.of())); } @Test @DisplayName("quotes string values with special characters") void quotesSpecialChars() { + // Then assertEquals("note: \"a:b\"", encode(obj("note", "a:b"))); assertEquals("note: \"a,b\"", encode(obj("note", "a,b"))); assertEquals("text: \"line1\\nline2\"", encode(obj("text", "line1\nline2"))); @@ -179,6 +197,7 @@ void quotesSpecialChars() { @Test @DisplayName("quotes string values with leading/trailing spaces") void quotesWhitespace() { + // Then assertEquals("text: \" padded \"", encode(obj("text", " padded "))); assertEquals("text: \" \"", encode(obj("text", " "))); } @@ -186,6 +205,7 @@ void quotesWhitespace() { @Test @DisplayName("quotes string values that look like booleans/numbers") void quotesAmbiguous() { + // Then assertEquals("v: \"true\"", encode(obj("v", "true"))); assertEquals("v: \"42\"", encode(obj("v", "42"))); assertEquals("v: \"-7.5\"", encode(obj("v", "-7.5"))); @@ -199,6 +219,7 @@ class ObjectKeys { @Test @DisplayName("quotes keys with special characters") void quotesKeysWithSpecialChars() { + // Then assertEquals("\"order:id\": 7", encode(obj("order:id", 7))); assertEquals("\"[index]\": 5", encode(obj("[index]", 5))); assertEquals("\"{key}\": 5", encode(obj("{key}", 5))); @@ -208,6 +229,7 @@ void quotesKeysWithSpecialChars() { @Test @DisplayName("quotes keys with spaces or leading hyphens") void quotesKeysWithSpaces() { + // Then assertEquals("\"full name\": Ada", encode(obj("full name", "Ada"))); assertEquals("\"-lead\": 1", encode(obj("-lead", 1))); assertEquals("\" a \": 1", encode(obj(" a ", 1))); @@ -216,20 +238,25 @@ void quotesKeysWithSpaces() { @Test @DisplayName("quotes numeric keys") void quotesNumericKeys() { + // Given Map map = new LinkedHashMap<>(); map.put("123", "x"); + + // Then assertEquals("\"123\": x", encode(map)); } @Test @DisplayName("quotes empty string key") void quotesEmptyKey() { + // Then assertEquals("\"\": 1", encode(obj("", 1))); } @Test @DisplayName("escapes control characters in keys") void escapesControlCharsInKeys() { + // Then assertEquals("\"line\\nbreak\": 1", encode(obj("line\nbreak", 1))); assertEquals("\"tab\\there\": 2", encode(obj("tab\there", 2))); } @@ -237,6 +264,7 @@ void escapesControlCharsInKeys() { @Test @DisplayName("escapes quotes in keys") void escapesQuotesInKeys() { + // Then assertEquals("\"he said \\\"hi\\\"\": 1", encode(obj("he said \"hi\"", 1))); } } @@ -248,16 +276,20 @@ class NestedObjects { @Test @DisplayName("encodes deeply nested objects") void encodesDeeplyNested() { + // Given Map obj = obj( "a", obj( "b", obj( "c", "deep"))); + + // Then assertEquals("a:\n b:\n c: deep", encode(obj)); } @Test @DisplayName("encodes empty nested object") void encodesEmptyNested() { + // Then assertEquals("user:", encode(obj("user", Map.of()))); } } @@ -269,65 +301,100 @@ class PrimitiveArrays { @Test @DisplayName("encodes string arrays inline") void encodesStringArrays() { + // Given Map obj = obj("tags", list("reading", "gaming")); + + // Then assertEquals("tags[2]: reading,gaming", encode(obj)); } @Test @DisplayName("encodes number arrays inline") void encodesNumberArrays() { + // Given Map obj = obj("nums", list(1, 2, 3)); + + // Then assertEquals("nums[3]: 1,2,3", encode(obj)); } @Test @DisplayName("encodes mixed primitive arrays inline") void encodesMixedPrimitiveArrays() { + // Given Map obj = obj("data", list("x", "y", true, 10)); + + // Then assertEquals("data[4]: x,y,true,10", encode(obj)); } @Test @DisplayName("encodes empty arrays") void encodesEmptyArrays() { + // Given Map obj = obj("items", List.of()); + + // Then assertEquals("items[0]:", encode(obj)); } @Test @DisplayName("handles empty string in arrays") void handlesEmptyStringInArrays() { + // Given Map obj = obj("items", list("")); + + // Then assertEquals("items[1]: \"\"", encode(obj)); + } + + @Test + @DisplayName("handles empty string in arrays") + void handlesEmptyStringInArrays2() { + // Given Map obj2 = obj("items", list("a", "", "b")); + + // Then assertEquals("items[3]: a,\"\",b", encode(obj2)); } @Test @DisplayName("handles whitespace-only strings in arrays") void handlesWhitespaceOnlyStrings() { + // Given Map obj = obj("items", list(" ", " ")); + + // Then assertEquals("items[2]: \" \",\" \"", encode(obj)); } @Test @DisplayName("quotes array strings with special characters") void quotesArrayStringsWithSpecialChars() { + // Given Map obj = obj("items", list("a", "b,c", "d:e")); + + // Then assertEquals("items[3]: a,\"b,c\",\"d:e\"", encode(obj)); } @Test @DisplayName("quotes strings that look like booleans/numbers in arrays") void quotesAmbiguousInArrays() { + // Given Map obj = obj("items", list("x", "true", "42", "-3.14")); + + // Then assertEquals("items[4]: x,\"true\",\"42\",\"-3.14\"", encode(obj)); } @Test @DisplayName("quotes strings with structural meanings in arrays") void quotesStructuralInArrays() { + // Given Map obj = obj("items", list("[5]", "- item", "{key}")); + + // Then assertEquals("items[3]: \"[5]\",\"- item\",\"{key}\"", encode(obj)); } } @@ -339,60 +406,78 @@ class ObjectArrays { @Test @DisplayName("encodes arrays of similar objects in tabular format") void encodesTabularFormat() { + // Given Map obj = obj( "items", list( obj("sku", "A1", "qty", 2, "price", 9.99), obj("sku", "B2", "qty", 1, "price", 14.5))); + + // Then assertEquals("items[2]{sku,qty,price}:\n A1,2,9.99\n B2,1,14.5", encode(obj)); } @Test @DisplayName("handles null values in tabular format") void handlesNullInTabular() { + // Given Map obj = obj( "items", list( obj("id", 1, "value", null), obj("id", 2, "value", "test"))); + + // Then assertEquals("items[2]{id,value}:\n 1,null\n 2,test", encode(obj)); } @Test @DisplayName("quotes strings containing delimiters in tabular rows") void quotesDelimitersInTabular() { + // Given Map obj = obj( "items", list( obj("sku", "A,1", "desc", "cool", "qty", 2), obj("sku", "B2", "desc", "wip: test", "qty", 1))); + + // Then assertEquals("items[2]{sku,desc,qty}:\n \"A,1\",cool,2\n B2,\"wip: test\",1", encode(obj)); } @Test @DisplayName("quotes ambiguous strings in tabular rows") void quotesAmbiguousInTabular() { + // Given Map obj = obj( "items", list( obj("id", 1, "status", "true"), obj("id", 2, "status", "false"))); + + // Then assertEquals("items[2]{id,status}:\n 1,\"true\"\n 2,\"false\"", encode(obj)); } @Test @DisplayName("handles tabular arrays with keys needing quotes") void handlesQuotedKeysInTabular() { + // Given Map obj = obj( "items", list( obj("order:id", 1, "full name", "Ada"), obj("order:id", 2, "full name", "Bob"))); + + // Then assertEquals("items[2]{\"order:id\",\"full name\"}:\n 1,Ada\n 2,Bob", encode(obj)); } @Test @DisplayName("uses list format for objects with different fields") void usesListForDifferentFields() { + // Given Map obj = obj( "items", list( obj("id", 1, "name", "First"), obj("id", 2, "name", "Second", "extra", true))); + + // Then assertEquals( """ items[2]: @@ -407,9 +492,12 @@ void usesListForDifferentFields() { @Test @DisplayName("uses list format for objects with nested values") void usesListForNestedValues() { + // Given Map obj = obj( "items", list( obj("id", 1, "nested", obj("x", 1)))); + + // Then assertEquals( """ items[1]: @@ -422,7 +510,10 @@ void usesListForNestedValues() { @Test @DisplayName("preserves field order in list items") void preservesFieldOrderInListItems() { + // Given Map obj = obj("items", list(obj("nums", list(1, 2, 3), "name", "test"))); + + // Then assertEquals( """ items[1]: @@ -434,7 +525,10 @@ void preservesFieldOrderInListItems() { @Test @DisplayName("preserves field order when primitive appears first") void preservesFieldOrderPrimitiveFirst() { + // Given Map obj = obj("items", list(obj("name", "test", "nums", list(1, 2, 3)))); + + // Then assertEquals( """ items[1]: @@ -446,9 +540,12 @@ void preservesFieldOrderPrimitiveFirst() { @Test @DisplayName("uses list format for objects containing arrays of arrays") void usesListForArrayOfArrays() { + // Given Map obj = obj( "items", list( obj("matrix", list(list(1, 2), list(3, 4)), "name", "grid"))); + + // Then assertEquals( """ items[1]: @@ -462,10 +559,13 @@ void usesListForArrayOfArrays() { @Test @DisplayName("uses tabular format for nested uniform object arrays") void usesTabularForNestedUniformArrays() { + // Given Map obj = obj( "items", list( obj("users", list(obj("id", 1, "name", "Ada"), obj("id", 2, "name", "Bob")), "status", "active"))); + + // Then assertEquals( """ items[1]: @@ -479,9 +579,12 @@ void usesTabularForNestedUniformArrays() { @Test @DisplayName("uses list format for nested object arrays with mismatched keys") void usesListForMismatchedKeys() { + // Given Map obj = obj( "items", list( obj("users", list(obj("id", 1, "name", "Ada"), obj("id", 2)), "status", "active"))); + + // Then assertEquals( """ items[1]: @@ -496,8 +599,11 @@ void usesListForMismatchedKeys() { @Test @DisplayName("uses list format for objects with multiple array fields") void usesListForMultipleArrays() { + // Given Map obj = obj("items", list(obj("nums", list(1, 2), "tags", list("a", "b"), "name", "test"))); + + // Then assertEquals( """ items[1]: @@ -510,7 +616,10 @@ void usesListForMultipleArrays() { @Test @DisplayName("uses list format for objects with only array fields") void usesListForOnlyArrayFields() { + // Given Map obj = obj("items", list(obj("nums", list(1, 2, 3), "tags", list("a", "b")))); + + // Then assertEquals( """ items[1]: @@ -522,9 +631,12 @@ void usesListForOnlyArrayFields() { @Test @DisplayName("handles objects with empty arrays in list format") void handlesEmptyArraysInList() { + // Given Map obj = obj( "items", list( obj("name", "test", "data", List.of()))); + + // Then assertEquals( """ items[1]: @@ -536,7 +648,10 @@ void handlesEmptyArraysInList() { @Test @DisplayName("places first field of nested tabular arrays on hyphen line") void placesTabularOnHyphenLine() { + // Given Map obj = obj("items", list(obj("users", list(obj("id", 1), obj("id", 2)), "note", "x"))); + + // Then assertEquals( """ items[1]: @@ -550,7 +665,10 @@ void placesTabularOnHyphenLine() { @Test @DisplayName("places empty arrays on hyphen line when first") void placesEmptyArrayOnHyphenLine() { + // Given Map obj = obj("items", list(obj("data", List.of(), "name", "x"))); + + // Then assertEquals( """ items[1]: @@ -562,20 +680,26 @@ void placesEmptyArrayOnHyphenLine() { @Test @DisplayName("uses field order from first object for tabular headers") void usesFirstObjectFieldOrder() { + // Given Map obj = obj( "items", list( obj("a", 1, "b", 2, "c", 3), obj("c", 30, "b", 20, "a", 10))); + + // Then assertEquals("items[2]{a,b,c}:\n 1,2,3\n 10,20,30", encode(obj)); } @Test @DisplayName("uses list format for one object with nested column") void usesListForNestedColumn() { + // Given Map obj = obj( "items", list( obj("id", 1, "data", "string"), obj("id", 2, "data", obj("nested", true)))); + + // Then assertEquals( """ items[2]: @@ -595,32 +719,44 @@ class ArrayOfArrays { @Test @DisplayName("encodes nested arrays of primitives") void encodesNestedArrays() { + // Given Map obj = obj( "pairs", list(list("a", "b"), list("c", "d"))); + + // Then assertEquals("pairs[2]:\n - [2]: a,b\n - [2]: c,d", encode(obj)); } @Test @DisplayName("quotes strings containing delimiters in nested arrays") void quotesDelimitersInNested() { + // Given Map obj = obj( "pairs", list(list("a", "b"), list("c,d", "e:f", "true"))); + + // Then assertEquals("pairs[2]:\n - [2]: a,b\n - [3]: \"c,d\",\"e:f\",\"true\"", encode(obj)); } @Test @DisplayName("handles empty inner arrays") void handlesEmptyInnerArrays() { + // Given Map obj = obj( "pairs", list(List.of(), List.of())); + + // Then assertEquals("pairs[2]:\n - [0]:\n - [0]:", encode(obj)); } @Test @DisplayName("handles mixed-length inner arrays") void handlesMixedLengthArrays() { + // Given Map obj = obj( "pairs", list(list(1), list(2, 3))); + + // Then assertEquals("pairs[2]:\n - [1]: 1\n - [2]: 2,3", encode(obj)); } } @@ -632,34 +768,47 @@ class RootArrays { @Test @DisplayName("encodes arrays of primitives at root level") void encodesPrimitivesAtRoot() { + // Given List arr = list("x", "y", "true", true, 10); + + // Then assertEquals("[5]: x,y,\"true\",true,10", encode(arr)); } @Test @DisplayName("encodes arrays of similar objects in tabular format") void encodesTabularAtRoot() { + // Given List arr = list(obj("id", 1), obj("id", 2)); + + // Then assertEquals("[2]{id}:\n 1\n 2", encode(arr)); } @Test @DisplayName("encodes arrays of different objects in list format") void encodesListAtRoot() { + // Given List arr = list(obj("id", 1), obj("id", 2, "name", "Ada")); + + // Then assertEquals("[2]:\n - id: 1\n - id: 2\n name: Ada", encode(arr)); } @Test @DisplayName("encodes empty arrays at root level") void encodesEmptyAtRoot() { + // Then assertEquals("[0]:", encode(List.of())); } @Test @DisplayName("encodes arrays of arrays at root level") void encodesArrayOfArraysAtRoot() { + // Given List arr = list(list(1, 2), List.of()); + + // Then assertEquals("[2]:\n - [2]: 1,2\n - [0]:", encode(arr)); } } @@ -671,6 +820,7 @@ class ComplexStructures { @Test @DisplayName("encodes objects with mixed arrays and nested objects") void encodesMixedStructures() { + // Given Map obj = obj( "user", obj( "id", 123, @@ -678,6 +828,8 @@ void encodesMixedStructures() { "tags", list("reading", "gaming"), "active", true, "prefs", List.of())); + + // Then assertEquals( """ user: @@ -697,8 +849,11 @@ class MixedArrays { @Test @DisplayName("uses list format for arrays mixing primitives and objects") void mixesPrimitivesAndObjects() { + // Given Map obj = obj( "items", list(1, obj("a", 1), "text")); + + // Then assertEquals( """ items[3]: @@ -711,8 +866,11 @@ void mixesPrimitivesAndObjects() { @Test @DisplayName("uses list format for arrays mixing objects and arrays") void mixesObjectsAndArrays() { + // Given Map obj = obj( "items", list(obj("a", 1), list(1, 2))); + + // Then assertEquals( """ items[2]: @@ -729,12 +887,17 @@ class Formatting { @Test @DisplayName("produces no trailing spaces at end of lines") void noTrailingSpaces() { + // Given Map obj = obj( "user", obj( "id", 123, "name", "Ada"), "items", list("a", "b")); + + // When String result = encode(obj); + + // Then String[] lines = result.split("\n"); for (String line : lines) { assertFalse(line.matches(".* $"), "Line has trailing space: '" + line + "'"); @@ -744,8 +907,13 @@ void noTrailingSpaces() { @Test @DisplayName("produces no trailing newline at end of output") void noTrailingNewline() { + // Given Map obj = obj("id", 123); + + // When String result = encode(obj); + + // Then assertFalse(result.matches(".*\\n$"), "Output has trailing newline"); } } @@ -757,6 +925,7 @@ class NonJson { @Test @DisplayName("converts BigInt to string") void convertsBigInt() { + // Then assertEquals("123", encode(BigInteger.valueOf(123))); assertEquals("id: 456", encode(obj("id", BigInteger.valueOf(456)))); } @@ -764,7 +933,10 @@ void convertsBigInt() { @Test @DisplayName("converts Date to ISO string") void convertsDate() { + // Given Instant date = Instant.parse("2025-01-01T00:00:00.000Z"); + + // Then assertEquals("\"2025-01-01T00:00:00Z\"", encode(date)); assertEquals("created: \"2025-01-01T00:00:00Z\"", encode(obj("created", date))); } @@ -772,6 +944,7 @@ void convertsDate() { @Test @DisplayName("converts null to null") void convertsNull() { + // Then assertEquals("null", encode(null)); assertEquals("value: null", encode(obj("value", null))); } @@ -779,10 +952,12 @@ void convertsNull() { @Test @DisplayName("converts non-finite numbers to null") void convertsNonFiniteNumbers() { + // Given String positive = encode(Double.POSITIVE_INFINITY); String negative = encode(Double.NEGATIVE_INFINITY); String nan = encode(Double.NaN); + // Then assertNotNull(positive); assertNotNull(negative); assertNotNull(nan); @@ -804,7 +979,10 @@ class BasicDelimiterUsage { @Test @DisplayName("encodes primitive arrays with tab") void encodesWithTab() { + // Given Map obj = obj("tags", list("reading", "gaming", "coding")); + + // Then assertEquals("tags[3\t]: reading\tgaming\tcoding", encode(obj, new EncodeOptions(2, Delimiter.TAB, false, KeyFolding.OFF, Integer.MAX_VALUE))); } @@ -812,7 +990,10 @@ void encodesWithTab() { @Test @DisplayName("encodes primitive arrays with pipe") void encodesWithPipe() { + // Given Map obj = obj("tags", list("reading", "gaming", "coding")); + + // Then assertEquals("tags[3|]: reading|gaming|coding", encode(obj, new EncodeOptions(2, Delimiter.PIPE, false, KeyFolding.OFF, Integer.MAX_VALUE))); } @@ -820,7 +1001,10 @@ void encodesWithPipe() { @Test @DisplayName("encodes primitive arrays with comma") void encodesWithComma() { + // Given Map obj = obj("tags", list("reading", "gaming", "coding")); + + // Then assertEquals("tags[3]: reading,gaming,coding", encode(obj, new EncodeOptions(2, Delimiter.COMMA, false, KeyFolding.OFF, Integer.MAX_VALUE))); } @@ -828,10 +1012,13 @@ void encodesWithComma() { @Test @DisplayName("encodes tabular arrays with tab") void encodesTabularWithTab() { + // Given Map obj = obj( "items", list( obj("sku", "A1", "qty", 2, "price", 9.99), obj("sku", "B2", "qty", 1, "price", 14.5))); + + // Then assertEquals("items[2\t]{sku\tqty\tprice}:\n A1\t2\t9.99\n B2\t1\t14.5", encode(obj, new EncodeOptions(2, Delimiter.TAB, false, KeyFolding.OFF, Integer.MAX_VALUE))); } @@ -839,10 +1026,13 @@ void encodesTabularWithTab() { @Test @DisplayName("encodes tabular arrays with pipe") void encodesTabularWithPipe() { + // Given Map obj = obj( "items", list( obj("sku", "A1", "qty", 2, "price", 9.99), obj("sku", "B2", "qty", 1, "price", 14.5))); + + // Then assertEquals("items[2|]{sku|qty|price}:\n A1|2|9.99\n B2|1|14.5", encode(obj, new EncodeOptions(2, Delimiter.PIPE, false, KeyFolding.OFF, Integer.MAX_VALUE))); } @@ -850,7 +1040,10 @@ void encodesTabularWithPipe() { @Test @DisplayName("encodes nested arrays with tab") void encodesNestedWithTab() { + // Given Map obj = obj("pairs", list(list("a", "b"), list("c", "d"))); + + // Then assertEquals("pairs[2\t]:\n - [2\t]: a\tb\n - [2\t]: c\td", encode(obj, new EncodeOptions(2, Delimiter.TAB, false, KeyFolding.OFF, Integer.MAX_VALUE))); } @@ -858,7 +1051,10 @@ void encodesNestedWithTab() { @Test @DisplayName("encodes nested arrays with pipe") void encodesNestedWithPipe() { + // Given Map obj = obj("pairs", list(list("a", "b"), list("c", "d"))); + + // Then assertEquals("pairs[2|]:\n - [2|]: a|b\n - [2|]: c|d", encode(obj, new EncodeOptions(2, Delimiter.PIPE, false, KeyFolding.OFF, Integer.MAX_VALUE))); } @@ -866,28 +1062,40 @@ void encodesNestedWithPipe() { @Test @DisplayName("encodes root arrays with tab") void encodesRootWithTab() { + // Given List arr = list("x", "y", "z"); + + // Then assertEquals("[3\t]: x\ty\tz", encode(arr, new EncodeOptions(2, Delimiter.TAB, false, KeyFolding.OFF, Integer.MAX_VALUE))); } @Test @DisplayName("encodes root arrays with pipe") void encodesRootWithPipe() { + // Given List arr = list("x", "y", "z"); + + // Then assertEquals("[3|]: x|y|z", encode(arr, new EncodeOptions(2, Delimiter.PIPE, false, KeyFolding.OFF, Integer.MAX_VALUE))); } @Test @DisplayName("encodes root arrays of objects with tab") void encodesRootObjectsWithTab() { + // Given List arr = list(obj("id", 1), obj("id", 2)); + + // Then assertEquals("[2\t]{id}:\n 1\n 2", encode(arr, new EncodeOptions(2, Delimiter.TAB, false, KeyFolding.OFF, Integer.MAX_VALUE))); } @Test @DisplayName("encodes root arrays of objects with pipe") void encodesRootObjectsWithPipe() { + // Given List arr = list(obj("id", 1), obj("id", 2)); + + // Then assertEquals("[2|]{id}:\n 1\n 2", encode(arr, new EncodeOptions(2, Delimiter.PIPE, false, KeyFolding.OFF, Integer.MAX_VALUE))); } } @@ -899,7 +1107,10 @@ class DelimiterQuoting { @Test @DisplayName("quotes strings containing tab") void quotesTab() { + // Given List input = list("a", "b\tc", "d"); + + // Then assertEquals("items[3\t]: a\t\"b\\tc\"\td", encode(obj("items", input), new EncodeOptions(2, Delimiter.TAB, false, KeyFolding.OFF, Integer.MAX_VALUE))); } @@ -907,7 +1118,10 @@ void quotesTab() { @Test @DisplayName("quotes strings containing pipe") void quotesPipe() { + // Given List input = list("a", "b|c", "d"); + + // Then assertEquals("items[3|]: a|\"b|c\"|d", encode(obj("items", input), new EncodeOptions(2, Delimiter.PIPE, false, KeyFolding.OFF, Integer.MAX_VALUE))); } @@ -915,7 +1129,10 @@ void quotesPipe() { @Test @DisplayName("does not quote commas with tab") void doesNotQuoteCommasWithTab() { + // Given List input = list("a,b", "c,d"); + + // Then assertEquals("items[2\t]: a,b\tc,d", encode(obj("items", input), new EncodeOptions(2, Delimiter.TAB, false, KeyFolding.OFF, Integer.MAX_VALUE))); } @@ -923,7 +1140,10 @@ void doesNotQuoteCommasWithTab() { @Test @DisplayName("does not quote commas with pipe") void doesNotQuoteCommasWithPipe() { + // Given List input = list("a,b", "c,d"); + + // Then assertEquals("items[2|]: a,b|c,d", encode(obj("items", input), new EncodeOptions(2, Delimiter.PIPE, false, KeyFolding.OFF, Integer.MAX_VALUE))); } @@ -931,10 +1151,13 @@ void doesNotQuoteCommasWithPipe() { @Test @DisplayName("quotes tabular values containing the delimiter") void quotesTabularDelimiter() { + // Given Map obj = obj( "items", list( obj("id", 1, "note", "a,b"), obj("id", 2, "note", "c,d"))); + + // Then assertEquals("items[2]{id,note}:\n 1,\"a,b\"\n 2,\"c,d\"", encode(obj, new EncodeOptions(2, Delimiter.COMMA, false, KeyFolding.OFF, Integer.MAX_VALUE))); assertEquals("items[2\t]{id\tnote}:\n 1\ta,b\n 2\tc,d", @@ -944,6 +1167,7 @@ void quotesTabularDelimiter() { @Test @DisplayName("does not quote commas in object values with non-comma delimiter") void doesNotQuoteCommasInValues() { + // Then assertEquals("note: a,b", encode(obj("note", "a,b"), new EncodeOptions(2, Delimiter.PIPE, false, KeyFolding.OFF, Integer.MAX_VALUE))); assertEquals("note: a,b", encode(obj("note", "a,b"), new EncodeOptions(2, Delimiter.TAB, false, KeyFolding.OFF, Integer.MAX_VALUE))); } @@ -951,6 +1175,7 @@ void doesNotQuoteCommasInValues() { @Test @DisplayName("quotes nested array values containing the delimiter") void quotesNestedDelimiter() { + // Then assertEquals("pairs[1|]:\n - [2|]: a|\"b|c\"", encode(obj("pairs", list(list("a", "b|c"))), new EncodeOptions(2, Delimiter.PIPE, false, KeyFolding.OFF, Integer.MAX_VALUE))); assertEquals("pairs[1\t]:\n - [2\t]: a\t\"b\\tc\"", @@ -966,23 +1191,30 @@ class LengthMarker { @Test @DisplayName("adds length marker to primitive arrays") void addsMarkerToPrimitives() { + // Given Map obj = obj("tags", list("reading", "gaming", "coding")); + + // Then assertEquals("tags[#3]: reading,gaming,coding", encode(obj, new EncodeOptions(2, Delimiter.COMMA, true, KeyFolding.OFF, Integer.MAX_VALUE))); } @Test @DisplayName("handles empty arrays") void handlesEmptyArrays() { + // Then assertEquals("items[#0]:", encode(obj("items", List.of()), new EncodeOptions(2, Delimiter.COMMA, true, KeyFolding.OFF, Integer.MAX_VALUE))); } @Test @DisplayName("adds length marker to tabular arrays") void addsMarkerToTabular() { + // Given Map obj = obj( "items", list( obj("sku", "A1", "qty", 2, "price", 9.99), obj("sku", "B2", "qty", 1, "price", 14.5))); + + // Then assertEquals("items[#2]{sku,qty,price}:\n A1,2,9.99\n B2,1,14.5", encode(obj, new EncodeOptions(2, Delimiter.COMMA, true, KeyFolding.OFF, Integer.MAX_VALUE))); } @@ -990,7 +1222,10 @@ void addsMarkerToTabular() { @Test @DisplayName("adds length marker to nested arrays") void addsMarkerToNested() { + // Given Map obj = obj("pairs", list(list("a", "b"), list("c", "d"))); + + // Then assertEquals("pairs[#2]:\n - [#2]: a,b\n - [#2]: c,d", encode(obj, new EncodeOptions(2, Delimiter.COMMA, true, KeyFolding.OFF, Integer.MAX_VALUE))); } @@ -998,14 +1233,20 @@ void addsMarkerToNested() { @Test @DisplayName("works with delimiter option") void worksWithDelimiter() { + // Given Map obj = obj("tags", list("reading", "gaming", "coding")); + + // Then assertEquals("tags[#3|]: reading|gaming|coding", encode(obj, new EncodeOptions(2, Delimiter.PIPE, true, KeyFolding.OFF, Integer.MAX_VALUE))); } @Test @DisplayName("default is false (no length marker)") void defaultIsFalse() { + // Given Map obj = obj("tags", list("reading", "gaming", "coding")); + + // Then assertEquals("tags[3]: reading,gaming,coding", encode(obj)); } } @@ -1021,36 +1262,51 @@ class SimplePOJOs { @Test @DisplayName("encodes simple POJO with basic fields") void encodesSimplePOJO() { + // Given Person person = new Person("Ada", 30, true); + + // Then assertEquals("name: Ada\nage: 30\nactive: true", encode(person)); } @Test @DisplayName("encodes POJO with multiple field types") void encodesMultipleFieldTypes() { + // Given Product product = new Product(101, "Laptop", 999.99, true); + + // Then assertEquals("id: 101\nname: Laptop\nprice: 999.99\ninStock: true", encode(product)); } @Test @DisplayName("encodes POJO with null values") void encodesNullValues() { + // Given NullableData data = new NullableData("hello", null, null); + + // Then assertEquals("text: hello\ncount: null\nflag: null", encode(data)); } @Test @DisplayName("encodes POJO with all null values") void encodesAllNulls() { + // Given NullableData data = new NullableData(null, null, null); + + // Then assertEquals("text: null\ncount: null\nflag: null", encode(data)); } @Test @DisplayName("encodes POJO in object context") void encodesPOJOInObject() { + // Given Person person = new Person("Bob", 25, false); Map obj = obj("user", person); + + // Then assertEquals("user:\n name: Bob\n age: 25\n active: false", encode(obj)); } } @@ -1062,8 +1318,11 @@ class NestedAndCollections { @Test @DisplayName("encodes POJO with nested POJO") void encodesNestedPOJO() { + // Given Address address = new Address("123 Main St", "Springfield", "12345"); Employee employee = new Employee("Alice", 1001, address); + + // Then assertEquals( """ name: Alice @@ -1078,9 +1337,12 @@ void encodesNestedPOJO() { @Test @DisplayName("encodes deeply nested POJOs") void encodesDeeplyNested() { + // Given Address address = new Address("456 Oak Ave", "Metropolis", "54321"); Employee manager = new Employee("Carol", 2001, address); Company company = new Company("TechCorp", manager); + + // Then assertEquals( """ name: TechCorp @@ -1097,16 +1359,22 @@ void encodesDeeplyNested() { @Test @DisplayName("encodes POJO with list of primitives") void encodesListOfPrimitives() { + // Given Skills skills = new Skills("Developer", List.of("Java", "Python", "JavaScript")); + + // Then assertEquals("owner: Developer\nskillList[3]: Java,Python,JavaScript", encode(skills)); } @Test @DisplayName("encodes POJO with list of POJOs in tabular format") void encodesListOfPOJOs() { + // Given Person person1 = new Person("Alice", 30, true); Person person2 = new Person("Bob", 25, false); Team team = new Team("DevTeam", List.of(person1, person2)); + + // Then assertEquals( """ name: DevTeam @@ -1119,9 +1387,14 @@ void encodesListOfPOJOs() { @Test @DisplayName("encodes POJO with Map fields") void encodesMapFields() { + // Given Map settings = Map.of("debug", true, "timeout", 30, "mode", "production"); Configuration config = new Configuration("AppConfig", settings); + + // When String result = encode(config); + + // Then assertTrue(result.startsWith("name: AppConfig\nsettings:")); assertTrue(result.contains("debug: true")); assertTrue(result.contains("timeout: 30")); @@ -1131,18 +1404,26 @@ void encodesMapFields() { @Test @DisplayName("encodes POJO with empty collections") void encodesEmptyCollections() { + // Given EmptyCollections empty = new EmptyCollections(List.of(), Map.of()); + + // Then assertEquals("emptyList[0]:\nemptyMap:", encode(empty)); } @Test @DisplayName("encodes POJO with multiple collection fields") void encodesMultipleCollections() { + // Given MultiCollection multi = new MultiCollection( List.of(1, 2, 3), List.of("a", "b"), Map.of("x", 10, "y", 20)); + + // When String result = encode(multi); + + // Then assertTrue(result.contains("numbers[3]: 1,2,3")); assertTrue(result.contains("tags[2]: a,b")); assertTrue(result.contains("counts:")); @@ -1156,21 +1437,30 @@ class JacksonAnnotations { @Test @DisplayName("encodes POJO with @JsonProperty annotation") void encodesJsonProperty() { + // Given AnnotatedProduct product = new AnnotatedProduct(501, "Mouse", 29.99); + + // Then assertEquals("product_id: 501\nproduct_name: Mouse\nprice: 29.99", encode(product)); } @Test @DisplayName("encodes POJO with @JsonIgnore annotation") void encodesJsonIgnore() { + // Given SecureData data = new SecureData("public info", "secret", 1); + + // Then assertEquals("publicField: public info\nversion: 1", encode(data)); } @Test @DisplayName("encodes POJO with multiple annotations") void encodesMultipleAnnotations() { + // Given ComplexAnnotated obj = new ComplexAnnotated(123, "Test", "internal data", true); + + // Then assertEquals("user_id: 123\nname: Test\nis_active: true", encode(obj)); } @@ -1243,7 +1533,7 @@ void encodesWithJsonPropertyOrderAnnotations() { @DisplayName("encodes a POJO with SQLDate") void encodesSQLDate() { // Given - UserDTO userDTO = new UserDTO(123,"Bob", "Marley", new java.sql.Date(1766419274)); + UserDTO userDTO = new UserDTO(123, "Bob", "Marley", new java.sql.Date(1766419274)); // When String encode = encode(userDTO); @@ -1287,9 +1577,12 @@ void encodesWithJsonSerializeAnnotations() { @Test @DisplayName("encodes list of annotated POJOs in tabular format") void encodesListOfAnnotatedPOJOs() { + // Given AnnotatedProduct p1 = new AnnotatedProduct(101, "Keyboard", 79.99); AnnotatedProduct p2 = new AnnotatedProduct(102, "Monitor", 299.99); Map obj = obj("products", List.of(p1, p2)); + + // Then assertEquals( """ products[2]{product_id,product_name,price}: @@ -1301,6 +1594,7 @@ void encodesListOfAnnotatedPOJOs() { @Test @DisplayName("encodes nested POJO with keeping the order") void encodesNestedWithKeepingTheOrder() { + // Given List hotelList = new ArrayList<>(); for (int i = 0; i < 5; i++) { hotelList.add(new HotelInfoLlmRerankDTO("A" + (i + 1), @@ -1313,6 +1607,7 @@ void encodesNestedWithKeepingTheOrder() { )); } + // Then assertTrue(encode(hotelList).startsWith("[5]{no,hotelId,hotelName,hotelBrand,hotelCategory,hotelPrice,hotelAddressDistance}:")); } } @@ -1321,12 +1616,15 @@ void encodesNestedWithKeepingTheOrder() { @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") void throwsOnConstructor() throws NoSuchMethodException { + // Given final Constructor constructor = JToon.class.getDeclaredConstructor(); constructor.setAccessible(true); + // When final InvocationTargetException thrown = assertThrows(InvocationTargetException.class, constructor::newInstance); + // Then final Throwable cause = thrown.getCause(); assertInstanceOf(UnsupportedOperationException.class, cause); assertEquals("Utility class cannot be instantiated", cause.getMessage()); diff --git a/src/test/java/dev/toonformat/jtoon/RoundTripTest.java b/src/test/java/dev/toonformat/jtoon/RoundTripTest.java index 216b046..b278cad 100644 --- a/src/test/java/dev/toonformat/jtoon/RoundTripTest.java +++ b/src/test/java/dev/toonformat/jtoon/RoundTripTest.java @@ -11,14 +11,14 @@ import java.util.List; import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * Tests for round-trip encode/decode symmetry. * Verifies that decode(encode(object)) preserves the original data structure. */ @Tag("integration") -public class RoundTripTest { +class RoundTripTest { @Nested @DisplayName("Primitives Round-Trip") @@ -27,39 +27,48 @@ class PrimitivesRoundTrip { @Test @DisplayName("should preserve null values") void testNullRoundTrip() { + // Given Map data = new LinkedHashMap<>(); data.put("value", null); + // When String toon = JToon.encode(data); Object decoded = JToon.decode(toon); + // Then assertEquals(data, decoded); } @Test @DisplayName("should preserve boolean values") void testBooleanRoundTrip() { + // Given Map data = new LinkedHashMap<>(); data.put("active", true); data.put("enabled", false); + // When String toon = JToon.encode(data); Object decoded = JToon.decode(toon); + // Then assertEquals(data, decoded); } @Test @DisplayName("should preserve integer values") void testIntegerRoundTrip() { + // Given Map data = new LinkedHashMap<>(); data.put("count", 42); data.put("zero", 0); data.put("negative", -100); + // When String toon = JToon.encode(data); Object decoded = JToon.decode(toon); + // Then @SuppressWarnings("unchecked") Map decodedMap = (Map) decoded; // Integers decode as Long, so compare numeric values @@ -71,13 +80,16 @@ void testIntegerRoundTrip() { @Test @DisplayName("should preserve floating point values") void testFloatRoundTrip() { + // Given Map data = new LinkedHashMap<>(); data.put("pi", 3.14); data.put("price", 99.99); + // When String toon = JToon.encode(data); Object decoded = JToon.decode(toon); + // Then @SuppressWarnings("unchecked") Map decodedMap = (Map) decoded; assertEquals(3.14, (Double) decodedMap.get("pi"), 0.0001); @@ -87,28 +99,34 @@ void testFloatRoundTrip() { @Test @DisplayName("should preserve string values") void testStringRoundTrip() { + // Given Map data = new LinkedHashMap<>(); data.put("name", "Ada"); data.put("note", "hello, world"); data.put("empty", ""); + // When String toon = JToon.encode(data); Object decoded = JToon.decode(toon); + // Then assertEquals(data, decoded); } @Test @DisplayName("should preserve strings with special characters") void testSpecialCharacterStringRoundTrip() { + // Given Map data = new LinkedHashMap<>(); data.put("text", "line1\nline2"); data.put("path", "C:\\Users\\Documents"); data.put("quote", "He said \"hello\""); + // When String toon = JToon.encode(data); Object decoded = JToon.decode(toon); + // Then assertEquals(data, decoded); } } @@ -120,18 +138,22 @@ class ArraysRoundTrip { @Test @DisplayName("should preserve primitive arrays") void testPrimitiveArrayRoundTrip() { + // Given Map data = new LinkedHashMap<>(); data.put("tags", Arrays.asList("reading", "gaming", "coding")); + // When String toon = JToon.encode(data); Object decoded = JToon.decode(toon); + // Then assertEquals(data, decoded); } @Test @DisplayName("should preserve tabular arrays") void testTabularArrayRoundTrip() { + // Given Map data = new LinkedHashMap<>(); Map user1 = new LinkedHashMap<>(); @@ -146,9 +168,11 @@ void testTabularArrayRoundTrip() { data.put("users", Arrays.asList(user1, user2)); + // When String toon = JToon.encode(data); Object decoded = JToon.decode(toon); + // Then @SuppressWarnings("unchecked") Map decodedMap = (Map) decoded; @SuppressWarnings("unchecked") @@ -165,12 +189,15 @@ void testTabularArrayRoundTrip() { @Test @DisplayName("should preserve empty arrays") void testEmptyArrayRoundTrip() { + // Given Map data = new LinkedHashMap<>(); data.put("items", List.of()); + // When String toon = JToon.encode(data); Object decoded = JToon.decode(toon); + // Then assertEquals(data, decoded); } } @@ -182,6 +209,7 @@ class NestedObjectsRoundTrip { @Test @DisplayName("should preserve nested objects") void testNestedObjectRoundTrip() { + // Given Map contact = new LinkedHashMap<>(); contact.put("email", "ada@example.com"); contact.put("phone", "555-1234"); @@ -194,9 +222,11 @@ void testNestedObjectRoundTrip() { Map data = new LinkedHashMap<>(); data.put("user", user); + // When String toon = JToon.encode(data); Object decoded = JToon.decode(toon); + // Then @SuppressWarnings("unchecked") Map decodedMap = (Map) decoded; @SuppressWarnings("unchecked") @@ -212,6 +242,7 @@ void testNestedObjectRoundTrip() { @Test @DisplayName("should preserve deeply nested structures") void testDeeplyNestedRoundTrip() { + // Given Map level3 = new LinkedHashMap<>(); level3.put("value", 42); @@ -224,9 +255,11 @@ void testDeeplyNestedRoundTrip() { Map data = new LinkedHashMap<>(); data.put("nested", level1); + // When String toon = JToon.encode(data); Object decoded = JToon.decode(toon); + // Then // Navigate through nested structure and verify @SuppressWarnings("unchecked") Map decodedMap = (Map) decoded; @@ -247,15 +280,18 @@ class ComplexStructuresRoundTrip { @Test @DisplayName("should preserve mixed root-level content") void testMixedContentRoundTrip() { + // Given Map data = new LinkedHashMap<>(); data.put("id", 123); data.put("name", "Ada"); data.put("tags", Arrays.asList("dev", "admin")); data.put("active", true); + // When String toon = JToon.encode(data); Object decoded = JToon.decode(toon); + // Then @SuppressWarnings("unchecked") Map decodedMap = (Map) decoded; assertEquals(123L, decodedMap.get("id")); // Integers decode as Long @@ -269,6 +305,7 @@ void testMixedContentRoundTrip() { @Test @DisplayName("should preserve objects with nested arrays and objects") void testComplexNestedRoundTrip() { + // Given Map contact = new LinkedHashMap<>(); contact.put("email", "ada@example.com"); @@ -281,9 +318,11 @@ void testComplexNestedRoundTrip() { Map data = new LinkedHashMap<>(); data.put("user", user); + // When String toon = JToon.encode(data); Object decoded = JToon.decode(toon); + // Then @SuppressWarnings("unchecked") Map decodedMap = (Map) decoded; @SuppressWarnings("unchecked") @@ -306,30 +345,36 @@ class DelimiterOptionsRoundTrip { @Test @DisplayName("should preserve data with tab delimiter") void testTabDelimiterRoundTrip() { + // Given Map data = new LinkedHashMap<>(); data.put("tags", Arrays.asList("a", "b", "c")); EncodeOptions encodeOpts = new EncodeOptions(2, Delimiter.TAB, false, KeyFolding.OFF, Integer.MAX_VALUE); DecodeOptions decodeOpts = new DecodeOptions(2, Delimiter.TAB, true, PathExpansion.OFF); + // When String toon = JToon.encode(data, encodeOpts); Object decoded = JToon.decode(toon, decodeOpts); + // Then assertEquals(data, decoded); } @Test @DisplayName("should preserve data with pipe delimiter") void testPipeDelimiterRoundTrip() { + // Given Map data = new LinkedHashMap<>(); data.put("tags", Arrays.asList("a", "b", "c")); EncodeOptions encodeOpts = new EncodeOptions(2, Delimiter.PIPE, false, KeyFolding.OFF, Integer.MAX_VALUE); DecodeOptions decodeOpts = new DecodeOptions(2, Delimiter.PIPE, true, PathExpansion.OFF); + // When String toon = JToon.encode(data, encodeOpts); Object decoded = JToon.decode(toon, decodeOpts); + // Then assertEquals(data, decoded); } } @@ -341,16 +386,20 @@ class JsonRoundTrip { @Test @DisplayName("should preserve data through JSON intermediary") void testJsonRoundTrip() { + // Given Map data = new LinkedHashMap<>(); data.put("id", 123); data.put("name", "Ada"); data.put("tags", Arrays.asList("dev", "admin")); + // When String toon = JToon.encode(data); String json = JToon.decodeToJson(toon); String toon2 = JToon.encodeJson(json); Object decoded = JToon.decode(toon2); + + // Then @SuppressWarnings("unchecked") Map decodedMap = (Map) decoded; assertEquals(123L, decodedMap.get("id")); // Integers decode as Long @@ -368,11 +417,14 @@ class EdgeCasesRoundTrip { @Test @DisplayName("should preserve empty object") void testEmptyObjectRoundTrip() { + // Given Map data = new LinkedHashMap<>(); + // When String toon = JToon.encode(data); Object decoded = JToon.decode(toon); + // Then // Empty object encodes to empty string, which decodes to empty object assertEquals(Collections.emptyMap(), decoded); } @@ -380,13 +432,16 @@ void testEmptyObjectRoundTrip() { @Test @DisplayName("should preserve special character keys") void testSpecialKeyRoundTrip() { + // Given Map data = new LinkedHashMap<>(); data.put("order:id", 42); data.put("full name", "Alice"); + // When String toon = JToon.encode(data); Object decoded = JToon.decode(toon); + // Then @SuppressWarnings("unchecked") Map decodedMap = (Map) decoded; assertEquals(42L, decodedMap.get("order:id")); // Integers decode as Long diff --git a/src/test/java/dev/toonformat/jtoon/decoder/ArrayDecoderTest.java b/src/test/java/dev/toonformat/jtoon/decoder/ArrayDecoderTest.java index 9fde695..353385e 100644 --- a/src/test/java/dev/toonformat/jtoon/decoder/ArrayDecoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/decoder/ArrayDecoderTest.java @@ -1,6 +1,7 @@ package dev.toonformat.jtoon.decoder; import dev.toonformat.jtoon.DecodeOptions; +import dev.toonformat.jtoon.Delimiter; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -10,10 +11,7 @@ import java.lang.reflect.Method; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; @Tag("unit") class ArrayDecoderTest { @@ -23,12 +21,15 @@ class ArrayDecoderTest { @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") void throwsOnConstructor() throws NoSuchMethodException { + // Given final Constructor constructor = ArrayDecoder.class.getDeclaredConstructor(); constructor.setAccessible(true); + // When final InvocationTargetException thrown = assertThrows(InvocationTargetException.class, constructor::newInstance); + // Then final Throwable cause = thrown.getCause(); assertInstanceOf(UnsupportedOperationException.class, cause); assertEquals("Utility class cannot be instantiated", cause.getMessage()); @@ -44,44 +45,82 @@ private static Object invokePrivateStatic(String methodName, Class[] paramTyp @Test @DisplayName("Should parse TOON format numerical array to JSON") void parseNumericalPrimitiveArray() { + // Given setUpContext("[3]: 1,2,3"); + + // When List result = ArrayDecoder.parseArray("[3]: 1,2,3", 0, context); + + // Then assertEquals("[1, 2, 3]", result.toString()); } @Test @DisplayName("Should parse TOON format string array to JSON") void parseStrPrimitiveArray() { + // Given setUpContext("[3]: reading,gaming,coding"); + + // When List result = ArrayDecoder.parseArray("[3]: reading,gaming,coding", 0, context); + + // Then assertEquals("[reading, gaming, coding]", result.toString()); } @Test @DisplayName("Should parse TOON format tabular array to JSON") void parseTabularArray() { + // Given setUpContext("[2]{sku,qty,price}:\n A1,2,9.99\n B2,1,14.5"); + + // When List result = ArrayDecoder.parseArray("[2]{sku,qty,price}:\n A1,2,9.99\n B2,1,14.5", 0, context); + + // Then assertEquals("[{sku=A1, qty=2, price=9.99}, {sku=B2, qty=1, price=14.5}]", result.toString()); } @Test @DisplayName("Should parse TOON format list array to JSON") void parseListArray() { + // Given setUpContext("[1]:\n - first\n - second\n -"); + + // When List result = ArrayDecoder.parseArray("[1]:\n - first\n - second\n -", 0, context); + + // Then assertEquals(""" - [- first - - second - -]""", result.toString()); + [- first + - second + -]""", result.toString()); } @Test @DisplayName("Should extract the correct comma from delimiter") void expectsToExtractCommaFromDelimiter() { + // Given setUpContext("items[3]: a,b,c"); - String result = ArrayDecoder.extractDelimiterFromHeader("items[3]: a,b,c", context); - assertEquals(",", result); + + // When + Delimiter result = ArrayDecoder.extractDelimiterFromHeader("items[3]: a,b,c", context); + + // Then + assertEquals(",", result.toString()); + } + + @Test + @DisplayName("Should extract the correct slash from delimiter") + void expectsToExtractSlashFromDelimiter() { + // Given + setUpContext("items[3|]: a|b|c"); + + // When + Delimiter result = ArrayDecoder.extractDelimiterFromHeader("[3|]", context); + + // Then + assertEquals("|", result.toString()); } @Test @@ -90,6 +129,39 @@ void validateArrayLength() { assertThrows(IllegalArgumentException.class, () -> ArrayDecoder.validateArrayLength("[2]: 1,2,3", 3)); } + @Test + @DisplayName("Should validate array length") + void validateArrayLengthWithoutException() { + assertDoesNotThrow(() -> ArrayDecoder.validateArrayLength("[2]: 1,2,3", 2)); + } + + @Test + @DisplayName("Should split a array") + void parseDelimitedValues() { + // When + List strings = ArrayDecoder.parseDelimitedValues("1,2,3", Delimiter.COMMA); + // Then + assertEquals(3, strings.size()); + } + + @Test + void shouldAddEmptyFinalValueWhenInputEndsWithDelimiter() { + // When + List result = ArrayDecoder.parseDelimitedValues("a,b,", Delimiter.COMMA); + + // Then + assertEquals(List.of("a", "b", ""), result); + } + + @Test + void shouldReturnEmptyListWhenInputIsEmpty() { + // When + List result = ArrayDecoder.parseDelimitedValues("", Delimiter.COMMA); + + // Then + assertTrue(result.isEmpty()); + } + @Test @DisplayName("extract length from the Header") void extractLengthFromHeader() throws Exception { @@ -97,7 +169,7 @@ void extractLengthFromHeader() throws Exception { String input = "[2]{sku,qty,price}:\n A1,2,9.99\n B2,1,14.5"; // When - Integer extractLengthFromHeader = (Integer) invokePrivateStatic("extractLengthFromHeader", new Class[] { String.class }, input); + Integer extractLengthFromHeader = (Integer) invokePrivateStatic("extractLengthFromHeader", new Class[]{String.class}, input); // Then assertEquals(2, extractLengthFromHeader); @@ -110,15 +182,28 @@ void extractLengthFromHeaderNullReturn() throws Exception { String input = "[T]{sku,qty,price}:\n A1,2,9.99\n B2,1,14.5"; // When - Integer extractLengthFromHeader = (Integer) invokePrivateStatic("extractLengthFromHeader", new Class[] { String.class }, input); + Integer extractLengthFromHeader = (Integer) invokePrivateStatic("extractLengthFromHeader", new Class[]{String.class}, input); // Then assertNull(extractLengthFromHeader); } + @Test + @DisplayName("do not terminate the List Array") + void shouldTerminateListArrayReturnFalse() throws Exception { + // Given + setUpContext("items[3]: a,b,c"); + + // When + boolean terminateListArray = (boolean) invokePrivateStatic("shouldTerminateListArray", new Class[]{int.class, int.class, String.class, DecodeContext.class}, 3, 1, " - item", this.context); + + // Then + assertFalse(terminateListArray); + } + private void setUpContext(String toon) { this.context.lines = toon.split("\n", -1); this.context.options = DecodeOptions.DEFAULT; - this.context.delimiter = DecodeOptions.DEFAULT.delimiter().toString(); + this.context.delimiter = DecodeOptions.DEFAULT.delimiter(); } } diff --git a/src/test/java/dev/toonformat/jtoon/decoder/DecodeHelperTest.java b/src/test/java/dev/toonformat/jtoon/decoder/DecodeHelperTest.java index 9273734..57e024f 100644 --- a/src/test/java/dev/toonformat/jtoon/decoder/DecodeHelperTest.java +++ b/src/test/java/dev/toonformat/jtoon/decoder/DecodeHelperTest.java @@ -26,12 +26,16 @@ class DecodeHelperTest { @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") void throwsOnConstructor() throws NoSuchMethodException { + // Given final Constructor constructor = DecodeHelper.class.getDeclaredConstructor(); constructor.setAccessible(true); + // When final InvocationTargetException thrown = assertThrows(InvocationTargetException.class, constructor::newInstance); + + // Then final Throwable cause = thrown.getCause(); assertInstanceOf(UnsupportedOperationException.class, cause); assertEquals("Utility class cannot be instantiated", cause.getMessage()); @@ -47,72 +51,111 @@ void isBlankLine() { @Test @DisplayName("Should find the correct depth for primitive arrays") void findDepthForPrimitiveArrays() { + // Given setUpContext("tags[3]: 1,2,3"); + + // When int result = DecodeHelper.getDepth("tags[3]: 1,2,3", context); + + // Then assertEquals(0, result); } @Test @DisplayName("Should find the correct depth for nested arrays") void findDepthForNestedArrays() { + // Given setUpContext("items[1]:\n - id: 1\n nested:\n x: 1"); + + // When int result = DecodeHelper.getDepth("items[1]:\n - id: 1\n nested:\n x: 1", context); + + // Then assertEquals(0, result); } @Test @DisplayName("Should find the correct depth for tabular arrays") void findDepthForTabularArrays() { + // Given setUpContext("items[2]{\"order:id\",\"full name\"}:\n 1,Ada\n 2,Bob"); + + // When int result = DecodeHelper.getDepth("items[2]{\"order:id\",\"full name\"}:\n 1,Ada\n 2,Bob", context); + + // Then assertEquals(0, result); } @Test @DisplayName("Should find the correct depth for objects") void findDepthForObjects() { + // Given setUpContext("id: 123\nname: Ada\nactive: true"); + + // When int result = DecodeHelper.getDepth("id: 123\nname: Ada\nactive: true", context); + + // Then assertEquals(0, result); } @Test @DisplayName("getDepth: throws on tab indentation in strict mode") void getDepth_throwsOnTabInStrict() { + // Given setUpContext("\tkey: 1"); context.currentLine = 0; + + // When IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> DecodeHelper.getDepth("\tkey: 1", context)); + + // Then assertTrue(ex.getMessage().contains("Tab character")); } @Test @DisplayName("getDepth: throws on non-multiple spaces in strict mode") void getDepth_throwsOnNonMultipleIndent() { + // Given setUpContext(" key: 1"); // 3 leading spaces, default indent=2 context.currentLine = 0; + + // When IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> DecodeHelper.getDepth(" key: 1", context)); + + // Then assertTrue(ex.getMessage().contains("Non-multiple indentation")); } @Test @DisplayName("getDepth: computes depth with custom indent size") void getDepth_withCustomIndent() { + // Given // indent size 4, 8 spaces -> depth 2 setUpContext(" key: 1"); context.options = DecodeOptions.withIndent(4); + + // When int depth = DecodeHelper.getDepth(" key: 1", context); + + // Then assertEquals(2, depth); } @Test @DisplayName("getDepth: lenient mode allows tabs and odd spaces") void getDepth_lenientMode() { - // Make it lenient + // Given setUpContext("\tkey: 1"); context.options = DecodeOptions.withStrict(false); + + // When int depth = DecodeHelper.getDepth("\tkey: 1", context); + + // Then assertEquals(0, depth); // Odd number of spaces doesn't throw either depth = DecodeHelper.getDepth(" key: 1", context); @@ -121,33 +164,61 @@ void getDepth_lenientMode() { @Test @DisplayName("findUnquotedColon: ignores colons inside quotes and handles escapes") - void findUnquotedColon_cases() { + void findUnquotedColon_case1() { + // Given String s1 = "\"order:id\": 1"; // first colon quoted, second is key-value + + // When int idx1 = DecodeHelper.findUnquotedColon(s1); + + // Then assertEquals(s1.lastIndexOf(':'), idx1); + } + @Test + void findUnquotedColon_case2() { + // Given String s2 = "no colon here"; + // When/Then assertEquals(-1, DecodeHelper.findUnquotedColon(s2)); + } + @Test + void findUnquotedColon_case() { + // Given String s3 = "\"escaped\\\"quote\":42"; // quoted section contains escaped quote + + // When int idx3 = DecodeHelper.findUnquotedColon(s3); + + // Then assertEquals(s3.lastIndexOf(':'), idx3); } @Test @DisplayName("findNextNonBlankLine: skips blank lines") void findNextNonBlankLine_skipsBlanks() { + // Given setUpContext("\n\n a: 1\n\n"); + + // When int idx = DecodeHelper.findNextNonBlankLine(0, context); + + // Then assertEquals(2, idx); } @Test @DisplayName("findNextNonBlankLineDepth: returns depth or null when none") void findNextNonBlankLineDepth_cases() { + // Given setUpContext("\n a: 1\n\n"); context.currentLine = 0; + + // When Integer depth = DecodeHelper.findNextNonBlankLineDepth(context); + + // Then assertNotNull(depth); assertEquals(1, depth); } @@ -155,16 +226,22 @@ void findNextNonBlankLineDepth_cases() { @Test @DisplayName("findNextNonBlankLineDepth: returns depth or null when none") void findNextNonBlankLineDepth_casesOnStrangeContent() { + // Given setUpContext("\n\n"); context.currentLine = 0; + + // When/Then assertNull(DecodeHelper.findNextNonBlankLineDepth(context)); } @Test @DisplayName("validateNoMultiplePrimitivesAtRoot: throws when another root primitive follows") void validateNoMultiplePrimitivesAtRoot_throws() { + // Given setUpContext("1\n2\n"); context.currentLine = 0; + +// When/Then assertThrows(IllegalArgumentException.class, () -> DecodeHelper.validateNoMultiplePrimitivesAtRoot(context)); } @@ -172,31 +249,55 @@ void validateNoMultiplePrimitivesAtRoot_throws() { @Test @DisplayName("checkFinalValueConflict: throws when existing is object/array and new is scalar (strict)") void checkFinalValueConflict_strictConflicts() { + // Given setUpContext(""); + + // When // object vs scalar IllegalArgumentException ex1 = assertThrows(IllegalArgumentException.class, () -> DecodeHelper.checkFinalValueConflict("a", new java.util.HashMap<>(), 1, context)); + // Then assertTrue(ex1.getMessage().contains("object")); + } + + @Test + @DisplayName("checkFinalValueConflict: throws when existing is object/array and new is scalar (strict)") + void checkFinalValueConflict_strictConflicts2() { + // Given + setUpContext(""); + + // When // array vs scalar IllegalArgumentException ex2 = assertThrows(IllegalArgumentException.class, - () -> DecodeHelper.checkFinalValueConflict("a", new java.util.ArrayList<>(), 1, context)); + () -> DecodeHelper.checkFinalValueConflict("a", new ArrayList<>(), 1, context)); + + // Then assertTrue(ex2.getMessage().contains("array")); } @Test @DisplayName("checkPathExpansionConflict: respects strict mode toggle") void checkPathExpansionConflict_strictToggle() { - java.util.Map map = new java.util.HashMap<>(); - map.put("a", new java.util.HashMap<>()); + // Given + Map map = new HashMap<>(); + map.put("a", new HashMap<>()); + // When // strict true -> conflict setUpContext(""); assertThrows(IllegalArgumentException.class, () -> DecodeHelper.checkPathExpansionConflict(map, "a", 1, context)); + } + @Test + @DisplayName("checkPathExpansionConflict: respects strict mode toggle") + void checkPathExpansionConflict_strictToggle2() { + // Given + Map map = new HashMap<>(); // strict false -> no conflict setUpContext(""); context.options = DecodeOptions.withStrict(false); + // When assertDoesNotThrow(() -> DecodeHelper.checkPathExpansionConflict(map, "a", 1, context)); } @@ -219,7 +320,10 @@ void tearDown() { @Test @DisplayName("Given blank line, When getting depth, Then returns 0") void blankLineDepth() { + // Given setUpContext(new String[]{" "}, false, 2); + + // When / Then assertEquals(0, DecodeHelper.getDepth(" ", context)); } @@ -251,47 +355,59 @@ void strictNonMultipleIndentThrows() { @DisplayName("Given non-strict mode and non-multiple indentation, Then allowed") void nonStrictAllowsNonMultiple() { setUpContext(new String[]{" abc"}, false, 2); + + // When / Then assertEquals(1, DecodeHelper.getDepth(" abc", context)); // 3 / 2 -> 1 } @Test @DisplayName("Given indentSize=0, Then return leading spaces") void indentZeroReturnsSpaces() { + // Given setUpContext(new String[]{" abc"}, false, 0); + + // When / Then assertEquals(4, DecodeHelper.getDepth(" abc", context)); } @Test @DisplayName("Given correct multiple indentation, Then correct depth returned") void correctDepth() { + // Given setUpContext(new String[]{" abc"}, true, 2); + + // When / Then assertEquals(2, DecodeHelper.getDepth(" abc", context)); } @Test @DisplayName("Given strict mode and blank line with non-multiple spaces, Then getDepth() throws (validateIndentation skipped)") void strictBlankNonMultipleIndentationThrows() { + // Given // 3 spaces + NON-BREAKING SPACE (U+00A0) // NBSP is whitespace but NOT trimmed away and not counted as space -> perfect case String line = " \u00A0"; setUpContext(new String[]{line}, true, 2); context.currentLine = 0; - + // When IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> DecodeHelper.getDepth(line, context)); + // Then assertTrue(ex.getMessage().startsWith("Non-multiple indentation")); } @Test @DisplayName("validateIndentation: strict mode tab β†’ throws") void validateIndentationTabThrows() { + // Given String line = "\tabc"; setUpContext(new String[]{line}, true, 2); context.currentLine = 0; + // When / Then assertThrows(IllegalArgumentException.class, () -> DecodeHelper.getDepth(line, context)); } @@ -299,36 +415,43 @@ void validateIndentationTabThrows() { @Test @DisplayName("validateIndentation: leading spaces then text β†’ ok") void validateIndentationLeadingSpacesThenStop() { + // Given String line = " abc"; // 3 spaces then letter setUpContext(new String[]{line}, true, 3); // indent 3 β†’ valid context.currentLine = 0; + // When / Then assertEquals(1, DecodeHelper.getDepth(line, context)); } @Test @DisplayName("validateIndentation: strict + non-multiple indentation β†’ throws") void validateIndentationNonMultipleThrows() { + // Given String line = " abc"; // 3 spaces β†’ not multiple of indent 2 setUpContext(new String[]{line}, true, 2); context.currentLine = 0; - + // When IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> DecodeHelper.getDepth(line, context)); + + // Then assertTrue(ex.getMessage().contains("Non-multiple indentation")); } @Test @DisplayName("validateIndentation: non-breaking space stops loop") void validateIndentationStopsAtNonBreakingSpace() { + // Given String line = " \u00A0abc"; setUpContext(new String[]{line}, true, 3); context.currentLine = 0; + // When / Then // 3 spaces β†’ valid multiple of 3 assertEquals(1, DecodeHelper.getDepth(line, context)); } @@ -340,13 +463,19 @@ class NextNonBlankTests { @Test void findNextNonBlank() { + // Given setUpContext(new String[]{"", " ", "abc"}, false, 2); + + // When / Then assertEquals(2, DecodeHelper.findNextNonBlankLine(0, context)); } @Test void noneFound() { + // Given setUpContext(new String[]{"", " "}, false, 2); + + // When / Then assertEquals(2, DecodeHelper.findNextNonBlankLine(0, context)); } } @@ -358,8 +487,10 @@ class ConflictTests { @Test @DisplayName("Given strict mode and existing map but new primitive -> conflict") void mapToPrimitiveConflict() { + // Given setUpContext(new String[]{}, true, 2); + // When / Then assertThrows(IllegalArgumentException.class, () -> DecodeHelper.checkFinalValueConflict("a", new HashMap<>(), 5, context)); } @@ -367,7 +498,10 @@ void mapToPrimitiveConflict() { @Test @DisplayName("Given strict mode and existing list but new primitive -> conflict") void listToPrimitiveConflict() { + // Given setUpContext(new String[]{}, true, 2); + + // When / Then assertThrows(IllegalArgumentException.class, () -> DecodeHelper.checkFinalValueConflict("a", new ArrayList<>(), 5, context)); } @@ -375,7 +509,10 @@ void listToPrimitiveConflict() { @Test @DisplayName("Given non-strict mode -> no conflict") void nonStrictNoConflict() { + // Given setUpContext(new String[]{}, false, 2); + + // When / Then assertDoesNotThrow( () -> DecodeHelper.checkFinalValueConflict("a", new HashMap<>(), 5, context) ); @@ -384,10 +521,12 @@ void nonStrictNoConflict() { @Test @DisplayName("checkPathExpansionConflict delegates to final conflict check") void pathExpansionConflict() { + // Given setUpContext(new String[]{}, true, 2); Map map = new HashMap<>(); map.put("x", new HashMap<>()); + // When / Then assertThrows(IllegalArgumentException.class, () -> DecodeHelper.checkPathExpansionConflict(map, "x", 5, context)); } @@ -412,8 +551,11 @@ void tearDown() { @Test @DisplayName("Given next line at depth 0 in strict mode -> throw") void rootPrimitiveConflict() { + // Given setUpContext(new String[]{"abc"}, true, 2); context.currentLine = 0; + + // When / Then assertThrows(IllegalArgumentException.class, () -> DecodeHelper.validateNoMultiplePrimitivesAtRoot(context)); } @@ -421,16 +563,22 @@ void rootPrimitiveConflict() { @Test @DisplayName("Given deeper indentation -> OK") void deeperIndentOk() { + // Given setUpContext(new String[]{" abc"}, true, 2); context.currentLine = 0; + + // When / Then assertDoesNotThrow(() -> DecodeHelper.validateNoMultiplePrimitivesAtRoot(context)); } @Test @DisplayName("Given only blanks -> OK") void blanksOnlyOk() { + // Given setUpContext(new String[]{" "}, true, 2); context.currentLine = 0; + + // When / Then assertDoesNotThrow(() -> DecodeHelper.validateNoMultiplePrimitivesAtRoot(context)); } } @@ -498,12 +646,12 @@ void testBlankLinesReturnZero() throws Exception { private void setUpContext(String toon) { this.context.lines = toon.split("\n", -1); this.context.options = DecodeOptions.DEFAULT; - this.context.delimiter = DecodeOptions.DEFAULT.delimiter().toString(); + this.context.delimiter = DecodeOptions.DEFAULT.delimiter(); } private void setUpContext(String[] lines, boolean strict, int indent) { this.context.lines = lines; this.context.options = new DecodeOptions(indent, Delimiter.COMMA, strict, PathExpansion.OFF); - this.context.delimiter = DecodeOptions.DEFAULT.delimiter().toString(); + this.context.delimiter = DecodeOptions.DEFAULT.delimiter(); } } diff --git a/src/test/java/dev/toonformat/jtoon/decoder/KeyDecoderTest.java b/src/test/java/dev/toonformat/jtoon/decoder/KeyDecoderTest.java index da73c8f..5c231ed 100644 --- a/src/test/java/dev/toonformat/jtoon/decoder/KeyDecoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/decoder/KeyDecoderTest.java @@ -23,12 +23,15 @@ class KeyDecoderTest { @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") void throwsOnConstructor() throws NoSuchMethodException { + // Given final Constructor constructor = KeyDecoder.class.getDeclaredConstructor(); constructor.setAccessible(true); + // When final InvocationTargetException thrown = assertThrows(InvocationTargetException.class, constructor::newInstance); + // Then final Throwable cause = thrown.getCause(); assertInstanceOf(UnsupportedOperationException.class, cause); assertEquals("Utility class cannot be instantiated", cause.getMessage()); diff --git a/src/test/java/dev/toonformat/jtoon/decoder/ListItemDecoderTest.java b/src/test/java/dev/toonformat/jtoon/decoder/ListItemDecoderTest.java index 8bcc88d..75252b3 100644 --- a/src/test/java/dev/toonformat/jtoon/decoder/ListItemDecoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/decoder/ListItemDecoderTest.java @@ -21,12 +21,15 @@ class ListItemDecoderTest { @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") void throwsOnConstructor() throws NoSuchMethodException { + // Given final Constructor constructor = ListItemDecoder.class.getDeclaredConstructor(); constructor.setAccessible(true); + // When final InvocationTargetException thrown = assertThrows(InvocationTargetException.class, constructor::newInstance); + // Then final Throwable cause = thrown.getCause(); assertInstanceOf(UnsupportedOperationException.class, cause); assertEquals("Utility class cannot be instantiated", cause.getMessage()); @@ -34,9 +37,9 @@ void throwsOnConstructor() throws NoSuchMethodException { // Reflection helpers for invoking private static methods private static Object invokePrivateStatic(String methodName, Class[] paramTypes, Object... args) throws Exception { - Method m = ListItemDecoder.class.getDeclaredMethod(methodName, paramTypes); - m.setAccessible(true); - return m.invoke(null, args); + final Method declaredMethod = ListItemDecoder.class.getDeclaredMethod(methodName, paramTypes); + declaredMethod.setAccessible(true); + return declaredMethod.invoke(null, args); } @Test diff --git a/src/test/java/dev/toonformat/jtoon/decoder/ObjectDecoderTest.java b/src/test/java/dev/toonformat/jtoon/decoder/ObjectDecoderTest.java index e72dcd9..6f0020f 100644 --- a/src/test/java/dev/toonformat/jtoon/decoder/ObjectDecoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/decoder/ObjectDecoderTest.java @@ -25,12 +25,15 @@ class ObjectDecoderTest { @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") void throwsOnConstructor() throws NoSuchMethodException { + // Given final Constructor constructor = ObjectDecoder.class.getDeclaredConstructor(); constructor.setAccessible(true); + // When final InvocationTargetException thrown = assertThrows(InvocationTargetException.class, constructor::newInstance); + // Then final Throwable cause = thrown.getCause(); assertInstanceOf(UnsupportedOperationException.class, cause); assertEquals("Utility class cannot be instantiated", cause.getMessage()); @@ -39,16 +42,26 @@ void throwsOnConstructor() throws NoSuchMethodException { @Test @DisplayName("Should parse scalar value to JSON") void parseBareScalarValue() { + // Given setUpContext("v: \"true\""); + + // When Object result = ObjectDecoder.parseBareScalarValue("v: \"true\"", 0, context); + + // Then assertEquals("v: \"true\"", result.toString()); } @Test @DisplayName("Should parse item value to JSON") void parseObjectItemValue() { + // Given setUpContext("note: \"a,b\""); + + // When Object result = ObjectDecoder.parseObjectItemValue("note: \"a,b\"", 0, context); + + // Then assertEquals("note: \"a,b\"", result.toString()); } @@ -71,7 +84,7 @@ void tearDown() { @Test @DisplayName("GIVEN nested structure WHEN parsing THEN nested map is returned") void parseNestedObject_basic() { - // given + // Given setUpContext(""" parent: child1: A @@ -80,10 +93,10 @@ void parseNestedObject_basic() { context.currentLine = 1; // simulate: parser is already on the nested part - // when + // When Map result = ObjectDecoder.parseNestedObject(0, context); - // then + // Then assertEquals("A", result.get("child1")); assertEquals("B", result.get("child2")); @@ -93,7 +106,7 @@ void parseNestedObject_basic() { @Test @DisplayName("GIVEN deeper indentation WHEN child is not direct child THEN skip line") void parseNestedObject_skips_invalid_depth() { - // given + // Given setUpContext(""" parent: tooDeep: X @@ -102,10 +115,10 @@ void parseNestedObject_skips_invalid_depth() { context.currentLine = 1; - // when + // When Map result = ObjectDecoder.parseNestedObject(0, context); - // then + // Then assertEquals("OK", result.get("child")); assertEquals(3, context.currentLine); } @@ -130,32 +143,36 @@ void tearDown() { @Test @DisplayName("GIVEN root kv WHEN parsing THEN map is filled") void parseRootObjectFields_basic() { + // Given setUpContext(""" a: 10 b: 20 nested: IGNORE """); - Map root = new LinkedHashMap<>(); + // When ObjectDecoder.parseRootObjectFields(root, 0, context); + // Then assertEquals(10L, root.get("a")); assertEquals(3, context.currentLine); } @Test void parseRootObjectFields_WithWrongDepth() { + // Given setUpContext(""" a: 10 b: 20 nested: IGNORE """); - Map root = new LinkedHashMap<>(); + // When ObjectDecoder.parseRootObjectFields(root, 25, context); + // Then assertNull(root.get("a")); assertEquals(0, context.currentLine); } @@ -183,10 +200,13 @@ void tearDown() { @Test @DisplayName("GIVEN primitive WHEN parsing THEN returned and currentLine++") void parseBareScalarValue_basic() { + // Given setUpContext("123"); + // When Object result = ObjectDecoder.parseBareScalarValue("123", 0, context); + // Then assertEquals(123L, result); assertEquals(1, context.currentLine); } @@ -194,13 +214,14 @@ void parseBareScalarValue_basic() { @Test @DisplayName("GIVEN strict mode WHEN multiple root primitives THEN exception") void parseBareScalarValue_multiple_primitives_strict() { + // Given setUpContext(""" 42 99 """); - context.options = DecodeOptions.withStrict(true); + // When / then assertThrows(IllegalArgumentException.class, () -> ObjectDecoder.parseBareScalarValue("99", 0, context)); } @@ -226,19 +247,21 @@ void tearDown() { @DisplayName("GIVEN empty value + nested => nested map parsed") @SuppressWarnings("unchecked") void parseFieldValue_nested() { + // Given setUpContext(""" key: a: 1 b: 2 """); - context.currentLine = 0; + // When Object value = ObjectDecoder.parseFieldValue("", 0, context); + // Then assertInstanceOf(Map.class, value); - Map map = (Map) value; + Map map = (Map) value; assertEquals(1L, map.get("a")); assertEquals(2L, map.get("b")); } @@ -247,42 +270,51 @@ void parseFieldValue_nested() { @Test @DisplayName("GIVEN primitive string => primitive returned") void parseFieldValue_primitive() { + // Given setUpContext("key: 15"); context.currentLine = 0; - Object v = ObjectDecoder.parseFieldValue("15", 0, context); + // When + Object parseFieldValue = ObjectDecoder.parseFieldValue("15", 0, context); - assertEquals(15L, v); + // Then + assertEquals(15L, parseFieldValue); assertEquals(1L, context.currentLine); } @Test @DisplayName("GIVEN empty and no nested => empty map") void parseFieldValue_empty_no_nested() { + // Given setUpContext(""" key: next """); context.currentLine = 0; - Object v = ObjectDecoder.parseFieldValue("", 0, context); + // When + Object parseFieldValue = ObjectDecoder.parseFieldValue("", 0, context); - assertInstanceOf(Map.class, v); + // Then + assertInstanceOf(Map.class, parseFieldValue); assertEquals(1, context.currentLine); } @Test @DisplayName("GIVEN empty and no nested => empty map") void parseFieldValue_empty_no_nestedButBigCurrentLine() { + // Given setUpContext(""" key: next """); context.currentLine = 25; - Object v = ObjectDecoder.parseFieldValue("", 0, context); + // When + Object parseFieldValue = ObjectDecoder.parseFieldValue("", 0, context); - assertInstanceOf(Map.class, v); + // Then + assertInstanceOf(Map.class, parseFieldValue); assertEquals(26, context.currentLine); } } @@ -307,57 +339,66 @@ void tearDown() { @DisplayName("GIVEN empty + nested => nested map") @SuppressWarnings("unchecked") void parseObjectItemValue_nested() { + // Given setUpContext(""" - key: a: 5 b: 6 """); - context.currentLine = 0; - Object v = ObjectDecoder.parseObjectItemValue("", 0, context); + // When + Object parseObjectItemValue = ObjectDecoder.parseObjectItemValue("", 0, context); - assertInstanceOf(Map.class, v); - Map map = (Map) v; + // Then + assertInstanceOf(Map.class, parseObjectItemValue); + Map map = (Map) parseObjectItemValue; assertNull(map.get("a")); } @Test @DisplayName("GIVEN primitive => primitive returned") void parseObjectItemValue_primitive() { + // Given setUpContext("- value"); context.currentLine = 0; - Object v = ObjectDecoder.parseObjectItemValue("value", 0, context); + // When + Object parseObjectItemValue = ObjectDecoder.parseObjectItemValue("value", 0, context); - assertEquals("value", v); + // Then + assertEquals("value", parseObjectItemValue); } @Test @DisplayName("GIVEN empty and no nested => empty map") void parseObjectItemValue_empty() { + // Given setUpContext(""" - """); - context.currentLine = 0; - Object v = ObjectDecoder.parseObjectItemValue("", 0, context); + // When + Object parseObjectItemValue = ObjectDecoder.parseObjectItemValue("", 0, context); - assertInstanceOf(Map.class, v); + // Then + assertInstanceOf(Map.class, parseObjectItemValue); } @Test @DisplayName("GIVEN empty and no nested => empty map") void parseObjectItemValue_emptyContext() { + // Given setUpContext("\n\n"); - context.currentLine = 0; - Object v = ObjectDecoder.parseObjectItemValue("", 0, context); + // When + Object parseObjectItemValue = ObjectDecoder.parseObjectItemValue("", 0, context); - assertInstanceOf(Map.class, v); + // Then + assertInstanceOf(Map.class, parseObjectItemValue); } } @@ -391,6 +432,6 @@ private static Object invokePrivateStatic(String methodName, Class[] paramTyp private void setUpContext(String toon) { this.context.lines = toon.split("\n"); this.context.options = DecodeOptions.DEFAULT; - this.context.delimiter = DecodeOptions.DEFAULT.delimiter().toString(); + this.context.delimiter = DecodeOptions.DEFAULT.delimiter(); } } diff --git a/src/test/java/dev/toonformat/jtoon/decoder/PrimitiveDecoderTest.java b/src/test/java/dev/toonformat/jtoon/decoder/PrimitiveDecoderTest.java index 3d2464e..c63102d 100644 --- a/src/test/java/dev/toonformat/jtoon/decoder/PrimitiveDecoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/decoder/PrimitiveDecoderTest.java @@ -1,11 +1,13 @@ package dev.toonformat.jtoon.decoder; +import dev.toonformat.jtoon.encoder.PrimitiveEncoder; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import static org.junit.jupiter.api.Assertions.*; @@ -19,12 +21,15 @@ class PrimitiveDecoderTest { @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") void throwsOnConstructor() throws NoSuchMethodException { - final Constructor constructor = PrimitiveDecoder.class.getDeclaredConstructor(); + // Given + final Constructor constructor = PrimitiveEncoder.class.getDeclaredConstructor(); constructor.setAccessible(true); + // When final InvocationTargetException thrown = assertThrows(InvocationTargetException.class, constructor::newInstance); + // Then final Throwable cause = thrown.getCause(); assertInstanceOf(UnsupportedOperationException.class, cause); assertEquals("Utility class cannot be instantiated", cause.getMessage()); @@ -190,6 +195,98 @@ void givenNegativeZeroDouble_whenParse_thenReturnsZeroLong() { assertEquals(0L, result); } + @Test + void givenOctalNumber_whenParse_thenReturnsLong() { + // Given + String input = "07"; + + // When + Object result = PrimitiveDecoder.parse(input); + + // Then + assertNotNull(result); + assertEquals("07", result.toString()); + } + + @Test + void givenNumberWithLeadingZero_whenParse_thenReturnsLong() { + // Given + String input = "0.7"; + + // When + Object result = PrimitiveDecoder.parse(input); + + // Then + assertNotNull(result); + assertEquals("0.7", result.toString()); + } + + @Test + void givenNumberWithLeadingZeroOutsideTheOctalRange_whenParse_thenReturnsLong() { + // Given + String input = "0.9"; + + // When + Object result = PrimitiveDecoder.parse(input); + + // Then + assertNotNull(result); + assertEquals("0.9", result.toString()); + } + + @Test + void givenMinLongNumber_whenParse_thenReturnsLong() { + // Given + String input = String.valueOf(Long.MIN_VALUE); + + // When + Object result = PrimitiveDecoder.parse(input); + + // Then + assertNotNull(result); + assertEquals("-9223372036854775808", result.toString()); + } + + @Test + void givenMaxLongNumber_whenParse_thenReturnsLong() { + // Given + String input = String.valueOf(Long.MAX_VALUE); + + // When + Object result = PrimitiveDecoder.parse(input); + + // Then + assertNotNull(result); + assertEquals("9223372036854775807", result.toString()); + } + + @Test + void givenSmallerMinLongNumber_whenParse_thenReturnsLong() { + // Given + String input = String.valueOf(Long.MIN_VALUE - 1); + + // When + Object result = PrimitiveDecoder.parse(input); + + // Then + assertNotNull(result); + assertEquals("9223372036854775807", result.toString()); + } + + @Test + void givenBiggerMaxLongNumber_whenParse_thenReturnsLong() { + // Given + String input = String.valueOf(Long.MAX_VALUE + 1); + + // When + Object result = PrimitiveDecoder.parse(input); + + // Then + assertNotNull(result); + assertEquals("-9223372036854775808", result.toString()); + } + + @Test void givenInvalidNumber_whenParse_thenReturnsOriginalString() { // Given @@ -201,4 +298,35 @@ void givenInvalidNumber_whenParse_thenReturnsOriginalString() { // Then assertEquals("123abc", result); } + + @Test + void testing_SkipTrailingZeros() throws Exception { + // Given + String input = "10.000"; + + // When + String result = (String) invokePrivateStatic("stripTrailingZeros", new Class[]{String.class}, input); + + // Then + assertEquals("10", result); + } + + @Test + void testing_SkipTrailingZeros_WithSmallNUmber() throws Exception { + // Given + String input = "1.0"; + + // When + String result = (String) invokePrivateStatic("stripTrailingZeros", new Class[]{String.class}, input); + + // Then + assertEquals("1", result); + } + + // Reflection helpers for invoking private static methods + private static Object invokePrivateStatic(String methodName, Class[] paramTypes, Object... args) throws Exception { + Method declaredMethod = PrimitiveEncoder.class.getDeclaredMethod(methodName, paramTypes); + declaredMethod.setAccessible(true); + return declaredMethod.invoke(null, args); + } } diff --git a/src/test/java/dev/toonformat/jtoon/decoder/TabularArrayDecoderTest.java b/src/test/java/dev/toonformat/jtoon/decoder/TabularArrayDecoderTest.java index a7c900c..e31ca49 100644 --- a/src/test/java/dev/toonformat/jtoon/decoder/TabularArrayDecoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/decoder/TabularArrayDecoderTest.java @@ -38,12 +38,15 @@ void tearDown() { @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") void throwsOnConstructor() throws NoSuchMethodException { + // Given final Constructor constructor = TabularArrayDecoder.class.getDeclaredConstructor(); constructor.setAccessible(true); + // When final InvocationTargetException thrown = assertThrows(InvocationTargetException.class, constructor::newInstance); + // Then final Throwable cause = thrown.getCause(); assertInstanceOf(UnsupportedOperationException.class, cause); assertEquals("Utility class cannot be instantiated", cause.getMessage()); @@ -52,20 +55,27 @@ void throwsOnConstructor() throws NoSuchMethodException { @Test @DisplayName("Parse TOON format tabular array to JSON") void parseTabularArray() { + // Given setUpContext("[2]{id,value}:\n 1,null\n 2,\"test\""); + + // When List result = TabularArrayDecoder.parseTabularArray( "[2]{id,value}:\n 1,null\n 2,\"test\"", 0, - Delimiter.COMMA.toString(), context); + Delimiter.COMMA, context); + + // Then assertEquals("[{id=1, value=null}, {id=2, value=test}]", result.toString()); } @Test @DisplayName("Throws an exception if the wrong delimiter is being used") void inCaseOfMismatchInDelimiter_ThrowAnException() { + // Given setUpContext("[2]{id,value}:\n 1,null\n 2,\"test\""); + // When / then assertThrows(IllegalArgumentException.class, () -> TabularArrayDecoder.parseTabularArray( "[2]{id,value}:\n 1,null\n 2,\"test\"", 0, - Delimiter.TAB.toString(), context)); + Delimiter.TAB, context)); } @Test @@ -78,7 +88,7 @@ void processTabularRow_skipsDeeperIndentedLine() { // When List result = TabularArrayDecoder.parseTabularArray(toon, 0, - Delimiter.COMMA.toString(), context); + Delimiter.COMMA, context); // Then assertEquals(2, result.size(), "Should parse exactly two rows, skipping the deeper-indented line"); @@ -106,14 +116,13 @@ void testReturnsTrueWhenLineDepthLessThanExpected() throws Exception { int expectedRowDepth = 3; // Ensures we fall to final return List keys = List.of("a", "b", "c"); - String arrayDelimiter = ","; List result = new ArrayList<>(); // When boolean processed = (boolean) invokePrivateStatic("processTabularRow", - new Class[]{String.class, int.class, int.class, List.class, String.class, List.class, DecodeContext.class}, + new Class[]{String.class, int.class, int.class, List.class, Delimiter.class, List.class, DecodeContext.class}, line, lineDepth, expectedRowDepth, - keys, arrayDelimiter, result, context + keys, Delimiter.COMMA, result, context ); // Then @@ -154,10 +163,9 @@ void testReturnsTrueWhenNextDepthIsHeaderOrLess() throws Exception { void validateKeysDelimiter() throws Exception { // Given String keysStr = "sad\\a\"sd"; - String expectedDelimiter = ","; // When / Then - invokePrivateStatic("validateKeysDelimiter", new Class[]{String.class, String.class}, keysStr, expectedDelimiter); + invokePrivateStatic("validateKeysDelimiter", new Class[]{String.class, Delimiter.class}, keysStr, Delimiter.COMMA); } @Test @@ -192,32 +200,38 @@ void checkDelimiterMismatchExecutionWithComa() { @Test void testTerminateWhenLineDepthLessThanExpected() throws Exception { + // Given context.options = new DecodeOptions(2, Delimiter.COMMA, true, PathExpansion.OFF); String line = " some value"; // Any line works; we won't reach colon logic. int lineDepth = 1; // < expectedRowDepth int expectedRowDepth = 3; // Must be > lineDepth + // When boolean result = (boolean) invokePrivateStatic("shouldTerminateTabularArray", new Class[]{String.class, int.class, int.class, DecodeContext.class}, line, lineDepth, expectedRowDepth, context); + // Then assertTrue(result, "Should terminate when lineDepth < expectedRowDepth"); } @Test void testParseTabularArray_ReturnsEmptyList_WhenHeaderDoesNotMatchPattern() { + // Given context.options = new DecodeOptions(2, Delimiter.COMMA, false, PathExpansion.OFF); context.lines = new String[]{"ignored"}; context.currentLine = 0; + // When List result = TabularArrayDecoder.parseTabularArray( "not a header", // DOES NOT MATCH pattern 0, - ",", + Delimiter.COMMA, context ); + // Then assertNotNull(result); assertTrue(result.isEmpty(), "Expected empty list for non-matching header"); } @@ -225,7 +239,7 @@ void testParseTabularArray_ReturnsEmptyList_WhenHeaderDoesNotMatchPattern() { private void setUpContext(String toon) { this.context.lines = toon.split("\n", -1); this.context.options = DecodeOptions.DEFAULT; - this.context.delimiter = DecodeOptions.DEFAULT.delimiter().toString(); + this.context.delimiter = DecodeOptions.DEFAULT.delimiter(); } // Reflection helpers for invoking private static methods diff --git a/src/test/java/dev/toonformat/jtoon/decoder/ValueDecoderTest.java b/src/test/java/dev/toonformat/jtoon/decoder/ValueDecoderTest.java index 14edf7c..0dc207a 100644 --- a/src/test/java/dev/toonformat/jtoon/decoder/ValueDecoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/decoder/ValueDecoderTest.java @@ -24,12 +24,15 @@ class ValueDecoderTest { @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") void throwsOnConstructor() throws NoSuchMethodException { + // Given final Constructor constructor = ValueDecoder.class.getDeclaredConstructor(); constructor.setAccessible(true); + // When final InvocationTargetException thrown = assertThrows(InvocationTargetException.class, constructor::newInstance); + // Then final Throwable cause = thrown.getCause(); assertInstanceOf(UnsupportedOperationException.class, cause); assertEquals("Utility class cannot be instantiated", cause.getMessage()); @@ -164,10 +167,13 @@ void givenIndentedLineAndStrict_whenParse_thenThrow() { @Test void decode_keyValuePair_callsKeyDecoder() { + // Given DecodeOptions decodeOptions = new DecodeOptions(2, Delimiter.COMMA, false, PathExpansion.OFF); + // When Object result = ValueDecoder.decode("name: Ada", decodeOptions); + // Then // Whatever KeyDecoder returns, you simply assert expected behavior. // Usually: { "name" : "Ada" } as a map assertInstanceOf(Map.class, result); @@ -180,15 +186,18 @@ void decode_keyValuePair_callsKeyDecoder() { @Test void decodeToJson_throwsWrappedException_whenDecodeFails() { + // Given DecodeOptions options = new DecodeOptions(2, Delimiter.COMMA, true, PathExpansion.OFF); String invalidIndentedInput = " badIndent"; + // When IllegalArgumentException ex = assertThrows( IllegalArgumentException.class, () -> ValueDecoder.decodeToJson(invalidIndentedInput, options) ); + // Then assertTrue(ex.getMessage().contains("Failed to convert decoded value to JSON")); assertNotNull(ex.getCause()); // original decode() exception is preserved assertInstanceOf(IllegalArgumentException.class, ex.getCause()); diff --git a/src/test/java/dev/toonformat/jtoon/encoder/ArrayEncoderTest.java b/src/test/java/dev/toonformat/jtoon/encoder/ArrayEncoderTest.java index cc084e2..67bc058 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/ArrayEncoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/ArrayEncoderTest.java @@ -136,12 +136,15 @@ void encodeArrayWithAllPrimitivesArrayOfArrays() { @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") void throwsOnConstructor() throws NoSuchMethodException { + // Given final Constructor constructor = ArrayEncoder.class.getDeclaredConstructor(); constructor.setAccessible(true); + // When final InvocationTargetException thrown = assertThrows(InvocationTargetException.class, constructor::newInstance); + // Then final Throwable cause = thrown.getCause(); assertInstanceOf(UnsupportedOperationException.class, cause); assertEquals("Utility class cannot be instantiated", cause.getMessage()); diff --git a/src/test/java/dev/toonformat/jtoon/encoder/FlattenTest.java b/src/test/java/dev/toonformat/jtoon/encoder/FlattenTest.java index c2d39ae..1515b74 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/FlattenTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/FlattenTest.java @@ -23,12 +23,15 @@ class FlattenTest { @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") void throwsOnConstructor() throws NoSuchMethodException { + // Given final Constructor constructor = Flatten.class.getDeclaredConstructor(); constructor.setAccessible(true); + // When final InvocationTargetException thrown = assertThrows(InvocationTargetException.class, constructor::newInstance); + // Then final Throwable cause = thrown.getCause(); assertInstanceOf(UnsupportedOperationException.class, cause); assertEquals("Utility class cannot be instantiated", cause.getMessage()); diff --git a/src/test/java/dev/toonformat/jtoon/encoder/HeaderFormatterTest.java b/src/test/java/dev/toonformat/jtoon/encoder/HeaderFormatterTest.java index 0d09a82..988c688 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/HeaderFormatterTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/HeaderFormatterTest.java @@ -29,28 +29,40 @@ class SimpleArrayHeaders { @Test @DisplayName("should format simple array header without key") void testSimpleArrayWithoutKey() { + // Given String result = HeaderFormatter.format(3, null, null, ",", false); + + // Then assertEquals("[3]:", result); } @Test @DisplayName("should format simple array header with key") void testSimpleArrayWithKey() { + // Given String result = HeaderFormatter.format(5, "items", null, ",", false); + + // Then assertEquals("items[5]:", result); } @Test @DisplayName("should format empty array") void testEmptyArray() { + // Given String result = HeaderFormatter.format(0, "items", null, ",", false); + + // Then assertEquals("items[0]:", result); } @Test @DisplayName("should format array with length marker") void testArrayWithLengthMarker() { + // Given String result = HeaderFormatter.format(3, "items", null, ",", true); + + // Then assertEquals("items[#3]:", result); } } @@ -62,40 +74,65 @@ class TabularHeaders { @Test @DisplayName("should format tabular header with fields") void testTabularHeader() { + // Given List fields = List.of("id", "name", "age"); + + // When String result = HeaderFormatter.format(2, "users", fields, ",", false); + + // Then assertEquals("users[2]{id,name,age}:", result); } @Test @DisplayName("should format tabular header with single field") void testSingleField() { + // Given List fields = List.of("value"); + + // When String result = HeaderFormatter.format(5, "data", fields, ",", false); + + // Then assertEquals("data[5]{value}:", result); } @Test @DisplayName("should format tabular header without key") void testTabularWithoutKey() { + // Given List fields = List.of("x", "y"); + + // When String result = HeaderFormatter.format(10, null, fields, ",", false); + + // Then assertEquals("[10]{x,y}:", result); } @Test @DisplayName("should format empty tabular header (no fields)") void testEmptyFields() { + // Given List fields = List.of(); + + // When String result = HeaderFormatter.format(3, "items", fields, ",", false); + + // Then assertEquals("items[3]:", result); } @Test @DisplayName("should format tabular header with length marker") void testTabularWithLengthMarker() { + // Given List fields = List.of("id", "name"); + + // When String result = HeaderFormatter.format(2, "users", fields, ",", true); + + // Then assertEquals("users[#2]{id,name}:", result); } } @@ -108,37 +145,53 @@ class DelimiterVariations { @MethodSource("delimiterTestData") @DisplayName("should format with different delimiters") void testDelimiterFormatting(String delimiterName, String delimiter, String expected) { + // Given List fields = List.of("a", "b", "c"); + + // When String result = HeaderFormatter.format(3, "data", fields, delimiter, false); + + // Then assertEquals(expected, result); } static Stream delimiterTestData() { return Stream.of( - Arguments.of("comma (implicit)", ",", "data[3]{a,b,c}:"), - Arguments.of("pipe (explicit)", "|", "data[3|]{a|b|c}:"), - Arguments.of("tab (explicit)", "\t", "data[3\t]{a\tb\tc}:")); + Arguments.of("comma (implicit)", ",", "data[3]{a,b,c}:"), + Arguments.of("pipe (explicit)", "|", "data[3|]{a|b|c}:"), + Arguments.of("tab (explicit)", "\t", "data[3\t]{a\tb\tc}:")); } @Test @DisplayName("should format array with pipe delimiter") void testArrayWithPipeDelimiter() { + // Given String result = HeaderFormatter.format(5, "items", null, "|", false); + + // Then assertEquals("items[5|]:", result); } @Test @DisplayName("should format array with tab delimiter") void testArrayWithTabDelimiter() { + // Given String result = HeaderFormatter.format(5, "items", null, "\t", false); + + // Then assertEquals("items[5\t]:", result); } @Test @DisplayName("should format with pipe delimiter and length marker") void testPipeWithLengthMarker() { + // Given List fields = List.of("x", "y"); + + // When String result = HeaderFormatter.format(2, "points", fields, "|", true); + + // Then assertEquals("points[#2|]{x|y}:", result); } } @@ -150,37 +203,56 @@ class KeyQuoting { @Test @DisplayName("should quote key with spaces") void testKeyWithSpaces() { + // Given String result = HeaderFormatter.format(3, "my items", null, ",", false); + + // Then assertEquals("\"my items\"[3]:", result); } @Test @DisplayName("should quote numeric key") void testNumericKey() { + // Given String result = HeaderFormatter.format(2, "123", null, ",", false); + + // Then assertEquals("\"123\"[2]:", result); } @Test @DisplayName("should not quote simple alphanumeric key") void testSimpleKey() { + // Given String result = HeaderFormatter.format(3, "items", null, ",", false); + + // Then assertEquals("items[3]:", result); } @Test @DisplayName("should quote field names with special characters") void testFieldQuoting() { + // Given List fields = List.of("first name", "last name"); + + // When String result = HeaderFormatter.format(2, "users", fields, ",", false); + + // Then assertEquals("users[2]{\"first name\",\"last name\"}:", result); } @Test @DisplayName("should handle mix of quoted and unquoted field names") void testMixedFieldQuoting() { + // Given List fields = List.of("id", "full name", "age"); + + // When String result = HeaderFormatter.format(2, "users", fields, ",", false); + + // Then assertEquals("users[2]{id,\"full name\",age}:", result); } } @@ -192,27 +264,41 @@ class RecordBasedFormat { @Test @DisplayName("should format using HeaderConfig record") void testRecordFormat() { + // Given HeaderFormatter.HeaderConfig config = new HeaderFormatter.HeaderConfig( - 3, "items", List.of("id", "name"), ",", false); + 3, "items", List.of("id", "name"), ",", false); + + // When String result = HeaderFormatter.format(config); + + // Then assertEquals("items[3]{id,name}:", result); } @Test @DisplayName("should format using record with null key") void testRecordWithNullKey() { + // Given HeaderFormatter.HeaderConfig config = new HeaderFormatter.HeaderConfig( - 5, null, null, ",", false); + 5, null, null, ",", false); + + // When String result = HeaderFormatter.format(config); + + // Then assertEquals("[5]:", result); } @Test @DisplayName("should format using record with pipe delimiter") void testRecordWithPipeDelimiter() { + // Given HeaderFormatter.HeaderConfig config = new HeaderFormatter.HeaderConfig( - 2, "data", List.of("x", "y"), "|", true); + 2, "data", List.of("x", "y"), "|", true); + // When String result = HeaderFormatter.format(config); + + // Then assertEquals("data[#2|]{x|y}:", result); } } @@ -224,30 +310,46 @@ class EdgeCases { @Test @DisplayName("should handle large array length") void testLargeLength() { + // Given String result = HeaderFormatter.format(999999, "data", null, ",", false); + + // Then assertEquals("data[999999]:", result); } @Test @DisplayName("should handle zero length with fields") void testZeroLengthWithFields() { + // Given List fields = List.of("id", "name"); + + // When String result = HeaderFormatter.format(0, "empty", fields, ",", false); + + // Then assertEquals("empty[0]{id,name}:", result); } @Test @DisplayName("should handle many fields") void testManyFields() { + // Given List fields = List.of("f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10"); + + // When String result = HeaderFormatter.format(1, "data", fields, ",", false); + + // Then assertEquals("data[1]{f1,f2,f3,f4,f5,f6,f7,f8,f9,f10}:", result); } @Test @DisplayName("should handle null fields list (treated as no fields)") void testNullFields() { + // Given String result = HeaderFormatter.format(3, "items", null, ",", false); + + // Then assertEquals("items[3]:", result); } } @@ -259,32 +361,52 @@ class RealWorldExamples { @Test @DisplayName("should format GitHub repositories header") void testGitHubRepos() { + // Given List fields = List.of("id", "name", "stars", "forks"); + + // When String result = HeaderFormatter.format(100, "repositories", fields, ",", false); + + // Then assertEquals("repositories[100]{id,name,stars,forks}:", result); } @Test @DisplayName("should format analytics metrics header") void testAnalyticsMetrics() { + // Given List fields = List.of("date", "views", "clicks", "conversions", "revenue"); + + // When String result = HeaderFormatter.format(180, "metrics", fields, ",", false); + + // Then assertEquals("metrics[180]{date,views,clicks,conversions,revenue}:", result); } @Test @DisplayName("should format employee records with tab delimiter") void testEmployeeRecords() { + // Given List fields = List.of("id", "name", "department", "salary"); + + // When String result = HeaderFormatter.format(50, "employees", fields, "\t", false); + + // Then assertEquals("employees[50\t]{id\tname\tdepartment\tsalary}:", result); } @Test @DisplayName("should format nested array in list item") void testNestedArray() { + // Given List fields = List.of("sku", "qty", "price"); + + // When String result = HeaderFormatter.format(3, "items", fields, ",", false); + + // Then assertEquals("items[3]{sku,qty,price}:", result); } } @@ -292,12 +414,15 @@ void testNestedArray() { @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") void throwsOnConstructor() throws NoSuchMethodException { + // Given final Constructor constructor = HeaderFormatter.class.getDeclaredConstructor(); constructor.setAccessible(true); + // When final InvocationTargetException thrown = assertThrows(InvocationTargetException.class, constructor::newInstance); + // Then final Throwable cause = thrown.getCause(); assertInstanceOf(UnsupportedOperationException.class, cause); assertEquals("Utility class cannot be instantiated", cause.getMessage()); diff --git a/src/test/java/dev/toonformat/jtoon/encoder/LineWriterTest.java b/src/test/java/dev/toonformat/jtoon/encoder/LineWriterTest.java index 77e1db8..45cd8c7 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/LineWriterTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/LineWriterTest.java @@ -7,14 +7,15 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Unit tests for LineWriter utility class. * Tests line accumulation and indentation logic for TOON format output. */ @Tag("unit") -public class LineWriterTest { +class LineWriterTest { @Nested @DisplayName("Basic Line Writing") @@ -23,25 +24,38 @@ class BasicLineWriting { @Test @DisplayName("should write single line at depth 0") void testSingleLine() { + // Given LineWriter writer = new LineWriter(2); + + // When writer.push(0, "hello"); + + // Then assertEquals("hello", writer.toString()); } @Test @DisplayName("should write multiple lines at depth 0") void testMultipleLines() { + // Given LineWriter writer = new LineWriter(2); + + // When writer.push(0, "line1"); writer.push(0, "line2"); writer.push(0, "line3"); + + // Then assertEquals("line1\nline2\nline3", writer.toString()); } @Test @DisplayName("should return empty string for no lines") void testNoLines() { + // Given LineWriter writer = new LineWriter(2); + + // Then assertEquals("", writer.toString()); } } @@ -53,24 +67,34 @@ class IndentationTwoSpaces { @ParameterizedTest @DisplayName("should indent correctly based on depth") @CsvSource({ - "1, ' content'", - "2, ' content'", - "3, ' content'" + "1, ' content'", + "2, ' content'", + "3, ' content'" }) void testIndentationByDepth(int depth, String expected) { + // Given LineWriter writer = new LineWriter(2); + + // When writer.push(depth, "content"); + + // Then assertEquals(expected, writer.toString()); } @Test @DisplayName("should handle mixed depths") void testMixedDepths() { + // Given LineWriter writer = new LineWriter(2); + + // When writer.push(0, "root"); writer.push(1, "level1"); writer.push(2, "level2"); writer.push(1, "level1again"); + + // Then assertEquals("root\n level1\n level2\n level1again", writer.toString()); } } @@ -82,27 +106,42 @@ class IndentationFourSpaces { @Test @DisplayName("should indent depth 1 with 4 spaces") void testDepth1() { + // Given LineWriter writer = new LineWriter(4); + + // When writer.push(1, "content"); + + // Then assertEquals(" content", writer.toString()); } @Test @DisplayName("should indent depth 2 with 8 spaces") void testDepth2() { + // Given LineWriter writer = new LineWriter(4); + + // When writer.push(2, "content"); + + // Then assertEquals(" content", writer.toString()); } @Test @DisplayName("should handle nested structure") void testNestedStructure() { + // Given LineWriter writer = new LineWriter(4); + + // When writer.push(0, "user:"); writer.push(1, "id: 1"); writer.push(1, "address:"); writer.push(2, "city: NYC"); + + // Then assertEquals("user:\n id: 1\n address:\n city: NYC", writer.toString()); } } @@ -114,22 +153,32 @@ class ContentTypes { @Test @DisplayName("should handle empty content") void testEmptyContent() { + // Given LineWriter writer = new LineWriter(2); + + // When writer.push(0, ""); + + // Then assertEquals("", writer.toString()); } @ParameterizedTest @DisplayName("should handle various content types") @CsvSource({ - "hello world", - "key: \"value\"", - "Hello δΈ–η•Œ", - "Hello 🌍" + "hello world", + "key: \"value\"", + "Hello δΈ–η•Œ", + "Hello 🌍" }) void testVariousContentTypes(String content) { + // Given LineWriter writer = new LineWriter(2); + + // When writer.push(0, content); + + // Then assertEquals(content, writer.toString()); } } @@ -141,62 +190,90 @@ class RealWorldStructures { @Test @DisplayName("should build simple object") void testSimpleObject() { + // Given LineWriter writer = new LineWriter(2); + + // When writer.push(0, "id: 123"); writer.push(0, "name: Ada"); writer.push(0, "active: true"); + + // Then assertEquals("id: 123\nname: Ada\nactive: true", writer.toString()); } @Test @DisplayName("should build nested object") void testNestedObject() { + // Given LineWriter writer = new LineWriter(2); + + // When writer.push(0, "user:"); writer.push(1, "id: 123"); writer.push(1, "name: Ada"); + + // Then assertEquals("user:\n id: 123\n name: Ada", writer.toString()); } @Test @DisplayName("should build array header with values") void testArrayWithValues() { + // Given LineWriter writer = new LineWriter(2); + + // When writer.push(0, "items[3]{id,name}:"); writer.push(1, "1,Alice"); writer.push(1, "2,Bob"); writer.push(1, "3,Charlie"); + + // Then assertEquals("items[3]{id,name}:\n 1,Alice\n 2,Bob\n 3,Charlie", writer.toString()); } @Test @DisplayName("should build list items") void testListItems() { + // Given LineWriter writer = new LineWriter(2); + + // When writer.push(0, "items[3]:"); writer.push(1, "- id: 1"); writer.push(2, "name: First"); writer.push(1, "- id: 2"); writer.push(2, "name: Second"); + + // Then assertEquals("items[3]:\n - id: 1\n name: First\n - id: 2\n name: Second", writer.toString()); } @Test @DisplayName("should build deeply nested structure") void testDeeplyNested() { + // Given LineWriter writer = new LineWriter(2); + + // When writer.push(0, "root:"); writer.push(1, "level1:"); writer.push(2, "level2:"); writer.push(3, "level3:"); writer.push(4, "value: deep"); + + // Then assertEquals("root:\n level1:\n level2:\n level3:\n value: deep", writer.toString()); } @Test @DisplayName("should build complex mixed structure") void testComplexMixedStructure() { + // Given LineWriter writer = new LineWriter(2); + + // When writer.push(0, "data:"); writer.push(1, "users[2]{id,name}:"); writer.push(2, "1,Alice"); @@ -205,14 +282,15 @@ void testComplexMixedStructure() { writer.push(1, "config:"); writer.push(2, "enabled: true"); + // Then String expected = """ - data: - users[2]{id,name}: - 1,Alice - 2,Bob - tags[3]: admin,user,guest - config: - enabled: true"""; + data: + users[2]{id,name}: + 1,Alice + 2,Bob + tags[3]: admin,user,guest + config: + enabled: true"""; assertEquals(expected, writer.toString()); } } @@ -224,45 +302,70 @@ class EdgeCases { @Test @DisplayName("should handle depth 0 correctly") void testDepthZero() { + // Given LineWriter writer = new LineWriter(2); + + // When writer.push(0, "content"); + + // Then assertEquals("content", writer.toString()); } @Test @DisplayName("should handle very deep nesting") void testVeryDeepNesting() { + // Given LineWriter writer = new LineWriter(2); + + // When writer.push(10, "deep"); + + // Then assertEquals(" deep", writer.toString()); } @Test @DisplayName("should handle indentation size 1") void testIndentSize1() { + // Given LineWriter writer = new LineWriter(1); + + // When writer.push(0, "root"); writer.push(1, "child"); writer.push(2, "grandchild"); + + // Then assertEquals("root\n child\n grandchild", writer.toString()); } @Test @DisplayName("should handle indentation size 8") void testIndentSize8() { + // Given LineWriter writer = new LineWriter(8); + + // When writer.push(0, "root"); writer.push(1, "child"); + + // Then assertEquals("root\n child", writer.toString()); } @Test @DisplayName("should handle many lines") void testManyLines() { + // Given LineWriter writer = new LineWriter(2); + + // When for (int i = 0; i < 100; i++) { writer.push(0, "line" + i); } + + // Then String result = writer.toString(); assertTrue(result.startsWith("line0\nline1\nline2")); assertTrue(result.endsWith("line98\nline99")); diff --git a/src/test/java/dev/toonformat/jtoon/encoder/ListItemEncoderTest.java b/src/test/java/dev/toonformat/jtoon/encoder/ListItemEncoderTest.java index f407f5c..8a962d2 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/ListItemEncoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/ListItemEncoderTest.java @@ -22,12 +22,15 @@ class ListItemEncoderTest { @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") void throwsOnConstructor() throws NoSuchMethodException { + // Given final Constructor constructor = ListItemEncoder.class.getDeclaredConstructor(); constructor.setAccessible(true); + // When final InvocationTargetException thrown = assertThrows(InvocationTargetException.class, constructor::newInstance); + // Then final Throwable cause = thrown.getCause(); assertInstanceOf(UnsupportedOperationException.class, cause); assertEquals("Utility class cannot be instantiated", cause.getMessage()); diff --git a/src/test/java/dev/toonformat/jtoon/encoder/ObjectEncoderTest.java b/src/test/java/dev/toonformat/jtoon/encoder/ObjectEncoderTest.java index a620721..f19634f 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/ObjectEncoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/ObjectEncoderTest.java @@ -1,13 +1,23 @@ package dev.toonformat.jtoon.encoder; import dev.toonformat.jtoon.EncodeOptions; +import dev.toonformat.jtoon.KeyFolding; +import dev.toonformat.jtoon.decoder.DecodeContext; +import dev.toonformat.jtoon.normalizer.JsonNormalizer; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.JsonNodeFactory; +import tools.jackson.databind.node.JsonNodeType; import tools.jackson.databind.node.ObjectNode; +import tools.jackson.databind.node.ValueNode; +import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -41,6 +51,38 @@ void givenSimpleObject_whenEncoding_thenOutputsCorrectLines() { assertEquals("x: 10", writer.toString()); } + @Test + void givenSimpleObject_withNullRootLiteralKeys_whenEncoding_thenOutputsCorrectLines() { + // Given + ObjectNode objectNode = MAPPER.createObjectNode(); + objectNode.put("x", 10); + + EncodeOptions options = EncodeOptions.DEFAULT; + LineWriter writer = new LineWriter(options.indent()); + + // When + ObjectEncoder.encodeObject(objectNode, writer, 0, options, null, null, null, new HashSet<>()); + + // Then + assertEquals("x: 10", writer.toString()); + } + + @Test + void givenSimpleObject_whenEncoding_thenOutputsInCorrectLines() { + // Given + ObjectNode objectNode = MAPPER.createObjectNode(); + objectNode.put("x", 10); + + EncodeOptions options = EncodeOptions.DEFAULT; + LineWriter writer = new LineWriter(options.indent()); + + // When + ObjectEncoder.encodeObject(objectNode, writer, 25, options, new HashSet<>(), null, null, new HashSet<>()); + + // Then + assertEquals(" x: 10", writer.toString()); + } + @Test @DisplayName("given fully-folded primitive leaf when flatten then writes inline value and returns null") void givenFullyFoldedPrimitiveLeaf_whenFlatten_thenWritesInlineAndReturnsNull() throws Exception { @@ -192,8 +234,8 @@ void givenFullyFoldedObjectLeaf_whenFlatten_thenWritesObjectAndReturnsNull() thr // Then assertNull(result); String expected = String.join("\n", - "user.info:", - " id: 1" + "user.info:", + " id: 1" ); assertEquals(expected, writer.toString()); assertTrue(blockedKeys.contains("user")); @@ -269,8 +311,8 @@ void givenNestedObjectAndFlattenOff_whenEncoding_thenWritesIndentedBlocks() { // Then assertEquals(""" - x: - y: ok""", writer.toString()); + x: + y: ok""", writer.toString()); } @Test @@ -308,9 +350,9 @@ void givenPartiallyFoldableKeyChain_whenRemainingDepthTooSmall_thenFlattenStops( // Then assertEquals(""" - a: - b: - z: 1""", writer.toString()); + a: + b: + z: 1""", writer.toString()); } @Test @@ -405,24 +447,27 @@ void givenPartiallyFoldedKeyChain_whenFoldResultHasRemainder_thenEncodesCase2Pat // Then assertEquals(""" - items[3]: - - summary - - id: 1 - name: Ada - - [2]: - - id: 2 - - status: draft""", writer.toString()); + items[3]: + - summary + - id: 1 + name: Ada + - [2]: + - id: 2 + - status: draft""", writer.toString()); } @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") void throwsOnConstructor() throws NoSuchMethodException { + // Given final Constructor constructor = ObjectEncoder.class.getDeclaredConstructor(); constructor.setAccessible(true); + // When final InvocationTargetException thrown = assertThrows(InvocationTargetException.class, constructor::newInstance); + // Then final Throwable cause = thrown.getCause(); assertInstanceOf(UnsupportedOperationException.class, cause); assertEquals("Utility class cannot be instantiated", cause.getMessage()); @@ -430,7 +475,7 @@ void throwsOnConstructor() throws NoSuchMethodException { @Test void givenPrimitiveLeaf_whenFlatten_thenWriterReceivesEncodedLine() throws Exception { - // given + // Given String key = "a"; EncodeOptions options = EncodeOptions.withFlatten(true); LineWriter writer = new LineWriter(options.indent()); @@ -459,7 +504,7 @@ void givenPrimitiveLeaf_whenFlatten_thenWriterReceivesEncodedLine() throws Excep ); flattenMethod.setAccessible(true); - // when + // When Object returnValue = flattenMethod.invoke( null, // static method key, @@ -473,7 +518,7 @@ void givenPrimitiveLeaf_whenFlatten_thenWriterReceivesEncodedLine() throws Excep 5 ); - // then + // Then assertNull(returnValue, "Expected null for fully folded primitive case"); assertEquals(1, writer.toString().lines().count(), "Writer should contain one line"); @@ -486,7 +531,7 @@ void givenPrimitiveLeaf_whenFlatten_thenWriterReceivesEncodedLine() throws Excep @Test void givenPartiallyFolded_whenFlatten_thenWriterReceivesFoldedKeyAndObjectIsEncoded() throws Exception { - // given + // Given String key = "a"; EncodeOptions options = EncodeOptions.withFlattenDepth(5); @@ -519,7 +564,7 @@ void givenPartiallyFolded_whenFlatten_thenWriterReceivesFoldedKeyAndObjectIsEnco ); flattenMethod.setAccessible(true); - // when + // When Object result = flattenMethod.invoke( null, // static key, // "a" @@ -533,7 +578,7 @@ void givenPartiallyFolded_whenFlatten_thenWriterReceivesFoldedKeyAndObjectIsEnco 1 // remainingDepth (will go to <=0, disable flattening) ); - // then + // Then assertNull(result); assertEquals(2, writer.toString().lines().count(), "Writer should contain two lines"); @@ -553,18 +598,18 @@ void usesListFormatForObjectsContainingArraysOfArrays() { EncodeOptions options = EncodeOptions.withFlatten(true); LineWriter writer = new LineWriter(options.indent()); - Set rootKeys = new HashSet<>(); + Set siblings = new HashSet<>(); // When - ObjectEncoder.encodeObject(node, writer, 0, options, rootKeys, null, null, new HashSet<>()); + ObjectEncoder.encodeObject(node, writer, 0, options, siblings, null, null, new HashSet<>()); // Then String expected = String.join("\n", - "items[1]:", - " - matrix[2]:", - " - [2]: 1,2", - " - [2]: 3,4", - " name: grid"); + "items[1]:", + " - matrix[2]:", + " - [2]: 1,2", + " - [2]: 3,4", + " name: grid"); assertEquals(expected, writer.toString()); } @@ -580,21 +625,44 @@ void testEncodeKeyValuePairWithAKey() { EncodeOptions options = EncodeOptions.withFlatten(true); LineWriter writer = new LineWriter(options.indent()); - Set rootKeys = new HashSet<>(); + Set siblings = new HashSet<>(); // When - ObjectEncoder.encodeKeyValuePair("items", node, writer, 0, options, rootKeys, null, null, 10, new HashSet<>()); + ObjectEncoder.encodeKeyValuePair("items", node, writer, 0, options, siblings, null, null, 10, new HashSet<>()); // Then String expected = String.join("\n", - "items:", - " items[1]:", - " - matrix[2]:", - " - [2]: 1,2", - " - [2]: 3,4", - " name: grid"); + "items:", + " items[1]:", + " - matrix[2]:", + " - [2]: 1,2", + " - [2]: 3,4", + " name: grid"); + assertEquals(expected, writer.toString()); + } + + @Test + void testEncodeKeyValuePairWithANullKey() { + // Given + String json = "{\n" + + " \"items\": [\n" + + " { \"matrix\": [[1, 2], [3, 4]], \"name\": \"grid\" }\n" + + " ]\n" + + " }"; + ObjectNode node = (ObjectNode) new ObjectMapper().readTree(json); + + EncodeOptions options = EncodeOptions.withFlatten(true); + LineWriter writer = new LineWriter(options.indent()); + Set siblings = new HashSet<>(); + + // When + ObjectEncoder.encodeKeyValuePair(null, node, writer, 0, options, siblings, null, null, 10, new HashSet<>()); + + // Then + String expected = ""; assertEquals(expected, writer.toString()); } + @Test void testEncodeKeyValuePairWithNullFlattenDepth() { // Given @@ -607,10 +675,10 @@ void testEncodeKeyValuePairWithNullFlattenDepth() { EncodeOptions options = EncodeOptions.withFlatten(true); LineWriter writer = new LineWriter(options.indent()); - Set rootKeys = new HashSet<>(); + Set siblings = new HashSet<>(); // When - ObjectEncoder.encodeKeyValuePair("items", node, writer, 0, options, rootKeys, null, null, null, new HashSet<>()); + ObjectEncoder.encodeKeyValuePair("items", node, writer, 0, options, siblings, null, null, null, new HashSet<>()); // Then String expected = String.join("\n", @@ -622,6 +690,7 @@ void testEncodeKeyValuePairWithNullFlattenDepth() { " name: grid"); assertEquals(expected, writer.toString()); } + @Test void testEncodeKeyValuePairWithToSmallFlattenDepth() { // Given @@ -634,10 +703,10 @@ void testEncodeKeyValuePairWithToSmallFlattenDepth() { EncodeOptions options = EncodeOptions.withFlatten(true); LineWriter writer = new LineWriter(options.indent()); - Set rootKeys = new HashSet<>(); + Set siblings = new HashSet<>(); // When - ObjectEncoder.encodeKeyValuePair("items", node, writer, 0, options, rootKeys, null, null, 0, new HashSet<>()); + ObjectEncoder.encodeKeyValuePair("items", node, writer, 0, options, siblings, null, null, 0, new HashSet<>()); // Then String expected = String.join("\n", @@ -662,11 +731,173 @@ void testEncodeKeyValuePairWithoutEmptySiblings() { siblings.add("world"); // When - ObjectEncoder.encodeKeyValuePair("", node, writer, 0, options, siblings, null, null, 10, new HashSet<>()); + ObjectEncoder.encodeKeyValuePair("items", node, writer, 0, options, siblings, null, null, null, new HashSet<>()); // Then assertFalse(writer.toString().trim().isEmpty()); //we only get a String with "" } + @Test + void testEncodeKeyValuePairWithKeyInBlockedKeysSet() { + // Given + String json = "{\n" + + " \"items\": [\n" + + " { \"matrix\": [[1, 2], [3, 4]], \"name\": \"grid\" }\n" + + " ]\n" + + " }"; + ObjectNode node = (ObjectNode) new ObjectMapper().readTree(json); + + EncodeOptions options = EncodeOptions.withFlatten(true); + LineWriter writer = new LineWriter(options.indent()); + Set siblings = Set.of("hello", "world"); + Set blockedKeys = Set.of("items"); + + // When + ObjectEncoder.encodeKeyValuePair("items", node, writer, 0, options, siblings, null, null, 10, blockedKeys); + + // Then + String expected = String.join("\n", + "items:", + " items[1]:", + " - matrix[2]:", + " - [2]: 1,2", + " - [2]: 3,4", + " name: grid"); + assertEquals(expected, writer.toString()); + } + + @Test + void testEncodeKeyValuePairWithoutFlattenWithAKey() { + // Given + String json = "{\n" + + " \"items\": [\n" + + " { \"matrix\": [[1, 2], [3, 4]], \"name\": \"grid\" }\n" + + " ]\n" + + " }"; + ObjectNode node = (ObjectNode) new ObjectMapper().readTree(json); + + EncodeOptions options = EncodeOptions.withFlatten(false); + LineWriter writer = new LineWriter(options.indent()); + Set siblings = new HashSet<>(); + + // When + ObjectEncoder.encodeKeyValuePair("items", node, writer, 0, options, siblings, null, null, 10, new HashSet<>()); + + // Then + String expected = String.join("\n", + "items:", + " items[1]:", + " - matrix[2]:", + " - [2]: 1,2", + " - [2]: 3,4", + " name: grid"); + assertEquals(expected, writer.toString()); + } + + @Test + void handleFullyFoldedLeafForObjectNodeAsLeaf() throws Exception { + // Given + ObjectNode objectLeaf = (ObjectNode) new ObjectMapper().readTree("{\"id\":1}"); + Flatten.FoldResult foldResult = new Flatten.FoldResult( + "user.info", + null, + objectLeaf, + 2 + ); + + EncodeOptions options = EncodeOptions.withFlatten(false); + LineWriter writer = new LineWriter(options.indent()); + + // When + invokePrivateStatic("handleFullyFoldedLeaf", new Class[]{Flatten.FoldResult.class, LineWriter.class, int.class, EncodeOptions.class, String.class}, foldResult, writer, 2, options, "item"); + + // Then + String expected = String.join("\n", " item:", + " id: 1"); + assertEquals(expected, writer.toString()); + } + + @Test + void handleFullyFoldedLeafForBokenNodeAsLeaf() throws Exception { + // Given + abstract class a extends JsonNode { + protected a() { + } + } + + + ObjectNode objectLeaf = (ObjectNode) new ObjectMapper().readTree("{\"id\":1}"); + Flatten.FoldResult foldResult = new Flatten.FoldResult( + "user.info", + null, + objectLeaf, + 2 + ); + + EncodeOptions options = EncodeOptions.withFlatten(false); + LineWriter writer = new LineWriter(options.indent()); + + // When + invokePrivateStatic("handleFullyFoldedLeaf", new Class[]{Flatten.FoldResult.class, LineWriter.class, int.class, EncodeOptions.class, String.class}, foldResult, writer, 2, options, "item"); + + // Then + String expected = String.join("\n", " item:", + " id: 1"); + assertEquals(expected, writer.toString()); + } + + @Test + void testingFlattenWithoutPathPrefix() throws Exception { + // Given + ObjectNode reminder = (ObjectNode) new ObjectMapper().readTree("{\"id\":2}"); + ObjectNode objectLeaf = (ObjectNode) new ObjectMapper().readTree("{\"id\":1}"); + Flatten.FoldResult foldResult = new Flatten.FoldResult( + "user.info", + reminder, + objectLeaf, + 2 + ); + + Set blockedKeys = new HashSet<>(); + + EncodeOptions options = EncodeOptions.withFlatten(false); + LineWriter writer = new LineWriter(options.indent()); + + // When + EncodeOptions expectedEncodeOptions = (EncodeOptions) invokePrivateStatic("flatten", new Class[]{String.class, Flatten.FoldResult.class, LineWriter.class, int.class, EncodeOptions.class, Set.class, String.class, Set.class, int.class}, "key", foldResult, writer, 2, options, Set.of(), null, blockedKeys, 3); + + // Then + assertNull(expectedEncodeOptions); + } + @Test + void testingFlattenWithPathPrefix() throws Exception { + // Given + ObjectNode reminder = (ObjectNode) new ObjectMapper().readTree("{\"id\":2}"); + ObjectNode objectLeaf = (ObjectNode) new ObjectMapper().readTree("{\"id\":1}"); + Flatten.FoldResult foldResult = new Flatten.FoldResult( + "user.info", + reminder, + objectLeaf, + 2 + ); + + Set blockedKeys = new HashSet<>(); + + EncodeOptions options = EncodeOptions.withFlatten(false); + LineWriter writer = new LineWriter(options.indent()); + + // When + EncodeOptions expectedEncodeOptions = (EncodeOptions) invokePrivateStatic("flatten", new Class[]{String.class, Flatten.FoldResult.class, LineWriter.class, int.class, EncodeOptions.class, Set.class, String.class, Set.class, int.class}, "key", foldResult, writer, 2, options, Set.of(), "user", blockedKeys, 3); + + // Then + assertNull(expectedEncodeOptions); + } + + // Reflection helpers for invoking private static methods + private static Object invokePrivateStatic(String methodName, Class[] paramTypes, Object... args) throws Exception { + Method declaredMethod = ObjectEncoder.class.getDeclaredMethod(methodName, paramTypes); + declaredMethod.setAccessible(true); + return declaredMethod.invoke(null, args); + } } diff --git a/src/test/java/dev/toonformat/jtoon/encoder/PrimitiveEncoderTest.java b/src/test/java/dev/toonformat/jtoon/encoder/PrimitiveEncoderTest.java index 433d241..ca905f2 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/PrimitiveEncoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/PrimitiveEncoderTest.java @@ -23,7 +23,7 @@ * Tests encoding of primitive values, keys, and header formatting. */ @Tag("unit") -public class PrimitiveEncoderTest { +class PrimitiveEncoderTest { @Nested @DisplayName("encodePrimitive - Booleans") @@ -32,14 +32,20 @@ class EncodePrimitiveBoolean { @Test @DisplayName("should encode true") void testTrue() { + // Given String result = PrimitiveEncoder.encodePrimitive(BooleanNode.TRUE, ","); + + // Then assertEquals("true", result); } @Test @DisplayName("should encode false") void testFalse() { + // Given String result = PrimitiveEncoder.encodePrimitive(BooleanNode.FALSE, ","); + + // Then assertEquals("false", result); } } @@ -51,49 +57,70 @@ class EncodePrimitiveNumber { @Test @DisplayName("should encode integer") void testInteger() { + // Given String result = PrimitiveEncoder.encodePrimitive(IntNode.valueOf(42), ","); + + // Then assertEquals("42", result); } @Test @DisplayName("should encode negative integer") void testNegativeInteger() { + // Given String result = PrimitiveEncoder.encodePrimitive(IntNode.valueOf(-100), ","); + + // Then assertEquals("-100", result); } @Test @DisplayName("should encode zero") void testZero() { + // Given String result = PrimitiveEncoder.encodePrimitive(IntNode.valueOf(0), ","); + + // Then assertEquals("0", result); } @Test @DisplayName("should encode long") void testLong() { + // Given String result = PrimitiveEncoder.encodePrimitive(LongNode.valueOf(9999999999L), ","); + + // Then assertEquals("9999999999", result); } @Test @DisplayName("should encode double") void testDouble() { + // Given String result = PrimitiveEncoder.encodePrimitive(DoubleNode.valueOf(3.14), ","); + + // Then assertEquals("3.14", result); } @Test @DisplayName("should encode float") void testFloat() { + // Given String result = PrimitiveEncoder.encodePrimitive(FloatNode.valueOf(2.5f), ","); + + // Then assertEquals("2.5", result); } @Test @DisplayName("should encode decimal node") void testDecimal() { + // Given String result = PrimitiveEncoder.encodePrimitive(DecimalNode.valueOf(new java.math.BigDecimal("123.456")), ","); + + // Then assertEquals("123.456", result); } } @@ -105,49 +132,70 @@ class EncodePrimitiveString { @Test @DisplayName("should encode simple string unquoted") void testSimpleString() { + // Given String result = PrimitiveEncoder.encodePrimitive(StringNode.valueOf("hello"), ","); + + // Then assertEquals("hello", result); } @Test @DisplayName("should quote string with comma when using comma delimiter") void testStringWithComma() { + //give String result = PrimitiveEncoder.encodePrimitive(StringNode.valueOf("a,b"), ","); + + // Then assertEquals("\"a,b\"", result); } @Test @DisplayName("should not quote string with comma when using pipe delimiter") void testStringWithCommaUsingPipe() { + // Given String result = PrimitiveEncoder.encodePrimitive(StringNode.valueOf("a,b"), "|"); + + // Then assertEquals("a,b", result); } @Test @DisplayName("should quote empty string") void testEmptyString() { + // Given String result = PrimitiveEncoder.encodePrimitive(StringNode.valueOf(""), ","); + + // Then assertEquals("\"\"", result); } @Test @DisplayName("should quote string that looks like boolean") void testBooleanLikeString() { + // Given String result = PrimitiveEncoder.encodePrimitive(StringNode.valueOf("true"), ","); + + // Then assertEquals("\"true\"", result); } @Test @DisplayName("should quote string that looks like null") void testNullLikeString() { + // Given String result = PrimitiveEncoder.encodePrimitive(StringNode.valueOf("null"), ","); + + // Then assertEquals("\"null\"", result); } @Test @DisplayName("should quote string that looks like number") void testNumberLikeString() { + // Given String result = PrimitiveEncoder.encodePrimitive(StringNode.valueOf("123"), ","); + + // Then assertEquals("\"123\"", result); } } @@ -159,7 +207,10 @@ class EncodePrimitiveNull { @Test @DisplayName("should encode null") void testNull() { + // Given String result = PrimitiveEncoder.encodePrimitive(NullNode.getInstance(), ","); + + // Then assertEquals("null", result); } } @@ -171,49 +222,70 @@ class EncodeStringLiteral { @Test @DisplayName("should encode simple string without quotes") void testSimpleString() { + // Given String result = PrimitiveEncoder.encodeStringLiteral("hello world", ","); + + // Then assertEquals("hello world", result); } @Test @DisplayName("should quote and escape string with quotes") void testStringWithQuotes() { + // Given String result = PrimitiveEncoder.encodeStringLiteral("say \"hi\"", ","); + + // Then assertEquals("\"say \\\"hi\\\"\"", result); } @Test @DisplayName("should quote string with leading space") void testLeadingSpace() { + // Given String result = PrimitiveEncoder.encodeStringLiteral(" hello", ","); + + // Then assertEquals("\" hello\"", result); } @Test @DisplayName("should quote string with trailing space") void testTrailingSpace() { + // Given String result = PrimitiveEncoder.encodeStringLiteral("hello ", ","); + + // Then assertEquals("\"hello \"", result); } @Test @DisplayName("should quote string with colon") void testColon() { + // Given String result = PrimitiveEncoder.encodeStringLiteral("key:value", ","); + + // Then assertEquals("\"key:value\"", result); } @Test @DisplayName("should quote string with active delimiter") void testDelimiter() { + // Given String result = PrimitiveEncoder.encodeStringLiteral("a,b,c", ","); + + // Then assertEquals("\"a,b,c\"", result); } @Test @DisplayName("should not quote string with inactive delimiter") void testInactiveDelimiter() { + // Given String result = PrimitiveEncoder.encodeStringLiteral("a|b|c", ","); + + // Then assertEquals("a|b|c", result); } } @@ -225,28 +297,40 @@ class EncodeKey { @Test @DisplayName("should encode simple key without quotes") void testSimpleKey() { + // Given String result = PrimitiveEncoder.encodeKey("name"); + + // Then assertEquals("name", result); } @Test @DisplayName("should encode key with underscores without quotes") void testKeyWithUnderscore() { + // Given String result = PrimitiveEncoder.encodeKey("user_name"); + + // Then assertEquals("user_name", result); } @Test @DisplayName("should encode key with dots without quotes") void testKeyWithDots() { + // Given String result = PrimitiveEncoder.encodeKey("com.example.key"); + + // Then assertEquals("com.example.key", result); } @Test @DisplayName("should quote key with spaces") void testKeyWithSpaces() { + // Given String result = PrimitiveEncoder.encodeKey("full name"); + + // Then assertEquals("\"full name\"", result); } @@ -267,14 +351,20 @@ void testKeyWithLeadingHyphen() { @Test @DisplayName("should quote empty key") void testEmptyKey() { + // Given String result = PrimitiveEncoder.encodeKey(""); + + // Then assertEquals("\"\"", result); } @Test @DisplayName("should quote key with special characters") void testKeyWithSpecialChars() { + // Given String result = PrimitiveEncoder.encodeKey("key:value"); + + // Then assertEquals("\"key:value\"", result); } } @@ -286,70 +376,105 @@ class JoinEncodedValues { @Test @DisplayName("should join primitive values with comma") void testJoinWithComma() { + // Given List values = List.of( - IntNode.valueOf(1), - StringNode.valueOf("hello"), - BooleanNode.TRUE); + IntNode.valueOf(1), + StringNode.valueOf("hello"), + BooleanNode.TRUE); + + // When String result = PrimitiveEncoder.joinEncodedValues(values, ","); + + // Then assertEquals("1,hello,true", result); } @Test @DisplayName("should join values with pipe delimiter") void testJoinWithPipe() { + // Given List values = List.of( - IntNode.valueOf(1), - StringNode.valueOf("test"), - IntNode.valueOf(2)); + IntNode.valueOf(1), + StringNode.valueOf("test"), + IntNode.valueOf(2)); + + // When String result = PrimitiveEncoder.joinEncodedValues(values, "|"); + + // Then assertEquals("1|test|2", result); } @Test @DisplayName("should join values with tab delimiter") void testJoinWithTab() { + // Given List values = List.of( - StringNode.valueOf("a"), - StringNode.valueOf("b"), - StringNode.valueOf("c")); + StringNode.valueOf("a"), + StringNode.valueOf("b"), + StringNode.valueOf("c")); + + // When String result = PrimitiveEncoder.joinEncodedValues(values, "\t"); + + // Then assertEquals("a\tb\tc", result); } @Test @DisplayName("should handle empty list") void testEmptyList() { + // Given List values = List.of(); + + // When String result = PrimitiveEncoder.joinEncodedValues(values, ","); + + // Then assertEquals("", result); } @Test @DisplayName("should handle single value") void testSingleValue() { + // Given List values = List.of(IntNode.valueOf(42)); + + // When String result = PrimitiveEncoder.joinEncodedValues(values, ","); + + // Then assertEquals("42", result); } @Test @DisplayName("should quote values containing delimiter") void testQuoteDelimiter() { + // Given List values = List.of( - StringNode.valueOf("a,b"), - StringNode.valueOf("c,d")); + StringNode.valueOf("a,b"), + StringNode.valueOf("c,d")); + + // When String result = PrimitiveEncoder.joinEncodedValues(values, ","); + + // Then assertEquals("\"a,b\",\"c,d\"", result); } @Test @DisplayName("should handle null values") void testNullValues() { + // Given List values = List.of( - IntNode.valueOf(1), - NullNode.getInstance(), - IntNode.valueOf(2)); + IntNode.valueOf(1), + NullNode.getInstance(), + IntNode.valueOf(2)); + + // When String result = PrimitiveEncoder.joinEncodedValues(values, ","); + + // Then assertEquals("1,null,2", result); } } @@ -361,37 +486,56 @@ class FormatHeader { @Test @DisplayName("should format simple array header") void testSimpleHeader() { + // Given String result = PrimitiveEncoder.formatHeader(5, "items", null, ",", false); + + // Then assertEquals("items[5]:", result); } @Test @DisplayName("should format tabular header") void testTabularHeader() { + // Given List fields = List.of("id", "name"); + + // When String result = PrimitiveEncoder.formatHeader(3, "users", fields, ",", false); + + // Then assertEquals("users[3]{id,name}:", result); } @Test @DisplayName("should format header with length marker") void testWithLengthMarker() { + // Given String result = PrimitiveEncoder.formatHeader(5, "data", null, ",", true); + + // Then assertEquals("data[#5]:", result); } @Test @DisplayName("should format header with pipe delimiter") void testPipeDelimiter() { + // Given List fields = List.of("x", "y"); + + // When String result = PrimitiveEncoder.formatHeader(2, "points", fields, "|", false); + + // Then assertEquals("points[2|]{x|y}:", result); } @Test @DisplayName("should format header without key") void testWithoutKey() { + // Given String result = PrimitiveEncoder.formatHeader(3, null, null, ",", false); + + // Then assertEquals("[3]:", result); } } @@ -403,34 +547,48 @@ class EdgeCasesIntegration { @Test @DisplayName("should handle Unicode in string encoding") void testUnicode() { + // Given String result = PrimitiveEncoder.encodePrimitive(StringNode.valueOf("Hello δΈ–η•Œ"), ","); + + // Then assertEquals("Hello δΈ–η•Œ", result); } @Test @DisplayName("should handle emoji in string encoding") void testEmoji() { + // Given String result = PrimitiveEncoder.encodePrimitive(StringNode.valueOf("Hello 🌍"), ","); + + // Then assertEquals("Hello 🌍", result); } @Test @DisplayName("should handle complex escaped string") void testComplexEscaping() { + // Given String result = PrimitiveEncoder.encodePrimitive(StringNode.valueOf("line1\nline2\ttab"), ","); + + // Then assertEquals("\"line1\\nline2\\ttab\"", result); } @Test @DisplayName("should join mixed type values correctly") void testMixedTypes() { + // Given List values = List.of( - IntNode.valueOf(123), - StringNode.valueOf("text"), - BooleanNode.FALSE, - NullNode.getInstance(), - DoubleNode.valueOf(3.14)); + IntNode.valueOf(123), + StringNode.valueOf("text"), + BooleanNode.FALSE, + NullNode.getInstance(), + DoubleNode.valueOf(3.14)); + + // When String result = PrimitiveEncoder.joinEncodedValues(values, ","); + + // Then assertEquals("123,text,false,null,3.14", result); } } diff --git a/src/test/java/dev/toonformat/jtoon/encoder/TabularArrayEncoderTest.java b/src/test/java/dev/toonformat/jtoon/encoder/TabularArrayEncoderTest.java index 00bd433..8b9d5e2 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/TabularArrayEncoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/TabularArrayEncoderTest.java @@ -13,7 +13,7 @@ import static org.junit.jupiter.api.Assertions.*; -public class TabularArrayEncoderTest { +class TabularArrayEncoderTest { private final JsonNodeFactory jsonNodeFactory = JsonNodeFactory.instance; private final EncodeOptions options = EncodeOptions.DEFAULT; diff --git a/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java b/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java index bfe98ae..ffb64c3 100644 --- a/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java +++ b/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java @@ -59,7 +59,10 @@ class NullAndJsonNode { @Test @DisplayName("should return NullNode for null input") void testNullInput() { + // Given JsonNode result = JsonNormalizer.normalize(null); + + // Then assertTrue(result.isNull()); assertInstanceOf(NullNode.class, result); } @@ -67,16 +70,37 @@ void testNullInput() { @Test @DisplayName("should pass through JsonNode unchanged") void testJsonNodePassthrough() { + // Given JsonNode textNode = StringNode.valueOf("test"); + + // When JsonNode result = JsonNormalizer.normalize(textNode); + + // Then assertSame(textNode, result); + } + @Test + void testJsonNodePassthrough2() { + // Given JsonNode intNode = IntNode.valueOf(42); - result = JsonNormalizer.normalize(intNode); + + // When + JsonNode result = JsonNormalizer.normalize(intNode); + + // Then assertSame(intNode, result); + } + @Test + void testJsonNodePassthrough3() { + // Given JsonNode boolNode = BooleanNode.TRUE; - result = JsonNormalizer.normalize(boolNode); + + // When + JsonNode result = JsonNormalizer.normalize(boolNode); + + // Then assertSame(boolNode, result); } } @@ -88,7 +112,10 @@ class PrimitiveTypes { @Test @DisplayName("should normalize String to StringNode") void testString() { + // Given JsonNode result = JsonNormalizer.normalize("hello"); + + // Then assertTrue(result.isString()); assertEquals("hello", result.asString()); assertInstanceOf(StringNode.class, result); @@ -97,7 +124,10 @@ void testString() { @Test @DisplayName("should normalize empty String to StringNode") void testEmptyString() { + // Given JsonNode result = JsonNormalizer.normalize(""); + + // Then assertTrue(result.isString()); assertEquals("", result.asString()); } @@ -105,12 +135,22 @@ void testEmptyString() { @Test @DisplayName("should normalize Boolean to BooleanNode") void testBoolean() { + // Given JsonNode resultTrue = JsonNormalizer.normalize(Boolean.TRUE); + + // Then assertTrue(resultTrue.isBoolean()); assertTrue(resultTrue.asBoolean()); assertInstanceOf(BooleanNode.class, resultTrue); + } + @Test + @DisplayName("should normalize Boolean to BooleanNode") + void testBoolean2() { + // Given JsonNode resultFalse = JsonNormalizer.normalize(Boolean.FALSE); + + // Then assertTrue(resultFalse.isBoolean()); assertFalse(resultFalse.asBoolean()); } @@ -118,7 +158,10 @@ void testBoolean() { @Test @DisplayName("should normalize Integer to IntNode") void testInteger() { + // Given JsonNode result = JsonNormalizer.normalize(42); + + // Then assertTrue(result.isInt()); assertEquals(42, result.asInt()); assertInstanceOf(IntNode.class, result); @@ -127,7 +170,10 @@ void testInteger() { @Test @DisplayName("should normalize Long to LongNode") void testLong() { + // Given JsonNode result = JsonNormalizer.normalize(9223372036854775807L); + + // Then assertTrue(result.isLong()); assertEquals(9223372036854775807L, result.asLong()); assertInstanceOf(LongNode.class, result); @@ -136,7 +182,10 @@ void testLong() { @Test @DisplayName("should normalize Short to ShortNode") void testShort() { + // Given JsonNode result = JsonNormalizer.normalize((short) 32767); + + // Then assertTrue(result.isShort()); assertEquals(32767, result.asInt()); assertInstanceOf(ShortNode.class, result); @@ -145,7 +194,10 @@ void testShort() { @Test @DisplayName("should normalize Byte to IntNode") void testByte() { + // Given JsonNode result = JsonNormalizer.normalize((byte) 127); + + // Then assertTrue(result.isInt()); assertEquals(127, result.asInt()); assertInstanceOf(IntNode.class, result); @@ -154,7 +206,10 @@ void testByte() { @Test @DisplayName("should normalize Float to FloatNode") void testFloat() { + // Given JsonNode result = JsonNormalizer.normalize(3.14f); + + // Then assertTrue(result.isFloat()); assertEquals(3.14f, result.floatValue(), 0.001); assertInstanceOf(FloatNode.class, result); @@ -163,7 +218,10 @@ void testFloat() { @Test @DisplayName("should normalize Double to DoubleNode") void testDouble() { + // Given JsonNode result = JsonNormalizer.normalize(3.14159); + + // Then assertTrue(result.isDouble()); assertEquals(3.14159, result.asDouble(), 0.00001); assertInstanceOf(DoubleNode.class, result); @@ -177,7 +235,10 @@ class SpecialDoubleCases { @Test @DisplayName("should convert NaN to NullNode") void testNaN() { + // Given JsonNode result = JsonNormalizer.normalize(Double.NaN); + + // Then assertTrue(result.isNull()); assertInstanceOf(NullNode.class, result); } @@ -185,7 +246,10 @@ void testNaN() { @Test @DisplayName("should convert positive Infinity to NullNode") void testPositiveInfinity() { + // Given JsonNode result = JsonNormalizer.normalize(Double.POSITIVE_INFINITY); + + // Then assertTrue(result.isNull()); assertInstanceOf(NullNode.class, result); } @@ -193,7 +257,10 @@ void testPositiveInfinity() { @Test @DisplayName("should convert negative Infinity to NullNode") void testNegativeInfinity() { + // Given JsonNode result = JsonNormalizer.normalize(Double.NEGATIVE_INFINITY); + + // Then assertTrue(result.isNull()); assertInstanceOf(NullNode.class, result); } @@ -201,7 +268,10 @@ void testNegativeInfinity() { @Test @DisplayName("should canonicalize -0.0 to IntNode(0)") void testNegativeZero() { + // Given JsonNode result = JsonNormalizer.normalize(-0.0); + + // Then assertTrue(result.isInt()); assertEquals(0, result.asInt()); assertInstanceOf(IntNode.class, result); @@ -210,7 +280,10 @@ void testNegativeZero() { @Test @DisplayName("should canonicalize +0.0 to IntNode(0)") void testPositiveZero() { + // Given JsonNode result = JsonNormalizer.normalize(0.0); + + // Then assertTrue(result.isInt()); assertEquals(0, result.asInt()); assertInstanceOf(IntNode.class, result); @@ -219,12 +292,21 @@ void testPositiveZero() { @Test @DisplayName("should convert whole numbers to LongNode when in range") void testWholeNumbers() { + // Given JsonNode result = JsonNormalizer.normalize(42.0); + // Then assertTrue(result.isIntegralNumber()); assertEquals(42, result.asLong()); assertInstanceOf(LongNode.class, result); + } + + @Test + @DisplayName("should convert whole numbers to LongNode when in range") + void testWholeNumbers2() { + // Given + JsonNode result = JsonNormalizer.normalize(1000000.0); - result = JsonNormalizer.normalize(1000000.0); + // Then assertTrue(result.isIntegralNumber()); assertEquals(1000000, result.asLong()); } @@ -232,7 +314,10 @@ void testWholeNumbers() { @Test @DisplayName("should keep regular decimals as DoubleNode") void testRegularDecimals() { + // Given JsonNode result = JsonNormalizer.normalize(3.14159); + + // Then assertTrue(result.isDouble()); assertEquals(3.14159, result.asDouble(), 0.00001); assertInstanceOf(DoubleNode.class, result); @@ -241,17 +326,30 @@ void testRegularDecimals() { @Test @DisplayName("should convert Float NaN to NullNode") void testFloatNaN() { + // Given JsonNode result = JsonNormalizer.normalize(Float.NaN); + + // Then assertTrue(result.isNull()); } @Test @DisplayName("should convert Float Infinity to NullNode") void testFloatInfinity() { + // Given JsonNode result = JsonNormalizer.normalize(Float.POSITIVE_INFINITY); + + // Then assertTrue(result.isNull()); + } + + @Test + @DisplayName("should convert Float Infinity to NullNode") + void testFloatInfinity2() { + // Given + JsonNode result = JsonNormalizer.normalize(Float.NEGATIVE_INFINITY); - result = JsonNormalizer.normalize(Float.NEGATIVE_INFINITY); + // Then assertTrue(result.isNull()); } } @@ -263,8 +361,13 @@ class BigNumbers { @Test @DisplayName("should convert BigInteger within Long range to LongNode") void testBigIntegerInRange() { + // Given BigInteger bigInt = BigInteger.valueOf(123456789L); + + // When JsonNode result = JsonNormalizer.normalize(bigInt); + + // Then assertTrue(result.isLong()); assertEquals(123456789L, result.asLong()); assertInstanceOf(LongNode.class, result); @@ -273,8 +376,13 @@ void testBigIntegerInRange() { @Test @DisplayName("should convert BigInteger at Long.MAX_VALUE to LongNode") void testBigIntegerAtMaxLong() { + // Given BigInteger bigInt = BigInteger.valueOf(Long.MAX_VALUE); + + // When JsonNode result = JsonNormalizer.normalize(bigInt); + + // Then assertTrue(result.isLong()); assertEquals(Long.MAX_VALUE, result.asLong()); } @@ -282,8 +390,13 @@ void testBigIntegerAtMaxLong() { @Test @DisplayName("should convert BigInteger at Long.MIN_VALUE to LongNode") void testBigIntegerAtMinLong() { + // Given BigInteger bigInt = BigInteger.valueOf(Long.MIN_VALUE); + + // When JsonNode result = JsonNormalizer.normalize(bigInt); + + // Then assertTrue(result.isLong()); assertEquals(Long.MIN_VALUE, result.asLong()); } @@ -291,8 +404,13 @@ void testBigIntegerAtMinLong() { @Test @DisplayName("should convert BigInteger outside Long range to StringNode") void testBigIntegerOutOfRange() { + // Given BigInteger bigInt = new BigInteger("99999999999999999999999999999999"); + + // When JsonNode result = JsonNormalizer.normalize(bigInt); + + // Then assertTrue(result.isString()); assertEquals("99999999999999999999999999999999", result.asString()); assertInstanceOf(StringNode.class, result); @@ -301,8 +419,13 @@ void testBigIntegerOutOfRange() { @Test @DisplayName("should convert BigDecimal to DecimalNode") void testBigDecimal() { + // Given BigDecimal bigDec = new BigDecimal("123.456"); + + // When JsonNode result = JsonNormalizer.normalize(bigDec); + + // Then assertTrue(result.isBigDecimal()); assertEquals(new BigDecimal("123.456"), result.decimalValue()); assertInstanceOf(DecimalNode.class, result); @@ -311,8 +434,13 @@ void testBigDecimal() { @Test @DisplayName("should convert large BigDecimal to DecimalNode") void testLargeBigDecimal() { + // Given BigDecimal bigDec = new BigDecimal("999999999999999999999.999999999999999999"); + + // When JsonNode result = JsonNormalizer.normalize(bigDec); + + // Then assertTrue(result.isBigDecimal()); assertInstanceOf(DecimalNode.class, result); } @@ -325,8 +453,13 @@ class TemporalTypes { @Test @DisplayName("should convert LocalDateTime to ISO formatted StringNode") void testLocalDateTime() { + // Given LocalDateTime dateTime = LocalDateTime.of(2023, 10, 15, 14, 30, 45); + + // When JsonNode result = JsonNormalizer.normalize(dateTime); + + // Then assertTrue(result.isString()); assertEquals("2023-10-15T14:30:45", result.asString()); } @@ -334,8 +467,13 @@ void testLocalDateTime() { @Test @DisplayName("should convert java.sql.Date to ISO formatted StringNode") void testSQLDate() { + // Given java.sql.Date dateTime = new java.sql.Date(1766419274); + + // When JsonNode result = JsonNormalizer.normalize(dateTime); + + // Then assertTrue(result.isString()); assertEquals("1970-01-21", result.asString()); } @@ -343,8 +481,13 @@ void testSQLDate() { @Test @DisplayName("should convert java.sql.Time to ISO formatted StringNode") void testSQLTime() { + // Given java.sql.Time time = new java.sql.Time(1766419274); + + // When JsonNode result = JsonNormalizer.normalize(time); + + // Then assertTrue(result.isString()); String expected = time.toLocalTime().format(DateTimeFormatter.ISO_LOCAL_TIME); assertEquals(expected, result.asString()); @@ -353,8 +496,13 @@ void testSQLTime() { @Test @DisplayName("should convert java.sql.Timestamp to ISO formatted StringNode") void testSQLTimeStamp() { + // Given java.sql.Timestamp dateTime = new java.sql.Timestamp(1766419274); + + // When JsonNode result = JsonNormalizer.normalize(dateTime); + + // Then assertTrue(result.isString()); String expected = dateTime.toLocalDateTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); assertEquals(expected, result.asString()); @@ -364,8 +512,13 @@ void testSQLTimeStamp() { @Test @DisplayName("should convert LocalDate to ISO formatted StringNode") void testLocalDate() { + // Given LocalDate date = LocalDate.of(2023, 10, 15); + + // When JsonNode result = JsonNormalizer.normalize(date); + + // Then assertTrue(result.isString()); assertEquals("2023-10-15", result.asString()); } @@ -373,8 +526,13 @@ void testLocalDate() { @Test @DisplayName("should convert LocalTime to ISO formatted StringNode") void testLocalTime() { + // Given LocalTime time = LocalTime.of(14, 30, 45); + + // When JsonNode result = JsonNormalizer.normalize(time); + + // Then assertTrue(result.isString()); assertEquals("14:30:45", result.asString()); } @@ -382,8 +540,13 @@ void testLocalTime() { @Test @DisplayName("should convert ZonedDateTime to ISO formatted StringNode") void testZonedDateTime() { + // Given ZonedDateTime zonedDateTime = ZonedDateTime.of(2023, 10, 15, 14, 30, 45, 0, ZoneId.of("UTC")); + + // When JsonNode result = JsonNormalizer.normalize(zonedDateTime); + + // Then assertTrue(result.isString()); assertTrue(result.asString().startsWith("2023-10-15T14:30:45")); } @@ -391,8 +554,13 @@ void testZonedDateTime() { @Test @DisplayName("should convert OffsetDateTime to ISO formatted StringNode") void testOffsetDateTime() { + // Given OffsetDateTime offsetDateTime = OffsetDateTime.of(2023, 10, 15, 14, 30, 45, 0, ZoneOffset.UTC); + + // When JsonNode result = JsonNormalizer.normalize(offsetDateTime); + + // Then assertTrue(result.isString()); assertEquals("2023-10-15T14:30:45Z", result.asString()); } @@ -400,8 +568,13 @@ void testOffsetDateTime() { @Test @DisplayName("should convert Instant to ISO formatted StringNode") void testInstant() { + // Given Instant instant = Instant.parse("2023-10-15T14:30:45.123Z"); + + // When JsonNode result = JsonNormalizer.normalize(instant); + + // Then assertTrue(result.isString()); assertEquals("2023-10-15T14:30:45.123Z", result.asString()); } @@ -409,8 +582,13 @@ void testInstant() { @Test @DisplayName("should convert java.util.Date to ISO formatted StringNode") void testUtilDate() { + // Given Date date = Date.from(Instant.parse("2023-10-15T14:30:45.123Z")); + + // When JsonNode result = JsonNormalizer.normalize(date); + + // Then assertTrue(result.isString()); assertEquals("2023-10-15", result.asString()); } @@ -423,8 +601,13 @@ class Collections { @Test @DisplayName("should convert List to ArrayNode") void testList() { + // Given List list = List.of(1, 2, 3, "four"); + + // When JsonNode result = JsonNormalizer.normalize(list); + + // Then assertTrue(result.isArray()); assertEquals(4, result.size()); assertEquals(1, result.get(0).asInt()); @@ -436,8 +619,13 @@ void testList() { @Test @DisplayName("should convert empty List to empty ArrayNode") void testEmptyList() { + // Given List list = List.of(); + + // When JsonNode result = JsonNormalizer.normalize(list); + + // Then assertTrue(result.isArray()); assertEquals(0, result.size()); } @@ -445,8 +633,13 @@ void testEmptyList() { @Test @DisplayName("should convert Set to ArrayNode") void testSet() { + // Given Set set = new LinkedHashSet<>(List.of(1, 2, 3)); + + // When JsonNode result = JsonNormalizer.normalize(set); + + // Then assertTrue(result.isArray()); assertEquals(3, result.size()); } @@ -454,12 +647,16 @@ void testSet() { @Test @DisplayName("should convert Map to ObjectNode") void testMap() { + // Given Map map = new LinkedHashMap<>(); map.put("name", "John"); map.put("age", 30); map.put("active", true); + // When JsonNode result = JsonNormalizer.normalize(map); + + // Then assertTrue(result.isObject()); assertEquals(3, result.size()); assertEquals("John", result.get("name").asString()); @@ -470,8 +667,13 @@ void testMap() { @Test @DisplayName("should convert empty Map to empty ObjectNode") void testEmptyMap() { + // Given Map map = new HashMap<>(); + + // When JsonNode result = JsonNormalizer.normalize(map); + + // Then assertTrue(result.isObject()); assertEquals(0, result.size()); } @@ -479,11 +681,15 @@ void testEmptyMap() { @Test @DisplayName("should handle nested collections") void testNestedCollections() { + // Given Map map = new LinkedHashMap<>(); map.put("numbers", List.of(1, 2, 3)); map.put("nested", Map.of("key", "value")); + // When JsonNode result = JsonNormalizer.normalize(map); + + // Then assertTrue(result.isObject()); assertTrue(result.get("numbers").isArray()); assertEquals(3, result.get("numbers").size()); @@ -494,11 +700,15 @@ void testNestedCollections() { @Test @DisplayName("should convert non-String Map keys to String") void testMapWithNonStringKeys() { + // Given Map map = new HashMap<>(); map.put(1, "one"); map.put(2, "two"); + // When JsonNode result = JsonNormalizer.normalize(map); + + // Then assertTrue(result.isObject()); assertEquals("one", result.get("1").asString()); assertEquals("two", result.get("2").asString()); @@ -507,8 +717,13 @@ void testMapWithNonStringKeys() { @Test @DisplayName("should handle collections with null values") void testCollectionWithNulls() { + // Given List list = java.util.Arrays.asList(1, null, 3); + + // When JsonNode result = JsonNormalizer.normalize(list); + + // Then assertTrue(result.isArray()); assertEquals(3, result.size()); assertEquals(1, result.get(0).asInt()); @@ -524,8 +739,13 @@ class Arrays { @Test @DisplayName("should convert int[] to ArrayNode") void testIntArray() { + // Given int[] array = {1, 2, 3, 4, 5}; + + // When JsonNode result = JsonNormalizer.normalize(array); + + // Then assertTrue(result.isArray()); assertEquals(5, result.size()); assertEquals(1, result.get(0).asInt()); @@ -535,8 +755,13 @@ void testIntArray() { @Test @DisplayName("should convert long[] to ArrayNode") void testLongArray() { + // Given long[] array = {1L, 2L, 9223372036854775807L}; + + // When JsonNode result = JsonNormalizer.normalize(array); + + // Then assertTrue(result.isArray()); assertEquals(3, result.size()); assertEquals(9223372036854775807L, result.get(2).asLong()); @@ -545,8 +770,13 @@ void testLongArray() { @Test @DisplayName("should convert double[] to ArrayNode") void testDoubleArray() { + // Given double[] array = {1.1, 2.2, 3.3}; + + // When JsonNode result = JsonNormalizer.normalize(array); + + // Then assertTrue(result.isArray()); assertEquals(3, result.size()); assertEquals(1.1, result.get(0).asDouble(), 0.001); @@ -555,8 +785,13 @@ void testDoubleArray() { @Test @DisplayName("should convert double[] with special values to ArrayNode with nulls") void testDoubleArrayWithSpecialValues() { + // Given double[] array = {1.0, Double.NaN, Double.POSITIVE_INFINITY, 4.0, Double.NEGATIVE_INFINITY}; + + // When JsonNode result = JsonNormalizer.normalize(array); + + // Then assertTrue(result.isArray()); assertEquals(5, result.size()); assertEquals(1, result.get(0).asInt()); @@ -569,8 +804,13 @@ void testDoubleArrayWithSpecialValues() { @Test @DisplayName("should convert float[] to ArrayNode") void testFloatArray() { + // Given float[] array = {1.1f, 2.2f, 3.3f}; + + // When JsonNode result = JsonNormalizer.normalize(array); + + // Then assertTrue(result.isArray()); assertEquals(3, result.size()); assertEquals(1.1f, result.get(0).floatValue(), 0.001); @@ -579,8 +819,13 @@ void testFloatArray() { @Test @DisplayName("should convert float[] with special values to ArrayNode with nulls") void testFloatArrayWithSpecialValues() { + // Given float[] array = {1.0f, Float.NaN, Float.POSITIVE_INFINITY}; + + // When JsonNode result = JsonNormalizer.normalize(array); + + // Then assertTrue(result.isArray()); assertEquals(3, result.size()); assertEquals(1.0f, result.get(0).floatValue(), 0.001); @@ -591,8 +836,13 @@ void testFloatArrayWithSpecialValues() { @Test @DisplayName("should convert boolean[] to ArrayNode") void testBooleanArray() { + // Given boolean[] array = {true, false, true}; + + // When JsonNode result = JsonNormalizer.normalize(array); + + // Then assertTrue(result.isArray()); assertEquals(3, result.size()); assertTrue(result.get(0).asBoolean()); @@ -602,8 +852,13 @@ void testBooleanArray() { @Test @DisplayName("should convert byte[] to ArrayNode") void testByteArray() { + // Given byte[] array = {1, 2, 127}; + + // When JsonNode result = JsonNormalizer.normalize(array); + + // Then assertTrue(result.isArray()); assertEquals(3, result.size()); assertEquals(127, result.get(2).asInt()); @@ -612,8 +867,13 @@ void testByteArray() { @Test @DisplayName("should convert short[] to ArrayNode") void testShortArray() { + // Given short[] array = {1, 2, 32767}; + + // When JsonNode result = JsonNormalizer.normalize(array); + + // Then assertTrue(result.isArray()); assertEquals(3, result.size()); assertEquals(32767, result.get(2).asInt()); @@ -622,8 +882,13 @@ void testShortArray() { @Test @DisplayName("should convert char[] to ArrayNode of strings") void testCharArray() { + // Given char[] array = {'a', 'b', 'c'}; + + // When JsonNode result = JsonNormalizer.normalize(array); + + // Then assertTrue(result.isArray()); assertEquals(3, result.size()); assertEquals("a", result.get(0).asString()); @@ -634,8 +899,13 @@ void testCharArray() { @Test @DisplayName("should convert Object[] to ArrayNode") void testObjectArray() { + // Given Object[] array = {1, "two", true, 3.14}; + + // When JsonNode result = JsonNormalizer.normalize(array); + + // Then assertTrue(result.isArray()); assertEquals(4, result.size()); assertEquals(1, result.get(0).asInt()); @@ -647,13 +917,27 @@ void testObjectArray() { @Test @DisplayName("should convert empty arrays to empty ArrayNode") void testEmptyArrays() { + // Given int[] intArray = {}; + + // When JsonNode result = JsonNormalizer.normalize(intArray); + + // Then assertTrue(result.isArray()); assertEquals(0, result.size()); + } + @Test + @DisplayName("should convert empty arrays to empty ArrayNode") + void testEmptyArraysOfObjects() { + // Given Object[] objArray = {}; - result = JsonNormalizer.normalize(objArray); + + // When + JsonNode result = JsonNormalizer.normalize(objArray); + + // Then assertTrue(result.isArray()); assertEquals(0, result.size()); } @@ -661,11 +945,16 @@ void testEmptyArrays() { @Test @DisplayName("should handle nested arrays") void testNestedArrays() { + // Given Object[] array = { new int[]{1, 2}, new String[]{"a", "b"} }; + + // When JsonNode result = JsonNormalizer.normalize(array); + + // Then assertTrue(result.isArray()); assertEquals(2, result.size()); assertTrue(result.get(0).isArray()); @@ -682,21 +971,40 @@ class SpecialTypes { @Test @DisplayName("should convert Optional.empty() to NullNode") void testEmptyOptional() { + // Given Optional optional = Optional.empty(); + + // When JsonNode result = JsonNormalizer.normalize(optional); + + // Then assertTrue(result.isNull()); } @Test @DisplayName("should unwrap Optional.of(value)") void testOptionalWithValue() { + // Given Optional optional = Optional.of("hello"); + + // When JsonNode result = JsonNormalizer.normalize(optional); + + // Then assertTrue(result.isString()); assertEquals("hello", result.asString()); + } + @Test + @DisplayName("should unwrap Optional.of(value)") + void testOptionalWithValue2() { + // Given Optional intOptional = Optional.of(42); - result = JsonNormalizer.normalize(intOptional); + + // When + JsonNode result = JsonNormalizer.normalize(intOptional); + + // Then assertTrue(result.isInt()); assertEquals(42, result.asInt()); } @@ -704,8 +1012,13 @@ void testOptionalWithValue() { @Test @DisplayName("should unwrap nested Optional") void testNestedOptional() { + // Given Optional> nested = Optional.of(Optional.of("nested")); + + // When JsonNode result = JsonNormalizer.normalize(nested); + + // Then assertTrue(result.isString()); assertEquals("nested", result.asString()); } @@ -713,8 +1026,13 @@ void testNestedOptional() { @Test @DisplayName("should convert Stream to ArrayNode") void testStream() { + // Given Stream stream = Stream.of(1, 2, 3, 4, 5); + + // When JsonNode result = JsonNormalizer.normalize(stream); + + // Then assertTrue(result.isArray()); assertEquals(5, result.size()); assertEquals(1, result.get(0).asInt()); @@ -724,8 +1042,13 @@ void testStream() { @Test @DisplayName("should convert empty Stream to empty ArrayNode") void testEmptyStream() { + // Given Stream stream = Stream.empty(); + + // When JsonNode result = JsonNormalizer.normalize(stream); + + // Then assertTrue(result.isArray()); assertEquals(0, result.size()); } @@ -733,8 +1056,13 @@ void testEmptyStream() { @Test @DisplayName("should handle Stream with null values") void testStreamWithNulls() { + // Given Stream stream = Stream.of(1, null, 3); + + // When JsonNode result = JsonNormalizer.normalize(stream); + + // Then assertTrue(result.isArray()); assertEquals(3, result.size()); assertEquals(1, result.get(0).asInt()); @@ -763,8 +1091,13 @@ record PojoWithGetters(String value) { @Test @DisplayName("should convert simple POJO to ObjectNode") void testSimplePojo() { + // Given SimplePojo pojo = new SimplePojo("Alice", 25); + + // When JsonNode result = JsonNormalizer.normalize(pojo); + + // Then assertTrue(result.isObject()); assertEquals("Alice", result.get("name").asString()); assertEquals(25, result.get("age").asInt()); @@ -773,8 +1106,13 @@ void testSimplePojo() { @Test @DisplayName("should convert POJO with getters to ObjectNode") void testPojoWithGetters() { + // Given PojoWithGetters pojo = new PojoWithGetters("test"); + + // When JsonNode result = JsonNormalizer.normalize(pojo); + + // Then assertTrue(result.isObject()); assertEquals("test", result.get("value").asString()); } @@ -782,11 +1120,15 @@ void testPojoWithGetters() { @Test @DisplayName("should handle nested POJOs") void testNestedPojo() { + // Given Map map = new LinkedHashMap<>(); map.put("pojo", new SimplePojo("Bob", 30)); map.put("id", 123); + // When JsonNode result = JsonNormalizer.normalize(map); + + // Then assertTrue(result.isObject()); assertTrue(result.get("pojo").isObject()); assertEquals("Bob", result.get("pojo").get("name").asString()); @@ -796,11 +1138,16 @@ void testNestedPojo() { @Test @DisplayName("should handle collections of POJOs") void testCollectionOfPojos() { + // Given List pojos = List.of( new SimplePojo("Alice", 25), new SimplePojo("Bob", 30) ); + + // When JsonNode result = JsonNormalizer.normalize(pojos); + + // Then assertTrue(result.isArray()); assertEquals(2, result.size()); assertEquals("Alice", result.get(0).get("name").asString()); @@ -810,9 +1157,14 @@ void testCollectionOfPojos() { @Test @DisplayName("should convert non-serializable objects to NullNode") void testNonSerializableObject() { + // Given // Thread is not easily serializable by Jackson Thread thread = new Thread(); + + // When JsonNode result = JsonNormalizer.normalize(thread); + + // Then // Jackson may succeed or fail depending on version // Just verify it doesn't throw an exception assertNotNull(result); @@ -826,17 +1178,22 @@ class EdgeCases { @Test @DisplayName("should handle deeply nested structures") void testDeeplyNested() { + // Given Map level3 = Map.of("value", 42); Map level2 = Map.of("level3", level3); Map level1 = Map.of("level2", level2); + // When JsonNode result = JsonNormalizer.normalize(level1); + + // Then assertEquals(42, result.get("level2").get("level3").get("value").asInt()); } @Test @DisplayName("should handle mixed types in collections") void testMixedTypes() { + // Given List mixed = java.util.Arrays.asList( 1, "text", @@ -846,7 +1203,11 @@ void testMixedTypes() { Map.of("key", "value"), null ); + + // When JsonNode result = JsonNormalizer.normalize(mixed); + + // Then assertTrue(result.isArray()); assertEquals(7, result.size()); assertEquals(1, result.get(0).asInt()); @@ -861,16 +1222,26 @@ void testMixedTypes() { @Test @DisplayName("should handle Optional with null value") void testOptionalOfNull() { + // Given Optional optional = Optional.empty(); + + // When JsonNode result = JsonNormalizer.normalize(optional); + + // Then assertTrue(result.isNull()); } @Test @DisplayName("should handle arrays containing arrays") void testArrayOfArrays() { + // Given int[][] matrix = {{1, 2}, {3, 4}}; + + // When JsonNode result = JsonNormalizer.normalize(matrix); + + // Then assertTrue(result.isArray()); assertEquals(2, result.size()); assertTrue(result.get(0).isArray()); @@ -881,11 +1252,15 @@ void testArrayOfArrays() { @Test @DisplayName("should handle Map with null values") void testMapWithNullValues() { + // Given Map map = new LinkedHashMap<>(); map.put("key1", "value"); map.put("key2", null); + // When JsonNode result = JsonNormalizer.normalize(map); + + // Then assertTrue(result.isObject()); assertEquals("value", result.get("key1").asString()); assertTrue(result.get("key2").isNull()); @@ -895,12 +1270,15 @@ void testMapWithNullValues() { @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") void throwsOnConstructor() throws NoSuchMethodException { + // Given final Constructor constructor = JsonNormalizer.class.getDeclaredConstructor(); constructor.setAccessible(true); + // When final InvocationTargetException thrown = assertThrows(InvocationTargetException.class, constructor::newInstance); + // Then final Throwable cause = thrown.getCause(); assertInstanceOf(UnsupportedOperationException.class, cause); assertEquals("Utility class cannot be instantiated", cause.getMessage()); @@ -908,9 +1286,9 @@ void throwsOnConstructor() throws NoSuchMethodException { // Reflection helpers for invoking private static methods private static Object invokePrivateStatic(String methodName, Class[] paramTypes, Object... args) throws Exception { - Method m = JsonNormalizer.class.getDeclaredMethod(methodName, paramTypes); - m.setAccessible(true); - return m.invoke(null, args); + Method declaredMethod = JsonNormalizer.class.getDeclaredMethod(methodName, paramTypes); + declaredMethod.setAccessible(true); + return declaredMethod.invoke(null, args); } @@ -1319,6 +1697,48 @@ void testIntegerValueReturnsOptional_whenTryConvertToLong() throws Exception { assertFalse(((Optional) result).isEmpty()); } + @Test + @DisplayName("Given Long Max Value, When tryConvertToLong is called, Then Optional is returned") + void testLongMaxValueReturnsOptional_whenTryConvertToLong() throws Exception { + // Given + Double input = (double) Long.MAX_VALUE + 1; + + // When + Object result = invokePrivateStatic("tryConvertToLong", new Class[]{Double.class}, input); + + // Then + assertInstanceOf(Optional.class, result); + assertFalse(((Optional) result).isEmpty()); + } + + @Test + @DisplayName("Given Long Min Value, When tryConvertToLong is called, Then Optional is returned") + void testLongMinValueReturnsOptional_whenTryConvertToLong() throws Exception { + // Given + Double input = (double) Long.MIN_VALUE - 1; + + // When + Object result = invokePrivateStatic("tryConvertToLong", new Class[]{Double.class}, input); + + // Then + assertInstanceOf(Optional.class, result); + assertFalse(((Optional) result).isEmpty()); + } + + @Test + @DisplayName("Given Long Min Value, When tryConvertToLong is called, Then Optional is returned") + void testLongNormalizeBigInteger() throws Exception { + // Given + BigInteger input = BigInteger.valueOf(Long.MIN_VALUE - 1); + + // When + Object result = invokePrivateStatic("normalizeBigInteger", new Class[]{BigInteger.class}, input); + + // Then + assertInstanceOf(JsonNode.class, result); + assertFalse(((JsonNode) result).isBigDecimal()); + } + @Test @DisplayName("Given negative NonInteger, When tryConvertToLong is called, Then Optional.empty is returned") void testNegativeNonIntegerValueReturnsEmptyWhenTryConvertToLong() throws Exception { @@ -1446,7 +1866,7 @@ class NormalizeArray { @Test @DisplayName("Given Object, When normalizeArray is called, Then ArrayNode get return") - void givenException_whenTryNormalizePojo_thenNullNode() throws Exception { + void NormalizeArray_thenNullNode() throws Exception { // Given Object input = new Object(); @@ -1455,9 +1875,76 @@ void givenException_whenTryNormalizePojo_thenNullNode() throws Exception { // Then assertInstanceOf(ArrayNode.class, result); + } + } + + @Nested + @DisplayName("NormalizePojo") + class NormalizePojo { + class ExplodingPojo { + public String getValue() { + throw new RuntimeException("Boom"); + } + } + + @Test + @DisplayName("Given Object, When tryNormalizePojo is called, Then ArrayNode get return") + void tryNormalizePojo_thenNullNode() throws Exception { + // Given + Object input = new Object(); + // When + Object result = invokePrivateStatic("tryNormalizePojo", new Class[]{Object.class}, input); + // Then + assertInstanceOf(ObjectNode.class, result); } + + @Test + void returnsNullNodeWhenJacksonExceptionOccurs() throws Exception { + // Given + Object input = new ExplodingPojo(); + + // When + Object result = invokePrivateStatic("tryNormalizePojo", new Class[]{Object.class}, input); + + // Then + assertInstanceOf(NullNode.class, result); + } + } + + @Nested + @DisplayName("parse") + class parse { + @Test + void parseNullAsString() { + // Given + String input = null; + + // When + final IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> JsonNormalizer.parse(input)); + + + // Then + assertEquals("Invalid JSON", thrown.getMessage()); + } + + + @Test + void parseEmptyString() { + // Given + String input = " "; + + // When + final IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> JsonNormalizer.parse(input)); + + + // Then + assertEquals("Invalid JSON", thrown.getMessage()); + } + } } diff --git a/src/test/java/dev/toonformat/jtoon/util/ConstantsTest.java b/src/test/java/dev/toonformat/jtoon/util/ConstantsTest.java index 83deb6c..358cf18 100644 --- a/src/test/java/dev/toonformat/jtoon/util/ConstantsTest.java +++ b/src/test/java/dev/toonformat/jtoon/util/ConstantsTest.java @@ -18,12 +18,15 @@ public class ConstantsTest { @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") void throwsOnConstructor() throws NoSuchMethodException { + // Given final Constructor constructor = Constants.class.getDeclaredConstructor(); constructor.setAccessible(true); + // When final InvocationTargetException thrown = assertThrows(InvocationTargetException.class, constructor::newInstance); + // Then final Throwable cause = thrown.getCause(); assertInstanceOf(UnsupportedOperationException.class, cause); assertEquals("Utility class cannot be instantiated", cause.getMessage()); diff --git a/src/test/java/dev/toonformat/jtoon/util/ObjectMapperSingletonTest.java b/src/test/java/dev/toonformat/jtoon/util/ObjectMapperSingletonTest.java index 9f574fe..3600baf 100644 --- a/src/test/java/dev/toonformat/jtoon/util/ObjectMapperSingletonTest.java +++ b/src/test/java/dev/toonformat/jtoon/util/ObjectMapperSingletonTest.java @@ -20,12 +20,15 @@ class ObjectMapperSingletonTest { @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") void throwsOnConstructor() throws NoSuchMethodException { + // Given final Constructor constructor = ObjectMapperSingleton.class.getDeclaredConstructor(); constructor.setAccessible(true); + // When final InvocationTargetException thrown = assertThrows(InvocationTargetException.class, constructor::newInstance); + // Then final Throwable cause = thrown.getCause(); assertInstanceOf(UnsupportedOperationException.class, cause); assertEquals("Utility class cannot be instantiated", cause.getMessage()); diff --git a/src/test/java/dev/toonformat/jtoon/util/StringEscaperTest.java b/src/test/java/dev/toonformat/jtoon/util/StringEscaperTest.java index b4d81f7..68a7b1b 100644 --- a/src/test/java/dev/toonformat/jtoon/util/StringEscaperTest.java +++ b/src/test/java/dev/toonformat/jtoon/util/StringEscaperTest.java @@ -28,17 +28,18 @@ class BasicEscaping { static Stream basicEscapingCases() { return Stream.of( - Arguments.of("backslashes", "path\\to\\file", "path\\\\to\\\\file"), - Arguments.of("double quotes", "He said \"hello\"", "He said \\\"hello\\\""), - Arguments.of("newlines", "line1\nline2", "line1\\nline2"), - Arguments.of("carriage returns", "line1\rline2", "line1\\rline2"), - Arguments.of("tabs", "col1\tcol2", "col1\\tcol2")); + Arguments.of("backslashes", "path\\to\\file", "path\\\\to\\\\file"), + Arguments.of("double quotes", "He said \"hello\"", "He said \\\"hello\\\""), + Arguments.of("newlines", "line1\nline2", "line1\\nline2"), + Arguments.of("carriage returns", "line1\rline2", "line1\\rline2"), + Arguments.of("tabs", "col1\tcol2", "col1\\tcol2")); } @ParameterizedTest(name = "should escape {0}") @MethodSource("basicEscapingCases") @DisplayName("should escape basic special characters") void testBasicEscaping(String description, String input, String expected) { + // Then assertEquals(expected, StringEscaper.escape(input)); } } @@ -49,16 +50,17 @@ class CombinedEscaping { static Stream combinedEscapingCases() { return Stream.of( - Arguments.of("multiple special characters", "He said \"test\\path\"\nNext line", - "He said \\\"test\\\\path\\\"\\nNext line"), - Arguments.of("all control characters together", "text\n\r\t", "text\\n\\r\\t"), - Arguments.of("backslash before quote", "\\\"", "\\\\\\\"")); + Arguments.of("multiple special characters", "He said \"test\\path\"\nNext line", + "He said \\\"test\\\\path\\\"\\nNext line"), + Arguments.of("all control characters together", "text\n\r\t", "text\\n\\r\\t"), + Arguments.of("backslash before quote", "\\\"", "\\\\\\\"")); } @ParameterizedTest(name = "should escape {0}") @MethodSource("combinedEscapingCases") @DisplayName("should escape combined special characters") void testCombinedEscaping(String description, String input, String expected) { + // Then assertEquals(expected, StringEscaper.escape(input)); } } @@ -70,25 +72,30 @@ class EdgeCases { @Test @DisplayName("should return empty string for empty input") void testEmptyString() { + // Then assertEquals("", StringEscaper.escape("")); } @ParameterizedTest @DisplayName("should not modify strings without special characters") @ValueSource(strings = { - "hello world", - "Hello World 123 @#$%^&*()_+-=[]{}|;:',.<>?/", - "Hello δΈ–η•Œ 🌍" + "hello world", + "Hello World 123 @#$%^&*()_+-=[]{}|;:',.<>?/", + "Hello δΈ–η•Œ 🌍" }) void testStringsWithoutSpecialCharacters(String input) { + // Then assertEquals(input, StringEscaper.escape(input)); } @Test @DisplayName("should handle consecutive backslashes") void testConsecutiveBackslashes() { + // Given String input = "\\\\\\"; String expected = "\\\\\\\\\\\\"; + + // Then assertEquals(expected, StringEscaper.escape(input)); } } @@ -99,19 +106,20 @@ class RealWorldScenarios { static Stream realWorldScenarios() { return Stream.of( - Arguments.of("JSON string", "{\"key\": \"value\"}", "{\\\"key\\\": \\\"value\\\"}"), - Arguments.of("Windows file path", "C:\\Users\\Documents\\file.txt", - "C:\\\\Users\\\\Documents\\\\file.txt"), - Arguments.of("multi-line text", "Line 1\nLine 2\nLine 3", "Line 1\\nLine 2\\nLine 3"), - Arguments.of("SQL query", "SELECT * FROM users WHERE name = \"John\"", - "SELECT * FROM users WHERE name = \\\"John\\\""), - Arguments.of("regex pattern", "\\d+\\.\\d+", "\\\\d+\\\\.\\\\d+")); + Arguments.of("JSON string", "{\"key\": \"value\"}", "{\\\"key\\\": \\\"value\\\"}"), + Arguments.of("Windows file path", "C:\\Users\\Documents\\file.txt", + "C:\\\\Users\\\\Documents\\\\file.txt"), + Arguments.of("multi-line text", "Line 1\nLine 2\nLine 3", "Line 1\\nLine 2\\nLine 3"), + Arguments.of("SQL query", "SELECT * FROM users WHERE name = \"John\"", + "SELECT * FROM users WHERE name = \\\"John\\\""), + Arguments.of("regex pattern", "\\d+\\.\\d+", "\\\\d+\\\\.\\\\d+")); } @ParameterizedTest(name = "should escape {0}") @MethodSource("realWorldScenarios") @DisplayName("should escape real-world scenarios") void testRealWorldScenarios(String scenario, String input, String expected) { + // Then assertEquals(expected, StringEscaper.escape(input)); } } @@ -122,17 +130,18 @@ class BasicUnescaping { static Stream basicUnescapingCases() { return Stream.of( - Arguments.of("backslashes", "path\\\\to\\\\file", "path\\to\\file"), - Arguments.of("double quotes", "He said \\\"hello\\\"", "He said \"hello\""), - Arguments.of("newlines", "line1\\nline2", "line1\nline2"), - Arguments.of("carriage returns", "line1\\rline2", "line1\rline2"), - Arguments.of("tabs", "col1\\tcol2", "col1\tcol2")); + Arguments.of("backslashes", "path\\\\to\\\\file", "path\\to\\file"), + Arguments.of("double quotes", "He said \\\"hello\\\"", "He said \"hello\""), + Arguments.of("newlines", "line1\\nline2", "line1\nline2"), + Arguments.of("carriage returns", "line1\\rline2", "line1\rline2"), + Arguments.of("tabs", "col1\\tcol2", "col1\tcol2")); } @ParameterizedTest(name = "should unescape {0}") @MethodSource("basicUnescapingCases") @DisplayName("should unescape basic special characters") void testBasicUnescaping(String description, String input, String expected) { + // Then assertEquals(expected, StringEscaper.unescape(input)); } } @@ -144,24 +153,28 @@ class QuoteRemoval { @Test @DisplayName("should remove surrounding quotes") void testQuoteRemoval() { + // Then assertEquals("hello", StringEscaper.unescape("\"hello\"")); } @Test @DisplayName("should handle quotes with escaped content") void testQuotedEscapedContent() { + // Then assertEquals("hello\nworld", StringEscaper.unescape("\"hello\\nworld\"")); } @Test @DisplayName("should not remove quotes if not surrounding") void testNonSurroundingQuotes() { + // Then assertEquals("hello\"world", StringEscaper.unescape("hello\"world")); } @Test @DisplayName("should handle empty quoted string") void testEmptyQuotedString() { + // Then assertEquals("", StringEscaper.unescape("\"\"")); } } @@ -172,13 +185,13 @@ class RoundTripEscaping { static Stream roundTripCases() { return Stream.of( - "simple text", - "path\\to\\file", - "He said \"hello\"", - "line1\nline2\nline3", - "col1\tcol2\tcol3", - "C:\\Users\\Documents", - "text\n\r\t\"\\" + "simple text", + "path\\to\\file", + "He said \"hello\"", + "line1\nline2\nline3", + "col1\tcol2\tcol3", + "C:\\Users\\Documents", + "text\n\r\t\"\\" ); } @@ -186,8 +199,11 @@ static Stream roundTripCases() { @DisplayName("should preserve content through escape/unescape cycle") @MethodSource("roundTripCases") void testRoundTrip(String original) { + // Given String escaped = StringEscaper.escape(original); String unescaped = StringEscaper.unescape("\"" + escaped + "\""); + + // Then assertEquals(original, unescaped); } } @@ -199,55 +215,66 @@ class UnescapeEdgeCases { @Test @DisplayName("should handle null input") void testNullInput() { + // Then assertNull(StringEscaper.unescape(null)); } @Test @DisplayName("should handle empty string") void testEmptyString() { + // Then assertEquals("", StringEscaper.unescape("")); } @Test @DisplayName("should handle single character") void testSingleCharacter() { + // Then assertEquals("a", StringEscaper.unescape("a")); } @Test @DisplayName("should handle strings without escape sequences") void testNoEscapeSequences() { + // Then assertEquals("hello world", StringEscaper.unescape("hello world")); } @Test @DisplayName("should handle unknown escape sequences as literals") void testUnknownEscapeSequences() { + // Then assertEquals("ax", StringEscaper.unescape("\\ax")); } @Test void unquotesValueWhenStartsAndEndsWithQuote() { + // Then assertEquals("abc", StringEscaper.unescape("\"abc\"")); } + @Test void unescapesBackslashSequences() { + // Then assertEquals("a\"b", StringEscaper.unescape("a\\\"b")); } @Test void unescapesMultipleCharacters() { + // Then assertEquals("a\nb\tc", StringEscaper.unescape("a\\nb\\tc")); } @Test void handlesTrailingBackslashCorrectly() { + // Then // trailing \ will set escaped=true but there is no next char β†’ nothing appended assertEquals("abc", StringEscaper.unescape("abc\\")); } @Test void handlesDoubleBackslashCorrectly() { + // Then assertEquals("a\\b", StringEscaper.unescape("a\\\\b")); } } @@ -255,12 +282,15 @@ void handlesDoubleBackslashCorrectly() { @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") void throwsOnConstructor() throws NoSuchMethodException { + // Given final Constructor constructor = StringEscaper.class.getDeclaredConstructor(); constructor.setAccessible(true); + // When final InvocationTargetException thrown = - assertThrows(InvocationTargetException.class, constructor::newInstance); + assertThrows(InvocationTargetException.class, constructor::newInstance); + // Then final Throwable cause = thrown.getCause(); assertInstanceOf(UnsupportedOperationException.class, cause); assertEquals("Utility class cannot be instantiated", cause.getMessage()); @@ -275,6 +305,7 @@ void testingValidateString_WithNull() { // Then } + @Test void testingValidateString_WithEmptyString() { // Given @@ -284,14 +315,43 @@ void testingValidateString_WithEmptyString() { // Then } + @Test void testingValidateString_WithWildStringToThrowsException() { // Given String input = "\"te\\st\""; // When // Then - assertThrows(IllegalArgumentException.class, - ()->{ + final IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, + () -> { StringEscaper.validateString(input); }); + + assertEquals("Invalid escape sequence: \\s", thrown.getMessage()); + } + + @Test + void testingValidateString_WithWildStringOnlyAtTheStartToThrowsException() { + // Given + String input = "\"te\\st"; + // When // Then + final IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, + () -> { + StringEscaper.validateString(input); + }); + + assertEquals("Unterminated string", thrown.getMessage()); + } + + @Test + void testingValidateString_WithWildStringOnlyAtTheStartAndEndToThrowsException() { + // Given + String input = "\"abc\\\""; + // When // Then + final IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, + () -> { + StringEscaper.validateString(input); + }); + + assertEquals("Invalid escape sequence: trailing backslash", thrown.getMessage()); } } diff --git a/src/test/java/dev/toonformat/jtoon/util/StringValidatorTest.java b/src/test/java/dev/toonformat/jtoon/util/StringValidatorTest.java index 93dc4d9..02df727 100644 --- a/src/test/java/dev/toonformat/jtoon/util/StringValidatorTest.java +++ b/src/test/java/dev/toonformat/jtoon/util/StringValidatorTest.java @@ -8,11 +8,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; /** * Unit tests for StringValidator utility class. @@ -28,36 +24,42 @@ class IsSafeUnquotedBasic { @Test @DisplayName("should return false for null") void testNullValue() { + // Then assertFalse(StringValidator.isSafeUnquoted(null, ",")); } @Test @DisplayName("should return false for empty string") void testEmptyString() { + // Then assertFalse(StringValidator.isSafeUnquoted("", ",")); } @Test @DisplayName("should return true for simple alphanumeric string") void testSimpleString() { + // Then assertTrue(StringValidator.isSafeUnquoted("hello123", ",")); } @Test @DisplayName("should return true for string with spaces") void testStringWithInnerSpaces() { + // Then assertTrue(StringValidator.isSafeUnquoted("hello world", ",")); } @Test @DisplayName("should return false for a number") void testNumber() { + // Then assertFalse(StringValidator.isSafeUnquoted("123456", ",")); } @Test @DisplayName("should return false for a Scientific Notation number") void testScientificNumber() { + // Then assertFalse(StringValidator.isSafeUnquoted("-2.5E-8", ",")); assertFalse(StringValidator.isSafeUnquoted("1e10", ",")); } @@ -65,17 +67,21 @@ void testScientificNumber() { @Test @DisplayName("should return false for a octal number") void testOctalNumber() { + // Then assertFalse(StringValidator.isSafeUnquoted("07", ",")); } @Test @DisplayName("should return false for a number with a leading zero") void testLeadingZeroNumber() { + // Then assertFalse(StringValidator.isSafeUnquoted("0.07", ",")); } + @Test @DisplayName("should return false for a negative number with a leading zero") void testLeadingNegativeZeroNumber() { + // Then assertFalse(StringValidator.isSafeUnquoted("-0.07", ",")); } } @@ -87,24 +93,28 @@ class WhitespacePadding { @Test @DisplayName("should return false for leading space") void testLeadingSpace() { + // Then assertFalse(StringValidator.isSafeUnquoted(" hello", ",")); } @Test @DisplayName("should return false for trailing space") void testTrailingSpace() { + // Then assertFalse(StringValidator.isSafeUnquoted("hello ", ",")); } @Test @DisplayName("should return false for both leading and trailing spaces") void testBothSpaces() { + // Then assertFalse(StringValidator.isSafeUnquoted(" hello ", ",")); } @Test @DisplayName("should return false for only spaces") void testOnlySpaces() { + // Then assertFalse(StringValidator.isSafeUnquoted(" ", ",")); } } @@ -116,24 +126,28 @@ class Keywords { @Test @DisplayName("should return false for 'true'") void testTrueKeyword() { + // Then assertFalse(StringValidator.isSafeUnquoted("true", ",")); } @Test @DisplayName("should return false for 'false'") void testFalseKeyword() { + // Then assertFalse(StringValidator.isSafeUnquoted("false", ",")); } @Test @DisplayName("should return false for 'null'") void testNullKeyword() { + // Then assertFalse(StringValidator.isSafeUnquoted("null", ",")); } @Test @DisplayName("should return true for 'True' (case sensitive)") void testTrueCaseSensitive() { + // Then assertTrue(StringValidator.isSafeUnquoted("True", ",")); } } @@ -145,24 +159,28 @@ class Numbers { @Test @DisplayName("should return false for integer") void testInteger() { + // Then assertFalse(StringValidator.isSafeUnquoted("123", ",")); } @Test @DisplayName("should return false for negative integer") void testNegativeInteger() { + // Then assertFalse(StringValidator.isSafeUnquoted("-456", ",")); } @Test @DisplayName("should return false for decimal") void testDecimal() { + // Then assertFalse(StringValidator.isSafeUnquoted("3.14", ",")); } @Test @DisplayName("should return false for scientific notation") void testScientificNotation() { + // Then assertFalse(StringValidator.isSafeUnquoted("1.5e10", ",")); assertFalse(StringValidator.isSafeUnquoted("1.5E-10", ",")); } @@ -170,6 +188,7 @@ void testScientificNotation() { @Test @DisplayName("should return false for octal-like numbers") void testOctalNumber() { + // Then assertFalse(StringValidator.isSafeUnquoted("0123", ",")); assertFalse(StringValidator.isSafeUnquoted("0777", ",")); } @@ -177,6 +196,7 @@ void testOctalNumber() { @Test @DisplayName("should return true for text starting with number") void testTextStartingWithNumber() { + // Then assertTrue(StringValidator.isSafeUnquoted("123abc", ",")); } } @@ -188,30 +208,35 @@ class SpecialCharacters { @Test @DisplayName("should return false for string with colon") void testColon() { + // Then assertFalse(StringValidator.isSafeUnquoted("key:value", ",")); } @Test @DisplayName("should return false for string with double quote") void testDoubleQuote() { + // Then assertFalse(StringValidator.isSafeUnquoted("say \"hi\"", ",")); } @Test @DisplayName("should return false for string with backslash") void testBackslash() { + // Then assertFalse(StringValidator.isSafeUnquoted("path\\file", ",")); } @Test @DisplayName("should return false for string with brackets") void testBrackets() { + // Then assertFalse(StringValidator.isSafeUnquoted("array[0]", ",")); } @Test @DisplayName("should return false for string with braces") void testBraces() { + // Then assertFalse(StringValidator.isSafeUnquoted("obj{key}", ",")); } } @@ -223,18 +248,21 @@ class ControlCharacters { @Test @DisplayName("should return false for newline") void testNewline() { + // Then assertFalse(StringValidator.isSafeUnquoted("line1\nline2", ",")); } @Test @DisplayName("should return false for carriage return") void testCarriageReturn() { + // Then assertFalse(StringValidator.isSafeUnquoted("line1\rline2", ",")); } @Test @DisplayName("should return false for tab") void testTab() { + // Then assertFalse(StringValidator.isSafeUnquoted("col1\tcol2", ",")); } } @@ -246,36 +274,42 @@ class DelimiterAware { @Test @DisplayName("should return false for comma with comma delimiter") void testCommaDelimiter() { + // Then assertFalse(StringValidator.isSafeUnquoted("a,b", ",")); } @Test @DisplayName("should return true for comma with pipe delimiter") void testCommaWithPipeDelimiter() { + // Then assertTrue(StringValidator.isSafeUnquoted("a,b", "|")); } @Test @DisplayName("should return false for pipe with pipe delimiter") void testPipeDelimiter() { + // Then assertFalse(StringValidator.isSafeUnquoted("a|b", "|")); } @Test @DisplayName("should return true for pipe with comma delimiter") void testPipeWithCommaDelimiter() { + // Then assertTrue(StringValidator.isSafeUnquoted("a|b", ",")); } @Test @DisplayName("should return false for tab with tab delimiter") void testTabDelimiter() { + // Then assertFalse(StringValidator.isSafeUnquoted("a\tb", "\t")); } @Test @DisplayName("should return true for tab with comma delimiter") void testTabWithCommaDelimiter() { + // Then assertFalse(StringValidator.isSafeUnquoted("a\tb", ",")); // Still false due to control char } } @@ -287,18 +321,21 @@ class ListMarker { @Test @DisplayName("should return false for string starting with list marker") void testListMarker() { + // Then assertFalse(StringValidator.isSafeUnquoted("- item", ",")); } @Test @DisplayName("should return true for string containing but not starting with dash-space") void testDashSpaceInMiddle() { + // Then assertTrue(StringValidator.isSafeUnquoted("item - note", ",")); } @Test @DisplayName("should return false for string starting with dash") void testDashWithoutSpace() { + // Then assertFalse(StringValidator.isSafeUnquoted("-item", ",")); } } @@ -310,24 +347,28 @@ class SafeStrings { @Test @DisplayName("should return true for alphanumeric with underscores") void testAlphanumericUnderscore() { + // Then assertTrue(StringValidator.isSafeUnquoted("hello_world_123", ",")); } @Test @DisplayName("should return true for string with hyphens") void testHyphens() { + // Then assertTrue(StringValidator.isSafeUnquoted("hello-world", ",")); } @Test @DisplayName("should return true for string with dots") void testDots() { + // Then assertTrue(StringValidator.isSafeUnquoted("hello.world", ",")); } @Test @DisplayName("should return true for Unicode characters") void testUnicode() { + // Then assertTrue(StringValidator.isSafeUnquoted("Hello δΈ–η•Œ", ",")); } @@ -345,12 +386,14 @@ class ValidUnquotedKey { @Test @DisplayName("should return true for simple alphanumeric key") void testSimpleKey() { + // Then assertTrue(StringValidator.isValidUnquotedKey("key")); } @Test @DisplayName("should return true for key with underscores") void testKeyWithUnderscore() { + // Then assertTrue(StringValidator.isValidUnquotedKey("my_key")); assertTrue(StringValidator.isValidUnquotedKey("_private")); } @@ -358,18 +401,21 @@ void testKeyWithUnderscore() { @Test @DisplayName("should return true for key with dots") void testKeyWithDots() { + // Then assertTrue(StringValidator.isValidUnquotedKey("com.example.key")); } @Test @DisplayName("should return true for key with numbers") void testKeyWithNumbers() { + // Then assertTrue(StringValidator.isValidUnquotedKey("key123")); } @Test @DisplayName("should return true for uppercase keys") void testUppercaseKey() { + // Then assertTrue(StringValidator.isValidUnquotedKey("KEY")); assertTrue(StringValidator.isValidUnquotedKey("MyKey")); } @@ -377,12 +423,14 @@ void testUppercaseKey() { @Test @DisplayName("should return false for key starting with number") void testKeyStartingWithNumber() { + // Then assertFalse(StringValidator.isValidUnquotedKey("123key")); } @Test @DisplayName("should return false for key with spaces") void testKeyWithSpaces() { + // Then assertFalse(StringValidator.isValidUnquotedKey("my key")); } @@ -395,6 +443,7 @@ void testKeyWithHyphen() { @Test @DisplayName("should return false for key with special characters") void testKeyWithSpecialChars() { + // Then assertFalse(StringValidator.isValidUnquotedKey("key:value")); assertFalse(StringValidator.isValidUnquotedKey("key,value")); assertFalse(StringValidator.isValidUnquotedKey("key[0]")); @@ -403,6 +452,7 @@ void testKeyWithSpecialChars() { @Test @DisplayName("should return false for empty key") void testEmptyKey() { + // Then assertFalse(StringValidator.isValidUnquotedKey("")); } } @@ -410,12 +460,15 @@ void testEmptyKey() { @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") void throwsOnConstructor() throws NoSuchMethodException { + // Given final Constructor constructor = StringValidator.class.getDeclaredConstructor(); constructor.setAccessible(true); + // When final InvocationTargetException thrown = - assertThrows(InvocationTargetException.class, constructor::newInstance); + assertThrows(InvocationTargetException.class, constructor::newInstance); + // Then final Throwable cause = thrown.getCause(); assertInstanceOf(UnsupportedOperationException.class, cause); assertEquals("Utility class cannot be instantiated", cause.getMessage()); @@ -423,26 +476,31 @@ void throwsOnConstructor() throws NoSuchMethodException { @Test void returnsFalseForStringWithoutQuotesOrBackslash() { + // Then assertFalse(StringValidator.containsQuotesOrBackslash("abc")); } @Test void detectsDoubleQuote() { + // Then assertTrue(StringValidator.containsQuotesOrBackslash("a\"b")); } @Test void detectsBackslash() { + // Then assertTrue(StringValidator.containsQuotesOrBackslash("a\\b")); } @Test void detectsBoth() { + // Then assertTrue(StringValidator.containsQuotesOrBackslash("x\"y\\z")); } @Test void detectsQuoteAtStart() { + // Then assertTrue(StringValidator.containsQuotesOrBackslash("\"abc")); assertTrue(StringValidator.containsQuotesOrBackslash("\\abc")); assertTrue(StringValidator.containsQuotesOrBackslash("x\"y\\z")); @@ -450,11 +508,13 @@ void detectsQuoteAtStart() { @Test void detectsBackslashAtEnd() { + // Then assertTrue(StringValidator.containsQuotesOrBackslash("abc\\")); } @Test void emptyStringReturnsFalse() { + // Then assertFalse(StringValidator.containsQuotesOrBackslash("")); } }