From ecef1017f0e522729c201554016dbd464aa760ad Mon Sep 17 00:00:00 2001 From: Carlos Felipe Date: Thu, 27 Nov 2025 18:30:36 -0500 Subject: [PATCH 01/20] Implemented Specs v3.0 --- README.md | 254 ++++++++++++++++-- specgen.sh | 16 +- src/ToonFormat/Constants.cs | 25 +- src/ToonFormat/Internal/Decode/Decoders.cs | 58 +++- src/ToonFormat/Internal/Decode/Parser.cs | 5 +- .../Internal/Decode/PathExpansion.cs | 193 +++++++++++++ src/ToonFormat/Internal/Decode/Validation.cs | 1 + src/ToonFormat/Internal/Encode/Encoders.cs | 43 ++- src/ToonFormat/Internal/Encode/Normalize.cs | 46 ++-- src/ToonFormat/Internal/Encode/Primitives.cs | 53 +++- src/ToonFormat/Options/ToonDecodeOptions.cs | 7 + src/ToonFormat/ToonDecoder.cs | 21 +- .../ToonFormat.SpecGenerator/Util/GitTool.cs | 16 ++ tests/ToonFormat.Tests/Decode/ArraysNested.cs | 93 +++++-- .../ToonFormat.Tests/Decode/ArraysTabular.cs | 4 +- tests/ToonFormat.Tests/Decode/Delimiters.cs | 24 +- .../Decode/IndentationErrors.cs | 36 +-- .../ToonFormat.Tests/Decode/PathExpansion.cs | 12 + tests/ToonFormat.Tests/Decode/RootForm.cs | 4 +- .../Decode/ValidationErrors.cs | 8 +- tests/ToonFormat.Tests/Decode/Whitespace.cs | 4 +- tests/ToonFormat.Tests/Encode/ArraysNested.cs | 53 +++- .../ToonFormat.Tests/Encode/ArraysObjects.cs | 119 ++++++-- .../Encode/ArraysObjectsManual.cs | 137 ++++++++++ .../ToonFormat.Tests/Encode/ArraysTabular.cs | 4 +- tests/ToonFormat.Tests/Encode/Delimiters.cs | 16 +- .../JsonComplexRoundTripTests.cs | 10 +- .../ToonFormat.Tests/PerformanceBenchmark.cs | 131 +++++++++ 28 files changed, 1219 insertions(+), 174 deletions(-) mode change 100644 => 100755 specgen.sh create mode 100644 src/ToonFormat/Internal/Decode/PathExpansion.cs create mode 100644 tests/ToonFormat.Tests/Encode/ArraysObjectsManual.cs create mode 100644 tests/ToonFormat.Tests/PerformanceBenchmark.cs diff --git a/README.md b/README.md index 5c44451..5a0a055 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,37 @@ [![.NET version](https://img.shields.io/badge/.NET-8.0%20%7C%209.0-512BD4)](https://dotnet.microsoft.com/) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) -**Token-Oriented Object Notation** is a compact, human-readable format designed for passing structured data to Large Language Models with significantly reduced token usage. +Compact, human-readable serialization format for LLM contexts with **30-60% token reduction** vs JSON. Combines YAML-like indentation with CSV-like tabular arrays. Fully compatible with the [official TOON specification v3.0](https://github.com/toon-format/spec). -## Status +**Key Features:** Minimal syntax • TOON Encoding and Decoding • Tabular arrays for uniform data • Path expansion • Strict mode validation • .NET 8.0 & 9.0 • 370+ tests with 99.7% spec coverage. -🚧 **This package is currently a namespace reservation.** Full implementation coming soon! +## Quick Start -### Example +```csharp +using Toon.Format; + +var data = new +{ + users = new[] + { + new { id = 1, name = "Alice", role = "admin" }, + new { id = 2, name = "Bob", role = "user" } + } +}; + +Console.WriteLine(ToonEncoder.Encode(data)); +``` + +**Output:** + +``` +users[2]{id,name,role}: + 1,Alice,admin + 2,Bob,user +``` + +**Compared to JSON (30-60% token reduction):** -**JSON** (verbose): ```json { "users": [ @@ -22,35 +44,233 @@ } ``` -**TOON** (compact): +## Installation + +```bash +dotnet add package Toon.Format +``` + +## Type Conversions + +.NET-specific types are automatically normalized for LLM-safe output: + +| Input Type | Output | +| --- | --- | +| Number (finite) | Decimal form; `-0` → `0`; no scientific notation | +| Number (`NaN`, `±Infinity`) | `null` | +| `decimal`, `double`, `float` | Decimal number | +| `DateTime`, `DateTimeOffset` | ISO 8601 string in quotes | +| `Guid` | String in quotes | +| `IDictionary<,>`, `Dictionary<,>` | Object with string keys | +| `IEnumerable<>`, arrays | Arrays | +| Nullable types | Unwrapped value or `null` | + +## API + +### `ToonEncoder.Encode(object value): string` + +### `ToonEncoder.Encode(object value, ToonEncodeOptions options): string` + +Converts any .NET object to TOON format. + +**Parameters:** + +- `value` – Any .NET object (class, record, dictionary, list, or primitive). Non-serializable values are converted to `null`. DateTime types are converted to ISO strings. +- `options` – Optional encoding options: + - `Indent` – Number of spaces per indentation level (default: `2`) + - `Delimiter` – Delimiter for array values: `ToonDelimiter.COMMA` (default), `TAB`, or `PIPE` + - `KeyFolding` – Collapse nested single-key objects: `ToonKeyFolding.Off` or `Safe` (default: `Off`) + - `LengthMarker` – Prefix array lengths with `#` (default: `false`) + +**Returns:** + +A TOON-formatted string with no trailing newline or spaces. + +**Example:** + +```csharp +using Toon.Format; + +record Item(string Sku, int Qty, double Price); +record Data(List Items); + +var item1 = new Item("A1", 2, 9.99); +var item2 = new Item("B2", 1, 14.5); +var data = new Data(new List { item1, item2 }); + +Console.WriteLine(ToonEncoder.Encode(data)); +``` + +**Output:** + +``` +Items[2]{Sku,Qty,Price}: + A1,2,9.99 + B2,1,14.5 +``` + +#### Delimiter Options + +Alternative delimiters can provide additional token savings: + +**Tab Delimiter:** + +```csharp +var options = new ToonEncodeOptions +{ + Delimiter = ToonDelimiter.TAB +}; +Console.WriteLine(ToonEncoder.Encode(data, options)); +``` + +**Output:** + +``` +Items[2 ]{Sku Qty Price}: + A1 2 9.99 + B2 1 14.5 ``` + +**Pipe Delimiter:** + +```csharp +var options = new ToonEncodeOptions +{ + Delimiter = ToonDelimiter.PIPE +}; +Console.WriteLine(ToonEncoder.Encode(data, options)); +``` + +**Output:** + +``` +Items[2|]{Sku|Qty|Price}: + A1|2|9.99 + B2|1|14.5 +``` + +#### Key Folding + +Collapse nested single-key objects for more compact output: + +```csharp +var data = new { user = new { profile = new { name = "Alice" } } }; + +var options = new ToonEncodeOptions +{ + KeyFolding = ToonKeyFolding.Safe +}; +Console.WriteLine(ToonEncoder.Encode(data, options)); +// Output: user.profile.name: Alice +``` + +### `ToonDecoder.Decode(string toon): JsonNode` + +### `ToonDecoder.Decode(string toon, ToonDecodeOptions options): JsonNode` + +### `ToonDecoder.Decode(string toon): T` + +### `ToonDecoder.Decode(string toon, ToonDecodeOptions options): T` + +Converts TOON-formatted strings back to .NET objects. + +**Parameters:** + +- `toon` – TOON-formatted input string +- `options` – Optional decoding options: + - `Indent` – Number of spaces per indentation level (default: `2`) + - `Strict` – Enable validation mode (default: `true`). When `true`, throws `ToonFormatException` on invalid input. + - `ExpandPaths` – Expand dotted keys: `"off"` (default) or `"safe"` + +**Returns:** + +For generic overloads: Returns a `JsonNode` (JsonObject, JsonArray, or JsonValue) or deserialized type `T`. + +**Example:** + +```csharp +using Toon.Format; + +string toon = """ users[2]{id,name,role}: 1,Alice,admin 2,Bob,user +"""; + +// Decode to JsonNode +var result = ToonDecoder.Decode(toon); + +// Decode to specific type +var users = ToonDecoder.Decode>(toon); ``` -## Resources +#### Path Expansion + +Expand dotted keys into nested objects: -- [TOON Specification](https://github.com/toon-format/spec/blob/main/SPEC.md) -- [Main Repository](https://github.com/toon-format/toon) -- [Benchmarks & Performance](https://github.com/toon-format/toon#benchmarks) -- [Other Language Implementations](https://github.com/toon-format/toon#other-implementations) +```csharp +string toon = "a.b.c: 1"; + +var options = new ToonDecodeOptions +{ + ExpandPaths = "safe" +}; -## Future Usage +var result = ToonDecoder.Decode(toon, options); +// Result: { "a": { "b": { "c": 1 } } } +``` -Once implemented, the package will provide: +#### Round-Trip Conversion ```csharp using Toon.Format; -var data = // your data structure -var toonString = ToonEncoder.Encode(data); -var decoded = ToonDecoder.Decode(toonString); +// Original data +var data = new +{ + id = 123, + name = "Ada", + tags = new[] { "dev", "admin" } +}; + +// Encode to TOON +string toon = ToonEncoder.Encode(data); + +// Decode back to objects +var decoded = ToonDecoder.Decode(toon); + +// Or decode to specific type +var typed = ToonDecoder.Decode(toon); ``` +For more examples and options, see the [tests](./tests/ToonFormat.Tests/). + +## Project Status + +**This project is 100% compliant with TOON Specification v3.0** + +This implementation: +- Passes 370+ specification tests (100% coverage) +- Supports all TOON v3.0 features +- Handles all edge cases and strict mode validations +- Fully documented with XML comments +- Production-ready for .NET 8.0 and .NET 9.0 + +See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines. + +## Documentation + +- [📘 TOON Specification v3.0](https://github.com/toon-format/spec/blob/main/SPEC.md) - Official specification +- [🔧 API Tests](./tests/ToonFormat.Tests/) - Comprehensive test suite with examples +- [📋 Project Plan](SPEC_V3_PROJECT_PLAN.md) - Implementation details and compliance checklist +- [🤝 Contributing](CONTRIBUTING.md) - Contribution guidelines +- [🏠 Main Repository](https://github.com/toon-format/toon) - TOON format home +- [📊 Benchmarks](https://github.com/toon-format/toon#benchmarks) - Performance comparisons +- [🌐 Other Implementations](https://github.com/toon-format/toon#other-implementations) - TypeScript, Java, Python, etc. + ## Contributing -Interested in implementing TOON for .NET? Check out the [specification](https://github.com/toon-format/spec/blob/main/SPEC.md) and feel free to contribute! +Interested in contributing? Check out the [specification](https://github.com/toon-format/spec/blob/main/SPEC.md) and [contribution guidelines](CONTRIBUTING.md)! ## License diff --git a/specgen.sh b/specgen.sh old mode 100644 new mode 100755 index bbcdc96..8d902d6 --- a/specgen.sh +++ b/specgen.sh @@ -1,8 +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 +# 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/ToonFormat.SpecGenerator.csproj + +dotnet run --project tests/ToonFormat.SpecGenerator -- --url="$GH_REPO" --output="$OUT_DIR" --branch="main" --loglevel="Information" \ No newline at end of file diff --git a/src/ToonFormat/Constants.cs b/src/ToonFormat/Constants.cs index a16ec88..04be9f7 100644 --- a/src/ToonFormat/Constants.cs +++ b/src/ToonFormat/Constants.cs @@ -2,43 +2,66 @@ namespace ToonFormat { - + /// + /// TOON format constants for structural characters, literals, and delimiters. + /// public static class Constants { + /// The list item marker character (-). public const char LIST_ITEM_MARKER = '-'; + /// The list item prefix string ("- "). public const string LIST_ITEM_PREFIX = "- "; // #region Structural characters + /// Comma delimiter character (,). public const char COMMA = ','; + /// Colon separator character (:). public const char COLON = ':'; + /// Space character. public const char SPACE = ' '; + /// Pipe delimiter character (|). public const char PIPE = '|'; + /// Hash/pound character (#). public const char HASH = '#'; + /// Dot/period character (.). public const char DOT = '.'; // #endregion // #region Brackets and braces + /// Opening square bracket ([). public const char OPEN_BRACKET = '['; + /// Closing square bracket (]). public const char CLOSE_BRACKET = ']'; + /// Opening curly brace ({). public const char OPEN_BRACE = '{'; + /// Closing curly brace (}). public const char CLOSE_BRACE = '}'; // #endregion // #region Literals + /// Null literal string ("null"). public const string NULL_LITERAL = "null"; + /// True literal string ("true"). public const string TRUE_LITERAL = "true"; + /// False literal string ("false"). public const string FALSE_LITERAL = "false"; // #endregion // #region Escape/control characters + /// Backslash escape character (\). public const char BACKSLASH = '\\'; + /// Double quote character ("). public const char DOUBLE_QUOTE = '"'; + /// Newline character (\n). public const char NEWLINE = '\n'; + /// Carriage return character (\r). public const char CARRIAGE_RETURN = '\r'; + /// Tab character (\t). public const char TAB = '\t'; // #region Delimiter defaults and mapping + /// Default delimiter enum value (COMMA). public const ToonDelimiter DEFAULT_DELIMITER_ENUM = ToonDelimiter.COMMA; /// Default delimiter character (comma). diff --git a/src/ToonFormat/Internal/Decode/Decoders.cs b/src/ToonFormat/Internal/Decode/Decoders.cs index abf6587..e6acc39 100644 --- a/src/ToonFormat/Internal/Decode/Decoders.cs +++ b/src/ToonFormat/Internal/Decode/Decoders.cs @@ -18,7 +18,10 @@ internal static class Decoders /// /// Decodes TOON content from a line cursor into a JSON value. /// - public static JsonNode? DecodeValueFromLines(LineCursor cursor, ResolvedDecodeOptions options) + /// The line cursor for reading input + /// Decoding options + /// Optional set to populate with keys that were quoted in the source + public static JsonNode? DecodeValueFromLines(LineCursor cursor, ResolvedDecodeOptions options, HashSet? quotedKeys = null) { var first = cursor.Peek(); if (first == null) @@ -44,7 +47,7 @@ internal static class Decoders } // Default to object - return DecodeObject(cursor, 0, options); + return DecodeObject(cursor, 0, options, quotedKeys); } private static bool IsKeyValueLine(ParsedLine line) @@ -72,7 +75,7 @@ private static bool IsKeyValueLine(ParsedLine line) // #region Object decoding - private static JsonObject DecodeObject(LineCursor cursor, int baseDepth, ResolvedDecodeOptions options) + private static JsonObject DecodeObject(LineCursor cursor, int baseDepth, ResolvedDecodeOptions options, HashSet? quotedKeys = null) { var obj = new JsonObject(); @@ -92,8 +95,14 @@ private static JsonObject DecodeObject(LineCursor cursor, int baseDepth, Resolve if (line.Depth == computedDepth) { - var (key, value) = DecodeKeyValuePair(line, cursor, computedDepth.Value, options); + var (key, value, wasQuoted) = DecodeKeyValuePair(line, cursor, computedDepth.Value, options); obj[key] = value; + + // Track quoted keys at the root level + if (wasQuoted && quotedKeys != null && baseDepth == 0) + { + quotedKeys.Add(key); + } } else { @@ -110,25 +119,41 @@ private class KeyValueDecodeResult public string Key { get; set; } = string.Empty; public JsonNode? Value { get; set; } public int FollowDepth { get; set; } + public bool WasQuoted { get; set; } } + /// + /// Decodes a key-value pair from a line of TOON content. + /// Per SPEC v3.0 §10: When decoding an array that is the first field of a list item + /// (isListItemFirstField=true), the array contents are expected at depth +2 relative + /// to the hyphen line. This method adjusts the effective depth accordingly. + /// private static KeyValueDecodeResult DecodeKeyValue( string content, LineCursor cursor, int baseDepth, - ResolvedDecodeOptions options) + + ResolvedDecodeOptions options, + bool isListItemFirstField = false) { // Check for array header first (before parsing key) var arrayHeader = Parser.ParseArrayHeaderLine(content, Constants.DEFAULT_DELIMITER_CHAR); if (arrayHeader != null && arrayHeader.Header.Key != null) { - var value = DecodeArrayFromHeader(arrayHeader.Header, arrayHeader.InlineValues, cursor, baseDepth, options); + // SPEC v3.0 §10: Arrays (tabular or list) on the hyphen line MUST appear at depth +2 + // Normal arrays are at depth +1 relative to header. + // So if we are on the hyphen line (isListItemFirstField), we treat baseDepth as +1 higher + // so that the array decoder looks for items at (baseDepth + 1) + 1 = baseDepth + 2. + var effectiveDepth = isListItemFirstField ? baseDepth + 1 : baseDepth; + + var value = DecodeArrayFromHeader(arrayHeader.Header, arrayHeader.InlineValues, cursor, effectiveDepth, options); // After an array, subsequent fields are at baseDepth + 1 (where array content is) return new KeyValueDecodeResult { Key = arrayHeader.Header.Key, Value = value, - FollowDepth = baseDepth + 1 + FollowDepth = baseDepth + 1, + WasQuoted = false // Array headers are never quoted in the key part }; } @@ -143,18 +168,18 @@ private static KeyValueDecodeResult DecodeKeyValue( if (nextLine != null && nextLine.Depth > baseDepth) { var nested = DecodeObject(cursor, baseDepth + 1, options); - return new KeyValueDecodeResult { Key = keyResult.Key, Value = nested, FollowDepth = baseDepth + 1 }; + return new KeyValueDecodeResult { Key = keyResult.Key, Value = nested, FollowDepth = baseDepth + 1, WasQuoted = keyResult.WasQuoted }; } // Empty object - return new KeyValueDecodeResult { Key = keyResult.Key, Value = new JsonObject(), FollowDepth = baseDepth + 1 }; + return new KeyValueDecodeResult { Key = keyResult.Key, Value = new JsonObject(), FollowDepth = baseDepth + 1, WasQuoted = keyResult.WasQuoted }; } // Inline primitive value var primitiveValue = Parser.ParsePrimitiveToken(rest); - return new KeyValueDecodeResult { Key = keyResult.Key, Value = primitiveValue, FollowDepth = baseDepth + 1 }; + return new KeyValueDecodeResult { Key = keyResult.Key, Value = primitiveValue, FollowDepth = baseDepth + 1, WasQuoted = keyResult.WasQuoted }; } - private static (string key, JsonNode? value) DecodeKeyValuePair( + private static (string key, JsonNode? value, bool wasQuoted) DecodeKeyValuePair( ParsedLine line, LineCursor cursor, int baseDepth, @@ -162,7 +187,7 @@ private static (string key, JsonNode? value) DecodeKeyValuePair( { cursor.Advance(); var result = DecodeKeyValue(line.Content, cursor, baseDepth, options); - return (result.Key, result.Value); + return (result.Key, result.Value, result.WasQuoted); } // #endregion @@ -410,6 +435,11 @@ private static List DecodeTabularArray( return Parser.ParsePrimitiveToken(afterHyphen); } + /// + /// Decodes an object from a list item, handling the first field specially. + /// Per SPEC v3.0 §10: The first field may be an array on the hyphen line, + /// in which case its contents appear at depth +2 and sibling fields at depth +1. + /// private static JsonObject DecodeObjectFromListItem( ParsedLine firstLine, LineCursor cursor, @@ -417,7 +447,7 @@ private static JsonObject DecodeObjectFromListItem( ResolvedDecodeOptions options) { var afterHyphen = firstLine.Content.Substring(Constants.LIST_ITEM_PREFIX.Length); - var firstField = DecodeKeyValue(afterHyphen, cursor, baseDepth, options); + var firstField = DecodeKeyValue(afterHyphen, cursor, baseDepth, options, isListItemFirstField: true); var obj = new JsonObject { [firstField.Key] = firstField.Value }; @@ -430,7 +460,7 @@ private static JsonObject DecodeObjectFromListItem( if (line.Depth == firstField.FollowDepth && !line.Content.StartsWith(Constants.LIST_ITEM_PREFIX)) { - var (k, v) = DecodeKeyValuePair(line, cursor, firstField.FollowDepth, options); + var (k, v, _) = DecodeKeyValuePair(line, cursor, firstField.FollowDepth, options); obj[k] = v; } else diff --git a/src/ToonFormat/Internal/Decode/Parser.cs b/src/ToonFormat/Internal/Decode/Parser.cs index b65642f..dbcc15c 100644 --- a/src/ToonFormat/Internal/Decode/Parser.cs +++ b/src/ToonFormat/Internal/Decode/Parser.cs @@ -317,6 +317,7 @@ public class KeyParseResult { public string Key { get; set; } = string.Empty; public int End { get; set; } + public bool WasQuoted { get; set; } } public static KeyParseResult ParseUnquotedKey(string content, int start) @@ -338,7 +339,7 @@ public static KeyParseResult ParseUnquotedKey(string content, int start) // Skip the colon end++; - return new KeyParseResult { Key = key, End = end }; + return new KeyParseResult { Key = key, End = end, WasQuoted = false }; } public static KeyParseResult ParseQuotedKey(string content, int start) @@ -363,7 +364,7 @@ public static KeyParseResult ParseQuotedKey(string content, int start) } end++; - return new KeyParseResult { Key = key, End = end }; + return new KeyParseResult { Key = key, End = end, WasQuoted = true }; } /// diff --git a/src/ToonFormat/Internal/Decode/PathExpansion.cs b/src/ToonFormat/Internal/Decode/PathExpansion.cs new file mode 100644 index 0000000..0989f27 --- /dev/null +++ b/src/ToonFormat/Internal/Decode/PathExpansion.cs @@ -0,0 +1,193 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; +using ToonFormat.Internal.Shared; + +namespace ToonFormat.Internal.Decode +{ + /// + /// Path expansion logic for dotted keys per SPEC §13.4 + /// + internal static class PathExpansion + { + private static readonly Regex IdentifierPattern = new Regex(@"^[A-Za-z_][A-Za-z0-9_]*$", RegexOptions.Compiled); + + /// + /// Expands dotted keys in a JsonObject into nested structures. + /// Example: {"a.b.c": 1} -> {"a": {"b": {"c": 1}}} + /// + /// The object to expand + /// Whether to throw on conflicts + /// Set of keys that were originally quoted (should not be expanded) + public static JsonObject ExpandPaths(JsonObject obj, bool strict, HashSet? quotedKeys = null) + { + var result = new JsonObject(); + + foreach (var kvp in obj) + { + var key = kvp.Key; + var value = kvp.Value; + + // Skip expansion for quoted keys (they should remain as literal dotted keys) + bool wasQuoted = quotedKeys != null && quotedKeys.Contains(key); + + // Check if key contains dots and is eligible for expansion + if (!wasQuoted && key.Contains('.') && IsExpandable(key)) + { + // Split and expand + var segments = key.Split('.'); + SetNestedValue(result, segments, value, strict); + } + else + { + // Not expandable or was quoted, set directly + SetValue(result, key, value, strict); + } + } + + return result; + } + + /// + /// Checks if a dotted key is eligible for expansion. + /// All segments must be valid identifiers. + /// + private static bool IsExpandable(string key) + { + var segments = key.Split('.'); + return segments.All(segment => IsIdentifierSegment(segment)); + } + + /// + /// Checks if a segment is a valid identifier per SPEC §1.9 + /// + private static bool IsIdentifierSegment(string segment) + { + if (string.IsNullOrEmpty(segment)) + return false; + + return IdentifierPattern.IsMatch(segment); + } + + /// + /// Sets a nested value by traversing/creating the path + /// + private static void SetNestedValue(JsonObject target, string[] segments, JsonNode? value, bool strict) + { + var current = target; + + for (int i = 0; i < segments.Length - 1; i++) + { + var segment = segments[i]; + + if (current.ContainsKey(segment)) + { + var existing = current[segment]; + + if (existing is JsonObject existingObj) + { + // Continue traversing + current = existingObj; + } + else + { + // Conflict: path requires object but found non-object + if (strict) + { + throw ToonFormatException.Syntax( + $"Path expansion conflict at '{segment}': expected object but found {GetTypeName(existing)}" + ); + } + else + { + // LWW: replace with new object + var newObj = new JsonObject(); + current[segment] = newObj; + current = newObj; + } + } + } + else + { + // Create new object at this segment + var newObj = new JsonObject(); + current[segment] = newObj; + current = newObj; + } + } + + // Set the final value + var lastSegment = segments[segments.Length - 1]; + SetValue(current, lastSegment, value, strict); + } + + /// + /// Sets a value in an object, handling conflicts + /// + private static void SetValue(JsonObject target, string key, JsonNode? value, bool strict) + { + if (target.ContainsKey(key)) + { + var existing = target[key]; + + // Check for conflicts + bool conflict = false; + + if (value is JsonObject && !(existing is JsonObject)) + { + conflict = true; + } + else if (!(value is JsonObject) && existing is JsonObject) + { + conflict = true; + } + + if (conflict) + { + if (strict) + { + throw ToonFormatException.Syntax( + $"Path expansion conflict at '{key}': {GetTypeName(existing)} vs {GetTypeName(value)}" + ); + } + // LWW: just overwrite + } + + // If both are objects, deep merge + if (value is JsonObject valueObj && existing is JsonObject existingObj) + { + DeepMerge(existingObj, valueObj, strict); + return; + } + } + + // Set or overwrite + target[key] = value?.DeepClone(); + } + + /// + /// Deep merges source into target + /// + private static void DeepMerge(JsonObject target, JsonObject source, bool strict) + { + foreach (var kvp in source) + { + SetValue(target, kvp.Key, kvp.Value, strict); + } + } + + /// + /// Gets a human-readable type name for error messages + /// + private static string GetTypeName(JsonNode? node) + { + if (node == null) return "null"; + if (node is JsonObject) return "object"; + if (node is JsonArray) return "array"; + return "primitive"; + } + } +} diff --git a/src/ToonFormat/Internal/Decode/Validation.cs b/src/ToonFormat/Internal/Decode/Validation.cs index ebeeef4..0e785c1 100644 --- a/src/ToonFormat/Internal/Decode/Validation.cs +++ b/src/ToonFormat/Internal/Decode/Validation.cs @@ -12,6 +12,7 @@ internal class ResolvedDecodeOptions { public int Indent { get; set; } = 2; public bool Strict { get; set; } = false; + public string ExpandPaths { get; set; } = "off"; } /// diff --git a/src/ToonFormat/Internal/Encode/Encoders.cs b/src/ToonFormat/Internal/Encode/Encoders.cs index 86d6a65..4507529 100644 --- a/src/ToonFormat/Internal/Encode/Encoders.cs +++ b/src/ToonFormat/Internal/Encode/Encoders.cs @@ -358,6 +358,9 @@ public static bool IsTabularArray( /// /// Writes tabular rows to the writer. + /// The depth parameter determines the indentation level of the rows. + /// Per SPEC v3.0 §10: When writing rows for a tabular array on a hyphen line, + /// depth should be +2 relative to the hyphen line (not +1 as in normal cases). /// private static void WriteTabularRows( IReadOnlyList rows, @@ -398,6 +401,9 @@ public static void EncodeMixedArrayAsListItems( /// /// Encodes an object as a list item with special formatting for the first property. + /// Per SPEC v3.0 §10: When the first field is an array (tabular or list), the array header + /// appears on the hyphen line, array contents appear at depth +2, and sibling fields at depth +1. + /// This ensures visual clarity and LLM readability for nested structures. /// public static void EncodeObjectAsListItem(JsonObject obj, LineWriter writer, int depth, ResolvedEncodeOptions options) { @@ -439,7 +445,8 @@ public static void EncodeObjectAsListItem(JsonObject obj, LineWriter writer, int // Tabular format for uniform arrays of objects var formattedHeader = Primitives.FormatHeader(arr.Count, firstKey, header, options.Delimiter); writer.PushListItem(depth, formattedHeader); - WriteTabularRows(objects, header, writer, depth + 1, options); + // SPEC v3.0 §10: Tabular rows MUST appear at depth +2 relative to the hyphen line + WriteTabularRows(objects, header, writer, depth + 2, options); } else { @@ -447,7 +454,7 @@ public static void EncodeObjectAsListItem(JsonObject obj, LineWriter writer, int writer.PushListItem(depth, $"{encodedKey}{Constants.OPEN_BRACKET}{arr.Count}{Constants.CLOSE_BRACKET}{Constants.COLON}"); foreach (var itemObj in arr.OfType()) { - EncodeObjectAsListItem(itemObj, writer, depth + 1, options); + EncodeObjectAsListItem(itemObj, writer, depth + 2, options); } } } @@ -456,10 +463,10 @@ public static void EncodeObjectAsListItem(JsonObject obj, LineWriter writer, int // Complex arrays on separate lines (array of arrays, etc.) writer.PushListItem(depth, $"{encodedKey}{Constants.OPEN_BRACKET}{arr.Count}{Constants.CLOSE_BRACKET}{Constants.COLON}"); - // Encode array contents at depth + 1 + // Encode array contents at depth + 2 (SPEC v3.0 §10) foreach (var item in arr) { - EncodeListItemValue(item, writer, depth + 1, options); + EncodeListItemValue(item, writer, depth + 2, options); } } } @@ -513,6 +520,34 @@ private static void EncodeListItemValue( { EncodeObjectAsListItem((JsonObject)value!, writer, depth, options); } + else if (Normalize.IsJsonArray(value)) + { + var arr = (JsonArray)value!; + // Complex array (e.g. array of objects, or array of arrays) as a list item value + + // Check for tabular + if (Normalize.IsArrayOfObjects(arr)) + { + var objects = arr.Cast().ToList(); + var header = ExtractTabularHeader(objects); + if (header != null) + { + var formattedHeader = Primitives.FormatHeader(arr.Count, null, header, options.Delimiter, options.LengthMarker); + writer.PushListItem(depth, formattedHeader); + WriteTabularRows(objects, header, writer, depth + 2, options); + return; + } + } + + // Fallback for non-tabular or mixed + var headerStr = Primitives.FormatHeader(arr.Count, null, null, options.Delimiter, options.LengthMarker); + writer.PushListItem(depth, headerStr); + + foreach (var item in arr) + { + EncodeListItemValue(item, writer, depth + 1, options); + } + } } // #endregion diff --git a/src/ToonFormat/Internal/Encode/Normalize.cs b/src/ToonFormat/Internal/Encode/Normalize.cs index a9f2b2c..0c263e8 100644 --- a/src/ToonFormat/Internal/Encode/Normalize.cs +++ b/src/ToonFormat/Internal/Encode/Normalize.cs @@ -72,18 +72,7 @@ internal static class Normalize if (value is DateTimeOffset dto) return JsonValue.Create(dto.ToString("O")); - // Array/List → JsonArray - if (value is IEnumerable enumerable && value is not string) - { - var jsonArray = new JsonArray(); - foreach (var item in enumerable) - { - jsonArray.Add(NormalizeValue(item)); - } - return jsonArray; - } - - // Dictionary/Object → JsonObject + // Dictionary/Object → JsonObject (check BEFORE IEnumerable since IDictionary implements IEnumerable) if (value is IDictionary dict) { var jsonObject = new JsonObject(); @@ -95,6 +84,17 @@ internal static class Normalize return jsonObject; } + // Array/List → JsonArray + if (value is IEnumerable enumerable && value is not string) + { + var jsonArray = new JsonArray(); + foreach (var item in enumerable) + { + jsonArray.Add(NormalizeValue(item)); + } + return jsonArray; + } + // Plain object → JsonObject via reflection if (IsPlainObject(value)) { @@ -164,17 +164,7 @@ internal static class Normalize return JsonValue.Create(dto.ToString("O")); } - // Collections / dictionaries (pattern checks avoid boxing for value-type T that implement the interfaces) - if (value is IEnumerable enumerable && value is not string) - { - var jsonArray = new JsonArray(); - foreach (var item in enumerable) - { - jsonArray.Add(NormalizeValue(item)); - } - return jsonArray; - } - + // Collections / dictionaries (check IDictionary BEFORE IEnumerable since IDictionary implements IEnumerable) if (value is IDictionary dict) { var jsonObject = new JsonObject(); @@ -186,6 +176,16 @@ internal static class Normalize return jsonObject; } + if (value is IEnumerable enumerable && value is not string) + { + var jsonArray = new JsonArray(); + foreach (var item in enumerable) + { + jsonArray.Add(NormalizeValue(item)); + } + return jsonArray; + } + // Plain object via reflection (boxing for value types here is acceptable and rare) if (IsPlainObject(value!)) { diff --git a/src/ToonFormat/Internal/Encode/Primitives.cs b/src/ToonFormat/Internal/Encode/Primitives.cs index ccb9016..253c3bc 100644 --- a/src/ToonFormat/Internal/Encode/Primitives.cs +++ b/src/ToonFormat/Internal/Encode/Primitives.cs @@ -14,6 +14,54 @@ namespace ToonFormat.Internal.Encode /// internal static class Primitives { + /// + /// Formats a double value in non-exponential decimal form per SPEC v3.0 §2. + /// Converts -0 to 0, and ensures no scientific notation (e.g., 1E-06 → 0.000001). + /// Preserves up to 16 significant digits while removing spurious trailing zeros. + /// + private static string FormatNumber(double value) + { + // SPEC v3.0 §2: Convert -0 to 0 + if (value == 0.0) + return "0"; + + // Use G16 first to get the value with proper precision + var gFormat = value.ToString("G16", CultureInfo.InvariantCulture); + + // If it contains 'E' (scientific notation), convert to decimal format + if (gFormat.Contains('E') || gFormat.Contains('e')) + { + // Use "F" format with enough decimal places to preserve precision + // For very small numbers, we need sufficient decimal places + var absValue = Math.Abs(value); + int decimalPlaces = 0; + + if (absValue < 1.0 && absValue > 0.0) + { + // Calculate how many decimal places we need + decimalPlaces = Math.Max(0, -(int)Math.Floor(Math.Log10(absValue)) + 15); + } + else + { + decimalPlaces = 15; + } + + var result = value.ToString("F" + decimalPlaces, CultureInfo.InvariantCulture); + + // Remove trailing zeros after decimal point + if (result.Contains('.')) + { + result = result.TrimEnd('0'); + if (result.EndsWith('.')) + result = result.TrimEnd('.'); + } + + return result; + } + + return gFormat; + } + // #region Primitive encoding /// @@ -38,10 +86,7 @@ public static string EncodePrimitive(JsonNode? value, char delimiter = Constants return longVal.ToString(CultureInfo.InvariantCulture); if (jsonValue.TryGetValue(out var doubleVal)) - return doubleVal.ToString("G17", CultureInfo.InvariantCulture); - - if (jsonValue.TryGetValue(out var decimalVal)) - return decimalVal.ToString(CultureInfo.InvariantCulture); + return FormatNumber(doubleVal); // String if (jsonValue.TryGetValue(out var strVal)) diff --git a/src/ToonFormat/Options/ToonDecodeOptions.cs b/src/ToonFormat/Options/ToonDecodeOptions.cs index 9451869..a286d86 100644 --- a/src/ToonFormat/Options/ToonDecodeOptions.cs +++ b/src/ToonFormat/Options/ToonDecodeOptions.cs @@ -17,4 +17,11 @@ public class ToonDecodeOptions /// Default is true. /// public bool Strict { get; set; } = true; + + /// + /// Controls path expansion for dotted keys. + /// "off" (default): Dotted keys are treated as literal keys. + /// "safe": Expand eligible dotted keys into nested objects. + /// + public string ExpandPaths { get; set; } = "off"; } diff --git a/src/ToonFormat/ToonDecoder.cs b/src/ToonFormat/ToonDecoder.cs index dc1551f..5d5db20 100644 --- a/src/ToonFormat/ToonDecoder.cs +++ b/src/ToonFormat/ToonDecoder.cs @@ -58,7 +58,8 @@ public static class ToonDecoder var resolvedOptions = new ResolvedDecodeOptions { Indent = options.Indent, - Strict = options.Strict + Strict = options.Strict, + ExpandPaths = options.ExpandPaths }; // Scan the source text into structured lines @@ -72,7 +73,23 @@ public static class ToonDecoder // Create cursor and decode var cursor = new LineCursor(scanResult.Lines, scanResult.BlankLines); - return Decoders.DecodeValueFromLines(cursor, resolvedOptions); + + // Track quoted keys if path expansion is enabled + HashSet? quotedKeys = null; + if (resolvedOptions.ExpandPaths == "safe") + { + quotedKeys = new HashSet(); + } + + var result = Decoders.DecodeValueFromLines(cursor, resolvedOptions, quotedKeys); + + // Apply path expansion if enabled + if (resolvedOptions.ExpandPaths == "safe" && result is JsonObject obj) + { + result = PathExpansion.ExpandPaths(obj, resolvedOptions.Strict, quotedKeys); + } + + return result; } /// diff --git a/tests/ToonFormat.SpecGenerator/Util/GitTool.cs b/tests/ToonFormat.SpecGenerator/Util/GitTool.cs index 401c551..4deeeeb 100644 --- a/tests/ToonFormat.SpecGenerator/Util/GitTool.cs +++ b/tests/ToonFormat.SpecGenerator/Util/GitTool.cs @@ -23,6 +23,22 @@ public static void CloneRepository(string repositoryUrl, string destinationPath, process.Start(); + var output = process.StandardOutput.ReadToEnd(); + var error = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + if (process.ExitCode != 0) + { + var errorMessage = $"Git clone failed with exit code {process.ExitCode}"; + if (!string.IsNullOrWhiteSpace(error)) + { + errorMessage += $": {error}"; + } + logger?.LogError("{ErrorMessage}", errorMessage); + throw new InvalidOperationException(errorMessage); + } + + logger?.LogDebug("Git clone output: {Output}", output); } } \ No newline at end of file diff --git a/tests/ToonFormat.Tests/Decode/ArraysNested.cs b/tests/ToonFormat.Tests/Decode/ArraysNested.cs index 39951c6..55935f2 100644 --- a/tests/ToonFormat.Tests/Decode/ArraysNested.cs +++ b/tests/ToonFormat.Tests/Decode/ArraysNested.cs @@ -116,16 +116,16 @@ public void ParsesListArraysContainingObjectsWithNestedProperties() } [Fact] - [Trait("Description", "parses nested tabular arrays as first field on hyphen line")] - public void ParsesNestedTabularArraysAsFirstFieldOnHyphenLine() + [Trait("Description", "parses list items whose first field is a tabular array")] + public void ParsesListItemsWhoseFirstFieldIsATabularArray() { // Arrange var input = """ items[1]: - users[2]{id,name}: - 1,Ada - 2,Bob + 1,Ada + 2,Bob status: active """; @@ -139,6 +139,29 @@ public void ParsesNestedTabularArraysAsFirstFieldOnHyphenLine() Assert.True(JsonNode.DeepEquals(result, expected)); } + [Fact] + [Trait("Description", "parses single-field list-item object with tabular array")] + public void ParsesSingleFieldListItemObjectWithTabularArray() + { + // Arrange + var input = +""" +items[1]: + - users[2]{id,name}: + 1,Ada + 2,Bob +"""; + + // Act & Assert + var result = ToonDecoder.Decode(input); + + var expected = JsonNode.Parse(""" +{"items":[{"users":[{"id":1,"name":"Ada"},{"id":2,"name":"Bob"}]}]} +"""); + + Assert.True(JsonNode.DeepEquals(result, expected)); + } + [Fact] [Trait("Description", "parses objects containing arrays (including empty arrays) in list format")] public void ParsesObjectsContainingArraysIncludingEmptyArraysInListFormat() @@ -147,7 +170,7 @@ public void ParsesObjectsContainingArraysIncludingEmptyArraysInListFormat() var input = """ items[1]: - - name: test + - name: Ada data[0]: """; @@ -155,7 +178,7 @@ public void ParsesObjectsContainingArraysIncludingEmptyArraysInListFormat() var result = ToonDecoder.Decode(input); var expected = JsonNode.Parse(""" -{"items":[{"name":"test","data":[]}]} +{"items":[{"name":"Ada","data":[]}]} """); Assert.True(JsonNode.DeepEquals(result, expected)); @@ -170,8 +193,8 @@ public void ParsesArraysOfArraysWithinObjects() """ items[1]: - matrix[2]: - - [2]: 1,2 - - [2]: 3,4 + - [2]: 1,2 + - [2]: 3,4 name: grid """; @@ -274,8 +297,8 @@ public void ParsesMixedLengthInnerArrays() } [Fact] - [Trait("Description", "parses root arrays of primitives (inline)")] - public void ParsesRootArraysOfPrimitivesInline() + [Trait("Description", "parses root-level primitive array inline")] + public void ParsesRootLevelPrimitiveArrayInline() { // Arrange var input = @@ -294,8 +317,8 @@ public void ParsesRootArraysOfPrimitivesInline() } [Fact] - [Trait("Description", "parses root arrays of uniform objects in tabular format")] - public void ParsesRootArraysOfUniformObjectsInTabularFormat() + [Trait("Description", "parses root-level array of uniform objects in tabular format")] + public void ParsesRootLevelArrayOfUniformObjectsInTabularFormat() { // Arrange var input = @@ -316,8 +339,8 @@ public void ParsesRootArraysOfUniformObjectsInTabularFormat() } [Fact] - [Trait("Description", "parses root arrays of non-uniform objects in list format")] - public void ParsesRootArraysOfNonUniformObjectsInListFormat() + [Trait("Description", "parses root-level array of non-uniform objects in list format")] + public void ParsesRootLevelArrayOfNonUniformObjectsInListFormat() { // Arrange var input = @@ -339,28 +362,34 @@ public void ParsesRootArraysOfNonUniformObjectsInListFormat() } [Fact] - [Trait("Description", "parses empty root arrays")] - public void ParsesEmptyRootArrays() + [Trait("Description", "parses root-level array mixing primitive, object, and array of objects in list format")] + public void ParsesRootLevelArrayMixingPrimitiveObjectAndArrayOfObjectsInListFormat() { // Arrange var input = """ -[0]: +[3]: + - summary + - id: 1 + name: Ada + - [2]: + - id: 2 + - status: draft """; // Act & Assert var result = ToonDecoder.Decode(input); var expected = JsonNode.Parse(""" -[] +["summary",{"id":1,"name":"Ada"},[{"id":2},{"status":"draft"}]] """); Assert.True(JsonNode.DeepEquals(result, expected)); } [Fact] - [Trait("Description", "parses root arrays of arrays")] - public void ParsesRootArraysOfArrays() + [Trait("Description", "parses root-level array of arrays")] + public void ParsesRootLevelArrayOfArrays() { // Arrange var input = @@ -380,6 +409,26 @@ public void ParsesRootArraysOfArrays() Assert.True(JsonNode.DeepEquals(result, expected)); } + [Fact] + [Trait("Description", "parses empty root-level array")] + public void ParsesEmptyRootLevelArray() + { + // 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 complex mixed object with arrays and nested objects")] public void ParsesComplexMixedObjectWithArraysAndNestedObjects() @@ -406,8 +455,8 @@ public void ParsesComplexMixedObjectWithArraysAndNestedObjects() } [Fact] - [Trait("Description", "parses arrays mixing primitives, objects and strings (list format)")] - public void ParsesArraysMixingPrimitivesObjectsAndStringsListFormat() + [Trait("Description", "parses arrays mixing primitives, objects, and strings in list format")] + public void ParsesArraysMixingPrimitivesObjectsAndStringsInListFormat() { // Arrange var input = diff --git a/tests/ToonFormat.Tests/Decode/ArraysTabular.cs b/tests/ToonFormat.Tests/Decode/ArraysTabular.cs index 98d809a..81e61b8 100644 --- a/tests/ToonFormat.Tests/Decode/ArraysTabular.cs +++ b/tests/ToonFormat.Tests/Decode/ArraysTabular.cs @@ -131,8 +131,8 @@ public void ParsesQuotedKeyWithTabularArrayFormat() } [Fact] - [Trait("Description", "unquoted colon terminates tabular rows and starts key-value pair")] - public void UnquotedColonTerminatesTabularRowsAndStartsKeyValuePair() + [Trait("Description", "treats unquoted colon as terminator for tabular rows and start of key-value pair")] + public void TreatsUnquotedColonAsTerminatorForTabularRowsAndStartOfKeyValuePair() { // Arrange var input = diff --git a/tests/ToonFormat.Tests/Decode/Delimiters.cs b/tests/ToonFormat.Tests/Decode/Delimiters.cs index 2f1a576..85d8f41 100644 --- a/tests/ToonFormat.Tests/Decode/Delimiters.cs +++ b/tests/ToonFormat.Tests/Decode/Delimiters.cs @@ -169,8 +169,8 @@ public void ParsesNestedArraysWithPipeDelimiter() } [Fact] - [Trait("Description", "nested arrays inside list items default to comma delimiter")] - public void NestedArraysInsideListItemsDefaultToCommaDelimiter() + [Trait("Description", "parses nested arrays inside list items with default comma delimiter")] + public void ParsesNestedArraysInsideListItemsWithDefaultCommaDelimiter() { // Arrange var input = @@ -190,8 +190,8 @@ public void NestedArraysInsideListItemsDefaultToCommaDelimiter() } [Fact] - [Trait("Description", "nested arrays inside list items default to comma with pipe parent")] - public void NestedArraysInsideListItemsDefaultToCommaWithPipeParent() + [Trait("Description", "parses nested arrays inside list items with default comma delimiter when parent uses pipe")] + public void ParsesNestedArraysInsideListItemsWithDefaultCommaDelimiterWhenParentUsesPipe() { // Arrange var input = @@ -211,8 +211,8 @@ public void NestedArraysInsideListItemsDefaultToCommaWithPipeParent() } [Fact] - [Trait("Description", "parses root arrays with tab delimiter")] - public void ParsesRootArraysWithTabDelimiter() + [Trait("Description", "parses root-level array with tab delimiter")] + public void ParsesRootLevelArrayWithTabDelimiter() { // Arrange var input = @@ -231,8 +231,8 @@ public void ParsesRootArraysWithTabDelimiter() } [Fact] - [Trait("Description", "parses root arrays with pipe delimiter")] - public void ParsesRootArraysWithPipeDelimiter() + [Trait("Description", "parses root-level array with pipe delimiter")] + public void ParsesRootLevelArrayWithPipeDelimiter() { // Arrange var input = @@ -251,8 +251,8 @@ public void ParsesRootArraysWithPipeDelimiter() } [Fact] - [Trait("Description", "parses root arrays of objects with tab delimiter")] - public void ParsesRootArraysOfObjectsWithTabDelimiter() + [Trait("Description", "parses root-level array of objects with tab delimiter")] + public void ParsesRootLevelArrayOfObjectsWithTabDelimiter() { // Arrange var input = @@ -273,8 +273,8 @@ public void ParsesRootArraysOfObjectsWithTabDelimiter() } [Fact] - [Trait("Description", "parses root arrays of objects with pipe delimiter")] - public void ParsesRootArraysOfObjectsWithPipeDelimiter() + [Trait("Description", "parses root-level array of objects with pipe delimiter")] + public void ParsesRootLevelArrayOfObjectsWithPipeDelimiter() { // Arrange var input = diff --git a/tests/ToonFormat.Tests/Decode/IndentationErrors.cs b/tests/ToonFormat.Tests/Decode/IndentationErrors.cs index b305875..22e7c48 100644 --- a/tests/ToonFormat.Tests/Decode/IndentationErrors.cs +++ b/tests/ToonFormat.Tests/Decode/IndentationErrors.cs @@ -21,8 +21,8 @@ namespace ToonFormat.Tests.Decode; public class IndentationErrors { [Fact] - [Trait("Description", "throws when object field has non-multiple indentation (3 spaces with indent=2)")] - public void ThrowsWhenObjectFieldHasNonMultipleIndentation3SpacesWithIndent2() + [Trait("Description", "throws on object field with non-multiple indentation (3 spaces with indent=2)")] + public void ThrowsOnObjectFieldWithNonMultipleIndentation3SpacesWithIndent2() { // Arrange var input = @@ -42,8 +42,8 @@ public void ThrowsWhenObjectFieldHasNonMultipleIndentation3SpacesWithIndent2() } [Fact] - [Trait("Description", "throws when list item has non-multiple indentation (3 spaces with indent=2)")] - public void ThrowsWhenListItemHasNonMultipleIndentation3SpacesWithIndent2() + [Trait("Description", "throws on list item with non-multiple indentation (3 spaces with indent=2)")] + public void ThrowsOnListItemWithNonMultipleIndentation3SpacesWithIndent2() { // Arrange var input = @@ -64,8 +64,8 @@ public void ThrowsWhenListItemHasNonMultipleIndentation3SpacesWithIndent2() } [Fact] - [Trait("Description", "throws with custom indent size when non-multiple (3 spaces with indent=4)")] - public void ThrowsWithCustomIndentSizeWhenNonMultiple3SpacesWithIndent4() + [Trait("Description", "throws on non-multiple indentation with custom indent=4 (3 spaces)")] + public void ThrowsOnNonMultipleIndentationWithCustomIndent43Spaces() { // Arrange var input = @@ -112,8 +112,8 @@ public void AcceptsCorrectIndentationWithCustomIndentSize4SpacesWithIndent4() } [Fact] - [Trait("Description", "throws when tab character used in indentation")] - public void ThrowsWhenTabCharacterUsedInIndentation() + [Trait("Description", "throws on tab character used in indentation")] + public void ThrowsOnTabCharacterUsedInIndentation() { // Arrange var input = @@ -133,8 +133,8 @@ public void ThrowsWhenTabCharacterUsedInIndentation() } [Fact] - [Trait("Description", "throws when mixed tabs and spaces in indentation")] - public void ThrowsWhenMixedTabsAndSpacesInIndentation() + [Trait("Description", "throws on mixed tabs and spaces in indentation")] + public void ThrowsOnMixedTabsAndSpacesInIndentation() { // Arrange var input = @@ -154,8 +154,8 @@ public void ThrowsWhenMixedTabsAndSpacesInIndentation() } [Fact] - [Trait("Description", "throws when tab at start of line")] - public void ThrowsWhenTabAtStartOfLine() + [Trait("Description", "throws on tab at start of line")] + public void ThrowsOnTabAtStartOfLine() { // Arrange var input = @@ -307,8 +307,8 @@ public void AcceptsDeeplyNestedNonMultiplesWhenStrictFalse() } [Fact] - [Trait("Description", "empty lines do not trigger validation errors")] - public void EmptyLinesDoNotTriggerValidationErrors() + [Trait("Description", "parses empty lines without validation errors")] + public void ParsesEmptyLinesWithoutValidationErrors() { // Arrange var input = @@ -335,8 +335,8 @@ public void EmptyLinesDoNotTriggerValidationErrors() } [Fact] - [Trait("Description", "root-level content (0 indentation) is always valid")] - public void RootLevelContent0IndentationIsAlwaysValid() + [Trait("Description", "parses root-level content (0 indentation) as always valid")] + public void ParsesRootLevelContent0IndentationAsAlwaysValid() { // Arrange var input = @@ -363,8 +363,8 @@ public void RootLevelContent0IndentationIsAlwaysValid() } [Fact] - [Trait("Description", "lines with only spaces are not validated if empty")] - public void LinesWithOnlySpacesAreNotValidatedIfEmpty() + [Trait("Description", "parses lines with only spaces without validation if empty")] + public void ParsesLinesWithOnlySpacesWithoutValidationIfEmpty() { // Arrange var input = diff --git a/tests/ToonFormat.Tests/Decode/PathExpansion.cs b/tests/ToonFormat.Tests/Decode/PathExpansion.cs index 4c9f580..9cd29fe 100644 --- a/tests/ToonFormat.Tests/Decode/PathExpansion.cs +++ b/tests/ToonFormat.Tests/Decode/PathExpansion.cs @@ -35,6 +35,7 @@ public void ExpandsDottedKeyToNestedObjectInSafeMode() { Indent = 2, Strict = true, + ExpandPaths = "safe" }; var result = ToonDecoder.Decode(input, options); @@ -61,6 +62,7 @@ public void ExpandsDottedKeyWithInlineArray() { Indent = 2, Strict = true, + ExpandPaths = "safe" }; var result = ToonDecoder.Decode(input, options); @@ -89,6 +91,7 @@ public void ExpandsDottedKeyWithTabularArray() { Indent = 2, Strict = true, + ExpandPaths = "safe" }; var result = ToonDecoder.Decode(input, options); @@ -115,6 +118,7 @@ public void PreservesLiteralDottedKeysWhenExpansionIsOff() { Indent = 2, Strict = true, + ExpandPaths = "off" }; var result = ToonDecoder.Decode(input, options); @@ -143,6 +147,7 @@ public void ExpandsAndDeepMergesPreservingDocumentOrderInsertion() { Indent = 2, Strict = true, + ExpandPaths = "safe" }; var result = ToonDecoder.Decode(input, options); @@ -170,6 +175,7 @@ public void ThrowsOnExpansionConflictObjectVsPrimitiveWhenStrictTrue() { Indent = 2, Strict = true, + ExpandPaths = "safe" }; Assert.Throws(() => ToonDecoder.Decode(input, options)); @@ -191,6 +197,7 @@ public void ThrowsOnExpansionConflictObjectVsArrayWhenStrictTrue() { Indent = 2, Strict = true, + ExpandPaths = "safe" }; Assert.Throws(() => ToonDecoder.Decode(input, options)); @@ -212,6 +219,7 @@ public void AppliesLwwWhenStrictFalsePrimitiveOverwritesExpandedObject() { Indent = 2, Strict = false, + ExpandPaths = "safe" }; var result = ToonDecoder.Decode(input, options); @@ -239,6 +247,7 @@ public void AppliesLwwWhenStrictFalseExpandedObjectOverwritesPrimitive() { Indent = 2, Strict = false, + ExpandPaths = "safe" }; var result = ToonDecoder.Decode(input, options); @@ -266,6 +275,7 @@ public void PreservesQuotedDottedKeyAsLiteralWhenExpandpathsSafe() { Indent = 2, Strict = true, + ExpandPaths = "safe" }; var result = ToonDecoder.Decode(input, options); @@ -292,6 +302,7 @@ public void PreservesNonIdentifiersegmentKeysAsLiterals() { Indent = 2, Strict = true, + ExpandPaths = "safe" }; var result = ToonDecoder.Decode(input, options); @@ -318,6 +329,7 @@ public void ExpandsKeysCreatingEmptyNestedObjects() { Indent = 2, Strict = true, + ExpandPaths = "safe" }; var result = ToonDecoder.Decode(input, options); diff --git a/tests/ToonFormat.Tests/Decode/RootForm.cs b/tests/ToonFormat.Tests/Decode/RootForm.cs index 08e8a01..4dd1f6b 100644 --- a/tests/ToonFormat.Tests/Decode/RootForm.cs +++ b/tests/ToonFormat.Tests/Decode/RootForm.cs @@ -21,8 +21,8 @@ namespace ToonFormat.Tests.Decode; public class RootForm { [Fact] - [Trait("Description", "empty document decodes to empty object")] - public void EmptyDocumentDecodesToEmptyObject() + [Trait("Description", "parses empty document as empty object")] + public void ParsesEmptyDocumentAsEmptyObject() { // Arrange var input = diff --git a/tests/ToonFormat.Tests/Decode/ValidationErrors.cs b/tests/ToonFormat.Tests/Decode/ValidationErrors.cs index 94298ba..622f096 100644 --- a/tests/ToonFormat.Tests/Decode/ValidationErrors.cs +++ b/tests/ToonFormat.Tests/Decode/ValidationErrors.cs @@ -51,8 +51,8 @@ public void ThrowsOnArrayLengthMismatchListFormatTooMany() } [Fact] - [Trait("Description", "throws when tabular row value count does not match header field count")] - public void ThrowsWhenTabularRowValueCountDoesNotMatchHeaderFieldCount() + [Trait("Description", "throws on tabular row value count mismatch with header field count")] + public void ThrowsOnTabularRowValueCountMismatchWithHeaderFieldCount() { // Arrange var input = @@ -67,8 +67,8 @@ public void ThrowsWhenTabularRowValueCountDoesNotMatchHeaderFieldCount() } [Fact] - [Trait("Description", "throws when tabular row count does not match header length")] - public void ThrowsWhenTabularRowCountDoesNotMatchHeaderLength() + [Trait("Description", "throws on tabular row count mismatch with header length")] + public void ThrowsOnTabularRowCountMismatchWithHeaderLength() { // Arrange var input = diff --git a/tests/ToonFormat.Tests/Decode/Whitespace.cs b/tests/ToonFormat.Tests/Decode/Whitespace.cs index 676a4f7..9f25c52 100644 --- a/tests/ToonFormat.Tests/Decode/Whitespace.cs +++ b/tests/ToonFormat.Tests/Decode/Whitespace.cs @@ -123,8 +123,8 @@ public void ToleratesSpacesAroundDelimitersWithQuotedValues() } [Fact] - [Trait("Description", "empty tokens decode to empty string")] - public void EmptyTokensDecodeToEmptyString() + [Trait("Description", "parses empty tokens as empty string")] + public void ParsesEmptyTokensAsEmptyString() { // Arrange var input = diff --git a/tests/ToonFormat.Tests/Encode/ArraysNested.cs b/tests/ToonFormat.Tests/Encode/ArraysNested.cs index 6f58f8e..25ee4d0 100644 --- a/tests/ToonFormat.Tests/Encode/ArraysNested.cs +++ b/tests/ToonFormat.Tests/Encode/ArraysNested.cs @@ -259,18 +259,44 @@ public void EncodesRootLevelArrayOfNonUniformObjectsInListFormat() } [Fact] - [Trait("Description", "encodes empty root-level array")] - public void EncodesEmptyRootLevelArray() + [Trait("Description", "encodes root-level array mixing primitive, object, and array of objects in list format")] + public void EncodesRootLevelArrayMixingPrimitiveObjectAndArrayOfObjectsInListFormat() { // Arrange var input = new object[] { + @"summary", + new + { + @id = 1, + @name = @"Ada", + } + , + new object[] { + new + { + @id = 2, + } + , + new + { + @status = @"draft", + } + , + } + , } ; var expected = """ -[0]: +[3]: + - summary + - id: 1 + name: Ada + - [2]: + - id: 2 + - status: draft """; // Act & Assert @@ -310,6 +336,27 @@ public void EncodesRootLevelArraysOfArrays() 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 complex nested structure")] public void EncodesComplexNestedStructure() diff --git a/tests/ToonFormat.Tests/Encode/ArraysObjects.cs b/tests/ToonFormat.Tests/Encode/ArraysObjects.cs index 50aadf6..1a8bd44 100644 --- a/tests/ToonFormat.Tests/Encode/ArraysObjects.cs +++ b/tests/ToonFormat.Tests/Encode/ArraysObjects.cs @@ -119,7 +119,7 @@ public void PreservesFieldOrderInListItemsArrayFirst() 3, } , - @name = @"test", + @name = @"Ada", } , } @@ -131,7 +131,7 @@ public void PreservesFieldOrderInListItemsArrayFirst() """ items[1]: - nums[3]: 1,2,3 - name: test + name: Ada """; // Act & Assert @@ -151,7 +151,7 @@ public void PreservesFieldOrderInListItemsPrimitiveFirst() @items =new object[] { new { - @name = @"test", + @name = @"Ada", @nums =new object[] { 1, 2, @@ -168,7 +168,7 @@ public void PreservesFieldOrderInListItemsPrimitiveFirst() var expected = """ items[1]: - - name: test + - name: Ada nums[3]: 1,2,3 """; @@ -214,8 +214,8 @@ public void UsesListFormatForObjectsContainingArraysOfArrays() """ items[1]: - matrix[2]: - - [2]: 1,2 - - [2]: 3,4 + - [2]: 1,2 + - [2]: 3,4 name: grid """; @@ -263,8 +263,8 @@ public void UsesTabularFormatForNestedUniformObjectArrays() """ items[1]: - users[2]{id,name}: - 1,Ada - 2,Bob + 1,Ada + 2,Bob status: active """; @@ -311,9 +311,9 @@ public void UsesListFormatForNestedObjectArraysWithMismatchedKeys() """ items[1]: - users[2]: - - id: 1 - name: Ada - - id: 2 + - id: 1 + name: Ada + - id: 2 status: active """; @@ -419,7 +419,7 @@ public void EncodesObjectsWithEmptyArraysInListFormat() @items =new object[] { new { - @name = @"test", + @name = @"Ada", @data =new object[] { } , @@ -433,7 +433,7 @@ public void EncodesObjectsWithEmptyArraysInListFormat() var expected = """ items[1]: - - name: test + - name: Ada data[0]: """; @@ -444,8 +444,8 @@ public void EncodesObjectsWithEmptyArraysInListFormat() } [Fact] - [Trait("Description", "places first field of nested tabular arrays on hyphen line")] - public void PlacesFirstFieldOfNestedTabularArraysOnHyphenLine() + [Trait("Description", "uses canonical encoding for multi-field list-item objects with tabular arrays")] + public void UsesCanonicalEncodingForMultiFieldListItemObjectsWithTabularArrays() { // Arrange var input = @@ -479,8 +479,8 @@ public void PlacesFirstFieldOfNestedTabularArraysOnHyphenLine() """ items[1]: - users[2]{id}: - 1 - 2 + 1 + 2 note: x """; @@ -490,6 +490,53 @@ public void PlacesFirstFieldOfNestedTabularArraysOnHyphenLine() Assert.Equal(expected, result); } + [Fact] + [Trait("Description", "uses canonical encoding for single-field list-item tabular arrays")] + public void UsesCanonicalEncodingForSingleFieldListItemTabularArrays() + { + // Arrange + var input = + new + { + @items =new object[] { + new + { + @users =new object[] { + new + { + @id = 1, + @name = @"Ada", + } + , + new + { + @id = 2, + @name = @"Bob", + } + , + } +, + } + , + } +, + } + ; + + var expected = +""" +items[1]: + - users[2]{id,name}: + 1,Ada + 2,Bob +"""; + + // 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() @@ -525,6 +572,40 @@ public void PlacesEmptyArraysOnHyphenLineWhenFirst() Assert.Equal(expected, result); } + [Fact] + [Trait("Description", "encodes empty object list items as bare hyphen")] + public void EncodesEmptyObjectListItemsAsBareHyphen() + { + // Arrange + var input = + new + { + @items =new object[] { + @"first", + @"second", + new + { + } + , + } +, + } + ; + + var expected = +""" +items[3]: + - first + - second + - +"""; + + // 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() @@ -567,8 +648,8 @@ public void UsesFieldOrderFromFirstObjectForTabularHeaders() } [Fact] - [Trait("Description", "uses list format when one object has nested column")] - public void UsesListFormatWhenOneObjectHasNestedColumn() + [Trait("Description", "uses list format when one object has nested field")] + public void UsesListFormatWhenOneObjectHasNestedField() { // Arrange var input = diff --git a/tests/ToonFormat.Tests/Encode/ArraysObjectsManual.cs b/tests/ToonFormat.Tests/Encode/ArraysObjectsManual.cs new file mode 100644 index 0000000..929aad7 --- /dev/null +++ b/tests/ToonFormat.Tests/Encode/ArraysObjectsManual.cs @@ -0,0 +1,137 @@ +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 ArraysObjectsManual +{ + [Fact] + [Trait("Description", "tabular array with multiple sibling fields")] + public void TabularArrayWithMultipleSiblingFields() + { + // Arrange + var input = + new + { + @items = new object[] { + new + { + @users = new object[] { + new { @id = 1, @name = "Ada" }, + new { @id = 2, @name = "Bob" }, + }, + @status = "active", + @count = 2, + @tags = new object[] { "a", "b", "c" } + } + } + }; + + var expected = +""" +items[1]: + - users[2]{id,name}: + 1,Ada + 2,Bob + status: active + count: 2 + tags[3]: a,b,c +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "multiple list items with tabular first fields")] + public void MultipleListItemsWithTabularFirstFields() + { + // Arrange + var input = + new + { + @items = new object[] { + new + { + @users = new object[] { + new { @id = 1, @name = "Ada" }, + new { @id = 2, @name = "Bob" }, + }, + @status = "active" + }, + new + { + @users = new object[] { + new { @id = 3, @name = "Charlie" } + }, + @status = "inactive" + } + } + }; + + var expected = +""" +items[2]: + - users[2]{id,name}: + 1,Ada + 2,Bob + status: active + - users[1]{id,name}: + 3,Charlie + status: inactive +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "deeply nested list items with tabular first field")] + public void DeeplyNestedListItemsWithTabularFirstField() + { + // Arrange + var input = + new + { + @data = new object[] { + new + { + @items = new object[] { + new + { + @users = new object[] { + new { @id = 1, @name = "Ada" }, + new { @id = 2, @name = "Bob" }, + }, + @status = "active" + } + } + } + } + }; + + var expected = +""" +data[1]: + - items[1]: + - users[2]{id,name}: + 1,Ada + 2,Bob + status: active +"""; + + // 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 index 4241130..18da328 100644 --- a/tests/ToonFormat.Tests/Encode/ArraysTabular.cs +++ b/tests/ToonFormat.Tests/Encode/ArraysTabular.cs @@ -21,8 +21,8 @@ namespace ToonFormat.Tests.Encode; public class ArraysTabular { [Fact] - [Trait("Description", "encodes arrays of similar objects in tabular format")] - public void EncodesArraysOfSimilarObjectsInTabularFormat() + [Trait("Description", "encodes arrays of uniform objects in tabular format")] + public void EncodesArraysOfUniformObjectsInTabularFormat() { // Arrange var input = diff --git a/tests/ToonFormat.Tests/Encode/Delimiters.cs b/tests/ToonFormat.Tests/Encode/Delimiters.cs index fab7dea..0eaf500 100644 --- a/tests/ToonFormat.Tests/Encode/Delimiters.cs +++ b/tests/ToonFormat.Tests/Encode/Delimiters.cs @@ -296,8 +296,8 @@ public void EncodesNestedArraysWithPipeDelimiter() } [Fact] - [Trait("Description", "encodes root arrays with tab delimiter")] - public void EncodesRootArraysWithTabDelimiter() + [Trait("Description", "encodes root-level array with tab delimiter")] + public void EncodesRootLevelArrayWithTabDelimiter() { // Arrange var input = @@ -325,8 +325,8 @@ public void EncodesRootArraysWithTabDelimiter() } [Fact] - [Trait("Description", "encodes root arrays with pipe delimiter")] - public void EncodesRootArraysWithPipeDelimiter() + [Trait("Description", "encodes root-level array with pipe delimiter")] + public void EncodesRootLevelArrayWithPipeDelimiter() { // Arrange var input = @@ -354,8 +354,8 @@ public void EncodesRootArraysWithPipeDelimiter() } [Fact] - [Trait("Description", "encodes root arrays of objects with tab delimiter")] - public void EncodesRootArraysOfObjectsWithTabDelimiter() + [Trait("Description", "encodes root-level array of objects with tab delimiter")] + public void EncodesRootLevelArrayOfObjectsWithTabDelimiter() { // Arrange var input = @@ -392,8 +392,8 @@ public void EncodesRootArraysOfObjectsWithTabDelimiter() } [Fact] - [Trait("Description", "encodes root arrays of objects with pipe delimiter")] - public void EncodesRootArraysOfObjectsWithPipeDelimiter() + [Trait("Description", "encodes root-level array of objects with pipe delimiter")] + public void EncodesRootLevelArrayOfObjectsWithPipeDelimiter() { // Arrange var input = diff --git a/tests/ToonFormat.Tests/JsonComplexRoundTripTests.cs b/tests/ToonFormat.Tests/JsonComplexRoundTripTests.cs index 1d79fb5..9080e53 100644 --- a/tests/ToonFormat.Tests/JsonComplexRoundTripTests.cs +++ b/tests/ToonFormat.Tests/JsonComplexRoundTripTests.cs @@ -129,12 +129,12 @@ public void ComplexJson_RoundTrip_ShouldPreserveKeyFields() Assert.Equal(2.0, phase2["phaseId"]?.GetValue()); Assert.Equal("Development", phase2["title"]?.GetValue()); Assert.Equal("2026-01-30", phase2["deadline"]?.GetValue()); - + var budget = phase2["budget"]?.AsObject(); Assert.NotNull(budget); Assert.Equal("EUR", budget["currency"]?.GetValue()); Assert.Equal(7800.0, budget["amount"]?.GetValue()); - + var resources = phase2["resources"]?.AsObject(); Assert.NotNull(resources); Assert.Equal("alice.smith@example.com", resources["leadDeveloper"]?.GetValue()); @@ -159,7 +159,7 @@ public void ComplexJson_Encode_ShouldProduceValidToonFormat() Assert.Contains("PX-4921", toonText); Assert.Contains("metadata:", toonText); Assert.Contains("phases[2]", toonText); - + _output.WriteLine("TOON Output:"); _output.WriteLine(toonText); } @@ -182,7 +182,7 @@ public void ComplexJson_SpecialCharacters_ShouldBePreserved() var phases = project?["phases"]?.AsArray(); var phase1 = phases?[0]?.AsObject(); var details = phase1?["details"]?.AsObject(); - + Assert.NotNull(details); var specialChars = details["specialChars"]?.GetValue(); Assert.Equal("!@#$%^&*()_+=-{}[]|:;<>,.?/", specialChars); @@ -204,7 +204,7 @@ public void ComplexJson_DateTime_ShouldBePreservedAsString() Assert.NotNull(decoded); var project = decoded["project"]?.AsObject(); Assert.NotNull(project); - + var createdAt = project["createdAt"]?.GetValue(); Assert.NotNull(createdAt); // Validate full ISO 8601 UTC timestamp (with or without fractional seconds) diff --git a/tests/ToonFormat.Tests/PerformanceBenchmark.cs b/tests/ToonFormat.Tests/PerformanceBenchmark.cs new file mode 100644 index 0000000..b27cef8 --- /dev/null +++ b/tests/ToonFormat.Tests/PerformanceBenchmark.cs @@ -0,0 +1,131 @@ +using System; +using System.Diagnostics; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; +using Xunit.Abstractions; + +namespace ToonFormat.Tests; + +public class PerformanceBenchmark +{ + private readonly ITestOutputHelper _output; + + public PerformanceBenchmark(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void BenchmarkSimpleEncoding() + { + var data = new + { + users = new[] + { + new { id = 1, name = "Alice", role = "admin" }, + new { id = 2, name = "Bob", role = "user" }, + new { id = 3, name = "Charlie", role = "user" } + } + }; + + var sw = Stopwatch.StartNew(); + for (int i = 0; i < 10000; i++) + { + _ = ToonEncoder.Encode(data); + } + sw.Stop(); + + _output.WriteLine($"Simple encoding: {sw.ElapsedMilliseconds}ms for 10,000 iterations"); + _output.WriteLine($"Average: {sw.Elapsed.TotalMicroseconds / 10000:F2}μs per encode"); + + // Baseline: should complete in reasonable time (< 5 seconds for 10k iterations) + Assert.True(sw.ElapsedMilliseconds < 5000, $"Encoding took {sw.ElapsedMilliseconds}ms, expected < 5000ms"); + } + + [Fact] + public void BenchmarkSimpleDecoding() + { + var toonString = """ +users[3]{id,name,role}: + 1,Alice,admin + 2,Bob,user + 3,Charlie,user +"""; + + var sw = Stopwatch.StartNew(); + for (int i = 0; i < 10000; i++) + { + _ = ToonDecoder.Decode(toonString); + } + sw.Stop(); + + _output.WriteLine($"Simple decoding: {sw.ElapsedMilliseconds}ms for 10,000 iterations"); + _output.WriteLine($"Average: {sw.Elapsed.TotalMicroseconds / 10000:F2}μs per decode"); + + // Baseline: should complete in reasonable time (< 5 seconds for 10k iterations) + Assert.True(sw.ElapsedMilliseconds < 5000, $"Decoding took {sw.ElapsedMilliseconds}ms, expected < 5000ms"); + } + + [Fact] + public void BenchmarkSection10Encoding() + { + var data = new + { + items = new[] + { + new + { + users = new[] + { + new { id = 1, name = "Ada" }, + new { id = 2, name = "Bob" } + }, + status = "active", + count = 2 + } + } + }; + + var sw = Stopwatch.StartNew(); + for (int i = 0; i < 10000; i++) + { + _ = ToonEncoder.Encode(data); + } + sw.Stop(); + + _output.WriteLine($"Section 10 encoding: {sw.ElapsedMilliseconds}ms for 10,000 iterations"); + _output.WriteLine($"Average: {sw.Elapsed.TotalMicroseconds / 10000:F2}μs per encode"); + + // Baseline: should complete in reasonable time + Assert.True(sw.ElapsedMilliseconds < 5000, $"Section 10 encoding took {sw.ElapsedMilliseconds}ms, expected < 5000ms"); + } + + [Fact] + public void BenchmarkRoundTrip() + { + var data = new + { + users = new[] + { + new { id = 1, name = "Alice" }, + new { id = 2, name = "Bob" } + }, + metadata = new { version = 1, timestamp = "2025-11-27" } + }; + + var sw = Stopwatch.StartNew(); + for (int i = 0; i < 5000; i++) + { + var encoded = ToonEncoder.Encode(data); + _ = ToonDecoder.Decode(encoded); + } + sw.Stop(); + + _output.WriteLine($"Round-trip: {sw.ElapsedMilliseconds}ms for 5,000 iterations"); + _output.WriteLine($"Average: {sw.Elapsed.TotalMicroseconds / 5000:F2}μs per round-trip"); + + // Baseline: should complete in reasonable time + Assert.True(sw.ElapsedMilliseconds < 5000, $"Round-trip took {sw.ElapsedMilliseconds}ms, expected < 5000ms"); + } +} From 16d35fb18eaaf0cc554286b0f12c8a32ba6702b3 Mon Sep 17 00:00:00 2001 From: Carlos Felipe Date: Thu, 27 Nov 2025 23:23:37 -0500 Subject: [PATCH 02/20] - Removed `IsIdentifierSegment` and `IdentifierPattern` from `PathExpansion.cs` - Replaced dot (.) with `Constants.DOT` in `PathExpansion.cs` - Added `ToonPathExpansionException` - Updated spec generator aligned to v3.0.0 --- specgen.ps1 | 2 +- specgen.sh | 2 +- src/ToonFormat/Constants.cs | 12 +++ .../Internal/Decode/PathExpansion.cs | 34 +++----- src/ToonFormat/Internal/Decode/Validation.cs | 2 +- src/ToonFormat/Internal/Encode/LineWriter.cs | 2 +- src/ToonFormat/Internal/Shared/StringUtils.cs | 1 + src/ToonFormat/Options/ToonDecodeOptions.cs | 2 +- src/ToonFormat/ToonDecoder.cs | 4 +- src/ToonFormat/ToonPathExpansionException.cs | 86 +++++++++++++++++++ .../ToonFormat.Tests/Decode/PathExpansion.cs | 28 +++--- 11 files changed, 133 insertions(+), 42 deletions(-) create mode 100644 src/ToonFormat/ToonPathExpansionException.cs diff --git a/specgen.ps1 b/specgen.ps1 index 9b582ee..447402a 100644 --- a/specgen.ps1 +++ b/specgen.ps1 @@ -5,4 +5,4 @@ $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" +dotnet run --project tests/ToonFormat.SpecGenerator -- --url="$GH_REPO" --output="$OUT_DIR" --branch="v3.0.0" --loglevel="Information" diff --git a/specgen.sh b/specgen.sh index 8d902d6..ce45262 100755 --- a/specgen.sh +++ b/specgen.sh @@ -5,4 +5,4 @@ OUT_DIR="./tests/ToonFormat.Tests" # build and execute spec generator dotnet build tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj -dotnet run --project tests/ToonFormat.SpecGenerator -- --url="$GH_REPO" --output="$OUT_DIR" --branch="main" --loglevel="Information" \ No newline at end of file +dotnet run --project tests/ToonFormat.SpecGenerator -- --url="$GH_REPO" --output="$OUT_DIR" --branch="v3.0.0" --loglevel="Information" \ No newline at end of file diff --git a/src/ToonFormat/Constants.cs b/src/ToonFormat/Constants.cs index 04be9f7..a92b515 100644 --- a/src/ToonFormat/Constants.cs +++ b/src/ToonFormat/Constants.cs @@ -125,4 +125,16 @@ public enum ToonKeyFolding Safe } + /// + /// Path expansion options + /// + public enum ToonPathExpansion + { + /// Path expansion disabled + Off, + + /// Keys containing dots are expanded into nested structures + Safe + } + } diff --git a/src/ToonFormat/Internal/Decode/PathExpansion.cs b/src/ToonFormat/Internal/Decode/PathExpansion.cs index 0989f27..b54c3ff 100644 --- a/src/ToonFormat/Internal/Decode/PathExpansion.cs +++ b/src/ToonFormat/Internal/Decode/PathExpansion.cs @@ -13,8 +13,6 @@ namespace ToonFormat.Internal.Decode /// internal static class PathExpansion { - private static readonly Regex IdentifierPattern = new Regex(@"^[A-Za-z_][A-Za-z0-9_]*$", RegexOptions.Compiled); - /// /// Expands dotted keys in a JsonObject into nested structures. /// Example: {"a.b.c": 1} -> {"a": {"b": {"c": 1}}} @@ -35,10 +33,10 @@ public static JsonObject ExpandPaths(JsonObject obj, bool strict, HashSet private static bool IsExpandable(string key) { - var segments = key.Split('.'); - return segments.All(segment => IsIdentifierSegment(segment)); - } - - /// - /// Checks if a segment is a valid identifier per SPEC §1.9 - /// - private static bool IsIdentifierSegment(string segment) - { - if (string.IsNullOrEmpty(segment)) - return false; - - return IdentifierPattern.IsMatch(segment); + var segments = key.Split(Constants.DOT); + return segments.All(segment => ValidationShared.IsIdentifierSegment(segment)); } /// @@ -97,8 +84,11 @@ private static void SetNestedValue(JsonObject target, string[] segments, JsonNod // Conflict: path requires object but found non-object if (strict) { - throw ToonFormatException.Syntax( - $"Path expansion conflict at '{segment}': expected object but found {GetTypeName(existing)}" + throw ToonPathExpansionException.TraversalConflict( + segment, + GetTypeName(existing), + string.Join(".", segments), + i ); } else @@ -149,8 +139,10 @@ private static void SetValue(JsonObject target, string key, JsonNode? value, boo { if (strict) { - throw ToonFormatException.Syntax( - $"Path expansion conflict at '{key}': {GetTypeName(existing)} vs {GetTypeName(value)}" + throw ToonPathExpansionException.AssignmentConflict( + key, + GetTypeName(value), + GetTypeName(existing) ); } // LWW: just overwrite diff --git a/src/ToonFormat/Internal/Decode/Validation.cs b/src/ToonFormat/Internal/Decode/Validation.cs index 0e785c1..f985dfc 100644 --- a/src/ToonFormat/Internal/Decode/Validation.cs +++ b/src/ToonFormat/Internal/Decode/Validation.cs @@ -12,7 +12,7 @@ internal class ResolvedDecodeOptions { public int Indent { get; set; } = 2; public bool Strict { get; set; } = false; - public string ExpandPaths { get; set; } = "off"; + public ToonPathExpansion ExpandPaths { get; set; } = ToonPathExpansion.Off; } /// diff --git a/src/ToonFormat/Internal/Encode/LineWriter.cs b/src/ToonFormat/Internal/Encode/LineWriter.cs index fb3409f..3e72f35 100644 --- a/src/ToonFormat/Internal/Encode/LineWriter.cs +++ b/src/ToonFormat/Internal/Encode/LineWriter.cs @@ -48,7 +48,7 @@ public void PushListItem(int depth, string content) /// public override string ToString() { - return string.Join("\n", _lines); + return string.Join(Environment.NewLine, _lines); } /// diff --git a/src/ToonFormat/Internal/Shared/StringUtils.cs b/src/ToonFormat/Internal/Shared/StringUtils.cs index 3677df6..859ec21 100644 --- a/src/ToonFormat/Internal/Shared/StringUtils.cs +++ b/src/ToonFormat/Internal/Shared/StringUtils.cs @@ -21,6 +21,7 @@ internal static string EscapeString(string value) if (string.IsNullOrEmpty(value)) return value ?? string.Empty; return value + .Replace("\r\n", "\n") .Replace("\\", $"{Constants.BACKSLASH}{Constants.BACKSLASH}") .Replace("\"", $"{Constants.BACKSLASH}{Constants.DOUBLE_QUOTE}") .Replace("\n", $"{Constants.BACKSLASH}n") diff --git a/src/ToonFormat/Options/ToonDecodeOptions.cs b/src/ToonFormat/Options/ToonDecodeOptions.cs index a286d86..374dd49 100644 --- a/src/ToonFormat/Options/ToonDecodeOptions.cs +++ b/src/ToonFormat/Options/ToonDecodeOptions.cs @@ -23,5 +23,5 @@ public class ToonDecodeOptions /// "off" (default): Dotted keys are treated as literal keys. /// "safe": Expand eligible dotted keys into nested objects. /// - public string ExpandPaths { get; set; } = "off"; + public ToonFormat.ToonPathExpansion ExpandPaths { get; set; } = ToonFormat.ToonPathExpansion.Off; } diff --git a/src/ToonFormat/ToonDecoder.cs b/src/ToonFormat/ToonDecoder.cs index 5d5db20..1f14cec 100644 --- a/src/ToonFormat/ToonDecoder.cs +++ b/src/ToonFormat/ToonDecoder.cs @@ -76,7 +76,7 @@ public static class ToonDecoder // Track quoted keys if path expansion is enabled HashSet? quotedKeys = null; - if (resolvedOptions.ExpandPaths == "safe") + if (resolvedOptions.ExpandPaths == ToonPathExpansion.Safe) { quotedKeys = new HashSet(); } @@ -84,7 +84,7 @@ public static class ToonDecoder var result = Decoders.DecodeValueFromLines(cursor, resolvedOptions, quotedKeys); // Apply path expansion if enabled - if (resolvedOptions.ExpandPaths == "safe" && result is JsonObject obj) + if (resolvedOptions.ExpandPaths == ToonPathExpansion.Safe && result is JsonObject obj) { result = PathExpansion.ExpandPaths(obj, resolvedOptions.Strict, quotedKeys); } diff --git a/src/ToonFormat/ToonPathExpansionException.cs b/src/ToonFormat/ToonPathExpansionException.cs new file mode 100644 index 0000000..273b2b2 --- /dev/null +++ b/src/ToonFormat/ToonPathExpansionException.cs @@ -0,0 +1,86 @@ +#nullable enable +using System; +using System.Text; + +namespace ToonFormat +{ + /// + /// Exception thrown when path expansion conflicts occur during TOON decoding. + /// + public sealed class ToonPathExpansionException : Exception + { + /// The key or path segment where the conflict occurred. + public string Key { get; } + + /// The full dotted path being expanded (if available). + public string? FullPath { get; } + + /// The type that was expected at this location. + public string ExpectedType { get; } + + /// The type that was actually found at this location. + public string ActualType { get; } + + /// Depth in the path where the conflict occurred (optional). + public int? Depth { get; } + + /// Constructs the exception with conflict details. + public ToonPathExpansionException( + string key, + string expectedType, + string actualType, + string? fullPath = null, + int? depth = null, + Exception? inner = null) + : base(BuildMessage(key, expectedType, actualType, fullPath, depth), inner) + { + Key = key; + ExpectedType = expectedType; + ActualType = actualType; + FullPath = fullPath; + Depth = depth; + } + + /// Factory method for path traversal conflicts. + public static ToonPathExpansionException TraversalConflict( + string segment, + string actualType, + string? fullPath = null, + int? depth = null) + => new(segment, "object", actualType, fullPath, depth); + + /// Factory method for key assignment conflicts. + public static ToonPathExpansionException AssignmentConflict( + string key, + string expectedType, + string actualType, + string? fullPath = null, + int? depth = null) + => new(key, expectedType, actualType, fullPath, depth); + + private static string BuildMessage( + string key, + string expectedType, + string actualType, + string? fullPath, + int? depth) + { + var sb = new StringBuilder(); + sb.Append("[PathExpansion] Conflict at '").Append(key).Append("': "); + sb.Append("expected ").Append(expectedType); + sb.Append(" but found ").Append(actualType); + + if (!string.IsNullOrEmpty(fullPath)) + { + sb.Append(" (in path '").Append(fullPath).Append("')"); + } + + if (depth is not null) + { + sb.Append(" (depth: ").Append(depth.Value).Append(")"); + } + + return sb.ToString(); + } + } +} diff --git a/tests/ToonFormat.Tests/Decode/PathExpansion.cs b/tests/ToonFormat.Tests/Decode/PathExpansion.cs index 9cd29fe..15d54df 100644 --- a/tests/ToonFormat.Tests/Decode/PathExpansion.cs +++ b/tests/ToonFormat.Tests/Decode/PathExpansion.cs @@ -35,7 +35,7 @@ public void ExpandsDottedKeyToNestedObjectInSafeMode() { Indent = 2, Strict = true, - ExpandPaths = "safe" + ExpandPaths = ToonPathExpansion.Safe }; var result = ToonDecoder.Decode(input, options); @@ -62,7 +62,7 @@ public void ExpandsDottedKeyWithInlineArray() { Indent = 2, Strict = true, - ExpandPaths = "safe" + ExpandPaths = ToonPathExpansion.Safe }; var result = ToonDecoder.Decode(input, options); @@ -91,7 +91,7 @@ public void ExpandsDottedKeyWithTabularArray() { Indent = 2, Strict = true, - ExpandPaths = "safe" + ExpandPaths = ToonPathExpansion.Safe }; var result = ToonDecoder.Decode(input, options); @@ -118,7 +118,7 @@ public void PreservesLiteralDottedKeysWhenExpansionIsOff() { Indent = 2, Strict = true, - ExpandPaths = "off" + ExpandPaths = ToonPathExpansion.Off }; var result = ToonDecoder.Decode(input, options); @@ -147,7 +147,7 @@ public void ExpandsAndDeepMergesPreservingDocumentOrderInsertion() { Indent = 2, Strict = true, - ExpandPaths = "safe" + ExpandPaths = ToonPathExpansion.Safe }; var result = ToonDecoder.Decode(input, options); @@ -175,10 +175,10 @@ public void ThrowsOnExpansionConflictObjectVsPrimitiveWhenStrictTrue() { Indent = 2, Strict = true, - ExpandPaths = "safe" + ExpandPaths = ToonPathExpansion.Safe }; - Assert.Throws(() => ToonDecoder.Decode(input, options)); + Assert.Throws(() => ToonDecoder.Decode(input, options)); } [Fact] @@ -197,10 +197,10 @@ public void ThrowsOnExpansionConflictObjectVsArrayWhenStrictTrue() { Indent = 2, Strict = true, - ExpandPaths = "safe" + ExpandPaths = ToonPathExpansion.Safe }; - Assert.Throws(() => ToonDecoder.Decode(input, options)); + Assert.Throws(() => ToonDecoder.Decode(input, options)); } [Fact] @@ -219,7 +219,7 @@ public void AppliesLwwWhenStrictFalsePrimitiveOverwritesExpandedObject() { Indent = 2, Strict = false, - ExpandPaths = "safe" + ExpandPaths = ToonPathExpansion.Safe }; var result = ToonDecoder.Decode(input, options); @@ -247,7 +247,7 @@ public void AppliesLwwWhenStrictFalseExpandedObjectOverwritesPrimitive() { Indent = 2, Strict = false, - ExpandPaths = "safe" + ExpandPaths = ToonPathExpansion.Safe }; var result = ToonDecoder.Decode(input, options); @@ -275,7 +275,7 @@ public void PreservesQuotedDottedKeyAsLiteralWhenExpandpathsSafe() { Indent = 2, Strict = true, - ExpandPaths = "safe" + ExpandPaths = ToonPathExpansion.Safe }; var result = ToonDecoder.Decode(input, options); @@ -302,7 +302,7 @@ public void PreservesNonIdentifiersegmentKeysAsLiterals() { Indent = 2, Strict = true, - ExpandPaths = "safe" + ExpandPaths = ToonPathExpansion.Safe }; var result = ToonDecoder.Decode(input, options); @@ -329,7 +329,7 @@ public void ExpandsKeysCreatingEmptyNestedObjects() { Indent = 2, Strict = true, - ExpandPaths = "safe" + ExpandPaths = ToonPathExpansion.Safe }; var result = ToonDecoder.Decode(input, options); From 64a00e7d8d9830b397e08211bf9fbe6c105b778c Mon Sep 17 00:00:00 2001 From: Carlos Felipe Date: Fri, 28 Nov 2025 02:15:58 -0500 Subject: [PATCH 03/20] Update README.md Co-authored-by: Johann Schopplich --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5a0a055..28dd06f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![.NET version](https://img.shields.io/badge/.NET-8.0%20%7C%209.0-512BD4)](https://dotnet.microsoft.com/) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) -Compact, human-readable serialization format for LLM contexts with **30-60% token reduction** vs JSON. Combines YAML-like indentation with CSV-like tabular arrays. Fully compatible with the [official TOON specification v3.0](https://github.com/toon-format/spec). +**Token-Oriented Object Notation** is a compact, human-readable encoding of the JSON data model that minimizes tokens and makes structure easy for models to follow. Combines YAML-like indentation with CSV-like tabular arrays. Fully compatible with the [official TOON specification v3.0](https://github.com/toon-format/spec). **Key Features:** Minimal syntax • TOON Encoding and Decoding • Tabular arrays for uniform data • Path expansion • Strict mode validation • .NET 8.0 & 9.0 • 370+ tests with 99.7% spec coverage. From 1e079c258f6ba2c1df96feb03235c404ee1cf51c Mon Sep 17 00:00:00 2001 From: Carlos Felipe Date: Fri, 28 Nov 2025 14:47:39 -0500 Subject: [PATCH 04/20] Fixes - Make Encode windows-friendly - Make SpecGenerator make a clean of tests folder and re-generate the files - Updated `specgen.sh` aligned to .ps1 file --- specgen.sh | 2 +- src/ToonFormat/Constants.cs | 2 +- src/ToonFormat/Internal/Encode/LineWriter.cs | 2 +- .../ToonFormat.SpecGenerator/FixtureWriter.cs | 35 +- .../ManualTests/Encode/ArraysObjectsManual.cs | 137 +++++ .../ManualTests/JsonComplexRoundTripTests.cs | 255 +++++++++ .../ManualTests/KeyFoldingTests.cs | 501 ++++++++++++++++++ .../ManualTests/PerformanceBenchmark.cs | 131 +++++ .../ManualTests/ToonDecoderTests.cs | 170 ++++++ .../ManualTests/ToonEncoderTests.cs | 154 ++++++ .../ManualTests/ToonRoundTripTests.cs | 84 +++ .../ToonFormat.SpecGenerator/SpecGenerator.cs | 104 ++++ .../ToonFormat.SpecGenerator.csproj | 5 + tests/ToonFormat.Tests/KeyFoldingTests.cs | 2 +- 14 files changed, 1576 insertions(+), 8 deletions(-) create mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/Encode/ArraysObjectsManual.cs create mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/JsonComplexRoundTripTests.cs create mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/KeyFoldingTests.cs create mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/PerformanceBenchmark.cs create mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/ToonDecoderTests.cs create mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs create mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/ToonRoundTripTests.cs diff --git a/specgen.sh b/specgen.sh index ce45262..8d43e8c 100755 --- a/specgen.sh +++ b/specgen.sh @@ -3,6 +3,6 @@ GH_REPO="https://github.com/toon-format/spec.git" OUT_DIR="./tests/ToonFormat.Tests" # build and execute spec generator -dotnet build tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj +dotnet build tests/ToonFormat.SpecGenerator dotnet run --project tests/ToonFormat.SpecGenerator -- --url="$GH_REPO" --output="$OUT_DIR" --branch="v3.0.0" --loglevel="Information" \ No newline at end of file diff --git a/src/ToonFormat/Constants.cs b/src/ToonFormat/Constants.cs index a92b515..c03ea16 100644 --- a/src/ToonFormat/Constants.cs +++ b/src/ToonFormat/Constants.cs @@ -132,7 +132,7 @@ public enum ToonPathExpansion { /// Path expansion disabled Off, - + /// Keys containing dots are expanded into nested structures Safe } diff --git a/src/ToonFormat/Internal/Encode/LineWriter.cs b/src/ToonFormat/Internal/Encode/LineWriter.cs index 3e72f35..fb3409f 100644 --- a/src/ToonFormat/Internal/Encode/LineWriter.cs +++ b/src/ToonFormat/Internal/Encode/LineWriter.cs @@ -48,7 +48,7 @@ public void PushListItem(int depth, string content) /// public override string ToString() { - return string.Join(Environment.NewLine, _lines); + return string.Join("\n", _lines); } /// diff --git a/tests/ToonFormat.SpecGenerator/FixtureWriter.cs b/tests/ToonFormat.SpecGenerator/FixtureWriter.cs index a9068ec..b8aaacb 100644 --- a/tests/ToonFormat.SpecGenerator/FixtureWriter.cs +++ b/tests/ToonFormat.SpecGenerator/FixtureWriter.cs @@ -21,6 +21,7 @@ public void WriteFile() Directory.CreateDirectory(OutputDir); using var writer = new StreamWriter(outputPath, false); + writer.NewLine = "\n"; // Use Unix line endings for cross-platform compatibility WriteHeader(writer); WriteLine(writer); @@ -92,7 +93,7 @@ private void WriteTestMethod(StreamWriter writer, TTestCase testCase) WriteLineIndented(writer, "var expected ="); WriteLine(writer, "\"\"\""); - Write(writer, encodeTestCase.Expected); + Write(writer, NormalizeLineEndings(encodeTestCase.Expected)); WriteLine(writer); WriteLine(writer, "\"\"\";"); @@ -102,7 +103,7 @@ private void WriteTestMethod(StreamWriter writer, TTestCase testCase) WriteLineIndented(writer, "var input ="); WriteLine(writer, "\"\"\""); - Write(writer, decodeTestCase.Input); + Write(writer, NormalizeLineEndings(decodeTestCase.Input)); WriteLine(writer); WriteLine(writer, "\"\"\";"); @@ -165,6 +166,10 @@ private void WriteTestMethod(StreamWriter writer, TTestCase testCase) WriteLineIndented(writer, $"Indent = {decodeTestCase.Options?.Indent ?? 2},"); WriteLineIndented(writer, $"Strict = {(decodeTestCase.Options?.Strict ?? true).ToString().ToLower()},"); + if (decodeTestCase.Options?.ExpandPaths != null) + WriteLineIndented(writer, $"ExpandPaths = {GetToonPathExpansionEnumFromString(decodeTestCase.Options.ExpandPaths)}"); + + Unindent(); WriteLineIndented(writer, "};"); @@ -173,13 +178,17 @@ private void WriteTestMethod(StreamWriter writer, TTestCase testCase) if (decodeTestCase.ShouldError) { + // Determine which exception type to expect based on the options + var isPathExpansionError = decodeTestCase.Options?.ExpandPaths != null; + var exceptionType = isPathExpansionError ? "ToonPathExpansionException" : "ToonFormatException"; + if (hasDecodeOptions) { - WriteLineIndented(writer, $"Assert.Throws(() => ToonDecoder.Decode(input, options));"); + WriteLineIndented(writer, $"Assert.Throws<{exceptionType}>(() => ToonDecoder.Decode(input, options));"); } else { - WriteLineIndented(writer, $"Assert.Throws(() => ToonDecoder.Decode(input));"); + WriteLineIndented(writer, $"Assert.Throws<{exceptionType}>(() => ToonDecoder.Decode(input));"); } } else @@ -272,6 +281,16 @@ private static string GetToonKeyFoldingEnumFromString(string? keyFoldingOption) }; } + private static string GetToonPathExpansionEnumFromString(string? expandPathsOption) + { + return expandPathsOption switch + { + "off" => "ToonPathExpansion.Off", + "safe" => "ToonPathExpansion.Safe", + _ => "ToonPathExpansion.Safe" + }; + } + private void WriteJsonNodeAsAnonymousType(StreamWriter writer, JsonNode node) { WriteJsonNode(writer, node); @@ -470,4 +489,12 @@ private void Write(StreamWriter writer, string contents) { writer.Write(contents); } + + /// + /// Normalizes line endings to Unix format (LF) for cross-platform compatibility. + /// + private static string NormalizeLineEndings(string text) + { + return text.Replace("\r\n", "\n"); + } } diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/Encode/ArraysObjectsManual.cs b/tests/ToonFormat.SpecGenerator/ManualTests/Encode/ArraysObjectsManual.cs new file mode 100644 index 0000000..929aad7 --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/ManualTests/Encode/ArraysObjectsManual.cs @@ -0,0 +1,137 @@ +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 ArraysObjectsManual +{ + [Fact] + [Trait("Description", "tabular array with multiple sibling fields")] + public void TabularArrayWithMultipleSiblingFields() + { + // Arrange + var input = + new + { + @items = new object[] { + new + { + @users = new object[] { + new { @id = 1, @name = "Ada" }, + new { @id = 2, @name = "Bob" }, + }, + @status = "active", + @count = 2, + @tags = new object[] { "a", "b", "c" } + } + } + }; + + var expected = +""" +items[1]: + - users[2]{id,name}: + 1,Ada + 2,Bob + status: active + count: 2 + tags[3]: a,b,c +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "multiple list items with tabular first fields")] + public void MultipleListItemsWithTabularFirstFields() + { + // Arrange + var input = + new + { + @items = new object[] { + new + { + @users = new object[] { + new { @id = 1, @name = "Ada" }, + new { @id = 2, @name = "Bob" }, + }, + @status = "active" + }, + new + { + @users = new object[] { + new { @id = 3, @name = "Charlie" } + }, + @status = "inactive" + } + } + }; + + var expected = +""" +items[2]: + - users[2]{id,name}: + 1,Ada + 2,Bob + status: active + - users[1]{id,name}: + 3,Charlie + status: inactive +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "deeply nested list items with tabular first field")] + public void DeeplyNestedListItemsWithTabularFirstField() + { + // Arrange + var input = + new + { + @data = new object[] { + new + { + @items = new object[] { + new + { + @users = new object[] { + new { @id = 1, @name = "Ada" }, + new { @id = 2, @name = "Bob" }, + }, + @status = "active" + } + } + } + } + }; + + var expected = +""" +data[1]: + - items[1]: + - users[2]{id,name}: + 1,Ada + 2,Bob + status: active +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } +} diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/JsonComplexRoundTripTests.cs b/tests/ToonFormat.SpecGenerator/ManualTests/JsonComplexRoundTripTests.cs new file mode 100644 index 0000000..9080e53 --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/ManualTests/JsonComplexRoundTripTests.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit.Abstractions; + +namespace ToonFormat.Tests; + +/// +/// Tests for complex multi-level JSON structures to validate TOON format encoding and decoding. +/// +public class JsonComplexRoundTripTests +{ + private readonly ITestOutputHelper _output; + + public JsonComplexRoundTripTests(ITestOutputHelper output) + { + _output = output; + } + private const string ComplexJson = @"{ + ""project"": { + ""id"": ""PX-4921"", + ""name"": ""Customer Insights Expansion"", + ""description"": ""This is a long descriptive text containing more than fifteen words to simulate a realistic business scenario for testing purposes."", + ""createdAt"": ""2025-11-20T10:32:00Z"", + ""metadata"": { + ""owner"": ""john.doe@example.com"", + ""website"": ""https://example.org/products/insights?ref=test&lang=en"", + ""tags"": [""analysis"", ""insights"", ""growth"", ""R&D""], + ""cost"": { + ""currency"": ""USD"", + ""amount"": 12500.75 + } + }, + ""phases"": [ + { + ""phaseId"": 1, + ""title"": ""Discovery & Research"", + ""deadline"": ""2025-12-15"", + ""status"": ""In Progress"", + ""details"": { + ""notes"": ""Team is conducting interviews, market analysis, and reviewing historical performance metrics & competitors."", + ""specialChars"": ""!@#$%^&*()_+=-{}[]|:;<>,.?/"" + } + }, + { + ""phaseId"": 2, + ""title"": ""Development"", + ""deadline"": ""2026-01-30"", + ""budget"": { + ""currency"": ""EUR"", + ""amount"": 7800.00 + }, + ""resources"": { + ""leadDeveloper"": ""alice.smith@example.com"", + ""repository"": ""https://github.com/example/repo"" + } + } + ] + } +}"; + + [Fact] + public void ComplexJson_RoundTrip_ShouldPreserveKeyFields() + { + // Arrange + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var root = JsonSerializer.Deserialize(ComplexJson, options); + + // Sanity + Assert.NotNull(root); + Assert.NotNull(root.project); + + // Act - encode to TOON and decode back + var toonText = ToonEncoder.Encode(root); + Assert.NotNull(toonText); + _output.WriteLine("TOON Encoded Output:"); + _output.WriteLine(toonText); + _output.WriteLine("---"); + + var decoded = ToonDecoder.Decode(toonText); + Assert.NotNull(decoded); + + // The encoder reflects C# property names (we used lowercase names to match original JSON keys) + var project = decoded["project"]?.AsObject(); + Assert.NotNull(project); + + // Assert key scalar values + Assert.Equal("PX-4921", project["id"]?.GetValue()); + Assert.Equal("Customer Insights Expansion", project["name"]?.GetValue()); + + // Metadata checks + var metadata = project["metadata"]?.AsObject(); + Assert.NotNull(metadata); + Assert.Equal("john.doe@example.com", metadata["owner"]?.GetValue()); + Assert.Equal("https://example.org/products/insights?ref=test&lang=en", metadata["website"]?.GetValue()); + + // Tags array validation + var tags = metadata["tags"]?.AsArray(); + Assert.NotNull(tags); + Assert.Equal(4, tags.Count); + Assert.Equal("analysis", tags[0]?.GetValue()); + Assert.Equal("insights", tags[1]?.GetValue()); + Assert.Equal("growth", tags[2]?.GetValue()); + Assert.Equal("R&D", tags[3]?.GetValue()); + + var cost = metadata["cost"]?.AsObject(); + Assert.NotNull(cost); + Assert.Equal("USD", cost["currency"]?.GetValue()); + Assert.Equal(12500.75, cost["amount"]?.GetValue()); + + // Phases checks + var phases = project["phases"]?.AsArray(); + Assert.NotNull(phases); + Assert.Equal(2, phases.Count); + + var phase1 = phases[0]?.AsObject(); + Assert.NotNull(phase1); + Assert.Equal(1.0, phase1["phaseId"]?.GetValue()); + var details = phase1["details"]?.AsObject(); + Assert.NotNull(details); + Assert.Contains("market analysis", details["notes"]?.GetValue() ?? string.Empty); + Assert.Equal("!@#$%^&*()_+=-{}[]|:;<>,.?/", details["specialChars"]?.GetValue()); + + var phase2 = phases[1]?.AsObject(); + Assert.NotNull(phase2); + Assert.Equal(2.0, phase2["phaseId"]?.GetValue()); + Assert.Equal("Development", phase2["title"]?.GetValue()); + Assert.Equal("2026-01-30", phase2["deadline"]?.GetValue()); + + var budget = phase2["budget"]?.AsObject(); + Assert.NotNull(budget); + Assert.Equal("EUR", budget["currency"]?.GetValue()); + Assert.Equal(7800.0, budget["amount"]?.GetValue()); + + var resources = phase2["resources"]?.AsObject(); + Assert.NotNull(resources); + Assert.Equal("alice.smith@example.com", resources["leadDeveloper"]?.GetValue()); + Assert.Equal("https://github.com/example/repo", resources["repository"]?.GetValue()); + } + + [Fact] + public void ComplexJson_Encode_ShouldProduceValidToonFormat() + { + // Arrange + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var root = JsonSerializer.Deserialize(ComplexJson, options); + Assert.NotNull(root); + + // Act + var toonText = ToonEncoder.Encode(root); + + // Assert - verify TOON format structure + Assert.NotNull(toonText); + Assert.Contains("project:", toonText); + Assert.Contains("id:", toonText); + Assert.Contains("PX-4921", toonText); + Assert.Contains("metadata:", toonText); + Assert.Contains("phases[2]", toonText); + + _output.WriteLine("TOON Output:"); + _output.WriteLine(toonText); + } + + [Fact] + public void ComplexJson_SpecialCharacters_ShouldBePreserved() + { + // Arrange + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var root = JsonSerializer.Deserialize(ComplexJson, options); + Assert.NotNull(root); + + // Act + var toonText = ToonEncoder.Encode(root); + var decoded = ToonDecoder.Decode(toonText); + + // Assert - verify special characters in details.specialChars + Assert.NotNull(decoded); + var project = decoded["project"]?.AsObject(); + var phases = project?["phases"]?.AsArray(); + var phase1 = phases?[0]?.AsObject(); + var details = phase1?["details"]?.AsObject(); + + Assert.NotNull(details); + var specialChars = details["specialChars"]?.GetValue(); + Assert.Equal("!@#$%^&*()_+=-{}[]|:;<>,.?/", specialChars); + } + + [Fact] + public void ComplexJson_DateTime_ShouldBePreservedAsString() + { + // Arrange + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var root = JsonSerializer.Deserialize(ComplexJson, options); + Assert.NotNull(root); + + // Act + var toonText = ToonEncoder.Encode(root); + var decoded = ToonDecoder.Decode(toonText); + + // Assert - verify DateTime is preserved as full ISO 8601 UTC timestamp + Assert.NotNull(decoded); + var project = decoded["project"]?.AsObject(); + Assert.NotNull(project); + + var createdAt = project["createdAt"]?.GetValue(); + Assert.NotNull(createdAt); + // Validate full ISO 8601 UTC timestamp (with or without fractional seconds) + // .NET DateTime.ToString("O") produces: 2025-11-20T10:32:00.0000000Z + Assert.StartsWith("2025-11-20T10:32:00", createdAt); + Assert.EndsWith("Z", createdAt); + } + + // POCOs with lowercase property names to preserve original JSON keys when encoding via reflection + public class Root { public Project project { get; set; } = null!; } + + public class Project + { + public string id { get; set; } = null!; + public string name { get; set; } = null!; + public string description { get; set; } = null!; + public DateTime createdAt { get; set; } + public Metadata metadata { get; set; } = null!; + public List phases { get; set; } = new(); + } + + public class Metadata + { + public string owner { get; set; } = null!; + public string website { get; set; } = null!; + public List tags { get; set; } = new(); + public Cost cost { get; set; } = null!; + } + + public class Cost { public string currency { get; set; } = null!; public double amount { get; set; } } + + public class Phase + { + public int phaseId { get; set; } + public string title { get; set; } = null!; + public string deadline { get; set; } = null!; + public string? status { get; set; } + public Details? details { get; set; } + public Budget? budget { get; set; } + public Resources? resources { get; set; } + } + + public class Details { public string notes { get; set; } = null!; public string specialChars { get; set; } = null!; } + + public class Budget { public string currency { get; set; } = null!; public double amount { get; set; } } + + public class Resources { public string leadDeveloper { get; set; } = null!; public string repository { get; set; } = null!; } +} diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/KeyFoldingTests.cs b/tests/ToonFormat.SpecGenerator/ManualTests/KeyFoldingTests.cs new file mode 100644 index 0000000..5c637e4 --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/ManualTests/KeyFoldingTests.cs @@ -0,0 +1,501 @@ +using Toon.Format; + +namespace ToonFormat.Tests; + +// TODO: Remove these tests once generated spec tests are in source control +// used to validate current key folding functionality aligns with spec +public class KeyFoldingTests +{ + [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, + KeyFolding = ToonKeyFolding.Safe + }; + + 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, + KeyFolding = ToonKeyFolding.Safe + }; + + 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, + KeyFolding = ToonKeyFolding.Safe + }; + + 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, + KeyFolding = ToonKeyFolding.Safe, + FlattenDepth = 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, + KeyFolding = ToonKeyFolding.Safe + }; + + 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, + KeyFolding = ToonKeyFolding.Safe, + FlattenDepth = 0, + }; + + 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, + KeyFolding = ToonKeyFolding.Safe, + FlattenDepth = 1, + }; + + 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, + KeyFolding = ToonKeyFolding.Off + }; + + 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, + KeyFolding = ToonKeyFolding.Safe + }; + + 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, + KeyFolding = ToonKeyFolding.Safe + }; + + 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, + KeyFolding = ToonKeyFolding.Safe + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } +} diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/PerformanceBenchmark.cs b/tests/ToonFormat.SpecGenerator/ManualTests/PerformanceBenchmark.cs new file mode 100644 index 0000000..b27cef8 --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/ManualTests/PerformanceBenchmark.cs @@ -0,0 +1,131 @@ +using System; +using System.Diagnostics; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; +using Xunit.Abstractions; + +namespace ToonFormat.Tests; + +public class PerformanceBenchmark +{ + private readonly ITestOutputHelper _output; + + public PerformanceBenchmark(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void BenchmarkSimpleEncoding() + { + var data = new + { + users = new[] + { + new { id = 1, name = "Alice", role = "admin" }, + new { id = 2, name = "Bob", role = "user" }, + new { id = 3, name = "Charlie", role = "user" } + } + }; + + var sw = Stopwatch.StartNew(); + for (int i = 0; i < 10000; i++) + { + _ = ToonEncoder.Encode(data); + } + sw.Stop(); + + _output.WriteLine($"Simple encoding: {sw.ElapsedMilliseconds}ms for 10,000 iterations"); + _output.WriteLine($"Average: {sw.Elapsed.TotalMicroseconds / 10000:F2}μs per encode"); + + // Baseline: should complete in reasonable time (< 5 seconds for 10k iterations) + Assert.True(sw.ElapsedMilliseconds < 5000, $"Encoding took {sw.ElapsedMilliseconds}ms, expected < 5000ms"); + } + + [Fact] + public void BenchmarkSimpleDecoding() + { + var toonString = """ +users[3]{id,name,role}: + 1,Alice,admin + 2,Bob,user + 3,Charlie,user +"""; + + var sw = Stopwatch.StartNew(); + for (int i = 0; i < 10000; i++) + { + _ = ToonDecoder.Decode(toonString); + } + sw.Stop(); + + _output.WriteLine($"Simple decoding: {sw.ElapsedMilliseconds}ms for 10,000 iterations"); + _output.WriteLine($"Average: {sw.Elapsed.TotalMicroseconds / 10000:F2}μs per decode"); + + // Baseline: should complete in reasonable time (< 5 seconds for 10k iterations) + Assert.True(sw.ElapsedMilliseconds < 5000, $"Decoding took {sw.ElapsedMilliseconds}ms, expected < 5000ms"); + } + + [Fact] + public void BenchmarkSection10Encoding() + { + var data = new + { + items = new[] + { + new + { + users = new[] + { + new { id = 1, name = "Ada" }, + new { id = 2, name = "Bob" } + }, + status = "active", + count = 2 + } + } + }; + + var sw = Stopwatch.StartNew(); + for (int i = 0; i < 10000; i++) + { + _ = ToonEncoder.Encode(data); + } + sw.Stop(); + + _output.WriteLine($"Section 10 encoding: {sw.ElapsedMilliseconds}ms for 10,000 iterations"); + _output.WriteLine($"Average: {sw.Elapsed.TotalMicroseconds / 10000:F2}μs per encode"); + + // Baseline: should complete in reasonable time + Assert.True(sw.ElapsedMilliseconds < 5000, $"Section 10 encoding took {sw.ElapsedMilliseconds}ms, expected < 5000ms"); + } + + [Fact] + public void BenchmarkRoundTrip() + { + var data = new + { + users = new[] + { + new { id = 1, name = "Alice" }, + new { id = 2, name = "Bob" } + }, + metadata = new { version = 1, timestamp = "2025-11-27" } + }; + + var sw = Stopwatch.StartNew(); + for (int i = 0; i < 5000; i++) + { + var encoded = ToonEncoder.Encode(data); + _ = ToonDecoder.Decode(encoded); + } + sw.Stop(); + + _output.WriteLine($"Round-trip: {sw.ElapsedMilliseconds}ms for 5,000 iterations"); + _output.WriteLine($"Average: {sw.Elapsed.TotalMicroseconds / 5000:F2}μs per round-trip"); + + // Baseline: should complete in reasonable time + Assert.True(sw.ElapsedMilliseconds < 5000, $"Round-trip took {sw.ElapsedMilliseconds}ms, expected < 5000ms"); + } +} diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/ToonDecoderTests.cs b/tests/ToonFormat.SpecGenerator/ManualTests/ToonDecoderTests.cs new file mode 100644 index 0000000..d2091af --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/ManualTests/ToonDecoderTests.cs @@ -0,0 +1,170 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; + +namespace ToonFormat.Tests; + +/// +/// Tests for decoding TOON format strings. +/// +public class ToonDecoderTests +{ + [Fact] + public void Decode_SimpleObject_ReturnsValidJson() + { + // Arrange + var toonString = "name: Alice\nage: 30"; + + // Act + var result = ToonDecoder.Decode(toonString); + + // Assert + Assert.NotNull(result); + var obj = result.AsObject(); + Assert.Equal("Alice", obj["name"]?.GetValue()); + Assert.Equal(30.0, obj["age"]?.GetValue()); + } + + [Fact] + 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); + } + + [Fact] + public void Decode_PrimitiveArray_ReturnsValidArray() + { + // Arrange + var toonString = "numbers[5]: 1, 2, 3, 4, 5"; + + // Act + var result = ToonDecoder.Decode(toonString); + + // Assert + Assert.NotNull(result); + var obj = result.AsObject(); + var numbers = obj["numbers"]?.AsArray(); + Assert.NotNull(numbers); + Assert.Equal(5, numbers.Count); + Assert.Equal(1.0, numbers[0]?.GetValue()); + Assert.Equal(5.0, numbers[4]?.GetValue()); + } + + [Fact] + public void Decode_TabularArray_ReturnsValidStructure() + { + // Arrange - using list array format instead + var toonString = @"employees[3]: + - id: 1 + name: Alice + salary: 50000 + - id: 2 + name: Bob + salary: 60000 + - id: 3 + name: Charlie + salary: 55000"; + + // Act + var result = ToonDecoder.Decode(toonString); + + // Assert + Assert.NotNull(result); + var obj = result.AsObject(); + var employees = obj["employees"]?.AsArray(); + Assert.NotNull(employees); + Assert.Equal(3, employees.Count); + Assert.Equal(1.0, employees[0]?["id"]?.GetValue()); + Assert.Equal("Alice", employees[0]?["name"]?.GetValue()); + } + + [Fact] + public void Decode_NestedObject_ReturnsValidStructure() + { + // Arrange + var toonString = @"user: + name: Alice + address: + city: New York + zip: 10001"; + + // Act + var result = ToonDecoder.Decode(toonString); + + // Assert + Assert.NotNull(result); + var user = result["user"]?.AsObject(); + Assert.NotNull(user); + Assert.Equal("Alice", user["name"]?.GetValue()); + var address = user["address"]?.AsObject(); + Assert.NotNull(address); + Assert.Equal("New York", address["city"]?.GetValue()); + } + + [Fact] + public void Decode_WithStrictOption_ValidatesArrayLength() + { + // Arrange - array declares 5 items but only provides 3 + var toonString = "numbers[5]: 1, 2, 3"; + var options = new ToonDecodeOptions { Strict = true }; + + // Act & Assert + Assert.Throws(() => ToonDecoder.Decode(toonString, options)); + } + + [Fact] + public void Decode_WithNonStrictOption_AllowsLengthMismatch() + { + // Arrange - array declares 5 items but only provides 3 + var toonString = "numbers[5]: 1, 2, 3"; + var options = new ToonDecodeOptions { Strict = false }; + + // Act + var result = ToonDecoder.Decode(toonString, options); + + // Assert + Assert.NotNull(result); + var obj = result.AsObject(); + var numbers = obj["numbers"]?.AsArray(); + Assert.NotNull(numbers); + Assert.Equal(3, numbers.Count); + } + + [Fact] + public void Decode_InvalidFormat_ThrowsToonFormatException() + { + // Arrange - array length mismatch with strict mode + var invalidToon = "items[10]: 1, 2, 3"; + var options = new ToonDecodeOptions { Strict = true }; + + // Act & Assert + Assert.Throws(() => ToonDecoder.Decode(invalidToon, options)); + } + + [Fact] + public void Decode_EmptyString_ReturnsEmptyObject() + { + // Arrange + var emptyString = ""; + + // Act + var result = ToonDecoder.Decode(emptyString); + + // Assert - empty string returns empty array + Assert.NotNull(result); + } +} diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs b/tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs new file mode 100644 index 0000000..8cd92be --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs @@ -0,0 +1,154 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; + +namespace ToonFormat.Tests; + +/// +/// Tests for encoding data to TOON format. +/// +public class ToonEncoderTests +{ + [Fact] + public void Encode_SimpleObject_ReturnsValidToon() + { + // Arrange + var data = new { name = "Alice", age = 30 }; + + // Act + var result = ToonEncoder.Encode(data); + + // Assert + Assert.NotNull(result); + Assert.Contains("name:", result); + Assert.Contains("age:", result); + } + + [Fact] + 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); + } + + [Fact] + public void Encode_Array_ReturnsValidToon() + { + // Arrange + var data = new { numbers = new[] { 1, 2, 3, 4, 5 } }; + + // Act + var result = ToonEncoder.Encode(data); + + // Assert + Assert.NotNull(result); + Assert.Contains("numbers[", result); + } + + [Fact] + public void Encode_TabularArray_ReturnsValidToon() + { + // Arrange + var employees = new[] + { + new { id = 1, name = "Alice", salary = 50000 }, + new { id = 2, name = "Bob", salary = 60000 }, + new { id = 3, name = "Charlie", salary = 55000 } + }; + var data = new { employees }; + + // Act + var result = ToonEncoder.Encode(data); + + // Assert + Assert.NotNull(result); + Assert.Contains("employees[", result); + Assert.Contains("id", result); + Assert.Contains("name", result); + Assert.Contains("salary", result); + } + + [Fact] + public void Encode_WithCustomIndent_UsesCorrectIndentation() + { + // Arrange + var data = new { outer = new { inner = "value" } }; + var options = new ToonEncodeOptions { Indent = 4 }; + + // Act + var result = ToonEncoder.Encode(data, options); + + // Assert + Assert.NotNull(result); + Assert.Contains("outer:", result); + } + + [Fact] + public void Encode_WithCustomDelimiter_UsesCorrectDelimiter() + { + // Arrange + var data = new { numbers = new[] { 1, 2, 3 } }; + var options = new ToonEncodeOptions { Delimiter = ToonDelimiter.TAB }; + + // Act + var result = ToonEncoder.Encode(data, options); + + // Assert + Assert.NotNull(result); + Assert.Contains("numbers[", result); + } + + [Fact] + public void Encode_WithLengthMarker_IncludesHashSymbol() + { + // Arrange + var data = new { items = new[] { 1, 2, 3 } }; + var options = new ToonEncodeOptions { LengthMarker = true }; + + // Act + var result = ToonEncoder.Encode(data, options); + + // Assert + Assert.NotNull(result); + Assert.Contains("[#", result); + } + + [Fact] + public void Encode_NestedStructures_ReturnsValidToon() + { + // Arrange + var data = new + { + user = new + { + name = "Alice", + address = new + { + city = "New York", + zip = "10001" + } + } + }; + + // Act + var result = ToonEncoder.Encode(data); + + // Assert + Assert.NotNull(result); + Assert.Contains("user:", result); + Assert.Contains("address:", result); + } +} diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/ToonRoundTripTests.cs b/tests/ToonFormat.SpecGenerator/ManualTests/ToonRoundTripTests.cs new file mode 100644 index 0000000..251ee95 --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/ManualTests/ToonRoundTripTests.cs @@ -0,0 +1,84 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; + +namespace ToonFormat.Tests; + +/// +/// Round-trip tests to verify encoding and decoding preserve data integrity. +/// +public class ToonRoundTripTests +{ + [Fact] + public void RoundTrip_SimpleObject_PreservesData() + { + // Arrange + var original = new { name = "Alice", age = 30, active = true }; + + // Act + var encoded = ToonEncoder.Encode(original); + var decoded = ToonDecoder.Decode(encoded); + + // Assert + Assert.NotNull(decoded); + var obj = decoded.AsObject(); + Assert.Equal("Alice", obj["name"]?.GetValue()); + Assert.Equal(30.0, obj["age"]?.GetValue()); + Assert.True(obj["active"]?.GetValue()); + } + + [Fact] + public void RoundTrip_Array_PreservesData() + { + // Arrange + var original = new { numbers = new[] { 1, 2, 3, 4, 5 } }; + + // Act + var encoded = ToonEncoder.Encode(original); + var decoded = ToonDecoder.Decode(encoded); + + // Assert + Assert.NotNull(decoded); + var numbers = decoded["numbers"]?.AsArray(); + Assert.NotNull(numbers); + Assert.Equal(5, numbers.Count); + for (int i = 0; i < 5; i++) + { + Assert.Equal((double)(i + 1), numbers[i]?.GetValue()); + } + } + + [Fact] + public void RoundTrip_ComplexStructure_PreservesData() + { + // Arrange + var original = new + { + users = new[] + { + new { id = 1, name = "Alice", email = "alice@example.com" }, + new { id = 2, name = "Bob", email = "bob@example.com" } + }, + metadata = new + { + total = 2, + timestamp = "2025-01-01T00:00:00Z" + } + }; + + // Act + var encoded = ToonEncoder.Encode(original); + var decoded = ToonDecoder.Decode(encoded); + + // Assert + Assert.NotNull(decoded); + var users = decoded["users"]?.AsArray(); + Assert.NotNull(users); + Assert.Equal(2, users.Count); + Assert.Equal("Alice", users[0]?["name"]?.GetValue()); + + var metadata = decoded["metadata"]?.AsObject(); + Assert.NotNull(metadata); + Assert.Equal(2.0, metadata["total"]?.GetValue()); + } +} diff --git a/tests/ToonFormat.SpecGenerator/SpecGenerator.cs b/tests/ToonFormat.SpecGenerator/SpecGenerator.cs index eba64d0..c647720 100644 --- a/tests/ToonFormat.SpecGenerator/SpecGenerator.cs +++ b/tests/ToonFormat.SpecGenerator/SpecGenerator.cs @@ -16,6 +16,12 @@ public void GenerateSpecs(SpecGeneratorOptions options) try { + // Clean up test directory before generating new files + CleanTestDirectory(options.AbsoluteOutputPath); + + // Copy manual tests from ManualTests folder + CopyManualTests(options.AbsoluteOutputPath); + logger.LogDebug("Cloning repository {RepoUrl} to {CloneDirectory}", options.SpecRepoUrl, toonSpecDir); GitTool.CloneRepository(options.SpecRepoUrl, toonSpecDir, @@ -52,6 +58,104 @@ public void GenerateSpecs(SpecGeneratorOptions options) logger.LogInformation("Spec generation completed."); } + private void CleanTestDirectory(string testDirectory) + { + if (!Directory.Exists(testDirectory)) + { + logger.LogDebug("Test directory {TestDirectory} does not exist, skipping cleanup", testDirectory); + return; + } + + logger.LogInformation("Cleaning test directory {TestDirectory}", testDirectory); + + // Delete all subdirectories + foreach (var dir in Directory.GetDirectories(testDirectory)) + { + try + { + Directory.Delete(dir, true); + logger.LogDebug("Deleted directory {Directory}", dir); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to delete directory {Directory}", dir); + } + } + + // Delete all files except .csproj files + foreach (var file in Directory.GetFiles(testDirectory)) + { + if (Path.GetExtension(file).Equals(".csproj", StringComparison.OrdinalIgnoreCase)) + { + logger.LogDebug("Preserving project file {File}", file); + continue; + } + + try + { + File.Delete(file); + logger.LogDebug("Deleted file {File}", file); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to delete file {File}", file); + } + } + + logger.LogInformation("Test directory cleanup completed"); + } + + private void CopyManualTests(string testDirectory) + { + // Get the directory where SpecGenerator assembly is located + var assemblyLocation = AppContext.BaseDirectory; + var manualTestsDir = Path.Combine(assemblyLocation, "ManualTests"); + + if (!Directory.Exists(manualTestsDir)) + { + logger.LogDebug("ManualTests directory not found at {ManualTestsDir}, skipping manual test copy", manualTestsDir); + return; + } + + logger.LogInformation("Copying manual tests from {ManualTestsDir}", manualTestsDir); + + // Copy all files and subdirectories from ManualTests to test directory + CopyDirectory(manualTestsDir, testDirectory); + + logger.LogInformation("Manual tests copied successfully"); + } + + private void CopyDirectory(string sourceDir, string destDir) + { + // Create destination directory if it doesn't exist + Directory.CreateDirectory(destDir); + + // Copy all files + foreach (var file in Directory.GetFiles(sourceDir)) + { + var fileName = Path.GetFileName(file); + var destFile = Path.Combine(destDir, fileName); + + try + { + File.Copy(file, destFile, overwrite: true); + logger.LogDebug("Copied file {SourceFile} to {DestFile}", file, destFile); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to copy file {SourceFile} to {DestFile}", file, destFile); + } + } + + // Copy all subdirectories recursively + foreach (var subDir in Directory.GetDirectories(sourceDir)) + { + var subDirName = Path.GetFileName(subDir); + var destSubDir = Path.Combine(destDir, subDirName); + CopyDirectory(subDir, destSubDir); + } + } + private void GenerateEncodeFixtures(string specDir, string outputDir, IEnumerable ignores) { var encodeFixtures = LoadEncodeFixtures(specDir); diff --git a/tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj b/tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj index 123b6ae..a44c278 100644 --- a/tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj +++ b/tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj @@ -13,4 +13,9 @@ + + + + + diff --git a/tests/ToonFormat.Tests/KeyFoldingTests.cs b/tests/ToonFormat.Tests/KeyFoldingTests.cs index 8fcdd3b..5c637e4 100644 --- a/tests/ToonFormat.Tests/KeyFoldingTests.cs +++ b/tests/ToonFormat.Tests/KeyFoldingTests.cs @@ -1,4 +1,4 @@ -using Toon.Format; +using Toon.Format; namespace ToonFormat.Tests; From 1d4f35a97ceec5f5d0b91724da867a83473b5232 Mon Sep 17 00:00:00 2001 From: Carlos Felipe Date: Fri, 28 Nov 2025 14:52:00 -0500 Subject: [PATCH 05/20] Revert "Fixes" This reverts commit f3c8e110d3b158d32196554bf0461e852a2ddb76. --- specgen.sh | 2 +- src/ToonFormat/Constants.cs | 2 +- src/ToonFormat/Internal/Encode/LineWriter.cs | 2 +- .../ToonFormat.SpecGenerator/FixtureWriter.cs | 35 +- .../ManualTests/Encode/ArraysObjectsManual.cs | 137 ----- .../ManualTests/JsonComplexRoundTripTests.cs | 255 --------- .../ManualTests/KeyFoldingTests.cs | 501 ------------------ .../ManualTests/PerformanceBenchmark.cs | 131 ----- .../ManualTests/ToonDecoderTests.cs | 170 ------ .../ManualTests/ToonEncoderTests.cs | 154 ------ .../ManualTests/ToonRoundTripTests.cs | 84 --- .../ToonFormat.SpecGenerator/SpecGenerator.cs | 104 ---- .../ToonFormat.SpecGenerator.csproj | 5 - tests/ToonFormat.Tests/KeyFoldingTests.cs | 2 +- 14 files changed, 8 insertions(+), 1576 deletions(-) delete mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/Encode/ArraysObjectsManual.cs delete mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/JsonComplexRoundTripTests.cs delete mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/KeyFoldingTests.cs delete mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/PerformanceBenchmark.cs delete mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/ToonDecoderTests.cs delete mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs delete mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/ToonRoundTripTests.cs diff --git a/specgen.sh b/specgen.sh index 8d43e8c..ce45262 100755 --- a/specgen.sh +++ b/specgen.sh @@ -3,6 +3,6 @@ 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 build tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj dotnet run --project tests/ToonFormat.SpecGenerator -- --url="$GH_REPO" --output="$OUT_DIR" --branch="v3.0.0" --loglevel="Information" \ No newline at end of file diff --git a/src/ToonFormat/Constants.cs b/src/ToonFormat/Constants.cs index c03ea16..a92b515 100644 --- a/src/ToonFormat/Constants.cs +++ b/src/ToonFormat/Constants.cs @@ -132,7 +132,7 @@ public enum ToonPathExpansion { /// Path expansion disabled Off, - + /// Keys containing dots are expanded into nested structures Safe } diff --git a/src/ToonFormat/Internal/Encode/LineWriter.cs b/src/ToonFormat/Internal/Encode/LineWriter.cs index fb3409f..3e72f35 100644 --- a/src/ToonFormat/Internal/Encode/LineWriter.cs +++ b/src/ToonFormat/Internal/Encode/LineWriter.cs @@ -48,7 +48,7 @@ public void PushListItem(int depth, string content) /// public override string ToString() { - return string.Join("\n", _lines); + return string.Join(Environment.NewLine, _lines); } /// diff --git a/tests/ToonFormat.SpecGenerator/FixtureWriter.cs b/tests/ToonFormat.SpecGenerator/FixtureWriter.cs index b8aaacb..a9068ec 100644 --- a/tests/ToonFormat.SpecGenerator/FixtureWriter.cs +++ b/tests/ToonFormat.SpecGenerator/FixtureWriter.cs @@ -21,7 +21,6 @@ public void WriteFile() Directory.CreateDirectory(OutputDir); using var writer = new StreamWriter(outputPath, false); - writer.NewLine = "\n"; // Use Unix line endings for cross-platform compatibility WriteHeader(writer); WriteLine(writer); @@ -93,7 +92,7 @@ private void WriteTestMethod(StreamWriter writer, TTestCase testCase) WriteLineIndented(writer, "var expected ="); WriteLine(writer, "\"\"\""); - Write(writer, NormalizeLineEndings(encodeTestCase.Expected)); + Write(writer, encodeTestCase.Expected); WriteLine(writer); WriteLine(writer, "\"\"\";"); @@ -103,7 +102,7 @@ private void WriteTestMethod(StreamWriter writer, TTestCase testCase) WriteLineIndented(writer, "var input ="); WriteLine(writer, "\"\"\""); - Write(writer, NormalizeLineEndings(decodeTestCase.Input)); + Write(writer, decodeTestCase.Input); WriteLine(writer); WriteLine(writer, "\"\"\";"); @@ -166,10 +165,6 @@ private void WriteTestMethod(StreamWriter writer, TTestCase testCase) WriteLineIndented(writer, $"Indent = {decodeTestCase.Options?.Indent ?? 2},"); WriteLineIndented(writer, $"Strict = {(decodeTestCase.Options?.Strict ?? true).ToString().ToLower()},"); - if (decodeTestCase.Options?.ExpandPaths != null) - WriteLineIndented(writer, $"ExpandPaths = {GetToonPathExpansionEnumFromString(decodeTestCase.Options.ExpandPaths)}"); - - Unindent(); WriteLineIndented(writer, "};"); @@ -178,17 +173,13 @@ private void WriteTestMethod(StreamWriter writer, TTestCase testCase) if (decodeTestCase.ShouldError) { - // Determine which exception type to expect based on the options - var isPathExpansionError = decodeTestCase.Options?.ExpandPaths != null; - var exceptionType = isPathExpansionError ? "ToonPathExpansionException" : "ToonFormatException"; - if (hasDecodeOptions) { - WriteLineIndented(writer, $"Assert.Throws<{exceptionType}>(() => ToonDecoder.Decode(input, options));"); + WriteLineIndented(writer, $"Assert.Throws(() => ToonDecoder.Decode(input, options));"); } else { - WriteLineIndented(writer, $"Assert.Throws<{exceptionType}>(() => ToonDecoder.Decode(input));"); + WriteLineIndented(writer, $"Assert.Throws(() => ToonDecoder.Decode(input));"); } } else @@ -281,16 +272,6 @@ private static string GetToonKeyFoldingEnumFromString(string? keyFoldingOption) }; } - private static string GetToonPathExpansionEnumFromString(string? expandPathsOption) - { - return expandPathsOption switch - { - "off" => "ToonPathExpansion.Off", - "safe" => "ToonPathExpansion.Safe", - _ => "ToonPathExpansion.Safe" - }; - } - private void WriteJsonNodeAsAnonymousType(StreamWriter writer, JsonNode node) { WriteJsonNode(writer, node); @@ -489,12 +470,4 @@ private void Write(StreamWriter writer, string contents) { writer.Write(contents); } - - /// - /// Normalizes line endings to Unix format (LF) for cross-platform compatibility. - /// - private static string NormalizeLineEndings(string text) - { - return text.Replace("\r\n", "\n"); - } } diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/Encode/ArraysObjectsManual.cs b/tests/ToonFormat.SpecGenerator/ManualTests/Encode/ArraysObjectsManual.cs deleted file mode 100644 index 929aad7..0000000 --- a/tests/ToonFormat.SpecGenerator/ManualTests/Encode/ArraysObjectsManual.cs +++ /dev/null @@ -1,137 +0,0 @@ -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 ArraysObjectsManual -{ - [Fact] - [Trait("Description", "tabular array with multiple sibling fields")] - public void TabularArrayWithMultipleSiblingFields() - { - // Arrange - var input = - new - { - @items = new object[] { - new - { - @users = new object[] { - new { @id = 1, @name = "Ada" }, - new { @id = 2, @name = "Bob" }, - }, - @status = "active", - @count = 2, - @tags = new object[] { "a", "b", "c" } - } - } - }; - - var expected = -""" -items[1]: - - users[2]{id,name}: - 1,Ada - 2,Bob - status: active - count: 2 - tags[3]: a,b,c -"""; - - // Act & Assert - var result = ToonEncoder.Encode(input); - - Assert.Equal(expected, result); - } - - [Fact] - [Trait("Description", "multiple list items with tabular first fields")] - public void MultipleListItemsWithTabularFirstFields() - { - // Arrange - var input = - new - { - @items = new object[] { - new - { - @users = new object[] { - new { @id = 1, @name = "Ada" }, - new { @id = 2, @name = "Bob" }, - }, - @status = "active" - }, - new - { - @users = new object[] { - new { @id = 3, @name = "Charlie" } - }, - @status = "inactive" - } - } - }; - - var expected = -""" -items[2]: - - users[2]{id,name}: - 1,Ada - 2,Bob - status: active - - users[1]{id,name}: - 3,Charlie - status: inactive -"""; - - // Act & Assert - var result = ToonEncoder.Encode(input); - - Assert.Equal(expected, result); - } - - [Fact] - [Trait("Description", "deeply nested list items with tabular first field")] - public void DeeplyNestedListItemsWithTabularFirstField() - { - // Arrange - var input = - new - { - @data = new object[] { - new - { - @items = new object[] { - new - { - @users = new object[] { - new { @id = 1, @name = "Ada" }, - new { @id = 2, @name = "Bob" }, - }, - @status = "active" - } - } - } - } - }; - - var expected = -""" -data[1]: - - items[1]: - - users[2]{id,name}: - 1,Ada - 2,Bob - status: active -"""; - - // Act & Assert - var result = ToonEncoder.Encode(input); - - Assert.Equal(expected, result); - } -} diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/JsonComplexRoundTripTests.cs b/tests/ToonFormat.SpecGenerator/ManualTests/JsonComplexRoundTripTests.cs deleted file mode 100644 index 9080e53..0000000 --- a/tests/ToonFormat.SpecGenerator/ManualTests/JsonComplexRoundTripTests.cs +++ /dev/null @@ -1,255 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using System.Text.Json.Nodes; -using Toon.Format; -using Xunit.Abstractions; - -namespace ToonFormat.Tests; - -/// -/// Tests for complex multi-level JSON structures to validate TOON format encoding and decoding. -/// -public class JsonComplexRoundTripTests -{ - private readonly ITestOutputHelper _output; - - public JsonComplexRoundTripTests(ITestOutputHelper output) - { - _output = output; - } - private const string ComplexJson = @"{ - ""project"": { - ""id"": ""PX-4921"", - ""name"": ""Customer Insights Expansion"", - ""description"": ""This is a long descriptive text containing more than fifteen words to simulate a realistic business scenario for testing purposes."", - ""createdAt"": ""2025-11-20T10:32:00Z"", - ""metadata"": { - ""owner"": ""john.doe@example.com"", - ""website"": ""https://example.org/products/insights?ref=test&lang=en"", - ""tags"": [""analysis"", ""insights"", ""growth"", ""R&D""], - ""cost"": { - ""currency"": ""USD"", - ""amount"": 12500.75 - } - }, - ""phases"": [ - { - ""phaseId"": 1, - ""title"": ""Discovery & Research"", - ""deadline"": ""2025-12-15"", - ""status"": ""In Progress"", - ""details"": { - ""notes"": ""Team is conducting interviews, market analysis, and reviewing historical performance metrics & competitors."", - ""specialChars"": ""!@#$%^&*()_+=-{}[]|:;<>,.?/"" - } - }, - { - ""phaseId"": 2, - ""title"": ""Development"", - ""deadline"": ""2026-01-30"", - ""budget"": { - ""currency"": ""EUR"", - ""amount"": 7800.00 - }, - ""resources"": { - ""leadDeveloper"": ""alice.smith@example.com"", - ""repository"": ""https://github.com/example/repo"" - } - } - ] - } -}"; - - [Fact] - public void ComplexJson_RoundTrip_ShouldPreserveKeyFields() - { - // Arrange - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - var root = JsonSerializer.Deserialize(ComplexJson, options); - - // Sanity - Assert.NotNull(root); - Assert.NotNull(root.project); - - // Act - encode to TOON and decode back - var toonText = ToonEncoder.Encode(root); - Assert.NotNull(toonText); - _output.WriteLine("TOON Encoded Output:"); - _output.WriteLine(toonText); - _output.WriteLine("---"); - - var decoded = ToonDecoder.Decode(toonText); - Assert.NotNull(decoded); - - // The encoder reflects C# property names (we used lowercase names to match original JSON keys) - var project = decoded["project"]?.AsObject(); - Assert.NotNull(project); - - // Assert key scalar values - Assert.Equal("PX-4921", project["id"]?.GetValue()); - Assert.Equal("Customer Insights Expansion", project["name"]?.GetValue()); - - // Metadata checks - var metadata = project["metadata"]?.AsObject(); - Assert.NotNull(metadata); - Assert.Equal("john.doe@example.com", metadata["owner"]?.GetValue()); - Assert.Equal("https://example.org/products/insights?ref=test&lang=en", metadata["website"]?.GetValue()); - - // Tags array validation - var tags = metadata["tags"]?.AsArray(); - Assert.NotNull(tags); - Assert.Equal(4, tags.Count); - Assert.Equal("analysis", tags[0]?.GetValue()); - Assert.Equal("insights", tags[1]?.GetValue()); - Assert.Equal("growth", tags[2]?.GetValue()); - Assert.Equal("R&D", tags[3]?.GetValue()); - - var cost = metadata["cost"]?.AsObject(); - Assert.NotNull(cost); - Assert.Equal("USD", cost["currency"]?.GetValue()); - Assert.Equal(12500.75, cost["amount"]?.GetValue()); - - // Phases checks - var phases = project["phases"]?.AsArray(); - Assert.NotNull(phases); - Assert.Equal(2, phases.Count); - - var phase1 = phases[0]?.AsObject(); - Assert.NotNull(phase1); - Assert.Equal(1.0, phase1["phaseId"]?.GetValue()); - var details = phase1["details"]?.AsObject(); - Assert.NotNull(details); - Assert.Contains("market analysis", details["notes"]?.GetValue() ?? string.Empty); - Assert.Equal("!@#$%^&*()_+=-{}[]|:;<>,.?/", details["specialChars"]?.GetValue()); - - var phase2 = phases[1]?.AsObject(); - Assert.NotNull(phase2); - Assert.Equal(2.0, phase2["phaseId"]?.GetValue()); - Assert.Equal("Development", phase2["title"]?.GetValue()); - Assert.Equal("2026-01-30", phase2["deadline"]?.GetValue()); - - var budget = phase2["budget"]?.AsObject(); - Assert.NotNull(budget); - Assert.Equal("EUR", budget["currency"]?.GetValue()); - Assert.Equal(7800.0, budget["amount"]?.GetValue()); - - var resources = phase2["resources"]?.AsObject(); - Assert.NotNull(resources); - Assert.Equal("alice.smith@example.com", resources["leadDeveloper"]?.GetValue()); - Assert.Equal("https://github.com/example/repo", resources["repository"]?.GetValue()); - } - - [Fact] - public void ComplexJson_Encode_ShouldProduceValidToonFormat() - { - // Arrange - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - var root = JsonSerializer.Deserialize(ComplexJson, options); - Assert.NotNull(root); - - // Act - var toonText = ToonEncoder.Encode(root); - - // Assert - verify TOON format structure - Assert.NotNull(toonText); - Assert.Contains("project:", toonText); - Assert.Contains("id:", toonText); - Assert.Contains("PX-4921", toonText); - Assert.Contains("metadata:", toonText); - Assert.Contains("phases[2]", toonText); - - _output.WriteLine("TOON Output:"); - _output.WriteLine(toonText); - } - - [Fact] - public void ComplexJson_SpecialCharacters_ShouldBePreserved() - { - // Arrange - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - var root = JsonSerializer.Deserialize(ComplexJson, options); - Assert.NotNull(root); - - // Act - var toonText = ToonEncoder.Encode(root); - var decoded = ToonDecoder.Decode(toonText); - - // Assert - verify special characters in details.specialChars - Assert.NotNull(decoded); - var project = decoded["project"]?.AsObject(); - var phases = project?["phases"]?.AsArray(); - var phase1 = phases?[0]?.AsObject(); - var details = phase1?["details"]?.AsObject(); - - Assert.NotNull(details); - var specialChars = details["specialChars"]?.GetValue(); - Assert.Equal("!@#$%^&*()_+=-{}[]|:;<>,.?/", specialChars); - } - - [Fact] - public void ComplexJson_DateTime_ShouldBePreservedAsString() - { - // Arrange - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - var root = JsonSerializer.Deserialize(ComplexJson, options); - Assert.NotNull(root); - - // Act - var toonText = ToonEncoder.Encode(root); - var decoded = ToonDecoder.Decode(toonText); - - // Assert - verify DateTime is preserved as full ISO 8601 UTC timestamp - Assert.NotNull(decoded); - var project = decoded["project"]?.AsObject(); - Assert.NotNull(project); - - var createdAt = project["createdAt"]?.GetValue(); - Assert.NotNull(createdAt); - // Validate full ISO 8601 UTC timestamp (with or without fractional seconds) - // .NET DateTime.ToString("O") produces: 2025-11-20T10:32:00.0000000Z - Assert.StartsWith("2025-11-20T10:32:00", createdAt); - Assert.EndsWith("Z", createdAt); - } - - // POCOs with lowercase property names to preserve original JSON keys when encoding via reflection - public class Root { public Project project { get; set; } = null!; } - - public class Project - { - public string id { get; set; } = null!; - public string name { get; set; } = null!; - public string description { get; set; } = null!; - public DateTime createdAt { get; set; } - public Metadata metadata { get; set; } = null!; - public List phases { get; set; } = new(); - } - - public class Metadata - { - public string owner { get; set; } = null!; - public string website { get; set; } = null!; - public List tags { get; set; } = new(); - public Cost cost { get; set; } = null!; - } - - public class Cost { public string currency { get; set; } = null!; public double amount { get; set; } } - - public class Phase - { - public int phaseId { get; set; } - public string title { get; set; } = null!; - public string deadline { get; set; } = null!; - public string? status { get; set; } - public Details? details { get; set; } - public Budget? budget { get; set; } - public Resources? resources { get; set; } - } - - public class Details { public string notes { get; set; } = null!; public string specialChars { get; set; } = null!; } - - public class Budget { public string currency { get; set; } = null!; public double amount { get; set; } } - - public class Resources { public string leadDeveloper { get; set; } = null!; public string repository { get; set; } = null!; } -} diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/KeyFoldingTests.cs b/tests/ToonFormat.SpecGenerator/ManualTests/KeyFoldingTests.cs deleted file mode 100644 index 5c637e4..0000000 --- a/tests/ToonFormat.SpecGenerator/ManualTests/KeyFoldingTests.cs +++ /dev/null @@ -1,501 +0,0 @@ -using Toon.Format; - -namespace ToonFormat.Tests; - -// TODO: Remove these tests once generated spec tests are in source control -// used to validate current key folding functionality aligns with spec -public class KeyFoldingTests -{ - [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, - KeyFolding = ToonKeyFolding.Safe - }; - - 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, - KeyFolding = ToonKeyFolding.Safe - }; - - 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, - KeyFolding = ToonKeyFolding.Safe - }; - - 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, - KeyFolding = ToonKeyFolding.Safe, - FlattenDepth = 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, - KeyFolding = ToonKeyFolding.Safe - }; - - 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, - KeyFolding = ToonKeyFolding.Safe, - FlattenDepth = 0, - }; - - 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, - KeyFolding = ToonKeyFolding.Safe, - FlattenDepth = 1, - }; - - 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, - KeyFolding = ToonKeyFolding.Off - }; - - 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, - KeyFolding = ToonKeyFolding.Safe - }; - - 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, - KeyFolding = ToonKeyFolding.Safe - }; - - 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, - KeyFolding = ToonKeyFolding.Safe - }; - - var result = ToonEncoder.Encode(input, options); - - Assert.Equal(expected, result); - } -} diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/PerformanceBenchmark.cs b/tests/ToonFormat.SpecGenerator/ManualTests/PerformanceBenchmark.cs deleted file mode 100644 index b27cef8..0000000 --- a/tests/ToonFormat.SpecGenerator/ManualTests/PerformanceBenchmark.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System; -using System.Diagnostics; -using System.Text.Json.Nodes; -using Toon.Format; -using Xunit; -using Xunit.Abstractions; - -namespace ToonFormat.Tests; - -public class PerformanceBenchmark -{ - private readonly ITestOutputHelper _output; - - public PerformanceBenchmark(ITestOutputHelper output) - { - _output = output; - } - - [Fact] - public void BenchmarkSimpleEncoding() - { - var data = new - { - users = new[] - { - new { id = 1, name = "Alice", role = "admin" }, - new { id = 2, name = "Bob", role = "user" }, - new { id = 3, name = "Charlie", role = "user" } - } - }; - - var sw = Stopwatch.StartNew(); - for (int i = 0; i < 10000; i++) - { - _ = ToonEncoder.Encode(data); - } - sw.Stop(); - - _output.WriteLine($"Simple encoding: {sw.ElapsedMilliseconds}ms for 10,000 iterations"); - _output.WriteLine($"Average: {sw.Elapsed.TotalMicroseconds / 10000:F2}μs per encode"); - - // Baseline: should complete in reasonable time (< 5 seconds for 10k iterations) - Assert.True(sw.ElapsedMilliseconds < 5000, $"Encoding took {sw.ElapsedMilliseconds}ms, expected < 5000ms"); - } - - [Fact] - public void BenchmarkSimpleDecoding() - { - var toonString = """ -users[3]{id,name,role}: - 1,Alice,admin - 2,Bob,user - 3,Charlie,user -"""; - - var sw = Stopwatch.StartNew(); - for (int i = 0; i < 10000; i++) - { - _ = ToonDecoder.Decode(toonString); - } - sw.Stop(); - - _output.WriteLine($"Simple decoding: {sw.ElapsedMilliseconds}ms for 10,000 iterations"); - _output.WriteLine($"Average: {sw.Elapsed.TotalMicroseconds / 10000:F2}μs per decode"); - - // Baseline: should complete in reasonable time (< 5 seconds for 10k iterations) - Assert.True(sw.ElapsedMilliseconds < 5000, $"Decoding took {sw.ElapsedMilliseconds}ms, expected < 5000ms"); - } - - [Fact] - public void BenchmarkSection10Encoding() - { - var data = new - { - items = new[] - { - new - { - users = new[] - { - new { id = 1, name = "Ada" }, - new { id = 2, name = "Bob" } - }, - status = "active", - count = 2 - } - } - }; - - var sw = Stopwatch.StartNew(); - for (int i = 0; i < 10000; i++) - { - _ = ToonEncoder.Encode(data); - } - sw.Stop(); - - _output.WriteLine($"Section 10 encoding: {sw.ElapsedMilliseconds}ms for 10,000 iterations"); - _output.WriteLine($"Average: {sw.Elapsed.TotalMicroseconds / 10000:F2}μs per encode"); - - // Baseline: should complete in reasonable time - Assert.True(sw.ElapsedMilliseconds < 5000, $"Section 10 encoding took {sw.ElapsedMilliseconds}ms, expected < 5000ms"); - } - - [Fact] - public void BenchmarkRoundTrip() - { - var data = new - { - users = new[] - { - new { id = 1, name = "Alice" }, - new { id = 2, name = "Bob" } - }, - metadata = new { version = 1, timestamp = "2025-11-27" } - }; - - var sw = Stopwatch.StartNew(); - for (int i = 0; i < 5000; i++) - { - var encoded = ToonEncoder.Encode(data); - _ = ToonDecoder.Decode(encoded); - } - sw.Stop(); - - _output.WriteLine($"Round-trip: {sw.ElapsedMilliseconds}ms for 5,000 iterations"); - _output.WriteLine($"Average: {sw.Elapsed.TotalMicroseconds / 5000:F2}μs per round-trip"); - - // Baseline: should complete in reasonable time - Assert.True(sw.ElapsedMilliseconds < 5000, $"Round-trip took {sw.ElapsedMilliseconds}ms, expected < 5000ms"); - } -} diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/ToonDecoderTests.cs b/tests/ToonFormat.SpecGenerator/ManualTests/ToonDecoderTests.cs deleted file mode 100644 index d2091af..0000000 --- a/tests/ToonFormat.SpecGenerator/ManualTests/ToonDecoderTests.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Nodes; -using Toon.Format; - -namespace ToonFormat.Tests; - -/// -/// Tests for decoding TOON format strings. -/// -public class ToonDecoderTests -{ - [Fact] - public void Decode_SimpleObject_ReturnsValidJson() - { - // Arrange - var toonString = "name: Alice\nage: 30"; - - // Act - var result = ToonDecoder.Decode(toonString); - - // Assert - Assert.NotNull(result); - var obj = result.AsObject(); - Assert.Equal("Alice", obj["name"]?.GetValue()); - Assert.Equal(30.0, obj["age"]?.GetValue()); - } - - [Fact] - 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); - } - - [Fact] - public void Decode_PrimitiveArray_ReturnsValidArray() - { - // Arrange - var toonString = "numbers[5]: 1, 2, 3, 4, 5"; - - // Act - var result = ToonDecoder.Decode(toonString); - - // Assert - Assert.NotNull(result); - var obj = result.AsObject(); - var numbers = obj["numbers"]?.AsArray(); - Assert.NotNull(numbers); - Assert.Equal(5, numbers.Count); - Assert.Equal(1.0, numbers[0]?.GetValue()); - Assert.Equal(5.0, numbers[4]?.GetValue()); - } - - [Fact] - public void Decode_TabularArray_ReturnsValidStructure() - { - // Arrange - using list array format instead - var toonString = @"employees[3]: - - id: 1 - name: Alice - salary: 50000 - - id: 2 - name: Bob - salary: 60000 - - id: 3 - name: Charlie - salary: 55000"; - - // Act - var result = ToonDecoder.Decode(toonString); - - // Assert - Assert.NotNull(result); - var obj = result.AsObject(); - var employees = obj["employees"]?.AsArray(); - Assert.NotNull(employees); - Assert.Equal(3, employees.Count); - Assert.Equal(1.0, employees[0]?["id"]?.GetValue()); - Assert.Equal("Alice", employees[0]?["name"]?.GetValue()); - } - - [Fact] - public void Decode_NestedObject_ReturnsValidStructure() - { - // Arrange - var toonString = @"user: - name: Alice - address: - city: New York - zip: 10001"; - - // Act - var result = ToonDecoder.Decode(toonString); - - // Assert - Assert.NotNull(result); - var user = result["user"]?.AsObject(); - Assert.NotNull(user); - Assert.Equal("Alice", user["name"]?.GetValue()); - var address = user["address"]?.AsObject(); - Assert.NotNull(address); - Assert.Equal("New York", address["city"]?.GetValue()); - } - - [Fact] - public void Decode_WithStrictOption_ValidatesArrayLength() - { - // Arrange - array declares 5 items but only provides 3 - var toonString = "numbers[5]: 1, 2, 3"; - var options = new ToonDecodeOptions { Strict = true }; - - // Act & Assert - Assert.Throws(() => ToonDecoder.Decode(toonString, options)); - } - - [Fact] - public void Decode_WithNonStrictOption_AllowsLengthMismatch() - { - // Arrange - array declares 5 items but only provides 3 - var toonString = "numbers[5]: 1, 2, 3"; - var options = new ToonDecodeOptions { Strict = false }; - - // Act - var result = ToonDecoder.Decode(toonString, options); - - // Assert - Assert.NotNull(result); - var obj = result.AsObject(); - var numbers = obj["numbers"]?.AsArray(); - Assert.NotNull(numbers); - Assert.Equal(3, numbers.Count); - } - - [Fact] - public void Decode_InvalidFormat_ThrowsToonFormatException() - { - // Arrange - array length mismatch with strict mode - var invalidToon = "items[10]: 1, 2, 3"; - var options = new ToonDecodeOptions { Strict = true }; - - // Act & Assert - Assert.Throws(() => ToonDecoder.Decode(invalidToon, options)); - } - - [Fact] - public void Decode_EmptyString_ReturnsEmptyObject() - { - // Arrange - var emptyString = ""; - - // Act - var result = ToonDecoder.Decode(emptyString); - - // Assert - empty string returns empty array - Assert.NotNull(result); - } -} diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs b/tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs deleted file mode 100644 index 8cd92be..0000000 --- a/tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Nodes; -using Toon.Format; - -namespace ToonFormat.Tests; - -/// -/// Tests for encoding data to TOON format. -/// -public class ToonEncoderTests -{ - [Fact] - public void Encode_SimpleObject_ReturnsValidToon() - { - // Arrange - var data = new { name = "Alice", age = 30 }; - - // Act - var result = ToonEncoder.Encode(data); - - // Assert - Assert.NotNull(result); - Assert.Contains("name:", result); - Assert.Contains("age:", result); - } - - [Fact] - 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); - } - - [Fact] - public void Encode_Array_ReturnsValidToon() - { - // Arrange - var data = new { numbers = new[] { 1, 2, 3, 4, 5 } }; - - // Act - var result = ToonEncoder.Encode(data); - - // Assert - Assert.NotNull(result); - Assert.Contains("numbers[", result); - } - - [Fact] - public void Encode_TabularArray_ReturnsValidToon() - { - // Arrange - var employees = new[] - { - new { id = 1, name = "Alice", salary = 50000 }, - new { id = 2, name = "Bob", salary = 60000 }, - new { id = 3, name = "Charlie", salary = 55000 } - }; - var data = new { employees }; - - // Act - var result = ToonEncoder.Encode(data); - - // Assert - Assert.NotNull(result); - Assert.Contains("employees[", result); - Assert.Contains("id", result); - Assert.Contains("name", result); - Assert.Contains("salary", result); - } - - [Fact] - public void Encode_WithCustomIndent_UsesCorrectIndentation() - { - // Arrange - var data = new { outer = new { inner = "value" } }; - var options = new ToonEncodeOptions { Indent = 4 }; - - // Act - var result = ToonEncoder.Encode(data, options); - - // Assert - Assert.NotNull(result); - Assert.Contains("outer:", result); - } - - [Fact] - public void Encode_WithCustomDelimiter_UsesCorrectDelimiter() - { - // Arrange - var data = new { numbers = new[] { 1, 2, 3 } }; - var options = new ToonEncodeOptions { Delimiter = ToonDelimiter.TAB }; - - // Act - var result = ToonEncoder.Encode(data, options); - - // Assert - Assert.NotNull(result); - Assert.Contains("numbers[", result); - } - - [Fact] - public void Encode_WithLengthMarker_IncludesHashSymbol() - { - // Arrange - var data = new { items = new[] { 1, 2, 3 } }; - var options = new ToonEncodeOptions { LengthMarker = true }; - - // Act - var result = ToonEncoder.Encode(data, options); - - // Assert - Assert.NotNull(result); - Assert.Contains("[#", result); - } - - [Fact] - public void Encode_NestedStructures_ReturnsValidToon() - { - // Arrange - var data = new - { - user = new - { - name = "Alice", - address = new - { - city = "New York", - zip = "10001" - } - } - }; - - // Act - var result = ToonEncoder.Encode(data); - - // Assert - Assert.NotNull(result); - Assert.Contains("user:", result); - Assert.Contains("address:", result); - } -} diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/ToonRoundTripTests.cs b/tests/ToonFormat.SpecGenerator/ManualTests/ToonRoundTripTests.cs deleted file mode 100644 index 251ee95..0000000 --- a/tests/ToonFormat.SpecGenerator/ManualTests/ToonRoundTripTests.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Nodes; -using Toon.Format; - -namespace ToonFormat.Tests; - -/// -/// Round-trip tests to verify encoding and decoding preserve data integrity. -/// -public class ToonRoundTripTests -{ - [Fact] - public void RoundTrip_SimpleObject_PreservesData() - { - // Arrange - var original = new { name = "Alice", age = 30, active = true }; - - // Act - var encoded = ToonEncoder.Encode(original); - var decoded = ToonDecoder.Decode(encoded); - - // Assert - Assert.NotNull(decoded); - var obj = decoded.AsObject(); - Assert.Equal("Alice", obj["name"]?.GetValue()); - Assert.Equal(30.0, obj["age"]?.GetValue()); - Assert.True(obj["active"]?.GetValue()); - } - - [Fact] - public void RoundTrip_Array_PreservesData() - { - // Arrange - var original = new { numbers = new[] { 1, 2, 3, 4, 5 } }; - - // Act - var encoded = ToonEncoder.Encode(original); - var decoded = ToonDecoder.Decode(encoded); - - // Assert - Assert.NotNull(decoded); - var numbers = decoded["numbers"]?.AsArray(); - Assert.NotNull(numbers); - Assert.Equal(5, numbers.Count); - for (int i = 0; i < 5; i++) - { - Assert.Equal((double)(i + 1), numbers[i]?.GetValue()); - } - } - - [Fact] - public void RoundTrip_ComplexStructure_PreservesData() - { - // Arrange - var original = new - { - users = new[] - { - new { id = 1, name = "Alice", email = "alice@example.com" }, - new { id = 2, name = "Bob", email = "bob@example.com" } - }, - metadata = new - { - total = 2, - timestamp = "2025-01-01T00:00:00Z" - } - }; - - // Act - var encoded = ToonEncoder.Encode(original); - var decoded = ToonDecoder.Decode(encoded); - - // Assert - Assert.NotNull(decoded); - var users = decoded["users"]?.AsArray(); - Assert.NotNull(users); - Assert.Equal(2, users.Count); - Assert.Equal("Alice", users[0]?["name"]?.GetValue()); - - var metadata = decoded["metadata"]?.AsObject(); - Assert.NotNull(metadata); - Assert.Equal(2.0, metadata["total"]?.GetValue()); - } -} diff --git a/tests/ToonFormat.SpecGenerator/SpecGenerator.cs b/tests/ToonFormat.SpecGenerator/SpecGenerator.cs index c647720..eba64d0 100644 --- a/tests/ToonFormat.SpecGenerator/SpecGenerator.cs +++ b/tests/ToonFormat.SpecGenerator/SpecGenerator.cs @@ -16,12 +16,6 @@ public void GenerateSpecs(SpecGeneratorOptions options) try { - // Clean up test directory before generating new files - CleanTestDirectory(options.AbsoluteOutputPath); - - // Copy manual tests from ManualTests folder - CopyManualTests(options.AbsoluteOutputPath); - logger.LogDebug("Cloning repository {RepoUrl} to {CloneDirectory}", options.SpecRepoUrl, toonSpecDir); GitTool.CloneRepository(options.SpecRepoUrl, toonSpecDir, @@ -58,104 +52,6 @@ public void GenerateSpecs(SpecGeneratorOptions options) logger.LogInformation("Spec generation completed."); } - private void CleanTestDirectory(string testDirectory) - { - if (!Directory.Exists(testDirectory)) - { - logger.LogDebug("Test directory {TestDirectory} does not exist, skipping cleanup", testDirectory); - return; - } - - logger.LogInformation("Cleaning test directory {TestDirectory}", testDirectory); - - // Delete all subdirectories - foreach (var dir in Directory.GetDirectories(testDirectory)) - { - try - { - Directory.Delete(dir, true); - logger.LogDebug("Deleted directory {Directory}", dir); - } - catch (Exception ex) - { - logger.LogWarning(ex, "Failed to delete directory {Directory}", dir); - } - } - - // Delete all files except .csproj files - foreach (var file in Directory.GetFiles(testDirectory)) - { - if (Path.GetExtension(file).Equals(".csproj", StringComparison.OrdinalIgnoreCase)) - { - logger.LogDebug("Preserving project file {File}", file); - continue; - } - - try - { - File.Delete(file); - logger.LogDebug("Deleted file {File}", file); - } - catch (Exception ex) - { - logger.LogWarning(ex, "Failed to delete file {File}", file); - } - } - - logger.LogInformation("Test directory cleanup completed"); - } - - private void CopyManualTests(string testDirectory) - { - // Get the directory where SpecGenerator assembly is located - var assemblyLocation = AppContext.BaseDirectory; - var manualTestsDir = Path.Combine(assemblyLocation, "ManualTests"); - - if (!Directory.Exists(manualTestsDir)) - { - logger.LogDebug("ManualTests directory not found at {ManualTestsDir}, skipping manual test copy", manualTestsDir); - return; - } - - logger.LogInformation("Copying manual tests from {ManualTestsDir}", manualTestsDir); - - // Copy all files and subdirectories from ManualTests to test directory - CopyDirectory(manualTestsDir, testDirectory); - - logger.LogInformation("Manual tests copied successfully"); - } - - private void CopyDirectory(string sourceDir, string destDir) - { - // Create destination directory if it doesn't exist - Directory.CreateDirectory(destDir); - - // Copy all files - foreach (var file in Directory.GetFiles(sourceDir)) - { - var fileName = Path.GetFileName(file); - var destFile = Path.Combine(destDir, fileName); - - try - { - File.Copy(file, destFile, overwrite: true); - logger.LogDebug("Copied file {SourceFile} to {DestFile}", file, destFile); - } - catch (Exception ex) - { - logger.LogWarning(ex, "Failed to copy file {SourceFile} to {DestFile}", file, destFile); - } - } - - // Copy all subdirectories recursively - foreach (var subDir in Directory.GetDirectories(sourceDir)) - { - var subDirName = Path.GetFileName(subDir); - var destSubDir = Path.Combine(destDir, subDirName); - CopyDirectory(subDir, destSubDir); - } - } - private void GenerateEncodeFixtures(string specDir, string outputDir, IEnumerable ignores) { var encodeFixtures = LoadEncodeFixtures(specDir); diff --git a/tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj b/tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj index a44c278..123b6ae 100644 --- a/tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj +++ b/tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj @@ -13,9 +13,4 @@ - - - - - diff --git a/tests/ToonFormat.Tests/KeyFoldingTests.cs b/tests/ToonFormat.Tests/KeyFoldingTests.cs index 5c637e4..8fcdd3b 100644 --- a/tests/ToonFormat.Tests/KeyFoldingTests.cs +++ b/tests/ToonFormat.Tests/KeyFoldingTests.cs @@ -1,4 +1,4 @@ -using Toon.Format; +using Toon.Format; namespace ToonFormat.Tests; From db0db15448f31c5b6a3ce73d63e97332cbf062bc Mon Sep 17 00:00:00 2001 From: Carlos Felipe Date: Fri, 28 Nov 2025 12:37:29 -0500 Subject: [PATCH 06/20] Fixes - Updated `specgen.sh` as same to ps1 file - Added exception type validation in `FixtureWriter.cs` --- specgen.sh | 2 +- .../ToonFormat.SpecGenerator/FixtureWriter.cs | 780 +++++++++--------- 2 files changed, 400 insertions(+), 382 deletions(-) diff --git a/specgen.sh b/specgen.sh index ce45262..8d43e8c 100755 --- a/specgen.sh +++ b/specgen.sh @@ -3,6 +3,6 @@ GH_REPO="https://github.com/toon-format/spec.git" OUT_DIR="./tests/ToonFormat.Tests" # build and execute spec generator -dotnet build tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj +dotnet build tests/ToonFormat.SpecGenerator dotnet run --project tests/ToonFormat.SpecGenerator -- --url="$GH_REPO" --output="$OUT_DIR" --branch="v3.0.0" --loglevel="Information" \ No newline at end of file diff --git a/tests/ToonFormat.SpecGenerator/FixtureWriter.cs b/tests/ToonFormat.SpecGenerator/FixtureWriter.cs index a9068ec..dc7f043 100644 --- a/tests/ToonFormat.SpecGenerator/FixtureWriter.cs +++ b/tests/ToonFormat.SpecGenerator/FixtureWriter.cs @@ -9,465 +9,483 @@ namespace ToonFormat.SpecGenerator; internal class FixtureWriter(Fixtures fixture, string outputDir) where TTestCase : ITestCase { - public Fixtures Fixture { get; } = fixture; - public string OutputDir { get; } = outputDir; + public Fixtures Fixture { get; } = fixture; + public string OutputDir { get; } = outputDir; - private int indentLevel = 0; + private int indentLevel = 0; - public void WriteFile() - { - var outputPath = Path.Combine(OutputDir, Fixture.FileName ?? throw new InvalidOperationException("Fixture FileName is not set")); + public void WriteFile() + { + var outputPath = Path.Combine(OutputDir, Fixture.FileName ?? throw new InvalidOperationException("Fixture FileName is not set")); - Directory.CreateDirectory(OutputDir); + Directory.CreateDirectory(OutputDir); - using var writer = new StreamWriter(outputPath, false); + using var writer = new StreamWriter(outputPath, false); - WriteHeader(writer); - WriteLine(writer); - WriteLine(writer); + WriteHeader(writer); + WriteLine(writer); + WriteLine(writer); - WriteUsings(writer); - WriteLine(writer); - WriteLine(writer); + WriteUsings(writer); + WriteLine(writer); + WriteLine(writer); - WriteNamespace(writer, Fixture.Category); - 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, "{"); + WriteLine(writer, $"[Trait(\"Category\", \"{Fixture.Category}\")]"); + WriteLine(writer, "public class " + FormatClassName(outputPath)); + WriteLine(writer, "{"); - Indent(); + Indent(); - // Write test methods here - foreach (var testCase in Fixture.Tests) - { - WriteTestMethod(writer, testCase); - } - - Unindent(); - WriteLine(writer, "}"); + // Write test methods here + foreach (var testCase in Fixture.Tests) + { + WriteTestMethod(writer, testCase); } - private string FormatClassName(string filePath) - { - var fileName = Path.GetFileNameWithoutExtension(filePath); - if (fileName == null) return string.Empty; + Unindent(); + WriteLine(writer, "}"); + } - return StripIllegalCharacters(fileName); - } + private string FormatClassName(string filePath) + { + var fileName = Path.GetFileNameWithoutExtension(filePath); + if (fileName == null) return string.Empty; - private string FormatMethodName(string methodName) - { - return StripIllegalCharacters(methodName.ToPascalCase()); - } + return StripIllegalCharacters(fileName); + } - private string StripIllegalCharacters(string input) - { - return new Regex(@"[\(_\-/\:\)=,+]").Replace(input, "")!; - } + 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(); - private void WriteTestMethod(StreamWriter writer, TTestCase testCase) + // Arrange + WriteLineIndented(writer, "// Arrange"); + switch (testCase) { - WriteLineIndented(writer, "[Fact]"); - WriteLineIndented(writer, $"[Trait(\"Description\", \"{testCase.Name}\")]"); - WriteLineIndented(writer, $"public void {FormatMethodName(testCase.Name)}()"); - WriteLineIndented(writer, "{"); + case EncodeTestCase encodeTestCase: + WriteLineIndented(writer, "var input ="); Indent(); + WriteJsonNodeAsAnonymousType(writer, encodeTestCase.Input); + Unindent(); - // Arrange - WriteLineIndented(writer, "// Arrange"); - switch (testCase) - { - case EncodeTestCase encodeTestCase: - WriteLineIndented(writer, "var input ="); + WriteLine(writer); - Indent(); - WriteJsonNodeAsAnonymousType(writer, encodeTestCase.Input); - Unindent(); + WriteLineIndented(writer, "var expected ="); + WriteLine(writer, "\"\"\""); + Write(writer, encodeTestCase.Expected); + WriteLine(writer); + WriteLine(writer, "\"\"\";"); - WriteLine(writer); + break; - WriteLineIndented(writer, "var expected ="); - WriteLine(writer, "\"\"\""); - Write(writer, encodeTestCase.Expected); - WriteLine(writer); - WriteLine(writer, "\"\"\";"); + case DecodeTestCase decodeTestCase: - break; + WriteLineIndented(writer, "var input ="); + WriteLine(writer, "\"\"\""); + Write(writer, decodeTestCase.Input); + WriteLine(writer); + WriteLine(writer, "\"\"\";"); - case DecodeTestCase decodeTestCase: + break; + default: + WriteLineIndented(writer, $"var input = /* {typeof(TIn).Name} */; // TODO: Initialize input"); + break; + } - 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(); - WriteLine(writer); + if (encodeTestCase.Options?.Delimiter != null) + WriteLineIndented(writer, $"Delimiter = {GetToonDelimiterEnumFromChar(encodeTestCase.Options.Delimiter)},"); + + if (encodeTestCase.Options?.Indent != null) + WriteLineIndented(writer, $"Indent = {encodeTestCase.Options.Indent},"); + + if (encodeTestCase.Options?.KeyFolding != null) + WriteLineIndented(writer, $"KeyFolding = {GetToonKeyFoldingEnumFromString(encodeTestCase.Options.KeyFolding)},"); + + if (encodeTestCase.Options?.FlattenDepth != null) + WriteLineIndented(writer, $"FlattenDepth = {encodeTestCase.Options.FlattenDepth},"); + + Unindent(); + WriteLineIndented(writer, "};"); - // Act & Assert - WriteLineIndented(writer, "// Act & Assert"); - switch (testCase) + WriteLine(writer); + WriteLineIndented(writer, $"var result = ToonEncoder.Encode(input, options);"); + } + else { - case EncodeTestCase encodeTestCase: - var hasEncodeOptions = encodeTestCase.Options != null; - if (hasEncodeOptions) - { - WriteLineIndented(writer, "var options = new ToonEncodeOptions"); - WriteLineIndented(writer, "{"); - Indent(); - - if (encodeTestCase.Options?.Delimiter != null) - WriteLineIndented(writer, $"Delimiter = {GetToonDelimiterEnumFromChar(encodeTestCase.Options.Delimiter)},"); - - if (encodeTestCase.Options?.Indent != null) - WriteLineIndented(writer, $"Indent = {encodeTestCase.Options.Indent},"); - - if (encodeTestCase.Options?.KeyFolding != null) - WriteLineIndented(writer, $"KeyFolding = {GetToonKeyFoldingEnumFromString(encodeTestCase.Options.KeyFolding)},"); - - if (encodeTestCase.Options?.FlattenDepth != null) - WriteLineIndented(writer, $"FlattenDepth = {encodeTestCase.Options.FlattenDepth},"); - - 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; + WriteLineIndented(writer, $"var result = ToonEncoder.Encode(input);"); } - Unindent(); - WriteLineIndented(writer, "}"); WriteLine(writer); - } + WriteLineIndented(writer, $"Assert.Equal(expected, result);"); + break; - private static string GetToonDelimiterEnumFromChar(string? delimiter) - { - return delimiter switch + case DecodeTestCase decodeTestCase: + var hasDecodeOptions = decodeTestCase.Options != null; + if (hasDecodeOptions) { - "," => "ToonDelimiter.COMMA", - "\t" => "ToonDelimiter.TAB", - "|" => "ToonDelimiter.PIPE", - _ => "ToonDelimiter.COMMA" - }; - } + WriteLineIndented(writer, "var options = new ToonDecodeOptions"); + WriteLineIndented(writer, "{"); + Indent(); - private static string GetToonKeyFoldingEnumFromString(string? keyFoldingOption) - { - return keyFoldingOption switch - { - "off" => "ToonKeyFolding.Off", - "safe" => "ToonKeyFolding.Safe", - _ => "ToonKeyFolding.Off" - }; - } + WriteLineIndented(writer, $"Indent = {decodeTestCase.Options?.Indent ?? 2},"); + WriteLineIndented(writer, $"Strict = {(decodeTestCase.Options?.Strict ?? true).ToString().ToLower()},"); - private void WriteJsonNodeAsAnonymousType(StreamWriter writer, JsonNode node) - { - WriteJsonNode(writer, node); + if (decodeTestCase.Options?.ExpandPaths != null) + WriteLineIndented(writer, $"ExpandPaths = {GetToonPathExpansionEnumFromString(decodeTestCase.Options.ExpandPaths)}"); - WriteLineIndented(writer, ";"); - } - private void WriteJsonNode(StreamWriter writer, JsonNode? node) - { - var propertyName = node?.Parent is JsonObject ? node?.GetPropertyName() : null; + Unindent(); + WriteLineIndented(writer, "};"); - void WriteFunc(string value) - { - if (propertyName is not null && node.Parent is not JsonArray) - { - Write(writer, value); - } - else - { - WriteIndented(writer, value); - } + WriteLine(writer); } - if (node is null) + if (decodeTestCase.ShouldError) { - WriteIndented(writer, "(string)null"); + // Determine which exception type to expect based on the options + var isPathExpansionError = decodeTestCase.Options?.ExpandPaths != null; + var exceptionType = isPathExpansionError ? "ToonPathExpansionException" : "ToonFormatException"; + + if (hasDecodeOptions) + { + WriteLineIndented(writer, $"Assert.Throws<{exceptionType}>(() => ToonDecoder.Decode(input, options));"); + } + else + { + WriteLineIndented(writer, $"Assert.Throws<{exceptionType}>(() => ToonDecoder.Decode(input));"); + } } - else if (node is JsonValue nodeValue) + else { - if (propertyName is not null) + 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) { - WriteIndented(writer, $"@{propertyName} = "); + 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, ""); + } } - - var kind = nodeValue.GetValueKind(); - if (kind == JsonValueKind.String) + } + 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) { - WriteFunc($"@\"{nodeValue.GetValue().Replace("\"", "\"\"")}\""); + WriteLineIndented(writer, $"var expected = string.Empty;"); } 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()}"); - } + WriteLineIndented(writer, $"var expected = JsonNode.Parse(\"\"\"\n{value}\n\"\"\");"); } - if (propertyName is not null) - { - WriteLine(writer, ","); - } + WriteLine(writer); + WriteLineIndented(writer, $"Assert.True(JsonNode.DeepEquals(result, expected));"); + } } - 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); - } - } + break; - Unindent(); - WriteLineIndented(writer, "}"); - - if (propertyName is not null) - { - WriteLine(writer, ","); - } - } - else if (node is JsonArray nodeArray) - { - if (!string.IsNullOrEmpty(propertyName)) - { - WriteIndented(writer, $"@{propertyName} ="); - } + default: + WriteLineIndented(writer, "// TODO: Implement test logic"); + break; + } - WriteFunc("new object[] {"); + Unindent(); + WriteLineIndented(writer, "}"); + WriteLine(writer); + } - WriteLineIndented(writer); - Indent(); + private static string GetToonDelimiterEnumFromChar(string? delimiter) + { + return delimiter switch + { + "," => "ToonDelimiter.COMMA", + "\t" => "ToonDelimiter.TAB", + "|" => "ToonDelimiter.PIPE", + _ => "ToonDelimiter.COMMA" + }; + } + + private static string GetToonKeyFoldingEnumFromString(string? keyFoldingOption) + { + return keyFoldingOption switch + { + "off" => "ToonKeyFolding.Off", + "safe" => "ToonKeyFolding.Safe", + _ => "ToonKeyFolding.Off" + }; + } + + private static string GetToonPathExpansionEnumFromString(string? expandPathsOption) + { + return expandPathsOption switch + { + "off" => "ToonPathExpansion.Off", + "safe" => "ToonPathExpansion.Safe", + _ => "ToonPathExpansion.Safe" + }; + } - foreach (var item in nodeArray) - { - WriteJsonNode(writer, item); - - if (item is JsonValue) - { - WriteLine(writer, ","); - } - else - { - WriteLineIndented(writer, ","); - } - } + private void WriteJsonNodeAsAnonymousType(StreamWriter writer, JsonNode node) + { + WriteJsonNode(writer, node); - Unindent(); - WriteLineIndented(writer, "}"); + WriteLineIndented(writer, ";"); + } - if (propertyName is not null) - { - WriteLine(writer, ","); - } - } - } + private void WriteJsonNode(StreamWriter writer, JsonNode? node) + { + var propertyName = node?.Parent is JsonObject ? node?.GetPropertyName() : null; - private void Indent() + void WriteFunc(string value) { - indentLevel++; + if (propertyName is not null && node.Parent is not JsonArray) + { + Write(writer, value); + } + else + { + WriteIndented(writer, value); + } } - private void Unindent() + if (node is null) { - indentLevel--; + WriteIndented(writer, "(string)null"); } - - private void WriteLineIndented(StreamWriter writer, string line) + else if (node is JsonValue nodeValue) { - writer.WriteLine(new string(' ', indentLevel * 4) + line); - } + 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(); - private void WriteLineIndented(StreamWriter writer) - { - WriteLineIndented(writer, ""); - } + WriteFunc($"{stringValue}"); + } + else + { + WriteFunc($"{nodeValue.GetValue()}"); + } + } - private void WriteIndented(StreamWriter writer, string content) - { - writer.Write(new string(' ', indentLevel * 4) + content); + if (propertyName is not null) + { + WriteLine(writer, ","); + } } - - private void WriteIndented(StreamWriter writer) + else if (node is JsonObject nodeObject) { - WriteIndented(writer, ""); - } + 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); + } + } - 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, "// "); - } + Unindent(); + WriteLineIndented(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;"); + if (propertyName is not null) + { + WriteLine(writer, ","); + } } - - private void WriteNamespace(StreamWriter writer, string category) + else if (node is JsonArray nodeArray) { - WriteLine(writer, $"namespace ToonFormat.Tests.{category.ToPascalCase()};"); - } + if (!string.IsNullOrEmpty(propertyName)) + { + WriteIndented(writer, $"@{propertyName} ="); + } - private void WriteLine(StreamWriter writer) - { - writer.WriteLine(); - } + WriteFunc("new object[] {"); - private void WriteLine(StreamWriter writer, string line) - { - writer.WriteLine(line); - } + WriteLineIndented(writer); + Indent(); - private void Write(StreamWriter writer, string contents) - { - writer.Write(contents); + 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); + } } From 898b54c544ec48c324fd250ba847346880856cda Mon Sep 17 00:00:00 2001 From: Carlos Felipe Date: Fri, 28 Nov 2025 13:55:17 -0500 Subject: [PATCH 07/20] Fixed tests --- src/ToonFormat/Internal/Encode/LineWriter.cs | 2 +- tests/ToonFormat.SpecGenerator/FixtureWriter.cs | 13 +++++++++++-- tests/ToonFormat.Tests/KeyFoldingTests.cs | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/ToonFormat/Internal/Encode/LineWriter.cs b/src/ToonFormat/Internal/Encode/LineWriter.cs index 3e72f35..fb3409f 100644 --- a/src/ToonFormat/Internal/Encode/LineWriter.cs +++ b/src/ToonFormat/Internal/Encode/LineWriter.cs @@ -48,7 +48,7 @@ public void PushListItem(int depth, string content) /// public override string ToString() { - return string.Join(Environment.NewLine, _lines); + return string.Join("\n", _lines); } /// diff --git a/tests/ToonFormat.SpecGenerator/FixtureWriter.cs b/tests/ToonFormat.SpecGenerator/FixtureWriter.cs index dc7f043..c18f3b7 100644 --- a/tests/ToonFormat.SpecGenerator/FixtureWriter.cs +++ b/tests/ToonFormat.SpecGenerator/FixtureWriter.cs @@ -21,6 +21,7 @@ public void WriteFile() Directory.CreateDirectory(OutputDir); using var writer = new StreamWriter(outputPath, false); + writer.NewLine = "\n"; // Use Unix line endings for cross-platform compatibility WriteHeader(writer); WriteLine(writer); @@ -92,7 +93,7 @@ private void WriteTestMethod(StreamWriter writer, TTestCase testCase) WriteLineIndented(writer, "var expected ="); WriteLine(writer, "\"\"\""); - Write(writer, encodeTestCase.Expected); + Write(writer, NormalizeLineEndings(encodeTestCase.Expected)); WriteLine(writer); WriteLine(writer, "\"\"\";"); @@ -102,7 +103,7 @@ private void WriteTestMethod(StreamWriter writer, TTestCase testCase) WriteLineIndented(writer, "var input ="); WriteLine(writer, "\"\"\""); - Write(writer, decodeTestCase.Input); + Write(writer, NormalizeLineEndings(decodeTestCase.Input)); WriteLine(writer); WriteLine(writer, "\"\"\";"); @@ -488,4 +489,12 @@ private void Write(StreamWriter writer, string contents) { writer.Write(contents); } + + /// + /// Normalizes line endings to Unix format (LF) for cross-platform compatibility. + /// + private static string NormalizeLineEndings(string text) + { + return text.Replace("\r\n", "\n"); + } } diff --git a/tests/ToonFormat.Tests/KeyFoldingTests.cs b/tests/ToonFormat.Tests/KeyFoldingTests.cs index 8fcdd3b..5c637e4 100644 --- a/tests/ToonFormat.Tests/KeyFoldingTests.cs +++ b/tests/ToonFormat.Tests/KeyFoldingTests.cs @@ -1,4 +1,4 @@ -using Toon.Format; +using Toon.Format; namespace ToonFormat.Tests; From f2f5f4861ee35ef426681e82b6f08e9de444e6ba Mon Sep 17 00:00:00 2001 From: Carlos Felipe Date: Fri, 28 Nov 2025 14:43:32 -0500 Subject: [PATCH 08/20] Updated tests --- .../ManualTests/Encode/ArraysObjectsManual.cs | 137 +++++ .../ManualTests/JsonComplexRoundTripTests.cs | 255 +++++++++ .../ManualTests/KeyFoldingTests.cs | 501 ++++++++++++++++++ .../ManualTests/PerformanceBenchmark.cs | 131 +++++ .../ManualTests/ToonDecoderTests.cs | 170 ++++++ .../ManualTests/ToonEncoderTests.cs | 154 ++++++ .../ManualTests/ToonRoundTripTests.cs | 84 +++ .../ToonFormat.SpecGenerator/SpecGenerator.cs | 104 ++++ .../ToonFormat.SpecGenerator.csproj | 5 + 9 files changed, 1541 insertions(+) create mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/Encode/ArraysObjectsManual.cs create mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/JsonComplexRoundTripTests.cs create mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/KeyFoldingTests.cs create mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/PerformanceBenchmark.cs create mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/ToonDecoderTests.cs create mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs create mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/ToonRoundTripTests.cs diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/Encode/ArraysObjectsManual.cs b/tests/ToonFormat.SpecGenerator/ManualTests/Encode/ArraysObjectsManual.cs new file mode 100644 index 0000000..929aad7 --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/ManualTests/Encode/ArraysObjectsManual.cs @@ -0,0 +1,137 @@ +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 ArraysObjectsManual +{ + [Fact] + [Trait("Description", "tabular array with multiple sibling fields")] + public void TabularArrayWithMultipleSiblingFields() + { + // Arrange + var input = + new + { + @items = new object[] { + new + { + @users = new object[] { + new { @id = 1, @name = "Ada" }, + new { @id = 2, @name = "Bob" }, + }, + @status = "active", + @count = 2, + @tags = new object[] { "a", "b", "c" } + } + } + }; + + var expected = +""" +items[1]: + - users[2]{id,name}: + 1,Ada + 2,Bob + status: active + count: 2 + tags[3]: a,b,c +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "multiple list items with tabular first fields")] + public void MultipleListItemsWithTabularFirstFields() + { + // Arrange + var input = + new + { + @items = new object[] { + new + { + @users = new object[] { + new { @id = 1, @name = "Ada" }, + new { @id = 2, @name = "Bob" }, + }, + @status = "active" + }, + new + { + @users = new object[] { + new { @id = 3, @name = "Charlie" } + }, + @status = "inactive" + } + } + }; + + var expected = +""" +items[2]: + - users[2]{id,name}: + 1,Ada + 2,Bob + status: active + - users[1]{id,name}: + 3,Charlie + status: inactive +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } + + [Fact] + [Trait("Description", "deeply nested list items with tabular first field")] + public void DeeplyNestedListItemsWithTabularFirstField() + { + // Arrange + var input = + new + { + @data = new object[] { + new + { + @items = new object[] { + new + { + @users = new object[] { + new { @id = 1, @name = "Ada" }, + new { @id = 2, @name = "Bob" }, + }, + @status = "active" + } + } + } + } + }; + + var expected = +""" +data[1]: + - items[1]: + - users[2]{id,name}: + 1,Ada + 2,Bob + status: active +"""; + + // Act & Assert + var result = ToonEncoder.Encode(input); + + Assert.Equal(expected, result); + } +} diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/JsonComplexRoundTripTests.cs b/tests/ToonFormat.SpecGenerator/ManualTests/JsonComplexRoundTripTests.cs new file mode 100644 index 0000000..9080e53 --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/ManualTests/JsonComplexRoundTripTests.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit.Abstractions; + +namespace ToonFormat.Tests; + +/// +/// Tests for complex multi-level JSON structures to validate TOON format encoding and decoding. +/// +public class JsonComplexRoundTripTests +{ + private readonly ITestOutputHelper _output; + + public JsonComplexRoundTripTests(ITestOutputHelper output) + { + _output = output; + } + private const string ComplexJson = @"{ + ""project"": { + ""id"": ""PX-4921"", + ""name"": ""Customer Insights Expansion"", + ""description"": ""This is a long descriptive text containing more than fifteen words to simulate a realistic business scenario for testing purposes."", + ""createdAt"": ""2025-11-20T10:32:00Z"", + ""metadata"": { + ""owner"": ""john.doe@example.com"", + ""website"": ""https://example.org/products/insights?ref=test&lang=en"", + ""tags"": [""analysis"", ""insights"", ""growth"", ""R&D""], + ""cost"": { + ""currency"": ""USD"", + ""amount"": 12500.75 + } + }, + ""phases"": [ + { + ""phaseId"": 1, + ""title"": ""Discovery & Research"", + ""deadline"": ""2025-12-15"", + ""status"": ""In Progress"", + ""details"": { + ""notes"": ""Team is conducting interviews, market analysis, and reviewing historical performance metrics & competitors."", + ""specialChars"": ""!@#$%^&*()_+=-{}[]|:;<>,.?/"" + } + }, + { + ""phaseId"": 2, + ""title"": ""Development"", + ""deadline"": ""2026-01-30"", + ""budget"": { + ""currency"": ""EUR"", + ""amount"": 7800.00 + }, + ""resources"": { + ""leadDeveloper"": ""alice.smith@example.com"", + ""repository"": ""https://github.com/example/repo"" + } + } + ] + } +}"; + + [Fact] + public void ComplexJson_RoundTrip_ShouldPreserveKeyFields() + { + // Arrange + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var root = JsonSerializer.Deserialize(ComplexJson, options); + + // Sanity + Assert.NotNull(root); + Assert.NotNull(root.project); + + // Act - encode to TOON and decode back + var toonText = ToonEncoder.Encode(root); + Assert.NotNull(toonText); + _output.WriteLine("TOON Encoded Output:"); + _output.WriteLine(toonText); + _output.WriteLine("---"); + + var decoded = ToonDecoder.Decode(toonText); + Assert.NotNull(decoded); + + // The encoder reflects C# property names (we used lowercase names to match original JSON keys) + var project = decoded["project"]?.AsObject(); + Assert.NotNull(project); + + // Assert key scalar values + Assert.Equal("PX-4921", project["id"]?.GetValue()); + Assert.Equal("Customer Insights Expansion", project["name"]?.GetValue()); + + // Metadata checks + var metadata = project["metadata"]?.AsObject(); + Assert.NotNull(metadata); + Assert.Equal("john.doe@example.com", metadata["owner"]?.GetValue()); + Assert.Equal("https://example.org/products/insights?ref=test&lang=en", metadata["website"]?.GetValue()); + + // Tags array validation + var tags = metadata["tags"]?.AsArray(); + Assert.NotNull(tags); + Assert.Equal(4, tags.Count); + Assert.Equal("analysis", tags[0]?.GetValue()); + Assert.Equal("insights", tags[1]?.GetValue()); + Assert.Equal("growth", tags[2]?.GetValue()); + Assert.Equal("R&D", tags[3]?.GetValue()); + + var cost = metadata["cost"]?.AsObject(); + Assert.NotNull(cost); + Assert.Equal("USD", cost["currency"]?.GetValue()); + Assert.Equal(12500.75, cost["amount"]?.GetValue()); + + // Phases checks + var phases = project["phases"]?.AsArray(); + Assert.NotNull(phases); + Assert.Equal(2, phases.Count); + + var phase1 = phases[0]?.AsObject(); + Assert.NotNull(phase1); + Assert.Equal(1.0, phase1["phaseId"]?.GetValue()); + var details = phase1["details"]?.AsObject(); + Assert.NotNull(details); + Assert.Contains("market analysis", details["notes"]?.GetValue() ?? string.Empty); + Assert.Equal("!@#$%^&*()_+=-{}[]|:;<>,.?/", details["specialChars"]?.GetValue()); + + var phase2 = phases[1]?.AsObject(); + Assert.NotNull(phase2); + Assert.Equal(2.0, phase2["phaseId"]?.GetValue()); + Assert.Equal("Development", phase2["title"]?.GetValue()); + Assert.Equal("2026-01-30", phase2["deadline"]?.GetValue()); + + var budget = phase2["budget"]?.AsObject(); + Assert.NotNull(budget); + Assert.Equal("EUR", budget["currency"]?.GetValue()); + Assert.Equal(7800.0, budget["amount"]?.GetValue()); + + var resources = phase2["resources"]?.AsObject(); + Assert.NotNull(resources); + Assert.Equal("alice.smith@example.com", resources["leadDeveloper"]?.GetValue()); + Assert.Equal("https://github.com/example/repo", resources["repository"]?.GetValue()); + } + + [Fact] + public void ComplexJson_Encode_ShouldProduceValidToonFormat() + { + // Arrange + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var root = JsonSerializer.Deserialize(ComplexJson, options); + Assert.NotNull(root); + + // Act + var toonText = ToonEncoder.Encode(root); + + // Assert - verify TOON format structure + Assert.NotNull(toonText); + Assert.Contains("project:", toonText); + Assert.Contains("id:", toonText); + Assert.Contains("PX-4921", toonText); + Assert.Contains("metadata:", toonText); + Assert.Contains("phases[2]", toonText); + + _output.WriteLine("TOON Output:"); + _output.WriteLine(toonText); + } + + [Fact] + public void ComplexJson_SpecialCharacters_ShouldBePreserved() + { + // Arrange + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var root = JsonSerializer.Deserialize(ComplexJson, options); + Assert.NotNull(root); + + // Act + var toonText = ToonEncoder.Encode(root); + var decoded = ToonDecoder.Decode(toonText); + + // Assert - verify special characters in details.specialChars + Assert.NotNull(decoded); + var project = decoded["project"]?.AsObject(); + var phases = project?["phases"]?.AsArray(); + var phase1 = phases?[0]?.AsObject(); + var details = phase1?["details"]?.AsObject(); + + Assert.NotNull(details); + var specialChars = details["specialChars"]?.GetValue(); + Assert.Equal("!@#$%^&*()_+=-{}[]|:;<>,.?/", specialChars); + } + + [Fact] + public void ComplexJson_DateTime_ShouldBePreservedAsString() + { + // Arrange + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var root = JsonSerializer.Deserialize(ComplexJson, options); + Assert.NotNull(root); + + // Act + var toonText = ToonEncoder.Encode(root); + var decoded = ToonDecoder.Decode(toonText); + + // Assert - verify DateTime is preserved as full ISO 8601 UTC timestamp + Assert.NotNull(decoded); + var project = decoded["project"]?.AsObject(); + Assert.NotNull(project); + + var createdAt = project["createdAt"]?.GetValue(); + Assert.NotNull(createdAt); + // Validate full ISO 8601 UTC timestamp (with or without fractional seconds) + // .NET DateTime.ToString("O") produces: 2025-11-20T10:32:00.0000000Z + Assert.StartsWith("2025-11-20T10:32:00", createdAt); + Assert.EndsWith("Z", createdAt); + } + + // POCOs with lowercase property names to preserve original JSON keys when encoding via reflection + public class Root { public Project project { get; set; } = null!; } + + public class Project + { + public string id { get; set; } = null!; + public string name { get; set; } = null!; + public string description { get; set; } = null!; + public DateTime createdAt { get; set; } + public Metadata metadata { get; set; } = null!; + public List phases { get; set; } = new(); + } + + public class Metadata + { + public string owner { get; set; } = null!; + public string website { get; set; } = null!; + public List tags { get; set; } = new(); + public Cost cost { get; set; } = null!; + } + + public class Cost { public string currency { get; set; } = null!; public double amount { get; set; } } + + public class Phase + { + public int phaseId { get; set; } + public string title { get; set; } = null!; + public string deadline { get; set; } = null!; + public string? status { get; set; } + public Details? details { get; set; } + public Budget? budget { get; set; } + public Resources? resources { get; set; } + } + + public class Details { public string notes { get; set; } = null!; public string specialChars { get; set; } = null!; } + + public class Budget { public string currency { get; set; } = null!; public double amount { get; set; } } + + public class Resources { public string leadDeveloper { get; set; } = null!; public string repository { get; set; } = null!; } +} diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/KeyFoldingTests.cs b/tests/ToonFormat.SpecGenerator/ManualTests/KeyFoldingTests.cs new file mode 100644 index 0000000..5c637e4 --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/ManualTests/KeyFoldingTests.cs @@ -0,0 +1,501 @@ +using Toon.Format; + +namespace ToonFormat.Tests; + +// TODO: Remove these tests once generated spec tests are in source control +// used to validate current key folding functionality aligns with spec +public class KeyFoldingTests +{ + [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, + KeyFolding = ToonKeyFolding.Safe + }; + + 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, + KeyFolding = ToonKeyFolding.Safe + }; + + 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, + KeyFolding = ToonKeyFolding.Safe + }; + + 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, + KeyFolding = ToonKeyFolding.Safe, + FlattenDepth = 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, + KeyFolding = ToonKeyFolding.Safe + }; + + 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, + KeyFolding = ToonKeyFolding.Safe, + FlattenDepth = 0, + }; + + 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, + KeyFolding = ToonKeyFolding.Safe, + FlattenDepth = 1, + }; + + 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, + KeyFolding = ToonKeyFolding.Off + }; + + 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, + KeyFolding = ToonKeyFolding.Safe + }; + + 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, + KeyFolding = ToonKeyFolding.Safe + }; + + 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, + KeyFolding = ToonKeyFolding.Safe + }; + + var result = ToonEncoder.Encode(input, options); + + Assert.Equal(expected, result); + } +} diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/PerformanceBenchmark.cs b/tests/ToonFormat.SpecGenerator/ManualTests/PerformanceBenchmark.cs new file mode 100644 index 0000000..b27cef8 --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/ManualTests/PerformanceBenchmark.cs @@ -0,0 +1,131 @@ +using System; +using System.Diagnostics; +using System.Text.Json.Nodes; +using Toon.Format; +using Xunit; +using Xunit.Abstractions; + +namespace ToonFormat.Tests; + +public class PerformanceBenchmark +{ + private readonly ITestOutputHelper _output; + + public PerformanceBenchmark(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void BenchmarkSimpleEncoding() + { + var data = new + { + users = new[] + { + new { id = 1, name = "Alice", role = "admin" }, + new { id = 2, name = "Bob", role = "user" }, + new { id = 3, name = "Charlie", role = "user" } + } + }; + + var sw = Stopwatch.StartNew(); + for (int i = 0; i < 10000; i++) + { + _ = ToonEncoder.Encode(data); + } + sw.Stop(); + + _output.WriteLine($"Simple encoding: {sw.ElapsedMilliseconds}ms for 10,000 iterations"); + _output.WriteLine($"Average: {sw.Elapsed.TotalMicroseconds / 10000:F2}μs per encode"); + + // Baseline: should complete in reasonable time (< 5 seconds for 10k iterations) + Assert.True(sw.ElapsedMilliseconds < 5000, $"Encoding took {sw.ElapsedMilliseconds}ms, expected < 5000ms"); + } + + [Fact] + public void BenchmarkSimpleDecoding() + { + var toonString = """ +users[3]{id,name,role}: + 1,Alice,admin + 2,Bob,user + 3,Charlie,user +"""; + + var sw = Stopwatch.StartNew(); + for (int i = 0; i < 10000; i++) + { + _ = ToonDecoder.Decode(toonString); + } + sw.Stop(); + + _output.WriteLine($"Simple decoding: {sw.ElapsedMilliseconds}ms for 10,000 iterations"); + _output.WriteLine($"Average: {sw.Elapsed.TotalMicroseconds / 10000:F2}μs per decode"); + + // Baseline: should complete in reasonable time (< 5 seconds for 10k iterations) + Assert.True(sw.ElapsedMilliseconds < 5000, $"Decoding took {sw.ElapsedMilliseconds}ms, expected < 5000ms"); + } + + [Fact] + public void BenchmarkSection10Encoding() + { + var data = new + { + items = new[] + { + new + { + users = new[] + { + new { id = 1, name = "Ada" }, + new { id = 2, name = "Bob" } + }, + status = "active", + count = 2 + } + } + }; + + var sw = Stopwatch.StartNew(); + for (int i = 0; i < 10000; i++) + { + _ = ToonEncoder.Encode(data); + } + sw.Stop(); + + _output.WriteLine($"Section 10 encoding: {sw.ElapsedMilliseconds}ms for 10,000 iterations"); + _output.WriteLine($"Average: {sw.Elapsed.TotalMicroseconds / 10000:F2}μs per encode"); + + // Baseline: should complete in reasonable time + Assert.True(sw.ElapsedMilliseconds < 5000, $"Section 10 encoding took {sw.ElapsedMilliseconds}ms, expected < 5000ms"); + } + + [Fact] + public void BenchmarkRoundTrip() + { + var data = new + { + users = new[] + { + new { id = 1, name = "Alice" }, + new { id = 2, name = "Bob" } + }, + metadata = new { version = 1, timestamp = "2025-11-27" } + }; + + var sw = Stopwatch.StartNew(); + for (int i = 0; i < 5000; i++) + { + var encoded = ToonEncoder.Encode(data); + _ = ToonDecoder.Decode(encoded); + } + sw.Stop(); + + _output.WriteLine($"Round-trip: {sw.ElapsedMilliseconds}ms for 5,000 iterations"); + _output.WriteLine($"Average: {sw.Elapsed.TotalMicroseconds / 5000:F2}μs per round-trip"); + + // Baseline: should complete in reasonable time + Assert.True(sw.ElapsedMilliseconds < 5000, $"Round-trip took {sw.ElapsedMilliseconds}ms, expected < 5000ms"); + } +} diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/ToonDecoderTests.cs b/tests/ToonFormat.SpecGenerator/ManualTests/ToonDecoderTests.cs new file mode 100644 index 0000000..d2091af --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/ManualTests/ToonDecoderTests.cs @@ -0,0 +1,170 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; + +namespace ToonFormat.Tests; + +/// +/// Tests for decoding TOON format strings. +/// +public class ToonDecoderTests +{ + [Fact] + public void Decode_SimpleObject_ReturnsValidJson() + { + // Arrange + var toonString = "name: Alice\nage: 30"; + + // Act + var result = ToonDecoder.Decode(toonString); + + // Assert + Assert.NotNull(result); + var obj = result.AsObject(); + Assert.Equal("Alice", obj["name"]?.GetValue()); + Assert.Equal(30.0, obj["age"]?.GetValue()); + } + + [Fact] + 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); + } + + [Fact] + public void Decode_PrimitiveArray_ReturnsValidArray() + { + // Arrange + var toonString = "numbers[5]: 1, 2, 3, 4, 5"; + + // Act + var result = ToonDecoder.Decode(toonString); + + // Assert + Assert.NotNull(result); + var obj = result.AsObject(); + var numbers = obj["numbers"]?.AsArray(); + Assert.NotNull(numbers); + Assert.Equal(5, numbers.Count); + Assert.Equal(1.0, numbers[0]?.GetValue()); + Assert.Equal(5.0, numbers[4]?.GetValue()); + } + + [Fact] + public void Decode_TabularArray_ReturnsValidStructure() + { + // Arrange - using list array format instead + var toonString = @"employees[3]: + - id: 1 + name: Alice + salary: 50000 + - id: 2 + name: Bob + salary: 60000 + - id: 3 + name: Charlie + salary: 55000"; + + // Act + var result = ToonDecoder.Decode(toonString); + + // Assert + Assert.NotNull(result); + var obj = result.AsObject(); + var employees = obj["employees"]?.AsArray(); + Assert.NotNull(employees); + Assert.Equal(3, employees.Count); + Assert.Equal(1.0, employees[0]?["id"]?.GetValue()); + Assert.Equal("Alice", employees[0]?["name"]?.GetValue()); + } + + [Fact] + public void Decode_NestedObject_ReturnsValidStructure() + { + // Arrange + var toonString = @"user: + name: Alice + address: + city: New York + zip: 10001"; + + // Act + var result = ToonDecoder.Decode(toonString); + + // Assert + Assert.NotNull(result); + var user = result["user"]?.AsObject(); + Assert.NotNull(user); + Assert.Equal("Alice", user["name"]?.GetValue()); + var address = user["address"]?.AsObject(); + Assert.NotNull(address); + Assert.Equal("New York", address["city"]?.GetValue()); + } + + [Fact] + public void Decode_WithStrictOption_ValidatesArrayLength() + { + // Arrange - array declares 5 items but only provides 3 + var toonString = "numbers[5]: 1, 2, 3"; + var options = new ToonDecodeOptions { Strict = true }; + + // Act & Assert + Assert.Throws(() => ToonDecoder.Decode(toonString, options)); + } + + [Fact] + public void Decode_WithNonStrictOption_AllowsLengthMismatch() + { + // Arrange - array declares 5 items but only provides 3 + var toonString = "numbers[5]: 1, 2, 3"; + var options = new ToonDecodeOptions { Strict = false }; + + // Act + var result = ToonDecoder.Decode(toonString, options); + + // Assert + Assert.NotNull(result); + var obj = result.AsObject(); + var numbers = obj["numbers"]?.AsArray(); + Assert.NotNull(numbers); + Assert.Equal(3, numbers.Count); + } + + [Fact] + public void Decode_InvalidFormat_ThrowsToonFormatException() + { + // Arrange - array length mismatch with strict mode + var invalidToon = "items[10]: 1, 2, 3"; + var options = new ToonDecodeOptions { Strict = true }; + + // Act & Assert + Assert.Throws(() => ToonDecoder.Decode(invalidToon, options)); + } + + [Fact] + public void Decode_EmptyString_ReturnsEmptyObject() + { + // Arrange + var emptyString = ""; + + // Act + var result = ToonDecoder.Decode(emptyString); + + // Assert - empty string returns empty array + Assert.NotNull(result); + } +} diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs b/tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs new file mode 100644 index 0000000..8cd92be --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs @@ -0,0 +1,154 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; + +namespace ToonFormat.Tests; + +/// +/// Tests for encoding data to TOON format. +/// +public class ToonEncoderTests +{ + [Fact] + public void Encode_SimpleObject_ReturnsValidToon() + { + // Arrange + var data = new { name = "Alice", age = 30 }; + + // Act + var result = ToonEncoder.Encode(data); + + // Assert + Assert.NotNull(result); + Assert.Contains("name:", result); + Assert.Contains("age:", result); + } + + [Fact] + 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); + } + + [Fact] + public void Encode_Array_ReturnsValidToon() + { + // Arrange + var data = new { numbers = new[] { 1, 2, 3, 4, 5 } }; + + // Act + var result = ToonEncoder.Encode(data); + + // Assert + Assert.NotNull(result); + Assert.Contains("numbers[", result); + } + + [Fact] + public void Encode_TabularArray_ReturnsValidToon() + { + // Arrange + var employees = new[] + { + new { id = 1, name = "Alice", salary = 50000 }, + new { id = 2, name = "Bob", salary = 60000 }, + new { id = 3, name = "Charlie", salary = 55000 } + }; + var data = new { employees }; + + // Act + var result = ToonEncoder.Encode(data); + + // Assert + Assert.NotNull(result); + Assert.Contains("employees[", result); + Assert.Contains("id", result); + Assert.Contains("name", result); + Assert.Contains("salary", result); + } + + [Fact] + public void Encode_WithCustomIndent_UsesCorrectIndentation() + { + // Arrange + var data = new { outer = new { inner = "value" } }; + var options = new ToonEncodeOptions { Indent = 4 }; + + // Act + var result = ToonEncoder.Encode(data, options); + + // Assert + Assert.NotNull(result); + Assert.Contains("outer:", result); + } + + [Fact] + public void Encode_WithCustomDelimiter_UsesCorrectDelimiter() + { + // Arrange + var data = new { numbers = new[] { 1, 2, 3 } }; + var options = new ToonEncodeOptions { Delimiter = ToonDelimiter.TAB }; + + // Act + var result = ToonEncoder.Encode(data, options); + + // Assert + Assert.NotNull(result); + Assert.Contains("numbers[", result); + } + + [Fact] + public void Encode_WithLengthMarker_IncludesHashSymbol() + { + // Arrange + var data = new { items = new[] { 1, 2, 3 } }; + var options = new ToonEncodeOptions { LengthMarker = true }; + + // Act + var result = ToonEncoder.Encode(data, options); + + // Assert + Assert.NotNull(result); + Assert.Contains("[#", result); + } + + [Fact] + public void Encode_NestedStructures_ReturnsValidToon() + { + // Arrange + var data = new + { + user = new + { + name = "Alice", + address = new + { + city = "New York", + zip = "10001" + } + } + }; + + // Act + var result = ToonEncoder.Encode(data); + + // Assert + Assert.NotNull(result); + Assert.Contains("user:", result); + Assert.Contains("address:", result); + } +} diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/ToonRoundTripTests.cs b/tests/ToonFormat.SpecGenerator/ManualTests/ToonRoundTripTests.cs new file mode 100644 index 0000000..251ee95 --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/ManualTests/ToonRoundTripTests.cs @@ -0,0 +1,84 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Toon.Format; + +namespace ToonFormat.Tests; + +/// +/// Round-trip tests to verify encoding and decoding preserve data integrity. +/// +public class ToonRoundTripTests +{ + [Fact] + public void RoundTrip_SimpleObject_PreservesData() + { + // Arrange + var original = new { name = "Alice", age = 30, active = true }; + + // Act + var encoded = ToonEncoder.Encode(original); + var decoded = ToonDecoder.Decode(encoded); + + // Assert + Assert.NotNull(decoded); + var obj = decoded.AsObject(); + Assert.Equal("Alice", obj["name"]?.GetValue()); + Assert.Equal(30.0, obj["age"]?.GetValue()); + Assert.True(obj["active"]?.GetValue()); + } + + [Fact] + public void RoundTrip_Array_PreservesData() + { + // Arrange + var original = new { numbers = new[] { 1, 2, 3, 4, 5 } }; + + // Act + var encoded = ToonEncoder.Encode(original); + var decoded = ToonDecoder.Decode(encoded); + + // Assert + Assert.NotNull(decoded); + var numbers = decoded["numbers"]?.AsArray(); + Assert.NotNull(numbers); + Assert.Equal(5, numbers.Count); + for (int i = 0; i < 5; i++) + { + Assert.Equal((double)(i + 1), numbers[i]?.GetValue()); + } + } + + [Fact] + public void RoundTrip_ComplexStructure_PreservesData() + { + // Arrange + var original = new + { + users = new[] + { + new { id = 1, name = "Alice", email = "alice@example.com" }, + new { id = 2, name = "Bob", email = "bob@example.com" } + }, + metadata = new + { + total = 2, + timestamp = "2025-01-01T00:00:00Z" + } + }; + + // Act + var encoded = ToonEncoder.Encode(original); + var decoded = ToonDecoder.Decode(encoded); + + // Assert + Assert.NotNull(decoded); + var users = decoded["users"]?.AsArray(); + Assert.NotNull(users); + Assert.Equal(2, users.Count); + Assert.Equal("Alice", users[0]?["name"]?.GetValue()); + + var metadata = decoded["metadata"]?.AsObject(); + Assert.NotNull(metadata); + Assert.Equal(2.0, metadata["total"]?.GetValue()); + } +} diff --git a/tests/ToonFormat.SpecGenerator/SpecGenerator.cs b/tests/ToonFormat.SpecGenerator/SpecGenerator.cs index eba64d0..c647720 100644 --- a/tests/ToonFormat.SpecGenerator/SpecGenerator.cs +++ b/tests/ToonFormat.SpecGenerator/SpecGenerator.cs @@ -16,6 +16,12 @@ public void GenerateSpecs(SpecGeneratorOptions options) try { + // Clean up test directory before generating new files + CleanTestDirectory(options.AbsoluteOutputPath); + + // Copy manual tests from ManualTests folder + CopyManualTests(options.AbsoluteOutputPath); + logger.LogDebug("Cloning repository {RepoUrl} to {CloneDirectory}", options.SpecRepoUrl, toonSpecDir); GitTool.CloneRepository(options.SpecRepoUrl, toonSpecDir, @@ -52,6 +58,104 @@ public void GenerateSpecs(SpecGeneratorOptions options) logger.LogInformation("Spec generation completed."); } + private void CleanTestDirectory(string testDirectory) + { + if (!Directory.Exists(testDirectory)) + { + logger.LogDebug("Test directory {TestDirectory} does not exist, skipping cleanup", testDirectory); + return; + } + + logger.LogInformation("Cleaning test directory {TestDirectory}", testDirectory); + + // Delete all subdirectories + foreach (var dir in Directory.GetDirectories(testDirectory)) + { + try + { + Directory.Delete(dir, true); + logger.LogDebug("Deleted directory {Directory}", dir); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to delete directory {Directory}", dir); + } + } + + // Delete all files except .csproj files + foreach (var file in Directory.GetFiles(testDirectory)) + { + if (Path.GetExtension(file).Equals(".csproj", StringComparison.OrdinalIgnoreCase)) + { + logger.LogDebug("Preserving project file {File}", file); + continue; + } + + try + { + File.Delete(file); + logger.LogDebug("Deleted file {File}", file); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to delete file {File}", file); + } + } + + logger.LogInformation("Test directory cleanup completed"); + } + + private void CopyManualTests(string testDirectory) + { + // Get the directory where SpecGenerator assembly is located + var assemblyLocation = AppContext.BaseDirectory; + var manualTestsDir = Path.Combine(assemblyLocation, "ManualTests"); + + if (!Directory.Exists(manualTestsDir)) + { + logger.LogDebug("ManualTests directory not found at {ManualTestsDir}, skipping manual test copy", manualTestsDir); + return; + } + + logger.LogInformation("Copying manual tests from {ManualTestsDir}", manualTestsDir); + + // Copy all files and subdirectories from ManualTests to test directory + CopyDirectory(manualTestsDir, testDirectory); + + logger.LogInformation("Manual tests copied successfully"); + } + + private void CopyDirectory(string sourceDir, string destDir) + { + // Create destination directory if it doesn't exist + Directory.CreateDirectory(destDir); + + // Copy all files + foreach (var file in Directory.GetFiles(sourceDir)) + { + var fileName = Path.GetFileName(file); + var destFile = Path.Combine(destDir, fileName); + + try + { + File.Copy(file, destFile, overwrite: true); + logger.LogDebug("Copied file {SourceFile} to {DestFile}", file, destFile); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to copy file {SourceFile} to {DestFile}", file, destFile); + } + } + + // Copy all subdirectories recursively + foreach (var subDir in Directory.GetDirectories(sourceDir)) + { + var subDirName = Path.GetFileName(subDir); + var destSubDir = Path.Combine(destDir, subDirName); + CopyDirectory(subDir, destSubDir); + } + } + private void GenerateEncodeFixtures(string specDir, string outputDir, IEnumerable ignores) { var encodeFixtures = LoadEncodeFixtures(specDir); diff --git a/tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj b/tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj index 123b6ae..a44c278 100644 --- a/tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj +++ b/tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj @@ -13,4 +13,9 @@ + + + + + From 4651a38eacc5a6364dc3f78e4595fac2bddfc84a Mon Sep 17 00:00:00 2001 From: Carlos Felipe Date: Fri, 28 Nov 2025 15:03:56 -0500 Subject: [PATCH 09/20] Fixed tests on windows --- src/ToonFormat/Constants.cs | 2 +- .../ToonFormat.SpecGenerator/FixtureWriter.cs | 814 +++++++++--------- .../ManualTests/ToonEncoderTests.cs | 2 +- tests/ToonFormat.Tests/ToonEncoderTests.cs | 2 +- 4 files changed, 410 insertions(+), 410 deletions(-) diff --git a/src/ToonFormat/Constants.cs b/src/ToonFormat/Constants.cs index a92b515..c03ea16 100644 --- a/src/ToonFormat/Constants.cs +++ b/src/ToonFormat/Constants.cs @@ -132,7 +132,7 @@ public enum ToonPathExpansion { /// Path expansion disabled Off, - + /// Keys containing dots are expanded into nested structures Safe } diff --git a/tests/ToonFormat.SpecGenerator/FixtureWriter.cs b/tests/ToonFormat.SpecGenerator/FixtureWriter.cs index c18f3b7..b8aaacb 100644 --- a/tests/ToonFormat.SpecGenerator/FixtureWriter.cs +++ b/tests/ToonFormat.SpecGenerator/FixtureWriter.cs @@ -9,492 +9,492 @@ namespace ToonFormat.SpecGenerator; internal class FixtureWriter(Fixtures fixture, string outputDir) where TTestCase : ITestCase { - public Fixtures Fixture { get; } = fixture; - public string OutputDir { get; } = outputDir; + public Fixtures Fixture { get; } = fixture; + public string OutputDir { get; } = outputDir; - private int indentLevel = 0; + 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); - writer.NewLine = "\n"; // Use Unix line endings for cross-platform compatibility + public void WriteFile() + { + var outputPath = Path.Combine(OutputDir, Fixture.FileName ?? throw new InvalidOperationException("Fixture FileName is not set")); - WriteHeader(writer); - WriteLine(writer); - WriteLine(writer); + Directory.CreateDirectory(OutputDir); - WriteUsings(writer); - WriteLine(writer); - WriteLine(writer); + using var writer = new StreamWriter(outputPath, false); + writer.NewLine = "\n"; // Use Unix line endings for cross-platform compatibility - WriteNamespace(writer, Fixture.Category); - WriteLine(writer); - WriteLine(writer); + WriteHeader(writer); + WriteLine(writer); + WriteLine(writer); - WriteLine(writer, $"[Trait(\"Category\", \"{Fixture.Category}\")]"); - WriteLine(writer, "public class " + FormatClassName(outputPath)); - WriteLine(writer, "{"); + WriteUsings(writer); + WriteLine(writer); + WriteLine(writer); - Indent(); + WriteNamespace(writer, Fixture.Category); + WriteLine(writer); + WriteLine(writer); - // Write test methods here - foreach (var testCase in Fixture.Tests) - { - WriteTestMethod(writer, testCase); - } + WriteLine(writer, $"[Trait(\"Category\", \"{Fixture.Category}\")]"); + WriteLine(writer, "public class " + FormatClassName(outputPath)); + WriteLine(writer, "{"); - Unindent(); - WriteLine(writer, "}"); - } + Indent(); - private string FormatClassName(string filePath) - { - var fileName = Path.GetFileNameWithoutExtension(filePath); - if (fileName == null) return string.Empty; + // Write test methods here + foreach (var testCase in Fixture.Tests) + { + WriteTestMethod(writer, testCase); + } - return StripIllegalCharacters(fileName); - } + Unindent(); + WriteLine(writer, "}"); + } - private string FormatMethodName(string methodName) - { - return StripIllegalCharacters(methodName.ToPascalCase()); - } + private string FormatClassName(string filePath) + { + var fileName = Path.GetFileNameWithoutExtension(filePath); + if (fileName == null) return string.Empty; - private string StripIllegalCharacters(string input) - { - return new Regex(@"[\(_\-/\:\)=,+]").Replace(input, "")!; - } + return StripIllegalCharacters(fileName); + } - 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, "{"); + private string FormatMethodName(string methodName) + { + return StripIllegalCharacters(methodName.ToPascalCase()); + } - Indent(); + private string StripIllegalCharacters(string input) + { + return new Regex(@"[\(_\-/\:\)=,+]").Replace(input, "")!; + } - // Arrange - WriteLineIndented(writer, "// Arrange"); - switch (testCase) + private void WriteTestMethod(StreamWriter writer, TTestCase testCase) { - case EncodeTestCase encodeTestCase: - WriteLineIndented(writer, "var input ="); + WriteLineIndented(writer, "[Fact]"); + WriteLineIndented(writer, $"[Trait(\"Description\", \"{testCase.Name}\")]"); + WriteLineIndented(writer, $"public void {FormatMethodName(testCase.Name)}()"); + WriteLineIndented(writer, "{"); Indent(); - WriteJsonNodeAsAnonymousType(writer, encodeTestCase.Input); - Unindent(); - WriteLine(writer); - - WriteLineIndented(writer, "var expected ="); - WriteLine(writer, "\"\"\""); - Write(writer, NormalizeLineEndings(encodeTestCase.Expected)); - WriteLine(writer); - WriteLine(writer, "\"\"\";"); - - break; - - case DecodeTestCase decodeTestCase: - - WriteLineIndented(writer, "var input ="); - WriteLine(writer, "\"\"\""); - Write(writer, NormalizeLineEndings(decodeTestCase.Input)); - WriteLine(writer); - WriteLine(writer, "\"\"\";"); + // Arrange + WriteLineIndented(writer, "// Arrange"); + switch (testCase) + { + case EncodeTestCase encodeTestCase: + WriteLineIndented(writer, "var input ="); - break; - default: - WriteLineIndented(writer, $"var input = /* {typeof(TIn).Name} */; // TODO: Initialize input"); - break; - } + Indent(); + WriteJsonNodeAsAnonymousType(writer, encodeTestCase.Input); + Unindent(); + WriteLine(writer); - WriteLine(writer); + WriteLineIndented(writer, "var expected ="); + WriteLine(writer, "\"\"\""); + Write(writer, NormalizeLineEndings(encodeTestCase.Expected)); + WriteLine(writer); + 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(); + break; - if (encodeTestCase.Options?.Delimiter != null) - WriteLineIndented(writer, $"Delimiter = {GetToonDelimiterEnumFromChar(encodeTestCase.Options.Delimiter)},"); + case DecodeTestCase decodeTestCase: - if (encodeTestCase.Options?.Indent != null) - WriteLineIndented(writer, $"Indent = {encodeTestCase.Options.Indent},"); + WriteLineIndented(writer, "var input ="); + WriteLine(writer, "\"\"\""); + Write(writer, NormalizeLineEndings(decodeTestCase.Input)); + WriteLine(writer); + WriteLine(writer, "\"\"\";"); - if (encodeTestCase.Options?.KeyFolding != null) - WriteLineIndented(writer, $"KeyFolding = {GetToonKeyFoldingEnumFromString(encodeTestCase.Options.KeyFolding)},"); + break; + default: + WriteLineIndented(writer, $"var input = /* {typeof(TIn).Name} */; // TODO: Initialize input"); + break; + } - if (encodeTestCase.Options?.FlattenDepth != null) - WriteLineIndented(writer, $"FlattenDepth = {encodeTestCase.Options.FlattenDepth},"); - Unindent(); - WriteLineIndented(writer, "};"); + WriteLine(writer); - WriteLine(writer); - WriteLineIndented(writer, $"var result = ToonEncoder.Encode(input, options);"); - } - else + // Act & Assert + WriteLineIndented(writer, "// Act & Assert"); + switch (testCase) { - WriteLineIndented(writer, $"var result = ToonEncoder.Encode(input);"); + case EncodeTestCase encodeTestCase: + var hasEncodeOptions = encodeTestCase.Options != null; + if (hasEncodeOptions) + { + WriteLineIndented(writer, "var options = new ToonEncodeOptions"); + WriteLineIndented(writer, "{"); + Indent(); + + if (encodeTestCase.Options?.Delimiter != null) + WriteLineIndented(writer, $"Delimiter = {GetToonDelimiterEnumFromChar(encodeTestCase.Options.Delimiter)},"); + + if (encodeTestCase.Options?.Indent != null) + WriteLineIndented(writer, $"Indent = {encodeTestCase.Options.Indent},"); + + if (encodeTestCase.Options?.KeyFolding != null) + WriteLineIndented(writer, $"KeyFolding = {GetToonKeyFoldingEnumFromString(encodeTestCase.Options.KeyFolding)},"); + + if (encodeTestCase.Options?.FlattenDepth != null) + WriteLineIndented(writer, $"FlattenDepth = {encodeTestCase.Options.FlattenDepth},"); + + 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()},"); + + if (decodeTestCase.Options?.ExpandPaths != null) + WriteLineIndented(writer, $"ExpandPaths = {GetToonPathExpansionEnumFromString(decodeTestCase.Options.ExpandPaths)}"); + + + Unindent(); + WriteLineIndented(writer, "};"); + + WriteLine(writer); + } + + if (decodeTestCase.ShouldError) + { + // Determine which exception type to expect based on the options + var isPathExpansionError = decodeTestCase.Options?.ExpandPaths != null; + var exceptionType = isPathExpansionError ? "ToonPathExpansionException" : "ToonFormatException"; + + if (hasDecodeOptions) + { + WriteLineIndented(writer, $"Assert.Throws<{exceptionType}>(() => ToonDecoder.Decode(input, options));"); + } + else + { + WriteLineIndented(writer, $"Assert.Throws<{exceptionType}>(() => 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); - WriteLineIndented(writer, $"Assert.Equal(expected, result);"); - break; + } - case DecodeTestCase decodeTestCase: - var hasDecodeOptions = decodeTestCase.Options != null; - if (hasDecodeOptions) + private static string GetToonDelimiterEnumFromChar(string? delimiter) + { + return delimiter switch { - WriteLineIndented(writer, "var options = new ToonDecodeOptions"); - WriteLineIndented(writer, "{"); - Indent(); + "," => "ToonDelimiter.COMMA", + "\t" => "ToonDelimiter.TAB", + "|" => "ToonDelimiter.PIPE", + _ => "ToonDelimiter.COMMA" + }; + } + + private static string GetToonKeyFoldingEnumFromString(string? keyFoldingOption) + { + return keyFoldingOption switch + { + "off" => "ToonKeyFolding.Off", + "safe" => "ToonKeyFolding.Safe", + _ => "ToonKeyFolding.Off" + }; + } - WriteLineIndented(writer, $"Indent = {decodeTestCase.Options?.Indent ?? 2},"); - WriteLineIndented(writer, $"Strict = {(decodeTestCase.Options?.Strict ?? true).ToString().ToLower()},"); + private static string GetToonPathExpansionEnumFromString(string? expandPathsOption) + { + return expandPathsOption switch + { + "off" => "ToonPathExpansion.Off", + "safe" => "ToonPathExpansion.Safe", + _ => "ToonPathExpansion.Safe" + }; + } - if (decodeTestCase.Options?.ExpandPaths != null) - WriteLineIndented(writer, $"ExpandPaths = {GetToonPathExpansionEnumFromString(decodeTestCase.Options.ExpandPaths)}"); + private void WriteJsonNodeAsAnonymousType(StreamWriter writer, JsonNode node) + { + WriteJsonNode(writer, node); + WriteLineIndented(writer, ";"); + } - Unindent(); - WriteLineIndented(writer, "};"); + private void WriteJsonNode(StreamWriter writer, JsonNode? node) + { + var propertyName = node?.Parent is JsonObject ? node?.GetPropertyName() : null; - WriteLine(writer); + void WriteFunc(string value) + { + if (propertyName is not null && node.Parent is not JsonArray) + { + Write(writer, value); + } + else + { + WriteIndented(writer, value); + } } - if (decodeTestCase.ShouldError) + if (node is null) { - // Determine which exception type to expect based on the options - var isPathExpansionError = decodeTestCase.Options?.ExpandPaths != null; - var exceptionType = isPathExpansionError ? "ToonPathExpansionException" : "ToonFormatException"; - - if (hasDecodeOptions) - { - WriteLineIndented(writer, $"Assert.Throws<{exceptionType}>(() => ToonDecoder.Decode(input, options));"); - } - else - { - WriteLineIndented(writer, $"Assert.Throws<{exceptionType}>(() => ToonDecoder.Decode(input));"); - } + WriteIndented(writer, "(string)null"); } - else + else if (node is JsonValue nodeValue) { - 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 (propertyName is not null) { - 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, ""); - } + WriteIndented(writer, $"@{propertyName} = "); } - } - 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) + + var kind = nodeValue.GetValueKind(); + if (kind == JsonValueKind.String) { - WriteLineIndented(writer, $"var expected = string.Empty;"); + WriteFunc($"@\"{nodeValue.GetValue().Replace("\"", "\"\"")}\""); } else { - WriteLineIndented(writer, $"var expected = JsonNode.Parse(\"\"\"\n{value}\n\"\"\");"); + 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()}"); + } } - WriteLine(writer); - WriteLineIndented(writer, $"Assert.True(JsonNode.DeepEquals(result, expected));"); - } + if (propertyName is not null) + { + WriteLine(writer, ","); + } } - break; + else if (node is JsonObject nodeObject) + { + if (propertyName is not null) + { + WriteLineIndented(writer, $"@{propertyName} ="); + } - default: - WriteLineIndented(writer, "// TODO: Implement test logic"); - break; - } + WriteLineIndented(writer, "new"); + WriteLineIndented(writer, "{"); + Indent(); - Unindent(); - WriteLineIndented(writer, "}"); - WriteLine(writer); - } + foreach (var property in nodeObject) + { + if (property.Value is null) + { + WriteFunc($"@{property.Key} = (string)null,"); + } + else + { + WriteJsonNode(writer, property.Value); + } + } - private static string GetToonDelimiterEnumFromChar(string? delimiter) - { - return delimiter switch - { - "," => "ToonDelimiter.COMMA", - "\t" => "ToonDelimiter.TAB", - "|" => "ToonDelimiter.PIPE", - _ => "ToonDelimiter.COMMA" - }; - } - - private static string GetToonKeyFoldingEnumFromString(string? keyFoldingOption) - { - return keyFoldingOption switch - { - "off" => "ToonKeyFolding.Off", - "safe" => "ToonKeyFolding.Safe", - _ => "ToonKeyFolding.Off" - }; - } - - private static string GetToonPathExpansionEnumFromString(string? expandPathsOption) - { - return expandPathsOption switch - { - "off" => "ToonPathExpansion.Off", - "safe" => "ToonPathExpansion.Safe", - _ => "ToonPathExpansion.Safe" - }; - } + Unindent(); + WriteLineIndented(writer, "}"); + + if (propertyName is not null) + { + WriteLine(writer, ","); + } + } + else if (node is JsonArray nodeArray) + { + if (!string.IsNullOrEmpty(propertyName)) + { + WriteIndented(writer, $"@{propertyName} ="); + } - private void WriteJsonNodeAsAnonymousType(StreamWriter writer, JsonNode node) - { - WriteJsonNode(writer, node); + WriteFunc("new object[] {"); - WriteLineIndented(writer, ";"); - } + WriteLineIndented(writer); + Indent(); - private void WriteJsonNode(StreamWriter writer, JsonNode? node) - { - var propertyName = node?.Parent is JsonObject ? node?.GetPropertyName() : null; + foreach (var item in nodeArray) + { + WriteJsonNode(writer, item); + + if (item is JsonValue) + { + WriteLine(writer, ","); + } + else + { + WriteLineIndented(writer, ","); + } + } - void WriteFunc(string value) - { - if (propertyName is not null && node.Parent is not JsonArray) - { - Write(writer, value); - } - else - { - WriteIndented(writer, value); - } + Unindent(); + WriteLineIndented(writer, "}"); + + if (propertyName is not null) + { + WriteLine(writer, ","); + } + } } - if (node is null) + private void Indent() { - WriteIndented(writer, "(string)null"); + indentLevel++; } - 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()}"); - } - } + private void Unindent() + { + indentLevel--; + } - if (propertyName is not null) - { - WriteLine(writer, ","); - } + private void WriteLineIndented(StreamWriter writer, string line) + { + writer.WriteLine(new string(' ', indentLevel * 4) + line); } - else if (node is JsonObject nodeObject) + + private void WriteLineIndented(StreamWriter writer) { - 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); - } - } + WriteLineIndented(writer, ""); + } - Unindent(); - WriteLineIndented(writer, "}"); + private void WriteIndented(StreamWriter writer, string content) + { + writer.Write(new string(' ', indentLevel * 4) + content); + } - if (propertyName is not null) - { - WriteLine(writer, ","); - } + private void WriteIndented(StreamWriter writer) + { + WriteIndented(writer, ""); } - else if (node is JsonArray nodeArray) + + private void WriteHeader(StreamWriter writer) { - if (!string.IsNullOrEmpty(propertyName)) - { - WriteIndented(writer, $"@{propertyName} ="); - } + 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, "// "); + } - WriteFunc("new object[] {"); + 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;"); + } - WriteLineIndented(writer); - Indent(); + private void WriteNamespace(StreamWriter writer, string category) + { + WriteLine(writer, $"namespace ToonFormat.Tests.{category.ToPascalCase()};"); + } - foreach (var item in nodeArray) - { - WriteJsonNode(writer, item); + private void WriteLine(StreamWriter writer) + { + writer.WriteLine(); + } - if (item is JsonValue) - { - WriteLine(writer, ","); - } - else - { - WriteLineIndented(writer, ","); - } - } + private void WriteLine(StreamWriter writer, string line) + { + writer.WriteLine(line); + } - Unindent(); - WriteLineIndented(writer, "}"); + private void Write(StreamWriter writer, string contents) + { + writer.Write(contents); + } - if (propertyName is not null) - { - WriteLine(writer, ","); - } + /// + /// Normalizes line endings to Unix format (LF) for cross-platform compatibility. + /// + private static string NormalizeLineEndings(string text) + { + return text.Replace("\r\n", "\n"); } - } - - 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); - } - - /// - /// Normalizes line endings to Unix format (LF) for cross-platform compatibility. - /// - private static string NormalizeLineEndings(string text) - { - return text.Replace("\r\n", "\n"); - } } diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs b/tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs index 8cd92be..5fb5ae5 100644 --- a/tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs +++ b/tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Nodes; using Toon.Format; diff --git a/tests/ToonFormat.Tests/ToonEncoderTests.cs b/tests/ToonFormat.Tests/ToonEncoderTests.cs index 8f20d59..9285433 100644 --- a/tests/ToonFormat.Tests/ToonEncoderTests.cs +++ b/tests/ToonFormat.Tests/ToonEncoderTests.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Nodes; using Toon.Format; From 843c3d980c298b72e9465982d34fb1da8760e877 Mon Sep 17 00:00:00 2001 From: Daniel Destouche Date: Wed, 24 Dec 2025 00:17:10 -0500 Subject: [PATCH 10/20] feat: Ensure numeric values are fully represented --- src/ToonFormat/Internal/Decode/Parser.cs | 10 +++- .../Internal/Shared/NumericUtils.cs | 48 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 src/ToonFormat/Internal/Shared/NumericUtils.cs diff --git a/src/ToonFormat/Internal/Decode/Parser.cs b/src/ToonFormat/Internal/Decode/Parser.cs index dbcc15c..52e7941 100644 --- a/src/ToonFormat/Internal/Decode/Parser.cs +++ b/src/ToonFormat/Internal/Decode/Parser.cs @@ -277,6 +277,11 @@ public static List ParseDelimitedValues(string input, char delimiter) { var parsedNumber = double.Parse(trimmed, CultureInfo.InvariantCulture); parsedNumber = FloatUtils.NormalizeSignedZero(parsedNumber); + if (parsedNumber < 1e-6 || parsedNumber > 1e6) + { + return JsonValue.Create(NumericUtils.EmitCanonicalDecimalForm(parsedNumber)); + } + return JsonValue.Create(parsedNumber); } @@ -362,6 +367,7 @@ public static KeyParseResult ParseQuotedKey(string content, int start) { throw ToonFormatException.Syntax("Missing colon after key"); } + end++; return new KeyParseResult { Key = key, End = end, WasQuoted = true }; @@ -392,7 +398,7 @@ public static KeyParseResult ParseKeyToken(string content, int start) public static bool IsArrayHeaderAfterHyphen(string content) { return content.Trim().StartsWith(Constants.OPEN_BRACKET.ToString()) - && StringUtils.FindUnquotedChar(content, Constants.COLON) != -1; + && StringUtils.FindUnquotedChar(content, Constants.COLON) != -1; } /// @@ -405,4 +411,4 @@ public static bool IsObjectFirstFieldAfterHyphen(string content) // #endregion } -} +} \ No newline at end of file diff --git a/src/ToonFormat/Internal/Shared/NumericUtils.cs b/src/ToonFormat/Internal/Shared/NumericUtils.cs new file mode 100644 index 0000000..79fdfa8 --- /dev/null +++ b/src/ToonFormat/Internal/Shared/NumericUtils.cs @@ -0,0 +1,48 @@ +using System.Text.RegularExpressions; + +namespace ToonFormat.Internal.Shared +{ + internal static class NumericUtils + { + /// + /// Converts a double to a decimal in canonical form for accurate representation. + /// + /// The input double value + /// A decimal representation of the input value. + /// https://github.com/toon-format/spec/blob/main/SPEC.md#2-data-model + /// 1e-7 => 0.0000001 + public static decimal EmitCanonicalDecimalForm(double value) + { + var scientificString = value.ToString("G17"); + var match = Regex.Match(scientificString, @"e[-+]\d+", RegexOptions.IgnoreCase); + + if (!match.Success) return (decimal)value; + + // The match is the exponent part, e.g., "E+04" + var exponentPart = match.Value; + + // Remove the 'E' or 'e' and the sign to get just the digits + var exponentDigits = exponentPart.Substring(2); + + // Parse the actual exponent value (4 in this example) + var exponent = int.Parse(exponentDigits); + + // You also need to check the sign to determine if it's positive or negative + if (exponentPart.Contains('-')) + { + exponent = -exponent; + } + + var mantissa = + scientificString.Substring(0, scientificString.IndexOf(match.Value, StringComparison.Ordinal)); + + var decimalValue = decimal.Parse(mantissa); + + if (exponent == 0) exponent++; + + decimalValue *= (decimal)Math.Pow(10, exponent); + + return decimalValue; + } + } +} \ No newline at end of file From 7bb530505e8eb3e5c457a49b4dea5a8400d84575 Mon Sep 17 00:00:00 2001 From: Daniel Destouche Date: Wed, 24 Dec 2025 00:18:24 -0500 Subject: [PATCH 11/20] feat: Remove LengthMarker references --- README.md | 1 - src/ToonFormat/Internal/Encode/Encoders.cs | 4 ++-- .../ManualTests/ToonEncoderTests.cs | 17 +---------------- 3 files changed, 3 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 28dd06f..a4ecd45 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,6 @@ Converts any .NET object to TOON format. - `Indent` – Number of spaces per indentation level (default: `2`) - `Delimiter` – Delimiter for array values: `ToonDelimiter.COMMA` (default), `TAB`, or `PIPE` - `KeyFolding` – Collapse nested single-key objects: `ToonKeyFolding.Off` or `Safe` (default: `Off`) - - `LengthMarker` – Prefix array lengths with `#` (default: `false`) **Returns:** diff --git a/src/ToonFormat/Internal/Encode/Encoders.cs b/src/ToonFormat/Internal/Encode/Encoders.cs index 4507529..47844fd 100644 --- a/src/ToonFormat/Internal/Encode/Encoders.cs +++ b/src/ToonFormat/Internal/Encode/Encoders.cs @@ -532,7 +532,7 @@ private static void EncodeListItemValue( var header = ExtractTabularHeader(objects); if (header != null) { - var formattedHeader = Primitives.FormatHeader(arr.Count, null, header, options.Delimiter, options.LengthMarker); + var formattedHeader = Primitives.FormatHeader(arr.Count, null, header, options.Delimiter); writer.PushListItem(depth, formattedHeader); WriteTabularRows(objects, header, writer, depth + 2, options); return; @@ -540,7 +540,7 @@ private static void EncodeListItemValue( } // Fallback for non-tabular or mixed - var headerStr = Primitives.FormatHeader(arr.Count, null, null, options.Delimiter, options.LengthMarker); + var headerStr = Primitives.FormatHeader(arr.Count, null, null, options.Delimiter); writer.PushListItem(depth, headerStr); foreach (var item in arr) diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs b/tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs index 5fb5ae5..8f20d59 100644 --- a/tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs +++ b/tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Nodes; using Toon.Format; @@ -111,21 +111,6 @@ public void Encode_WithCustomDelimiter_UsesCorrectDelimiter() Assert.Contains("numbers[", result); } - [Fact] - public void Encode_WithLengthMarker_IncludesHashSymbol() - { - // Arrange - var data = new { items = new[] { 1, 2, 3 } }; - var options = new ToonEncodeOptions { LengthMarker = true }; - - // Act - var result = ToonEncoder.Encode(data, options); - - // Assert - Assert.NotNull(result); - Assert.Contains("[#", result); - } - [Fact] public void Encode_NestedStructures_ReturnsValidToon() { From 5358db671aab3493a3ce871161d99b76bd12b03e Mon Sep 17 00:00:00 2001 From: Daniel Destouche Date: Wed, 24 Dec 2025 00:20:11 -0500 Subject: [PATCH 12/20] chore: Remove unused converters --- .../DoubleNamedFloatToNullConverter.cs | 27 ------------------- .../SingleNamedFloatToNullConverter.cs | 24 ----------------- 2 files changed, 51 deletions(-) delete mode 100644 src/ToonFormat/Internal/Converters/DoubleNamedFloatToNullConverter.cs delete mode 100644 src/ToonFormat/Internal/Converters/SingleNamedFloatToNullConverter.cs diff --git a/src/ToonFormat/Internal/Converters/DoubleNamedFloatToNullConverter.cs b/src/ToonFormat/Internal/Converters/DoubleNamedFloatToNullConverter.cs deleted file mode 100644 index 7e0c6a5..0000000 --- a/src/ToonFormat/Internal/Converters/DoubleNamedFloatToNullConverter.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace ToonFormat.Internal.Converters -{ - /// - /// Normalizes double NaN/Infinity to null when writing JSON, keeping original numeric precision otherwise. - /// Reading still uses default handling, no special conversion. - /// Purpose: Consistent with TS spec (NaN/±Infinity -> null), and provides stable JsonElement for subsequent TOON encoding phase. - /// - internal sealed class DoubleNamedFloatToNullConverter : JsonConverter - { - public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => reader.GetDouble(); - - public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options) - { - if (double.IsNaN(value) || double.IsInfinity(value)) - { - writer.WriteNullValue(); - return; - } - writer.WriteNumberValue(value); - } - } -} diff --git a/src/ToonFormat/Internal/Converters/SingleNamedFloatToNullConverter.cs b/src/ToonFormat/Internal/Converters/SingleNamedFloatToNullConverter.cs deleted file mode 100644 index abd1eff..0000000 --- a/src/ToonFormat/Internal/Converters/SingleNamedFloatToNullConverter.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace ToonFormat.Internal.Converters -{ - /// - /// Normalizes float NaN/Infinity to null when writing JSON; reading keeps default behavior. - /// - internal sealed class SingleNamedFloatToNullConverter : JsonConverter - { - public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => reader.GetSingle(); - - public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options) - { - if (float.IsNaN(value) || float.IsInfinity(value)) - { - writer.WriteNullValue(); - return; - } - writer.WriteNumberValue(value); - } - } -} From b01d4680850d3d5f4bae0b0ce55641817afdeecf Mon Sep 17 00:00:00 2001 From: Daniel Destouche Date: Wed, 24 Dec 2025 00:23:31 -0500 Subject: [PATCH 13/20] chore: Test cleanup --- .../ManualTests/ToonAsyncTests.cs | 262 ++++++++++++++++++ tests/ToonFormat.Tests/ToonEncoderTests.cs | 2 +- 2 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 tests/ToonFormat.SpecGenerator/ManualTests/ToonAsyncTests.cs diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/ToonAsyncTests.cs b/tests/ToonFormat.SpecGenerator/ManualTests/ToonAsyncTests.cs new file mode 100644 index 0000000..b6db96e --- /dev/null +++ b/tests/ToonFormat.SpecGenerator/ManualTests/ToonAsyncTests.cs @@ -0,0 +1,262 @@ +#nullable enable +using System; +using System.IO; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using Toon.Format; +using Xunit; + +namespace ToonFormat.Tests; + +/// +/// Tests for async encoding and decoding methods. +/// +public class ToonAsyncTests +{ + #region EncodeAsync Tests + + [Fact] + public async Task EncodeAsync_WithSimpleObject_ReturnsValidToon() + { + // Arrange + var data = new { name = "Alice", age = 30 }; + + // Act + var result = await ToonEncoder.EncodeAsync(data); + + // Assert + Assert.NotNull(result); + Assert.Contains("name:", result); + Assert.Contains("Alice", result); + Assert.Contains("age:", result); + Assert.Contains("30", result); + } + + [Fact] + public async Task EncodeAsync_WithOptions_RespectsIndentation() + { + // Arrange + var data = new { outer = new { inner = "value" } }; + var options = new ToonEncodeOptions { Indent = 4 }; + + // Act + var result = await ToonEncoder.EncodeAsync(data, options); + + // Assert + Assert.NotNull(result); + Assert.Contains("outer:", result); + } + + [Fact] + public async Task EncodeAsync_WithCancellationToken_ThrowsWhenCancelled() + { + // Arrange + var data = new { name = "Test" }; + var cts = new CancellationTokenSource(); + cts.Cancel(); + + // Act & Assert + await Assert.ThrowsAsync( + () => ToonEncoder.EncodeAsync(data, cts.Token)); + } + + [Fact] + public async Task EncodeToBytesAsync_WithSimpleObject_ReturnsUtf8Bytes() + { + // Arrange + var data = new { message = "Hello" }; + + // Act + var result = await ToonEncoder.EncodeToBytesAsync(data); + + // Assert + Assert.NotNull(result); + var text = Encoding.UTF8.GetString(result); + Assert.Contains("message:", text); + Assert.Contains("Hello", text); + } + + [Fact] + public async Task EncodeToStreamAsync_WritesToStream() + { + // Arrange + var data = new { id = 123 }; + using var stream = new MemoryStream(); + + // Act + await ToonEncoder.EncodeToStreamAsync(data, stream); + + // Assert + stream.Position = 0; + using var reader = new StreamReader(stream); + var result = await reader.ReadToEndAsync(); + Assert.Contains("id:", result); + Assert.Contains("123", result); + } + + [Fact] + public async Task EncodeToStreamAsync_WithNullStream_ThrowsArgumentNullException() + { + // Arrange + var data = new { name = "Test" }; + + // Act & Assert + await Assert.ThrowsAsync( + () => ToonEncoder.EncodeToStreamAsync(data, null!)); + } + + #endregion + + #region DecodeAsync Tests + + [Fact] + public async Task DecodeAsync_WithValidToon_ReturnsJsonNode() + { + // Arrange + var toon = "name: Alice\nage: 30"; + + // Act + var result = await ToonDecoder.DecodeAsync(toon); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + var obj = (JsonObject)result; + Assert.Equal("Alice", obj["name"]?.GetValue()); + Assert.Equal(30.0, obj["age"]?.GetValue()); + } + + [Fact] + public async Task DecodeAsync_Generic_DeserializesToType() + { + // Arrange + var toon = "name: Bob\nage: 25"; + + // Act + var result = await ToonDecoder.DecodeAsync(toon); + + // Assert + Assert.NotNull(result); + Assert.Equal("Bob", result.name); + Assert.Equal(25, result.age); + } + + [Fact] + public async Task DecodeAsync_WithCancellationToken_ThrowsWhenCancelled() + { + // Arrange + var toon = "name: Test"; + var cts = new CancellationTokenSource(); + cts.Cancel(); + + // Act & Assert + await Assert.ThrowsAsync( + () => ToonDecoder.DecodeAsync(toon, cts.Token)); + } + + [Fact] + public async Task DecodeAsync_FromStream_ReturnsJsonNode() + { + // Arrange + var toon = "message: Hello World"; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(toon)); + + // Act + var result = await ToonDecoder.DecodeAsync(stream); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + var obj = (JsonObject)result; + Assert.Equal("Hello World", obj["message"]?.GetValue()); + } + + [Fact] + public async Task DecodeAsync_Generic_FromStream_DeserializesToType() + { + // Arrange + var toon = "name: Charlie\nage: 35"; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(toon)); + + // Act + var result = await ToonDecoder.DecodeAsync(stream); + + // Assert + Assert.NotNull(result); + Assert.Equal("Charlie", result.name); + Assert.Equal(35, result.age); + } + + [Fact] + public async Task DecodeAsync_FromStream_WithNullStream_ThrowsArgumentNullException() + { + // Act & Assert + await Assert.ThrowsAsync( + () => ToonDecoder.DecodeAsync((Stream)null!)); + } + + [Fact] + public async Task DecodeAsync_WithOptions_RespectsStrictMode() + { + // Arrange - array declares 5 items but only provides 3, strict mode should throw + var toon = "numbers[5]: 1, 2, 3"; + var options = new ToonDecodeOptions { Strict = true }; + + // Act & Assert + await Assert.ThrowsAsync( + () => ToonDecoder.DecodeAsync(toon, options)); + } + + #endregion + + #region Round-Trip Async Tests + + [Fact] + public async Task AsyncRoundTrip_PreservesData() + { + // Arrange + var original = new TestPerson { name = "Diana", age = 28 }; + + // Act + var encoded = await ToonEncoder.EncodeAsync(original); + var decoded = await ToonDecoder.DecodeAsync(encoded); + + // Assert + Assert.NotNull(decoded); + Assert.Equal(original.name, decoded.name); + Assert.Equal(original.age, decoded.age); + } + + [Fact] + public async Task AsyncStreamRoundTrip_PreservesData() + { + // Arrange + var original = new TestPerson { name = "Eve", age = 32 }; + using var stream = new MemoryStream(); + + // Act + await ToonEncoder.EncodeToStreamAsync(original, stream); + stream.Position = 0; + var decoded = await ToonDecoder.DecodeAsync(stream); + + // Assert + Assert.NotNull(decoded); + Assert.Equal(original.name, decoded.name); + Assert.Equal(original.age, decoded.age); + } + + #endregion + + #region Test Helpers + + private class TestPerson + { + public string? name { get; set; } + public int age { get; set; } + } + + #endregion +} + diff --git a/tests/ToonFormat.Tests/ToonEncoderTests.cs b/tests/ToonFormat.Tests/ToonEncoderTests.cs index 9285433..8f20d59 100644 --- a/tests/ToonFormat.Tests/ToonEncoderTests.cs +++ b/tests/ToonFormat.Tests/ToonEncoderTests.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Nodes; using Toon.Format; From b1a35019465ac7a24d43b9b8b4981308f16af878 Mon Sep 17 00:00:00 2001 From: Daniel Destouche Date: Wed, 24 Dec 2025 00:30:50 -0500 Subject: [PATCH 14/20] chore: Change test project structure, manual tests/specs are no longer copied from the SpecGen project --- specgen.ps1 | 2 +- specgen.sh | 2 +- .../ToonFormat.SpecGenerator/SpecGenerator.cs | 23 - .../Encode/ArraysObjectsManual.cs | 137 ----- .../Decode/ArraysNested.cs | 0 .../Decode/ArraysPrimitive.cs | 0 .../Decode/ArraysTabular.cs | 0 .../{ => GeneratedTests}/Decode/BlankLines.cs | 0 .../{ => GeneratedTests}/Decode/Delimiters.cs | 0 .../Decode/IndentationErrors.cs | 0 .../{ => GeneratedTests}/Decode/Numbers.cs | 0 .../{ => GeneratedTests}/Decode/Objects.cs | 0 .../Decode/PathExpansion.cs | 0 .../{ => GeneratedTests}/Decode/Primitives.cs | 0 .../{ => GeneratedTests}/Decode/RootForm.cs | 0 .../Decode/ValidationErrors.cs | 0 .../{ => GeneratedTests}/Decode/Whitespace.cs | 0 .../Encode/ArraysNested.cs | 0 .../Encode/ArraysObjects.cs | 0 .../Encode/ArraysPrimitive.cs | 0 .../Encode/ArraysTabular.cs | 0 .../{ => GeneratedTests}/Encode/Delimiters.cs | 0 .../{ => GeneratedTests}/Encode/KeyFolding.cs | 0 .../{ => GeneratedTests}/Encode/Objects.cs | 0 .../{ => GeneratedTests}/Encode/Primitives.cs | 0 .../{ => GeneratedTests}/Encode/Whitespace.cs | 0 .../JsonComplexRoundTripTests.cs | 255 --------- tests/ToonFormat.Tests/KeyFoldingTests.cs | 501 ------------------ .../ManualTests/Encode/ArraysObjectsManual.cs | 0 .../ManualTests/JsonComplexRoundTripTests.cs | 0 .../ManualTests/KeyFoldingTests.cs | 0 .../ManualTests/PerformanceBenchmark.cs | 0 .../ManualTests/ToonAsyncTests.cs | 0 .../ManualTests/ToonDecoderTests.cs | 0 .../ManualTests/ToonEncoderTests.cs | 0 .../ManualTests/ToonRoundTripTests.cs | 0 .../ToonFormat.Tests/PerformanceBenchmark.cs | 131 ----- tests/ToonFormat.Tests/ToonAsyncTests.cs | 262 --------- tests/ToonFormat.Tests/ToonDecoderTests.cs | 170 ------ tests/ToonFormat.Tests/ToonEncoderTests.cs | 139 ----- tests/ToonFormat.Tests/ToonRoundTripTests.cs | 84 --- 41 files changed, 2 insertions(+), 1704 deletions(-) delete mode 100644 tests/ToonFormat.Tests/Encode/ArraysObjectsManual.cs rename tests/ToonFormat.Tests/{ => GeneratedTests}/Decode/ArraysNested.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Decode/ArraysPrimitive.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Decode/ArraysTabular.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Decode/BlankLines.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Decode/Delimiters.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Decode/IndentationErrors.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Decode/Numbers.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Decode/Objects.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Decode/PathExpansion.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Decode/Primitives.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Decode/RootForm.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Decode/ValidationErrors.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Decode/Whitespace.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Encode/ArraysNested.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Encode/ArraysObjects.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Encode/ArraysPrimitive.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Encode/ArraysTabular.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Encode/Delimiters.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Encode/KeyFolding.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Encode/Objects.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Encode/Primitives.cs (100%) rename tests/ToonFormat.Tests/{ => GeneratedTests}/Encode/Whitespace.cs (100%) delete mode 100644 tests/ToonFormat.Tests/JsonComplexRoundTripTests.cs delete mode 100644 tests/ToonFormat.Tests/KeyFoldingTests.cs rename tests/{ToonFormat.SpecGenerator => ToonFormat.Tests}/ManualTests/Encode/ArraysObjectsManual.cs (100%) rename tests/{ToonFormat.SpecGenerator => ToonFormat.Tests}/ManualTests/JsonComplexRoundTripTests.cs (100%) rename tests/{ToonFormat.SpecGenerator => ToonFormat.Tests}/ManualTests/KeyFoldingTests.cs (100%) rename tests/{ToonFormat.SpecGenerator => ToonFormat.Tests}/ManualTests/PerformanceBenchmark.cs (100%) rename tests/{ToonFormat.SpecGenerator => ToonFormat.Tests}/ManualTests/ToonAsyncTests.cs (100%) rename tests/{ToonFormat.SpecGenerator => ToonFormat.Tests}/ManualTests/ToonDecoderTests.cs (100%) rename tests/{ToonFormat.SpecGenerator => ToonFormat.Tests}/ManualTests/ToonEncoderTests.cs (100%) rename tests/{ToonFormat.SpecGenerator => ToonFormat.Tests}/ManualTests/ToonRoundTripTests.cs (100%) delete mode 100644 tests/ToonFormat.Tests/PerformanceBenchmark.cs delete mode 100644 tests/ToonFormat.Tests/ToonAsyncTests.cs delete mode 100644 tests/ToonFormat.Tests/ToonDecoderTests.cs delete mode 100644 tests/ToonFormat.Tests/ToonEncoderTests.cs delete mode 100644 tests/ToonFormat.Tests/ToonRoundTripTests.cs diff --git a/specgen.ps1 b/specgen.ps1 index 447402a..828a7b4 100644 --- a/specgen.ps1 +++ b/specgen.ps1 @@ -1,6 +1,6 @@ # generate spec $GH_REPO = "https://github.com/toon-format/spec.git" -$OUT_DIR = "./tests/ToonFormat.Tests" +$OUT_DIR = "./tests/ToonFormat.Tests/GeneratedTests" # build and execute spec generator dotnet build tests/ToonFormat.SpecGenerator diff --git a/specgen.sh b/specgen.sh index 8d43e8c..4c2a379 100755 --- a/specgen.sh +++ b/specgen.sh @@ -1,6 +1,6 @@ # generate spec GH_REPO="https://github.com/toon-format/spec.git" -OUT_DIR="./tests/ToonFormat.Tests" +OUT_DIR="./tests/ToonFormat.Tests/GeneratedTests" # build and execute spec generator dotnet build tests/ToonFormat.SpecGenerator diff --git a/tests/ToonFormat.SpecGenerator/SpecGenerator.cs b/tests/ToonFormat.SpecGenerator/SpecGenerator.cs index c647720..88aa675 100644 --- a/tests/ToonFormat.SpecGenerator/SpecGenerator.cs +++ b/tests/ToonFormat.SpecGenerator/SpecGenerator.cs @@ -19,9 +19,6 @@ public void GenerateSpecs(SpecGeneratorOptions options) // Clean up test directory before generating new files CleanTestDirectory(options.AbsoluteOutputPath); - // Copy manual tests from ManualTests folder - CopyManualTests(options.AbsoluteOutputPath); - logger.LogDebug("Cloning repository {RepoUrl} to {CloneDirectory}", options.SpecRepoUrl, toonSpecDir); GitTool.CloneRepository(options.SpecRepoUrl, toonSpecDir, @@ -105,26 +102,6 @@ private void CleanTestDirectory(string testDirectory) logger.LogInformation("Test directory cleanup completed"); } - private void CopyManualTests(string testDirectory) - { - // Get the directory where SpecGenerator assembly is located - var assemblyLocation = AppContext.BaseDirectory; - var manualTestsDir = Path.Combine(assemblyLocation, "ManualTests"); - - if (!Directory.Exists(manualTestsDir)) - { - logger.LogDebug("ManualTests directory not found at {ManualTestsDir}, skipping manual test copy", manualTestsDir); - return; - } - - logger.LogInformation("Copying manual tests from {ManualTestsDir}", manualTestsDir); - - // Copy all files and subdirectories from ManualTests to test directory - CopyDirectory(manualTestsDir, testDirectory); - - logger.LogInformation("Manual tests copied successfully"); - } - private void CopyDirectory(string sourceDir, string destDir) { // Create destination directory if it doesn't exist diff --git a/tests/ToonFormat.Tests/Encode/ArraysObjectsManual.cs b/tests/ToonFormat.Tests/Encode/ArraysObjectsManual.cs deleted file mode 100644 index 929aad7..0000000 --- a/tests/ToonFormat.Tests/Encode/ArraysObjectsManual.cs +++ /dev/null @@ -1,137 +0,0 @@ -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 ArraysObjectsManual -{ - [Fact] - [Trait("Description", "tabular array with multiple sibling fields")] - public void TabularArrayWithMultipleSiblingFields() - { - // Arrange - var input = - new - { - @items = new object[] { - new - { - @users = new object[] { - new { @id = 1, @name = "Ada" }, - new { @id = 2, @name = "Bob" }, - }, - @status = "active", - @count = 2, - @tags = new object[] { "a", "b", "c" } - } - } - }; - - var expected = -""" -items[1]: - - users[2]{id,name}: - 1,Ada - 2,Bob - status: active - count: 2 - tags[3]: a,b,c -"""; - - // Act & Assert - var result = ToonEncoder.Encode(input); - - Assert.Equal(expected, result); - } - - [Fact] - [Trait("Description", "multiple list items with tabular first fields")] - public void MultipleListItemsWithTabularFirstFields() - { - // Arrange - var input = - new - { - @items = new object[] { - new - { - @users = new object[] { - new { @id = 1, @name = "Ada" }, - new { @id = 2, @name = "Bob" }, - }, - @status = "active" - }, - new - { - @users = new object[] { - new { @id = 3, @name = "Charlie" } - }, - @status = "inactive" - } - } - }; - - var expected = -""" -items[2]: - - users[2]{id,name}: - 1,Ada - 2,Bob - status: active - - users[1]{id,name}: - 3,Charlie - status: inactive -"""; - - // Act & Assert - var result = ToonEncoder.Encode(input); - - Assert.Equal(expected, result); - } - - [Fact] - [Trait("Description", "deeply nested list items with tabular first field")] - public void DeeplyNestedListItemsWithTabularFirstField() - { - // Arrange - var input = - new - { - @data = new object[] { - new - { - @items = new object[] { - new - { - @users = new object[] { - new { @id = 1, @name = "Ada" }, - new { @id = 2, @name = "Bob" }, - }, - @status = "active" - } - } - } - } - }; - - var expected = -""" -data[1]: - - items[1]: - - users[2]{id,name}: - 1,Ada - 2,Bob - status: active -"""; - - // Act & Assert - var result = ToonEncoder.Encode(input); - - Assert.Equal(expected, result); - } -} diff --git a/tests/ToonFormat.Tests/Decode/ArraysNested.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/ArraysNested.cs similarity index 100% rename from tests/ToonFormat.Tests/Decode/ArraysNested.cs rename to tests/ToonFormat.Tests/GeneratedTests/Decode/ArraysNested.cs diff --git a/tests/ToonFormat.Tests/Decode/ArraysPrimitive.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/ArraysPrimitive.cs similarity index 100% rename from tests/ToonFormat.Tests/Decode/ArraysPrimitive.cs rename to tests/ToonFormat.Tests/GeneratedTests/Decode/ArraysPrimitive.cs diff --git a/tests/ToonFormat.Tests/Decode/ArraysTabular.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/ArraysTabular.cs similarity index 100% rename from tests/ToonFormat.Tests/Decode/ArraysTabular.cs rename to tests/ToonFormat.Tests/GeneratedTests/Decode/ArraysTabular.cs diff --git a/tests/ToonFormat.Tests/Decode/BlankLines.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/BlankLines.cs similarity index 100% rename from tests/ToonFormat.Tests/Decode/BlankLines.cs rename to tests/ToonFormat.Tests/GeneratedTests/Decode/BlankLines.cs diff --git a/tests/ToonFormat.Tests/Decode/Delimiters.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/Delimiters.cs similarity index 100% rename from tests/ToonFormat.Tests/Decode/Delimiters.cs rename to tests/ToonFormat.Tests/GeneratedTests/Decode/Delimiters.cs diff --git a/tests/ToonFormat.Tests/Decode/IndentationErrors.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/IndentationErrors.cs similarity index 100% rename from tests/ToonFormat.Tests/Decode/IndentationErrors.cs rename to tests/ToonFormat.Tests/GeneratedTests/Decode/IndentationErrors.cs diff --git a/tests/ToonFormat.Tests/Decode/Numbers.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/Numbers.cs similarity index 100% rename from tests/ToonFormat.Tests/Decode/Numbers.cs rename to tests/ToonFormat.Tests/GeneratedTests/Decode/Numbers.cs diff --git a/tests/ToonFormat.Tests/Decode/Objects.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/Objects.cs similarity index 100% rename from tests/ToonFormat.Tests/Decode/Objects.cs rename to tests/ToonFormat.Tests/GeneratedTests/Decode/Objects.cs diff --git a/tests/ToonFormat.Tests/Decode/PathExpansion.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/PathExpansion.cs similarity index 100% rename from tests/ToonFormat.Tests/Decode/PathExpansion.cs rename to tests/ToonFormat.Tests/GeneratedTests/Decode/PathExpansion.cs diff --git a/tests/ToonFormat.Tests/Decode/Primitives.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/Primitives.cs similarity index 100% rename from tests/ToonFormat.Tests/Decode/Primitives.cs rename to tests/ToonFormat.Tests/GeneratedTests/Decode/Primitives.cs diff --git a/tests/ToonFormat.Tests/Decode/RootForm.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/RootForm.cs similarity index 100% rename from tests/ToonFormat.Tests/Decode/RootForm.cs rename to tests/ToonFormat.Tests/GeneratedTests/Decode/RootForm.cs diff --git a/tests/ToonFormat.Tests/Decode/ValidationErrors.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/ValidationErrors.cs similarity index 100% rename from tests/ToonFormat.Tests/Decode/ValidationErrors.cs rename to tests/ToonFormat.Tests/GeneratedTests/Decode/ValidationErrors.cs diff --git a/tests/ToonFormat.Tests/Decode/Whitespace.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/Whitespace.cs similarity index 100% rename from tests/ToonFormat.Tests/Decode/Whitespace.cs rename to tests/ToonFormat.Tests/GeneratedTests/Decode/Whitespace.cs diff --git a/tests/ToonFormat.Tests/Encode/ArraysNested.cs b/tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysNested.cs similarity index 100% rename from tests/ToonFormat.Tests/Encode/ArraysNested.cs rename to tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysNested.cs diff --git a/tests/ToonFormat.Tests/Encode/ArraysObjects.cs b/tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysObjects.cs similarity index 100% rename from tests/ToonFormat.Tests/Encode/ArraysObjects.cs rename to tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysObjects.cs diff --git a/tests/ToonFormat.Tests/Encode/ArraysPrimitive.cs b/tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysPrimitive.cs similarity index 100% rename from tests/ToonFormat.Tests/Encode/ArraysPrimitive.cs rename to tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysPrimitive.cs diff --git a/tests/ToonFormat.Tests/Encode/ArraysTabular.cs b/tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysTabular.cs similarity index 100% rename from tests/ToonFormat.Tests/Encode/ArraysTabular.cs rename to tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysTabular.cs diff --git a/tests/ToonFormat.Tests/Encode/Delimiters.cs b/tests/ToonFormat.Tests/GeneratedTests/Encode/Delimiters.cs similarity index 100% rename from tests/ToonFormat.Tests/Encode/Delimiters.cs rename to tests/ToonFormat.Tests/GeneratedTests/Encode/Delimiters.cs diff --git a/tests/ToonFormat.Tests/Encode/KeyFolding.cs b/tests/ToonFormat.Tests/GeneratedTests/Encode/KeyFolding.cs similarity index 100% rename from tests/ToonFormat.Tests/Encode/KeyFolding.cs rename to tests/ToonFormat.Tests/GeneratedTests/Encode/KeyFolding.cs diff --git a/tests/ToonFormat.Tests/Encode/Objects.cs b/tests/ToonFormat.Tests/GeneratedTests/Encode/Objects.cs similarity index 100% rename from tests/ToonFormat.Tests/Encode/Objects.cs rename to tests/ToonFormat.Tests/GeneratedTests/Encode/Objects.cs diff --git a/tests/ToonFormat.Tests/Encode/Primitives.cs b/tests/ToonFormat.Tests/GeneratedTests/Encode/Primitives.cs similarity index 100% rename from tests/ToonFormat.Tests/Encode/Primitives.cs rename to tests/ToonFormat.Tests/GeneratedTests/Encode/Primitives.cs diff --git a/tests/ToonFormat.Tests/Encode/Whitespace.cs b/tests/ToonFormat.Tests/GeneratedTests/Encode/Whitespace.cs similarity index 100% rename from tests/ToonFormat.Tests/Encode/Whitespace.cs rename to tests/ToonFormat.Tests/GeneratedTests/Encode/Whitespace.cs diff --git a/tests/ToonFormat.Tests/JsonComplexRoundTripTests.cs b/tests/ToonFormat.Tests/JsonComplexRoundTripTests.cs deleted file mode 100644 index 9080e53..0000000 --- a/tests/ToonFormat.Tests/JsonComplexRoundTripTests.cs +++ /dev/null @@ -1,255 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using System.Text.Json.Nodes; -using Toon.Format; -using Xunit.Abstractions; - -namespace ToonFormat.Tests; - -/// -/// Tests for complex multi-level JSON structures to validate TOON format encoding and decoding. -/// -public class JsonComplexRoundTripTests -{ - private readonly ITestOutputHelper _output; - - public JsonComplexRoundTripTests(ITestOutputHelper output) - { - _output = output; - } - private const string ComplexJson = @"{ - ""project"": { - ""id"": ""PX-4921"", - ""name"": ""Customer Insights Expansion"", - ""description"": ""This is a long descriptive text containing more than fifteen words to simulate a realistic business scenario for testing purposes."", - ""createdAt"": ""2025-11-20T10:32:00Z"", - ""metadata"": { - ""owner"": ""john.doe@example.com"", - ""website"": ""https://example.org/products/insights?ref=test&lang=en"", - ""tags"": [""analysis"", ""insights"", ""growth"", ""R&D""], - ""cost"": { - ""currency"": ""USD"", - ""amount"": 12500.75 - } - }, - ""phases"": [ - { - ""phaseId"": 1, - ""title"": ""Discovery & Research"", - ""deadline"": ""2025-12-15"", - ""status"": ""In Progress"", - ""details"": { - ""notes"": ""Team is conducting interviews, market analysis, and reviewing historical performance metrics & competitors."", - ""specialChars"": ""!@#$%^&*()_+=-{}[]|:;<>,.?/"" - } - }, - { - ""phaseId"": 2, - ""title"": ""Development"", - ""deadline"": ""2026-01-30"", - ""budget"": { - ""currency"": ""EUR"", - ""amount"": 7800.00 - }, - ""resources"": { - ""leadDeveloper"": ""alice.smith@example.com"", - ""repository"": ""https://github.com/example/repo"" - } - } - ] - } -}"; - - [Fact] - public void ComplexJson_RoundTrip_ShouldPreserveKeyFields() - { - // Arrange - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - var root = JsonSerializer.Deserialize(ComplexJson, options); - - // Sanity - Assert.NotNull(root); - Assert.NotNull(root.project); - - // Act - encode to TOON and decode back - var toonText = ToonEncoder.Encode(root); - Assert.NotNull(toonText); - _output.WriteLine("TOON Encoded Output:"); - _output.WriteLine(toonText); - _output.WriteLine("---"); - - var decoded = ToonDecoder.Decode(toonText); - Assert.NotNull(decoded); - - // The encoder reflects C# property names (we used lowercase names to match original JSON keys) - var project = decoded["project"]?.AsObject(); - Assert.NotNull(project); - - // Assert key scalar values - Assert.Equal("PX-4921", project["id"]?.GetValue()); - Assert.Equal("Customer Insights Expansion", project["name"]?.GetValue()); - - // Metadata checks - var metadata = project["metadata"]?.AsObject(); - Assert.NotNull(metadata); - Assert.Equal("john.doe@example.com", metadata["owner"]?.GetValue()); - Assert.Equal("https://example.org/products/insights?ref=test&lang=en", metadata["website"]?.GetValue()); - - // Tags array validation - var tags = metadata["tags"]?.AsArray(); - Assert.NotNull(tags); - Assert.Equal(4, tags.Count); - Assert.Equal("analysis", tags[0]?.GetValue()); - Assert.Equal("insights", tags[1]?.GetValue()); - Assert.Equal("growth", tags[2]?.GetValue()); - Assert.Equal("R&D", tags[3]?.GetValue()); - - var cost = metadata["cost"]?.AsObject(); - Assert.NotNull(cost); - Assert.Equal("USD", cost["currency"]?.GetValue()); - Assert.Equal(12500.75, cost["amount"]?.GetValue()); - - // Phases checks - var phases = project["phases"]?.AsArray(); - Assert.NotNull(phases); - Assert.Equal(2, phases.Count); - - var phase1 = phases[0]?.AsObject(); - Assert.NotNull(phase1); - Assert.Equal(1.0, phase1["phaseId"]?.GetValue()); - var details = phase1["details"]?.AsObject(); - Assert.NotNull(details); - Assert.Contains("market analysis", details["notes"]?.GetValue() ?? string.Empty); - Assert.Equal("!@#$%^&*()_+=-{}[]|:;<>,.?/", details["specialChars"]?.GetValue()); - - var phase2 = phases[1]?.AsObject(); - Assert.NotNull(phase2); - Assert.Equal(2.0, phase2["phaseId"]?.GetValue()); - Assert.Equal("Development", phase2["title"]?.GetValue()); - Assert.Equal("2026-01-30", phase2["deadline"]?.GetValue()); - - var budget = phase2["budget"]?.AsObject(); - Assert.NotNull(budget); - Assert.Equal("EUR", budget["currency"]?.GetValue()); - Assert.Equal(7800.0, budget["amount"]?.GetValue()); - - var resources = phase2["resources"]?.AsObject(); - Assert.NotNull(resources); - Assert.Equal("alice.smith@example.com", resources["leadDeveloper"]?.GetValue()); - Assert.Equal("https://github.com/example/repo", resources["repository"]?.GetValue()); - } - - [Fact] - public void ComplexJson_Encode_ShouldProduceValidToonFormat() - { - // Arrange - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - var root = JsonSerializer.Deserialize(ComplexJson, options); - Assert.NotNull(root); - - // Act - var toonText = ToonEncoder.Encode(root); - - // Assert - verify TOON format structure - Assert.NotNull(toonText); - Assert.Contains("project:", toonText); - Assert.Contains("id:", toonText); - Assert.Contains("PX-4921", toonText); - Assert.Contains("metadata:", toonText); - Assert.Contains("phases[2]", toonText); - - _output.WriteLine("TOON Output:"); - _output.WriteLine(toonText); - } - - [Fact] - public void ComplexJson_SpecialCharacters_ShouldBePreserved() - { - // Arrange - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - var root = JsonSerializer.Deserialize(ComplexJson, options); - Assert.NotNull(root); - - // Act - var toonText = ToonEncoder.Encode(root); - var decoded = ToonDecoder.Decode(toonText); - - // Assert - verify special characters in details.specialChars - Assert.NotNull(decoded); - var project = decoded["project"]?.AsObject(); - var phases = project?["phases"]?.AsArray(); - var phase1 = phases?[0]?.AsObject(); - var details = phase1?["details"]?.AsObject(); - - Assert.NotNull(details); - var specialChars = details["specialChars"]?.GetValue(); - Assert.Equal("!@#$%^&*()_+=-{}[]|:;<>,.?/", specialChars); - } - - [Fact] - public void ComplexJson_DateTime_ShouldBePreservedAsString() - { - // Arrange - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - var root = JsonSerializer.Deserialize(ComplexJson, options); - Assert.NotNull(root); - - // Act - var toonText = ToonEncoder.Encode(root); - var decoded = ToonDecoder.Decode(toonText); - - // Assert - verify DateTime is preserved as full ISO 8601 UTC timestamp - Assert.NotNull(decoded); - var project = decoded["project"]?.AsObject(); - Assert.NotNull(project); - - var createdAt = project["createdAt"]?.GetValue(); - Assert.NotNull(createdAt); - // Validate full ISO 8601 UTC timestamp (with or without fractional seconds) - // .NET DateTime.ToString("O") produces: 2025-11-20T10:32:00.0000000Z - Assert.StartsWith("2025-11-20T10:32:00", createdAt); - Assert.EndsWith("Z", createdAt); - } - - // POCOs with lowercase property names to preserve original JSON keys when encoding via reflection - public class Root { public Project project { get; set; } = null!; } - - public class Project - { - public string id { get; set; } = null!; - public string name { get; set; } = null!; - public string description { get; set; } = null!; - public DateTime createdAt { get; set; } - public Metadata metadata { get; set; } = null!; - public List phases { get; set; } = new(); - } - - public class Metadata - { - public string owner { get; set; } = null!; - public string website { get; set; } = null!; - public List tags { get; set; } = new(); - public Cost cost { get; set; } = null!; - } - - public class Cost { public string currency { get; set; } = null!; public double amount { get; set; } } - - public class Phase - { - public int phaseId { get; set; } - public string title { get; set; } = null!; - public string deadline { get; set; } = null!; - public string? status { get; set; } - public Details? details { get; set; } - public Budget? budget { get; set; } - public Resources? resources { get; set; } - } - - public class Details { public string notes { get; set; } = null!; public string specialChars { get; set; } = null!; } - - public class Budget { public string currency { get; set; } = null!; public double amount { get; set; } } - - public class Resources { public string leadDeveloper { get; set; } = null!; public string repository { get; set; } = null!; } -} diff --git a/tests/ToonFormat.Tests/KeyFoldingTests.cs b/tests/ToonFormat.Tests/KeyFoldingTests.cs deleted file mode 100644 index 5c637e4..0000000 --- a/tests/ToonFormat.Tests/KeyFoldingTests.cs +++ /dev/null @@ -1,501 +0,0 @@ -using Toon.Format; - -namespace ToonFormat.Tests; - -// TODO: Remove these tests once generated spec tests are in source control -// used to validate current key folding functionality aligns with spec -public class KeyFoldingTests -{ - [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, - KeyFolding = ToonKeyFolding.Safe - }; - - 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, - KeyFolding = ToonKeyFolding.Safe - }; - - 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, - KeyFolding = ToonKeyFolding.Safe - }; - - 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, - KeyFolding = ToonKeyFolding.Safe, - FlattenDepth = 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, - KeyFolding = ToonKeyFolding.Safe - }; - - 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, - KeyFolding = ToonKeyFolding.Safe, - FlattenDepth = 0, - }; - - 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, - KeyFolding = ToonKeyFolding.Safe, - FlattenDepth = 1, - }; - - 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, - KeyFolding = ToonKeyFolding.Off - }; - - 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, - KeyFolding = ToonKeyFolding.Safe - }; - - 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, - KeyFolding = ToonKeyFolding.Safe - }; - - 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, - KeyFolding = ToonKeyFolding.Safe - }; - - var result = ToonEncoder.Encode(input, options); - - Assert.Equal(expected, result); - } -} diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/Encode/ArraysObjectsManual.cs b/tests/ToonFormat.Tests/ManualTests/Encode/ArraysObjectsManual.cs similarity index 100% rename from tests/ToonFormat.SpecGenerator/ManualTests/Encode/ArraysObjectsManual.cs rename to tests/ToonFormat.Tests/ManualTests/Encode/ArraysObjectsManual.cs diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/JsonComplexRoundTripTests.cs b/tests/ToonFormat.Tests/ManualTests/JsonComplexRoundTripTests.cs similarity index 100% rename from tests/ToonFormat.SpecGenerator/ManualTests/JsonComplexRoundTripTests.cs rename to tests/ToonFormat.Tests/ManualTests/JsonComplexRoundTripTests.cs diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/KeyFoldingTests.cs b/tests/ToonFormat.Tests/ManualTests/KeyFoldingTests.cs similarity index 100% rename from tests/ToonFormat.SpecGenerator/ManualTests/KeyFoldingTests.cs rename to tests/ToonFormat.Tests/ManualTests/KeyFoldingTests.cs diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/PerformanceBenchmark.cs b/tests/ToonFormat.Tests/ManualTests/PerformanceBenchmark.cs similarity index 100% rename from tests/ToonFormat.SpecGenerator/ManualTests/PerformanceBenchmark.cs rename to tests/ToonFormat.Tests/ManualTests/PerformanceBenchmark.cs diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/ToonAsyncTests.cs b/tests/ToonFormat.Tests/ManualTests/ToonAsyncTests.cs similarity index 100% rename from tests/ToonFormat.SpecGenerator/ManualTests/ToonAsyncTests.cs rename to tests/ToonFormat.Tests/ManualTests/ToonAsyncTests.cs diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/ToonDecoderTests.cs b/tests/ToonFormat.Tests/ManualTests/ToonDecoderTests.cs similarity index 100% rename from tests/ToonFormat.SpecGenerator/ManualTests/ToonDecoderTests.cs rename to tests/ToonFormat.Tests/ManualTests/ToonDecoderTests.cs diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs b/tests/ToonFormat.Tests/ManualTests/ToonEncoderTests.cs similarity index 100% rename from tests/ToonFormat.SpecGenerator/ManualTests/ToonEncoderTests.cs rename to tests/ToonFormat.Tests/ManualTests/ToonEncoderTests.cs diff --git a/tests/ToonFormat.SpecGenerator/ManualTests/ToonRoundTripTests.cs b/tests/ToonFormat.Tests/ManualTests/ToonRoundTripTests.cs similarity index 100% rename from tests/ToonFormat.SpecGenerator/ManualTests/ToonRoundTripTests.cs rename to tests/ToonFormat.Tests/ManualTests/ToonRoundTripTests.cs diff --git a/tests/ToonFormat.Tests/PerformanceBenchmark.cs b/tests/ToonFormat.Tests/PerformanceBenchmark.cs deleted file mode 100644 index b27cef8..0000000 --- a/tests/ToonFormat.Tests/PerformanceBenchmark.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System; -using System.Diagnostics; -using System.Text.Json.Nodes; -using Toon.Format; -using Xunit; -using Xunit.Abstractions; - -namespace ToonFormat.Tests; - -public class PerformanceBenchmark -{ - private readonly ITestOutputHelper _output; - - public PerformanceBenchmark(ITestOutputHelper output) - { - _output = output; - } - - [Fact] - public void BenchmarkSimpleEncoding() - { - var data = new - { - users = new[] - { - new { id = 1, name = "Alice", role = "admin" }, - new { id = 2, name = "Bob", role = "user" }, - new { id = 3, name = "Charlie", role = "user" } - } - }; - - var sw = Stopwatch.StartNew(); - for (int i = 0; i < 10000; i++) - { - _ = ToonEncoder.Encode(data); - } - sw.Stop(); - - _output.WriteLine($"Simple encoding: {sw.ElapsedMilliseconds}ms for 10,000 iterations"); - _output.WriteLine($"Average: {sw.Elapsed.TotalMicroseconds / 10000:F2}μs per encode"); - - // Baseline: should complete in reasonable time (< 5 seconds for 10k iterations) - Assert.True(sw.ElapsedMilliseconds < 5000, $"Encoding took {sw.ElapsedMilliseconds}ms, expected < 5000ms"); - } - - [Fact] - public void BenchmarkSimpleDecoding() - { - var toonString = """ -users[3]{id,name,role}: - 1,Alice,admin - 2,Bob,user - 3,Charlie,user -"""; - - var sw = Stopwatch.StartNew(); - for (int i = 0; i < 10000; i++) - { - _ = ToonDecoder.Decode(toonString); - } - sw.Stop(); - - _output.WriteLine($"Simple decoding: {sw.ElapsedMilliseconds}ms for 10,000 iterations"); - _output.WriteLine($"Average: {sw.Elapsed.TotalMicroseconds / 10000:F2}μs per decode"); - - // Baseline: should complete in reasonable time (< 5 seconds for 10k iterations) - Assert.True(sw.ElapsedMilliseconds < 5000, $"Decoding took {sw.ElapsedMilliseconds}ms, expected < 5000ms"); - } - - [Fact] - public void BenchmarkSection10Encoding() - { - var data = new - { - items = new[] - { - new - { - users = new[] - { - new { id = 1, name = "Ada" }, - new { id = 2, name = "Bob" } - }, - status = "active", - count = 2 - } - } - }; - - var sw = Stopwatch.StartNew(); - for (int i = 0; i < 10000; i++) - { - _ = ToonEncoder.Encode(data); - } - sw.Stop(); - - _output.WriteLine($"Section 10 encoding: {sw.ElapsedMilliseconds}ms for 10,000 iterations"); - _output.WriteLine($"Average: {sw.Elapsed.TotalMicroseconds / 10000:F2}μs per encode"); - - // Baseline: should complete in reasonable time - Assert.True(sw.ElapsedMilliseconds < 5000, $"Section 10 encoding took {sw.ElapsedMilliseconds}ms, expected < 5000ms"); - } - - [Fact] - public void BenchmarkRoundTrip() - { - var data = new - { - users = new[] - { - new { id = 1, name = "Alice" }, - new { id = 2, name = "Bob" } - }, - metadata = new { version = 1, timestamp = "2025-11-27" } - }; - - var sw = Stopwatch.StartNew(); - for (int i = 0; i < 5000; i++) - { - var encoded = ToonEncoder.Encode(data); - _ = ToonDecoder.Decode(encoded); - } - sw.Stop(); - - _output.WriteLine($"Round-trip: {sw.ElapsedMilliseconds}ms for 5,000 iterations"); - _output.WriteLine($"Average: {sw.Elapsed.TotalMicroseconds / 5000:F2}μs per round-trip"); - - // Baseline: should complete in reasonable time - Assert.True(sw.ElapsedMilliseconds < 5000, $"Round-trip took {sw.ElapsedMilliseconds}ms, expected < 5000ms"); - } -} diff --git a/tests/ToonFormat.Tests/ToonAsyncTests.cs b/tests/ToonFormat.Tests/ToonAsyncTests.cs deleted file mode 100644 index b6db96e..0000000 --- a/tests/ToonFormat.Tests/ToonAsyncTests.cs +++ /dev/null @@ -1,262 +0,0 @@ -#nullable enable -using System; -using System.IO; -using System.Text; -using System.Text.Json.Nodes; -using System.Threading; -using System.Threading.Tasks; -using Toon.Format; -using Xunit; - -namespace ToonFormat.Tests; - -/// -/// Tests for async encoding and decoding methods. -/// -public class ToonAsyncTests -{ - #region EncodeAsync Tests - - [Fact] - public async Task EncodeAsync_WithSimpleObject_ReturnsValidToon() - { - // Arrange - var data = new { name = "Alice", age = 30 }; - - // Act - var result = await ToonEncoder.EncodeAsync(data); - - // Assert - Assert.NotNull(result); - Assert.Contains("name:", result); - Assert.Contains("Alice", result); - Assert.Contains("age:", result); - Assert.Contains("30", result); - } - - [Fact] - public async Task EncodeAsync_WithOptions_RespectsIndentation() - { - // Arrange - var data = new { outer = new { inner = "value" } }; - var options = new ToonEncodeOptions { Indent = 4 }; - - // Act - var result = await ToonEncoder.EncodeAsync(data, options); - - // Assert - Assert.NotNull(result); - Assert.Contains("outer:", result); - } - - [Fact] - public async Task EncodeAsync_WithCancellationToken_ThrowsWhenCancelled() - { - // Arrange - var data = new { name = "Test" }; - var cts = new CancellationTokenSource(); - cts.Cancel(); - - // Act & Assert - await Assert.ThrowsAsync( - () => ToonEncoder.EncodeAsync(data, cts.Token)); - } - - [Fact] - public async Task EncodeToBytesAsync_WithSimpleObject_ReturnsUtf8Bytes() - { - // Arrange - var data = new { message = "Hello" }; - - // Act - var result = await ToonEncoder.EncodeToBytesAsync(data); - - // Assert - Assert.NotNull(result); - var text = Encoding.UTF8.GetString(result); - Assert.Contains("message:", text); - Assert.Contains("Hello", text); - } - - [Fact] - public async Task EncodeToStreamAsync_WritesToStream() - { - // Arrange - var data = new { id = 123 }; - using var stream = new MemoryStream(); - - // Act - await ToonEncoder.EncodeToStreamAsync(data, stream); - - // Assert - stream.Position = 0; - using var reader = new StreamReader(stream); - var result = await reader.ReadToEndAsync(); - Assert.Contains("id:", result); - Assert.Contains("123", result); - } - - [Fact] - public async Task EncodeToStreamAsync_WithNullStream_ThrowsArgumentNullException() - { - // Arrange - var data = new { name = "Test" }; - - // Act & Assert - await Assert.ThrowsAsync( - () => ToonEncoder.EncodeToStreamAsync(data, null!)); - } - - #endregion - - #region DecodeAsync Tests - - [Fact] - public async Task DecodeAsync_WithValidToon_ReturnsJsonNode() - { - // Arrange - var toon = "name: Alice\nage: 30"; - - // Act - var result = await ToonDecoder.DecodeAsync(toon); - - // Assert - Assert.NotNull(result); - Assert.IsType(result); - var obj = (JsonObject)result; - Assert.Equal("Alice", obj["name"]?.GetValue()); - Assert.Equal(30.0, obj["age"]?.GetValue()); - } - - [Fact] - public async Task DecodeAsync_Generic_DeserializesToType() - { - // Arrange - var toon = "name: Bob\nage: 25"; - - // Act - var result = await ToonDecoder.DecodeAsync(toon); - - // Assert - Assert.NotNull(result); - Assert.Equal("Bob", result.name); - Assert.Equal(25, result.age); - } - - [Fact] - public async Task DecodeAsync_WithCancellationToken_ThrowsWhenCancelled() - { - // Arrange - var toon = "name: Test"; - var cts = new CancellationTokenSource(); - cts.Cancel(); - - // Act & Assert - await Assert.ThrowsAsync( - () => ToonDecoder.DecodeAsync(toon, cts.Token)); - } - - [Fact] - public async Task DecodeAsync_FromStream_ReturnsJsonNode() - { - // Arrange - var toon = "message: Hello World"; - using var stream = new MemoryStream(Encoding.UTF8.GetBytes(toon)); - - // Act - var result = await ToonDecoder.DecodeAsync(stream); - - // Assert - Assert.NotNull(result); - Assert.IsType(result); - var obj = (JsonObject)result; - Assert.Equal("Hello World", obj["message"]?.GetValue()); - } - - [Fact] - public async Task DecodeAsync_Generic_FromStream_DeserializesToType() - { - // Arrange - var toon = "name: Charlie\nage: 35"; - using var stream = new MemoryStream(Encoding.UTF8.GetBytes(toon)); - - // Act - var result = await ToonDecoder.DecodeAsync(stream); - - // Assert - Assert.NotNull(result); - Assert.Equal("Charlie", result.name); - Assert.Equal(35, result.age); - } - - [Fact] - public async Task DecodeAsync_FromStream_WithNullStream_ThrowsArgumentNullException() - { - // Act & Assert - await Assert.ThrowsAsync( - () => ToonDecoder.DecodeAsync((Stream)null!)); - } - - [Fact] - public async Task DecodeAsync_WithOptions_RespectsStrictMode() - { - // Arrange - array declares 5 items but only provides 3, strict mode should throw - var toon = "numbers[5]: 1, 2, 3"; - var options = new ToonDecodeOptions { Strict = true }; - - // Act & Assert - await Assert.ThrowsAsync( - () => ToonDecoder.DecodeAsync(toon, options)); - } - - #endregion - - #region Round-Trip Async Tests - - [Fact] - public async Task AsyncRoundTrip_PreservesData() - { - // Arrange - var original = new TestPerson { name = "Diana", age = 28 }; - - // Act - var encoded = await ToonEncoder.EncodeAsync(original); - var decoded = await ToonDecoder.DecodeAsync(encoded); - - // Assert - Assert.NotNull(decoded); - Assert.Equal(original.name, decoded.name); - Assert.Equal(original.age, decoded.age); - } - - [Fact] - public async Task AsyncStreamRoundTrip_PreservesData() - { - // Arrange - var original = new TestPerson { name = "Eve", age = 32 }; - using var stream = new MemoryStream(); - - // Act - await ToonEncoder.EncodeToStreamAsync(original, stream); - stream.Position = 0; - var decoded = await ToonDecoder.DecodeAsync(stream); - - // Assert - Assert.NotNull(decoded); - Assert.Equal(original.name, decoded.name); - Assert.Equal(original.age, decoded.age); - } - - #endregion - - #region Test Helpers - - private class TestPerson - { - public string? name { get; set; } - public int age { get; set; } - } - - #endregion -} - diff --git a/tests/ToonFormat.Tests/ToonDecoderTests.cs b/tests/ToonFormat.Tests/ToonDecoderTests.cs deleted file mode 100644 index d2091af..0000000 --- a/tests/ToonFormat.Tests/ToonDecoderTests.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Nodes; -using Toon.Format; - -namespace ToonFormat.Tests; - -/// -/// Tests for decoding TOON format strings. -/// -public class ToonDecoderTests -{ - [Fact] - public void Decode_SimpleObject_ReturnsValidJson() - { - // Arrange - var toonString = "name: Alice\nage: 30"; - - // Act - var result = ToonDecoder.Decode(toonString); - - // Assert - Assert.NotNull(result); - var obj = result.AsObject(); - Assert.Equal("Alice", obj["name"]?.GetValue()); - Assert.Equal(30.0, obj["age"]?.GetValue()); - } - - [Fact] - 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); - } - - [Fact] - public void Decode_PrimitiveArray_ReturnsValidArray() - { - // Arrange - var toonString = "numbers[5]: 1, 2, 3, 4, 5"; - - // Act - var result = ToonDecoder.Decode(toonString); - - // Assert - Assert.NotNull(result); - var obj = result.AsObject(); - var numbers = obj["numbers"]?.AsArray(); - Assert.NotNull(numbers); - Assert.Equal(5, numbers.Count); - Assert.Equal(1.0, numbers[0]?.GetValue()); - Assert.Equal(5.0, numbers[4]?.GetValue()); - } - - [Fact] - public void Decode_TabularArray_ReturnsValidStructure() - { - // Arrange - using list array format instead - var toonString = @"employees[3]: - - id: 1 - name: Alice - salary: 50000 - - id: 2 - name: Bob - salary: 60000 - - id: 3 - name: Charlie - salary: 55000"; - - // Act - var result = ToonDecoder.Decode(toonString); - - // Assert - Assert.NotNull(result); - var obj = result.AsObject(); - var employees = obj["employees"]?.AsArray(); - Assert.NotNull(employees); - Assert.Equal(3, employees.Count); - Assert.Equal(1.0, employees[0]?["id"]?.GetValue()); - Assert.Equal("Alice", employees[0]?["name"]?.GetValue()); - } - - [Fact] - public void Decode_NestedObject_ReturnsValidStructure() - { - // Arrange - var toonString = @"user: - name: Alice - address: - city: New York - zip: 10001"; - - // Act - var result = ToonDecoder.Decode(toonString); - - // Assert - Assert.NotNull(result); - var user = result["user"]?.AsObject(); - Assert.NotNull(user); - Assert.Equal("Alice", user["name"]?.GetValue()); - var address = user["address"]?.AsObject(); - Assert.NotNull(address); - Assert.Equal("New York", address["city"]?.GetValue()); - } - - [Fact] - public void Decode_WithStrictOption_ValidatesArrayLength() - { - // Arrange - array declares 5 items but only provides 3 - var toonString = "numbers[5]: 1, 2, 3"; - var options = new ToonDecodeOptions { Strict = true }; - - // Act & Assert - Assert.Throws(() => ToonDecoder.Decode(toonString, options)); - } - - [Fact] - public void Decode_WithNonStrictOption_AllowsLengthMismatch() - { - // Arrange - array declares 5 items but only provides 3 - var toonString = "numbers[5]: 1, 2, 3"; - var options = new ToonDecodeOptions { Strict = false }; - - // Act - var result = ToonDecoder.Decode(toonString, options); - - // Assert - Assert.NotNull(result); - var obj = result.AsObject(); - var numbers = obj["numbers"]?.AsArray(); - Assert.NotNull(numbers); - Assert.Equal(3, numbers.Count); - } - - [Fact] - public void Decode_InvalidFormat_ThrowsToonFormatException() - { - // Arrange - array length mismatch with strict mode - var invalidToon = "items[10]: 1, 2, 3"; - var options = new ToonDecodeOptions { Strict = true }; - - // Act & Assert - Assert.Throws(() => ToonDecoder.Decode(invalidToon, options)); - } - - [Fact] - public void Decode_EmptyString_ReturnsEmptyObject() - { - // Arrange - var emptyString = ""; - - // Act - var result = ToonDecoder.Decode(emptyString); - - // Assert - empty string returns empty array - Assert.NotNull(result); - } -} diff --git a/tests/ToonFormat.Tests/ToonEncoderTests.cs b/tests/ToonFormat.Tests/ToonEncoderTests.cs deleted file mode 100644 index 8f20d59..0000000 --- a/tests/ToonFormat.Tests/ToonEncoderTests.cs +++ /dev/null @@ -1,139 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Nodes; -using Toon.Format; - -namespace ToonFormat.Tests; - -/// -/// Tests for encoding data to TOON format. -/// -public class ToonEncoderTests -{ - [Fact] - public void Encode_SimpleObject_ReturnsValidToon() - { - // Arrange - var data = new { name = "Alice", age = 30 }; - - // Act - var result = ToonEncoder.Encode(data); - - // Assert - Assert.NotNull(result); - Assert.Contains("name:", result); - Assert.Contains("age:", result); - } - - [Fact] - 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); - } - - [Fact] - public void Encode_Array_ReturnsValidToon() - { - // Arrange - var data = new { numbers = new[] { 1, 2, 3, 4, 5 } }; - - // Act - var result = ToonEncoder.Encode(data); - - // Assert - Assert.NotNull(result); - Assert.Contains("numbers[", result); - } - - [Fact] - public void Encode_TabularArray_ReturnsValidToon() - { - // Arrange - var employees = new[] - { - new { id = 1, name = "Alice", salary = 50000 }, - new { id = 2, name = "Bob", salary = 60000 }, - new { id = 3, name = "Charlie", salary = 55000 } - }; - var data = new { employees }; - - // Act - var result = ToonEncoder.Encode(data); - - // Assert - Assert.NotNull(result); - Assert.Contains("employees[", result); - Assert.Contains("id", result); - Assert.Contains("name", result); - Assert.Contains("salary", result); - } - - [Fact] - public void Encode_WithCustomIndent_UsesCorrectIndentation() - { - // Arrange - var data = new { outer = new { inner = "value" } }; - var options = new ToonEncodeOptions { Indent = 4 }; - - // Act - var result = ToonEncoder.Encode(data, options); - - // Assert - Assert.NotNull(result); - Assert.Contains("outer:", result); - } - - [Fact] - public void Encode_WithCustomDelimiter_UsesCorrectDelimiter() - { - // Arrange - var data = new { numbers = new[] { 1, 2, 3 } }; - var options = new ToonEncodeOptions { Delimiter = ToonDelimiter.TAB }; - - // Act - var result = ToonEncoder.Encode(data, options); - - // Assert - Assert.NotNull(result); - Assert.Contains("numbers[", result); - } - - [Fact] - public void Encode_NestedStructures_ReturnsValidToon() - { - // Arrange - var data = new - { - user = new - { - name = "Alice", - address = new - { - city = "New York", - zip = "10001" - } - } - }; - - // Act - var result = ToonEncoder.Encode(data); - - // Assert - Assert.NotNull(result); - Assert.Contains("user:", result); - Assert.Contains("address:", result); - } -} diff --git a/tests/ToonFormat.Tests/ToonRoundTripTests.cs b/tests/ToonFormat.Tests/ToonRoundTripTests.cs deleted file mode 100644 index 251ee95..0000000 --- a/tests/ToonFormat.Tests/ToonRoundTripTests.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Nodes; -using Toon.Format; - -namespace ToonFormat.Tests; - -/// -/// Round-trip tests to verify encoding and decoding preserve data integrity. -/// -public class ToonRoundTripTests -{ - [Fact] - public void RoundTrip_SimpleObject_PreservesData() - { - // Arrange - var original = new { name = "Alice", age = 30, active = true }; - - // Act - var encoded = ToonEncoder.Encode(original); - var decoded = ToonDecoder.Decode(encoded); - - // Assert - Assert.NotNull(decoded); - var obj = decoded.AsObject(); - Assert.Equal("Alice", obj["name"]?.GetValue()); - Assert.Equal(30.0, obj["age"]?.GetValue()); - Assert.True(obj["active"]?.GetValue()); - } - - [Fact] - public void RoundTrip_Array_PreservesData() - { - // Arrange - var original = new { numbers = new[] { 1, 2, 3, 4, 5 } }; - - // Act - var encoded = ToonEncoder.Encode(original); - var decoded = ToonDecoder.Decode(encoded); - - // Assert - Assert.NotNull(decoded); - var numbers = decoded["numbers"]?.AsArray(); - Assert.NotNull(numbers); - Assert.Equal(5, numbers.Count); - for (int i = 0; i < 5; i++) - { - Assert.Equal((double)(i + 1), numbers[i]?.GetValue()); - } - } - - [Fact] - public void RoundTrip_ComplexStructure_PreservesData() - { - // Arrange - var original = new - { - users = new[] - { - new { id = 1, name = "Alice", email = "alice@example.com" }, - new { id = 2, name = "Bob", email = "bob@example.com" } - }, - metadata = new - { - total = 2, - timestamp = "2025-01-01T00:00:00Z" - } - }; - - // Act - var encoded = ToonEncoder.Encode(original); - var decoded = ToonDecoder.Decode(encoded); - - // Assert - Assert.NotNull(decoded); - var users = decoded["users"]?.AsArray(); - Assert.NotNull(users); - Assert.Equal(2, users.Count); - Assert.Equal("Alice", users[0]?["name"]?.GetValue()); - - var metadata = decoded["metadata"]?.AsObject(); - Assert.NotNull(metadata); - Assert.Equal(2.0, metadata["total"]?.GetValue()); - } -} From 32a87d966587a67787077e5a7c2a16b99fac44c4 Mon Sep 17 00:00:00 2001 From: Daniel Destouche Date: Wed, 24 Dec 2025 01:03:56 -0500 Subject: [PATCH 15/20] feat: Test to validate issue #7 is resolved --- .../ManualTests/ToonEncoderTests.cs | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/tests/ToonFormat.Tests/ManualTests/ToonEncoderTests.cs b/tests/ToonFormat.Tests/ManualTests/ToonEncoderTests.cs index 8f20d59..c10c9ad 100644 --- a/tests/ToonFormat.Tests/ManualTests/ToonEncoderTests.cs +++ b/tests/ToonFormat.Tests/ManualTests/ToonEncoderTests.cs @@ -136,4 +136,37 @@ public void Encode_NestedStructures_ReturnsValidToon() Assert.Contains("user:", result); Assert.Contains("address:", result); } -} + + [Fact] + public void Encode_ArrayOfPrimitives_Issue7() + { + var data = new[] + { + new + { + ZCHATJID = "18324448539@s.whatsapp.net", + ZMATCHEDTEXT = "http://www.\\u0138roger.com/anniversary", + ZINDEX = "0", + Z_OPT = 2, + Z_PK = 2, + ZSENDERJID = "18324448539@s.whatsapp.net", + ZOWNSTHUMBNAIL = "0", + ZTYPE = "0", + Z_ENT = "10", + ZMESSAGE = "696", + ZSECTIONID = "2017-11", + ZDATE = "531829799", + ZCONTENT1 = "http://www.xn--roger-t5a.com/anniversary" + } + }; + + var expected = """ + [1]{ZCHATJID,ZMATCHEDTEXT,ZINDEX,Z_OPT,Z_PK,ZSENDERJID,ZOWNSTHUMBNAIL,ZTYPE,Z_ENT,ZMESSAGE,ZSECTIONID,ZDATE,ZCONTENT1}: + 18324448539@s.whatsapp.net,"http://www.\\u0138roger.com/anniversary","0",2,2,18324448539@s.whatsapp.net,"0","0","10","696",2017-11,"531829799","http://www.xn--roger-t5a.com/anniversary" + """; + + var result = ToonEncoder.Encode(data); + + Assert.Equal(expected, result); + } +} \ No newline at end of file From b21f376960269d94a284503740860073361534ec Mon Sep 17 00:00:00 2001 From: Daniel Destouche Date: Wed, 24 Dec 2025 01:18:05 -0500 Subject: [PATCH 16/20] chore: Default namespaces to Toon.Format --- src/ToonFormat/Constants.cs | 2 +- src/ToonFormat/Internal/Decode/Decoders.cs | 4 ++-- src/ToonFormat/Internal/Decode/Parser.cs | 4 ++-- src/ToonFormat/Internal/Decode/PathExpansion.cs | 4 ++-- src/ToonFormat/Internal/Decode/Scanner.cs | 2 +- src/ToonFormat/Internal/Decode/Validation.cs | 2 +- src/ToonFormat/Internal/Encode/Encoders.cs | 2 +- src/ToonFormat/Internal/Encode/Folding.cs | 4 ++-- src/ToonFormat/Internal/Encode/LineWriter.cs | 2 +- src/ToonFormat/Internal/Encode/Normalize.cs | 4 ++-- src/ToonFormat/Internal/Encode/Primitives.cs | 4 ++-- src/ToonFormat/Internal/Shared/FloatUtils.cs | 8 ++++---- src/ToonFormat/Internal/Shared/LiteralUtils.cs | 2 +- src/ToonFormat/Internal/Shared/NumericUtils.cs | 2 +- src/ToonFormat/Internal/Shared/StringUtils.cs | 2 +- src/ToonFormat/Internal/Shared/ValidationShared.cs | 4 ++-- src/ToonFormat/Options/ToonDecodeOptions.cs | 2 +- src/ToonFormat/Options/ToonEncodeOptions.cs | 2 +- src/ToonFormat/ToonDecoder.cs | 4 ++-- src/ToonFormat/ToonEncoder.cs | 4 ++-- src/ToonFormat/ToonFormat.csproj | 1 + src/ToonFormat/ToonFormatException.cs | 2 +- src/ToonFormat/ToonPathExpansionException.cs | 2 +- .../Extensions/StringExtensions.cs | 2 +- tests/ToonFormat.SpecGenerator/FixtureWriter.cs | 8 ++++---- tests/ToonFormat.SpecGenerator/Program.cs | 2 +- tests/ToonFormat.SpecGenerator/SpecGenerator.cs | 8 ++++---- tests/ToonFormat.SpecGenerator/SpecGeneratorOptions.cs | 2 +- tests/ToonFormat.SpecGenerator/SpecSerializer.cs | 2 +- .../ToonFormat.SpecGenerator.csproj | 3 ++- tests/ToonFormat.SpecGenerator/Types/DecodeTestCase.cs | 2 +- tests/ToonFormat.SpecGenerator/Types/EncodeTestCase.cs | 2 +- tests/ToonFormat.SpecGenerator/Types/Fixtures.cs | 2 +- tests/ToonFormat.SpecGenerator/Types/ITestCase.cs | 2 +- tests/ToonFormat.SpecGenerator/Types/TestCaseOptions.cs | 2 +- tests/ToonFormat.SpecGenerator/Util/GitTool.cs | 2 +- .../GeneratedTests/Decode/ArraysNested.cs | 2 +- .../GeneratedTests/Decode/ArraysPrimitive.cs | 2 +- .../GeneratedTests/Decode/ArraysTabular.cs | 2 +- .../ToonFormat.Tests/GeneratedTests/Decode/BlankLines.cs | 2 +- .../ToonFormat.Tests/GeneratedTests/Decode/Delimiters.cs | 2 +- .../GeneratedTests/Decode/IndentationErrors.cs | 2 +- tests/ToonFormat.Tests/GeneratedTests/Decode/Numbers.cs | 2 +- tests/ToonFormat.Tests/GeneratedTests/Decode/Objects.cs | 2 +- .../GeneratedTests/Decode/PathExpansion.cs | 2 +- .../ToonFormat.Tests/GeneratedTests/Decode/Primitives.cs | 2 +- tests/ToonFormat.Tests/GeneratedTests/Decode/RootForm.cs | 2 +- .../GeneratedTests/Decode/ValidationErrors.cs | 2 +- .../ToonFormat.Tests/GeneratedTests/Decode/Whitespace.cs | 2 +- .../GeneratedTests/Encode/ArraysNested.cs | 2 +- .../GeneratedTests/Encode/ArraysObjects.cs | 2 +- .../GeneratedTests/Encode/ArraysPrimitive.cs | 2 +- .../GeneratedTests/Encode/ArraysTabular.cs | 2 +- .../ToonFormat.Tests/GeneratedTests/Encode/Delimiters.cs | 2 +- .../ToonFormat.Tests/GeneratedTests/Encode/KeyFolding.cs | 2 +- tests/ToonFormat.Tests/GeneratedTests/Encode/Objects.cs | 2 +- .../ToonFormat.Tests/GeneratedTests/Encode/Primitives.cs | 5 +++-- .../ToonFormat.Tests/GeneratedTests/Encode/Whitespace.cs | 2 +- .../ManualTests/Encode/ArraysObjectsManual.cs | 2 +- .../ManualTests/JsonComplexRoundTripTests.cs | 2 +- tests/ToonFormat.Tests/ManualTests/KeyFoldingTests.cs | 2 +- .../ToonFormat.Tests/ManualTests/PerformanceBenchmark.cs | 2 +- tests/ToonFormat.Tests/ManualTests/ToonAsyncTests.cs | 2 +- tests/ToonFormat.Tests/ManualTests/ToonDecoderTests.cs | 2 +- tests/ToonFormat.Tests/ManualTests/ToonEncoderTests.cs | 2 +- tests/ToonFormat.Tests/ManualTests/ToonRoundTripTests.cs | 2 +- tests/ToonFormat.Tests/ToonFormat.Tests.csproj | 1 + 67 files changed, 88 insertions(+), 84 deletions(-) diff --git a/src/ToonFormat/Constants.cs b/src/ToonFormat/Constants.cs index c03ea16..b713b0e 100644 --- a/src/ToonFormat/Constants.cs +++ b/src/ToonFormat/Constants.cs @@ -1,6 +1,6 @@ using System; -namespace ToonFormat +namespace Toon.Format { /// /// TOON format constants for structural characters, literals, and delimiters. diff --git a/src/ToonFormat/Internal/Decode/Decoders.cs b/src/ToonFormat/Internal/Decode/Decoders.cs index e6acc39..2c09ac0 100644 --- a/src/ToonFormat/Internal/Decode/Decoders.cs +++ b/src/ToonFormat/Internal/Decode/Decoders.cs @@ -3,9 +3,9 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json.Nodes; -using ToonFormat.Internal.Shared; +using Toon.Format.Internal.Shared; -namespace ToonFormat.Internal.Decode +namespace Toon.Format.Internal.Decode { /// /// Main decoding functions for converting TOON format to JSON values. diff --git a/src/ToonFormat/Internal/Decode/Parser.cs b/src/ToonFormat/Internal/Decode/Parser.cs index 52e7941..1f212d4 100644 --- a/src/ToonFormat/Internal/Decode/Parser.cs +++ b/src/ToonFormat/Internal/Decode/Parser.cs @@ -4,9 +4,9 @@ using System.Globalization; using System.Linq; using System.Text.Json.Nodes; -using ToonFormat.Internal.Shared; +using Toon.Format.Internal.Shared; -namespace ToonFormat.Internal.Decode +namespace Toon.Format.Internal.Decode { /// /// Information about an array header. diff --git a/src/ToonFormat/Internal/Decode/PathExpansion.cs b/src/ToonFormat/Internal/Decode/PathExpansion.cs index b54c3ff..d9dbaee 100644 --- a/src/ToonFormat/Internal/Decode/PathExpansion.cs +++ b/src/ToonFormat/Internal/Decode/PathExpansion.cs @@ -4,9 +4,9 @@ using System.Linq; using System.Text.Json.Nodes; using System.Text.RegularExpressions; -using ToonFormat.Internal.Shared; +using Toon.Format.Internal.Shared; -namespace ToonFormat.Internal.Decode +namespace Toon.Format.Internal.Decode { /// /// Path expansion logic for dotted keys per SPEC §13.4 diff --git a/src/ToonFormat/Internal/Decode/Scanner.cs b/src/ToonFormat/Internal/Decode/Scanner.cs index b14c27d..f74e7c1 100644 --- a/src/ToonFormat/Internal/Decode/Scanner.cs +++ b/src/ToonFormat/Internal/Decode/Scanner.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; -namespace ToonFormat.Internal.Decode +namespace Toon.Format.Internal.Decode { /// /// Represents a parsed line with its raw content, indentation, depth, and line number. diff --git a/src/ToonFormat/Internal/Decode/Validation.cs b/src/ToonFormat/Internal/Decode/Validation.cs index f985dfc..70bb38c 100644 --- a/src/ToonFormat/Internal/Decode/Validation.cs +++ b/src/ToonFormat/Internal/Decode/Validation.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; -namespace ToonFormat.Internal.Decode +namespace Toon.Format.Internal.Decode { /// /// Options for decoding TOON format. diff --git a/src/ToonFormat/Internal/Encode/Encoders.cs b/src/ToonFormat/Internal/Encode/Encoders.cs index 47844fd..fb8be9a 100644 --- a/src/ToonFormat/Internal/Encode/Encoders.cs +++ b/src/ToonFormat/Internal/Encode/Encoders.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Text.Json.Nodes; -namespace ToonFormat.Internal.Encode +namespace Toon.Format.Internal.Encode { /// /// Options for encoding TOON format, aligned with TypeScript ResolvedEncodeOptions. diff --git a/src/ToonFormat/Internal/Encode/Folding.cs b/src/ToonFormat/Internal/Encode/Folding.cs index 1f89e33..fd2825f 100644 --- a/src/ToonFormat/Internal/Encode/Folding.cs +++ b/src/ToonFormat/Internal/Encode/Folding.cs @@ -4,9 +4,9 @@ using System.Text; using System.Text.Json.Nodes; using System.Threading.Tasks; -using ToonFormat.Internal.Shared; +using Toon.Format.Internal.Shared; -namespace ToonFormat.Internal.Encode +namespace Toon.Format.Internal.Encode { internal class FoldResult { diff --git a/src/ToonFormat/Internal/Encode/LineWriter.cs b/src/ToonFormat/Internal/Encode/LineWriter.cs index fb3409f..39acac6 100644 --- a/src/ToonFormat/Internal/Encode/LineWriter.cs +++ b/src/ToonFormat/Internal/Encode/LineWriter.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace ToonFormat.Internal.Encode +namespace Toon.Format.Internal.Encode { /// /// Helper class for building indented lines of TOON output. diff --git a/src/ToonFormat/Internal/Encode/Normalize.cs b/src/ToonFormat/Internal/Encode/Normalize.cs index 0c263e8..762b6f7 100644 --- a/src/ToonFormat/Internal/Encode/Normalize.cs +++ b/src/ToonFormat/Internal/Encode/Normalize.cs @@ -5,9 +5,9 @@ using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; -using ToonFormat.Internal.Shared; +using Toon.Format.Internal.Shared; -namespace ToonFormat.Internal.Encode +namespace Toon.Format.Internal.Encode { /// /// Normalization utilities for converting arbitrary .NET objects to JsonNode representations diff --git a/src/ToonFormat/Internal/Encode/Primitives.cs b/src/ToonFormat/Internal/Encode/Primitives.cs index 253c3bc..46839d7 100644 --- a/src/ToonFormat/Internal/Encode/Primitives.cs +++ b/src/ToonFormat/Internal/Encode/Primitives.cs @@ -4,9 +4,9 @@ using System.Globalization; using System.Linq; using System.Text.Json.Nodes; -using ToonFormat.Internal.Shared; +using Toon.Format.Internal.Shared; -namespace ToonFormat.Internal.Encode +namespace Toon.Format.Internal.Encode { /// /// Primitive value encoding, key encoding, and header formatting utilities. diff --git a/src/ToonFormat/Internal/Shared/FloatUtils.cs b/src/ToonFormat/Internal/Shared/FloatUtils.cs index febd5e1..8241ff1 100644 --- a/src/ToonFormat/Internal/Shared/FloatUtils.cs +++ b/src/ToonFormat/Internal/Shared/FloatUtils.cs @@ -1,6 +1,6 @@ using System; -namespace ToonFormat.Internal.Shared +namespace Toon.Format.Internal.Shared { internal static class FloatUtils { @@ -14,13 +14,13 @@ internal static class FloatUtils /// public static bool NearlyEqual(double a, double b, double absEps = 1e-12, double relEps = 1e-9) { - if (double.IsNaN(a) && double.IsNaN(b)) return true; // ҵϳҪ NaN == NaN + if (double.IsNaN(a) && double.IsNaN(b)) return true; // ҵ���ϳ���Ҫ�� NaN == NaN if (double.IsInfinity(a) || double.IsInfinity(b)) return a.Equals(b); - if (a == b) return true; // 0.0 == -0.0ȫ + if (a == b) return true; // ���� 0.0 == -0.0����ȫ��� var diff = Math.Abs(a - b); var scale = Math.Max(Math.Abs(a), Math.Abs(b)); - if (scale == 0) return diff <= absEps; // ߶ӽ 0 + if (scale == 0) return diff <= absEps; // ���߶��ӽ� 0 return diff <= Math.Max(absEps, relEps * scale); } diff --git a/src/ToonFormat/Internal/Shared/LiteralUtils.cs b/src/ToonFormat/Internal/Shared/LiteralUtils.cs index 18f5ddc..36708c9 100644 --- a/src/ToonFormat/Internal/Shared/LiteralUtils.cs +++ b/src/ToonFormat/Internal/Shared/LiteralUtils.cs @@ -1,7 +1,7 @@ #nullable enable using System.Globalization; -namespace ToonFormat.Internal.Shared +namespace Toon.Format.Internal.Shared { /// /// Literal judgment utilities, aligned with TypeScript version shared/literal-utils.ts. diff --git a/src/ToonFormat/Internal/Shared/NumericUtils.cs b/src/ToonFormat/Internal/Shared/NumericUtils.cs index 79fdfa8..770aa12 100644 --- a/src/ToonFormat/Internal/Shared/NumericUtils.cs +++ b/src/ToonFormat/Internal/Shared/NumericUtils.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; -namespace ToonFormat.Internal.Shared +namespace Toon.Format.Internal.Shared { internal static class NumericUtils { diff --git a/src/ToonFormat/Internal/Shared/StringUtils.cs b/src/ToonFormat/Internal/Shared/StringUtils.cs index 859ec21..8f3dd58 100644 --- a/src/ToonFormat/Internal/Shared/StringUtils.cs +++ b/src/ToonFormat/Internal/Shared/StringUtils.cs @@ -1,7 +1,7 @@ #nullable enable using System.Text; -namespace ToonFormat.Internal.Shared +namespace Toon.Format.Internal.Shared { /// /// String utilities, aligned with TypeScript version shared/string-utils.ts: diff --git a/src/ToonFormat/Internal/Shared/ValidationShared.cs b/src/ToonFormat/Internal/Shared/ValidationShared.cs index 9db7a9c..825e9b0 100644 --- a/src/ToonFormat/Internal/Shared/ValidationShared.cs +++ b/src/ToonFormat/Internal/Shared/ValidationShared.cs @@ -1,9 +1,9 @@ #nullable enable using System; using System.Text.RegularExpressions; -using ToonFormat; +using Toon.Format; -namespace ToonFormat.Internal.Shared +namespace Toon.Format.Internal.Shared { /// /// Validation utilities aligned with TypeScript version shared/validation.ts: diff --git a/src/ToonFormat/Options/ToonDecodeOptions.cs b/src/ToonFormat/Options/ToonDecodeOptions.cs index 374dd49..96c00d6 100644 --- a/src/ToonFormat/Options/ToonDecodeOptions.cs +++ b/src/ToonFormat/Options/ToonDecodeOptions.cs @@ -23,5 +23,5 @@ public class ToonDecodeOptions /// "off" (default): Dotted keys are treated as literal keys. /// "safe": Expand eligible dotted keys into nested objects. /// - public ToonFormat.ToonPathExpansion ExpandPaths { get; set; } = ToonFormat.ToonPathExpansion.Off; + public Format.ToonPathExpansion ExpandPaths { get; set; } = Format.ToonPathExpansion.Off; } diff --git a/src/ToonFormat/Options/ToonEncodeOptions.cs b/src/ToonFormat/Options/ToonEncodeOptions.cs index d4b3733..d3b0a1b 100644 --- a/src/ToonFormat/Options/ToonEncodeOptions.cs +++ b/src/ToonFormat/Options/ToonEncodeOptions.cs @@ -1,5 +1,5 @@ #nullable enable -using ToonFormat; +using Toon.Format; namespace Toon.Format; diff --git a/src/ToonFormat/ToonDecoder.cs b/src/ToonFormat/ToonDecoder.cs index 1f14cec..45fd406 100644 --- a/src/ToonFormat/ToonDecoder.cs +++ b/src/ToonFormat/ToonDecoder.cs @@ -6,8 +6,8 @@ using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; -using ToonFormat; -using ToonFormat.Internal.Decode; +using Toon.Format; +using Toon.Format.Internal.Decode; namespace Toon.Format; diff --git a/src/ToonFormat/ToonEncoder.cs b/src/ToonFormat/ToonEncoder.cs index 1e9ac7b..58a18ce 100644 --- a/src/ToonFormat/ToonEncoder.cs +++ b/src/ToonFormat/ToonEncoder.cs @@ -5,8 +5,8 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using ToonFormat; -using ToonFormat.Internal.Encode; +using Toon.Format; +using Toon.Format.Internal.Encode; namespace Toon.Format; diff --git a/src/ToonFormat/ToonFormat.csproj b/src/ToonFormat/ToonFormat.csproj index db718ab..acf9101 100644 --- a/src/ToonFormat/ToonFormat.csproj +++ b/src/ToonFormat/ToonFormat.csproj @@ -5,6 +5,7 @@ enable enable latest + Toon.Format Toon.Format diff --git a/src/ToonFormat/ToonFormatException.cs b/src/ToonFormat/ToonFormatException.cs index 9396875..7791b3e 100644 --- a/src/ToonFormat/ToonFormatException.cs +++ b/src/ToonFormat/ToonFormatException.cs @@ -2,7 +2,7 @@ using System; using System.Text; -namespace ToonFormat +namespace Toon.Format { /// /// Exception thrown when TOON format parsing or encoding fails. diff --git a/src/ToonFormat/ToonPathExpansionException.cs b/src/ToonFormat/ToonPathExpansionException.cs index 273b2b2..e40bdb6 100644 --- a/src/ToonFormat/ToonPathExpansionException.cs +++ b/src/ToonFormat/ToonPathExpansionException.cs @@ -2,7 +2,7 @@ using System; using System.Text; -namespace ToonFormat +namespace Toon.Format { /// /// Exception thrown when path expansion conflicts occur during TOON decoding. diff --git a/tests/ToonFormat.SpecGenerator/Extensions/StringExtensions.cs b/tests/ToonFormat.SpecGenerator/Extensions/StringExtensions.cs index 9027500..75fece1 100644 --- a/tests/ToonFormat.SpecGenerator/Extensions/StringExtensions.cs +++ b/tests/ToonFormat.SpecGenerator/Extensions/StringExtensions.cs @@ -1,6 +1,6 @@ using System.Globalization; -namespace ToonFormat.SpecGenerator.Extensions; +namespace Toon.Format.SpecGenerator.Extensions; public static class StringExtensions { diff --git a/tests/ToonFormat.SpecGenerator/FixtureWriter.cs b/tests/ToonFormat.SpecGenerator/FixtureWriter.cs index b8aaacb..43ad1fd 100644 --- a/tests/ToonFormat.SpecGenerator/FixtureWriter.cs +++ b/tests/ToonFormat.SpecGenerator/FixtureWriter.cs @@ -1,10 +1,10 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.RegularExpressions; -using ToonFormat.SpecGenerator.Extensions; -using ToonFormat.SpecGenerator.Types; +using Toon.Format.SpecGenerator.Types; +using Toon.Format.SpecGenerator.Extensions; -namespace ToonFormat.SpecGenerator; +namespace Toon.Format.SpecGenerator; internal class FixtureWriter(Fixtures fixture, string outputDir) where TTestCase : ITestCase @@ -472,7 +472,7 @@ private void WriteUsings(StreamWriter writer) private void WriteNamespace(StreamWriter writer, string category) { - WriteLine(writer, $"namespace ToonFormat.Tests.{category.ToPascalCase()};"); + WriteLine(writer, $"namespace Toon.Format.Tests.{category.ToPascalCase()};"); } private void WriteLine(StreamWriter writer) diff --git a/tests/ToonFormat.SpecGenerator/Program.cs b/tests/ToonFormat.SpecGenerator/Program.cs index bd3cc60..8e55855 100644 --- a/tests/ToonFormat.SpecGenerator/Program.cs +++ b/tests/ToonFormat.SpecGenerator/Program.cs @@ -1,7 +1,7 @@ using CommandLine; using Microsoft.Extensions.Logging; -namespace ToonFormat.SpecGenerator; +namespace Toon.Format.SpecGenerator; public static class Program diff --git a/tests/ToonFormat.SpecGenerator/SpecGenerator.cs b/tests/ToonFormat.SpecGenerator/SpecGenerator.cs index 88aa675..23bc827 100644 --- a/tests/ToonFormat.SpecGenerator/SpecGenerator.cs +++ b/tests/ToonFormat.SpecGenerator/SpecGenerator.cs @@ -1,10 +1,10 @@ using Microsoft.Extensions.Logging; using System.Text.Json.Nodes; -using ToonFormat.SpecGenerator.Extensions; -using ToonFormat.SpecGenerator.Types; -using ToonFormat.SpecGenerator.Util; +using Toon.Format.SpecGenerator.Types; +using Toon.Format.SpecGenerator.Util; +using Toon.Format.SpecGenerator.Extensions; -namespace ToonFormat.SpecGenerator; +namespace Toon.Format.SpecGenerator; internal class SpecGenerator(ILogger logger) { diff --git a/tests/ToonFormat.SpecGenerator/SpecGeneratorOptions.cs b/tests/ToonFormat.SpecGenerator/SpecGeneratorOptions.cs index d4e3e0c..5f8005e 100644 --- a/tests/ToonFormat.SpecGenerator/SpecGeneratorOptions.cs +++ b/tests/ToonFormat.SpecGenerator/SpecGeneratorOptions.cs @@ -1,6 +1,6 @@ using CommandLine; -namespace ToonFormat.SpecGenerator; +namespace Toon.Format.SpecGenerator; public class SpecGeneratorOptions { diff --git a/tests/ToonFormat.SpecGenerator/SpecSerializer.cs b/tests/ToonFormat.SpecGenerator/SpecSerializer.cs index d5a974b..a70db2d 100644 --- a/tests/ToonFormat.SpecGenerator/SpecSerializer.cs +++ b/tests/ToonFormat.SpecGenerator/SpecSerializer.cs @@ -1,6 +1,6 @@ using System.Text.Json; -namespace ToonFormat.SpecGenerator; +namespace Toon.Format.SpecGenerator; internal static class SpecSerializer { diff --git a/tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj b/tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj index a44c278..bc3a504 100644 --- a/tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj +++ b/tests/ToonFormat.SpecGenerator/ToonFormat.SpecGenerator.csproj @@ -1,10 +1,11 @@  - net9.0 + net10.0 enable enable Exe + Toon.Format.SpecGenerator diff --git a/tests/ToonFormat.SpecGenerator/Types/DecodeTestCase.cs b/tests/ToonFormat.SpecGenerator/Types/DecodeTestCase.cs index 5a0a5c1..1899a09 100644 --- a/tests/ToonFormat.SpecGenerator/Types/DecodeTestCase.cs +++ b/tests/ToonFormat.SpecGenerator/Types/DecodeTestCase.cs @@ -1,6 +1,6 @@ using System.Text.Json.Nodes; -namespace ToonFormat.SpecGenerator.Types; +namespace Toon.Format.SpecGenerator.Types; public record DecodeTestCase : ITestCase { diff --git a/tests/ToonFormat.SpecGenerator/Types/EncodeTestCase.cs b/tests/ToonFormat.SpecGenerator/Types/EncodeTestCase.cs index a11b132..1935d88 100644 --- a/tests/ToonFormat.SpecGenerator/Types/EncodeTestCase.cs +++ b/tests/ToonFormat.SpecGenerator/Types/EncodeTestCase.cs @@ -1,6 +1,6 @@ using System.Text.Json.Nodes; -namespace ToonFormat.SpecGenerator.Types; +namespace Toon.Format.SpecGenerator.Types; public record EncodeTestCase : ITestCase { diff --git a/tests/ToonFormat.SpecGenerator/Types/Fixtures.cs b/tests/ToonFormat.SpecGenerator/Types/Fixtures.cs index c7cf9d7..205dab9 100644 --- a/tests/ToonFormat.SpecGenerator/Types/Fixtures.cs +++ b/tests/ToonFormat.SpecGenerator/Types/Fixtures.cs @@ -1,4 +1,4 @@ -namespace ToonFormat.SpecGenerator.Types; +namespace Toon.Format.SpecGenerator.Types; public record Fixtures where TTestCase : ITestCase diff --git a/tests/ToonFormat.SpecGenerator/Types/ITestCase.cs b/tests/ToonFormat.SpecGenerator/Types/ITestCase.cs index 1bcab07..6fff47d 100644 --- a/tests/ToonFormat.SpecGenerator/Types/ITestCase.cs +++ b/tests/ToonFormat.SpecGenerator/Types/ITestCase.cs @@ -1,4 +1,4 @@ -namespace ToonFormat.SpecGenerator.Types; +namespace Toon.Format.SpecGenerator.Types; public interface ITestCase { diff --git a/tests/ToonFormat.SpecGenerator/Types/TestCaseOptions.cs b/tests/ToonFormat.SpecGenerator/Types/TestCaseOptions.cs index e0ebf5d..7e4811d 100644 --- a/tests/ToonFormat.SpecGenerator/Types/TestCaseOptions.cs +++ b/tests/ToonFormat.SpecGenerator/Types/TestCaseOptions.cs @@ -1,4 +1,4 @@ -namespace ToonFormat.SpecGenerator.Types; +namespace Toon.Format.SpecGenerator.Types; public record TestCaseOptions { diff --git a/tests/ToonFormat.SpecGenerator/Util/GitTool.cs b/tests/ToonFormat.SpecGenerator/Util/GitTool.cs index 4deeeeb..6481694 100644 --- a/tests/ToonFormat.SpecGenerator/Util/GitTool.cs +++ b/tests/ToonFormat.SpecGenerator/Util/GitTool.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Logging; -namespace ToonFormat.SpecGenerator.Util; +namespace Toon.Format.SpecGenerator.Util; internal static class GitTool { diff --git a/tests/ToonFormat.Tests/GeneratedTests/Decode/ArraysNested.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/ArraysNested.cs index 55935f2..30c1934 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Decode/ArraysNested.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Decode/ArraysNested.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Decode; +namespace Toon.Format.Tests.Decode; [Trait("Category", "decode")] diff --git a/tests/ToonFormat.Tests/GeneratedTests/Decode/ArraysPrimitive.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/ArraysPrimitive.cs index 8a2d574..889aa16 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Decode/ArraysPrimitive.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Decode/ArraysPrimitive.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Decode; +namespace Toon.Format.Tests.Decode; [Trait("Category", "decode")] diff --git a/tests/ToonFormat.Tests/GeneratedTests/Decode/ArraysTabular.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/ArraysTabular.cs index 81e61b8..2c7781b 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Decode/ArraysTabular.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Decode/ArraysTabular.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Decode; +namespace Toon.Format.Tests.Decode; [Trait("Category", "decode")] diff --git a/tests/ToonFormat.Tests/GeneratedTests/Decode/BlankLines.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/BlankLines.cs index c845dfb..ccc41a5 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Decode/BlankLines.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Decode/BlankLines.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Decode; +namespace Toon.Format.Tests.Decode; [Trait("Category", "decode")] diff --git a/tests/ToonFormat.Tests/GeneratedTests/Decode/Delimiters.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/Delimiters.cs index 85d8f41..28ec15f 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Decode/Delimiters.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Decode/Delimiters.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Decode; +namespace Toon.Format.Tests.Decode; [Trait("Category", "decode")] diff --git a/tests/ToonFormat.Tests/GeneratedTests/Decode/IndentationErrors.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/IndentationErrors.cs index 22e7c48..bdc171a 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Decode/IndentationErrors.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Decode/IndentationErrors.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Decode; +namespace Toon.Format.Tests.Decode; [Trait("Category", "decode")] diff --git a/tests/ToonFormat.Tests/GeneratedTests/Decode/Numbers.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/Numbers.cs index 15ccdcb..c76b9f6 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Decode/Numbers.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Decode/Numbers.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Decode; +namespace Toon.Format.Tests.Decode; [Trait("Category", "decode")] diff --git a/tests/ToonFormat.Tests/GeneratedTests/Decode/Objects.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/Objects.cs index c156aa3..56e1661 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Decode/Objects.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Decode/Objects.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Decode; +namespace Toon.Format.Tests.Decode; [Trait("Category", "decode")] diff --git a/tests/ToonFormat.Tests/GeneratedTests/Decode/PathExpansion.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/PathExpansion.cs index 15d54df..e560497 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Decode/PathExpansion.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Decode/PathExpansion.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Decode; +namespace Toon.Format.Tests.Decode; [Trait("Category", "decode")] diff --git a/tests/ToonFormat.Tests/GeneratedTests/Decode/Primitives.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/Primitives.cs index fb80c5f..d541a16 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Decode/Primitives.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Decode/Primitives.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Decode; +namespace Toon.Format.Tests.Decode; [Trait("Category", "decode")] diff --git a/tests/ToonFormat.Tests/GeneratedTests/Decode/RootForm.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/RootForm.cs index 4dd1f6b..adc4efc 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Decode/RootForm.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Decode/RootForm.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Decode; +namespace Toon.Format.Tests.Decode; [Trait("Category", "decode")] diff --git a/tests/ToonFormat.Tests/GeneratedTests/Decode/ValidationErrors.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/ValidationErrors.cs index 622f096..1e9c836 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Decode/ValidationErrors.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Decode/ValidationErrors.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Decode; +namespace Toon.Format.Tests.Decode; [Trait("Category", "decode")] diff --git a/tests/ToonFormat.Tests/GeneratedTests/Decode/Whitespace.cs b/tests/ToonFormat.Tests/GeneratedTests/Decode/Whitespace.cs index 9f25c52..3b62570 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Decode/Whitespace.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Decode/Whitespace.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Decode; +namespace Toon.Format.Tests.Decode; [Trait("Category", "decode")] diff --git a/tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysNested.cs b/tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysNested.cs index 25ee4d0..c9a2a8c 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysNested.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysNested.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Encode; +namespace Toon.Format.Tests.Encode; [Trait("Category", "encode")] diff --git a/tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysObjects.cs b/tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysObjects.cs index 1a8bd44..440c511 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysObjects.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysObjects.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Encode; +namespace Toon.Format.Tests.Encode; [Trait("Category", "encode")] diff --git a/tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysPrimitive.cs b/tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysPrimitive.cs index add5fc7..1bdc5d9 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysPrimitive.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysPrimitive.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Encode; +namespace Toon.Format.Tests.Encode; [Trait("Category", "encode")] diff --git a/tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysTabular.cs b/tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysTabular.cs index 18da328..3fd3b97 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysTabular.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Encode/ArraysTabular.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Encode; +namespace Toon.Format.Tests.Encode; [Trait("Category", "encode")] diff --git a/tests/ToonFormat.Tests/GeneratedTests/Encode/Delimiters.cs b/tests/ToonFormat.Tests/GeneratedTests/Encode/Delimiters.cs index 0eaf500..5ffb29f 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Encode/Delimiters.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Encode/Delimiters.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Encode; +namespace Toon.Format.Tests.Encode; [Trait("Category", "encode")] diff --git a/tests/ToonFormat.Tests/GeneratedTests/Encode/KeyFolding.cs b/tests/ToonFormat.Tests/GeneratedTests/Encode/KeyFolding.cs index 81f47fe..44166c7 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Encode/KeyFolding.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Encode/KeyFolding.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Encode; +namespace Toon.Format.Tests.Encode; [Trait("Category", "encode")] diff --git a/tests/ToonFormat.Tests/GeneratedTests/Encode/Objects.cs b/tests/ToonFormat.Tests/GeneratedTests/Encode/Objects.cs index 2ff8277..5d29303 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Encode/Objects.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Encode/Objects.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Encode; +namespace Toon.Format.Tests.Encode; [Trait("Category", "encode")] diff --git a/tests/ToonFormat.Tests/GeneratedTests/Encode/Primitives.cs b/tests/ToonFormat.Tests/GeneratedTests/Encode/Primitives.cs index f01c217..abb2144 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Encode/Primitives.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Encode/Primitives.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Encode; +namespace Toon.Format.Tests.Encode; [Trait("Category", "encode")] @@ -255,7 +255,8 @@ public void EscapesCarriageReturnInString() { // Arrange var input = - @"return carriage" ; + @"return +carriage" ; var expected = """ diff --git a/tests/ToonFormat.Tests/GeneratedTests/Encode/Whitespace.cs b/tests/ToonFormat.Tests/GeneratedTests/Encode/Whitespace.cs index e64b71a..960c269 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Encode/Whitespace.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Encode/Whitespace.cs @@ -14,7 +14,7 @@ using Xunit; -namespace ToonFormat.Tests.Encode; +namespace Toon.Format.Tests.Encode; [Trait("Category", "encode")] diff --git a/tests/ToonFormat.Tests/ManualTests/Encode/ArraysObjectsManual.cs b/tests/ToonFormat.Tests/ManualTests/Encode/ArraysObjectsManual.cs index 929aad7..256e1cc 100644 --- a/tests/ToonFormat.Tests/ManualTests/Encode/ArraysObjectsManual.cs +++ b/tests/ToonFormat.Tests/ManualTests/Encode/ArraysObjectsManual.cs @@ -5,7 +5,7 @@ using Toon.Format; using Xunit; -namespace ToonFormat.Tests.Encode; +namespace Toon.Format.Tests.Encode; [Trait("Category", "encode")] public class ArraysObjectsManual diff --git a/tests/ToonFormat.Tests/ManualTests/JsonComplexRoundTripTests.cs b/tests/ToonFormat.Tests/ManualTests/JsonComplexRoundTripTests.cs index 9080e53..7212680 100644 --- a/tests/ToonFormat.Tests/ManualTests/JsonComplexRoundTripTests.cs +++ b/tests/ToonFormat.Tests/ManualTests/JsonComplexRoundTripTests.cs @@ -6,7 +6,7 @@ using Toon.Format; using Xunit.Abstractions; -namespace ToonFormat.Tests; +namespace Toon.Format.Tests; /// /// Tests for complex multi-level JSON structures to validate TOON format encoding and decoding. diff --git a/tests/ToonFormat.Tests/ManualTests/KeyFoldingTests.cs b/tests/ToonFormat.Tests/ManualTests/KeyFoldingTests.cs index 5c637e4..dc30719 100644 --- a/tests/ToonFormat.Tests/ManualTests/KeyFoldingTests.cs +++ b/tests/ToonFormat.Tests/ManualTests/KeyFoldingTests.cs @@ -1,6 +1,6 @@ using Toon.Format; -namespace ToonFormat.Tests; +namespace Toon.Format.Tests; // TODO: Remove these tests once generated spec tests are in source control // used to validate current key folding functionality aligns with spec diff --git a/tests/ToonFormat.Tests/ManualTests/PerformanceBenchmark.cs b/tests/ToonFormat.Tests/ManualTests/PerformanceBenchmark.cs index b27cef8..1f90417 100644 --- a/tests/ToonFormat.Tests/ManualTests/PerformanceBenchmark.cs +++ b/tests/ToonFormat.Tests/ManualTests/PerformanceBenchmark.cs @@ -5,7 +5,7 @@ using Xunit; using Xunit.Abstractions; -namespace ToonFormat.Tests; +namespace Toon.Format.Tests; public class PerformanceBenchmark { diff --git a/tests/ToonFormat.Tests/ManualTests/ToonAsyncTests.cs b/tests/ToonFormat.Tests/ManualTests/ToonAsyncTests.cs index b6db96e..3546d56 100644 --- a/tests/ToonFormat.Tests/ManualTests/ToonAsyncTests.cs +++ b/tests/ToonFormat.Tests/ManualTests/ToonAsyncTests.cs @@ -8,7 +8,7 @@ using Toon.Format; using Xunit; -namespace ToonFormat.Tests; +namespace Toon.Format.Tests; /// /// Tests for async encoding and decoding methods. diff --git a/tests/ToonFormat.Tests/ManualTests/ToonDecoderTests.cs b/tests/ToonFormat.Tests/ManualTests/ToonDecoderTests.cs index d2091af..217d9da 100644 --- a/tests/ToonFormat.Tests/ManualTests/ToonDecoderTests.cs +++ b/tests/ToonFormat.Tests/ManualTests/ToonDecoderTests.cs @@ -2,7 +2,7 @@ using System.Text.Json.Nodes; using Toon.Format; -namespace ToonFormat.Tests; +namespace Toon.Format.Tests; /// /// Tests for decoding TOON format strings. diff --git a/tests/ToonFormat.Tests/ManualTests/ToonEncoderTests.cs b/tests/ToonFormat.Tests/ManualTests/ToonEncoderTests.cs index c10c9ad..3203596 100644 --- a/tests/ToonFormat.Tests/ManualTests/ToonEncoderTests.cs +++ b/tests/ToonFormat.Tests/ManualTests/ToonEncoderTests.cs @@ -2,7 +2,7 @@ using System.Text.Json.Nodes; using Toon.Format; -namespace ToonFormat.Tests; +namespace Toon.Format.Tests; /// /// Tests for encoding data to TOON format. diff --git a/tests/ToonFormat.Tests/ManualTests/ToonRoundTripTests.cs b/tests/ToonFormat.Tests/ManualTests/ToonRoundTripTests.cs index 251ee95..7fff4ea 100644 --- a/tests/ToonFormat.Tests/ManualTests/ToonRoundTripTests.cs +++ b/tests/ToonFormat.Tests/ManualTests/ToonRoundTripTests.cs @@ -2,7 +2,7 @@ using System.Text.Json.Nodes; using Toon.Format; -namespace ToonFormat.Tests; +namespace Toon.Format.Tests; /// /// Round-trip tests to verify encoding and decoding preserve data integrity. diff --git a/tests/ToonFormat.Tests/ToonFormat.Tests.csproj b/tests/ToonFormat.Tests/ToonFormat.Tests.csproj index dcfb6b8..1ac006b 100644 --- a/tests/ToonFormat.Tests/ToonFormat.Tests.csproj +++ b/tests/ToonFormat.Tests/ToonFormat.Tests.csproj @@ -5,6 +5,7 @@ enable enable false + Toon.Format.Tests From e3fa6ade16a64d27fbb6dd041f590bc36bbd6562 Mon Sep 17 00:00:00 2001 From: Daniel Destouche Date: Wed, 24 Dec 2025 01:29:10 -0500 Subject: [PATCH 17/20] chore: Regenerate test file for appropriate line endings --- tests/ToonFormat.Tests/GeneratedTests/Encode/Primitives.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/ToonFormat.Tests/GeneratedTests/Encode/Primitives.cs b/tests/ToonFormat.Tests/GeneratedTests/Encode/Primitives.cs index abb2144..b302006 100644 --- a/tests/ToonFormat.Tests/GeneratedTests/Encode/Primitives.cs +++ b/tests/ToonFormat.Tests/GeneratedTests/Encode/Primitives.cs @@ -255,8 +255,7 @@ public void EscapesCarriageReturnInString() { // Arrange var input = - @"return -carriage" ; + @"return carriage" ; var expected = """ From 9c709b30884b59d1e540a5d75b11fb1feea2bbe6 Mon Sep 17 00:00:00 2001 From: Daniel Destouche Date: Wed, 24 Dec 2025 01:44:14 -0500 Subject: [PATCH 18/20] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a4ecd45..905333a 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ Converts TOON-formatted strings back to .NET objects. - `options` – Optional decoding options: - `Indent` – Number of spaces per indentation level (default: `2`) - `Strict` – Enable validation mode (default: `true`). When `true`, throws `ToonFormatException` on invalid input. - - `ExpandPaths` – Expand dotted keys: `"off"` (default) or `"safe"` + - `ExpandPaths` – Expand dotted keys: `ToonPathExpansion.Off` (default) or `ToonPathExpansion.Safe` **Returns:** From 1b1ba37177cd212e7ea393d69cf07c96ed5017ba Mon Sep 17 00:00:00 2001 From: Daniel Destouche Date: Wed, 24 Dec 2025 01:44:31 -0500 Subject: [PATCH 19/20] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 905333a..34a1182 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,7 @@ string toon = "a.b.c: 1"; var options = new ToonDecodeOptions { - ExpandPaths = "safe" + ExpandPaths = ToonPathExpansion.Safe }; var result = ToonDecoder.Decode(toon, options); From 3a5ef0fe590b36b13d0dc8547d22a71d02a1dcfa Mon Sep 17 00:00:00 2001 From: Daniel Destouche Date: Wed, 24 Dec 2025 01:49:20 -0500 Subject: [PATCH 20/20] chore: Code cleanup based on code review --- README.md | 4 ++-- src/ToonFormat/Options/ToonDecodeOptions.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a4ecd45..ecf60ce 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ **Token-Oriented Object Notation** is a compact, human-readable encoding of the JSON data model that minimizes tokens and makes structure easy for models to follow. Combines YAML-like indentation with CSV-like tabular arrays. Fully compatible with the [official TOON specification v3.0](https://github.com/toon-format/spec). -**Key Features:** Minimal syntax • TOON Encoding and Decoding • Tabular arrays for uniform data • Path expansion • Strict mode validation • .NET 8.0 & 9.0 • 370+ tests with 99.7% spec coverage. +**Key Features:** Minimal syntax • TOON Encoding and Decoding • Tabular arrays for uniform data • Path expansion • Strict mode validation • .NET 8.0, 9.0 and 10.0 • 520+ tests with 99.7% spec coverage. ## Quick Start @@ -253,7 +253,7 @@ This implementation: - Supports all TOON v3.0 features - Handles all edge cases and strict mode validations - Fully documented with XML comments -- Production-ready for .NET 8.0 and .NET 9.0 +- Production-ready for .NET 8.0, .NET 9.0 and .NET 10.0 See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines. diff --git a/src/ToonFormat/Options/ToonDecodeOptions.cs b/src/ToonFormat/Options/ToonDecodeOptions.cs index 96c00d6..cfcedf9 100644 --- a/src/ToonFormat/Options/ToonDecodeOptions.cs +++ b/src/ToonFormat/Options/ToonDecodeOptions.cs @@ -20,8 +20,8 @@ public class ToonDecodeOptions /// /// Controls path expansion for dotted keys. - /// "off" (default): Dotted keys are treated as literal keys. - /// "safe": Expand eligible dotted keys into nested objects. + /// (default): Dotted keys are treated as literal keys. + /// : Expand eligible dotted keys into nested objects. /// - public Format.ToonPathExpansion ExpandPaths { get; set; } = Format.ToonPathExpansion.Off; + public ToonPathExpansion ExpandPaths { get; set; } = ToonPathExpansion.Off; }