diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..71f0f29 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto +*.cs text=lf \ No newline at end of file diff --git a/.specignore b/.specignore new file mode 100644 index 0000000..a22a118 --- /dev/null +++ b/.specignore @@ -0,0 +1,23 @@ +# reason: .net doesn't support hyphens in property/field names (full-name) +skips folding when segment requires quotes (safe mode) + +# reason: .net doesn't support . syntax for properties/fields (data.meta.items) +skips folding on sibling literal-key collision (safe mode) + +# reason: .net doesn't support special chars or colons/brackets/spaces in property/field names or spaces (order:id full name) +encodes tabular arrays with keys needing quotes +quotes key with colon +quotes key with brackets +quotes key with braces +quotes key with comma +quotes key with spaces +quotes key with leading hyphen +quotes key with leading and trailing spaces +quotes numeric key +quotes empty string key +escapes newline in key +escapes tab in key +escapes quotes in key + +# reason: may need to rely on System.Numerics.BigInteger for large numbers +encodes large number \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b918e6d..cc220f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,6 +63,9 @@ We target .NET 8.0 and .NET 9.0 for broad compatibility. dotnet test ``` +Some tests are auto generated to comply with the [TOON specification](https://github.com/toon-format/spec/blob/main/SPEC.md). To ensure tests are +aligned with the spec, execute the `specgen.sh` or `specgen.ps1` script. + ## SPEC Compliance All implementations must comply with the [TOON specification](https://github.com/toon-format/spec/blob/main/SPEC.md). diff --git a/ToonFormat.sln b/ToonFormat.sln index 59e7a2c..1d6ee6e 100644 --- a/ToonFormat.sln +++ b/ToonFormat.sln @@ -11,6 +11,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ToonFormat.Tests", "tests\ToonFormat.Tests\ToonFormat.Tests.csproj", "{25E10B0C-CAC4-475E-90C9-B4F85BB56C55}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ToonFormat.SpecGenerator", "tests\ToonFormat.SpecGenerator\ToonFormat.SpecGenerator.csproj", "{DB25DA28-D02C-4D8F-8C4A-D581D544607C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -45,6 +47,18 @@ Global {25E10B0C-CAC4-475E-90C9-B4F85BB56C55}.Release|x64.Build.0 = Release|Any CPU {25E10B0C-CAC4-475E-90C9-B4F85BB56C55}.Release|x86.ActiveCfg = Release|Any CPU {25E10B0C-CAC4-475E-90C9-B4F85BB56C55}.Release|x86.Build.0 = Release|Any CPU + {DB25DA28-D02C-4D8F-8C4A-D581D544607C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB25DA28-D02C-4D8F-8C4A-D581D544607C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB25DA28-D02C-4D8F-8C4A-D581D544607C}.Debug|x64.ActiveCfg = Debug|Any CPU + {DB25DA28-D02C-4D8F-8C4A-D581D544607C}.Debug|x64.Build.0 = Debug|Any CPU + {DB25DA28-D02C-4D8F-8C4A-D581D544607C}.Debug|x86.ActiveCfg = Debug|Any CPU + {DB25DA28-D02C-4D8F-8C4A-D581D544607C}.Debug|x86.Build.0 = Debug|Any CPU + {DB25DA28-D02C-4D8F-8C4A-D581D544607C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB25DA28-D02C-4D8F-8C4A-D581D544607C}.Release|Any CPU.Build.0 = Release|Any CPU + {DB25DA28-D02C-4D8F-8C4A-D581D544607C}.Release|x64.ActiveCfg = Release|Any CPU + {DB25DA28-D02C-4D8F-8C4A-D581D544607C}.Release|x64.Build.0 = Release|Any CPU + {DB25DA28-D02C-4D8F-8C4A-D581D544607C}.Release|x86.ActiveCfg = Release|Any CPU + {DB25DA28-D02C-4D8F-8C4A-D581D544607C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -52,5 +66,6 @@ Global GlobalSection(NestedProjects) = preSolution {1951209F-73E0-4B33-AB40-8D80CACCB507} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {25E10B0C-CAC4-475E-90C9-B4F85BB56C55} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {DB25DA28-D02C-4D8F-8C4A-D581D544607C} = {0AB3BF05-4346-4AA6-1389-037BE0695223} EndGlobalSection EndGlobal diff --git a/specgen.ps1 b/specgen.ps1 new file mode 100644 index 0000000..5d18b71 --- /dev/null +++ b/specgen.ps1 @@ -0,0 +1,8 @@ +# generate spec +$GH_REPO = "https://github.com/toon-format/spec.git" +$OUT_DIR = "./tests/ToonFormat.Tests" + +# build and execute spec generator +dotnet build tests/ToonFormat.SpecGenerator + +dotnet run --project tests/ToonFormat.SpecGenerator -- --url="$GH_REPO" --output="$OUT_DIR" --branch="v2.0.0" --loglevel="Information" diff --git a/specgen.sh b/specgen.sh new file mode 100644 index 0000000..fa3bd03 --- /dev/null +++ b/specgen.sh @@ -0,0 +1,8 @@ +# generate spec +GH_REPO="https://github.com/toon-format/spec.git" +OUT_DIR="./tests/ToonFormat.Tests" + +# build and execute spec generator +dotnet build tests/ToonFormat.SpecGenerator + +dotnet run --project tests/ToonFormat.SpecGenerator -- --url="$GH_REPO" --output="$OUT_DIR" --branch="v2.0.0" --loglevel="Information" \ No newline at end of file diff --git a/src/ToonFormat/Constants.cs b/src/ToonFormat/Constants.cs index 50dfe73..ea15849 100644 --- a/src/ToonFormat/Constants.cs +++ b/src/ToonFormat/Constants.cs @@ -7,7 +7,7 @@ public static class Constants { public const char LIST_ITEM_MARKER = '-'; - public const string LIST_ITEM_PREFIX = "- "; + public const string LIST_ITEM_PREFIX = "- "; // #region Structural characters public const char COMMA = ','; diff --git a/src/ToonFormat/Internal/Decode/Parser.cs b/src/ToonFormat/Internal/Decode/Parser.cs index 8f48663..f3c66c8 100644 --- a/src/ToonFormat/Internal/Decode/Parser.cs +++ b/src/ToonFormat/Internal/Decode/Parser.cs @@ -202,7 +202,7 @@ private static BracketSegmentResult ParseBracketSegment(string seg, char default /// public static List ParseDelimitedValues(string input, char delimiter) { - var values = new List(16); // Ô¤·ÖÅäһЩÈÝÁ¿ + var values = new List(16); // pre-allocate for performance var current = new System.Text.StringBuilder(input.Length); bool inQuotes = false; @@ -212,7 +212,7 @@ public static List ParseDelimitedValues(string input, char delimiter) if (ch == Constants.BACKSLASH && inQuotes && i + 1 < input.Length) { - // תÒå´¦Àí + // Escape sequence in quoted string current.Append(ch); current.Append(input[i + 1]); i++; @@ -244,7 +244,6 @@ public static List ParseDelimitedValues(string input, char delimiter) return values; } - /// /// Maps an array of string tokens to JSON primitive values. /// diff --git a/src/ToonFormat/Internal/Decode/Scanner.cs b/src/ToonFormat/Internal/Decode/Scanner.cs index 2fa9923..b14c27d 100644 --- a/src/ToonFormat/Internal/Decode/Scanner.cs +++ b/src/ToonFormat/Internal/Decode/Scanner.cs @@ -127,7 +127,7 @@ public static ScanResult ToParsedLines(string source, int indentSize, bool stric while (!span.IsEmpty) { lineNumber++; - // ÕÒµ½ÕâÒ»ÐеĽáÊøÎ»Öà + // find the end of this line int newlineIdx = span.IndexOf('\n'); ReadOnlySpan lineSpan; if (newlineIdx >= 0) @@ -140,12 +140,12 @@ public static ScanResult ToParsedLines(string source, int indentSize, bool stric lineSpan = span; span = ReadOnlySpan.Empty; } - // È¥µô½áβµÄ»»ÐзûºÍ»Ø³µ + // remove trailing carriage return if present if (!lineSpan.IsEmpty && lineSpan[lineSpan.Length - 1] == '\r') { lineSpan = lineSpan.Slice(0, lineSpan.Length - 1); } - // ¼ÆËãËõ½ø + // calculate indentation int indent = 0; while (indent < lineSpan.Length && lineSpan[indent] == Constants.SPACE) { diff --git a/src/ToonFormat/Internal/Decode/Validation.cs b/src/ToonFormat/Internal/Decode/Validation.cs index 2b6efbd..ebeeef4 100644 --- a/src/ToonFormat/Internal/Decode/Validation.cs +++ b/src/ToonFormat/Internal/Decode/Validation.cs @@ -78,7 +78,7 @@ public static void ValidateNoExtraTabularRows( return; var nextLine = cursor.Peek(); - if (nextLine != null + if (nextLine != null && nextLine.Depth == rowDepth && !nextLine.Content.StartsWith(Constants.LIST_ITEM_PREFIX) && IsDataRow(nextLine.Content, header.Delimiter)) diff --git a/src/ToonFormat/Internal/Encode/Encoders.cs b/src/ToonFormat/Internal/Encode/Encoders.cs index 70dfee1..e6c714b 100644 --- a/src/ToonFormat/Internal/Encode/Encoders.cs +++ b/src/ToonFormat/Internal/Encode/Encoders.cs @@ -193,7 +193,7 @@ public static string EncodeInlineArrayLine( bool lengthMarker = false) { var header = Primitives.FormatHeader(values.Count, prefix, null, delimiter, lengthMarker); - + if (values.Count == 0) { return header; @@ -235,7 +235,7 @@ public static void EncodeArrayOfObjectsAsTabular( var firstRow = rows[0]; var firstKeys = firstRow.Select(kvp => kvp.Key).ToList(); - + if (firstKeys.Count == 0) return null; @@ -323,7 +323,7 @@ public static void EncodeMixedArrayAsListItems( public static void EncodeObjectAsListItem(JsonObject obj, LineWriter writer, int depth, ResolvedEncodeOptions options) { var keys = obj.Select(kvp => kvp.Key).ToList(); - + if (keys.Count == 0) { writer.Push(depth, Constants.LIST_ITEM_MARKER.ToString()); @@ -342,7 +342,7 @@ public static void EncodeObjectAsListItem(JsonObject obj, LineWriter writer, int else if (Normalize.IsJsonArray(firstValue)) { var arr = (JsonArray)firstValue!; - + if (Normalize.IsArrayOfPrimitives(arr)) { // Inline format for primitive arrays @@ -354,7 +354,7 @@ public static void EncodeObjectAsListItem(JsonObject obj, LineWriter writer, int // Check if array of objects can use tabular format var objects = arr.Cast().ToList(); var header = ExtractTabularHeader(objects); - + if (header != null) { // Tabular format for uniform arrays of objects @@ -387,7 +387,7 @@ public static void EncodeObjectAsListItem(JsonObject obj, LineWriter writer, int else if (Normalize.IsJsonObject(firstValue)) { var nestedObj = (JsonObject)firstValue!; - + if (nestedObj.Count == 0) { writer.PushListItem(depth, $"{encodedKey}{Constants.COLON}"); diff --git a/src/ToonFormat/Internal/Encode/Primitives.cs b/src/ToonFormat/Internal/Encode/Primitives.cs index ce666bd..a86127f 100644 --- a/src/ToonFormat/Internal/Encode/Primitives.cs +++ b/src/ToonFormat/Internal/Encode/Primitives.cs @@ -56,7 +56,7 @@ public static string EncodePrimitive(JsonNode? value, char delimiter = Constants public static string EncodeStringLiteral(string value, char delimiter = Constants.COMMA) { var delimiterEnum = Constants.FromDelimiterChar(delimiter); - + if (ValidationShared.IsSafeUnquoted(value, delimiterEnum)) { return value; diff --git a/tests/ToonFormat.SpecGenerator/Extensions/StringExtensions.cs b/tests/ToonFormat.SpecGenerator/Extensions/StringExtensions.cs new file mode 100644 index 0000000..9027500 --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/Extensions/StringExtensions.cs @@ -0,0 +1,20 @@ +using System.Globalization; + +namespace ToonFormat.SpecGenerator.Extensions; + +public static class StringExtensions +{ + public static string ToPascalCase(this string input) + { + if (string.IsNullOrWhiteSpace(input)) + { + return input; + } + + string formattedInput = input.Replace("-", " "); + + TextInfo textInfo = CultureInfo.CurrentCulture.TextInfo; + string titleCase = textInfo.ToTitleCase(formattedInput.ToLower()); + return titleCase.Replace(" ", ""); + } +} diff --git a/tests/ToonFormat.SpecGenerator/FixtureWriter.cs b/tests/ToonFormat.SpecGenerator/FixtureWriter.cs new file mode 100644 index 0000000..056b794 --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/FixtureWriter.cs @@ -0,0 +1,454 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; +using ToonFormat.SpecGenerator.Extensions; +using ToonFormat.SpecGenerator.Types; + +namespace ToonFormat.SpecGenerator; + +internal class FixtureWriter(Fixtures fixture, string outputDir) + where TTestCase : ITestCase +{ + public Fixtures Fixture { get; } = fixture; + public string OutputDir { get; } = outputDir; + + private int indentLevel = 0; + + public void WriteFile() + { + var outputPath = Path.Combine(OutputDir, Fixture.FileName ?? throw new InvalidOperationException("Fixture FileName is not set")); + + Directory.CreateDirectory(OutputDir); + + using var writer = new StreamWriter(outputPath, false); + + WriteHeader(writer); + WriteLine(writer); + WriteLine(writer); + + WriteUsings(writer); + WriteLine(writer); + WriteLine(writer); + + WriteNamespace(writer, Fixture.Category); + WriteLine(writer); + WriteLine(writer); + + WriteLine(writer, $"[Trait(\"Category\", \"{Fixture.Category}\")]"); + WriteLine(writer, "public class " + FormatClassName(outputPath)); + WriteLine(writer, "{"); + + Indent(); + + // Write test methods here + foreach (var testCase in Fixture.Tests) + { + WriteTestMethod(writer, testCase); + } + + Unindent(); + WriteLine(writer, "}"); + } + + private string FormatClassName(string filePath) + { + var fileName = Path.GetFileNameWithoutExtension(filePath); + if (fileName == null) return string.Empty; + + return StripIllegalCharacters(fileName); + } + + private string FormatMethodName(string methodName) + { + return StripIllegalCharacters(methodName.ToPascalCase()); + } + + private string StripIllegalCharacters(string input) + { + return new Regex(@"[\(_\-/\:\)=,+]").Replace(input, "")!; + } + + private void WriteTestMethod(StreamWriter writer, TTestCase testCase) + { + WriteLineIndented(writer, "[Fact]"); + WriteLineIndented(writer, $"[Trait(\"Description\", \"{testCase.Name}\")]"); + WriteLineIndented(writer, $"public void {FormatMethodName(testCase.Name)}()"); + WriteLineIndented(writer, "{"); + + Indent(); + + // Arrange + WriteLineIndented(writer, "// Arrange"); + switch (testCase) + { + case EncodeTestCase encodeTestCase: + WriteLineIndented(writer, "var input ="); + + Indent(); + WriteJsonNodeAsAnonymousType(writer, encodeTestCase.Input); + Unindent(); + + WriteLine(writer); + + WriteLineIndented(writer, "var expected ="); + WriteLine(writer, "\"\"\""); + Write(writer, encodeTestCase.Expected); + WriteLine(writer); + WriteLine(writer, "\"\"\";"); + + break; + + case DecodeTestCase decodeTestCase: + + WriteLineIndented(writer, "var input ="); + WriteLine(writer, "\"\"\""); + Write(writer, decodeTestCase.Input); + WriteLine(writer); + WriteLine(writer, "\"\"\";"); + + break; + default: + WriteLineIndented(writer, $"var input = /* {typeof(TIn).Name} */; // TODO: Initialize input"); + break; + } + + + WriteLine(writer); + + // Act & Assert + WriteLineIndented(writer, "// Act & Assert"); + switch (testCase) + { + case EncodeTestCase encodeTestCase: + var hasEncodeOptions = encodeTestCase.Options != null; + if (hasEncodeOptions) + { + WriteLineIndented(writer, "var options = new ToonEncodeOptions"); + WriteLineIndented(writer, "{"); + Indent(); + + WriteLineIndented(writer, $"Delimiter = {GetToonDelimiterEnumFromChar(encodeTestCase.Options?.Delimiter)},"); + WriteLineIndented(writer, $"Indent = {encodeTestCase.Options?.Indent ?? 2},"); + + Unindent(); + WriteLineIndented(writer, "};"); + + WriteLine(writer); + WriteLineIndented(writer, $"var result = ToonEncoder.Encode(input, options);"); + } + else + { + WriteLineIndented(writer, $"var result = ToonEncoder.Encode(input);"); + } + + WriteLine(writer); + WriteLineIndented(writer, $"Assert.Equal(expected, result);"); + break; + + case DecodeTestCase decodeTestCase: + var hasDecodeOptions = decodeTestCase.Options != null; + if (hasDecodeOptions) + { + WriteLineIndented(writer, "var options = new ToonDecodeOptions"); + WriteLineIndented(writer, "{"); + Indent(); + + WriteLineIndented(writer, $"Indent = {decodeTestCase.Options?.Indent ?? 2},"); + WriteLineIndented(writer, $"Strict = {(decodeTestCase.Options?.Strict ?? true).ToString().ToLower()},"); + + Unindent(); + WriteLineIndented(writer, "};"); + + WriteLine(writer); + } + + if (decodeTestCase.ShouldError) + { + if (hasDecodeOptions) + { + WriteLineIndented(writer, $"Assert.Throws(() => ToonDecoder.Decode(input, options));"); + } + else + { + WriteLineIndented(writer, $"Assert.Throws(() => ToonDecoder.Decode(input));"); + } + } + else + { + var valueAsRawString = decodeTestCase.Expected?.ToString(); + var isNumeric = decodeTestCase.Expected?.GetValueKind() == JsonValueKind.Number; + var hasEmptyRawString = valueAsRawString == string.Empty; + var value = hasEmptyRawString || isNumeric ? valueAsRawString : decodeTestCase.Expected?.ToJsonString() ?? "null"; + + WriteIndented(writer, "var result = ToonDecoder.Decode"); + if (isNumeric) + { + if (decodeTestCase.Expected is JsonValue jsonValue) + { + if (jsonValue.TryGetValue(out var doubleValue)) + { + Write(writer, ""); + } + else if (jsonValue.TryGetValue(out var intValue)) + { + Write(writer, ""); + } + else if (jsonValue.TryGetValue(out var longValue)) + { + Write(writer, ""); + } + } + } + Write(writer, "(input"); + if (hasDecodeOptions) + { + Write(writer, ", options"); + } + WriteLine(writer, ");"); + + WriteLine(writer); + + if (isNumeric) + { + WriteLineIndented(writer, $"var expected = {value};"); + + WriteLine(writer); + WriteLineIndented(writer, "Assert.Equal(result, expected);"); + } + else + { + if (hasEmptyRawString) + { + WriteLineIndented(writer, $"var expected = string.Empty;"); + } + else + { + WriteLineIndented(writer, $"var expected = JsonNode.Parse(\"\"\"\n{value}\n\"\"\");"); + } + + WriteLine(writer); + WriteLineIndented(writer, $"Assert.True(JsonNode.DeepEquals(result, expected));"); + } + } + break; + + default: + WriteLineIndented(writer, "// TODO: Implement test logic"); + break; + } + + Unindent(); + WriteLineIndented(writer, "}"); + WriteLine(writer); + } + + private static string GetToonDelimiterEnumFromChar(string? delimiter) + { + return delimiter switch + { + "," => "ToonDelimiter.COMMA", + "\t" => "ToonDelimiter.TAB", + "|" => "ToonDelimiter.PIPE", + _ => "ToonDelimiter.COMMA" + }; + } + + private void WriteJsonNodeAsAnonymousType(StreamWriter writer, JsonNode node) + { + WriteJsonNode(writer, node); + + WriteLineIndented(writer, ";"); + } + + private void WriteJsonNode(StreamWriter writer, JsonNode? node) + { + var propertyName = node?.Parent is JsonObject ? node?.GetPropertyName() : null; + + void WriteFunc(string value) + { + if (propertyName is not null && node.Parent is not JsonArray) + { + Write(writer, value); + } + else + { + WriteIndented(writer, value); + } + } + + if (node is null) + { + WriteIndented(writer, "(string)null"); + } + else if (node is JsonValue nodeValue) + { + if (propertyName is not null) + { + WriteIndented(writer, $"@{propertyName} = "); + } + + var kind = nodeValue.GetValueKind(); + if (kind == JsonValueKind.String) + { + WriteFunc($"@\"{nodeValue.GetValue().Replace("\"", "\"\"")}\""); + } + else + { + if (kind == JsonValueKind.True || kind == JsonValueKind.False) + { + WriteFunc($"{nodeValue.GetValue().ToString().ToLower()}"); + } + else if (kind == JsonValueKind.Number) + { + var stringValue = nodeValue.ToString(); + + WriteFunc($"{stringValue}"); + } + else + { + WriteFunc($"{nodeValue.GetValue()}"); + } + } + + if (propertyName is not null) + { + WriteLine(writer, ","); + } + } + else if (node is JsonObject nodeObject) + { + if (propertyName is not null) + { + WriteLineIndented(writer, $"@{propertyName} ="); + } + + WriteLineIndented(writer, "new"); + WriteLineIndented(writer, "{"); + Indent(); + + foreach (var property in nodeObject) + { + if (property.Value is null) + { + WriteFunc($"@{property.Key} = (string)null,"); + } + else + { + WriteJsonNode(writer, property.Value); + } + } + + Unindent(); + WriteLineIndented(writer, "}"); + + if (propertyName is not null) + { + WriteLine(writer, ","); + } + } + else if (node is JsonArray nodeArray) + { + if (!string.IsNullOrEmpty(propertyName)) + { + WriteIndented(writer, $"@{propertyName} ="); + } + + WriteFunc("new object[] {"); + + WriteLineIndented(writer); + Indent(); + + foreach (var item in nodeArray) + { + WriteJsonNode(writer, item); + + if (item is JsonValue) + { + WriteLine(writer, ","); + } + else + { + WriteLineIndented(writer, ","); + } + } + + Unindent(); + WriteLineIndented(writer, "}"); + + if (propertyName is not null) + { + WriteLine(writer, ","); + } + } + } + + private void Indent() + { + indentLevel++; + } + + private void Unindent() + { + indentLevel--; + } + + private void WriteLineIndented(StreamWriter writer, string line) + { + writer.WriteLine(new string(' ', indentLevel * 4) + line); + } + + private void WriteLineIndented(StreamWriter writer) + { + WriteLineIndented(writer, ""); + } + + private void WriteIndented(StreamWriter writer, string content) + { + writer.Write(new string(' ', indentLevel * 4) + content); + } + + private void WriteIndented(StreamWriter writer) + { + WriteIndented(writer, ""); + } + + private void WriteHeader(StreamWriter writer) + { + WriteLine(writer, "// "); ; + WriteLine(writer, "// This code was generated by ToonFormat.SpecGenerator."); + WriteLine(writer, "//"); + WriteLine(writer, "// Changes to this file may cause incorrect behavior and will be lost if"); + WriteLine(writer, "// the code is regenerated."); + WriteLine(writer, "// "); + } + + private void WriteUsings(StreamWriter writer) + { + WriteLine(writer, "using System;"); + WriteLine(writer, "using System.Collections.Generic;"); + WriteLine(writer, "using System.Text.Json;"); + WriteLine(writer, "using System.Text.Json.Nodes;"); + WriteLine(writer, "using Toon.Format;"); + WriteLine(writer, "using Xunit;"); + } + + private void WriteNamespace(StreamWriter writer, string category) + { + WriteLine(writer, $"namespace ToonFormat.Tests.{category.ToPascalCase()};"); + } + + private void WriteLine(StreamWriter writer) + { + writer.WriteLine(); + } + + private void WriteLine(StreamWriter writer, string line) + { + writer.WriteLine(line); + } + + private void Write(StreamWriter writer, string contents) + { + writer.Write(contents); + } +} diff --git a/tests/ToonFormat.SpecGenerator/Program.cs b/tests/ToonFormat.SpecGenerator/Program.cs new file mode 100644 index 0000000..bd3cc60 --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/Program.cs @@ -0,0 +1,41 @@ +using CommandLine; +using Microsoft.Extensions.Logging; + +namespace ToonFormat.SpecGenerator; + + +public static class Program +{ + public static void Main(string[] args) + { + Parser.Default.ParseArguments(args) + .MapResult((opts) => + { + var loggerFactory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(Enum.TryParse(opts.LogLevel, true, out var level) ? level : LogLevel.Information); + builder.AddSimpleConsole(options => + { + options.IncludeScopes = false; + options.SingleLine = true; + }); + }); + + var logger = loggerFactory.CreateLogger(); + + var specGenerator = new SpecGenerator(logger); + + specGenerator.GenerateSpecs(opts); + + return 0; + }, HandleParseError); + } + + static int HandleParseError(IEnumerable errs) + { + var result = -2; + if (errs.Any(x => x is HelpRequestedError || x is VersionRequestedError)) + result = -1; + return result; + } +} diff --git a/tests/ToonFormat.SpecGenerator/Properties/launchSettings.json b/tests/ToonFormat.SpecGenerator/Properties/launchSettings.json new file mode 100644 index 0000000..8d8fce1 --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "ToonFormat.SpecGenerator": { + "commandName": "Project", + "commandLineArgs": "--url=\"https://github.com/toon-format/spec.git\" --output=\"../../../../../tests/ToonFormat.Tests\" --branch=\"v2.0.0\" --loglevel=\"Information\" --ignore=\"../../../../../\"" + } + } +} \ No newline at end of file diff --git a/tests/ToonFormat.SpecGenerator/SpecGenerator.cs b/tests/ToonFormat.SpecGenerator/SpecGenerator.cs new file mode 100644 index 0000000..eba64d0 --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/SpecGenerator.cs @@ -0,0 +1,160 @@ +using Microsoft.Extensions.Logging; +using System.Text.Json.Nodes; +using ToonFormat.SpecGenerator.Extensions; +using ToonFormat.SpecGenerator.Types; +using ToonFormat.SpecGenerator.Util; + +namespace ToonFormat.SpecGenerator; + +internal class SpecGenerator(ILogger logger) +{ + public void GenerateSpecs(SpecGeneratorOptions options) + { + var toonSpecDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + + logger.LogInformation("Cloning Toon Format spec repository to {ToonSpecDir}", toonSpecDir); + + try + { + logger.LogDebug("Cloning repository {RepoUrl} to {CloneDirectory}", options.SpecRepoUrl, toonSpecDir); + + GitTool.CloneRepository(options.SpecRepoUrl, toonSpecDir, + branch: options.Branch, depth: 1, logger: logger); + + var testsToIgnore = GenerateTestsToIgnore(options.AbsoluteSpecIgnorePath); + + GenerateEncodeFixtures( + toonSpecDir, + Path.Combine(options.AbsoluteOutputPath, "Encode"), + testsToIgnore + ); + + GenerateDecodeFixtures( + toonSpecDir, + Path.Combine(options.AbsoluteOutputPath, "Decode"), + testsToIgnore + ); + } + catch (Exception e) + { + logger.LogError(e, "Failed generating specs"); + + return; + } + finally + { + logger.LogDebug("Removing temp clone directory {CloneDirectory}", toonSpecDir); + + // delete folder recursively + TryDeleteDirectory(toonSpecDir); + } + + logger.LogInformation("Spec generation completed."); + } + + private void GenerateEncodeFixtures(string specDir, string outputDir, IEnumerable ignores) + { + var encodeFixtures = LoadEncodeFixtures(specDir); + + foreach (var fixture in encodeFixtures) + { + fixture.Tests = fixture.Tests.Where(t => !ignores.Contains(t.Name)); + + // Process each encode fixture as needed + var writer = new FixtureWriter(fixture, outputDir); + + writer.WriteFile(); + } + } + + private void GenerateDecodeFixtures(string specDir, string outputDir, IEnumerable ignores) + { + var decodeFixtures = LoadDecodeFixtures(specDir); + + foreach (var fixture in decodeFixtures) + { + fixture.Tests = fixture.Tests.Where(t => !ignores.Contains(t.Name)); + + // Process each decode fixture as needed + var writer = new FixtureWriter(fixture, outputDir); + + writer.WriteFile(); + } + } + + private IEnumerable> LoadEncodeFixtures(string specDir) + { + return LoadFixtures(specDir, "encode"); + } + + private IEnumerable> LoadDecodeFixtures(string specDir) + { + return LoadFixtures(specDir, "decode"); + } + + private IEnumerable GenerateTestsToIgnore(string specIgnorePath) + { + const string specIgnoreFileName = ".specignore"; + var specIgnoreFileAbsolutePath = !specIgnorePath.EndsWith(specIgnoreFileName) ? + Path.Combine(specIgnorePath, specIgnoreFileName) : specIgnorePath; + + if (!File.Exists(specIgnoreFileAbsolutePath)) + { + logger.LogDebug("No spec ignore file found at path {Path}", specIgnoreFileAbsolutePath); + + return Array.Empty(); + } + + // filter comments and empty lines + var testNames = File.ReadAllLines(specIgnoreFileAbsolutePath) + .Where(i => !string.IsNullOrWhiteSpace(i) && !i.StartsWith('#')); + + var set = new HashSet(testNames, StringComparer.OrdinalIgnoreCase); + + logger.LogDebug("Found {Count} tests to ignore", set.Count); + + return set; + } + + private static IEnumerable> LoadFixtures(string specDir, string testType) + where TTestCase : ITestCase + { + var fixturesPath = Path.Combine(specDir, "tests", "fixtures", testType); + + foreach (var testFixture in Directory.GetFiles(fixturesPath, "*.json")) + { + var fixtureFileName = Path.GetFileName(testFixture); + + var fixtureContent = File.ReadAllText(testFixture); + var fixture = SpecSerializer.Deserialize>(fixtureContent) ?? throw new InvalidOperationException($"Failed to deserialize fixture file: {testFixture}"); + + fixture.FileName = FixtureNameToCSharpFileName(fixtureFileName); + + yield return fixture; + } + } + + private static void TryDeleteDirectory(string path) + { + int i = 0; + do + { + if (Directory.Exists(path)) + { + try + { + Directory.Delete(path, true); + + break; + } + catch (Exception) { i++; } + } + } + while (i < 3); + } + + private static string FixtureNameToCSharpFileName(string fixtureName) + { + return Path.ChangeExtension(fixtureName.ToPascalCase(), ".cs"); + } +} diff --git a/tests/ToonFormat.SpecGenerator/SpecGeneratorOptions.cs b/tests/ToonFormat.SpecGenerator/SpecGeneratorOptions.cs new file mode 100644 index 0000000..d4e3e0c --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/SpecGeneratorOptions.cs @@ -0,0 +1,42 @@ +using CommandLine; + +namespace ToonFormat.SpecGenerator; + +public class SpecGeneratorOptions +{ + /// + /// The git repo of the toon spec + /// + [Option('u', "url", Required = true, HelpText = "The git repo of the toon spec")] + public required string SpecRepoUrl { get; set; } + + /// + /// The relative path of the output directory + /// + [Option('o', "output", Required = true, HelpText = "The relative path of the output directory")] + public required string OutputPath { get; set; } + + /// + /// The relative path to a .specignore file, to explicitly ignore generating tests based on a test name + /// + [Option('i', "ignore", Required = false, HelpText = "The relative path to a .specignore file, to explicitly ignore generating tests based on a test name")] + public string? IgnorePath { get; set; } + + /// + /// The branch version or tag name to reference + /// + [Option('b', "branch", Required = false, HelpText = "The branch version or tag name to reference")] + public string? Branch { get; set; } + + /// + /// The log level + /// + [Option('l', "loglevel", Required = false, HelpText = "The log level")] + public string? LogLevel { get; set; } = Microsoft.Extensions.Logging.LogLevel.Information.ToString(); + + internal string AbsoluteOutputPath => Path.GetFullPath( + Path.Combine(Environment.GetEnvironmentVariable("PWD") ?? Environment.CurrentDirectory, OutputPath)); + + internal string AbsoluteSpecIgnorePath => Path.GetFullPath( + Path.Combine(Environment.GetEnvironmentVariable("PWD") ?? Environment.CurrentDirectory, IgnorePath ?? "")); +} diff --git a/tests/ToonFormat.SpecGenerator/SpecSerializer.cs b/tests/ToonFormat.SpecGenerator/SpecSerializer.cs new file mode 100644 index 0000000..d5a974b --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/SpecSerializer.cs @@ -0,0 +1,22 @@ +using System.Text.Json; + +namespace ToonFormat.SpecGenerator; + +internal static class SpecSerializer +{ + public static T Deserialize(string input) + { + return JsonSerializer.Deserialize(input, jsonSerializerOptions) ?? throw new InvalidOperationException("Deserialization resulted in null"); + } + + public static string Serialize(T obj) + { + return JsonSerializer.Serialize(obj, jsonSerializerOptions); + } + + private static readonly JsonSerializerOptions jsonSerializerOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; +} diff --git a/tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj b/tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj new file mode 100644 index 0000000..3533eee --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj @@ -0,0 +1,16 @@ + + + + net9.0 + enable + enable + Exe + + + + + + + + + diff --git a/tests/ToonFormat.SpecGenerator/Types/DecodeTestCase.cs b/tests/ToonFormat.SpecGenerator/Types/DecodeTestCase.cs new file mode 100644 index 0000000..5a0a5c1 --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/Types/DecodeTestCase.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Nodes; + +namespace ToonFormat.SpecGenerator.Types; + +public record DecodeTestCase : ITestCase +{ + public required string Name { get; init; } + public required string Input { get; init; } + public required JsonNode Expected { get; init; } + public bool ShouldError { get; init; } = false; + public TestCaseOptions? Options { get; init; } + public string? SpecSection { get; init; } + public string? Note { get; init; } + public string? MinSpecVersion { get; init; } +} + diff --git a/tests/ToonFormat.SpecGenerator/Types/EncodeTestCase.cs b/tests/ToonFormat.SpecGenerator/Types/EncodeTestCase.cs new file mode 100644 index 0000000..a11b132 --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/Types/EncodeTestCase.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Nodes; + +namespace ToonFormat.SpecGenerator.Types; + +public record EncodeTestCase : ITestCase +{ + public required string Name { get; init; } + public required JsonNode Input { get; init; } + public required string Expected { get; init; } + public bool ShouldError { get; init; } = false; + public TestCaseOptions? Options { get; init; } + public string? SpecSection { get; init; } + public string? Note { get; init; } + public string? MinSpecVersion { get; init; } +} + diff --git a/tests/ToonFormat.SpecGenerator/Types/Fixtures.cs b/tests/ToonFormat.SpecGenerator/Types/Fixtures.cs new file mode 100644 index 0000000..c7cf9d7 --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/Types/Fixtures.cs @@ -0,0 +1,12 @@ +namespace ToonFormat.SpecGenerator.Types; + +public record Fixtures + where TTestCase : ITestCase +{ + public required string Version { get; init; } + public required string Category { get; init; } // "encode" | "decode" + public required string Description { get; init; } + public required IEnumerable Tests { get; set; } + public string? FileName { get; set; } +} + diff --git a/tests/ToonFormat.SpecGenerator/Types/ITestCase.cs b/tests/ToonFormat.SpecGenerator/Types/ITestCase.cs new file mode 100644 index 0000000..1bcab07 --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/Types/ITestCase.cs @@ -0,0 +1,13 @@ +namespace ToonFormat.SpecGenerator.Types; + +public interface ITestCase +{ + public string Name { get; init; } + public TInput Input { get; init; } + public TExpected Expected { get; init; } + public bool ShouldError { get; init; } + public string? SpecSection { get; init; } + public string? Note { get; init; } + public string? MinSpecVersion { get; init; } +} + diff --git a/tests/ToonFormat.SpecGenerator/Types/TestCaseOptions.cs b/tests/ToonFormat.SpecGenerator/Types/TestCaseOptions.cs new file mode 100644 index 0000000..e0ebf5d --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/Types/TestCaseOptions.cs @@ -0,0 +1,12 @@ +namespace ToonFormat.SpecGenerator.Types; + +public record TestCaseOptions +{ + public string? Delimiter { get; init; } + public int? Indent { get; init; } + public bool? Strict { get; init; } + public string? KeyFolding { get; init; } + public int? FlattenDepth { get; init; } + public string? ExpandPaths { get; init; } +} + diff --git a/tests/ToonFormat.SpecGenerator/Util/GitTool.cs b/tests/ToonFormat.SpecGenerator/Util/GitTool.cs new file mode 100644 index 0000000..401c551 --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/Util/GitTool.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Logging; + +namespace ToonFormat.SpecGenerator.Util; + +internal static class GitTool +{ + public static void CloneRepository(string repositoryUrl, string destinationPath, + string? branch = null, int? depth = null, ILogger? logger = null) + { + var depthArg = depth.HasValue ? $"--depth {depth.Value}" : string.Empty; + var branchArg = branch is not null ? $"--branch {branch}" : string.Empty; + + using var process = new System.Diagnostics.Process(); + + process.StartInfo.FileName = "git"; + process.StartInfo.Arguments = $"clone {branchArg} {depthArg} {repositoryUrl} {destinationPath}"; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.UseShellExecute = false; + process.StartInfo.CreateNoWindow = true; + + logger?.LogDebug("Executing git with arguments: {Arguments}", process.StartInfo.Arguments); + + process.Start(); + + process.WaitForExit(); + } +} \ No newline at end of file diff --git a/tests/ToonFormat.Tests/Decode/ArraysNested.cs b/tests/ToonFormat.Tests/Decode/ArraysNested.cs new file mode 100644 index 0000000..39951c6 --- /dev/null +++ b/tests/ToonFormat.Tests/Decode/ArraysNested.cs @@ -0,0 +1,475 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Decode; + + +[Trait("Category", "decode")] +public class ArraysNested +{ + [Fact] + [Trait("Description", "parses list arrays for non-uniform objects")] + public void ParsesListArraysForNonUniformObjects() + { + // Arrange + var input = +""" +items[2]: + - id: 1 + name: First + - id: 2 + name: Second + extra: true +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"id":1,"name":"First"},{"id":2,"name":"Second","extra":true}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses list arrays with empty items")] + public void ParsesListArraysWithEmptyItems() + { + // Arrange + var input = +""" +items[3]: + - first + - second + - +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":["first","second",{}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses list arrays with deeply nested objects")] + public void ParsesListArraysWithDeeplyNestedObjects() + { + // Arrange + var input = +""" +items[2]: + - properties: + state: + type: string + - id: 2 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"properties":{"state":{"type":"string"}}},{"id":2}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses list arrays containing objects with nested properties")] + public void ParsesListArraysContainingObjectsWithNestedProperties() + { + // Arrange + var input = +""" +items[1]: + - id: 1 + nested: + x: 1 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"id":1,"nested":{"x":1}}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses nested tabular arrays as first field on hyphen line")] + public void ParsesNestedTabularArraysAsFirstFieldOnHyphenLine() + { + // Arrange + var input = +""" +items[1]: + - users[2]{id,name}: + 1,Ada + 2,Bob + status: active +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"users":[{"id":1,"name":"Ada"},{"id":2,"name":"Bob"}],"status":"active"}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses objects containing arrays (including empty arrays) in list format")] + public void ParsesObjectsContainingArraysIncludingEmptyArraysInListFormat() + { + // Arrange + var input = +""" +items[1]: + - name: test + data[0]: +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"name":"test","data":[]}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses arrays of arrays within objects")] + public void ParsesArraysOfArraysWithinObjects() + { + // Arrange + var input = +""" +items[1]: + - matrix[2]: + - [2]: 1,2 + - [2]: 3,4 + name: grid +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"matrix":[[1,2],[3,4]],"name":"grid"}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses nested arrays of primitives")] + public void ParsesNestedArraysOfPrimitives() + { + // Arrange + var input = +""" +pairs[2]: + - [2]: a,b + - [2]: c,d +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"pairs":[["a","b"],["c","d"]]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted strings and mixed lengths in nested arrays")] + public void ParsesQuotedStringsAndMixedLengthsInNestedArrays() + { + // Arrange + var input = +""" +pairs[2]: + - [2]: a,b + - [3]: "c,d","e:f","true" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"pairs":[["a","b"],["c,d","e:f","true"]]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses empty inner arrays")] + public void ParsesEmptyInnerArrays() + { + // Arrange + var input = +""" +pairs[2]: + - [0]: + - [0]: +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"pairs":[[],[]]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses mixed-length inner arrays")] + public void ParsesMixedLengthInnerArrays() + { + // Arrange + var input = +""" +pairs[2]: + - [1]: 1 + - [2]: 2,3 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"pairs":[[1],[2,3]]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses root arrays of primitives (inline)")] + public void ParsesRootArraysOfPrimitivesInline() + { + // Arrange + var input = +""" +[5]: x,y,"true",true,10 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +["x","y","true",true,10] +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses root arrays of uniform objects in tabular format")] + public void ParsesRootArraysOfUniformObjectsInTabularFormat() + { + // Arrange + var input = +""" +[2]{id}: + 1 + 2 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +[{"id":1},{"id":2}] +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses root arrays of non-uniform objects in list format")] + public void ParsesRootArraysOfNonUniformObjectsInListFormat() + { + // Arrange + var input = +""" +[2]: + - id: 1 + - id: 2 + name: Ada +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +[{"id":1},{"id":2,"name":"Ada"}] +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses empty root arrays")] + public void ParsesEmptyRootArrays() + { + // Arrange + var input = +""" +[0]: +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +[] +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses root arrays of arrays")] + public void ParsesRootArraysOfArrays() + { + // Arrange + var input = +""" +[2]: + - [2]: 1,2 + - [0]: +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +[[1,2],[]] +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses complex mixed object with arrays and nested objects")] + public void ParsesComplexMixedObjectWithArraysAndNestedObjects() + { + // Arrange + var input = +""" +user: + id: 123 + name: Ada + tags[2]: reading,gaming + active: true + prefs[0]: +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"user":{"id":123,"name":"Ada","tags":["reading","gaming"],"active":true,"prefs":[]}} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses arrays mixing primitives, objects and strings (list format)")] + public void ParsesArraysMixingPrimitivesObjectsAndStringsListFormat() + { + // Arrange + var input = +""" +items[3]: + - 1 + - a: 1 + - text +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[1,{"a":1},"text"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses arrays mixing objects and arrays")] + public void ParsesArraysMixingObjectsAndArrays() + { + // Arrange + var input = +""" +items[2]: + - a: 1 + - [2]: 1,2 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"a":1},[1,2]]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted key with list array format")] + public void ParsesQuotedKeyWithListArrayFormat() + { + // Arrange + var input = +""" +"x-items"[2]: + - id: 1 + - id: 2 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"x-items":[{"id":1},{"id":2}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + +} diff --git a/tests/ToonFormat.Tests/Decode/ArraysPrimitive.cs b/tests/ToonFormat.Tests/Decode/ArraysPrimitive.cs new file mode 100644 index 0000000..8a2d574 --- /dev/null +++ b/tests/ToonFormat.Tests/Decode/ArraysPrimitive.cs @@ -0,0 +1,283 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Decode; + + +[Trait("Category", "decode")] +public class ArraysPrimitive +{ + [Fact] + [Trait("Description", "parses string arrays inline")] + public void ParsesStringArraysInline() + { + // Arrange + var input = +""" +tags[3]: reading,gaming,coding +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"tags":["reading","gaming","coding"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses number arrays inline")] + public void ParsesNumberArraysInline() + { + // Arrange + var input = +""" +nums[3]: 1,2,3 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"nums":[1,2,3]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses mixed primitive arrays inline")] + public void ParsesMixedPrimitiveArraysInline() + { + // Arrange + var input = +""" +data[4]: x,y,true,10 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"data":["x","y",true,10]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses empty arrays")] + public void ParsesEmptyArrays() + { + // Arrange + var input = +""" +items[0]: +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses single-item array with empty string")] + public void ParsesSingleItemArrayWithEmptyString() + { + // Arrange + var input = +""" +items[1]: "" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[""]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses multi-item array with empty string")] + public void ParsesMultiItemArrayWithEmptyString() + { + // Arrange + var input = +""" +items[3]: a,"",b +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":["a","","b"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses whitespace-only strings in arrays")] + public void ParsesWhitespaceOnlyStringsInArrays() + { + // Arrange + var input = +""" +items[2]: " "," " +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[" "," "]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses strings with delimiters in arrays")] + public void ParsesStringsWithDelimitersInArrays() + { + // Arrange + var input = +""" +items[3]: a,"b,c","d:e" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":["a","b,c","d:e"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses strings that look like primitives when quoted")] + public void ParsesStringsThatLookLikePrimitivesWhenQuoted() + { + // Arrange + var input = +""" +items[4]: x,"true","42","-3.14" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":["x","true","42","-3.14"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses strings with structural tokens in arrays")] + public void ParsesStringsWithStructuralTokensInArrays() + { + // Arrange + var input = +""" +items[3]: "[5]","- item","{key}" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":["[5]","- item","{key}"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted key with inline array")] + public void ParsesQuotedKeyWithInlineArray() + { + // Arrange + var input = +""" +"my-key"[3]: 1,2,3 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"my-key":[1,2,3]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted key containing brackets with inline array")] + public void ParsesQuotedKeyContainingBracketsWithInlineArray() + { + // Arrange + var input = +""" +"key[test]"[3]: 1,2,3 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"key[test]":[1,2,3]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted key with empty array")] + public void ParsesQuotedKeyWithEmptyArray() + { + // Arrange + var input = +""" +"x-custom"[0]: +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"x-custom":[]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + +} diff --git a/tests/ToonFormat.Tests/Decode/ArraysTabular.cs b/tests/ToonFormat.Tests/Decode/ArraysTabular.cs new file mode 100644 index 0000000..98d809a --- /dev/null +++ b/tests/ToonFormat.Tests/Decode/ArraysTabular.cs @@ -0,0 +1,156 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Decode; + + +[Trait("Category", "decode")] +public class ArraysTabular +{ + [Fact] + [Trait("Description", "parses tabular arrays of uniform objects")] + public void ParsesTabularArraysOfUniformObjects() + { + // Arrange + var input = +""" +items[2]{sku,qty,price}: + A1,2,9.99 + B2,1,14.5 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"sku":"A1","qty":2,"price":9.99},{"sku":"B2","qty":1,"price":14.5}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses nulls and quoted values in tabular rows")] + public void ParsesNullsAndQuotedValuesInTabularRows() + { + // Arrange + var input = +""" +items[2]{id,value}: + 1,null + 2,"test" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"id":1,"value":null},{"id":2,"value":"test"}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted colon in tabular row as data")] + public void ParsesQuotedColonInTabularRowAsData() + { + // Arrange + var input = +""" +items[2]{id,note}: + 1,"a:b" + 2,"c:d" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"id":1,"note":"a:b"},{"id":2,"note":"c:d"}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted header keys in tabular arrays")] + public void ParsesQuotedHeaderKeysInTabularArrays() + { + // Arrange + var input = +""" +items[2]{"order:id","full name"}: + 1,Ada + 2,Bob +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"order:id":1,"full name":"Ada"},{"order:id":2,"full name":"Bob"}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted key with tabular array format")] + public void ParsesQuotedKeyWithTabularArrayFormat() + { + // Arrange + var input = +""" +"x-items"[2]{id,name}: + 1,Ada + 2,Bob +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"x-items":[{"id":1,"name":"Ada"},{"id":2,"name":"Bob"}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "unquoted colon terminates tabular rows and starts key-value pair")] + public void UnquotedColonTerminatesTabularRowsAndStartsKeyValuePair() + { + // Arrange + var input = +""" +items[2]{id,name}: + 1,Alice + 2,Bob +count: 2 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}],"count":2} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + +} diff --git a/tests/ToonFormat.Tests/Decode/BlankLines.cs b/tests/ToonFormat.Tests/Decode/BlankLines.cs new file mode 100644 index 0000000..c845dfb --- /dev/null +++ b/tests/ToonFormat.Tests/Decode/BlankLines.cs @@ -0,0 +1,373 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Decode; + + +[Trait("Category", "decode")] +public class BlankLines +{ + [Fact] + [Trait("Description", "throws on blank line inside list array")] + public void ThrowsOnBlankLineInsideListArray() + { + // Arrange + var input = +""" +items[3]: + - a + + - b + - c +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + Assert.Throws(() => ToonDecoder.Decode(input, options)); + } + + [Fact] + [Trait("Description", "throws on blank line inside tabular array")] + public void ThrowsOnBlankLineInsideTabularArray() + { + // Arrange + var input = +""" +items[2]{id}: + 1 + + 2 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + Assert.Throws(() => ToonDecoder.Decode(input, options)); + } + + [Fact] + [Trait("Description", "throws on multiple blank lines inside array")] + public void ThrowsOnMultipleBlankLinesInsideArray() + { + // Arrange + var input = +""" +items[2]: + - a + + + - b +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + Assert.Throws(() => ToonDecoder.Decode(input, options)); + } + + [Fact] + [Trait("Description", "throws on blank line with spaces inside array")] + public void ThrowsOnBlankLineWithSpacesInsideArray() + { + // Arrange + var input = +""" +items[2]: + - a + + - b +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + Assert.Throws(() => ToonDecoder.Decode(input, options)); + } + + [Fact] + [Trait("Description", "throws on blank line in nested list array")] + public void ThrowsOnBlankLineInNestedListArray() + { + // Arrange + var input = +""" +outer[2]: + - inner[2]: + - a + + - b + - x +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + Assert.Throws(() => ToonDecoder.Decode(input, options)); + } + + [Fact] + [Trait("Description", "accepts blank line between root-level fields")] + public void AcceptsBlankLineBetweenRootLevelFields() + { + // Arrange + var input = +""" +a: 1 + +b: 2 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"a":1,"b":2} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "accepts trailing newline at end of file")] + public void AcceptsTrailingNewlineAtEndOfFile() + { + // Arrange + var input = +""" +a: 1 + +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"a":1} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "accepts multiple trailing newlines")] + public void AcceptsMultipleTrailingNewlines() + { + // Arrange + var input = +""" +a: 1 + + + +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"a":1} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "accepts blank line after array ends")] + public void AcceptsBlankLineAfterArrayEnds() + { + // Arrange + var input = +""" +items[1]: + - a + +b: 2 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"items":["a"],"b":2} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "accepts blank line between nested object fields")] + public void AcceptsBlankLineBetweenNestedObjectFields() + { + // Arrange + var input = +""" +a: + b: 1 + + c: 2 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"a":{"b":1,"c":2}} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "ignores blank lines inside list array when strict=false")] + public void IgnoresBlankLinesInsideListArrayWhenStrictFalse() + { + // Arrange + var input = +""" +items[3]: + - a + + - b + - c +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = false, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"items":["a","b","c"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "ignores blank lines inside tabular array when strict=false")] + public void IgnoresBlankLinesInsideTabularArrayWhenStrictFalse() + { + // Arrange + var input = +""" +items[2]{id,name}: + 1,Alice + + 2,Bob +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = false, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"items":[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "ignores multiple blank lines in arrays when strict=false")] + public void IgnoresMultipleBlankLinesInArraysWhenStrictFalse() + { + // Arrange + var input = +""" +items[2]: + - a + + + - b +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = false, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"items":["a","b"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + +} diff --git a/tests/ToonFormat.Tests/Decode/Delimiters.cs b/tests/ToonFormat.Tests/Decode/Delimiters.cs new file mode 100644 index 0000000..2f1a576 --- /dev/null +++ b/tests/ToonFormat.Tests/Decode/Delimiters.cs @@ -0,0 +1,629 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Decode; + + +[Trait("Category", "decode")] +public class Delimiters +{ + [Fact] + [Trait("Description", "parses primitive arrays with tab delimiter")] + public void ParsesPrimitiveArraysWithTabDelimiter() + { + // Arrange + var input = +""" +tags[3 ]: reading gaming coding +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"tags":["reading","gaming","coding"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses primitive arrays with pipe delimiter")] + public void ParsesPrimitiveArraysWithPipeDelimiter() + { + // Arrange + var input = +""" +tags[3|]: reading|gaming|coding +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"tags":["reading","gaming","coding"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses primitive arrays with comma delimiter")] + public void ParsesPrimitiveArraysWithCommaDelimiter() + { + // Arrange + var input = +""" +tags[3]: reading,gaming,coding +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"tags":["reading","gaming","coding"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses tabular arrays with tab delimiter")] + public void ParsesTabularArraysWithTabDelimiter() + { + // Arrange + var input = +""" +items[2 ]{sku qty price}: + A1 2 9.99 + B2 1 14.5 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"sku":"A1","qty":2,"price":9.99},{"sku":"B2","qty":1,"price":14.5}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses tabular arrays with pipe delimiter")] + public void ParsesTabularArraysWithPipeDelimiter() + { + // Arrange + var input = +""" +items[2|]{sku|qty|price}: + A1|2|9.99 + B2|1|14.5 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"sku":"A1","qty":2,"price":9.99},{"sku":"B2","qty":1,"price":14.5}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses nested arrays with tab delimiter")] + public void ParsesNestedArraysWithTabDelimiter() + { + // Arrange + var input = +""" +pairs[2 ]: + - [2 ]: a b + - [2 ]: c d +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"pairs":[["a","b"],["c","d"]]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses nested arrays with pipe delimiter")] + public void ParsesNestedArraysWithPipeDelimiter() + { + // Arrange + var input = +""" +pairs[2|]: + - [2|]: a|b + - [2|]: c|d +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"pairs":[["a","b"],["c","d"]]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "nested arrays inside list items default to comma delimiter")] + public void NestedArraysInsideListItemsDefaultToCommaDelimiter() + { + // Arrange + var input = +""" +items[1 ]: + - tags[3]: a,b,c +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"tags":["a","b","c"]}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "nested arrays inside list items default to comma with pipe parent")] + public void NestedArraysInsideListItemsDefaultToCommaWithPipeParent() + { + // Arrange + var input = +""" +items[1|]: + - tags[3]: a,b,c +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"tags":["a","b","c"]}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses root arrays with tab delimiter")] + public void ParsesRootArraysWithTabDelimiter() + { + // Arrange + var input = +""" +[3 ]: x y z +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +["x","y","z"] +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses root arrays with pipe delimiter")] + public void ParsesRootArraysWithPipeDelimiter() + { + // Arrange + var input = +""" +[3|]: x|y|z +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +["x","y","z"] +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses root arrays of objects with tab delimiter")] + public void ParsesRootArraysOfObjectsWithTabDelimiter() + { + // Arrange + var input = +""" +[2 ]{id}: + 1 + 2 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +[{"id":1},{"id":2}] +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses root arrays of objects with pipe delimiter")] + public void ParsesRootArraysOfObjectsWithPipeDelimiter() + { + // Arrange + var input = +""" +[2|]{id}: + 1 + 2 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +[{"id":1},{"id":2}] +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses values containing tab delimiter when quoted")] + public void ParsesValuesContainingTabDelimiterWhenQuoted() + { + // Arrange + var input = +""" +items[3 ]: a "b\tc" d +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":["a","b\tc","d"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses values containing pipe delimiter when quoted")] + public void ParsesValuesContainingPipeDelimiterWhenQuoted() + { + // Arrange + var input = +""" +items[3|]: a|"b|c"|d +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":["a","b|c","d"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "does not split on commas when using tab delimiter")] + public void DoesNotSplitOnCommasWhenUsingTabDelimiter() + { + // Arrange + var input = +""" +items[2 ]: a,b c,d +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":["a,b","c,d"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "does not split on commas when using pipe delimiter")] + public void DoesNotSplitOnCommasWhenUsingPipeDelimiter() + { + // Arrange + var input = +""" +items[2|]: a,b|c,d +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":["a,b","c,d"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses tabular values containing comma with comma delimiter")] + public void ParsesTabularValuesContainingCommaWithCommaDelimiter() + { + // Arrange + var input = +""" +items[2]{id,note}: + 1,"a,b" + 2,"c,d" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"id":1,"note":"a,b"},{"id":2,"note":"c,d"}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "does not require quoting commas with tab delimiter")] + public void DoesNotRequireQuotingCommasWithTabDelimiter() + { + // Arrange + var input = +""" +items[2 ]{id note}: + 1 a,b + 2 c,d +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"id":1,"note":"a,b"},{"id":2,"note":"c,d"}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "does not require quoting commas in object values")] + public void DoesNotRequireQuotingCommasInObjectValues() + { + // Arrange + var input = +""" +note: a,b +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"note":"a,b"} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "object values in list items follow document delimiter")] + public void ObjectValuesInListItemsFollowDocumentDelimiter() + { + // Arrange + var input = +""" +items[2 ]: + - status: a,b + - status: c,d +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"status":"a,b"},{"status":"c,d"}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "object values with comma must be quoted when document delimiter is comma")] + public void ObjectValuesWithCommaMustBeQuotedWhenDocumentDelimiterIsComma() + { + // Arrange + var input = +""" +items[2]: + - status: "a,b" + - status: "c,d" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"status":"a,b"},{"status":"c,d"}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses nested array values containing pipe delimiter")] + public void ParsesNestedArrayValuesContainingPipeDelimiter() + { + // Arrange + var input = +""" +pairs[1|]: + - [2|]: a|"b|c" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"pairs":[["a","b|c"]]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses nested array values containing tab delimiter")] + public void ParsesNestedArrayValuesContainingTabDelimiter() + { + // Arrange + var input = +""" +pairs[1 ]: + - [2 ]: a "b\tc" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"pairs":[["a","b\tc"]]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "preserves quoted ambiguity with pipe delimiter")] + public void PreservesQuotedAmbiguityWithPipeDelimiter() + { + // Arrange + var input = +""" +items[3|]: "true"|"42"|"-3.14" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":["true","42","-3.14"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "preserves quoted ambiguity with tab delimiter")] + public void PreservesQuotedAmbiguityWithTabDelimiter() + { + // Arrange + var input = +""" +items[3 ]: "true" "42" "-3.14" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":["true","42","-3.14"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses structural-looking strings when quoted with pipe delimiter")] + public void ParsesStructuralLookingStringsWhenQuotedWithPipeDelimiter() + { + // Arrange + var input = +""" +items[3|]: "[5]"|"{key}"|"- item" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":["[5]","{key}","- item"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses structural-looking strings when quoted with tab delimiter")] + public void ParsesStructuralLookingStringsWhenQuotedWithTabDelimiter() + { + // Arrange + var input = +""" +items[3 ]: "[5]" "{key}" "- item" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":["[5]","{key}","- item"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses tabular headers with keys containing the active delimiter")] + public void ParsesTabularHeadersWithKeysContainingTheActiveDelimiter() + { + // Arrange + var input = +""" +items[2|]{"a|b"}: + 1 + 2 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"a|b":1},{"a|b":2}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + +} diff --git a/tests/ToonFormat.Tests/Decode/IndentationErrors.cs b/tests/ToonFormat.Tests/Decode/IndentationErrors.cs new file mode 100644 index 0000000..b305875 --- /dev/null +++ b/tests/ToonFormat.Tests/Decode/IndentationErrors.cs @@ -0,0 +1,393 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Decode; + + +[Trait("Category", "decode")] +public class IndentationErrors +{ + [Fact] + [Trait("Description", "throws when object field has non-multiple indentation (3 spaces with indent=2)")] + public void ThrowsWhenObjectFieldHasNonMultipleIndentation3SpacesWithIndent2() + { + // Arrange + var input = +""" +a: + b: 1 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + Assert.Throws(() => ToonDecoder.Decode(input, options)); + } + + [Fact] + [Trait("Description", "throws when list item has non-multiple indentation (3 spaces with indent=2)")] + public void ThrowsWhenListItemHasNonMultipleIndentation3SpacesWithIndent2() + { + // Arrange + var input = +""" +items[2]: + - id: 1 + - id: 2 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + Assert.Throws(() => ToonDecoder.Decode(input, options)); + } + + [Fact] + [Trait("Description", "throws with custom indent size when non-multiple (3 spaces with indent=4)")] + public void ThrowsWithCustomIndentSizeWhenNonMultiple3SpacesWithIndent4() + { + // Arrange + var input = +""" +a: + b: 1 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 4, + Strict = true, + }; + + Assert.Throws(() => ToonDecoder.Decode(input, options)); + } + + [Fact] + [Trait("Description", "accepts correct indentation with custom indent size (4 spaces with indent=4)")] + public void AcceptsCorrectIndentationWithCustomIndentSize4SpacesWithIndent4() + { + // Arrange + var input = +""" +a: + b: 1 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 4, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"a":{"b":1}} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "throws when tab character used in indentation")] + public void ThrowsWhenTabCharacterUsedInIndentation() + { + // Arrange + var input = +""" +a: + b: 1 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + Assert.Throws(() => ToonDecoder.Decode(input, options)); + } + + [Fact] + [Trait("Description", "throws when mixed tabs and spaces in indentation")] + public void ThrowsWhenMixedTabsAndSpacesInIndentation() + { + // Arrange + var input = +""" +a: + b: 1 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + Assert.Throws(() => ToonDecoder.Decode(input, options)); + } + + [Fact] + [Trait("Description", "throws when tab at start of line")] + public void ThrowsWhenTabAtStartOfLine() + { + // Arrange + var input = +""" + a: 1 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + Assert.Throws(() => ToonDecoder.Decode(input, options)); + } + + [Fact] + [Trait("Description", "accepts tabs in quoted string values")] + public void AcceptsTabsInQuotedStringValues() + { + // Arrange + var input = +""" +text: "hello world" +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"text":"hello\tworld"} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "accepts tabs in quoted keys")] + public void AcceptsTabsInQuotedKeys() + { + // Arrange + var input = +""" +"key tab": value +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"key\ttab":"value"} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "accepts tabs in quoted array elements")] + public void AcceptsTabsInQuotedArrayElements() + { + // Arrange + var input = +""" +items[2]: "a b","c d" +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"items":["a\tb","c\td"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "accepts non-multiple indentation when strict=false")] + public void AcceptsNonMultipleIndentationWhenStrictFalse() + { + // Arrange + var input = +""" +a: + b: 1 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = false, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"a":{"b":1}} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "accepts deeply nested non-multiples when strict=false")] + public void AcceptsDeeplyNestedNonMultiplesWhenStrictFalse() + { + // Arrange + var input = +""" +a: + b: + c: 1 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = false, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"a":{"b":{"c":1}}} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "empty lines do not trigger validation errors")] + public void EmptyLinesDoNotTriggerValidationErrors() + { + // Arrange + var input = +""" +a: 1 + +b: 2 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"a":1,"b":2} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "root-level content (0 indentation) is always valid")] + public void RootLevelContent0IndentationIsAlwaysValid() + { + // Arrange + var input = +""" +a: 1 +b: 2 +c: 3 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"a":1,"b":2,"c":3} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "lines with only spaces are not validated if empty")] + public void LinesWithOnlySpacesAreNotValidatedIfEmpty() + { + // Arrange + var input = +""" +a: 1 + +b: 2 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"a":1,"b":2} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + +} diff --git a/tests/ToonFormat.Tests/Decode/Numbers.cs b/tests/ToonFormat.Tests/Decode/Numbers.cs new file mode 100644 index 0000000..15ccdcb --- /dev/null +++ b/tests/ToonFormat.Tests/Decode/Numbers.cs @@ -0,0 +1,377 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Decode; + + +[Trait("Category", "decode")] +public class Numbers +{ + [Fact] + [Trait("Description", "parses number with trailing zeros in fractional part")] + public void ParsesNumberWithTrailingZerosInFractionalPart() + { + // Arrange + var input = +""" +value: 1.5000 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"value":1.5} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses negative number with positive exponent")] + public void ParsesNegativeNumberWithPositiveExponent() + { + // Arrange + var input = +""" +value: -1E+03 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"value":-1000} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses lowercase exponent")] + public void ParsesLowercaseExponent() + { + // Arrange + var input = +""" +value: 2.5e2 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"value":250} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses uppercase exponent with negative sign")] + public void ParsesUppercaseExponentWithNegativeSign() + { + // Arrange + var input = +""" +value: 3E-02 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"value":0.03} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses negative zero as zero")] + public void ParsesNegativeZeroAsZero() + { + // Arrange + var input = +""" +value: -0 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"value":0} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses negative zero with fractional part")] + public void ParsesNegativeZeroWithFractionalPart() + { + // Arrange + var input = +""" +value: -0.0 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"value":0} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses array with mixed numeric forms")] + public void ParsesArrayWithMixedNumericForms() + { + // Arrange + var input = +""" +nums[5]: 42,-1E+03,1.5000,-0,2.5e2 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"nums":[42,-1000,1.5,0,250]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "treats leading zero as string not number")] + public void TreatsLeadingZeroAsStringNotNumber() + { + // Arrange + var input = +""" +value: 05 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"value":"05"} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses very small exponent")] + public void ParsesVerySmallExponent() + { + // Arrange + var input = +""" +value: 1e-10 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"value":0.0000000001} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses integer with positive exponent")] + public void ParsesIntegerWithPositiveExponent() + { + // Arrange + var input = +""" +value: 5E+00 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"value":5} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses exponent notation")] + public void ParsesExponentNotation() + { + // Arrange + var input = +""" +1e6 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = 1000000; + + Assert.Equal(result, expected); + } + + [Fact] + [Trait("Description", "parses exponent notation with uppercase E")] + public void ParsesExponentNotationWithUppercaseE() + { + // Arrange + var input = +""" +1E+6 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = 1000000; + + Assert.Equal(result, expected); + } + + [Fact] + [Trait("Description", "parses negative exponent notation")] + public void ParsesNegativeExponentNotation() + { + // Arrange + var input = +""" +-1e-3 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = -0.001; + + Assert.Equal(result, expected); + } + + [Fact] + [Trait("Description", "treats unquoted leading-zero number as string")] + public void TreatsUnquotedLeadingZeroNumberAsString() + { + // Arrange + var input = +""" +05 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"05" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "treats unquoted multi-leading-zero as string")] + public void TreatsUnquotedMultiLeadingZeroAsString() + { + // Arrange + var input = +""" +007 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"007" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "treats unquoted octal-like as string")] + public void TreatsUnquotedOctalLikeAsString() + { + // Arrange + var input = +""" +0123 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"0123" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "treats leading-zero in object value as string")] + public void TreatsLeadingZeroInObjectValueAsString() + { + // Arrange + var input = +""" +a: 05 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"a":"05"} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "treats leading-zeros in array as strings")] + public void TreatsLeadingZerosInArrayAsStrings() + { + // Arrange + var input = +""" +nums[3]: 05,007,0123 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"nums":["05","007","0123"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + +} diff --git a/tests/ToonFormat.Tests/Decode/Objects.cs b/tests/ToonFormat.Tests/Decode/Objects.cs new file mode 100644 index 0000000..c156aa3 --- /dev/null +++ b/tests/ToonFormat.Tests/Decode/Objects.cs @@ -0,0 +1,588 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Decode; + + +[Trait("Category", "decode")] +public class Objects +{ + [Fact] + [Trait("Description", "parses objects with primitive values")] + public void ParsesObjectsWithPrimitiveValues() + { + // Arrange + var input = +""" +id: 123 +name: Ada +active: true +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"id":123,"name":"Ada","active":true} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses null values in objects")] + public void ParsesNullValuesInObjects() + { + // Arrange + var input = +""" +id: 123 +value: null +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"id":123,"value":null} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses empty nested object header")] + public void ParsesEmptyNestedObjectHeader() + { + // Arrange + var input = +""" +user: +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"user":{}} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted object value with colon")] + public void ParsesQuotedObjectValueWithColon() + { + // Arrange + var input = +""" +note: "a:b" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"note":"a:b"} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted object value with comma")] + public void ParsesQuotedObjectValueWithComma() + { + // Arrange + var input = +""" +note: "a,b" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"note":"a,b"} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted object value with newline escape")] + public void ParsesQuotedObjectValueWithNewlineEscape() + { + // Arrange + var input = +""" +text: "line1\nline2" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"text":"line1\nline2"} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted object value with escaped quotes")] + public void ParsesQuotedObjectValueWithEscapedQuotes() + { + // Arrange + var input = +""" +text: "say \"hello\"" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"text":"say \u0022hello\u0022"} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted object value with leading/trailing spaces")] + public void ParsesQuotedObjectValueWithLeadingTrailingSpaces() + { + // Arrange + var input = +""" +text: " padded " +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"text":" padded "} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted object value with only spaces")] + public void ParsesQuotedObjectValueWithOnlySpaces() + { + // Arrange + var input = +""" +text: " " +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"text":" "} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted string value that looks like true")] + public void ParsesQuotedStringValueThatLooksLikeTrue() + { + // Arrange + var input = +""" +v: "true" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"v":"true"} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted string value that looks like integer")] + public void ParsesQuotedStringValueThatLooksLikeInteger() + { + // Arrange + var input = +""" +v: "42" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"v":"42"} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted string value that looks like negative decimal")] + public void ParsesQuotedStringValueThatLooksLikeNegativeDecimal() + { + // Arrange + var input = +""" +v: "-7.5" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"v":"-7.5"} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted key with colon")] + public void ParsesQuotedKeyWithColon() + { + // Arrange + var input = +""" +"order:id": 7 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"order:id":7} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted key with brackets")] + public void ParsesQuotedKeyWithBrackets() + { + // Arrange + var input = +""" +"[index]": 5 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"[index]":5} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted key with braces")] + public void ParsesQuotedKeyWithBraces() + { + // Arrange + var input = +""" +"{key}": 5 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"{key}":5} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted key with comma")] + public void ParsesQuotedKeyWithComma() + { + // Arrange + var input = +""" +"a,b": 1 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"a,b":1} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted key with spaces")] + public void ParsesQuotedKeyWithSpaces() + { + // Arrange + var input = +""" +"full name": Ada +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"full name":"Ada"} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted key with leading hyphen")] + public void ParsesQuotedKeyWithLeadingHyphen() + { + // Arrange + var input = +""" +"-lead": 1 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"-lead":1} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted key with leading and trailing spaces")] + public void ParsesQuotedKeyWithLeadingAndTrailingSpaces() + { + // Arrange + var input = +""" +" a ": 1 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{" a ":1} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted numeric key")] + public void ParsesQuotedNumericKey() + { + // Arrange + var input = +""" +"123": x +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"123":"x"} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted empty string key")] + public void ParsesQuotedEmptyStringKey() + { + // Arrange + var input = +""" +"": 1 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"":1} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses dotted keys as identifiers")] + public void ParsesDottedKeysAsIdentifiers() + { + // Arrange + var input = +""" +user.name: Ada +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"user.name":"Ada"} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses underscore-prefixed keys")] + public void ParsesUnderscorePrefixedKeys() + { + // Arrange + var input = +""" +_private: 1 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"_private":1} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses underscore-containing keys")] + public void ParsesUnderscoreContainingKeys() + { + // Arrange + var input = +""" +user_name: 1 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"user_name":1} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "unescapes newline in key")] + public void UnescapesNewlineInKey() + { + // Arrange + var input = +""" +"line\nbreak": 1 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"line\nbreak":1} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "unescapes tab in key")] + public void UnescapesTabInKey() + { + // Arrange + var input = +""" +"tab\there": 2 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"tab\there":2} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "unescapes quotes in key")] + public void UnescapesQuotesInKey() + { + // Arrange + var input = +""" +"he said \"hi\"": 1 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"he said \u0022hi\u0022":1} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses deeply nested objects with indentation")] + public void ParsesDeeplyNestedObjectsWithIndentation() + { + // Arrange + var input = +""" +a: + b: + c: deep +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"a":{"b":{"c":"deep"}}} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + +} diff --git a/tests/ToonFormat.Tests/Decode/PathExpansion.cs b/tests/ToonFormat.Tests/Decode/PathExpansion.cs new file mode 100644 index 0000000..4c9f580 --- /dev/null +++ b/tests/ToonFormat.Tests/Decode/PathExpansion.cs @@ -0,0 +1,332 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Decode; + + +[Trait("Category", "decode")] +public class PathExpansion +{ + [Fact] + [Trait("Description", "expands dotted key to nested object in safe mode")] + public void ExpandsDottedKeyToNestedObjectInSafeMode() + { + // Arrange + var input = +""" +a.b.c: 1 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"a":{"b":{"c":1}}} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "expands dotted key with inline array")] + public void ExpandsDottedKeyWithInlineArray() + { + // Arrange + var input = +""" +data.meta.items[2]: a,b +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"data":{"meta":{"items":["a","b"]}}} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "expands dotted key with tabular array")] + public void ExpandsDottedKeyWithTabularArray() + { + // Arrange + var input = +""" +a.b.items[2]{id,name}: + 1,A + 2,B +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"a":{"b":{"items":[{"id":1,"name":"A"},{"id":2,"name":"B"}]}}} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "preserves literal dotted keys when expansion is off")] + public void PreservesLiteralDottedKeysWhenExpansionIsOff() + { + // Arrange + var input = +""" +user.name: Ada +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"user.name":"Ada"} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "expands and deep-merges preserving document-order insertion")] + public void ExpandsAndDeepMergesPreservingDocumentOrderInsertion() + { + // Arrange + var input = +""" +a.b.c: 1 +a.b.d: 2 +a.e: 3 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"a":{"b":{"c":1,"d":2},"e":3}} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "throws on expansion conflict (object vs primitive) when strict=true")] + public void ThrowsOnExpansionConflictObjectVsPrimitiveWhenStrictTrue() + { + // Arrange + var input = +""" +a.b: 1 +a: 2 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + Assert.Throws(() => ToonDecoder.Decode(input, options)); + } + + [Fact] + [Trait("Description", "throws on expansion conflict (object vs array) when strict=true")] + public void ThrowsOnExpansionConflictObjectVsArrayWhenStrictTrue() + { + // Arrange + var input = +""" +a.b: 1 +a[2]: 2,3 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + Assert.Throws(() => ToonDecoder.Decode(input, options)); + } + + [Fact] + [Trait("Description", "applies LWW when strict=false (primitive overwrites expanded object)")] + public void AppliesLwwWhenStrictFalsePrimitiveOverwritesExpandedObject() + { + // Arrange + var input = +""" +a.b: 1 +a: 2 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = false, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"a":2} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "applies LWW when strict=false (expanded object overwrites primitive)")] + public void AppliesLwwWhenStrictFalseExpandedObjectOverwritesPrimitive() + { + // Arrange + var input = +""" +a: 1 +a.b: 2 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = false, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"a":{"b":2}} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "preserves quoted dotted key as literal when expandPaths=safe")] + public void PreservesQuotedDottedKeyAsLiteralWhenExpandpathsSafe() + { + // Arrange + var input = +""" +a.b: 1 +"c.d": 2 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"a":{"b":1},"c.d":2} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "preserves non-IdentifierSegment keys as literals")] + public void PreservesNonIdentifiersegmentKeysAsLiterals() + { + // Arrange + var input = +""" +full-name.x: 1 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"full-name.x":1} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "expands keys creating empty nested objects")] + public void ExpandsKeysCreatingEmptyNestedObjects() + { + // Arrange + var input = +""" +a.b.c: +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{"a":{"b":{"c":{}}}} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + +} diff --git a/tests/ToonFormat.Tests/Decode/Primitives.cs b/tests/ToonFormat.Tests/Decode/Primitives.cs new file mode 100644 index 0000000..fb80c5f --- /dev/null +++ b/tests/ToonFormat.Tests/Decode/Primitives.cs @@ -0,0 +1,515 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Decode; + + +[Trait("Category", "decode")] +public class Primitives +{ + [Fact] + [Trait("Description", "parses safe unquoted string")] + public void ParsesSafeUnquotedString() + { + // Arrange + var input = +""" +hello +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"hello" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses unquoted string with underscore and numbers")] + public void ParsesUnquotedStringWithUnderscoreAndNumbers() + { + // Arrange + var input = +""" +Ada_99 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"Ada_99" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses empty quoted string")] + public void ParsesEmptyQuotedString() + { + // Arrange + var input = +""" +"" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = string.Empty; + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted string with newline escape")] + public void ParsesQuotedStringWithNewlineEscape() + { + // Arrange + var input = +""" +"line1\nline2" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"line1\nline2" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted string with tab escape")] + public void ParsesQuotedStringWithTabEscape() + { + // Arrange + var input = +""" +"tab\there" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"tab\there" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted string with carriage return escape")] + public void ParsesQuotedStringWithCarriageReturnEscape() + { + // Arrange + var input = +""" +"return\rcarriage" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"return\rcarriage" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted string with backslash escape")] + public void ParsesQuotedStringWithBackslashEscape() + { + // Arrange + var input = +""" +"C:\\Users\\path" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"C:\\Users\\path" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses quoted string with escaped quotes")] + public void ParsesQuotedStringWithEscapedQuotes() + { + // Arrange + var input = +""" +"say \"hello\"" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"say \u0022hello\u0022" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses Unicode string")] + public void ParsesUnicodeString() + { + // Arrange + var input = +""" +café +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"caf\u00E9" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses Chinese characters")] + public void ParsesChineseCharacters() + { + // Arrange + var input = +""" +你好 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"\u4F60\u597D" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses emoji")] + public void ParsesEmoji() + { + // Arrange + var input = +""" +🚀 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"\uD83D\uDE80" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses string with emoji and spaces")] + public void ParsesStringWithEmojiAndSpaces() + { + // Arrange + var input = +""" +hello 👋 world +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"hello \uD83D\uDC4B world" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses positive integer")] + public void ParsesPositiveInteger() + { + // Arrange + var input = +""" +42 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = 42; + + Assert.Equal(result, expected); + } + + [Fact] + [Trait("Description", "parses decimal number")] + public void ParsesDecimalNumber() + { + // Arrange + var input = +""" +3.14 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = 3.14; + + Assert.Equal(result, expected); + } + + [Fact] + [Trait("Description", "parses negative integer")] + public void ParsesNegativeInteger() + { + // Arrange + var input = +""" +-7 +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = -7; + + Assert.Equal(result, expected); + } + + [Fact] + [Trait("Description", "parses true")] + public void ParsesTrue() + { + // Arrange + var input = +""" +true +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +true +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses false")] + public void ParsesFalse() + { + // Arrange + var input = +""" +false +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +false +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "parses null")] + public void ParsesNull() + { + // Arrange + var input = +""" +null +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +null +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "respects ambiguity quoting for true")] + public void RespectsAmbiguityQuotingForTrue() + { + // Arrange + var input = +""" +"true" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"true" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "respects ambiguity quoting for false")] + public void RespectsAmbiguityQuotingForFalse() + { + // Arrange + var input = +""" +"false" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"false" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "respects ambiguity quoting for null")] + public void RespectsAmbiguityQuotingForNull() + { + // Arrange + var input = +""" +"null" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"null" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "respects ambiguity quoting for integer")] + public void RespectsAmbiguityQuotingForInteger() + { + // Arrange + var input = +""" +"42" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"42" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "respects ambiguity quoting for negative decimal")] + public void RespectsAmbiguityQuotingForNegativeDecimal() + { + // Arrange + var input = +""" +"-3.14" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"-3.14" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "respects ambiguity quoting for scientific notation")] + public void RespectsAmbiguityQuotingForScientificNotation() + { + // Arrange + var input = +""" +"1e-6" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"1e-6" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "respects ambiguity quoting for leading-zero")] + public void RespectsAmbiguityQuotingForLeadingZero() + { + // Arrange + var input = +""" +"05" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +"05" +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + +} diff --git a/tests/ToonFormat.Tests/Decode/RootForm.cs b/tests/ToonFormat.Tests/Decode/RootForm.cs new file mode 100644 index 0000000..08e8a01 --- /dev/null +++ b/tests/ToonFormat.Tests/Decode/RootForm.cs @@ -0,0 +1,49 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Decode; + + +[Trait("Category", "decode")] +public class RootForm +{ + [Fact] + [Trait("Description", "empty document decodes to empty object")] + public void EmptyDocumentDecodesToEmptyObject() + { + // Arrange + var input = +""" + +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + var result = ToonDecoder.Decode(input, options); + + var expected = JsonNode.Parse(""" +{} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + +} diff --git a/tests/ToonFormat.Tests/Decode/ValidationErrors.cs b/tests/ToonFormat.Tests/Decode/ValidationErrors.cs new file mode 100644 index 0000000..94298ba --- /dev/null +++ b/tests/ToonFormat.Tests/Decode/ValidationErrors.cs @@ -0,0 +1,187 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Decode; + + +[Trait("Category", "decode")] +public class ValidationErrors +{ + [Fact] + [Trait("Description", "throws on array length mismatch (inline primitives - too many)")] + public void ThrowsOnArrayLengthMismatchInlinePrimitivesTooMany() + { + // Arrange + var input = +""" +tags[2]: a,b,c +"""; + + // Act & Assert + Assert.Throws(() => ToonDecoder.Decode(input)); + } + + [Fact] + [Trait("Description", "throws on array length mismatch (list format - too many)")] + public void ThrowsOnArrayLengthMismatchListFormatTooMany() + { + // Arrange + var input = +""" +items[1]: + - 1 + - 2 +"""; + + // Act & Assert + Assert.Throws(() => ToonDecoder.Decode(input)); + } + + [Fact] + [Trait("Description", "throws when tabular row value count does not match header field count")] + public void ThrowsWhenTabularRowValueCountDoesNotMatchHeaderFieldCount() + { + // Arrange + var input = +""" +items[2]{id,name}: + 1,Ada + 2 +"""; + + // Act & Assert + Assert.Throws(() => ToonDecoder.Decode(input)); + } + + [Fact] + [Trait("Description", "throws when tabular row count does not match header length")] + public void ThrowsWhenTabularRowCountDoesNotMatchHeaderLength() + { + // Arrange + var input = +""" +[1]{id}: + 1 + 2 +"""; + + // Act & Assert + Assert.Throws(() => ToonDecoder.Decode(input)); + } + + [Fact] + [Trait("Description", "throws on invalid escape sequence")] + public void ThrowsOnInvalidEscapeSequence() + { + // Arrange + var input = +""" +"a\x" +"""; + + // Act & Assert + Assert.Throws(() => ToonDecoder.Decode(input)); + } + + [Fact] + [Trait("Description", "throws on unterminated string")] + public void ThrowsOnUnterminatedString() + { + // Arrange + var input = +""" +"unterminated +"""; + + // Act & Assert + Assert.Throws(() => ToonDecoder.Decode(input)); + } + + [Fact] + [Trait("Description", "throws on missing colon in key-value context")] + public void ThrowsOnMissingColonInKeyValueContext() + { + // Arrange + var input = +""" +a: + user +"""; + + // Act & Assert + Assert.Throws(() => ToonDecoder.Decode(input)); + } + + [Fact] + [Trait("Description", "throws on two primitives at root depth in strict mode")] + public void ThrowsOnTwoPrimitivesAtRootDepthInStrictMode() + { + // Arrange + var input = +""" +hello +world +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + Assert.Throws(() => ToonDecoder.Decode(input, options)); + } + + [Fact] + [Trait("Description", "throws on delimiter mismatch (header declares tab, row uses comma)")] + public void ThrowsOnDelimiterMismatchHeaderDeclaresTabRowUsesComma() + { + // Arrange + var input = +""" +items[2 ]{a b}: + 1,2 + 3,4 +"""; + + // Act & Assert + Assert.Throws(() => ToonDecoder.Decode(input)); + } + + [Fact] + [Trait("Description", "throws on mismatched delimiter between bracket and brace fields")] + public void ThrowsOnMismatchedDelimiterBetweenBracketAndBraceFields() + { + // Arrange + var input = +""" +items[2 ]{a,b}: + 1 2 + 3 4 +"""; + + // Act & Assert + var options = new ToonDecodeOptions + { + Indent = 2, + Strict = true, + }; + + Assert.Throws(() => ToonDecoder.Decode(input, options)); + } + +} diff --git a/tests/ToonFormat.Tests/Decode/Whitespace.cs b/tests/ToonFormat.Tests/Decode/Whitespace.cs new file mode 100644 index 0000000..676a4f7 --- /dev/null +++ b/tests/ToonFormat.Tests/Decode/Whitespace.cs @@ -0,0 +1,145 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Decode; + + +[Trait("Category", "decode")] +public class Whitespace +{ + [Fact] + [Trait("Description", "tolerates spaces around commas in inline arrays")] + public void ToleratesSpacesAroundCommasInInlineArrays() + { + // Arrange + var input = +""" +tags[3]: a , b , c +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"tags":["a","b","c"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "tolerates spaces around pipes in inline arrays")] + public void ToleratesSpacesAroundPipesInInlineArrays() + { + // Arrange + var input = +""" +tags[3|]: a | b | c +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"tags":["a","b","c"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "tolerates spaces around tabs in inline arrays")] + public void ToleratesSpacesAroundTabsInInlineArrays() + { + // Arrange + var input = +""" +tags[3 ]: a b c +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"tags":["a","b","c"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "tolerates leading and trailing spaces in tabular row values")] + public void ToleratesLeadingAndTrailingSpacesInTabularRowValues() + { + // Arrange + var input = +""" +items[2]{id,name}: + 1 , Alice + 2 , Bob +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "tolerates spaces around delimiters with quoted values")] + public void ToleratesSpacesAroundDelimitersWithQuotedValues() + { + // Arrange + var input = +""" +items[3]: "a" , "b" , "c" +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":["a","b","c"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + + [Fact] + [Trait("Description", "empty tokens decode to empty string")] + public void EmptyTokensDecodeToEmptyString() + { + // Arrange + var input = +""" +items[3]: a,,c +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":["a","","c"]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + +} diff --git a/tests/ToonFormat.Tests/Encode/ArraysNested.cs b/tests/ToonFormat.Tests/Encode/ArraysNested.cs new file mode 100644 index 0000000..6f58f8e --- /dev/null +++ b/tests/ToonFormat.Tests/Encode/ArraysNested.cs @@ -0,0 +1,428 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Encode; + + +[Trait("Category", "encode")] +public class ArraysNested +{ + [Fact] + [Trait("Description", "encodes nested arrays of primitives")] + public void EncodesNestedArraysOfPrimitives() + { + // Arrange + var input = + new + { + @pairs =new object[] { + new object[] { + @"a", + @"b", + } + , + new object[] { + @"c", + @"d", + } + , + } +, + } + ; + + var expected = +""" +pairs[2]: + - [2]: a,b + - [2]: c,d +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes strings containing delimiters in nested arrays")] + public void QuotesStringsContainingDelimitersInNestedArrays() + { + // Arrange + var input = + new + { + @pairs =new object[] { + new object[] { + @"a", + @"b", + } + , + new object[] { + @"c,d", + @"e:f", + @"true", + } + , + } +, + } + ; + + var expected = +""" +pairs[2]: + - [2]: a,b + - [3]: "c,d","e:f","true" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes empty inner arrays")] + public void EncodesEmptyInnerArrays() + { + // Arrange + var input = + new + { + @pairs =new object[] { + new object[] { + } + , + new object[] { + } + , + } +, + } + ; + + var expected = +""" +pairs[2]: + - [0]: + - [0]: +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes mixed-length inner arrays")] + public void EncodesMixedLengthInnerArrays() + { + // Arrange + var input = + new + { + @pairs =new object[] { + new object[] { + 1, + } + , + new object[] { + 2, + 3, + } + , + } +, + } + ; + + var expected = +""" +pairs[2]: + - [1]: 1 + - [2]: 2,3 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes root-level primitive array")] + public void EncodesRootLevelPrimitiveArray() + { + // Arrange + var input = + new object[] { + @"x", + @"y", + @"true", + true, + 10, + } + ; + + var expected = +""" +[5]: x,y,"true",true,10 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes root-level array of uniform objects in tabular format")] + public void EncodesRootLevelArrayOfUniformObjectsInTabularFormat() + { + // Arrange + var input = + new object[] { + new + { + @id = 1, + } + , + new + { + @id = 2, + } + , + } + ; + + var expected = +""" +[2]{id}: + 1 + 2 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes root-level array of non-uniform objects in list format")] + public void EncodesRootLevelArrayOfNonUniformObjectsInListFormat() + { + // Arrange + var input = + new object[] { + new + { + @id = 1, + } + , + new + { + @id = 2, + @name = @"Ada", + } + , + } + ; + + var expected = +""" +[2]: + - id: 1 + - id: 2 + name: Ada +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes empty root-level array")] + public void EncodesEmptyRootLevelArray() + { + // Arrange + var input = + new object[] { + } + ; + + var expected = +""" +[0]: +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes root-level arrays of arrays")] + public void EncodesRootLevelArraysOfArrays() + { + // Arrange + var input = + new object[] { + new object[] { + 1, + 2, + } + , + new object[] { + } + , + } + ; + + var expected = +""" +[2]: + - [2]: 1,2 + - [0]: +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes complex nested structure")] + public void EncodesComplexNestedStructure() + { + // Arrange + var input = + new + { + @user = + new + { + @id = 123, + @name = @"Ada", + @tags =new object[] { + @"reading", + @"gaming", + } +, + @active = true, + @prefs =new object[] { + } +, + } +, + } + ; + + var expected = +""" +user: + id: 123 + name: Ada + tags[2]: reading,gaming + active: true + prefs[0]: +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "uses list format for arrays mixing primitives and objects")] + public void UsesListFormatForArraysMixingPrimitivesAndObjects() + { + // Arrange + var input = + new + { + @items =new object[] { + 1, + new + { + @a = 1, + } + , + @"text", + } +, + } + ; + + var expected = +""" +items[3]: + - 1 + - a: 1 + - text +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "uses list format for arrays mixing objects and arrays")] + public void UsesListFormatForArraysMixingObjectsAndArrays() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @a = 1, + } + , + new object[] { + 1, + 2, + } + , + } +, + } + ; + + var expected = +""" +items[2]: + - a: 1 + - [2]: 1,2 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + +} diff --git a/tests/ToonFormat.Tests/Encode/ArraysObjects.cs b/tests/ToonFormat.Tests/Encode/ArraysObjects.cs new file mode 100644 index 0000000..50aadf6 --- /dev/null +++ b/tests/ToonFormat.Tests/Encode/ArraysObjects.cs @@ -0,0 +1,616 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Encode; + + +[Trait("Category", "encode")] +public class ArraysObjects +{ + [Fact] + [Trait("Description", "uses list format for objects with different fields")] + public void UsesListFormatForObjectsWithDifferentFields() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @id = 1, + @name = @"First", + } + , + new + { + @id = 2, + @name = @"Second", + @extra = true, + } + , + } +, + } + ; + + var expected = +""" +items[2]: + - id: 1 + name: First + - id: 2 + name: Second + extra: true +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "uses list format for objects with nested values")] + public void UsesListFormatForObjectsWithNestedValues() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @id = 1, + @nested = + new + { + @x = 1, + } +, + } + , + } +, + } + ; + + var expected = +""" +items[1]: + - id: 1 + nested: + x: 1 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "preserves field order in list items - array first")] + public void PreservesFieldOrderInListItemsArrayFirst() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @nums =new object[] { + 1, + 2, + 3, + } +, + @name = @"test", + } + , + } +, + } + ; + + var expected = +""" +items[1]: + - nums[3]: 1,2,3 + name: test +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "preserves field order in list items - primitive first")] + public void PreservesFieldOrderInListItemsPrimitiveFirst() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @name = @"test", + @nums =new object[] { + 1, + 2, + 3, + } +, + } + , + } +, + } + ; + + var expected = +""" +items[1]: + - name: test + nums[3]: 1,2,3 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "uses list format for objects containing arrays of arrays")] + public void UsesListFormatForObjectsContainingArraysOfArrays() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @matrix =new object[] { + new object[] { + 1, + 2, + } + , + new object[] { + 3, + 4, + } + , + } +, + @name = @"grid", + } + , + } +, + } + ; + + var expected = +""" +items[1]: + - matrix[2]: + - [2]: 1,2 + - [2]: 3,4 + name: grid +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "uses tabular format for nested uniform object arrays")] + public void UsesTabularFormatForNestedUniformObjectArrays() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @users =new object[] { + new + { + @id = 1, + @name = @"Ada", + } + , + new + { + @id = 2, + @name = @"Bob", + } + , + } +, + @status = @"active", + } + , + } +, + } + ; + + var expected = +""" +items[1]: + - users[2]{id,name}: + 1,Ada + 2,Bob + status: active +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "uses list format for nested object arrays with mismatched keys")] + public void UsesListFormatForNestedObjectArraysWithMismatchedKeys() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @users =new object[] { + new + { + @id = 1, + @name = @"Ada", + } + , + new + { + @id = 2, + } + , + } +, + @status = @"active", + } + , + } +, + } + ; + + var expected = +""" +items[1]: + - users[2]: + - id: 1 + name: Ada + - id: 2 + status: active +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "uses list format for objects with multiple array fields")] + public void UsesListFormatForObjectsWithMultipleArrayFields() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @nums =new object[] { + 1, + 2, + } +, + @tags =new object[] { + @"a", + @"b", + } +, + @name = @"test", + } + , + } +, + } + ; + + var expected = +""" +items[1]: + - nums[2]: 1,2 + tags[2]: a,b + name: test +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "uses list format for objects with only array fields")] + public void UsesListFormatForObjectsWithOnlyArrayFields() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @nums =new object[] { + 1, + 2, + 3, + } +, + @tags =new object[] { + @"a", + @"b", + } +, + } + , + } +, + } + ; + + var expected = +""" +items[1]: + - nums[3]: 1,2,3 + tags[2]: a,b +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes objects with empty arrays in list format")] + public void EncodesObjectsWithEmptyArraysInListFormat() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @name = @"test", + @data =new object[] { + } +, + } + , + } +, + } + ; + + var expected = +""" +items[1]: + - name: test + data[0]: +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "places first field of nested tabular arrays on hyphen line")] + public void PlacesFirstFieldOfNestedTabularArraysOnHyphenLine() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @users =new object[] { + new + { + @id = 1, + } + , + new + { + @id = 2, + } + , + } +, + @note = @"x", + } + , + } +, + } + ; + + var expected = +""" +items[1]: + - users[2]{id}: + 1 + 2 + note: x +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "places empty arrays on hyphen line when first")] + public void PlacesEmptyArraysOnHyphenLineWhenFirst() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @data =new object[] { + } +, + @name = @"x", + } + , + } +, + } + ; + + var expected = +""" +items[1]: + - data[0]: + name: x +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "uses field order from first object for tabular headers")] + public void UsesFieldOrderFromFirstObjectForTabularHeaders() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @a = 1, + @b = 2, + @c = 3, + } + , + new + { + @c = 30, + @b = 20, + @a = 10, + } + , + } +, + } + ; + + var expected = +""" +items[2]{a,b,c}: + 1,2,3 + 10,20,30 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "uses list format when one object has nested column")] + public void UsesListFormatWhenOneObjectHasNestedColumn() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @id = 1, + @data = @"string", + } + , + new + { + @id = 2, + @data = + new + { + @nested = true, + } +, + } + , + } +, + } + ; + + var expected = +""" +items[2]: + - id: 1 + data: string + - id: 2 + data: + nested: true +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + +} diff --git a/tests/ToonFormat.Tests/Encode/ArraysPrimitive.cs b/tests/ToonFormat.Tests/Encode/ArraysPrimitive.cs new file mode 100644 index 0000000..add5fc7 --- /dev/null +++ b/tests/ToonFormat.Tests/Encode/ArraysPrimitive.cs @@ -0,0 +1,298 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Encode; + + +[Trait("Category", "encode")] +public class ArraysPrimitive +{ + [Fact] + [Trait("Description", "encodes string arrays inline")] + public void EncodesStringArraysInline() + { + // Arrange + var input = + new + { + @tags =new object[] { + @"reading", + @"gaming", + } +, + } + ; + + var expected = +""" +tags[2]: reading,gaming +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes number arrays inline")] + public void EncodesNumberArraysInline() + { + // Arrange + var input = + new + { + @nums =new object[] { + 1, + 2, + 3, + } +, + } + ; + + var expected = +""" +nums[3]: 1,2,3 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes mixed primitive arrays inline")] + public void EncodesMixedPrimitiveArraysInline() + { + // Arrange + var input = + new + { + @data =new object[] { + @"x", + @"y", + true, + 10, + } +, + } + ; + + var expected = +""" +data[4]: x,y,true,10 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes empty arrays")] + public void EncodesEmptyArrays() + { + // Arrange + var input = + new + { + @items =new object[] { + } +, + } + ; + + var expected = +""" +items[0]: +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes empty string in single-item array")] + public void EncodesEmptyStringInSingleItemArray() + { + // Arrange + var input = + new + { + @items =new object[] { + @"", + } +, + } + ; + + var expected = +""" +items[1]: "" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes empty string in multi-item array")] + public void EncodesEmptyStringInMultiItemArray() + { + // Arrange + var input = + new + { + @items =new object[] { + @"a", + @"", + @"b", + } +, + } + ; + + var expected = +""" +items[3]: a,"",b +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes whitespace-only strings in arrays")] + public void EncodesWhitespaceOnlyStringsInArrays() + { + // Arrange + var input = + new + { + @items =new object[] { + @" ", + @" ", + } +, + } + ; + + var expected = +""" +items[2]: " "," " +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes array strings with comma")] + public void QuotesArrayStringsWithComma() + { + // Arrange + var input = + new + { + @items =new object[] { + @"a", + @"b,c", + @"d:e", + } +, + } + ; + + var expected = +""" +items[3]: a,"b,c","d:e" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes strings that look like booleans in arrays")] + public void QuotesStringsThatLookLikeBooleansInArrays() + { + // Arrange + var input = + new + { + @items =new object[] { + @"x", + @"true", + @"42", + @"-3.14", + } +, + } + ; + + var expected = +""" +items[4]: x,"true","42","-3.14" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes strings with structural meanings in arrays")] + public void QuotesStringsWithStructuralMeaningsInArrays() + { + // Arrange + var input = + new + { + @items =new object[] { + @"[5]", + @"- item", + @"{key}", + } +, + } + ; + + var expected = +""" +items[3]: "[5]","- item","{key}" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + +} diff --git a/tests/ToonFormat.Tests/Encode/ArraysTabular.cs b/tests/ToonFormat.Tests/Encode/ArraysTabular.cs new file mode 100644 index 0000000..4241130 --- /dev/null +++ b/tests/ToonFormat.Tests/Encode/ArraysTabular.cs @@ -0,0 +1,182 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Encode; + + +[Trait("Category", "encode")] +public class ArraysTabular +{ + [Fact] + [Trait("Description", "encodes arrays of similar objects in tabular format")] + public void EncodesArraysOfSimilarObjectsInTabularFormat() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @sku = @"A1", + @qty = 2, + @price = 9.99, + } + , + new + { + @sku = @"B2", + @qty = 1, + @price = 14.5, + } + , + } +, + } + ; + + var expected = +""" +items[2]{sku,qty,price}: + A1,2,9.99 + B2,1,14.5 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes null values in tabular format")] + public void EncodesNullValuesInTabularFormat() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @id = 1, + @value = (string)null, } + , + new + { + @id = 2, + @value = @"test", + } + , + } +, + } + ; + + var expected = +""" +items[2]{id,value}: + 1,null + 2,test +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes strings containing delimiters in tabular rows")] + public void QuotesStringsContainingDelimitersInTabularRows() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @sku = @"A,1", + @desc = @"cool", + @qty = 2, + } + , + new + { + @sku = @"B2", + @desc = @"wip: test", + @qty = 1, + } + , + } +, + } + ; + + var expected = +""" +items[2]{sku,desc,qty}: + "A,1",cool,2 + B2,"wip: test",1 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes ambiguous strings in tabular rows")] + public void QuotesAmbiguousStringsInTabularRows() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @id = 1, + @status = @"true", + } + , + new + { + @id = 2, + @status = @"false", + } + , + } +, + } + ; + + var expected = +""" +items[2]{id,status}: + 1,"true" + 2,"false" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + +} diff --git a/tests/ToonFormat.Tests/Encode/Delimiters.cs b/tests/ToonFormat.Tests/Encode/Delimiters.cs new file mode 100644 index 0000000..3eee6c7 --- /dev/null +++ b/tests/ToonFormat.Tests/Encode/Delimiters.cs @@ -0,0 +1,833 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Encode; + + +[Trait("Category", "encode")] +public class Delimiters +{ + [Fact] + [Trait("Description", "encodes primitive arrays with tab delimiter")] + public void EncodesPrimitiveArraysWithTabDelimiter() + { + // Arrange + var input = + new + { + @tags =new object[] { + @"reading", + @"gaming", + @"coding", + } +, + } + ; + + var expected = +""" +tags[3 ]: reading gaming coding +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.TAB, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes primitive arrays with pipe delimiter")] + public void EncodesPrimitiveArraysWithPipeDelimiter() + { + // Arrange + var input = + new + { + @tags =new object[] { + @"reading", + @"gaming", + @"coding", + } +, + } + ; + + var expected = +""" +tags[3|]: reading|gaming|coding +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.PIPE, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes primitive arrays with comma delimiter")] + public void EncodesPrimitiveArraysWithCommaDelimiter() + { + // Arrange + var input = + new + { + @tags =new object[] { + @"reading", + @"gaming", + @"coding", + } +, + } + ; + + var expected = +""" +tags[3]: reading,gaming,coding +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.COMMA, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes tabular arrays with tab delimiter")] + public void EncodesTabularArraysWithTabDelimiter() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @sku = @"A1", + @qty = 2, + @price = 9.99, + } + , + new + { + @sku = @"B2", + @qty = 1, + @price = 14.5, + } + , + } +, + } + ; + + var expected = +""" +items[2 ]{sku qty price}: + A1 2 9.99 + B2 1 14.5 +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.TAB, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes tabular arrays with pipe delimiter")] + public void EncodesTabularArraysWithPipeDelimiter() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @sku = @"A1", + @qty = 2, + @price = 9.99, + } + , + new + { + @sku = @"B2", + @qty = 1, + @price = 14.5, + } + , + } +, + } + ; + + var expected = +""" +items[2|]{sku|qty|price}: + A1|2|9.99 + B2|1|14.5 +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.PIPE, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes nested arrays with tab delimiter")] + public void EncodesNestedArraysWithTabDelimiter() + { + // Arrange + var input = + new + { + @pairs =new object[] { + new object[] { + @"a", + @"b", + } + , + new object[] { + @"c", + @"d", + } + , + } +, + } + ; + + var expected = +""" +pairs[2 ]: + - [2 ]: a b + - [2 ]: c d +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.TAB, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes nested arrays with pipe delimiter")] + public void EncodesNestedArraysWithPipeDelimiter() + { + // Arrange + var input = + new + { + @pairs =new object[] { + new object[] { + @"a", + @"b", + } + , + new object[] { + @"c", + @"d", + } + , + } +, + } + ; + + var expected = +""" +pairs[2|]: + - [2|]: a|b + - [2|]: c|d +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.PIPE, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes root arrays with tab delimiter")] + public void EncodesRootArraysWithTabDelimiter() + { + // Arrange + var input = + new object[] { + @"x", + @"y", + @"z", + } + ; + + var expected = +""" +[3 ]: x y z +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.TAB, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes root arrays with pipe delimiter")] + public void EncodesRootArraysWithPipeDelimiter() + { + // Arrange + var input = + new object[] { + @"x", + @"y", + @"z", + } + ; + + var expected = +""" +[3|]: x|y|z +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.PIPE, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes root arrays of objects with tab delimiter")] + public void EncodesRootArraysOfObjectsWithTabDelimiter() + { + // Arrange + var input = + new object[] { + new + { + @id = 1, + } + , + new + { + @id = 2, + } + , + } + ; + + var expected = +""" +[2 ]{id}: + 1 + 2 +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.TAB, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes root arrays of objects with pipe delimiter")] + public void EncodesRootArraysOfObjectsWithPipeDelimiter() + { + // Arrange + var input = + new object[] { + new + { + @id = 1, + } + , + new + { + @id = 2, + } + , + } + ; + + var expected = +""" +[2|]{id}: + 1 + 2 +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.PIPE, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes strings containing tab delimiter")] + public void QuotesStringsContainingTabDelimiter() + { + // Arrange + var input = + new + { + @items =new object[] { + @"a", + @"b c", + @"d", + } +, + } + ; + + var expected = +""" +items[3 ]: a "b\tc" d +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.TAB, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes strings containing pipe delimiter")] + public void QuotesStringsContainingPipeDelimiter() + { + // Arrange + var input = + new + { + @items =new object[] { + @"a", + @"b|c", + @"d", + } +, + } + ; + + var expected = +""" +items[3|]: a|"b|c"|d +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.PIPE, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "does not quote commas with tab delimiter")] + public void DoesNotQuoteCommasWithTabDelimiter() + { + // Arrange + var input = + new + { + @items =new object[] { + @"a,b", + @"c,d", + } +, + } + ; + + var expected = +""" +items[2 ]: a,b c,d +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.TAB, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "does not quote commas with pipe delimiter")] + public void DoesNotQuoteCommasWithPipeDelimiter() + { + // Arrange + var input = + new + { + @items =new object[] { + @"a,b", + @"c,d", + } +, + } + ; + + var expected = +""" +items[2|]: a,b|c,d +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.PIPE, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes tabular values containing comma delimiter")] + public void QuotesTabularValuesContainingCommaDelimiter() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @id = 1, + @note = @"a,b", + } + , + new + { + @id = 2, + @note = @"c,d", + } + , + } +, + } + ; + + var expected = +""" +items[2]{id,note}: + 1,"a,b" + 2,"c,d" +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.COMMA, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "does not quote commas in tabular values with tab delimiter")] + public void DoesNotQuoteCommasInTabularValuesWithTabDelimiter() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @id = 1, + @note = @"a,b", + } + , + new + { + @id = 2, + @note = @"c,d", + } + , + } +, + } + ; + + var expected = +""" +items[2 ]{id note}: + 1 a,b + 2 c,d +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.TAB, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "does not quote commas in object values with pipe delimiter")] + public void DoesNotQuoteCommasInObjectValuesWithPipeDelimiter() + { + // Arrange + var input = + new + { + @note = @"a,b", + } + ; + + var expected = +""" +note: a,b +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.PIPE, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "does not quote commas in object values with tab delimiter")] + public void DoesNotQuoteCommasInObjectValuesWithTabDelimiter() + { + // Arrange + var input = + new + { + @note = @"a,b", + } + ; + + var expected = +""" +note: a,b +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.TAB, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes nested array values containing pipe delimiter")] + public void QuotesNestedArrayValuesContainingPipeDelimiter() + { + // Arrange + var input = + new + { + @pairs =new object[] { + new object[] { + @"a", + @"b|c", + } + , + } +, + } + ; + + var expected = +""" +pairs[1|]: + - [2|]: a|"b|c" +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.PIPE, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes nested array values containing tab delimiter")] + public void QuotesNestedArrayValuesContainingTabDelimiter() + { + // Arrange + var input = + new + { + @pairs =new object[] { + new object[] { + @"a", + @"b c", + } + , + } +, + } + ; + + var expected = +""" +pairs[1 ]: + - [2 ]: a "b\tc" +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.TAB, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "preserves ambiguity quoting regardless of delimiter")] + public void PreservesAmbiguityQuotingRegardlessOfDelimiter() + { + // Arrange + var input = + new + { + @items =new object[] { + @"true", + @"42", + @"-3.14", + } +, + } + ; + + var expected = +""" +items[3|]: "true"|"42"|"-3.14" +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.PIPE, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + +} diff --git a/tests/ToonFormat.Tests/Encode/KeyFolding.cs b/tests/ToonFormat.Tests/Encode/KeyFolding.cs new file mode 100644 index 0000000..f1ba7ce --- /dev/null +++ b/tests/ToonFormat.Tests/Encode/KeyFolding.cs @@ -0,0 +1,502 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Encode; + + +[Trait("Category", "encode")] +public class KeyFolding +{ + [Fact] + [Trait("Description", "encodes folded chain to primitive (safe mode)")] + public void EncodesFoldedChainToPrimitiveSafeMode() + { + // Arrange + var input = + new + { + @a = + new + { + @b = + new + { + @c = 1, + } +, + } +, + } + ; + + var expected = +""" +a.b.c: 1 +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.COMMA, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes folded chain with inline array")] + public void EncodesFoldedChainWithInlineArray() + { + // Arrange + var input = + new + { + @data = + new + { + @meta = + new + { + @items =new object[] { + @"x", + @"y", + } +, + } +, + } +, + } + ; + + var expected = +""" +data.meta.items[2]: x,y +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.COMMA, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes folded chain with tabular array")] + public void EncodesFoldedChainWithTabularArray() + { + // Arrange + var input = + new + { + @a = + new + { + @b = + new + { + @items =new object[] { + new + { + @id = 1, + @name = @"A", + } + , + new + { + @id = 2, + @name = @"B", + } + , + } +, + } +, + } +, + } + ; + + var expected = +""" +a.b.items[2]{id,name}: + 1,A + 2,B +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.COMMA, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes partial folding with flattenDepth=2")] + public void EncodesPartialFoldingWithFlattendepth2() + { + // Arrange + var input = + new + { + @a = + new + { + @b = + new + { + @c = + new + { + @d = 1, + } +, + } +, + } +, + } + ; + + var expected = +""" +a.b: + c: + d: 1 +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.COMMA, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes full chain with flattenDepth=Infinity (default)")] + public void EncodesFullChainWithFlattendepthInfinityDefault() + { + // Arrange + var input = + new + { + @a = + new + { + @b = + new + { + @c = + new + { + @d = 1, + } +, + } +, + } +, + } + ; + + var expected = +""" +a.b.c.d: 1 +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.COMMA, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes standard nesting with flattenDepth=0 (no folding)")] + public void EncodesStandardNestingWithFlattendepth0NoFolding() + { + // Arrange + var input = + new + { + @a = + new + { + @b = + new + { + @c = 1, + } +, + } +, + } + ; + + var expected = +""" +a: + b: + c: 1 +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.COMMA, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes standard nesting with flattenDepth=1 (no practical effect)")] + public void EncodesStandardNestingWithFlattendepth1NoPracticalEffect() + { + // Arrange + var input = + new + { + @a = + new + { + @b = + new + { + @c = 1, + } +, + } +, + } + ; + + var expected = +""" +a: + b: + c: 1 +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.COMMA, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes standard nesting with keyFolding=off (baseline)")] + public void EncodesStandardNestingWithKeyfoldingOffBaseline() + { + // Arrange + var input = + new + { + @a = + new + { + @b = + new + { + @c = 1, + } +, + } +, + } + ; + + var expected = +""" +a: + b: + c: 1 +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.COMMA, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes folded chain ending with empty object")] + public void EncodesFoldedChainEndingWithEmptyObject() + { + // Arrange + var input = + new + { + @a = + new + { + @b = + new + { + @c = + new + { + } +, + } +, + } +, + } + ; + + var expected = +""" +a.b.c: +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.COMMA, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "stops folding at array boundary (not single-key object)")] + public void StopsFoldingAtArrayBoundaryNotSingleKeyObject() + { + // Arrange + var input = + new + { + @a = + new + { + @b =new object[] { + 1, + 2, + } +, + } +, + } + ; + + var expected = +""" +a.b[2]: 1,2 +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.COMMA, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes folded chains preserving sibling field order")] + public void EncodesFoldedChainsPreservingSiblingFieldOrder() + { + // Arrange + var input = + new + { + @first = + new + { + @second = + new + { + @third = 1, + } +, + } +, + @simple = 2, + @short = + new + { + @path = 3, + } +, + } + ; + + var expected = +""" +first.second.third: 1 +simple: 2 +short.path: 3 +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.COMMA, + Indent = 2, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + +} diff --git a/tests/ToonFormat.Tests/Encode/Objects.cs b/tests/ToonFormat.Tests/Encode/Objects.cs new file mode 100644 index 0000000..2ff8277 --- /dev/null +++ b/tests/ToonFormat.Tests/Encode/Objects.cs @@ -0,0 +1,366 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Encode; + + +[Trait("Category", "encode")] +public class Objects +{ + [Fact] + [Trait("Description", "preserves key order in objects")] + public void PreservesKeyOrderInObjects() + { + // Arrange + var input = + new + { + @id = 123, + @name = @"Ada", + @active = true, + } + ; + + var expected = +""" +id: 123 +name: Ada +active: true +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes null values in objects")] + public void EncodesNullValuesInObjects() + { + // Arrange + var input = + new + { + @id = 123, + @value = (string)null, } + ; + + var expected = +""" +id: 123 +value: null +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes empty objects as empty string")] + public void EncodesEmptyObjectsAsEmptyString() + { + // Arrange + var input = + new + { + } + ; + + var expected = +""" + +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string value with colon")] + public void QuotesStringValueWithColon() + { + // Arrange + var input = + new + { + @note = @"a:b", + } + ; + + var expected = +""" +note: "a:b" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string value with comma")] + public void QuotesStringValueWithComma() + { + // Arrange + var input = + new + { + @note = @"a,b", + } + ; + + var expected = +""" +note: "a,b" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string value with newline")] + public void QuotesStringValueWithNewline() + { + // Arrange + var input = + new + { + @text = @"line1 +line2", + } + ; + + var expected = +""" +text: "line1\nline2" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string value with embedded quotes")] + public void QuotesStringValueWithEmbeddedQuotes() + { + // Arrange + var input = + new + { + @text = @"say ""hello""", + } + ; + + var expected = +""" +text: "say \"hello\"" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string value with leading space")] + public void QuotesStringValueWithLeadingSpace() + { + // Arrange + var input = + new + { + @text = @" padded ", + } + ; + + var expected = +""" +text: " padded " +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string value with only spaces")] + public void QuotesStringValueWithOnlySpaces() + { + // Arrange + var input = + new + { + @text = @" ", + } + ; + + var expected = +""" +text: " " +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string value that looks like true")] + public void QuotesStringValueThatLooksLikeTrue() + { + // Arrange + var input = + new + { + @v = @"true", + } + ; + + var expected = +""" +v: "true" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string value that looks like number")] + public void QuotesStringValueThatLooksLikeNumber() + { + // Arrange + var input = + new + { + @v = @"42", + } + ; + + var expected = +""" +v: "42" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string value that looks like negative decimal")] + public void QuotesStringValueThatLooksLikeNegativeDecimal() + { + // Arrange + var input = + new + { + @v = @"-7.5", + } + ; + + var expected = +""" +v: "-7.5" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes deeply nested objects")] + public void EncodesDeeplyNestedObjects() + { + // Arrange + var input = + new + { + @a = + new + { + @b = + new + { + @c = @"deep", + } +, + } +, + } + ; + + var expected = +""" +a: + b: + c: deep +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes empty nested object")] + public void EncodesEmptyNestedObject() + { + // Arrange + var input = + new + { + @user = + new + { + } +, + } + ; + + var expected = +""" +user: +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + +} diff --git a/tests/ToonFormat.Tests/Encode/Primitives.cs b/tests/ToonFormat.Tests/Encode/Primitives.cs new file mode 100644 index 0000000..e2af880 --- /dev/null +++ b/tests/ToonFormat.Tests/Encode/Primitives.cs @@ -0,0 +1,770 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Encode; + + +[Trait("Category", "encode")] +public class Primitives +{ + [Fact] + [Trait("Description", "encodes safe strings without quotes")] + public void EncodesSafeStringsWithoutQuotes() + { + // Arrange + var input = + @"hello" ; + + var expected = +""" +hello +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes safe string with underscore and numbers")] + public void EncodesSafeStringWithUnderscoreAndNumbers() + { + // Arrange + var input = + @"Ada_99" ; + + var expected = +""" +Ada_99 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes empty string")] + public void QuotesEmptyString() + { + // Arrange + var input = + @"" ; + + var expected = +""" +"" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string that looks like true")] + public void QuotesStringThatLooksLikeTrue() + { + // Arrange + var input = + @"true" ; + + var expected = +""" +"true" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string that looks like false")] + public void QuotesStringThatLooksLikeFalse() + { + // Arrange + var input = + @"false" ; + + var expected = +""" +"false" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string that looks like null")] + public void QuotesStringThatLooksLikeNull() + { + // Arrange + var input = + @"null" ; + + var expected = +""" +"null" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string that looks like integer")] + public void QuotesStringThatLooksLikeInteger() + { + // Arrange + var input = + @"42" ; + + var expected = +""" +"42" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string that looks like negative decimal")] + public void QuotesStringThatLooksLikeNegativeDecimal() + { + // Arrange + var input = + @"-3.14" ; + + var expected = +""" +"-3.14" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string that looks like scientific notation")] + public void QuotesStringThatLooksLikeScientificNotation() + { + // Arrange + var input = + @"1e-6" ; + + var expected = +""" +"1e-6" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string with leading zero")] + public void QuotesStringWithLeadingZero() + { + // Arrange + var input = + @"05" ; + + var expected = +""" +"05" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "escapes newline in string")] + public void EscapesNewlineInString() + { + // Arrange + var input = + @"line1 +line2" ; + + var expected = +""" +"line1\nline2" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "escapes tab in string")] + public void EscapesTabInString() + { + // Arrange + var input = + @"tab here" ; + + var expected = +""" +"tab\there" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "escapes carriage return in string")] + public void EscapesCarriageReturnInString() + { + // Arrange + var input = + @"return carriage" ; + + var expected = +""" +"return\rcarriage" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "escapes backslash in string")] + public void EscapesBackslashInString() + { + // Arrange + var input = + @"C:\Users\path" ; + + var expected = +""" +"C:\\Users\\path" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string with array-like syntax")] + public void QuotesStringWithArrayLikeSyntax() + { + // Arrange + var input = + @"[3]: x,y" ; + + var expected = +""" +"[3]: x,y" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string starting with hyphen-space")] + public void QuotesStringStartingWithHyphenSpace() + { + // Arrange + var input = + @"- item" ; + + var expected = +""" +"- item" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes single hyphen as object value")] + public void QuotesSingleHyphenAsObjectValue() + { + // Arrange + var input = + new + { + @marker = @"-", + } + ; + + var expected = +""" +marker: "-" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string starting with hyphen as object value")] + public void QuotesStringStartingWithHyphenAsObjectValue() + { + // Arrange + var input = + new + { + @note = @"- item", + } + ; + + var expected = +""" +note: "- item" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes single hyphen in array")] + public void QuotesSingleHyphenInArray() + { + // Arrange + var input = + new + { + @items =new object[] { + @"-", + } +, + } + ; + + var expected = +""" +items[1]: "-" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes leading-hyphen string in array")] + public void QuotesLeadingHyphenStringInArray() + { + // Arrange + var input = + new + { + @tags =new object[] { + @"a", + @"- item", + @"b", + } +, + } + ; + + var expected = +""" +tags[3]: a,"- item",b +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string with bracket notation")] + public void QuotesStringWithBracketNotation() + { + // Arrange + var input = + @"[test]" ; + + var expected = +""" +"[test]" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "quotes string with brace notation")] + public void QuotesStringWithBraceNotation() + { + // Arrange + var input = + @"{key}" ; + + var expected = +""" +"{key}" +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes Unicode string without quotes")] + public void EncodesUnicodeStringWithoutQuotes() + { + // Arrange + var input = + @"café" ; + + var expected = +""" +café +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes Chinese characters without quotes")] + public void EncodesChineseCharactersWithoutQuotes() + { + // Arrange + var input = + @"你好" ; + + var expected = +""" +你好 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes emoji without quotes")] + public void EncodesEmojiWithoutQuotes() + { + // Arrange + var input = + @"🚀" ; + + var expected = +""" +🚀 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes string with emoji and spaces")] + public void EncodesStringWithEmojiAndSpaces() + { + // Arrange + var input = + @"hello 👋 world" ; + + var expected = +""" +hello 👋 world +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes positive integer")] + public void EncodesPositiveInteger() + { + // Arrange + var input = + 42 ; + + var expected = +""" +42 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes decimal number")] + public void EncodesDecimalNumber() + { + // Arrange + var input = + 3.14 ; + + var expected = +""" +3.14 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes negative integer")] + public void EncodesNegativeInteger() + { + // Arrange + var input = + -7 ; + + var expected = +""" +-7 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes zero")] + public void EncodesZero() + { + // Arrange + var input = + 0 ; + + var expected = +""" +0 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes negative zero as zero")] + public void EncodesNegativeZeroAsZero() + { + // Arrange + var input = + -0 ; + + var expected = +""" +0 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes scientific notation as decimal")] + public void EncodesScientificNotationAsDecimal() + { + // Arrange + var input = + 1000000 ; + + var expected = +""" +1000000 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes small decimal from scientific notation")] + public void EncodesSmallDecimalFromScientificNotation() + { + // Arrange + var input = + 0.000001 ; + + var expected = +""" +0.000001 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes MAX_SAFE_INTEGER")] + public void EncodesMaxSafeInteger() + { + // Arrange + var input = + 9007199254740991 ; + + var expected = +""" +9007199254740991 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes repeating decimal with full precision")] + public void EncodesRepeatingDecimalWithFullPrecision() + { + // Arrange + var input = + 0.3333333333333333 ; + + var expected = +""" +0.3333333333333333 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes true")] + public void EncodesTrue() + { + // Arrange + var input = + true ; + + var expected = +""" +true +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes false")] + public void EncodesFalse() + { + // Arrange + var input = + false ; + + var expected = +""" +false +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "encodes null")] + public void EncodesNull() + { + // Arrange + var input = + (string)null ; + + var expected = +""" +null +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + +} diff --git a/tests/ToonFormat.Tests/Encode/Whitespace.cs b/tests/ToonFormat.Tests/Encode/Whitespace.cs new file mode 100644 index 0000000..b5a321f --- /dev/null +++ b/tests/ToonFormat.Tests/Encode/Whitespace.cs @@ -0,0 +1,120 @@ +// +// This code was generated by ToonFormat.SpecGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; + + +namespace ToonFormat.Tests.Encode; + + +[Trait("Category", "encode")] +public class Whitespace +{ + [Fact] + [Trait("Description", "produces no trailing newline at end of output")] + public void ProducesNoTrailingNewlineAtEndOfOutput() + { + // Arrange + var input = + new + { + @id = 123, + } + ; + + var expected = +""" +id: 123 +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "maintains proper indentation for nested structures")] + public void MaintainsProperIndentationForNestedStructures() + { + // Arrange + var input = + new + { + @user = + new + { + @id = 123, + @name = @"Ada", + } +, + @items =new object[] { + @"a", + @"b", + } +, + } + ; + + var expected = +""" +user: + id: 123 + name: Ada +items[2]: a,b +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "respects custom indent size option")] + public void RespectsCustomIndentSizeOption() + { + // Arrange + var input = + new + { + @user = + new + { + @name = @"Ada", + @role = @"admin", + } +, + } + ; + + var expected = +""" +user: + name: Ada + role: admin +"""; + + // Act & Assert + var options = new ToonEncodeOptions + { + Delimiter = ToonDelimiter.COMMA, + Indent = 4, + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } + +} diff --git a/tests/ToonFormat.Tests/ToonDecoderTests.cs b/tests/ToonFormat.Tests/ToonDecoderTests.cs index fbcbc1b..d2091af 100644 --- a/tests/ToonFormat.Tests/ToonDecoderTests.cs +++ b/tests/ToonFormat.Tests/ToonDecoderTests.cs @@ -31,15 +31,15 @@ public void Decode_PrimitiveTypes_ReturnsCorrectValues() // String var stringResult = ToonDecoder.Decode("hello"); Assert.Equal("hello", stringResult?.GetValue()); - + // Number - JSON defaults to double var numberResult = ToonDecoder.Decode("42"); Assert.Equal(42.0, numberResult?.GetValue()); - + // Boolean var boolResult = ToonDecoder.Decode("true"); Assert.True(boolResult?.GetValue()); - + // Null var nullResult = ToonDecoder.Decode("null"); Assert.Null(nullResult); diff --git a/tests/ToonFormat.Tests/ToonEncoderTests.cs b/tests/ToonFormat.Tests/ToonEncoderTests.cs index 6f9e70b..8cd92be 100644 --- a/tests/ToonFormat.Tests/ToonEncoderTests.cs +++ b/tests/ToonFormat.Tests/ToonEncoderTests.cs @@ -30,15 +30,15 @@ public void Encode_PrimitiveTypes_ReturnsValidToon() // String var stringResult = ToonEncoder.Encode("hello"); Assert.Equal("hello", stringResult); - + // Number var numberResult = ToonEncoder.Encode(42); Assert.Equal("42", numberResult); - + // Boolean var boolResult = ToonEncoder.Encode(true); Assert.Equal("true", boolResult); - + // Null var nullResult = ToonEncoder.Encode(null); Assert.Equal("null", nullResult);