From 041df1b3ccc9fc0fba4566152d6ec94da5522c52 Mon Sep 17 00:00:00 2001 From: jamesshenry <79054685+jamesshenry@users.noreply.github.com> Date: Tue, 16 Dec 2025 15:46:21 +0000 Subject: [PATCH 01/17] remove irrelevant projects from branch --- Kuddle.slnx | 2 - sampleApp/Program.cs | 2 - sampleApp/sampleApp.csproj | 10 ----- .../Kuddle.Generators.csproj | 16 ------- src/Kuddle.Generators/TheGenerator.cs | 45 ------------------- 5 files changed, 75 deletions(-) delete mode 100644 sampleApp/Program.cs delete mode 100644 sampleApp/sampleApp.csproj delete mode 100644 src/Kuddle.Generators/Kuddle.Generators.csproj delete mode 100644 src/Kuddle.Generators/TheGenerator.cs diff --git a/Kuddle.slnx b/Kuddle.slnx index d6f6259..283aa83 100644 --- a/Kuddle.slnx +++ b/Kuddle.slnx @@ -5,9 +5,7 @@ - - diff --git a/sampleApp/Program.cs b/sampleApp/Program.cs deleted file mode 100644 index 3751555..0000000 --- a/sampleApp/Program.cs +++ /dev/null @@ -1,2 +0,0 @@ -// See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); diff --git a/sampleApp/sampleApp.csproj b/sampleApp/sampleApp.csproj deleted file mode 100644 index ed9781c..0000000 --- a/sampleApp/sampleApp.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - Exe - net10.0 - enable - enable - - - diff --git a/src/Kuddle.Generators/Kuddle.Generators.csproj b/src/Kuddle.Generators/Kuddle.Generators.csproj deleted file mode 100644 index f86a614..0000000 --- a/src/Kuddle.Generators/Kuddle.Generators.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - netstandard2.0 - latest - enable - enable - true - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - diff --git a/src/Kuddle.Generators/TheGenerator.cs b/src/Kuddle.Generators/TheGenerator.cs deleted file mode 100644 index 45aee6f..0000000 --- a/src/Kuddle.Generators/TheGenerator.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace Kuddle.Generators; - -[Generator] -public class TheGenerator : IIncrementalGenerator -{ - public void Initialize(IncrementalGeneratorInitializationContext context) - { - var provider = context - .SyntaxProvider.CreateSyntaxProvider( - predicate: static (node, _) => node is ClassDeclarationSyntax, - transform: static (ctx, _) => ctx.Node as ClassDeclarationSyntax - ) - .Where(m => m is not null); - - var compilation = context.CompilationProvider.Combine(provider.Collect()); - - context.RegisterSourceOutput(compilation, Execute); - } - - private void Execute( - SourceProductionContext context, - (Compilation Left, ImmutableArray Right) tuple - ) - { - var (compilation, list) = tuple; - - var theCode = """ -namespace ClassListGenerator; - -public static class ClassNames -{ - public static List Names = new() - { - "SomeClass" - }; -} -"""; - - context.AddSource("YourClassList.g.cs", theCode); - } -} From 511baffbd0f6dbb0857538e16204c2135ed90d10 Mon Sep 17 00:00:00 2001 From: jamesshenry <79054685+jamesshenry@users.noreply.github.com> Date: Tue, 16 Dec 2025 23:44:42 +0000 Subject: [PATCH 02/17] eip: rename types --- readme.md | 195 ++-------------- src/Kuddle.Benchmarks/ParserBenchmarks.cs | 4 +- src/Kuddle.Tests/Errors/ErrorHandlingTests.cs | 3 +- src/Kuddle.Tests/Extensions/DxTests.cs | 23 +- .../Grammar/CommentParserTests.cs | 6 +- src/Kuddle.Tests/Grammar/NodeParserTests.cs | 20 +- .../Grammar/NumberParsersTests.cs | 46 ++-- src/Kuddle.Tests/Grammar/StringParserTests.cs | 54 ++--- .../Grammar/WhiteSpaceParsersTests.cs | 16 +- .../Serialization/DocumentToObjectTests.cs | 3 + .../Serialization/KuddleParsingTests.cs | 219 +++++++++++------- .../Serialization/KuddleWriterTests.cs | 14 +- .../Serialization/NodeToObjectTests.cs | 127 ++++++++++ .../Serialization/ObjectMapperTests.cs | 59 +++-- .../Validation/ReservedTypeValidatorTests.cs | 15 +- src/Kuddle/AST/KdlDocument.cs | 6 +- ...NodeExtensions.cs => KdlNodeExtensions.cs} | 2 +- ...lueExtensions.cs => KdlValueExtensions.cs} | 2 +- src/Kuddle/Extensions/SpanExtensions.cs | 18 +- .../{KuddleGrammar.cs => KdlGrammar.cs} | 5 +- .../Attributes/KdlPropertyAttribute.cs | 10 + .../{KuddleReader.cs => KdlReader.cs} | 22 +- src/Kuddle/Serialization/KdlReaderOptions.cs | 7 + .../{Attributes => }/KdlSerializer.cs | 120 +++++----- .../{KuddleWriter.cs => KdlWriter.cs} | 12 +- ...leWriterOptions.cs => KdlWriterOptions.cs} | 4 +- .../Serialization/KuddleReaderOptions.cs | 7 - .../Validation/KuddleReservedTypeValidator.cs | 8 +- 28 files changed, 531 insertions(+), 496 deletions(-) create mode 100644 src/Kuddle.Tests/Serialization/DocumentToObjectTests.cs create mode 100644 src/Kuddle.Tests/Serialization/NodeToObjectTests.cs rename src/Kuddle/Extensions/{KuddleNodeExtensions.cs => KdlNodeExtensions.cs} (97%) rename src/Kuddle/Extensions/{KuddleValueExtensions.cs => KdlValueExtensions.cs} (98%) rename src/Kuddle/Parser/{KuddleGrammar.cs => KdlGrammar.cs} (99%) rename src/Kuddle/Serialization/{KuddleReader.cs => KdlReader.cs} (71%) create mode 100644 src/Kuddle/Serialization/KdlReaderOptions.cs rename src/Kuddle/Serialization/{Attributes => }/KdlSerializer.cs (81%) rename src/Kuddle/Serialization/{KuddleWriter.cs => KdlWriter.cs} (95%) rename src/Kuddle/Serialization/{KuddleWriterOptions.cs => KdlWriterOptions.cs} (76%) delete mode 100644 src/Kuddle/Serialization/KuddleReaderOptions.cs diff --git a/readme.md b/readme.md index d85a754..df52e51 100644 --- a/readme.md +++ b/readme.md @@ -1,188 +1,27 @@ -```text -document := bom? version? nodes - -// Nodes -nodes := (line-space* node)* line-space* - -base-node := slashdash? type? node-space* string - (node-space+ slashdash? node-prop-or-arg)* - // slashdashed node-children must always be after props and args. - (node-space+ slashdash node-children)* - (node-space+ node-children)? - (node-space+ slashdash node-children)* - node-space* -node := base-node node-terminator -final-node := base-node node-terminator? - -// Entries -node-prop-or-arg := prop | value -node-children := '{' nodes final-node? '}' -node-terminator := single-line-comment | newline | ';' | eof - -prop := string node-space* '=' node-space* value -value := type? node-space* (string | number | keyword) -type := '(' node-space* string node-space* ')' - -// Strings -string := identifier-string | quoted-string | raw-string ¶ - -identifier-string := unambiguous-ident | signed-ident | dotted-ident -unambiguous-ident := - ((identifier-char - digit - sign - '.') identifier-char*) - - disallowed-keyword-strings -signed-ident := - sign ((identifier-char - digit - '.') identifier-char*)? -dotted-ident := - sign? '.' ((identifier-char - digit) identifier-char*)? -identifier-char := - unicode - unicode-space - newline - [\\/(){};\[\]"#=] - - disallowed-literal-code-points -disallowed-keyword-identifiers := - 'true' | 'false' | 'null' | 'inf' | '-inf' | 'nan' - -quoted-string := - '"' single-line-string-body '"' | - '"""' newline - (multi-line-string-body newline)? - (unicode-space | ws-escape)* '"""' -single-line-string-body := (string-character - newline)* -multi-line-string-body := (('"' | '""')? string-character)* -string-character := - '\\' (["\\bfnrts] | - 'u{' hex-unicode '}') | - ws-escape | - [^\\"] - disallowed-literal-code-points -ws-escape := '\\' (unicode-space | newline)+ -hex-digit := [0-9a-fA-F] -hex-unicode := hex-digit{1, 6} - surrogates -surrogates := [dD][8-9a-fA-F]hex-digit{2} -// U+D800-DFFF: D 8 00 -// D F FF - -raw-string := '#' raw-string-quotes '#' | '#' raw-string '#' -raw-string-quotes := - '"' single-line-raw-string-body '"' | - '"""' newline - (multi-line-raw-string-body newline)? - unicode-space* '"""' -single-line-raw-string-body := - '' | - (single-line-raw-string-char - '"') - single-line-raw-string-char*? | - '"' (single-line-raw-string-char - '"') - single-line-raw-string-char*? -single-line-raw-string-char := - unicode - newline - disallowed-literal-code-points -multi-line-raw-string-body := - (unicode - disallowed-literal-code-points)*? - -// Numbers -number := keyword-number | hex | octal | binary | decimal +# Kuddle.Net -decimal := sign? integer ('.' integer)? exponent? -exponent := ('e' | 'E') sign? integer -integer := digit (digit | '_')* -digit := [0-9] -sign := '+' | '-' +Kuddle.Net is a .NET implementation of a [KDL](https://kdl.dev) parser/serializer targeting [v2](https://kdl.dev/spec/) of the spec. +KDL is concise, human-readable language built for configuration and data exchange. Head to for more specifics on the KDL document language itself. -hex := sign? '0x' hex-digit (hex-digit | '_')* -octal := sign? '0o' [0-7] [0-7_]* -binary := sign? '0b' ('0' | '1') ('0' | '1' | '_')* +## Installation -// Keywords and booleans. -keyword := boolean | '#null' -keyword-number := '#inf' | '#-inf' | '#nan' -boolean := '#true' | '#false' - -// Specific code points -bom := '\u{FEFF}' -disallowed-literal-code-points := - See Table (Disallowed Literal Code Points) -unicode := Any Unicode Scalar Value -unicode-space := See Table - (All White_Space unicode characters which are not `newline`) - -// Comments -single-line-comment := '//' ^newline* (newline | eof) -multi-line-comment := '/*' commented-block -commented-block := - '*/' | (multi-line-comment | '*' | '/' | [^*/]+) commented-block -slashdash := '/-' line-space* - -// Whitespace -ws := unicode-space | multi-line-comment -escline := '\\' ws* (single-line-comment | newline | eof) -newline := See Table (All Newline White_Space) -// Whitespace where newlines are allowed. -line-space := node-space | newline | single-line-comment -// Whitespace within nodes, -// where newline-ish things must be esclined. -node-space := ws* escline ws* | ws+ - -// Version marker -version := - '/-' unicode-space* 'kdl-version' unicode-space+ ('1' | '2') - unicode-space* newline +```text +dotnet add package Kuddle.Net ``` -## Whitespace +## Usage -The following characters should be treated as non-Newline ({{newline}}) [white -space](https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt): +There are a few ways of using the library, the lower level `KdlReader` and `KdlWriter` classes, and the utility `KdlSerializer` class. For most use cases `KdlSerializer.Serialize`/`KdlSerializer.Deserialize` will be sufficient. -| Name | Code Pt | -| ------------------------- | -------- | -| Character Tabulation | `U+0009` | -| Space | `U+0020` | -| No-Break Space | `U+00A0` | -| Ogham Space Mark | `U+1680` | -| En Quad | `U+2000` | -| Em Quad | `U+2001` | -| En Space | `U+2002` | -| Em Space | `U+2003` | -| Three-Per-Em Space | `U+2004` | -| Four-Per-Em Space | `U+2005` | -| Six-Per-Em Space | `U+2006` | -| Figure Space | `U+2007` | -| Punctuation Space | `U+2008` | -| Thin Space | `U+2009` | -| Hair Space | `U+200A` | -| Narrow No-Break Space | `U+202F` | -| Medium Mathematical Space | `U+205F` | -| Ideographic Space | `U+3000` | +```cs +var dbKdl = """ +database main port=5432 +"""; -## Newline - -The following character sequences [should be treated as new -lines](https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-5/#G41643): - -| Acronym | Name | Code Pt | -| ------- | ----------------------------- | ------------------- | -| CRLF | Carriage Return and Line Feed | `U+000D` + `U+000A` | -| CR | Carriage Return | `U+000D` | -| LF | Line Feed | `U+000A` | -| NEL | Next Line | `U+0085` | -| VT | Vertical tab | `U+000B` | -| FF | Form Feed | `U+000C` | -| LS | Line Separator | `U+2028` | -| PS | Paragraph Separator | `U+2029` | - -Note that for the purpose of new lines, the specific sequence `CRLF` is -considered _a single newline_. - -## Disallowed Literal Code Points +var dbConfig = KdlSerializer.Deserialize(dbKdl); +``` -The following code points may not appear literally anywhere in the document. -They may be represented in Strings (but not Raw Strings) using Unicode Escapes ({{escapes}}) (`\u{...}`, -except for non Unicode Scalar Value, which can't be represented even as escapes). +KDL differs from other configuration languages like +yaml or toml in that it is node-based. The top-level document can consist of a collection of nodes, not args (e.g. `#true`, `#false`, `0xAF`) or properties (`key=#false`). To adhere to this, the serialization API depends on the use of several provided attributes to facilitate mapping `KDL` constructs to user defined types: -- The codepoints `U+0000-0008` or the codepoints `U+000E-001F` (various - control characters). -- `U+007F` (the Delete control character). -- Any codepoint that is not a [Unicode Scalar - Value](https://unicode.org/glossary/#unicode_scalar_value) (`U+D800-DFFF`). -- `U+200E-200F`, `U+202A-202E`, and `U+2066-2069`, the [unicode - "direction control" - characters](https://www.w3.org/International/questions/qa-bidi-unicode-controls) -- `U+FEFF`, aka Zero-width Non-breaking Space (ZWNBSP)/Byte Order Mark (BOM), - except as the first code point in a document. +### Attributes diff --git a/src/Kuddle.Benchmarks/ParserBenchmarks.cs b/src/Kuddle.Benchmarks/ParserBenchmarks.cs index d3ba66f..72b9743 100644 --- a/src/Kuddle.Benchmarks/ParserBenchmarks.cs +++ b/src/Kuddle.Benchmarks/ParserBenchmarks.cs @@ -64,8 +64,8 @@ port 8080 } _largeDocument = largeDocBuilder.ToString(); - _compiledParser = KuddleGrammar.Document.Compile(); - _nonCompiledParser = KuddleGrammar.Document; + _compiledParser = KdlGrammar.Document.Compile(); + _nonCompiledParser = KdlGrammar.Document; } [Benchmark] diff --git a/src/Kuddle.Tests/Errors/ErrorHandlingTests.cs b/src/Kuddle.Tests/Errors/ErrorHandlingTests.cs index 0c4a7bb..84c6ec0 100644 --- a/src/Kuddle.Tests/Errors/ErrorHandlingTests.cs +++ b/src/Kuddle.Tests/Errors/ErrorHandlingTests.cs @@ -1,4 +1,5 @@ using Kuddle.Exceptions; +using Kuddle.Serialization; namespace Kuddle.Tests.Errors; @@ -12,7 +13,7 @@ private static async Task AssertParseFails( int? expectedLine = null ) { - var ex = await Assert.ThrowsAsync(async () => KuddleReader.Parse(kdl)); + var ex = await Assert.ThrowsAsync(async () => KdlReader.Read(kdl)); await Assert .That(ex.Message) diff --git a/src/Kuddle.Tests/Extensions/DxTests.cs b/src/Kuddle.Tests/Extensions/DxTests.cs index a15c293..aa230db 100644 --- a/src/Kuddle.Tests/Extensions/DxTests.cs +++ b/src/Kuddle.Tests/Extensions/DxTests.cs @@ -1,6 +1,7 @@ using Kuddle.AST; using Kuddle.Extensions; using Kuddle.Parser; +using Kuddle.Serialization; namespace Kuddle.Tests.Extensions; @@ -9,7 +10,7 @@ public class DxTests [Test] public async Task TryGet_Int_Success() { - var doc = KuddleReader.Parse("node 123"); + var doc = KdlReader.Read("node 123"); var val = doc.Nodes[0].Arg(0)!; bool success = val.TryGetInt(out int result); @@ -21,7 +22,7 @@ public async Task TryGet_Int_Success() [Test] public async Task TryGet_Int_Failure_WrongType() { - var doc = KuddleReader.Parse("node \"hello\""); + var doc = KdlReader.Read("node \"hello\""); var val = doc.Nodes[0].Arg(0)!; bool success = val.TryGetInt(out int result); @@ -34,7 +35,7 @@ public async Task TryGet_Int_Failure_WrongType() public async Task TryGet_Int_Failure_Overflow() { // Value larger than Int32 - var doc = KuddleReader.Parse("node 9999999999"); + var doc = KdlReader.Read("node 9999999999"); var val = doc.Nodes[0].Arg(0)!; bool success = val.TryGetInt(out int result); @@ -45,7 +46,7 @@ public async Task TryGet_Int_Failure_Overflow() [Test] public async Task TryGet_Prop_Navigation_Success() { - var doc = KuddleReader.Parse("server port=8080"); + var doc = KdlReader.Read("server port=8080"); var node = doc.Nodes[0]; // Combine finding the prop and converting it @@ -64,7 +65,7 @@ public async Task TryGet_Prop_Navigation_Success() [Test] public async Task TryGet_Prop_Navigation_Missing() { - var doc = KuddleReader.Parse("server host=\"localhost\""); + var doc = KdlReader.Read("server host=\"localhost\""); var node = doc.Nodes[0]; var propVal = node.Prop("port")!; // Returns KdlNull @@ -78,7 +79,7 @@ public async Task TryGetUuid_ValidGuidString_ReturnsTrue() { var expected = Guid.NewGuid(); var kdl = $"node \"{expected}\""; // e.g. "f81d4fae-7dec-11d0-a765-00a0c91e6bf6" - var doc = KuddleReader.Parse(kdl); + var doc = KdlReader.Read(kdl); var val = doc.Nodes[0].Arg(0)!; bool success = val.TryGetUuid(out var result); @@ -90,7 +91,7 @@ public async Task TryGetUuid_ValidGuidString_ReturnsTrue() [Test] public async Task TryGetUuid_InvalidString_ReturnsFalse() { - var doc = KuddleReader.Parse("node \"not-a-guid\""); + var doc = KdlReader.Read("node \"not-a-guid\""); var val = doc.Nodes[0].Arg(0)!; bool success = val.TryGetUuid(out var result); @@ -104,7 +105,7 @@ public async Task TryGetUuid_FromAnnotatedValue_ReturnsTrue() { var expected = Guid.NewGuid(); var kdl = $"node (uuid)\"{expected}\""; - var doc = KuddleReader.Parse(kdl); + var doc = KdlReader.Read(kdl); var val = doc.Nodes[0].Arg(0)!; bool success = val.TryGetUuid(out var result); @@ -138,7 +139,7 @@ public async Task TryGetDateTime_ValidIso8601_ReturnsTrue() var now = DateTimeOffset.UtcNow; // Round-trip format "O" is standard for KDL/JSON var kdl = $"node \"{now:O}\""; - var doc = KuddleReader.Parse(kdl); + var doc = KdlReader.Read(kdl); var val = doc.Nodes[0].Arg(0)!; bool success = val.TryGetDateTime(out var result); @@ -153,7 +154,7 @@ public async Task TryGetDateTime_DateOnly_ReturnsTrue() { // YYYY-MM-DD var kdl = "node \"2023-10-25\""; - var doc = KuddleReader.Parse(kdl); + var doc = KdlReader.Read(kdl); var val = doc.Nodes[0].Arg(0)!; bool success = val.TryGetDateTime(out var result); @@ -167,7 +168,7 @@ public async Task TryGetDateTime_DateOnly_ReturnsTrue() [Test] public async Task TryGetDateTime_InvalidString_ReturnsFalse() { - var doc = KuddleReader.Parse("node \"tomorrow\""); + var doc = KdlReader.Read("node \"tomorrow\""); var val = doc.Nodes[0].Arg(0)!; bool success = val.TryGetDateTime(out var result); diff --git a/src/Kuddle.Tests/Grammar/CommentParserTests.cs b/src/Kuddle.Tests/Grammar/CommentParserTests.cs index 6448ef5..29b7750 100644 --- a/src/Kuddle.Tests/Grammar/CommentParserTests.cs +++ b/src/Kuddle.Tests/Grammar/CommentParserTests.cs @@ -7,7 +7,7 @@ public class CommentParserTests [Test] public async Task CanParseSimpleSingleLineComment() { - var sut = KuddleGrammar.SingleLineComment; + var sut = KdlGrammar.SingleLineComment; var comment = """// I am a single line comment"""; @@ -20,7 +20,7 @@ public async Task CanParseSimpleSingleLineComment() [Test] public async Task CanParseSimpleMultiLineComment() { - var sut = KuddleGrammar.MultiLineComment; + var sut = KdlGrammar.MultiLineComment; var comment = """ /* @@ -38,7 +38,7 @@ public async Task CanParseSimpleMultiLineComment() [Test] public async Task CanParseNestedMultiLineComment() { - var sut = KuddleGrammar.MultiLineComment; + var sut = KdlGrammar.MultiLineComment; var comment = """ /* diff --git a/src/Kuddle.Tests/Grammar/NodeParserTests.cs b/src/Kuddle.Tests/Grammar/NodeParserTests.cs index b9745fb..a8f59d7 100644 --- a/src/Kuddle.Tests/Grammar/NodeParserTests.cs +++ b/src/Kuddle.Tests/Grammar/NodeParserTests.cs @@ -10,7 +10,7 @@ public class NodeParsersTests [Test] public async Task Type_ParsesSimpleType() { - var sut = KuddleGrammar.Type; + var sut = KdlGrammar.Type; var input = "(string)"; bool success = sut.TryParse(input, out var result); @@ -22,7 +22,7 @@ public async Task Type_ParsesSimpleType() [Test] public async Task Prop_ParsesSimpleProperty() { - var sut = KuddleGrammar.Node; + var sut = KdlGrammar.Node; var input = "node key=value"; bool success = sut.TryParse(input, out var node); @@ -39,7 +39,7 @@ public async Task Prop_ParsesSimpleProperty() [Test] public async Task Node_ParsesComplexLine() { - var sut = KuddleGrammar.Node; + var sut = KdlGrammar.Node; // Test: Name, Arg, Prop, Type Annotation var input = "(my-type)node 123 key=\"value\";"; @@ -69,7 +69,7 @@ public async Task Node_ParsesComplexLine() [Test] public async Task Node_ParsesChildren() { - var sut = KuddleGrammar.Node; + var sut = KdlGrammar.Node; var input = "parent { child; }"; bool success = sut.TryParse(input, out var node, out var error); @@ -84,7 +84,7 @@ public async Task Node_ParsesChildren() [Test] public async Task Node_ParsesMixedContent() { - var sut = KuddleGrammar.Node; + var sut = KdlGrammar.Node; var input = "(type)node 10 prop=#true { child; }"; bool success = sut.TryParse(input, out var node); @@ -111,7 +111,7 @@ await Assert public async Task Node_SlashDash_SkipsNode() { // This tests the logic in 'Nodes' (plural) parser usually - var sut = KuddleGrammar.Document; + var sut = KdlGrammar.Document; var input = "node1; /- node2; node3;"; bool success = sut.TryParse(input, out var doc); @@ -125,7 +125,7 @@ public async Task Node_SlashDash_SkipsNode() [Test] public async Task Node_SlashDash_SkipsArg() { - var sut = KuddleGrammar.Node; + var sut = KdlGrammar.Node; var input = "node 1 /- 2 3"; bool success = sut.TryParse(input, out var node); @@ -145,7 +145,7 @@ public async Task Node_SlashDash_SkipsArg() [Test] public async Task SlashDash_SkipsProperty() { - var sut = KuddleGrammar.Node; + var sut = KdlGrammar.Node; var input = "node key=1 /- skipped=2 valid=3"; bool success = sut.TryParse(input, out var node); @@ -163,7 +163,7 @@ public async Task SlashDash_SkipsProperty() [Test] public async Task Node_SlashDash_SkipsChildrenBlock() { - var sut = KuddleGrammar.Node; + var sut = KdlGrammar.Node; // Parsing a node that has a slash-dashed children block var input = "node /- { child; }"; @@ -178,7 +178,7 @@ public async Task Node_SlashDash_SkipsChildrenBlock() [Test] public async Task Nodes_ParsesNodesWithWhitespace() { - var sut = KuddleGrammar.Nodes; + var sut = KdlGrammar.Nodes; var input = @" node1; diff --git a/src/Kuddle.Tests/Grammar/NumberParsersTests.cs b/src/Kuddle.Tests/Grammar/NumberParsersTests.cs index e9ea2c8..6edcdef 100644 --- a/src/Kuddle.Tests/Grammar/NumberParsersTests.cs +++ b/src/Kuddle.Tests/Grammar/NumberParsersTests.cs @@ -7,7 +7,7 @@ public class NumberParsersTests [Test] public async Task Decimal_ParsesPositiveInteger() { - var sut = KuddleGrammar.Decimal; + var sut = KdlGrammar.Decimal; var input = "42"; bool success = sut.TryParse(input, out var value); @@ -19,7 +19,7 @@ public async Task Decimal_ParsesPositiveInteger() [Test] public async Task Decimal_ParsesNegativeInteger() { - var sut = KuddleGrammar.Decimal; + var sut = KdlGrammar.Decimal; var input = "-42"; bool success = sut.TryParse(input, out var value); @@ -31,7 +31,7 @@ public async Task Decimal_ParsesNegativeInteger() [Test] public async Task Decimal_ParsesFractionalNumber() { - var sut = KuddleGrammar.Decimal; + var sut = KdlGrammar.Decimal; var input = "3.14159"; bool success = sut.TryParse(input, out var value); @@ -43,7 +43,7 @@ public async Task Decimal_ParsesFractionalNumber() [Test] public async Task Decimal_ParsesScientificNotation() { - var sut = KuddleGrammar.Decimal; + var sut = KdlGrammar.Decimal; var input = "1.23e-4"; bool success = sut.TryParse(input, out var value); @@ -55,7 +55,7 @@ public async Task Decimal_ParsesScientificNotation() [Test] public async Task Decimal_ParsesScientificNotationUppercase() { - var sut = KuddleGrammar.Decimal; + var sut = KdlGrammar.Decimal; var input = "6.02E23"; bool success = sut.TryParse(input, out var value); @@ -67,7 +67,7 @@ public async Task Decimal_ParsesScientificNotationUppercase() [Test] public async Task Decimal_ParsesWithUnderscoreSeparators() { - var sut = KuddleGrammar.Decimal; + var sut = KdlGrammar.Decimal; var input = "1_000_000"; bool success = sut.TryParse(input, out var value); @@ -79,7 +79,7 @@ public async Task Decimal_ParsesWithUnderscoreSeparators() [Test] public async Task Decimal_ParsesFractionalWithUnderscores() { - var sut = KuddleGrammar.Decimal; + var sut = KdlGrammar.Decimal; var input = "12_34.56_78"; bool success = sut.TryParse(input, out var value); @@ -91,7 +91,7 @@ public async Task Decimal_ParsesFractionalWithUnderscores() [Test] public async Task Hex_ParsesHexNumbers() { - var sut = KuddleGrammar.Hex; + var sut = KdlGrammar.Hex; var input = "0xFF"; bool success = sut.TryParse(input, out var value); @@ -103,7 +103,7 @@ public async Task Hex_ParsesHexNumbers() [Test] public async Task Hex_ParsesHexWithUnderscores() { - var sut = KuddleGrammar.Hex; + var sut = KdlGrammar.Hex; var input = "0x123_ABC"; bool success = sut.TryParse(input, out var value); @@ -115,7 +115,7 @@ public async Task Hex_ParsesHexWithUnderscores() [Test] public async Task Hex_ParsesNegativeHex() { - var sut = KuddleGrammar.Hex; + var sut = KdlGrammar.Hex; var input = "-0x42"; bool success = sut.TryParse(input, out var value); @@ -127,7 +127,7 @@ public async Task Hex_ParsesNegativeHex() [Test] public async Task Octal_ParsesOctalNumbers() { - var sut = KuddleGrammar.Octal; + var sut = KdlGrammar.Octal; var input = "0o777"; bool success = sut.TryParse(input, out var value); @@ -139,7 +139,7 @@ public async Task Octal_ParsesOctalNumbers() [Test] public async Task Octal_ParsesOctalWithUnderscores() { - var sut = KuddleGrammar.Octal; + var sut = KdlGrammar.Octal; var input = "0o123_456"; bool success = sut.TryParse(input, out var value); @@ -151,7 +151,7 @@ public async Task Octal_ParsesOctalWithUnderscores() [Test] public async Task Octal_ParsesNegativeOctal() { - var sut = KuddleGrammar.Octal; + var sut = KdlGrammar.Octal; var input = "-0o42"; bool success = sut.TryParse(input, out var value); @@ -163,7 +163,7 @@ public async Task Octal_ParsesNegativeOctal() [Test] public async Task Binary_ParsesBinaryNumbers() { - var sut = KuddleGrammar.Binary; + var sut = KdlGrammar.Binary; var input = "0b1010"; bool success = sut.TryParse(input, out var value); @@ -175,7 +175,7 @@ public async Task Binary_ParsesBinaryNumbers() [Test] public async Task Binary_ParsesBinaryWithUnderscores() { - var sut = KuddleGrammar.Binary; + var sut = KdlGrammar.Binary; var input = "0b1111_0000"; bool success = sut.TryParse(input, out var value); @@ -187,7 +187,7 @@ public async Task Binary_ParsesBinaryWithUnderscores() [Test] public async Task Binary_ParsesNegativeBinary() { - var sut = KuddleGrammar.Binary; + var sut = KdlGrammar.Binary; var input = "-0b101"; bool success = sut.TryParse(input, out var value); @@ -199,7 +199,7 @@ public async Task Binary_ParsesNegativeBinary() [Test] public async Task KeywordNumber_ParsesInfinity() { - var sut = KuddleGrammar.KeywordNumber; + var sut = KdlGrammar.KeywordNumber; var input = "#inf"; bool success = sut.TryParse(input, out var value); @@ -211,7 +211,7 @@ public async Task KeywordNumber_ParsesInfinity() [Test] public async Task KeywordNumber_ParsesNegativeInfinity() { - var sut = KuddleGrammar.KeywordNumber; + var sut = KdlGrammar.KeywordNumber; var input = "#-inf"; bool success = sut.TryParse(input, out var value); @@ -223,7 +223,7 @@ public async Task KeywordNumber_ParsesNegativeInfinity() [Test] public async Task KeywordNumber_ParsesNaN() { - var sut = KuddleGrammar.KeywordNumber; + var sut = KdlGrammar.KeywordNumber; var input = "#nan"; bool success = sut.TryParse(input, out var value); @@ -235,7 +235,7 @@ public async Task KeywordNumber_ParsesNaN() [Test] public async Task Number_ParsesDecimal() { - var sut = KuddleGrammar.Decimal; + var sut = KdlGrammar.Decimal; var input = "42"; bool success = sut.TryParse(input, out var value); @@ -247,7 +247,7 @@ public async Task Number_ParsesDecimal() [Test] public async Task Number_ParsesHex() { - var sut = KuddleGrammar.Hex; + var sut = KdlGrammar.Hex; var input = "0xFF"; bool success = sut.TryParse(input, out var value); @@ -259,7 +259,7 @@ public async Task Number_ParsesHex() [Test] public async Task Number_ParsesKeywordNumber() { - var sut = KuddleGrammar.KeywordNumber; + var sut = KdlGrammar.KeywordNumber; var input = "#inf"; bool success = sut.TryParse(input, out var value); @@ -271,7 +271,7 @@ public async Task Number_ParsesKeywordNumber() [Test] public async Task Decimal_RejectsDoubleDots() { - var sut = KuddleGrammar.Decimal.Eof(); + var sut = KdlGrammar.Decimal.Eof(); var input = "12.34.56"; bool success = sut.TryParse(input, out var value); diff --git a/src/Kuddle.Tests/Grammar/StringParserTests.cs b/src/Kuddle.Tests/Grammar/StringParserTests.cs index 88050cc..622221f 100644 --- a/src/Kuddle.Tests/Grammar/StringParserTests.cs +++ b/src/Kuddle.Tests/Grammar/StringParserTests.cs @@ -20,7 +20,7 @@ public class StringParserTests [Arguments("-negative")] public async Task SignedIdent_ParsesSignedIdentifier(string input) { - var sut = KuddleGrammar.SignedIdent; + var sut = KdlGrammar.SignedIdent; bool success = sut.TryParse(input, out var value); @@ -34,7 +34,7 @@ public async Task SignedIdent_ParsesSignedIdentifier(string input) [Arguments(".three")] public async Task DottedIdent_ParsesDottedIdentifier(string input) { - var sut = KuddleGrammar.DottedIdent; + var sut = KdlGrammar.DottedIdent; bool success = sut.TryParse(input, out var value); @@ -47,7 +47,7 @@ public async Task DottedIdent_ParsesDottedIdentifier(string input) [Arguments(".01")] public async Task DottedIdent_DoesNotParseNumberDottedNumber(string input) { - var sut = KuddleGrammar.DottedIdent; + var sut = KdlGrammar.DottedIdent; bool success = sut.TryParse(input, out var value); @@ -62,7 +62,7 @@ public async Task DottedIdent_DoesNotParseNumberDottedNumber(string input) [Arguments("test_case")] public async Task UnambiguousIdent_ParsesUnambiguousIdentifier(string input) { - var sut = KuddleGrammar.UnambiguousIdent; + var sut = KdlGrammar.UnambiguousIdent; bool success = sut.TryParse(input, out var value); @@ -76,7 +76,7 @@ public async Task UnambiguousIdent_ParsesUnambiguousIdentifier(string input) [Arguments(".one")] public async Task UnambiguousIdent_DoesNotParseInvalidIdentifier(string input) { - var sut = KuddleGrammar.UnambiguousIdent; + var sut = KdlGrammar.UnambiguousIdent; bool success = sut.TryParse(input, out var value); @@ -92,7 +92,7 @@ public async Task UnambiguousIdent_DoesNotParseInvalidIdentifier(string input) [Arguments("nan")] public async Task UnambiguousIdent_DoesNotParseDisallowedKeywordString(string input) { - var sut = KuddleGrammar.UnambiguousIdent; + var sut = KdlGrammar.UnambiguousIdent; bool success = sut.TryParse(input, out var value); @@ -112,7 +112,7 @@ public async Task UnambiguousIdent_DoesNotParseDisallowedKeywordString(string in [Arguments("-negative")] public async Task IdentifierString_ParsesIdentifierString(string input) { - var sut = KuddleGrammar.IdentifierString; + var sut = KdlGrammar.IdentifierString; bool success = sut.TryParse(input, out var value); @@ -124,7 +124,7 @@ public async Task IdentifierString_ParsesIdentifierString(string input) [Arguments("\"\\ \"")] public async Task WsEscape_ParsesWhiteSpace(string input) { - var sut = KuddleGrammar.QuotedString; + var sut = KdlGrammar.QuotedString; bool success = sut.TryParse(input, out var value, out var error); @@ -135,7 +135,7 @@ public async Task WsEscape_ParsesWhiteSpace(string input) [Test] public async Task QuotedString_ParsesSingleLineString() { - var sut = KuddleGrammar.QuotedString; + var sut = KdlGrammar.QuotedString; const string input = """ "hello world" @@ -149,7 +149,7 @@ public async Task QuotedString_ParsesSingleLineString() [Test] public async Task QuotedString_ParsesEmptyString() { - var sut = KuddleGrammar.QuotedString; + var sut = KdlGrammar.QuotedString; const string input = """ "" @@ -163,7 +163,7 @@ public async Task QuotedString_ParsesEmptyString() [Test] public async Task QuotedString_ParsesEmptyMultilineString() { - var sut = KuddleGrammar.QuotedString; + var sut = KdlGrammar.QuotedString; const string input = """" """ @@ -206,7 +206,7 @@ This is the base indentation )] public async Task MultiLineStringBody_HandlesVarious(string input, string expected) { - var sut = KuddleGrammar.MultiLineQuoted; + var sut = KdlGrammar.MultiLineQuoted; bool success = sut.TryParse(input, out var value); Debug.WriteLine(input); @@ -234,7 +234,7 @@ canis canem edit )] public async Task MultiLineQuotedString_CanParseMultiLine(string input, string expected) { - var sut = KuddleGrammar.MultiLineQuoted; + var sut = KdlGrammar.MultiLineQuoted; bool success = sut.TryParse(input, out var value, out var error); await Assert.That(success).IsTrue(); await Assert.That(value.ToString()).IsEqualTo(expected); @@ -249,7 +249,7 @@ public async Task MultiLineQuotedString_CanParseMultiLine(string input, string e )] public async Task QuotedString_HandlesUnicodeEscapes(string input, string expected) { - var sut = KuddleGrammar.QuotedString; + var sut = KdlGrammar.QuotedString; bool success = sut.TryParse(input, out var value); @@ -271,7 +271,7 @@ public async Task QuotedString_HandlesUnicodeEscapes(string input, string expect )] public async Task QuotedString_HandlesWhitespaceEscapes(string input) { - var sut = KuddleGrammar.QuotedString; + var sut = KdlGrammar.QuotedString; bool success = sut.TryParse(input, out var value, out var error); @@ -283,7 +283,7 @@ public async Task QuotedString_HandlesWhitespaceEscapes(string input) [Test] public async Task RawString_ParsesSimpleRawString() { - var sut = KuddleGrammar.RawString; + var sut = KdlGrammar.RawString; var input = """ #"\n will be literal"# @@ -297,7 +297,7 @@ public async Task RawString_ParsesSimpleRawString() [Test] public async Task RawString_ParsesRawStringWithQuotes() { - var sut = KuddleGrammar.RawString; + var sut = KdlGrammar.RawString; var input = "#\"content with \"quotes\"\"#"; bool success = sut.TryParse(input, out var value); @@ -309,7 +309,7 @@ public async Task RawString_ParsesRawStringWithQuotes() [Test] public async Task RawString_HandlesMultipleHashDelimiters() { - var sut = KuddleGrammar.RawString; + var sut = KdlGrammar.RawString; var input = """ ##"hello\n\r\asd"#world"## @@ -329,7 +329,7 @@ await Assert [Test] public async Task RawString_ParsesMultiLineRawString() { - var sut = KuddleGrammar.RawString; + var sut = KdlGrammar.RawString; var input = """"" #""" @@ -354,7 +354,7 @@ without escapes. [Test] public async Task IdentifierString_SetsStyleToBare() { - var sut = KuddleGrammar.IdentifierString; + var sut = KdlGrammar.IdentifierString; var input = "bare_identifier"; bool success = sut.TryParse(input, out var value); @@ -367,7 +367,7 @@ public async Task IdentifierString_SetsStyleToBare() [Test] public async Task QuotedString_SetsStyleToQuoted() { - var sut = KuddleGrammar.QuotedString; + var sut = KdlGrammar.QuotedString; var input = "\"quoted value\""; bool success = sut.TryParse(input, out var value); @@ -381,7 +381,7 @@ public async Task QuotedString_SetsStyleToQuoted() [Test] public async Task MultiLineString_SetsStyleToMultiline() { - var sut = KuddleGrammar.MultiLineQuoted; + var sut = KdlGrammar.MultiLineQuoted; var input = """" """ content @@ -399,7 +399,7 @@ public async Task MultiLineString_SetsStyleToMultiline() [Test] public async Task RawString_SingleLine_SetsStyleToRawAndQuoted() { - var sut = KuddleGrammar.RawString; + var sut = KdlGrammar.RawString; var input = "#\"\"raw content\"\"#"; bool success = sut.TryParse(input, out var value); @@ -416,7 +416,7 @@ public async Task RawString_SingleLine_SetsStyleToRawAndQuoted() [Test] public async Task RawString_MultiLine_SetsStyleToRawAndMultiline() { - var sut = KuddleGrammar.RawString; + var sut = KdlGrammar.RawString; var input = """" #""" multi @@ -438,7 +438,7 @@ public async Task RawString_MultiLine_SetsStyleToRawAndMultiline() [Test] public async Task String_UnifiedParser_DetectsBare() { - var sut = KuddleGrammar.String; + var sut = KdlGrammar.String; var input = "node_name"; bool success = sut.TryParse(input, out var value); @@ -450,7 +450,7 @@ public async Task String_UnifiedParser_DetectsBare() [Test] public async Task String_UnifiedParser_DetectsQuoted() { - var sut = KuddleGrammar.String; + var sut = KdlGrammar.String; var input = "\"node name\""; bool success = sut.TryParse(input, out var value); @@ -462,7 +462,7 @@ public async Task String_UnifiedParser_DetectsQuoted() [Test] public async Task String_UnifiedParser_DetectsRaw() { - var sut = KuddleGrammar.String; + var sut = KdlGrammar.String; var input = @"#""node name""#"; bool success = sut.TryParse(input, out var value); diff --git a/src/Kuddle.Tests/Grammar/WhiteSpaceParsersTests.cs b/src/Kuddle.Tests/Grammar/WhiteSpaceParsersTests.cs index 2613baa..4dd9a40 100644 --- a/src/Kuddle.Tests/Grammar/WhiteSpaceParsersTests.cs +++ b/src/Kuddle.Tests/Grammar/WhiteSpaceParsersTests.cs @@ -33,7 +33,7 @@ public class WhiteSpaceParsersTests [Arguments('\u3000')] public async Task Ws_ParsesUnicodeSpace(char input) { - var sut = KuddleGrammar.Ws; + var sut = KdlGrammar.Ws; bool success = sut.TryParse(input.ToString(), out var value); @@ -44,7 +44,7 @@ public async Task Ws_ParsesUnicodeSpace(char input) [Test] public async Task Ws_ParsesMultiLineComment() { - var sut = KuddleGrammar.Ws; + var sut = KdlGrammar.Ws; var input = "/* comment */"; bool success = sut.TryParse(input, out var value); @@ -56,7 +56,7 @@ public async Task Ws_ParsesMultiLineComment() [Test] public async Task EscLine_ParsesBackslashContinuation() { - var sut = KuddleGrammar.EscLine; + var sut = KdlGrammar.EscLine; var input = @"\ @@ -69,7 +69,7 @@ public async Task EscLine_ParsesBackslashContinuation() [Test] public async Task EscLine_ParsesBackslashWithComment() { - var sut = KuddleGrammar.EscLine; + var sut = KdlGrammar.EscLine; var input = @"\ // comment"; bool success = sut.TryParse(input, out var value); @@ -81,7 +81,7 @@ public async Task EscLine_ParsesBackslashWithComment() [Test] public async Task LineSpace_ParsesWhitespace() { - var sut = KuddleGrammar.LineSpace; + var sut = KdlGrammar.LineSpace; var input = " "; bool success = sut.TryParse(input, out var value); @@ -93,7 +93,7 @@ public async Task LineSpace_ParsesWhitespace() [Test] public async Task LineSpace_ParsesNewLine() { - var sut = KuddleGrammar.LineSpace; + var sut = KdlGrammar.LineSpace; var input = "\n"; bool success = sut.TryParse(input, out var value); @@ -105,7 +105,7 @@ public async Task LineSpace_ParsesNewLine() [Test] public async Task LineSpace_ParsesComment() { - var sut = KuddleGrammar.LineSpace; + var sut = KdlGrammar.LineSpace; var input = "// comment"; bool success = sut.TryParse(input, out var value); @@ -117,7 +117,7 @@ public async Task LineSpace_ParsesComment() [Test] public async Task NodeSpace_ParsesSimpleWhitespace() { - var sut = KuddleGrammar.NodeSpace; + var sut = KdlGrammar.NodeSpace; var input = " "; bool success = sut.TryParse(input, out var value); diff --git a/src/Kuddle.Tests/Serialization/DocumentToObjectTests.cs b/src/Kuddle.Tests/Serialization/DocumentToObjectTests.cs new file mode 100644 index 0000000..3c62d07 --- /dev/null +++ b/src/Kuddle.Tests/Serialization/DocumentToObjectTests.cs @@ -0,0 +1,3 @@ +namespace Kuddle.Tests.Serialization; + +public class DocumentToObjectTests { } diff --git a/src/Kuddle.Tests/Serialization/KuddleParsingTests.cs b/src/Kuddle.Tests/Serialization/KuddleParsingTests.cs index 9cdc429..57ad418 100644 --- a/src/Kuddle.Tests/Serialization/KuddleParsingTests.cs +++ b/src/Kuddle.Tests/Serialization/KuddleParsingTests.cs @@ -4,7 +4,7 @@ namespace Kuddle.Tests.Serialization; public class KuddleParsingTests { - readonly KuddleWriterOptions _options = new KuddleWriterOptions() with { RoundTrip = false }; + readonly KdlWriterOptions _options = new KdlWriterOptions() with { RoundTrip = false }; [Test] [MethodDataSource( @@ -16,7 +16,7 @@ public async Task TestBlockComment(ParsingTestData testData) if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -24,7 +24,7 @@ public async Task TestBlockComment(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } @@ -38,7 +38,7 @@ public async Task TestEscline(ParsingTestData testData) if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -46,7 +46,7 @@ public async Task TestEscline(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } @@ -60,7 +60,7 @@ public async Task TestMultilineRawString(ParsingTestData testData) if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -68,7 +68,7 @@ public async Task TestMultilineRawString(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } @@ -82,7 +82,7 @@ public async Task TestMultilineString(ParsingTestData testData) if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -90,18 +90,21 @@ public async Task TestMultilineString(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } [Test] - [MethodDataSource(typeof(KuddleParsingTestDataSources), nameof(KuddleParsingTestDataSources.ArgTestData))] + [MethodDataSource( + typeof(KuddleParsingTestDataSources), + nameof(KuddleParsingTestDataSources.ArgTestData) + )] public async Task TestArg(ParsingTestData testData) { if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); // Assumes KdlDocument has ToString() for serialization @@ -109,7 +112,7 @@ public async Task TestArg(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } @@ -123,7 +126,7 @@ public async Task TestBareIdent(ParsingTestData testData) if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -131,7 +134,7 @@ public async Task TestBareIdent(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } @@ -145,7 +148,7 @@ public async Task TestBinary(ParsingTestData testData) if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -153,18 +156,21 @@ public async Task TestBinary(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } [Test] - [MethodDataSource(typeof(KuddleParsingTestDataSources), nameof(KuddleParsingTestDataSources.BlankTestData))] + [MethodDataSource( + typeof(KuddleParsingTestDataSources), + nameof(KuddleParsingTestDataSources.BlankTestData) + )] public async Task TestBlank(ParsingTestData testData) { if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -172,7 +178,7 @@ public async Task TestBlank(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } @@ -186,7 +192,7 @@ public async Task TestBoolean(ParsingTestData testData) if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -194,18 +200,21 @@ public async Task TestBoolean(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } [Test] - [MethodDataSource(typeof(KuddleParsingTestDataSources), nameof(KuddleParsingTestDataSources.BomTestData))] + [MethodDataSource( + typeof(KuddleParsingTestDataSources), + nameof(KuddleParsingTestDataSources.BomTestData) + )] public async Task TestBom(ParsingTestData testData) { if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -213,7 +222,7 @@ public async Task TestBom(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } @@ -227,7 +236,7 @@ public async Task TestComment(ParsingTestData testData) if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -235,7 +244,7 @@ public async Task TestComment(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } @@ -249,7 +258,7 @@ public async Task TestCommented(ParsingTestData testData) if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -257,18 +266,21 @@ public async Task TestCommented(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } [Test] - [MethodDataSource(typeof(KuddleParsingTestDataSources), nameof(KuddleParsingTestDataSources.EmptyTestData))] + [MethodDataSource( + typeof(KuddleParsingTestDataSources), + nameof(KuddleParsingTestDataSources.EmptyTestData) + )] public async Task TestEmpty(ParsingTestData testData) { if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -276,18 +288,21 @@ public async Task TestEmpty(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } [Test] - [MethodDataSource(typeof(KuddleParsingTestDataSources), nameof(KuddleParsingTestDataSources.EscTestData))] + [MethodDataSource( + typeof(KuddleParsingTestDataSources), + nameof(KuddleParsingTestDataSources.EscTestData) + )] public async Task TestEsc(ParsingTestData testData) { if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -295,18 +310,21 @@ public async Task TestEsc(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } [Test] - [MethodDataSource(typeof(KuddleParsingTestDataSources), nameof(KuddleParsingTestDataSources.FalseTestData))] + [MethodDataSource( + typeof(KuddleParsingTestDataSources), + nameof(KuddleParsingTestDataSources.FalseTestData) + )] public async Task TestFalse(ParsingTestData testData) { if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -314,7 +332,7 @@ public async Task TestFalse(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } @@ -328,7 +346,7 @@ public async Task TestIllegal(ParsingTestData testData) if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -336,7 +354,7 @@ public async Task TestIllegal(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } @@ -350,7 +368,7 @@ public async Task TestBareEmoji(ParsingTestData testData) if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -358,18 +376,21 @@ public async Task TestBareEmoji(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } [Test] - [MethodDataSource(typeof(KuddleParsingTestDataSources), nameof(KuddleParsingTestDataSources.AllTestData))] + [MethodDataSource( + typeof(KuddleParsingTestDataSources), + nameof(KuddleParsingTestDataSources.AllTestData) + )] public async Task TestAll(ParsingTestData testData) { if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -377,7 +398,7 @@ public async Task TestAll(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } @@ -391,7 +412,7 @@ public async Task TestAsterisk(ParsingTestData testData) if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -399,7 +420,7 @@ public async Task TestAsterisk(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } @@ -413,7 +434,7 @@ public async Task TestBraces(ParsingTestData testData) if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -421,7 +442,7 @@ public async Task TestBraces(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } @@ -435,7 +456,7 @@ public async Task TestChevrons(ParsingTestData testData) if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -443,18 +464,21 @@ public async Task TestChevrons(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } [Test] - [MethodDataSource(typeof(KuddleParsingTestDataSources), nameof(KuddleParsingTestDataSources.CommaTestData))] + [MethodDataSource( + typeof(KuddleParsingTestDataSources), + nameof(KuddleParsingTestDataSources.CommaTestData) + )] public async Task TestComma(ParsingTestData testData) { if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -462,18 +486,21 @@ public async Task TestComma(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } [Test] - [MethodDataSource(typeof(KuddleParsingTestDataSources), nameof(KuddleParsingTestDataSources.CrlfTestData))] + [MethodDataSource( + typeof(KuddleParsingTestDataSources), + nameof(KuddleParsingTestDataSources.CrlfTestData) + )] public async Task TestCrlf(ParsingTestData testData) { if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -481,18 +508,21 @@ public async Task TestCrlf(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } [Test] - [MethodDataSource(typeof(KuddleParsingTestDataSources), nameof(KuddleParsingTestDataSources.DashTestData))] + [MethodDataSource( + typeof(KuddleParsingTestDataSources), + nameof(KuddleParsingTestDataSources.DashTestData) + )] public async Task TestDash(ParsingTestData testData) { if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -500,18 +530,21 @@ public async Task TestDash(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } [Test] - [MethodDataSource(typeof(KuddleParsingTestDataSources), nameof(KuddleParsingTestDataSources.DotTestData))] + [MethodDataSource( + typeof(KuddleParsingTestDataSources), + nameof(KuddleParsingTestDataSources.DotTestData) + )] public async Task TestDot(ParsingTestData testData) { if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -519,18 +552,21 @@ public async Task TestDot(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } [Test] - [MethodDataSource(typeof(KuddleParsingTestDataSources), nameof(KuddleParsingTestDataSources.EmojiTestData))] + [MethodDataSource( + typeof(KuddleParsingTestDataSources), + nameof(KuddleParsingTestDataSources.EmojiTestData) + )] public async Task TestEmoji(ParsingTestData testData) { if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -538,18 +574,21 @@ public async Task TestEmoji(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } [Test] - [MethodDataSource(typeof(KuddleParsingTestDataSources), nameof(KuddleParsingTestDataSources.EofTestData))] + [MethodDataSource( + typeof(KuddleParsingTestDataSources), + nameof(KuddleParsingTestDataSources.EofTestData) + )] public async Task TestEof(ParsingTestData testData) { if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -557,18 +596,21 @@ public async Task TestEof(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } [Test] - [MethodDataSource(typeof(KuddleParsingTestDataSources), nameof(KuddleParsingTestDataSources.ErrTestData))] + [MethodDataSource( + typeof(KuddleParsingTestDataSources), + nameof(KuddleParsingTestDataSources.ErrTestData) + )] public async Task TestErr(ParsingTestData testData) { if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -576,7 +618,7 @@ public async Task TestErr(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } @@ -590,7 +632,7 @@ public async Task TestEscaped(ParsingTestData testData) if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -598,18 +640,21 @@ public async Task TestEscaped(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } [Test] - [MethodDataSource(typeof(KuddleParsingTestDataSources), nameof(KuddleParsingTestDataSources.HexTestData))] + [MethodDataSource( + typeof(KuddleParsingTestDataSources), + nameof(KuddleParsingTestDataSources.HexTestData) + )] public async Task TestHex(ParsingTestData testData) { if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -617,7 +662,7 @@ public async Task TestHex(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } @@ -631,7 +676,7 @@ public async Task TestInitial(ParsingTestData testData) if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -639,18 +684,21 @@ public async Task TestInitial(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } [Test] - [MethodDataSource(typeof(KuddleParsingTestDataSources), nameof(KuddleParsingTestDataSources.IntTestData))] + [MethodDataSource( + typeof(KuddleParsingTestDataSources), + nameof(KuddleParsingTestDataSources.IntTestData) + )] public async Task TestInt(ParsingTestData testData) { if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -658,18 +706,21 @@ public async Task TestInt(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } [Test] - [MethodDataSource(typeof(KuddleParsingTestDataSources), nameof(KuddleParsingTestDataSources.JustTestData))] + [MethodDataSource( + typeof(KuddleParsingTestDataSources), + nameof(KuddleParsingTestDataSources.JustTestData) + )] public async Task TestJust(ParsingTestData testData) { if (File.Exists(testData.ExpectedFile)) { var inputKdl = await File.ReadAllTextAsync(testData.InputFile); - var doc = KuddleReader.Parse(inputKdl); + var doc = KdlReader.Read(inputKdl); var expected = await File.ReadAllTextAsync(testData.ExpectedFile); expected = expected.Replace("\r\n", "\n"); var serialized = doc.ToString(_options); @@ -677,7 +728,7 @@ public async Task TestJust(ParsingTestData testData) } else { - Assert.Throws(() => KuddleReader.Parse(testData.InputFile)); + Assert.Throws(() => KdlReader.Read(testData.InputFile)); } } } diff --git a/src/Kuddle.Tests/Serialization/KuddleWriterTests.cs b/src/Kuddle.Tests/Serialization/KuddleWriterTests.cs index 357858c..ed5bb09 100644 --- a/src/Kuddle.Tests/Serialization/KuddleWriterTests.cs +++ b/src/Kuddle.Tests/Serialization/KuddleWriterTests.cs @@ -10,9 +10,9 @@ public class KuddleWriterTests public async Task Write_SimpleNode_FormatsCorrectly() { var kdl = "node 1 2 key=\"val\""; - var doc = KuddleReader.Parse(kdl); + var doc = KdlReader.Read(kdl); - var output = KuddleWriter.Write(doc, KuddleWriterOptions.Default); + var output = KdlWriter.Write(doc, KdlWriterOptions.Default); await Assert.That(output.Trim()).IsEqualTo("node 1 2 key=\"val\""); } @@ -21,9 +21,9 @@ public async Task Write_SimpleNode_FormatsCorrectly() public async Task Write_NestedStructure_IndentsCorrectly() { var kdl = "parent { child; }"; - var doc = KuddleReader.Parse(kdl); + var doc = KdlReader.Read(kdl); - var output = KuddleWriter.Write(doc); + var output = KdlWriter.Write(doc); var expected = @"parent { child; @@ -36,9 +36,9 @@ public async Task Write_NestedStructure_IndentsCorrectly() public async Task Write_ComplexString_EscapesCorrectly() { var kdl = "node \"line1\\nline2\""; - var doc = KuddleReader.Parse(kdl); + var doc = KdlReader.Read(kdl); - var output = KuddleWriter.Write(doc); + var output = KdlWriter.Write(doc); await Assert.That(output.Trim()).IsEqualTo("node \"line1\\nline2\""); } @@ -51,7 +51,7 @@ public async Task Write_BareIdentifier_QuotesIfInvalid() Nodes = [new KdlNode(new KdlString("node name", StringKind.Quoted))], }; - var output = KuddleWriter.Write(doc); + var output = KdlWriter.Write(doc); await Assert.That(output.Trim()).IsEqualTo("\"node name\""); } diff --git a/src/Kuddle.Tests/Serialization/NodeToObjectTests.cs b/src/Kuddle.Tests/Serialization/NodeToObjectTests.cs new file mode 100644 index 0000000..ab20620 --- /dev/null +++ b/src/Kuddle.Tests/Serialization/NodeToObjectTests.cs @@ -0,0 +1,127 @@ +using Kuddle.Exceptions; +using Kuddle.Serialization; + +namespace Kuddle.Tests.Serialization; + +public class NodeToObjectTests +{ + // 1. Explicit Naming via [KdlType] + [KdlType("database")] + public class DbConfig + { + [KdlArgument(0)] + public string Name { get; set; } = ""; + + [KdlProperty("port")] + public int Port { get; set; } + + [KdlProperty("enabled")] + public bool Enabled { get; set; } = true; // Default value + } + + // 2. Implicit Naming (Class Name fallback) + public class Server + { + [KdlArgument(0)] + public string Host { get; set; } = ""; + } + + [Test] + public async Task Deserialize_ExplicitName_MapsArgumentsAndProperties() + { + var kdl = "database production port=5432 enabled=#false"; + + var result = KdlSerializer.Deserialize(kdl); + + await Assert.That(result).IsNotNull(); + await Assert.That(result.Name).IsEqualTo("production"); + await Assert.That(result.Port).IsEqualTo(5432); + await Assert.That(result.Enabled).IsFalse(); + } + + [Test] + public async Task Deserialize_ImplicitName_UsesClassName() + { + // Class is "Server", so it expects node "server" (case-insensitive usually, or exact match) + // Assuming your logic defaults to exact or lowercase. + // Let's assume case-insensitive or exact match "Server". + // If your logic uses .ToLowerInvariant(), input should be "server". + var kdl = "Server \"localhost\""; + + var result = KdlSerializer.Deserialize(kdl); + + await Assert.That(result).IsNotNull(); + await Assert.That(result.Host).IsEqualTo("localhost"); + } + + [Test] + public async Task Deserialize_MissingProperty_UsesDefault() + { + // "enabled" is missing, should stay true + var kdl = "database \"local\" port=3000"; + + var result = KdlSerializer.Deserialize(kdl); + + await Assert.That(result.Name).IsEqualTo("local"); + await Assert.That(result.Enabled).IsTrue(); + } + + [Test] + public async Task Deserialize_MismatchNodeName_Throws() + { + // Expecting "database", got "table" + var kdl = "table \"production\" port=5432"; + + // This asserts that the Serializer enforces the [KdlType] name + await Assert.ThrowsAsync(async () => + { + KdlSerializer.Deserialize(kdl); + }); + } + + [Test] + public async Task Deserialize_MultipleRootNodes_Throws() + { + // Node-to-Object strategy implies the Type represents A SINGLE node. + // A document with two nodes is ambiguous/invalid for this mapping. + var kdl = + @" + database ""primary"" port=5432 + database ""replica"" port=5433 + "; + + await Assert.ThrowsAsync(async () => + { + KdlSerializer.Deserialize(kdl); + }); + } + + [Test] + public async Task Deserialize_TypeMismatch_Throws() + { + // Port expects int, got string identifier + var kdl = "database \"db\" port=\"not-a-number\""; + + await Assert.ThrowsAsync(async () => + { + KdlSerializer.Deserialize(kdl); + }); + } + + [Test] + public async Task Deserialize_DocumentRoot_RejectsProperties() + { + // If the serializer logic correctly identifies this as a Node + // (because it has [KdlArgument]), it handles it. + // But if we tried to put a property at the top level in the FILE that doesn't + // belong to a node (which is syntactically impossible in KDL anyway), the Parser would catch it. + + // However, we can test that extraneous properties on the node are simply ignored + // (unless you implemented strict mode). + var kdl = "database \"db\" port=5432 unknown_prop=123"; + + // Should succeed and ignore unknown_prop + var result = KdlSerializer.Deserialize(kdl); + await Assert.That(result.Port).IsEqualTo(5432); + } +} diff --git a/src/Kuddle.Tests/Serialization/ObjectMapperTests.cs b/src/Kuddle.Tests/Serialization/ObjectMapperTests.cs index 27dd4a8..915455d 100644 --- a/src/Kuddle.Tests/Serialization/ObjectMapperTests.cs +++ b/src/Kuddle.Tests/Serialization/ObjectMapperTests.cs @@ -1,11 +1,10 @@ using Kuddle.AST; -using Kuddle.Exceptions; using Kuddle.Serialization; namespace Kuddle.Tests.Serialization; /// -/// Tests for await KdlSerializer.Deserialize() and KdlSerializer.Serialize() +/// Tests for KdlSerializer.Deserialize() and KdlSerializer.Serialize() /// which map between KDL documents and strongly-typed C# objects. /// public class ObjectMapperTests @@ -21,7 +20,7 @@ public async Task DeserializeSimpleObject_WithArgument_MapsCorrectly() """; // Act - var result = await KdlSerializer.Deserialize(kdl); + var result = KdlSerializer.Deserialize(kdl); // Assert await Assert.That(result.Name).IsEqualTo("my-lib"); @@ -36,7 +35,7 @@ public async Task DeserializeSimpleObject_WithProperties_MapsCorrectly() """; // Act - var result = await KdlSerializer.Deserialize(kdl); + var result = KdlSerializer.Deserialize(kdl); // Assert await Assert.That(result.Name).IsEqualTo("my-lib"); @@ -53,7 +52,7 @@ public async Task DeserializeObject_WithMissingOptionalProperty_UsesDefault() """; // Act - var result = await KdlSerializer.Deserialize(kdl); + var result = KdlSerializer.Deserialize(kdl); // Assert await Assert.That(result.Description).IsNull(); @@ -77,7 +76,7 @@ public async Task DeserializeObject_WithChildren_MapsChildListCorrectly() """; // Act - var result = await KdlSerializer.Deserialize(kdl); + var result = KdlSerializer.Deserialize(kdl); // Assert await Assert.That(result.Name).IsEqualTo("my-app"); @@ -101,7 +100,7 @@ public async Task DeserializeObject_WithMultipleChildTypes_MapsEachTypeToCorrect """; // Act - var result = await KdlSerializer.Deserialize(kdl); + var result = KdlSerializer.Deserialize(kdl); // Assert await Assert.That(result.Dependencies).Count().IsEqualTo(2); @@ -119,7 +118,7 @@ public async Task DeserializeObject_WithNoChildren_InitializesEmptyLists() """; // Act - var result = await KdlSerializer.Deserialize(kdl); + var result = KdlSerializer.Deserialize(kdl); // Assert await Assert.That(result.Dependencies).IsEmpty(); @@ -139,7 +138,7 @@ public async Task DeserializeObject_WithIntProperty_MapsCorrectly() """; // Act - var result = await KdlSerializer.Deserialize(kdl); + var result = KdlSerializer.Deserialize(kdl); // Assert await Assert.That(result.Timeout).IsEqualTo(5000); @@ -154,7 +153,7 @@ public async Task DeserializeObject_WithLongProperty_MapsCorrectly() """; // Act - var result = await KdlSerializer.Deserialize(kdl); + var result = KdlSerializer.Deserialize(kdl); // Assert await Assert.That(result.Retries).IsEqualTo(long.MaxValue); @@ -169,7 +168,7 @@ public async Task DeserializeObject_WithDoubleProperty_MapsCorrectly() """; // Act - var result = await KdlSerializer.Deserialize(kdl); + var result = KdlSerializer.Deserialize(kdl); // Assert await Assert.That(result.Ratio).IsEqualTo(3.14159).Within(0.00001); @@ -184,7 +183,7 @@ public async Task DeserializeObject_WithBoolProperty_MapsCorrectly() """; // Act - var result = await KdlSerializer.Deserialize(kdl); + var result = KdlSerializer.Deserialize(kdl); // Assert await Assert.That(result.Enabled).IsTrue(); @@ -199,7 +198,7 @@ public async Task DeserializeObject_WithAllNumericTypes_MapsAllCorrectly() """; // Act - var result = await KdlSerializer.Deserialize(kdl); + var result = KdlSerializer.Deserialize(kdl); // Assert await Assert.That(result.Timeout).IsEqualTo(500); @@ -222,7 +221,7 @@ public async Task DeserializeObject_WithGuidProperty_ParsesUuidTypeAnnotation() """; // Act - var result = await KdlSerializer.Deserialize(kdl); + var result = KdlSerializer.Deserialize(kdl); // Assert await Assert.That(result.Username).IsEqualTo("alice"); @@ -239,7 +238,7 @@ public async Task DeserializeObject_WithDateTimeProperty_ParsesDateTimeTypeAnnot """; // Act - var result = await KdlSerializer.Deserialize(kdl); + var result = KdlSerializer.Deserialize(kdl); // Assert await Assert.That(result.CreatedAt).IsEqualTo(now); @@ -309,7 +308,7 @@ public async Task RoundTrip_SimpleObject_PreservesData() // Act var kdl = KdlSerializer.Serialize(original); - var deserialized = await KdlSerializer.Deserialize(kdl); + var deserialized = KdlSerializer.Deserialize(kdl); // Assert await Assert.That(deserialized.Name).IsEqualTo(original.Name); @@ -344,7 +343,7 @@ public async Task RoundTrip_NestedObject_PreservesData() // Act var kdl = KdlSerializer.Serialize(original); - var deserialized = await KdlSerializer.Deserialize(kdl); + var deserialized = KdlSerializer.Deserialize(kdl); // Assert await Assert.That(deserialized.Name).IsEqualTo(original.Name); @@ -366,7 +365,7 @@ public async Task RoundTrip_NestedObject_PreservesData() // """; // // Act - // var result = await KdlSerializer.Deserialize(kdl); + // var result = KdlSerializer.Deserialize(kdl); // // Assert // await Assert.That(result).IsOfType(typeof(FileResource)); @@ -383,7 +382,7 @@ public async Task RoundTrip_NestedObject_PreservesData() // """; // // Act - // var result = await KdlSerializer.Deserialize(kdl); + // var result = KdlSerializer.Deserialize(kdl); // // Assert // await Assert.That(result).IsOfType(typeof(UrlResource)); @@ -404,7 +403,7 @@ public async Task DeserializeObject_WithEmptyString_MapsCorrectly() """; // Act - var result = await KdlSerializer.Deserialize(kdl); + var result = KdlSerializer.Deserialize(kdl); // Assert await Assert.That(result.Name).IsEqualTo(""); @@ -420,7 +419,7 @@ public async Task DeserializeObject_WithSpecialCharactersInString_MapsCorrectly( """; // Act - var result = await KdlSerializer.Deserialize(kdl); + var result = KdlSerializer.Deserialize(kdl); // Assert await Assert.That(result.Name).IsEqualTo("my-lib@1.0"); @@ -436,7 +435,7 @@ public async Task DeserializeObject_WithNegativeNumbers_MapsCorrectly() """; // Act - var result = await KdlSerializer.Deserialize(kdl); + var result = KdlSerializer.Deserialize(kdl); // Assert await Assert.That(result.Timeout).IsEqualTo(-1000); @@ -452,7 +451,7 @@ public async Task DeserializeObject_WithHexNumbers_MapsCorrectly() """; // Act - var result = await KdlSerializer.Deserialize(kdl); + var result = KdlSerializer.Deserialize(kdl); // Assert await Assert.That(result.Timeout).IsEqualTo(255); @@ -473,7 +472,7 @@ public async Task DeserializeObject_WithWrongNodeName_ThrowsException() // Act & Assert await Assert - .That(async () => await KdlSerializer.Deserialize(kdl)) + .That(async () => KdlSerializer.Deserialize(kdl)) .Throws(); } @@ -489,7 +488,7 @@ public async Task DeserializeToScalar_WithMultipleRootNodes_ThrowsException() // Act & Assert await Assert - .That(async () => await KdlSerializer.Deserialize(kdl)) + .That(async () => KdlSerializer.Deserialize(kdl)) .Throws(); } @@ -505,7 +504,7 @@ public async Task DeserializeToDictionary_ThrowsException() // Act & Assert await Assert - .That(async () => await KdlSerializer.Deserialize>(kdl)) + .That(async () => KdlSerializer.Deserialize>(kdl)) .Throws() .WithMessageContaining("not supported"); } @@ -522,7 +521,7 @@ public async Task DeserializeToEnumerable_WithDifferentNodeNames_ThrowsException // Act & Assert await Assert - .That(async () => await KdlSerializer.Deserialize>(kdl)) + .That(async () => KdlSerializer.Deserialize>(kdl)) .Throws(); } @@ -536,7 +535,7 @@ public async Task DeserializeObject_WithInvalidNumericValue_ThrowsException() // Act & Assert await Assert - .That(async () => await KdlSerializer.Deserialize(kdl)) + .That(async () => KdlSerializer.Deserialize(kdl)) .Throws(); } @@ -550,7 +549,7 @@ public async Task DeserializeObject_WithMissingRequiredArgument_ThrowsException( // Act & Assert await Assert - .That(async () => await KdlSerializer.Deserialize(kdl)) + .That(async () => KdlSerializer.Deserialize(kdl)) .Throws(); } @@ -564,7 +563,7 @@ public async Task DeserializeObject_WhenTargetIsSimpleValue_ThrowsException() // Act & Assert await Assert - .That(async () => await KdlSerializer.Deserialize(kdl)) + .That(async () => KdlSerializer.Deserialize(kdl)) .Throws() .WithMessageContaining("Cannot deserialize type"); } diff --git a/src/Kuddle.Tests/Validation/ReservedTypeValidatorTests.cs b/src/Kuddle.Tests/Validation/ReservedTypeValidatorTests.cs index 22deb82..6f4c310 100644 --- a/src/Kuddle.Tests/Validation/ReservedTypeValidatorTests.cs +++ b/src/Kuddle.Tests/Validation/ReservedTypeValidatorTests.cs @@ -1,5 +1,6 @@ using Kuddle.AST; using Kuddle.Exceptions; +using Kuddle.Serialization; using Kuddle.Validation; namespace Kuddle.Tests.Validation; @@ -15,7 +16,7 @@ public async Task Given_ValidU8_When_Validated_Then_NoExceptionThrown() var doc = Parse("node 255"); // Act & Assert - await Assert.That(() => KuddleReservedTypeValidator.Validate(doc)).ThrowsNothing(); + await Assert.That(() => KdlReservedTypeValidator.Validate(doc)).ThrowsNothing(); } [Test] @@ -26,7 +27,7 @@ public async Task Given_OverflowingU8_When_Validated_Then_ThrowsValidationExcept // Act var exception = await Assert - .That(() => KuddleReservedTypeValidator.Validate(doc)) + .That(() => KdlReservedTypeValidator.Validate(doc)) .Throws(); // Assert @@ -42,7 +43,7 @@ public async Task Given_StringForIntType_When_Validated_Then_ThrowsMismatchError // Act var exception = Assert.Throws(() => - KuddleReservedTypeValidator.Validate(doc) + KdlReservedTypeValidator.Validate(doc) ); // Assert @@ -57,7 +58,7 @@ public async Task Given_InvalidUuid_When_Validated_Then_ThrowsError() // Act var exception = Assert.Throws(() => - KuddleReservedTypeValidator.Validate(doc) + KdlReservedTypeValidator.Validate(doc) ); // Assert @@ -71,14 +72,14 @@ public async Task Given_NodeNameWithReservedType_When_Validated_Then_IgnoresIt() var doc = Parse("node (u8)123"); // Act & Assert - await Assert.That(() => KuddleReservedTypeValidator.Validate(doc)).ThrowsNothing(); + await Assert.That(() => KdlReservedTypeValidator.Validate(doc)).ThrowsNothing(); } private static KdlDocument Parse(string input) { - return KuddleReader.Parse( + return KdlReader.Read( input, - KuddleReaderOptions.Default with + KdlReaderOptions.Default with { ValidateReservedTypes = false, } diff --git a/src/Kuddle/AST/KdlDocument.cs b/src/Kuddle/AST/KdlDocument.cs index 8a11baf..f87ac17 100644 --- a/src/Kuddle/AST/KdlDocument.cs +++ b/src/Kuddle/AST/KdlDocument.cs @@ -7,13 +7,13 @@ public sealed record KdlDocument : KdlObject { public List Nodes { get; init; } = []; - public string ToString(KuddleWriterOptions? options = null) + public string ToString(KdlWriterOptions? options = null) { - return KuddleWriter.Write(this, options); + return KdlWriter.Write(this, options); } public override string ToString() { - return KuddleWriter.Write(this); + return KdlWriter.Write(this); } } diff --git a/src/Kuddle/Extensions/KuddleNodeExtensions.cs b/src/Kuddle/Extensions/KdlNodeExtensions.cs similarity index 97% rename from src/Kuddle/Extensions/KuddleNodeExtensions.cs rename to src/Kuddle/Extensions/KdlNodeExtensions.cs index 7c90031..18a3bb3 100644 --- a/src/Kuddle/Extensions/KuddleNodeExtensions.cs +++ b/src/Kuddle/Extensions/KdlNodeExtensions.cs @@ -2,7 +2,7 @@ namespace Kuddle.Extensions; -public static class KuddleNodeExtensions +public static class KdlNodeExtensions { extension(KdlNode node) { diff --git a/src/Kuddle/Extensions/KuddleValueExtensions.cs b/src/Kuddle/Extensions/KdlValueExtensions.cs similarity index 98% rename from src/Kuddle/Extensions/KuddleValueExtensions.cs rename to src/Kuddle/Extensions/KdlValueExtensions.cs index 662051c..73da3a7 100644 --- a/src/Kuddle/Extensions/KuddleValueExtensions.cs +++ b/src/Kuddle/Extensions/KdlValueExtensions.cs @@ -4,7 +4,7 @@ namespace Kuddle.Extensions; -public static class KuddleValueExtensions +public static class KdlValueExtensions { extension(KdlValue value) { diff --git a/src/Kuddle/Extensions/SpanExtensions.cs b/src/Kuddle/Extensions/SpanExtensions.cs index d1791b7..80b6f2a 100644 --- a/src/Kuddle/Extensions/SpanExtensions.cs +++ b/src/Kuddle/Extensions/SpanExtensions.cs @@ -6,15 +6,15 @@ public static class SpanExtensions { extension(ReadOnlySpan span) { - public bool Any(Func predicate) - { - foreach (var item in span) - { - if (predicate(item)) - return true; - } - return false; - } + // public bool Any(Func predicate) + // { + // foreach (var item in span) + // { + // if (predicate(item)) + // return true; + // } + // return false; + // } public int MaxConsecutive(T target) { diff --git a/src/Kuddle/Parser/KuddleGrammar.cs b/src/Kuddle/Parser/KdlGrammar.cs similarity index 99% rename from src/Kuddle/Parser/KuddleGrammar.cs rename to src/Kuddle/Parser/KdlGrammar.cs index 27fd64f..7d7d951 100644 --- a/src/Kuddle/Parser/KuddleGrammar.cs +++ b/src/Kuddle/Parser/KdlGrammar.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Text.RegularExpressions; using Kuddle.AST; using Kuddle.Extensions; @@ -11,7 +10,7 @@ namespace Kuddle.Parser; -public static class KuddleGrammar +public static class KdlGrammar { internal static readonly Parser Document; @@ -68,7 +67,7 @@ public static class KuddleGrammar IReadOnlyList >(); - static KuddleGrammar() + static KdlGrammar() { var nodeSpace = Deferred(); diff --git a/src/Kuddle/Serialization/Attributes/KdlPropertyAttribute.cs b/src/Kuddle/Serialization/Attributes/KdlPropertyAttribute.cs index 202d148..f1b892b 100644 --- a/src/Kuddle/Serialization/Attributes/KdlPropertyAttribute.cs +++ b/src/Kuddle/Serialization/Attributes/KdlPropertyAttribute.cs @@ -7,3 +7,13 @@ public sealed class KdlPropertyAttribute(string? key = null) : Attribute { public string? Key { get; } = key; } + +[AttributeUsage( + AttributeTargets.Class | AttributeTargets.Property, + AllowMultiple = false, + Inherited = false +)] +public sealed class KdlTypeAttribute(string name) : Attribute +{ + public string Name { get; set; } = name; +} diff --git a/src/Kuddle/Serialization/KuddleReader.cs b/src/Kuddle/Serialization/KdlReader.cs similarity index 71% rename from src/Kuddle/Serialization/KuddleReader.cs rename to src/Kuddle/Serialization/KdlReader.cs index fdbb10e..a465655 100644 --- a/src/Kuddle/Serialization/KuddleReader.cs +++ b/src/Kuddle/Serialization/KdlReader.cs @@ -1,17 +1,15 @@ using System; -using System.IO; -using System.Threading.Tasks; using Kuddle.AST; using Kuddle.Exceptions; using Kuddle.Parser; using Kuddle.Validation; using Parlot.Fluent; -namespace Kuddle; +namespace Kuddle.Serialization; -public static class KuddleReader +public static class KdlReader { - private static readonly Parser _parser = KuddleGrammar.Document.Compile(); + private static readonly Parser _parser = KdlGrammar.Document.Compile(); /// /// Parses a KDL string into a KdlDocument AST. @@ -20,10 +18,10 @@ public static class KuddleReader /// /// /// - public static KdlDocument Parse(string text, KuddleReaderOptions? options = null) + public static KdlDocument Read(string text, KdlReaderOptions? options = null) { ArgumentNullException.ThrowIfNull(text); - options ??= KuddleReaderOptions.Default; + options ??= KdlReaderOptions.Default; if (!_parser.TryParse(text, out var doc, out var error)) { @@ -41,7 +39,7 @@ public static KdlDocument Parse(string text, KuddleReaderOptions? options = null if (options.ValidateReservedTypes) { - KuddleReservedTypeValidator.Validate(doc); + KdlReservedTypeValidator.Validate(doc); } return doc; @@ -59,12 +57,4 @@ public static KdlDocument Parse(string text, KuddleReaderOptions? options = null // var text = await reader.ReadToEndAsync(); // return await ReadASync(text, options); // } - - public static async Task ReadAsync( - string text, - KuddleReaderOptions? options = null - ) - { - return Parse(text, options); - } } diff --git a/src/Kuddle/Serialization/KdlReaderOptions.cs b/src/Kuddle/Serialization/KdlReaderOptions.cs new file mode 100644 index 0000000..b0841d5 --- /dev/null +++ b/src/Kuddle/Serialization/KdlReaderOptions.cs @@ -0,0 +1,7 @@ +namespace Kuddle.Serialization; + +public record KdlReaderOptions +{ + public static KdlReaderOptions Default => new() { ValidateReservedTypes = true }; + public bool ValidateReservedTypes { get; init; } = true; +} diff --git a/src/Kuddle/Serialization/Attributes/KdlSerializer.cs b/src/Kuddle/Serialization/KdlSerializer.cs similarity index 81% rename from src/Kuddle/Serialization/Attributes/KdlSerializer.cs rename to src/Kuddle/Serialization/KdlSerializer.cs index 02c37d1..9720fd4 100644 --- a/src/Kuddle/Serialization/Attributes/KdlSerializer.cs +++ b/src/Kuddle/Serialization/KdlSerializer.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Threading.Tasks; using Kuddle.AST; using Kuddle.Extensions; @@ -11,70 +10,77 @@ namespace Kuddle.Serialization; public static class KdlSerializer { - public static async Task Deserialize(string text, KdlSerializerOptions? options = null) + public static T Deserialize(string text, KdlSerializerOptions? options = null) { + var document = KdlReader.Read(text); var t = typeof(T); - KdlDocument document; - if (t.IsDictionary) - { - throw new KuddleSerializationException( - "Deserialization to dictionaries is not supported" - ); - } - else if (t.IsIEnumerable) + + if (t.IsNodeDefinition) { - var genericTypeDef = t.GetGenericTypeDefinition(); - var args = t.GetGenericArguments(); - var listType = typeof(List<>).MakeGenericType(args); - var listInstance = (IList)Activator.CreateInstance(listType)!; + // T does not have any kdl args or properties, so should be a single root node doc - document = await KuddleReader.ReadAsync(text); + if (t.IsDictionary) + { + throw new KuddleSerializationException( + "Deserialization to dictionaries is not supported" + ); + } + else if (t.IsIEnumerable) + { + var genericTypeDef = t.GetGenericTypeDefinition(); + var args = t.GetGenericArguments(); + var listType = typeof(List<>).MakeGenericType(args); + var listInstance = (IList)Activator.CreateInstance(listType)!; - var firstName = document.Nodes.FirstOrDefault()?.Name; + var firstName = document.Nodes.FirstOrDefault()?.Name; - foreach (var node in document.Nodes) - { - if (node.Name != firstName) + foreach (var node in document.Nodes) { - throw new KuddleSerializationException( - "All root nodes must have the same name." - ); + if (node.Name != firstName) + { + throw new KuddleSerializationException( + "All root nodes must have the same name." + ); + } } + + return (T)listInstance; } - return (T)listInstance; - } + if (!t.IsComplexType) + { + throw new KuddleSerializationException( + $"Cannot deserialize type '{t.FullName}' as a complex object. " + + "Ensure the type is a concrete class with a public parameterless constructor." + ); + } + var instance = Activator.CreateInstance(); - if (!t.IsComplexType) - { - throw new KuddleSerializationException( - $"Cannot deserialize type '{t.FullName}' as a complex object. " - + "Ensure the type is a concrete class with a public parameterless constructor." - ); - } - var instance = Activator.CreateInstance(); - document = await KuddleReader.ReadAsync(text); + if (document.Nodes.Count != 1) + { + throw new KuddleSerializationException( + "Kdl document must have a single root node to map to an instance T" + ); + } + if ( + !typeof(T).Name.Equals( + document.Nodes[0].Name.Value, + StringComparison.InvariantCultureIgnoreCase + ) + ) + { + throw new KuddleSerializationException( + $"Node name '{document.Nodes[0].Name}' does not match target type name '{typeof(T).Name}'." + ); + } + MapToInstance(document.Nodes[0], instance); - if (document.Nodes.Count != 1) - { - throw new KuddleSerializationException( - "Kdl document must have a single root node to map to an instance T" - ); + return instance; } - if ( - !typeof(T).Name.Equals( - document.Nodes[0].Name.Value, - StringComparison.InvariantCultureIgnoreCase - ) - ) + else { - throw new KuddleSerializationException( - $"Node name '{document.Nodes[0].Name}' does not match target type name '{typeof(T).Name}'." - ); + throw new NotImplementedException(); } - MapToInstance(document.Nodes[0], instance); - - return instance; } private static void MapToInstance(KdlNode node, T instance) @@ -297,7 +303,7 @@ public static string Serialize(T original) doc.Nodes.Add(node); } - return KuddleWriter.Write(doc); + return KdlWriter.Write(doc); } private static KdlNode SerializeNode(object? instance, string? nodeName = null) @@ -396,7 +402,13 @@ internal static class TypeExtensions { extension(Type type) { - public bool IsComplexType => + internal bool IsNodeDefinition => + !type.GetProperties() + .Any(p => + p.GetCustomAttribute() != null + || p.GetCustomAttribute() != null + ); + internal bool IsComplexType => !type.IsValueType && !type.IsPrimitive && type != typeof(string) @@ -404,7 +416,7 @@ internal static class TypeExtensions && !type.IsInterface && !type.IsAbstract; - public bool IsDictionary => + internal bool IsDictionary => type.IsGenericType && type.GetInterfaces() .Any(i => @@ -415,7 +427,7 @@ internal static class TypeExtensions ) ); - public bool IsIEnumerable => + internal bool IsIEnumerable => type != typeof(string) && !type.IsDictionary && type.IsAssignableTo(typeof(IEnumerable)); diff --git a/src/Kuddle/Serialization/KuddleWriter.cs b/src/Kuddle/Serialization/KdlWriter.cs similarity index 95% rename from src/Kuddle/Serialization/KuddleWriter.cs rename to src/Kuddle/Serialization/KdlWriter.cs index 7ac405e..b757e52 100644 --- a/src/Kuddle/Serialization/KuddleWriter.cs +++ b/src/Kuddle/Serialization/KdlWriter.cs @@ -5,20 +5,20 @@ namespace Kuddle.Serialization; -public class KuddleWriter +public class KdlWriter { - private readonly KuddleWriterOptions _options; + private readonly KdlWriterOptions _options; private readonly StringBuilder _sb = new(); private int _depth = 0; - public KuddleWriter(KuddleWriterOptions? options = null) + public KdlWriter(KdlWriterOptions? options = null) { - _options ??= options ?? KuddleWriterOptions.Default; + _options ??= options ?? KdlWriterOptions.Default; } - public static string Write(KdlDocument document, KuddleWriterOptions? options = null) + public static string Write(KdlDocument document, KdlWriterOptions? options = null) { - var writer = new KuddleWriter(options); + var writer = new KdlWriter(options); writer.WriteDocument(document); return writer._sb.ToString(); } diff --git a/src/Kuddle/Serialization/KuddleWriterOptions.cs b/src/Kuddle/Serialization/KdlWriterOptions.cs similarity index 76% rename from src/Kuddle/Serialization/KuddleWriterOptions.cs rename to src/Kuddle/Serialization/KdlWriterOptions.cs index 7a6112a..5a84e3b 100644 --- a/src/Kuddle/Serialization/KuddleWriterOptions.cs +++ b/src/Kuddle/Serialization/KdlWriterOptions.cs @@ -1,8 +1,8 @@ namespace Kuddle.Serialization; -public record KuddleWriterOptions +public record KdlWriterOptions { - public static KuddleWriterOptions Default => new(); + public static KdlWriterOptions Default => new(); public string IndentChar { get; init; } = " "; public string NewLine { get; init; } = "\n"; diff --git a/src/Kuddle/Serialization/KuddleReaderOptions.cs b/src/Kuddle/Serialization/KuddleReaderOptions.cs deleted file mode 100644 index 95ab1bc..0000000 --- a/src/Kuddle/Serialization/KuddleReaderOptions.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Kuddle; - -public record KuddleReaderOptions -{ - public static KuddleReaderOptions Default => new() { ValidateReservedTypes = true }; - public bool ValidateReservedTypes { get; init; } = true; -} diff --git a/src/Kuddle/Validation/KuddleReservedTypeValidator.cs b/src/Kuddle/Validation/KuddleReservedTypeValidator.cs index c8125e9..ff05cb7 100644 --- a/src/Kuddle/Validation/KuddleReservedTypeValidator.cs +++ b/src/Kuddle/Validation/KuddleReservedTypeValidator.cs @@ -2,14 +2,13 @@ using System.Collections.Generic; using System.Net; using System.Net.Sockets; -using System.Text.Json; using System.Text.RegularExpressions; using Kuddle.AST; using Kuddle.Exceptions; namespace Kuddle.Validation; -public static class KuddleReservedTypeValidator +public static class KdlReservedTypeValidator { private static readonly HashSet ReservedTypes = [ @@ -174,7 +173,10 @@ private static void ValidateValue(KdlValue val, List erro catch (Exception) { errors.Add( - new KuddleValidationError($"Value '{val}' is not a valid '{val.TypeAnnotation}'.", val) + new KuddleValidationError( + $"Value '{val}' is not a valid '{val.TypeAnnotation}'.", + val + ) ); } } From d18ceba48839410c3c61087884b4b1e1b7cf3f88 Mon Sep 17 00:00:00 2001 From: jamesshenry <79054685+jamesshenry@users.noreply.github.com> Date: Wed, 17 Dec 2025 01:44:10 +0000 Subject: [PATCH 03/17] wip --- .../Serialization/DocumentToObjectTests.cs | 123 +++++++++- .../Serialization/NodeToObjectTests.cs | 24 +- .../Serialization/ObjectMapperTests.cs | 26 +- src/Kuddle/Extensions/TypeExtensions.cs | 79 +++++++ src/Kuddle/Serialization/KdlSerializer.cs | 222 ++++++++---------- 5 files changed, 319 insertions(+), 155 deletions(-) create mode 100644 src/Kuddle/Extensions/TypeExtensions.cs diff --git a/src/Kuddle.Tests/Serialization/DocumentToObjectTests.cs b/src/Kuddle.Tests/Serialization/DocumentToObjectTests.cs index 3c62d07..b2555d2 100644 --- a/src/Kuddle.Tests/Serialization/DocumentToObjectTests.cs +++ b/src/Kuddle.Tests/Serialization/DocumentToObjectTests.cs @@ -1,3 +1,124 @@ +using Kuddle.Exceptions; +using Kuddle.Serialization; + namespace Kuddle.Tests.Serialization; -public class DocumentToObjectTests { } +public class DocumentToObjectTests +{ + class AppConfig + { + [KdlNode("plugin")] + public List Plugins { get; set; } = new(); + + [KdlNode("logging")] + public LogSettings? Logging { get; set; } + + [KdlNode("experiments")] + public Experiments? Experiments { get; set; } + } + + class Plugin + { + [KdlArgument(0)] + public string Name { get; set; } = ""; + } + + class LogSettings + { + [KdlNode("level")] + public string LogLevel { get; set; } = "info"; + } + + class Experiments + { + [KdlProperty("enabled")] + public bool Enabled { get; set; } + } + + // --- Tests --- + + [Test] + public async Task Deserialize_Document_MapsListsAndSingleObjects() + { + var kdl = """ + plugin "Analytics" + plugin "Authentication" + + logging { + level "debug" + } + """; + + var result = KdlSerializer.Deserialize(kdl); + + // Assert + await Assert.That(result.Plugins).Count().IsEqualTo(2); + await Assert.That(result.Plugins[0].Name).IsEqualTo("Analytics"); + await Assert.That(result.Plugins[1].Name).IsEqualTo("Authentication"); + + await Assert.That(result.Logging).IsNotNull(); + + await Assert.That(result.Logging!.LogLevel).IsEqualTo("debug"); + } + + [Test] + public async Task Deserialize_EmptyDocument_ReturnsInitializedObject() + { + var kdl = ""; + + var result = KdlSerializer.Deserialize(kdl); + + await Assert.That(result).IsNotNull(); + await Assert.That(result.Plugins).IsEmpty(); + await Assert.That(result.Logging).IsNull(); + } + + [Test] + public async Task Deserialize_PartialMatch_IgnoresUnmappedNodes() + { + var kdl = """ + plugin "Core" + + // This node is not in AppConfig + garbage_data { + ignore me + } + """; + + var result = KdlSerializer.Deserialize(kdl); + + await Assert.That(result.Plugins).Count().IsEqualTo(1); + await Assert.That(result.Plugins[0].Name).IsEqualTo("Core"); + + await Assert.That(result.Logging).IsNull(); + } + + [Test] + public async Task Deserialize_NestedStructure_WithProperties() + { + var kdl = + @" + experiments enabled=#true + "; + + var result = KdlSerializer.Deserialize(kdl); + + await Assert.That(result.Experiments).IsNotNull(); + await Assert.That(result.Experiments!.Enabled).IsTrue(); + } + + [Test] + public async Task Deserialize_AmbiguousSingleNode_ThrowsOrHandles() + { + var kdl = + @" + logging { level ""info"" } + logging { level ""error"" } + "; + + await Assert.ThrowsAsync(async () => + { + KdlSerializer.Deserialize(kdl); + }); + } +} diff --git a/src/Kuddle.Tests/Serialization/NodeToObjectTests.cs b/src/Kuddle.Tests/Serialization/NodeToObjectTests.cs index ab20620..62933b1 100644 --- a/src/Kuddle.Tests/Serialization/NodeToObjectTests.cs +++ b/src/Kuddle.Tests/Serialization/NodeToObjectTests.cs @@ -5,9 +5,8 @@ namespace Kuddle.Tests.Serialization; public class NodeToObjectTests { - // 1. Explicit Naming via [KdlType] [KdlType("database")] - public class DbConfig + class DbConfig { [KdlArgument(0)] public string Name { get; set; } = ""; @@ -16,10 +15,9 @@ public class DbConfig public int Port { get; set; } [KdlProperty("enabled")] - public bool Enabled { get; set; } = true; // Default value + public bool Enabled { get; set; } = true; } - // 2. Implicit Naming (Class Name fallback) public class Server { [KdlArgument(0)] @@ -42,10 +40,6 @@ public async Task Deserialize_ExplicitName_MapsArgumentsAndProperties() [Test] public async Task Deserialize_ImplicitName_UsesClassName() { - // Class is "Server", so it expects node "server" (case-insensitive usually, or exact match) - // Assuming your logic defaults to exact or lowercase. - // Let's assume case-insensitive or exact match "Server". - // If your logic uses .ToLowerInvariant(), input should be "server". var kdl = "Server \"localhost\""; var result = KdlSerializer.Deserialize(kdl); @@ -57,7 +51,6 @@ public async Task Deserialize_ImplicitName_UsesClassName() [Test] public async Task Deserialize_MissingProperty_UsesDefault() { - // "enabled" is missing, should stay true var kdl = "database \"local\" port=3000"; var result = KdlSerializer.Deserialize(kdl); @@ -69,10 +62,8 @@ public async Task Deserialize_MissingProperty_UsesDefault() [Test] public async Task Deserialize_MismatchNodeName_Throws() { - // Expecting "database", got "table" var kdl = "table \"production\" port=5432"; - // This asserts that the Serializer enforces the [KdlType] name await Assert.ThrowsAsync(async () => { KdlSerializer.Deserialize(kdl); @@ -82,8 +73,6 @@ await Assert.ThrowsAsync(async () => [Test] public async Task Deserialize_MultipleRootNodes_Throws() { - // Node-to-Object strategy implies the Type represents A SINGLE node. - // A document with two nodes is ambiguous/invalid for this mapping. var kdl = @" database ""primary"" port=5432 @@ -99,7 +88,6 @@ await Assert.ThrowsAsync(async () => [Test] public async Task Deserialize_TypeMismatch_Throws() { - // Port expects int, got string identifier var kdl = "database \"db\" port=\"not-a-number\""; await Assert.ThrowsAsync(async () => @@ -111,16 +99,8 @@ await Assert.ThrowsAsync(async () => [Test] public async Task Deserialize_DocumentRoot_RejectsProperties() { - // If the serializer logic correctly identifies this as a Node - // (because it has [KdlArgument]), it handles it. - // But if we tried to put a property at the top level in the FILE that doesn't - // belong to a node (which is syntactically impossible in KDL anyway), the Parser would catch it. - - // However, we can test that extraneous properties on the node are simply ignored - // (unless you implemented strict mode). var kdl = "database \"db\" port=5432 unknown_prop=123"; - // Should succeed and ignore unknown_prop var result = KdlSerializer.Deserialize(kdl); await Assert.That(result.Port).IsEqualTo(5432); } diff --git a/src/Kuddle.Tests/Serialization/ObjectMapperTests.cs b/src/Kuddle.Tests/Serialization/ObjectMapperTests.cs index 915455d..2ad93b5 100644 --- a/src/Kuddle.Tests/Serialization/ObjectMapperTests.cs +++ b/src/Kuddle.Tests/Serialization/ObjectMapperTests.cs @@ -553,20 +553,20 @@ await Assert .Throws(); } - [Test] - public async Task DeserializeObject_WhenTargetIsSimpleValue_ThrowsException() - { - // Arrange - var kdl = """ - package - """; + // [Test] + // public async Task DeserializeObject_WhenTargetIsSimpleValue_ThrowsException() + // { + // // Arrange + // var kdl = """ + // package + // """; - // Act & Assert - await Assert - .That(async () => KdlSerializer.Deserialize(kdl)) - .Throws() - .WithMessageContaining("Cannot deserialize type"); - } + // // Act & Assert + // await Assert + // .That(async () => KdlSerializer.Deserialize(kdl)) + // .Throws() + // .WithMessageContaining("Cannot deserialize type"); + // } #endregion #region Test Models diff --git a/src/Kuddle/Extensions/TypeExtensions.cs b/src/Kuddle/Extensions/TypeExtensions.cs new file mode 100644 index 0000000..66a5ed6 --- /dev/null +++ b/src/Kuddle/Extensions/TypeExtensions.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Kuddle.Serialization; + +internal static class TypeExtensions +{ + extension(Type type) + { + internal bool IsNodeDefinition => + type.GetProperties() + .Any(p => + p.GetCustomAttribute() != null + || p.GetCustomAttribute() != null + ); + internal bool IsComplexType => + !type.IsValueType + && !type.IsPrimitive + && type != typeof(string) + && type != typeof(object) + && !type.IsInterface + && !type.IsAbstract; + + internal bool IsDictionary => + type.IsGenericType + && type.GetInterfaces() + .Any(i => + i.IsGenericType + && ( + i.GetGenericTypeDefinition() == typeof(IDictionary<,>) + || i.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>) + ) + ); + + internal bool IsIEnumerable => + type != typeof(string) + && !type.IsDictionary + && type.IsAssignableTo(typeof(IEnumerable)); + + internal (PropertyInfo, KdlArgumentAttribute)[] GetKdlArgProps() => + type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Select(p => new + { + Property = p, + ArgAttr = p.GetCustomAttribute(), + }) + .Where(x => x.ArgAttr is not null) + .OrderBy(x => x.ArgAttr!.Index) + .Select(x => (x.Property, x.ArgAttr!)) + .ToArray(); + + internal (PropertyInfo, KdlPropertyAttribute)[] GetKdlPropProps() => + type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Select(p => new + { + Property = p, + PropAttr = p.GetCustomAttribute(), + }) + .Where(x => x.PropAttr is not null) + .Select(x => (x.Property, x.PropAttr!)) + .ToArray(); + + internal (PropertyInfo, KdlChildrenAttribute)[] GetKdlChildProps() => + type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Select(p => new + { + Property = p, + ChildAttr = p.GetCustomAttribute(), + }) + .Where(x => x.ChildAttr is not null) + .Select(x => (x.Property, x.ChildAttr!)) + .ToArray(); + + public bool IsNullable() => Nullable.GetUnderlyingType(type) != null; + } +} diff --git a/src/Kuddle/Serialization/KdlSerializer.cs b/src/Kuddle/Serialization/KdlSerializer.cs index 9720fd4..bf7b2da 100644 --- a/src/Kuddle/Serialization/KdlSerializer.cs +++ b/src/Kuddle/Serialization/KdlSerializer.cs @@ -10,77 +10,134 @@ namespace Kuddle.Serialization; public static class KdlSerializer { - public static T Deserialize(string text, KdlSerializerOptions? options = null) + public static IEnumerable DeserializeMany( + string text, + KdlSerializerOptions? options = null + ) + where T : new() { - var document = KdlReader.Read(text); - var t = typeof(T); + var doc = KdlReader.Read(text); + var list = new List(doc.Nodes.Count); - if (t.IsNodeDefinition) - { - // T does not have any kdl args or properties, so should be a single root node doc + var expectedName = GetExpectedNodeName(typeof(T)); - if (t.IsDictionary) - { + foreach (var node in doc.Nodes) + { + if (expectedName != null && node.Name.Value != expectedName) throw new KuddleSerializationException( - "Deserialization to dictionaries is not supported" + $"Expected node '{expectedName}', found '{node.Name.Value}'" ); - } - else if (t.IsIEnumerable) - { - var genericTypeDef = t.GetGenericTypeDefinition(); - var args = t.GetGenericArguments(); - var listType = typeof(List<>).MakeGenericType(args); - var listInstance = (IList)Activator.CreateInstance(listType)!; - var firstName = document.Nodes.FirstOrDefault()?.Name; + var item = new T(); + MapToInstance(node, item); + list.Add(item); + } - foreach (var node in document.Nodes) - { - if (node.Name != firstName) - { - throw new KuddleSerializationException( - "All root nodes must have the same name." - ); - } - } + return list; + } - return (T)listInstance; - } + private static string? GetExpectedNodeName(Type type) + { + var kdlTypeAttr = type.GetCustomAttribute(); - if (!t.IsComplexType) - { - throw new KuddleSerializationException( - $"Cannot deserialize type '{t.FullName}' as a complex object. " - + "Ensure the type is a concrete class with a public parameterless constructor." - ); - } - var instance = Activator.CreateInstance(); + if (kdlTypeAttr is null) + return null; + + return kdlTypeAttr.Name; + } + + public static T Deserialize(string text, KdlSerializerOptions? options = null) + where T : new() + { + var document = KdlReader.Read(text); + var type = typeof(T); + if (type.IsNodeDefinition) + { if (document.Nodes.Count != 1) { throw new KuddleSerializationException( "Kdl document must have a single root node to map to an instance T" ); } - if ( - !typeof(T).Name.Equals( - document.Nodes[0].Name.Value, - StringComparison.InvariantCultureIgnoreCase - ) - ) + + var rootNode = document.Nodes[0]; + var expectedName = GetExpectedNodeName(type); + + if (expectedName != null && rootNode.Name.Value != expectedName) { throw new KuddleSerializationException( - $"Node name '{document.Nodes[0].Name}' does not match target type name '{typeof(T).Name}'." + $"Node name '{rootNode.Name.Value}' does not match target type name '{typeof(T).Name}'." ); } - MapToInstance(document.Nodes[0], instance); - + var instance = new T(); + MapToInstance(rootNode, instance); return instance; } else { - throw new NotImplementedException(); + var instance = new T(); + MapNodesToProperties(document.Nodes, instance); + return instance; + } + } + + private static void MapNodesToProperties(List nodes, T instance) + { + var props = instance! + .GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanWrite && p.GetCustomAttribute() == null); + + foreach (var prop in props) + { + var nodeName = + prop.GetCustomAttribute()?.Name + ?? prop.GetCustomAttribute()?.ChildNodeName + ?? null; + + if (nodeName == null) + continue; + + var matchingNodes = nodes + .Where(n => n.Name.Value.Equals(nodeName, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (matchingNodes.Count == 0) + continue; + + if (prop.PropertyType.IsIEnumerable) + { + SetCollectionPropValue(prop, instance, matchingNodes); + } + else + { + if (matchingNodes.Count > 1) + throw new KuddleSerializationException( + $"Property '{prop.Name}' expects a single node named '{nodeName}', but found {matchingNodes.Count}." + ); + + SetSingleComplexPropValue(prop, instance, matchingNodes[0]); + } + } + } + + private static void SetSingleComplexPropValue(PropertyInfo prop, object instance, KdlNode node) + { + var type = prop.PropertyType; + if (!type.IsComplexType) + { + throw new KuddleSerializationException( + $"Property '{prop.Name}' matches a Node, so it must be a complex class." + ); } + + var childInstance = + Activator.CreateInstance(type) ?? throw new Exception($"Could not create {type}"); + + MapToInstance(node, childInstance); + + prop.SetValue(instance, childInstance); } private static void MapToInstance(KdlNode node, T instance) @@ -192,7 +249,6 @@ private static object MapCollection(Type targetType, Type elementType, IList lis return list; } - // IEnumerable, IReadOnlyList, etc. return list; } @@ -397,75 +453,3 @@ private static IEnumerable SerializeCollection(IEnumerable? original) } public record KdlSerializerOptions { } - -internal static class TypeExtensions -{ - extension(Type type) - { - internal bool IsNodeDefinition => - !type.GetProperties() - .Any(p => - p.GetCustomAttribute() != null - || p.GetCustomAttribute() != null - ); - internal bool IsComplexType => - !type.IsValueType - && !type.IsPrimitive - && type != typeof(string) - && type != typeof(object) - && !type.IsInterface - && !type.IsAbstract; - - internal bool IsDictionary => - type.IsGenericType - && type.GetInterfaces() - .Any(i => - i.IsGenericType - && ( - i.GetGenericTypeDefinition() == typeof(IDictionary<,>) - || i.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>) - ) - ); - - internal bool IsIEnumerable => - type != typeof(string) - && !type.IsDictionary - && type.IsAssignableTo(typeof(IEnumerable)); - - internal (PropertyInfo, KdlArgumentAttribute)[] GetKdlArgProps() => - type.GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Select(p => new - { - Property = p, - ArgAttr = p.GetCustomAttribute(), - }) - .Where(x => x.ArgAttr is not null) - .OrderBy(x => x.ArgAttr!.Index) - .Select(x => (x.Property, x.ArgAttr!)) - .ToArray(); - - internal (PropertyInfo, KdlPropertyAttribute)[] GetKdlPropProps() => - type.GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Select(p => new - { - Property = p, - PropAttr = p.GetCustomAttribute(), - }) - .Where(x => x.PropAttr is not null) - .Select(x => (x.Property, x.PropAttr!)) - .ToArray(); - - internal (PropertyInfo, KdlChildrenAttribute)[] GetKdlChildProps() => - type.GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Select(p => new - { - Property = p, - ChildAttr = p.GetCustomAttribute(), - }) - .Where(x => x.ChildAttr is not null) - .Select(x => (x.Property, x.ChildAttr!)) - .ToArray(); - - public bool IsNullable() => Nullable.GetUnderlyingType(type) != null; - } -} From 04f4abeddfa18e46ae7589937cf64d732e08951f Mon Sep 17 00:00:00 2001 From: jamesshenry <79054685+jamesshenry@users.noreply.github.com> Date: Wed, 17 Dec 2025 08:33:32 +0000 Subject: [PATCH 04/17] combine KdlNodeAttribute and KdlChildrenAttribute --- .../Serialization/ObjectMapperTests.cs | 9 ++- src/Kuddle/Extensions/TypeExtensions.cs | 4 +- .../Attributes/KdlChildrenAttribute.cs | 24 +++--- src/Kuddle/Serialization/KdlSerializer.cs | 76 ++++++++++++++----- 4 files changed, 78 insertions(+), 35 deletions(-) diff --git a/src/Kuddle.Tests/Serialization/ObjectMapperTests.cs b/src/Kuddle.Tests/Serialization/ObjectMapperTests.cs index 2ad93b5..f208c54 100644 --- a/src/Kuddle.Tests/Serialization/ObjectMapperTests.cs +++ b/src/Kuddle.Tests/Serialization/ObjectMapperTests.cs @@ -1,3 +1,4 @@ +using System.Net.WebSockets; using Kuddle.AST; using Kuddle.Serialization; @@ -501,10 +502,10 @@ public async Task DeserializeToDictionary_ThrowsException() reference "my-dep1" version="2.1.0" node "my-dep2" version="3.2.1" """; - + var result = KdlSerializer.Deserialize>(kdl); // Act & Assert await Assert - .That(async () => KdlSerializer.Deserialize>(kdl)) + .That(() => KdlSerializer.Deserialize>(kdl)) .Throws() .WithMessageContaining("not supported"); } @@ -593,10 +594,10 @@ public class Project [KdlProperty("version")] public string Version { get; set; } = "1.0.0"; - [KdlChildren("dependency")] + [KdlNode("dependency")] public List Dependencies { get; set; } = []; - [KdlChildren("devDependency")] + [KdlNode("devDependency")] public List DevDependencies { get; set; } = []; } diff --git a/src/Kuddle/Extensions/TypeExtensions.cs b/src/Kuddle/Extensions/TypeExtensions.cs index 66a5ed6..5aa58a6 100644 --- a/src/Kuddle/Extensions/TypeExtensions.cs +++ b/src/Kuddle/Extensions/TypeExtensions.cs @@ -63,12 +63,12 @@ internal static class TypeExtensions .Select(x => (x.Property, x.PropAttr!)) .ToArray(); - internal (PropertyInfo, KdlChildrenAttribute)[] GetKdlChildProps() => + internal (PropertyInfo, KdlNodeAttribute)[] GetKdlChildProps() => type.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Select(p => new { Property = p, - ChildAttr = p.GetCustomAttribute(), + ChildAttr = p.GetCustomAttribute(), }) .Where(x => x.ChildAttr is not null) .Select(x => (x.Property, x.ChildAttr!)) diff --git a/src/Kuddle/Serialization/Attributes/KdlChildrenAttribute.cs b/src/Kuddle/Serialization/Attributes/KdlChildrenAttribute.cs index fc1e300..602e12e 100644 --- a/src/Kuddle/Serialization/Attributes/KdlChildrenAttribute.cs +++ b/src/Kuddle/Serialization/Attributes/KdlChildrenAttribute.cs @@ -1,14 +1,14 @@ -using System; +// using System; -namespace Kuddle.Serialization; +// namespace Kuddle.Serialization; -/// -/// Marks a property to receive child nodes of a specific type from the KDL node's children block. -/// -/// The KDL node name to match for children (e.g., "dependency"). -[AttributeUsage(AttributeTargets.Property)] -public class KdlChildrenAttribute(string childNodeName) : Attribute -{ - /// Gets the child node name to match. - public string ChildNodeName { get; } = childNodeName; -} +// /// +// /// Marks a property to receive child nodes of a specific type from the KDL node's children block. +// /// +// /// The KDL node name to match for children (e.g., "dependency"). +// [AttributeUsage(AttributeTargets.Property)] +// public class KdlChildrenAttribute(string childNodeName) : Attribute +// { +// /// Gets the child node name to match. +// public string ChildNodeName { get; } = childNodeName; +// } diff --git a/src/Kuddle/Serialization/KdlSerializer.cs b/src/Kuddle/Serialization/KdlSerializer.cs index bf7b2da..9701dbb 100644 --- a/src/Kuddle/Serialization/KdlSerializer.cs +++ b/src/Kuddle/Serialization/KdlSerializer.cs @@ -41,9 +41,13 @@ public static IEnumerable DeserializeMany( var kdlTypeAttr = type.GetCustomAttribute(); if (kdlTypeAttr is null) - return null; - - return kdlTypeAttr.Name; + { + return type.Name.ToLowerInvariant(); + } + else + { + return kdlTypeAttr.Name; + } } public static T Deserialize(string text, KdlSerializerOptions? options = null) @@ -64,7 +68,10 @@ public static T Deserialize(string text, KdlSerializerOptions? options = null var rootNode = document.Nodes[0]; var expectedName = GetExpectedNodeName(type); - if (expectedName != null && rootNode.Name.Value != expectedName) + if ( + expectedName != null + && !rootNode.Name.Value.Equals(expectedName, StringComparison.OrdinalIgnoreCase) + ) { throw new KuddleSerializationException( $"Node name '{rootNode.Name.Value}' does not match target type name '{typeof(T).Name}'." @@ -91,10 +98,7 @@ private static void MapNodesToProperties(List nodes, T instance) foreach (var prop in props) { - var nodeName = - prop.GetCustomAttribute()?.Name - ?? prop.GetCustomAttribute()?.ChildNodeName - ?? null; + var nodeName = prop.GetCustomAttribute()?.Name; if (nodeName == null) continue; @@ -178,17 +182,55 @@ private static void MapToInstance(KdlNode node, T instance) SetPropValue(prop, instance, kdlProp); } - var childAttr = prop.GetCustomAttribute(); + var childAttr = prop.GetCustomAttribute(); if (childAttr is not null) { - var nodeName = childAttr.ChildNodeName ?? prop.Name.ToLowerInvariant(); - var groupedChildNodes = node.Children?.Nodes.GroupBy(n => n.Name) ?? []; - foreach (var childNode in groupedChildNodes) + // // Usually a collection + var nodeName = childAttr.Name ?? prop.Name.ToLowerInvariant(); + // var groupedChildNodes = node.Children?.Nodes.GroupBy(n => n.Name) ?? []; + // foreach (var childNode in groupedChildNodes) + // { + // if (childNode.Key.Value == nodeName) + // { + // SetCollectionPropValue(prop, instance, childNode); + // } + // } + + var matchingNodes = node + .Children?.Nodes.Where(n => + n.Name.Value.Equals(nodeName, StringComparison.OrdinalIgnoreCase) + ) + .ToList(); + + if (matchingNodes == null || matchingNodes.Count == 0) + continue; + + if (prop.PropertyType.IsIEnumerable) + { + SetCollectionPropValue(prop, instance, matchingNodes); + } + // CASE B: Scalar (string, int, etc.) -> Map Arg(0) + else if (!prop.PropertyType.IsComplexType) { - if (childNode.Key.Value == nodeName) - { - SetCollectionPropValue(prop, instance, childNode); - } + // Expect exactly one node + if (matchingNodes.Count > 1) + throw new KuddleSerializationException( + $"Expected single node '{nodeName}' for scalar property, found {matchingNodes.Count}" + ); + + // Extract Arg 0 + var val = matchingNodes[0].Arg(0); + SetPropValue(prop, instance, val!); + } + // CASE C: Complex Object -> Recursion + else + { + if (matchingNodes.Count > 1) + throw new KuddleSerializationException( + $"Expected single node '{nodeName}' for object property, found {matchingNodes.Count}" + ); + + SetSingleComplexPropValue(prop, instance, matchingNodes[0]); } } } @@ -396,7 +438,7 @@ private static KdlNode SerializeNode(object? instance, string? nodeName = null) var block = new KdlBlock(); foreach (var (prop, childAttr) in childProps) { - var childNodeName = childAttr.ChildNodeName ?? prop.Name.ToLowerInvariant(); + var childNodeName = childAttr.Name ?? prop.Name.ToLowerInvariant(); if (prop.PropertyType.IsDictionary) throw new NotSupportedException(); From f6536e96e8883f37dc58f42f39c89ec69553d1f2 Mon Sep 17 00:00:00 2001 From: jamesshenry <79054685+jamesshenry@users.noreply.github.com> Date: Wed, 17 Dec 2025 13:54:56 +0000 Subject: [PATCH 05/17] add docs and kdl samples --- docs/serialization-attributes.md | 453 ++++++++++++ samples/01-simple-node.kdl | 4 + samples/02-multiple-arguments.kdl | 7 + samples/03-nested-children.kdl | 11 + samples/04-numeric-types.kdl | 31 + samples/05-type-annotations.kdl | 30 + samples/06-booleans-and-null.kdl | 19 + samples/07-string-varieties.kdl | 33 + samples/08-deeply-nested.kdl | 62 ++ samples/09-mixed-entries.kdl | 25 + samples/10-document-vs-node.kdl | 24 + samples/11-slashdash-comments.kdl | 31 + samples/12-edge-cases.kdl | 53 ++ samples/13-real-world-package.kdl | 47 ++ samples/14-real-world-ci.kdl | 87 +++ spec.md | 1112 +++++++++++++++++++++++++++++ 16 files changed, 2029 insertions(+) create mode 100644 docs/serialization-attributes.md create mode 100644 samples/01-simple-node.kdl create mode 100644 samples/02-multiple-arguments.kdl create mode 100644 samples/03-nested-children.kdl create mode 100644 samples/04-numeric-types.kdl create mode 100644 samples/05-type-annotations.kdl create mode 100644 samples/06-booleans-and-null.kdl create mode 100644 samples/07-string-varieties.kdl create mode 100644 samples/08-deeply-nested.kdl create mode 100644 samples/09-mixed-entries.kdl create mode 100644 samples/10-document-vs-node.kdl create mode 100644 samples/11-slashdash-comments.kdl create mode 100644 samples/12-edge-cases.kdl create mode 100644 samples/13-real-world-package.kdl create mode 100644 samples/14-real-world-ci.kdl create mode 100644 spec.md diff --git a/docs/serialization-attributes.md b/docs/serialization-attributes.md new file mode 100644 index 0000000..44fe741 --- /dev/null +++ b/docs/serialization-attributes.md @@ -0,0 +1,453 @@ +# KDL Serialization Attribute Guide + +This document describes how to use Kuddle's serialization attributes to map between KDL documents and C# types. + +## Quick Reference + +| Attribute | Target | Purpose | +|-----------|--------|---------| +| `[KdlType]` | Class | Override the expected node name | +| `[KdlArgument(index)]` | Property | Map to a positional argument | +| `[KdlProperty(key?)]` | Property | Map to a named property | +| `[KdlNode(name?)]` | Property | Map to child nodes | +| `[KdlIgnore]` | Property | Exclude from serialization | + +--- + +## Node Name Matching + +### Default Convention + +By default, the class name (lowercased) must match the KDL node name: + +```csharp +// Matches: package "my-lib" +public class Package { ... } +``` + +### Custom Node Name with `[KdlType]` + +Override the expected node name: + +```csharp +// Matches: db "primary" (not "database") +[KdlType("db")] +public class Database { ... } +``` + +--- + +## `[KdlArgument(index)]` — Positional Arguments + +Maps a property to a **positional argument** by zero-based index. + +### KDL + +```kdl +point 10 20 30 label="origin" +``` + +### C# Model + +```csharp +public class Point +{ + [KdlArgument(0)] + public int X { get; set; } + + [KdlArgument(1)] + public int Y { get; set; } + + [KdlArgument(2)] + public int Z { get; set; } + + [KdlProperty("label")] + public string? Label { get; set; } +} +``` + +### Rules + +1. **Index is required** — Each `[KdlArgument]` must specify its position +2. **Indices should be contiguous** — Gaps (0, 2 without 1) may cause errors +3. **Order matters** — During serialization, arguments are written in index order +4. **Scalar types only** — Arguments cannot be complex objects + +### Supported Types + +- `string` +- `int`, `long`, `double`, `decimal` +- `bool` +- `Guid` (with `(uuid)` type annotation) +- `DateTimeOffset` (with `(date-time)` type annotation) +- Nullable variants of all above + +--- + +## `[KdlProperty(key?)]` — Named Properties + +Maps a property to a **KDL property** (key=value pair). + +### KDL + +```kdl +dependency lodash version="4.17.21" optional=#false +``` + +### C# Model + +```csharp +public class Dependency +{ + [KdlArgument(0)] + public string Package { get; set; } = ""; + + [KdlProperty("version")] + public string Version { get; set; } = "*"; + + [KdlProperty("optional")] + public bool Optional { get; set; } +} +``` + +### Key Name Resolution + +1. **Explicit key**: `[KdlProperty("my-key")]` → matches `my-key=...` +2. **Implicit key**: `[KdlProperty]` → uses property name lowercased + +```csharp +[KdlProperty] // Matches "timeout=..." +public int Timeout { get; set; } + +[KdlProperty("max-retries")] // Matches "max-retries=..." +public int MaxRetries { get; set; } +``` + +### Rules + +1. **Last value wins** — Per KDL spec, if `key=1 key=2`, the value is `2` +2. **Missing properties use defaults** — No error if property absent in KDL +3. **Scalar types only** — Properties cannot be complex objects + +--- + +## `[KdlNode(name?)]` — Child Nodes + +Maps a property to **child nodes** within the parent's `{ }` block. + +### Basic Usage — Collection of Child Nodes + +#### KDL + +```kdl +project web-app version="1.0.0" { + dependency lodash version="4.17.21" + dependency react version="18.2.0" + devDependency jest version="29.0.0" +} +``` + +#### C# Model + +```csharp +public class Project +{ + [KdlArgument(0)] + public string Name { get; set; } = ""; + + [KdlProperty("version")] + public string Version { get; set; } = "1.0.0"; + + [KdlNode("dependency")] + public List Dependencies { get; set; } = []; + + [KdlNode("devDependency")] + public List DevDependencies { get; set; } = []; +} +``` + +### Single Complex Child + +When the property type is a **non-collection complex type**, it maps to a single child node: + +#### KDL + +```kdl +application myapp { + database { + host "localhost" + port 5432 + } +} +``` + +#### C# Model + +```csharp +public class Application +{ + [KdlArgument(0)] + public string Name { get; set; } = ""; + + [KdlNode("database")] + public DatabaseConfig? Database { get; set; } +} + +public class DatabaseConfig +{ + [KdlNode("host")] // Maps child node's Arg(0) + public string Host { get; set; } = ""; + + [KdlNode("port")] // Maps child node's Arg(0) + public int Port { get; set; } +} +``` + +### Scalar Child Node + +When the property type is a **scalar type**, it extracts `Arg(0)` from the child node: + +#### KDL + +```kdl +config { + timeout 5000 + enabled #true +} +``` + +#### C# Model + +```csharp +public class Config +{ + [KdlNode("timeout")] + public int Timeout { get; set; } + + [KdlNode("enabled")] + public bool Enabled { get; set; } +} +``` + +### Node Name Resolution + +1. **Explicit name**: `[KdlNode("my-items")]` → matches child nodes named `my-items` +2. **Implicit name**: `[KdlNode]` → uses property name lowercased + +### Rules + +1. **Collection types** → Collects all matching child nodes into the list/array +2. **Complex types** → Expects exactly one matching child node (throws if multiple) +3. **Scalar types** → Extracts `Arg(0)` from the single matching child node +4. **Supported collection types**: `List`, `T[]`, `IEnumerable`, `IList` + +--- + +## `[KdlIgnore]` — Exclude Properties + +Excludes a property from both serialization and deserialization: + +```csharp +public class User +{ + [KdlArgument(0)] + public string Username { get; set; } = ""; + + [KdlIgnore] + public string ComputedDisplayName => Username.ToUpperInvariant(); + + [KdlIgnore] + public DateTime LoadedAt { get; set; } = DateTime.UtcNow; +} +``` + +--- + +## Type Conversion + +### Automatic Type Mapping + +| C# Type | KDL Representation | +|---------|-------------------| +| `string` | `"value"` or `bare-string` | +| `int`, `long` | `123`, `0xFF`, `0o77`, `0b1010` | +| `double`, `decimal` | `3.14`, `1.5e-10` | +| `bool` | `#true`, `#false` | +| `Guid` | `(uuid)"550e8400-..."` | +| `DateTimeOffset` | `(date-time)"2024-01-15T10:30:00Z"` | +| Nullable `T?` | Value or `#null` | + +### Type Annotations + +Type annotations in KDL provide parsing hints. The serializer recognizes: + +- `(uuid)` → `Guid` +- `(date-time)` → `DateTimeOffset` + +```kdl +user alice { + id (uuid)"550e8400-e29b-41d4-a716-446655440000" + createdAt (date-time)"2024-01-15T10:30:00Z" +} +``` + +--- + +## Document-Level vs Node-Level Deserialization + +### Single Node → Object + +When your type has `[KdlArgument]` or `[KdlProperty]` attributes, the deserializer expects **exactly one root node**: + +```csharp +var kdl = "package my-lib version=\"1.0\""; +var pkg = KdlSerializer.Deserialize(kdl); +``` + +### Multiple Nodes → Collection + +Use `DeserializeMany` for documents with multiple top-level nodes: + +```kdl +server web-1 host="10.0.0.1" +server web-2 host="10.0.0.2" +server api-1 host="10.0.1.1" +``` + +```csharp +var servers = KdlSerializer.DeserializeMany(kdl); +// Returns IEnumerable with 3 items +``` + +### Document as Container + +For a document where each node type maps to a different property: + +```kdl +name "my-project" +version "1.0.0" +author "Alice" +``` + +```csharp +public class Manifest +{ + [KdlNode("name")] + public string Name { get; set; } = ""; + + [KdlNode("version")] + public string Version { get; set; } = ""; + + [KdlNode("author")] + public string Author { get; set; } = ""; +} +``` + +--- + +## Complete Example + +### KDL Document + +```kdl +project web-app version="2.0.0" { + dependency lodash version="4.17.21" optional=#false + dependency react version="18.2.0" optional=#false + + devDependency jest version="29.0.0" + devDependency typescript version="5.0.0" + + author "Alice" { + email "alice@example.com" + url "https://github.com/alice" + } + + repository type="git" url="https://github.com/alice/web-app" +} +``` + +### C# Models + +```csharp +public class Project +{ + [KdlArgument(0)] + public string Name { get; set; } = ""; + + [KdlProperty("version")] + public string Version { get; set; } = "1.0.0"; + + [KdlNode("dependency")] + public List Dependencies { get; set; } = []; + + [KdlNode("devDependency")] + public List DevDependencies { get; set; } = []; + + [KdlNode("author")] + public Author? Author { get; set; } + + [KdlNode("repository")] + public Repository? Repository { get; set; } +} + +public class Dependency +{ + [KdlArgument(0)] + public string Package { get; set; } = ""; + + [KdlProperty("version")] + public string Version { get; set; } = "*"; + + [KdlProperty("optional")] + public bool Optional { get; set; } +} + +public class Author +{ + [KdlArgument(0)] + public string Name { get; set; } = ""; + + [KdlNode("email")] + public string? Email { get; set; } + + [KdlNode("url")] + public string? Url { get; set; } +} + +public class Repository +{ + [KdlProperty("type")] + public string Type { get; set; } = ""; + + [KdlProperty("url")] + public string Url { get; set; } = ""; +} +``` + +--- + +## Decision Tree: Which Attribute to Use? + +``` +Is the data a positional value after the node name? +├─ YES → [KdlArgument(index)] +└─ NO + │ + Is the data a key=value pair on the same line? + ├─ YES → [KdlProperty("key")] + └─ NO + │ + Is the data inside a { } children block? + ├─ YES → [KdlNode("childNodeName")] + └─ NO → Not mappable with current attributes +``` + +--- + +## Limitations & Notes + +1. **No dictionary support** — `Dictionary` types are not currently supported +2. **No polymorphism** — Cannot deserialize to derived types based on discriminator +3. **Case sensitivity** — Node/property name matching is case-insensitive by default +4. **Argument gaps** — Missing argument indices will throw; ensure contiguous indices +5. **Round-trip fidelity** — Comments, formatting, and slashdash elements are not preserved diff --git a/samples/01-simple-node.kdl b/samples/01-simple-node.kdl new file mode 100644 index 0000000..9ee78ec --- /dev/null +++ b/samples/01-simple-node.kdl @@ -0,0 +1,4 @@ +// Simple node with arguments and properties +// Maps to a single object with scalar values + +package "my-awesome-lib" version="2.1.0" description="A really cool library" diff --git a/samples/02-multiple-arguments.kdl b/samples/02-multiple-arguments.kdl new file mode 100644 index 0000000..99e7d19 --- /dev/null +++ b/samples/02-multiple-arguments.kdl @@ -0,0 +1,7 @@ +// Node with multiple positional arguments +// Arguments are ordered and indexed (0, 1, 2...) + +command git commit -m Initial commit + +// Another example: coordinates +point 10 20 30 label="origin" diff --git a/samples/03-nested-children.kdl b/samples/03-nested-children.kdl new file mode 100644 index 0000000..ddce116 --- /dev/null +++ b/samples/03-nested-children.kdl @@ -0,0 +1,11 @@ +// Node with child nodes (children block) +// Children are nodes inside { } + +project web-app version="1.0.0" { + dependency lodash version="4.17.21" optional=#false + dependency react version="18.2.0" optional=#false + dependency typescript version="5.0.0" optional=#true + + devDependency jest version="29.0.0" + devDependency eslint version="8.0.0" +} diff --git a/samples/04-numeric-types.kdl b/samples/04-numeric-types.kdl new file mode 100644 index 0000000..81b14c4 --- /dev/null +++ b/samples/04-numeric-types.kdl @@ -0,0 +1,31 @@ +// Various numeric types and formats +// KDL supports integers, floats, hex, octal, binary + +config { + // Integer properties + timeout 5000 + maxRetries 3 + port 8080 + + // Large numbers (long) + maxFileSize 9223372036854775807 + + // Floating point + ratio 0.75 + pi 3.14159265358979 + + // Scientific notation + precision 1.5e-10 + + // Different bases (all represent the same value: 255) + hexValue 0xFF + octalValue 0o377 + binaryValue 0b11111111 + + // Negative numbers + offset -100 + temperature -273.15 + + // Underscores for readability + population 7_900_000_000 +} diff --git a/samples/05-type-annotations.kdl b/samples/05-type-annotations.kdl new file mode 100644 index 0000000..3cdf365 --- /dev/null +++ b/samples/05-type-annotations.kdl @@ -0,0 +1,30 @@ +// Type annotations provide hints about value interpretation +// Format: (type)value + +user alice { + id (uuid)"550e8400-e29b-41d4-a716-446655440000" + createdAt (date-time)"2024-01-15T10:30:00Z" + lastLogin (date-time)"2024-12-17T14:22:33.123Z" + + // Numeric type annotations + age (u8)28 + balance (decimal)"1234567.89" + score (f64)98.5 +} + +// Type annotation on node name (context/role annotation) +(admin)user "superuser" { + permissions "all" +} + +// Various string type annotations from the spec +metadata { + email (email)"user@example.com" + website (url)"https://example.com" + ip (ipv4)"192.168.1.1" + ipv6 (ipv6)"2001:0db8:85a3:0000:0000:8a2e:0370:7334" + country (country-2)"US" + duration (duration)"P1Y2M3DT4H5M6S" + pattern (regex)"^[a-z]+$" + binary (base64)"SGVsbG8gV29ybGQh" +} diff --git a/samples/06-booleans-and-null.kdl b/samples/06-booleans-and-null.kdl new file mode 100644 index 0000000..5b5ca71 --- /dev/null +++ b/samples/06-booleans-and-null.kdl @@ -0,0 +1,19 @@ +// Boolean and null values use # prefix in KDL 2.0 + +feature dark-mode { + enabled #true + beta #false + deprecatedAt #null +} + +settings { + // All the keyword values + debug #true + production #false + overrideConfig #null + + // Special float keywords + infiniteTimeout #inf + negativeInfinity #-inf + undefinedValue #nan +} diff --git a/samples/07-string-varieties.kdl b/samples/07-string-varieties.kdl new file mode 100644 index 0000000..c671b6b --- /dev/null +++ b/samples/07-string-varieties.kdl @@ -0,0 +1,33 @@ +// KDL supports multiple string syntaxes + +strings { + // Bare/identifier strings (no quotes needed for simple values) + simple hello-world + kebab my-kebab-case + + // Quoted strings (required for whitespace, special chars) + withSpaces "hello world" + withQuote "say \"hello\"" + withNewline "line1\nline2" + withTab "col1\tcol2" + unicode "emoji: \u{1F600}" + + // Raw strings (no escape processing) + rawPath #"C:\Users\name\file.txt"# + rawRegex #"^\d+\.\d+\.\d+$"# + rawWithQuotes ##"She said "hello""## + + // Multi-line strings + multiLine """ + This is a multi-line string. + The indentation of the closing quotes + determines the base indentation. + All lines are dedented accordingly. + """ + + // Raw multi-line + rawMultiLine #""" + No escapes here: \n \t \u{1234} + All literal backslashes. + """# +} diff --git a/samples/08-deeply-nested.kdl b/samples/08-deeply-nested.kdl new file mode 100644 index 0000000..99f2bf7 --- /dev/null +++ b/samples/08-deeply-nested.kdl @@ -0,0 +1,62 @@ +// Deeply nested structure - common in config files + +application "enterprise-app" version="3.0.0" { + database "primary" { + connection { + host "db.example.com" + port 5432 + ssl #true + + pool { + minSize 5 + maxSize 50 + idleTimeout 30000 + } + + credentials { + username "app_user" + // In real config, this would be a reference + passwordEnvVar "DB_PASSWORD" + } + } + + replica "read-replica-1" { + host "replica1.example.com" + port 5432 + readOnly #true + } + + replica "read-replica-2" { + host "replica2.example.com" + port 5432 + readOnly #true + } + } + + cache "redis" { + host "cache.example.com" + port 6379 + ttl 3600 + + cluster { + enabled #true + nodes 3 + } + } + + logging { + level "info" + format "json" + + output "console" { + colorize #true + } + + output "file" { + path "/var/log/app.log" + maxSize 10485760 + maxFiles 5 + compress #true + } + } +} diff --git a/samples/09-mixed-entries.kdl b/samples/09-mixed-entries.kdl new file mode 100644 index 0000000..94b07da --- /dev/null +++ b/samples/09-mixed-entries.kdl @@ -0,0 +1,25 @@ +// Arguments and properties can be freely intermixed +// This is valid KDL - order of args is preserved, props can appear anywhere + +// Properties between arguments +node 1 key=value 2 another=prop 3 + +// Real-world example: CLI-like syntax +run "npm" "install" --save=#true "lodash" --dev=#false + +// Build configuration with mixed entries +task "build" priority=high "src/**/*.ts" output="dist" "!src/**/*.test.ts" { + compiler "tsc" + watch #false +} + +// HTTP request definition +request "POST" "/api/users" content-type="application/json" { + header "Authorization" "Bearer token123" + header "X-Request-Id" (uuid)"550e8400-e29b-41d4-a716-446655440000" + + body { + name "John Doe" + email "john@example.com" + } +} diff --git a/samples/10-document-vs-node.kdl b/samples/10-document-vs-node.kdl new file mode 100644 index 0000000..7d77f6e --- /dev/null +++ b/samples/10-document-vs-node.kdl @@ -0,0 +1,24 @@ +// Document with multiple top-level nodes +// This tests DeserializeMany and document-level mapping + +// When deserializing as a document, each top-level node maps to a list item +// When deserializing as a single node, you'd wrap in a container + +server "web-1" host="10.0.0.1" port=80 { + role "frontend" + role "load-balancer" +} + +server "web-2" host="10.0.0.2" port=80 { + role "frontend" +} + +server "api-1" host="10.0.1.1" port=8080 { + role "backend" + role "api" +} + +server "db-1" host="10.0.2.1" port=5432 { + role "database" + role "primary" +} diff --git a/samples/11-slashdash-comments.kdl b/samples/11-slashdash-comments.kdl new file mode 100644 index 0000000..de7c4a5 --- /dev/null +++ b/samples/11-slashdash-comments.kdl @@ -0,0 +1,31 @@ +// Slashdash comments out entire elements +// The serializer should NOT see these in the parsed AST + +config { + // Regular comment - ignored + enabled #true + + /- disabled #true // This entire node is commented out + + database { + host "localhost" + /- port 5432 // Commented out argument + port 3306 // This is the actual port + + credentials user="admin" /- password="secret" // password prop commented out + } + + /- slashdashed-node { + // Entire children block commented out + this "is" ignored=#true + } + + // Slashdash can span multiple lines + /- + experimental { + feature1 #true + feature2 #true + } + + production #true +} diff --git a/samples/12-edge-cases.kdl b/samples/12-edge-cases.kdl new file mode 100644 index 0000000..cb0669b --- /dev/null +++ b/samples/12-edge-cases.kdl @@ -0,0 +1,53 @@ +// Edge cases and tricky scenarios + +// Empty nodes (valid - just a name) +empty-node + +// Node with only children, no args/props +container { + child1 + child2 +} + +// Same property multiple times - rightmost wins (per spec) +settings priority=1 priority=2 priority=3 // priority should be 3 + +// Unicode identifiers (bare strings) +日本語ノード value="Japanese node name" +émoji-🎉 value="Emoji in identifier" + +// Very long numbers +bignum 999999999999999999999999999999 + +// Numbers with many underscores +formatted 1_000_000_000 + +// Semicolon-separated nodes on one line +a; b; c + +// Children on same line +parent { child1; child2; child3 } + +// Line continuation +very-long-node \ + arg1 \ + arg2 \ + prop1="value1" \ + prop2="value2" \ + { + child + } + +// Property with same name as argument value (no conflict) +node "key" key="different-value" + +// Nested type annotations +typed-node (container)data { + item (u8)255 + item (i8)-128 +} + +// Null in various positions +nullable arg1=#null prop=#null { + child #null +} diff --git a/samples/13-real-world-package.kdl b/samples/13-real-world-package.kdl new file mode 100644 index 0000000..dd1a440 --- /dev/null +++ b/samples/13-real-world-package.kdl @@ -0,0 +1,47 @@ +// Real-world example: Package manifest (like package.json in KDL) + +package "kuddle" version="1.0.0" { + description "A KDL parser and serializer for .NET" + license "MIT" + + author "Henry" email="henry@example.com" { + url "https://github.com/henry" + } + + repository type="git" url="https://github.com/henry/kuddle" + + keywords "kdl" "parser" "serializer" "configuration" "dotnet" + + engines { + dotnet ">=8.0.0" + } + + scripts { + build "dotnet build" + test "dotnet test" + pack "dotnet pack" + publish "dotnet nuget push" + } + + dependency "Parlot" version="^1.0.0" { + optional #false + } + + devDependency "TUnit" version="^1.0.0" + devDependency "BenchmarkDotNet" version="^0.14.0" + + // Conditional dependencies + dependency "System.Text.Json" version=">=8.0.0" { + condition "net8.0" + } + + files "src/**/*.cs" "!src/**/*.Tests.cs" { + include "LICENSE" + include "README.md" + } + + exports { + main "./src/Kuddle/Kuddle.csproj" + types "./src/Kuddle/Kuddle.csproj" + } +} diff --git a/samples/14-real-world-ci.kdl b/samples/14-real-world-ci.kdl new file mode 100644 index 0000000..e8bc03e --- /dev/null +++ b/samples/14-real-world-ci.kdl @@ -0,0 +1,87 @@ +// Real-world example: CI/CD pipeline configuration + +pipeline "main" { + trigger { + branch "main" "develop" "release/*" + path "src/**" "tests/**" ".github/**" + + // Exclude paths + exclude "**/*.md" "docs/**" + } + + variables { + variable "DOTNET_VERSION" "8.0.x" + variable "BUILD_CONFIGURATION" "Release" + variable "NUGET_FEED" "https://api.nuget.org/v3/index.json" secret=#false + } + + stage "build" { + displayName "Build and Test" + + job "build-windows" { + pool "windows-latest" + + step "checkout" { + uses "actions/checkout@v4" + with { + fetch-depth 0 + } + } + + step "setup-dotnet" { + uses "actions/setup-dotnet@v4" + with { + dotnet-version "${{ variables.DOTNET_VERSION }}" + } + } + + step "restore" { + run "dotnet restore" + } + + step "build" { + run "dotnet build --configuration ${{ variables.BUILD_CONFIGURATION }} --no-restore" + } + + step "test" { + run "dotnet test --configuration ${{ variables.BUILD_CONFIGURATION }} --no-build --logger trx" + continueOnError #false + } + } + + job "build-linux" { + pool "ubuntu-latest" + dependsOn #null // Runs in parallel + + step "checkout" uses="actions/checkout@v4" + step "setup-dotnet" uses="actions/setup-dotnet@v4" + step "restore" run="dotnet restore" + step "build" run="dotnet build -c Release" + step "test" run="dotnet test -c Release" + } + } + + stage "publish" { + displayName "Publish Package" + dependsOn "build" + condition "eq(variables['Build.SourceBranch'], 'refs/heads/main')" + + job "publish-nuget" { + pool "ubuntu-latest" + + step "download-artifact" { + uses "actions/download-artifact@v4" + with { + name "nuget-package" + } + } + + step "push-nuget" { + run "dotnet nuget push **/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source ${{ variables.NUGET_FEED }}" + env { + NUGET_API_KEY "${{ secrets.NUGET_API_KEY }}" + } + } + } + } +} diff --git a/spec.md b/spec.md new file mode 100644 index 0000000..c34e33f --- /dev/null +++ b/spec.md @@ -0,0 +1,1112 @@ +--- +title: "The KDL Document Language" +abbrev: "KDL" +docname: draft-marchan-kdl2-latest +submissionType: independent +category: exp + +ipr: none +area: General +venue: + github: kdl-org/kdl + home: +workgroup: KDL Community +keyword: + +- Document-Language +- Configuration + +stand_alone: yes +smart_quotes: no +pi: [toc, sortrefs, symrefs] + +author: + +- name: Katerina Zoé Marchán Salvá + ins: K. Marchán + organization: Microsoft +- name: The KDL Contributors + ins: KDL Contributors + +normative: + +informative: + +--- abstract + +KDL is a node-oriented document language. Its niche and purpose overlaps with +XML, and as do many of its semantics. You can use KDL both as a configuration +language, and a data exchange or storage format, if you so choose. + +This is the formal specification for KDL, including the intended data model and +the grammar. + +This document describes an unreleased minor change to KDL. For the latest +oficial version of the language, see . + + + +--- note_License + +This work is licensed under Creative Commons Attribution-ShareAlike 4.0 +International. To view a copy of this license, visit + + +--- middle + +# Compatibility + +KDL 2.0 is designed such that for any given KDL document written as [KDL +1.0](./SPEC_v1.md) or KDL 2.0, the parse will either fail completely, or, if the +parse succeeds, the data represented by a v1 or v2 parser will be identical. +This means that it's safe to use a fallback parsing strategy in order to support +both v1 and v2 simultaneously. For example, `node "foo"` is a valid node in both +versions, and should be represented identically by parsers. + +A version marker `/- kdl-version 2` (or `1`) _MAY_ be added to the beginning of +a KDL document, optionally preceded by the BOM, and parsers _MAY_ use that as a +hint as to which version to parse the document as. + +# Introduction + +KDL is a node-oriented document language. Its niche and purpose overlaps with +XML, and as do many of its semantics. You can use KDL both as a configuration +language, and a data exchange or storage format, if you so choose. + +The bulk of this document is dedicated to a long-form description of all +Components ({{components}}) of a KDL document. +There is also a much more terse +Grammar ({{full-grammar}}) at the end of the document that covers most of the +rules, with some semantic exceptions involving the data model. + +KDL is designed to be easy to read _and_ easy to implement. + +In this document, references to "left" or "right" refer to directions in the +_data stream_ towards the beginning or end, respectively; in other words, +the directions if the data stream were only ASCII text. They do not refer +to the writing direction of text, which can flow in either direction, +depending on the characters used. + +# Components + +## Document + +The toplevel concept of KDL is a Document. A Document is composed of zero or +more Nodes ({{node}}), separated by newlines, semicolons, and whitespace, and eventually +terminated by an EOF. + +All KDL documents MUST be encoded in UTF-8 and conform to the specifications in +this document. + +### Example + +The following is a document composed of two toplevel nodes: + +~~~kdl +foo { + bar +} +baz +~~~ + +## Node + +Being a node-oriented language means that the real core component of any KDL +document is the "node". Every node must have a name, which must be a +String ({{string}}). + +The name may be preceded by a Type Annotation ({{type-annotation}}) to further +clarify its type, particularly in relation to its parent node. (For example, +clarifying that a particular `date` child node is for the _publication_ date, +rather than the last-modified date, with `(published)date`.) + +Following the name are zero or more Arguments ({{argument}}) or +Properties ({{property}}), separated by either whitespace ({{whitespace}}) or a +slash-escaped line continuation ({{line-continuation}}). Arguments and Properties +may be interspersed in any order, much like is common with positional arguments +vs options in command line tools. Collectively, Arguments and Properties may be +referred to as "Entries". + +Children ({{children-block}}) can be placed after the name and the optional +Entries, possibly separated by either whitespace or a +slash-escaped line continuation. + +Arguments are ordered relative to each other and that order must be preserved in +order to maintain the semantics. Properties between Arguments do not affect +Argument ordering. + +By contrast, Properties _SHOULD NOT_ be assumed to be presented in a given +order. Children ({{children-block}}) should be used if an order-sensitive +key/value data structure must be represented in KDL. Cf. JSON objects +preserving key order. + +Nodes _MAY_ be prefixed with Slashdash ({{slashdash-comments}}) to "comment out" +the entire node, including its properties, arguments, and children, and make +it act as plain whitespace, even if it spreads across multiple lines. + +Finally, a node is terminated by either a Newline ({{newline}}), a semicolon +(`;`), the end of its parent's child block (`}`) or the end of the file/stream +(an `EOF`). + +### Example + +~~~kdl +// `foo` will have an Argument value list like `[1, 3]`. +foo 1 key=val 3 { + bar + (role)baz 1 2 +} +~~~ + +## Line Continuation + +Line continuations allow Nodes ({{node}}) to be spread across multiple lines. + +A line continuation is a `\` character followed by zero or more whitespace +items (including multiline comments) and an optional single-line comment. It +must be terminated by a Newline ({{newline}}) (including the Newline that is +part of single-line comments). + +Following a line continuation, processing of a Node can continue as usual. + +### Example + +~~~kdl +my-node 1 2 \ // comments are ok after \ + 3 4 // This is the actual end of the Node. +~~~ + +## Property + +A Property is a key/value pair attached to a Node ({{node}}). A Property is +composed of a String ({{string}}), followed immediately by an equals sign (`=`, `U+003D`), +and then a Value ({{value}}). + +Properties should be interpreted left-to-right, with rightmost properties with +identical names overriding earlier properties. That is: + +~~~kdl +node a=1 a=2 +~~~ + +In this example, the node's `a` value must be `2`, not `1`. + +No other guarantees about order should be expected by implementers. +Deserialized representations may iterate over properties in any order and +still be spec-compliant. + +Properties _MAY_ be prefixed with `/-` to "comment out" the entire token and +make it act as plain whitespace, even if it spreads across multiple lines. + +## Argument + +An Argument is a bare Value ({{value}}) attached to a Node ({{node}}), with no +associated key. It shares the same space as Properties ({{property}}), and may be interleaved with them. + +A Node may have any number of Arguments, which should be evaluated left to +right. KDL implementations _MUST_ preserve the order of Arguments relative to +each other (not counting Properties). + +Arguments _MAY_ be prefixed with `/-` to "comment out" the entire token and +make it act as plain whitespace, even if it spreads across multiple lines. + +### Example + +~~~kdl +my-node 1 2 3 a b c +~~~ + +## Children Block + +A children block is a block of Nodes ({{node}}), surrounded by `{` and `}`. They +are an optional part of nodes, and create a hierarchy of KDL nodes. + +Regular node termination rules apply, which means multiple nodes can be +included in a single-line children block, as long as they're all terminated by +`;`. + +### Example + +~~~kdl +parent { + child1 + child2 +} + +parent { child1; child2 } +~~~ + +## Value + +A value is either: a String ({{string}}), a Number ({{number}}), a +Boolean ({{boolean}}), or Null ({{null}}). + +Values _MUST_ be either Arguments ({{argument}}) or values of +Properties ({{property}}). Only String ({{string}}) values may be used as +Node ({{node}}) names or Property ({{property}}) keys. + +Values (both as arguments and in properties) _MAY_ be prefixed by a single +Type Annotation ({{type-annotation}}). + +## Type Annotation + +A type annotation is a prefix to any Node Name ({{node}}) or Value ({{value}}) that +includes a _suggestion_ of what type the value is _intended_ to be treated as, +or as a _context-specific elaboration_ of the more generic type the node name +indicates. + +Type annotations are written as a set of `(` and `)` with a single +String ({{string}}) in it. It may contain Whitespace after the `(` and before +the `)`, and may be separated from its target by Whitespace. + +KDL does not specify any restrictions on what implementations might do with +these annotations. They are free to ignore them, or use them to make decisions +about how to interpret a value. + +Additionally, the following type annotations MAY be recognized by KDL parsers +and, if used, SHOULD interpret these types as follows: + +### Reserved Type Annotations for Numbers Without Decimals + +Signed integers of various sizes (the number is the bit size): + +- `i8` +- `i16` +- `i32` +- `i64` +- `i128` + +Unsigned integers of various sizes (the number is the bit size): + +- `u8` +- `u16` +- `u32` +- `u64` +- `u128` + +Platform-dependent integer types, both signed and unsigned: + +- `isize` +- `usize` + +### Reserved Type Annotations for Numbers With Decimals + +IEEE 754 floating point numbers, both single (32) and double (64) precision: + +- `f32` +- `f64` + +IEEE 754-2008 decimal floating point numbers + +- `decimal64` +- `decimal128` + +### Reserved Type Annotations for Strings + +- `date-time`: ISO8601 date/time format. +- `time`: "Time" section of ISO8601. +- `date`: "Date" section of ISO8601. +- `duration`: ISO8601 duration format. +- `decimal`: IEEE 754-2008 decimal string format. +- `currency`: ISO 4217 currency code. +- `country-2`: ISO 3166-1 alpha-2 country code. +- `country-3`: ISO 3166-1 alpha-3 country code. +- `country-subdivision`: ISO 3166-2 country subdivision code. +- `email`: RFC5322 email address. +- `idn-email`: RFC6531 internationalized email address. +- `hostname`: RFC1123 internet hostname (only ASCII segments) +- `idn-hostname`: RFC5890 internationalized internet hostname + (only `xn--`-prefixed ASCII "punycode" segments, or non-ASCII segments) +- `ipv4`: RFC2673 dotted-quad IPv4 address. +- `ipv6`: RFC2373 IPv6 address. +- `url`: RFC3986 URI. +- `url-reference`: RFC3986 URI Reference. +- `irl`: RFC3987 Internationalized Resource Identifier. +- `irl-reference`: RFC3987 Internationalized Resource Identifier Reference. +- `url-template`: RFC6570 URI Template. +- `uuid`: RFC4122 UUID. +- `regex`: Regular expression. Specific patterns may be implementation-dependent. +- `base64`: A Base64-encoded string, denoting arbitrary binary data. +- `base85`: An [Ascii85](https://en.wikipedia.org/wiki/Ascii85)-encoded string, denoting arbitrary binary data. + +### Examples + +~~~kdl +node (u8)123 +node prop=(regex).* +(published)date "1970-01-01" +(contributor)person name="Foo McBar" +~~~ + +## String + +Strings in KDL represent textual UTF-8 Values ({{value}}). A String is either an +Identifier String ({{identifier-string}}) (like `foo`), a +Quoted String ({{quoted-string}}) (like `"foo"`) +or a Multi-Line String ({{multi-line-string}}). +Both Quoted and Multiline strings come in normal +and Raw String ({{raw-string}}) variants (like `#"foo"#`): + +- Identifier Strings let you write short, "single-word" strings with a + minimum of syntax +- Quoted Strings let you write strings "like normal", with whitespace and escapes. +- Multi-Line Strings let you write strings across multiple lines + and with indentation that's not part of the string value. +- Raw Strings don't allow any escapes, + allowing you to not worry about the string's content containing anything that + might look like an escape. + +Strings _MUST_ be represented as UTF-8 values. + +Strings _MUST NOT_ include the code points for +disallowed literal code points ({{disallowed-literal-code-points}}) directly. +Quoted and Multi-Line Strings may include these code points as _values_ +by representing them with their corresponding `\u{...}` escape. + +## Identifier String + +An Identifier String (sometimes referred to as just an "identifier") is +composed of any [Unicode Scalar +Value](https://unicode.org/glossary/#unicode_scalar_value) other than +non-initial characters ({{non-initial-characters}}), followed by any number of +Unicode Scalar Values other than non-identifier +characters ({{non-identifier-characters}}). + +A handful of patterns are disallowed, to avoid confusion with other values: + +- idents that appear to start with a Number ({{number}}) (like `1.0v2` or + `-1em`) or the "almost a number" pattern of a decimal point without a + leading digit (like `.1`). +- idents that are the language keywords (`inf`, `-inf`, `nan`, `true`, + `false`, and `null`) without their leading `#`. + +Identifiers that match these patterns _MUST_ be treated as a syntax error; such +values can only be written as quoted or raw strings. The precise details of the +identifier syntax is specified in the Full Grammar in {{full-grammar}}. + +### Non-initial characters + +The following characters cannot be the first character in an +Identifier String ({{identifier-string}}): + +- Any decimal digit (0-9) +- Any non-identifier characters ({{non-identifier-characters}}) + +Additionally, the following initial characters impose limitations on subsequent +characters: + +- the `+` and `-` characters can only be used as an initial character if + the second character is _not_ a digit. If the second character is `.`, then + the third character must _not_ be a digit. +- the `.` character can only be used as an initial character if + the second character is _not_ a digit. + +This allows identifiers to look like `--this` or `.md`, and removes the +ambiguity of having an identifier look like a number. + +### Non-identifier characters + +The following characters cannot be used anywhere in a Identifier String ({{identifier-string}}): + +- Any of `(){}[]/\"#;=` +- Any Whitespace ({{whitespace}}) or Newline ({{newline}}). +- Any disallowed literal code points ({{disallowed-literal-code-points}}) in KDL + documents. + +## Quoted String + +A Quoted String is delimited by `"` on either side of any number of literal +string characters except unescaped `"` and `\`. + +Literal Newline ({{newline}}) characters can only be included +if they are Escaped Whitespace ({{escaped-whitespace}}), +which discards them from the string value. +Actually including a newline in the value requires using a newline escape sequence, +like `\n`, +or using a Multi-Line String ({{multi-line-string}}) +which is actually designed for strings stretching across multiple lines. + +Like Identifier Strings, Quoted Strings _MUST NOT_ include any of the +disallowed literal code-points ({{disallowed-literal-code-points}}) as code +points in their body. + +Quoted Strings have a Raw String ({{raw-string}}) variant, +which disallows escapes. + +### Escapes + +In addition to literal code points, a number of "escapes" are supported in Quoted Strings. +"Escapes" are the character `\` followed by another character, and are +interpreted as described in the following table: + +| Name | Escape | Code Pt | +|-------------------------------|--------|----------| +| Line Feed | `\n` | `U+000A` | +| Carriage Return | `\r` | `U+000D` | +| Character Tabulation (Tab) | `\t` | `U+0009` | +| Reverse Solidus (Backslash) | `\\` | `U+005C` | +| Quotation Mark (Double Quote) | `\"` | `U+0022` | +| Backspace | `\b` | `U+0008` | +| Form Feed | `\f` | `U+000C` | +| Space | `\s` | `U+0020` | +| Unicode Escape | `\u{(1-6 hex chars)}` | Code point described by hex characters, as long as it represents a [Unicode Scalar Value](https://unicode.org/glossary/#unicode_scalar_value) | +| Whitespace Escape | See below | N/A | + +#### Escaped Whitespace + +In addition to escaping individual characters, `\` can also escape whitespace. +When a `\` is followed by one or more literal whitespace characters, the `\` +and all of that whitespace are discarded. For example, + +~~~kdl +"Hello World" +~~~ + +and + +~~~kdl +"Hello \ World" +~~~ + +are semantically identical. See whitespace ({{whitespace}}) +and newlines ({{newline}}) for how whitespace is defined. + +Note that only literal whitespace is escaped; whitespace escapes (`\n` and +such) are retained. For example, these strings are all semantically identical: + +~~~kdl +"Hello\ \nWorld" + + "Hello\n\ + World" + +"Hello\nWorld" + +""" + Hello + World + """ +~~~ + +#### Invalid escapes + +Except as described in the escapes table, above, `\` _MUST NOT_ precede any +other characters in a string. + +## Multi-line String + +Multi-Line Strings support multiple lines with literal, non-escaped +Newlines. They must use a special multi-line syntax, and they automatically +"dedent" the string, allowing its value to be indented to a visually matching +level as desired. + +A Multi-Line String is opened and closed by _three_ double-quote characters, +like `"""`. +Its first line _MUST_ immediately start with a Newline ({{newline}}) +after its opening `"""`. +Its final line _MUST_ contain only whitespace +before the closing `"""`. +All in-between lines that contain non-newline, non-whitespace characters +_MUST_ start with _at least_ the exact same whitespace as the final line +(precisely matching codepoints, not merely counting characters or "size"); +they may contain additional whitespace following this prefix. The lines in +between may contain unescaped `"` (but no unescaped `"""` as this would close +the string). + +The value of the Multi-Line String omits the first and last Newline, the +Whitespace of the last line, and the matching Whitespace prefix on all +intermediate lines. The first and last Newline can be the same character (that +is, empty multi-line strings are legal). + +In other words, the final line specifies the whitespace prefix that will be +removed from all other lines. + +Whitespace-only lines (that is, lines containing only literal whitespace +characters, not including whitespace escapes like `\t`) always represent +empty lines in the string value, regardless of what whitespace they +contain (if any). They do not have to start with the same whitespace prefix +that other lines do; all characters on the line are ignored. + +Multi-line Strings that do not immediately start with a Newline and whose final +`"""` is not preceeded by optional whitespace and a Newline are illegal. This +also means that `"""` may not be used for a single-line String (e.g. +`"""foo"""`). + +### Newline Normalization + +Literal Newline sequences in Multi-line Strings must be normalized to a single +`U+000A` (`LF`) during deserialization. This means, for example, that `CR LF` +becomes a single `LF` during parsing. + +This normalization does not apply to non-literal Newlines entered using escape +sequences. That is: + +~~~kdl +multi-line """ + \r\n[CRLF] + foo[CRLF] + """ +~~~ + +becomes: + +~~~kdl +single-line "\r\n\nfoo" +~~~ + +For clarity: this normalization applies to each individual Newline sequence. +That is, the literal sequence `CRLF CRLF` becomes `LF LF`, not `LF`. + +### Examples + +#### Indented multi-line string + +~~~kdl +multi-line """ + foo + This is the base indentation + bar + """ +~~~ + +This example's string value will be: + +~~~ + foo +This is the base indentation + bar +~~~ + +which is equivalent to + +~~~kdl +" foo\nThis is the base indentation\n bar" +~~~ + +when written as a single-line string. + +#### Shorter last-line indent + +If the last line wasn't indented as far, +it won't dedent the rest of the lines as much: + +~~~kdl +multi-line """ + foo + This is no longer on the left edge + bar + """ +~~~ + +This example's string value will be: + +~~~ + foo + This is no longer on the left edge + bar +~~~ + +Equivalent to + +~~~kdl +" foo\n This is no longer on the left edge\n bar" +~~~ + +#### Empty lines + +Empty lines can contain any whitespace, or none at all, and will be reflected as empty in the value: + +~~~kdl +multi-line """ + Indented a bit + + A second indented paragraph. + """ +~~~ + +This example's string value will be: + +~~~ +Indented a bit. + +A second indented paragraph. +~~~ + +Equivalent to + +~~~kdl +"Indented a bit.\n\nA second indented paragraph." +~~~ + +#### Syntax errors + +The following yield **syntax errors**: + +~~~kdl +multi-line """can't be single line""" +~~~ + +~~~kdl +multi-line """ + closing quote with non-whitespace prefix""" +~~~ + +~~~kdl +multi-line """stuff + """ +~~~ + +~~~kdl +// Every line must share the exact same prefix as the closing line. +multi-line """[\n] +[tab]a[\n] +[space][space]b[\n] +[space][tab][\n] +[tab]""" +~~~ + +### Interaction with Whitespace Escapes + +Multi-line strings support the same mechanism for escaping whitespace as Quoted +Strings. + +When processing a Multi-line String, implementations MUST dedent the string +_after_ resolving all whitespace escapes, but _before_ resolving other backslash +escapes. This means a whitespace escape that attempts to escape the final line's +newline and/or whitespace prefix can be invalid: if removing escaped whitespace +places the closing `"""` on a line with non-whitespace characters, this escape +is invalid. + +For example, the following example is illegal: + +~~~kdl + """ + foo + bar\ + """ + + // equivalent to + """ + foo + bar""" +~~~ + +while the following example is allowed + +~~~kdl + """ + foo \ +bar + baz + \ """ + + // equivalent to + """ + foo bar + baz + """ +~~~ + +## Raw String + +Both Quoted ({{quoted-string}}) and Multi-Line Strings ({{multi-line-string}}) have +Raw String variants, which are identical in syntax except they do not support +`\`-escapes. This includes line-continuation escapes (`\` + `ws` collapsing to +nothing). They otherwise share the same properties as far as literal +Newline ({{newline}}) characters go, multi-line rules, and the requirement of +UTF-8 representation. + +The Raw String variants are indicated by preceding the strings's opening quotes +with one or more `#` characters. The string is then closed by its normal closing +quotes, followed by a _matching_ number of `#` characters. This means that the +string may contain any combination of `"` and `#` characters other than its +closing delimiter (e.g., if a raw string starts with `##"`, it can contain `"` +or `"#`, but not `"##` or `"###`). + +Like other Strings, Raw Strings _MUST NOT_ include any of the disallowed +literal code-points ({{disallowed-literal-code-points}}) as code points in their +body. Unlike with Quoted Strings, these cannot simply be escaped, and are thus +unrepresentable when using Raw Strings. + +### Example + +~~~kdl +just-escapes #"\n will be literal"# +~~~ + +The string contains the literal characters `\n will be literal`. + +~~~kdl +quotes-and-escapes ##"hello\n\r\asd"#world"## +~~~ + +The string contains the literal characters `hello\n\r\asd"#world` + +~~~kdl +raw-multi-line #""" + Here's a """ + multiline string + """ + without escapes. + """# +~~~ + +The string contains the value + +~~~ +Here's a """ + multiline string + """ +without escapes. +~~~ + +or equivalently, + +~~~kdl +"Here's a \"\"\"\n multiline string\n \"\"\"\nwithout escapes." +~~~ + +as a Quoted String. + +## Number + +Numbers in KDL represent numerical Values ({{value}}). There is no logical distinction in KDL +between real numbers, integers, and floating point numbers. It's up to +individual implementations to determine how to represent KDL numbers. + +There are five syntaxes for Numbers: Keywords, Decimal, Hexadecimal, Octal, and Binary. + +- All non-Keyword ({{keyword-numbers}}) numbers may optionally start with one of `-` or `+`, which determine whether they'll be positive or negative. +- Binary numbers start with `0b` and only allow `0` and `1` as digits, which may be separated by `_`. They represent numbers in radix 2. +- Octal numbers start with `0o` and only allow digits between `0` and `7`, which may be separated by `_`. They represent numbers in radix 8. +- Hexadecimal numbers start with `0x` and allow digits between `0` and `9`, as well as letters `A` through `F`, in either lower or upper case, which may be separated by `_`. They represent numbers in radix 16. +- Decimal numbers are a bit more special: + - They have no radix prefix. + - They use digits `0` through `9`, which may be separated by `_`. + - They may optionally include a decimal separator `.`, followed by more digits, which may again be separated by `_`. + - They may optionally be followed by `E` or `e`, an optional `-` or `+`, and more digits, to represent an exponent value. + +In all cases where the above says that digits "may be separated by `_`", +this means that between any two digits, or after the digits, any number of +consecutive `_` characters can appear. Underscores are not allowed _before_ the digits. +That is, `1___2` and `12____` are valid (and both equivalent to just `12`), but +`_12` is _not_ a valid number (it will instead parse as an identifier string), +nor is `0x_1a` (it will simply be invalid). + +Note that, similar to JSON and some other languages, +numbers without an integer digit (such as `.1`) are illegal. +They must be written with at least one integer digit, like `0.1`. +(These patterns are also disallowed from Identifier Strings ({{identifier-string}}), to avoid confusion.) + +### Keyword Numbers + +There are three special "keyword" numbers included in KDL to accomodate the +widespread use of [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754) floats: + +- `#inf` - floating point positive infinity. +- `#-inf` - floating point negative infinity. +- `#nan` - floating point NaN/Not a Number. + +To go along with this and prevent foot guns, the bare Identifier +Strings ({{identifier-string}}) `inf`, `-inf`, and `nan` are considered illegal +identifiers and should yield a syntax error. + +The existence of these keywords does not imply that any numbers be represented +as IEEE 754 floats. These are simply for clarity and convenience for any +implementation that chooses to represent their numbers in this way. + +## Boolean + +A boolean Value ({{value}}) is either the symbol `#true` or `#false`. These +_SHOULD_ be represented by implementation as boolean logical values, or some +approximation thereof. + +### Example + +~~~kdl +my-node #true value=#false +~~~ + +## Null + +The symbol `#null` represents a null Value ({{value}}). It's up to the +implementation to decide how to represent this, but it generally signals the +"absence" of a value. + +### Example + +~~~kdl +my-node #null key=#null +~~~ + +## Whitespace + +The following characters should be treated as non-Newline ({{newline}}) [white +space](https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt): + +| Name | Code Pt | +| ------------------------- | -------- | +| Character Tabulation | `U+0009` | +| Space | `U+0020` | +| No-Break Space | `U+00A0` | +| Ogham Space Mark | `U+1680` | +| En Quad | `U+2000` | +| Em Quad | `U+2001` | +| En Space | `U+2002` | +| Em Space | `U+2003` | +| Three-Per-Em Space | `U+2004` | +| Four-Per-Em Space | `U+2005` | +| Six-Per-Em Space | `U+2006` | +| Figure Space | `U+2007` | +| Punctuation Space | `U+2008` | +| Thin Space | `U+2009` | +| Hair Space | `U+200A` | +| Narrow No-Break Space | `U+202F` | +| Medium Mathematical Space | `U+205F` | +| Ideographic Space | `U+3000` | + +### Single-line comments + +Any text after `//`, until the next literal Newline ({{newline}}) is "commented +out", and is considered to be Whitespace ({{whitespace}}). + +### Multi-line comments + +In addition to single-line comments using `//`, comments can also be started +with `/*` and ended with `*/`. These comments can span multiple lines. They +are allowed in all positions where Whitespace ({{whitespace}}) is allowed and +can be nested. + +### Slashdash comments + +Finally, a special kind of comment called a "slashdash", denoted by `/-`, can +be used to comment out entire _components_ of a KDL document logically, and +have those elements not be included as part of the parsed document data. + +Slashdash comments can be used before the following, including before their type +annotations, if present: + +- A Node ({{node}}): the entire Node is treated as Whitespace, including all + props, args, and children. +- An Argument ({{argument}}): the Argument value is treated as Whitespace. +- A Property ({{property}}) key: the entire property, including both key and value, + is treated as Whitespace. A slashdash of just the property value is not allowed. +- A Children Block ({{children-block}}): the entire block, including all + children within, is treated as Whitespace. Only other children blocks, whether + slashdashed or not, may follow a slashdashed children block. + +A slashdash may be be followed by any amount of whitespace, including newlines and +comments (other than other slashdashes), before the element that it comments out. + +## Newline + +The following character sequences [should be treated as new +lines](https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-5/#G41643): + +| Acronym | Name | Code Pt | +| ------- | ----------------------------- | ------------------- | +| CRLF | Carriage Return and Line Feed | `U+000D` + `U+000A` | +| CR | Carriage Return | `U+000D` | +| LF | Line Feed | `U+000A` | +| NEL | Next Line | `U+0085` | +| VT | Vertical tab | `U+000B` | +| FF | Form Feed | `U+000C` | +| LS | Line Separator | `U+2028` | +| PS | Paragraph Separator | `U+2029` | + +Note that for the purpose of new lines, the specific sequence `CRLF` is +considered _a single newline_. + +## Disallowed Literal Code Points + +The following code points may not appear literally anywhere in the document. +They may be represented in Strings (but not Raw Strings) using Unicode Escapes ({{escapes}}) (`\u{...}`, +except for non Unicode Scalar Value, which can't be represented even as escapes). + +- The codepoints `U+0000-0008` or the codepoints `U+000E-001F` (various + control characters). +- `U+007F` (the Delete control character). +- Any codepoint that is not a [Unicode Scalar + Value](https://unicode.org/glossary/#unicode_scalar_value) (`U+D800-DFFF`). +- `U+200E-200F`, `U+202A-202E`, and `U+2066-2069`, the [unicode + "direction control" + characters](https://www.w3.org/International/questions/qa-bidi-unicode-controls) +- `U+FEFF`, aka Zero-width Non-breaking Space (ZWNBSP)/Byte Order Mark (BOM), + except as the first code point in a document. + +# Full Grammar + +This is the full official grammar for KDL and should be considered +authoritative if something seems to disagree with the text above. The grammar +language syntax is defined in {{grammar-language}}. + +~~~abnf +document := bom? version? nodes + +// Nodes +nodes := (line-space* node)* line-space* + +base-node := slashdash? type? node-space* string + (node-space* (node-space | slashdash) node-prop-or-arg)* + // slashdashed node-children must always be after props and args. + (node-space* slashdash node-children)* + (node-space* node-children)? + (node-space* slashdash node-children)* + node-space* +node := base-node node-terminator +final-node := base-node node-terminator? + +// Entries +node-prop-or-arg := prop | value +node-children := '{' nodes final-node? '}' +node-terminator := single-line-comment | newline | ';' | eof + +prop := string node-space* '=' node-space* value +value := type? node-space* (string | number | keyword) +type := '(' node-space* string node-space* ')' + +// Strings +string := identifier-string | quoted-string | raw-string ¶ + +identifier-string := + (unambiguous-ident | signed-ident | dotted-ident) + - disallowed-keyword-identifiers +unambiguous-ident := + (identifier-char - digit - sign - '.') identifier-char* +signed-ident := + sign ((identifier-char - digit - '.') identifier-char*)? +dotted-ident := + sign? '.' ((identifier-char - digit) identifier-char*)? +identifier-char := + unicode - unicode-space - newline - [\\/(){};\[\]"#=] + - disallowed-literal-code-points +disallowed-keyword-identifiers := + 'true' | 'false' | 'null' | 'inf' | '-inf' | 'nan' + +quoted-string := + '"' single-line-string-body '"' | + '"""' newline + (multi-line-string-body newline)? + (unicode-space | ws-escape)* '"""' +single-line-string-body := (string-character - newline)* +multi-line-string-body := (('"' | '""')? string-character)* +string-character := + '\\' (["\\bfnrts] | + 'u{' hex-unicode '}') | + ws-escape | + [^\\"] - disallowed-literal-code-points +ws-escape := '\\' (unicode-space | newline)+ +hex-digit := [0-9a-fA-F] +hex-unicode := hex-digit{1, 6} - surrogate - above-max-scalar +surrogate := [0]{0, 2} [dD] [8-9a-fA-F] hex-digit{2} +// U+D800-DFFF: D 8 00 +// D F FF +above-max-scalar = [2-9a-fA-F] hex-digit{5} | + [1] [1-9a-fA-F] hex-digit{4} + + +raw-string := '#' raw-string-quotes '#' | '#' raw-string '#' +raw-string-quotes := + '"' single-line-raw-string-body '"' | + '"""' newline + (multi-line-raw-string-body newline)? + unicode-space* '"""' +single-line-raw-string-body := + '' | + (single-line-raw-string-char - '"') + single-line-raw-string-char*? | + '"' (single-line-raw-string-char - '"') + single-line-raw-string-char*? +single-line-raw-string-char := + unicode - newline - disallowed-literal-code-points +multi-line-raw-string-body := + (unicode - disallowed-literal-code-points)*? + +// Numbers +number := keyword-number | hex | octal | binary | decimal + +decimal := sign? integer ('.' integer)? exponent? +exponent := ('e' | 'E') sign? integer +integer := digit (digit | '_')* +digit := [0-9] +sign := '+' | '-' + +hex := sign? '0x' hex-digit (hex-digit | '_')* +octal := sign? '0o' [0-7] [0-7_]* +binary := sign? '0b' ('0' | '1') ('0' | '1' | '_')* + +// Keywords and booleans. +keyword := boolean | '#null' +keyword-number := '#inf' | '#-inf' | '#nan' +boolean := '#true' | '#false' + +// Specific code points +bom := '\u{FEFF}' +disallowed-literal-code-points := + See Table (Disallowed Literal Code Points) +unicode := Any Unicode Scalar Value +unicode-space := See Table + (All White_Space unicode characters which are not `newline`) + +// Comments +single-line-comment := '//' ^newline* (newline | eof) +multi-line-comment := '/*' commented-block +commented-block := + '*/' | (multi-line-comment | '*' | '/' | [^*/]+) commented-block +slashdash := '/-' line-space* + +// Whitespace +ws := unicode-space | multi-line-comment +escline := '\\' ws* (single-line-comment | newline | eof) +newline := See Table (All Newline White_Space) +// Whitespace where newlines are allowed. +line-space := node-space | newline | single-line-comment +// Whitespace within nodes, +// where newline-ish things must be esclined. +node-space := ws* escline ws* | ws+ + +// Version marker +version := + '/-' unicode-space* 'kdl-version' unicode-space+ ('1' | '2') + unicode-space* newline +~~~ + +## Grammar language + +The grammar language syntax is a combination of ABNF with some regex spice thrown in. +Specifically: + +- Single quotes (`'`) are used to denote literal text. `\` within a literal + string is used for escaping other single-quotes, for initiating unicode + characters using hex values (`\u{FEFF}`), and for escaping `\` itself + (`\\`). +- `*` is used for "zero or more", `+` is used for "one or more", and `?` is + used for "zero or one". Per standard regex semantics, `*` and `+` are _greedy_; + they match as many instances as possible without failing the match. +- `*?` (used only in raw strings) indicates a _non-greedy_ match; + it matches as _few_ instances as possible without failing the match. +- `¶` is a _cut point_. It always matches and consumes no characters, + but once matched, the parser is not allowed to backtrack past that point in the source. + If a parser would rewind past the cut point, it must instead fail the overall parse, + as if it had run out of options. + (This is only used with the `raw-string` production, + to ensure the first instance of the appropriate closing quote sequence + is guaranteed to be the end of the raw string, + rather than allowing it to potentially consume more of the document unexpectedly.) +- `()` can be used to group matches that must be matched together. +- `a | b` means `a or b`, whichever matches first. If multiple items are before + a `|`, they are a single group. `a b c | d` is equivalent to `(a b c) | d`. +- `[]` are used for regex-style character matches, where any character between + the brackets will be a single match. `\` is used to escape `\`, `[`, and + `]`. They also support character ranges (`0-9`), and negation (`^`) +- `-` is used for "except for" or "minus" whatever follows it. For example, + `a - 'x'` means "any `a`, except something that matches the literal `'x'`". +- The prefix `^` means "something that does not match" whatever follows it. + For example, `^foo` means "must not match `foo`". +- A single definition may be split over multiple lines. Newlines are treated as + spaces. +- `//` followed by text on its own line is used as comment syntax. From 814f17e911d9ebe702174b131d039e9492121600 Mon Sep 17 00:00:00 2001 From: jamesshenry <79054685+jamesshenry@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:45:40 +0000 Subject: [PATCH 06/17] refactor KdlSerializer --- .../Serialization/ObjectMapperTests.cs | 3 +- .../Attributes/KdlChildrenAttribute.cs | 14 - src/Kuddle/Serialization/KdlSerializer.cs | 615 +++++++++--------- src/Kuddle/Serialization/KdlValueConverter.cs | 159 +++++ src/Kuddle/Serialization/TypeMetadata.cs | 122 ++++ 5 files changed, 574 insertions(+), 339 deletions(-) delete mode 100644 src/Kuddle/Serialization/Attributes/KdlChildrenAttribute.cs create mode 100644 src/Kuddle/Serialization/KdlValueConverter.cs create mode 100644 src/Kuddle/Serialization/TypeMetadata.cs diff --git a/src/Kuddle.Tests/Serialization/ObjectMapperTests.cs b/src/Kuddle.Tests/Serialization/ObjectMapperTests.cs index f208c54..718408c 100644 --- a/src/Kuddle.Tests/Serialization/ObjectMapperTests.cs +++ b/src/Kuddle.Tests/Serialization/ObjectMapperTests.cs @@ -1,4 +1,3 @@ -using System.Net.WebSockets; using Kuddle.AST; using Kuddle.Serialization; @@ -502,7 +501,7 @@ public async Task DeserializeToDictionary_ThrowsException() reference "my-dep1" version="2.1.0" node "my-dep2" version="3.2.1" """; - var result = KdlSerializer.Deserialize>(kdl); + // Act & Assert await Assert .That(() => KdlSerializer.Deserialize>(kdl)) diff --git a/src/Kuddle/Serialization/Attributes/KdlChildrenAttribute.cs b/src/Kuddle/Serialization/Attributes/KdlChildrenAttribute.cs deleted file mode 100644 index 602e12e..0000000 --- a/src/Kuddle/Serialization/Attributes/KdlChildrenAttribute.cs +++ /dev/null @@ -1,14 +0,0 @@ -// using System; - -// namespace Kuddle.Serialization; - -// /// -// /// Marks a property to receive child nodes of a specific type from the KDL node's children block. -// /// -// /// The KDL node name to match for children (e.g., "dependency"). -// [AttributeUsage(AttributeTargets.Property)] -// public class KdlChildrenAttribute(string childNodeName) : Attribute -// { -// /// Gets the child node name to match. -// public string ChildNodeName { get; } = childNodeName; -// } diff --git a/src/Kuddle/Serialization/KdlSerializer.cs b/src/Kuddle/Serialization/KdlSerializer.cs index 9701dbb..976799b 100644 --- a/src/Kuddle/Serialization/KdlSerializer.cs +++ b/src/Kuddle/Serialization/KdlSerializer.cs @@ -8,8 +8,16 @@ namespace Kuddle.Serialization; +/// +/// Serializes and deserializes C# objects to/from KDL format. +/// public static class KdlSerializer { + #region Deserialization + + /// + /// Deserializes a KDL document containing multiple nodes of type T. + /// public static IEnumerable DeserializeMany( string text, KdlSerializerOptions? options = null @@ -17,481 +25,442 @@ public static IEnumerable DeserializeMany( where T : new() { var doc = KdlReader.Read(text); - var list = new List(doc.Nodes.Count); - - var expectedName = GetExpectedNodeName(typeof(T)); + var metadata = TypeMetadata.For(); foreach (var node in doc.Nodes) { - if (expectedName != null && node.Name.Value != expectedName) + if (!node.Name.Value.Equals(metadata.NodeName, StringComparison.OrdinalIgnoreCase)) + { throw new KuddleSerializationException( - $"Expected node '{expectedName}', found '{node.Name.Value}'" + $"Expected node '{metadata.NodeName}', found '{node.Name.Value}'." ); + } var item = new T(); - MapToInstance(node, item); - list.Add(item); + MapNodeToObject(node, item, metadata); + yield return item; } - - return list; } - private static string? GetExpectedNodeName(Type type) + /// + /// Deserializes a KDL document to a single object of type T. + /// + public static T Deserialize(string text, KdlSerializerOptions? options = null) + where T : new() { - var kdlTypeAttr = type.GetCustomAttribute(); + var document = KdlReader.Read(text); + var metadata = TypeMetadata.For(); - if (kdlTypeAttr is null) + // Reject dictionary types + if (metadata.IsDictionary) { - return type.Name.ToLowerInvariant(); + throw new KuddleSerializationException( + $"Dictionary deserialization is not supported. Type: {metadata.Type.Name}" + ); } - else + + // Reject IEnumerable types (use DeserializeMany instead) + if (metadata.IsIEnumerable) { - return kdlTypeAttr.Name; + throw new KuddleSerializationException( + $"Cannot deserialize to collection type '{metadata.Type.Name}'. Use DeserializeMany() instead." + ); } - } - public static T Deserialize(string text, KdlSerializerOptions? options = null) - where T : new() - { - var document = KdlReader.Read(text); - var type = typeof(T); - - if (type.IsNodeDefinition) + if (metadata.IsNodeDefinition) { + // Type maps to a single KDL node if (document.Nodes.Count != 1) { throw new KuddleSerializationException( - "Kdl document must have a single root node to map to an instance T" + $"Expected exactly 1 root node for type '{typeof(T).Name}', found {document.Nodes.Count}." ); } var rootNode = document.Nodes[0]; - var expectedName = GetExpectedNodeName(type); - - if ( - expectedName != null - && !rootNode.Name.Value.Equals(expectedName, StringComparison.OrdinalIgnoreCase) - ) + if (!rootNode.Name.Value.Equals(metadata.NodeName, StringComparison.OrdinalIgnoreCase)) { throw new KuddleSerializationException( - $"Node name '{rootNode.Name.Value}' does not match target type name '{typeof(T).Name}'." + $"Expected node '{metadata.NodeName}', found '{rootNode.Name.Value}'." ); } + var instance = new T(); - MapToInstance(rootNode, instance); + MapNodeToObject(rootNode, instance, metadata); return instance; } else { + // Type maps to a document with child nodes as properties var instance = new T(); - MapNodesToProperties(document.Nodes, instance); + MapDocumentToObject(document.Nodes, instance, metadata); return instance; } } - private static void MapNodesToProperties(List nodes, T instance) + /// + /// Maps a KDL node's entries and children to an object instance. + /// + private static void MapNodeToObject(KdlNode node, object instance, TypeMetadata metadata) { - var props = instance! - .GetType() - .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(p => p.CanWrite && p.GetCustomAttribute() == null); - - foreach (var prop in props) + // Map arguments + foreach (var mapping in metadata.Arguments) { - var nodeName = prop.GetCustomAttribute()?.Name; - - if (nodeName == null) - continue; - - var matchingNodes = nodes - .Where(n => n.Name.Value.Equals(nodeName, StringComparison.OrdinalIgnoreCase)) - .ToList(); - - if (matchingNodes.Count == 0) - continue; - - if (prop.PropertyType.IsIEnumerable) + var argValue = node.Arg(mapping.Argument!.Index); + if (argValue is null) { - SetCollectionPropValue(prop, instance, matchingNodes); + throw new KuddleSerializationException( + $"Missing required argument at index {mapping.Argument.Index} for property '{mapping.Property.Name}'." + ); } - else - { - if (matchingNodes.Count > 1) - throw new KuddleSerializationException( - $"Property '{prop.Name}' expects a single node named '{nodeName}', but found {matchingNodes.Count}." - ); - SetSingleComplexPropValue(prop, instance, matchingNodes[0]); - } + SetPropertyValue(mapping.Property, instance, argValue); } - } - private static void SetSingleComplexPropValue(PropertyInfo prop, object instance, KdlNode node) - { - var type = prop.PropertyType; - if (!type.IsComplexType) + // Map properties + foreach (var mapping in metadata.Properties) { - throw new KuddleSerializationException( - $"Property '{prop.Name}' matches a Node, so it must be a complex class." - ); - } + var propKey = mapping.GetPropertyKey(); + var propValue = node.Prop(propKey); - var childInstance = - Activator.CreateInstance(type) ?? throw new Exception($"Could not create {type}"); + if (propValue is null) + { + continue; // Optional property, use default + } - MapToInstance(node, childInstance); + SetPropertyValue(mapping.Property, instance, propValue); + } - prop.SetValue(instance, childInstance); + // Map child nodes + MapChildNodes(node.Children?.Nodes, instance, metadata); } - private static void MapToInstance(KdlNode node, T instance) + /// + /// Maps document-level nodes to object properties (for non-node-definition types). + /// + private static void MapDocumentToObject( + List nodes, + object instance, + TypeMetadata metadata + ) { - var rootType = typeof(T); + MapChildNodes(nodes, instance, metadata); + } - var props = instance! - .GetType() - .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(p => p.CanWrite && p.GetCustomAttribute() == null); + /// + /// Maps child KDL nodes to properties marked with [KdlNode]. + /// + private static void MapChildNodes( + IReadOnlyList? nodes, + object instance, + TypeMetadata metadata + ) + { + if (nodes is null || nodes.Count == 0) + { + return; + } - foreach (var prop in props) + foreach (var mapping in metadata.Children) { - var argAttr = prop.GetCustomAttribute(); - if (argAttr is not null) - { - if (prop.PropertyType.IsComplexType) - throw new KuddleSerializationException(); + var nodeName = mapping.GetChildNodeName(); + var matchingNodes = nodes + .Where(n => n.Name.Value.Equals(nodeName, StringComparison.OrdinalIgnoreCase)) + .ToList(); - var targetArg = - node.Arg(argAttr.Index) - ?? throw new KuddleSerializationException( - $"Expected Kdl argument at index {argAttr.Index} but found nothing" - ); - SetPropValue(prop, instance, targetArg); + if (matchingNodes.Count == 0) + { continue; } - var propAttr = prop.GetCustomAttribute(); - if (propAttr is not null) - { - var propKey = propAttr.Key ?? prop.Name.ToLowerInvariant(); - var kdlProp = node.Prop(propKey); - - if (kdlProp is null) - continue; + var propType = mapping.Property.PropertyType; + var propMeta = TypeMetadata.For(propType); - SetPropValue(prop, instance, kdlProp); + if (propMeta.IsIEnumerable) + { + SetCollectionProperty(mapping.Property, instance, matchingNodes); } - - var childAttr = prop.GetCustomAttribute(); - if (childAttr is not null) + else if (propMeta.IsComplexType) { - // // Usually a collection - var nodeName = childAttr.Name ?? prop.Name.ToLowerInvariant(); - // var groupedChildNodes = node.Children?.Nodes.GroupBy(n => n.Name) ?? []; - // foreach (var childNode in groupedChildNodes) - // { - // if (childNode.Key.Value == nodeName) - // { - // SetCollectionPropValue(prop, instance, childNode); - // } - // } - - var matchingNodes = node - .Children?.Nodes.Where(n => - n.Name.Value.Equals(nodeName, StringComparison.OrdinalIgnoreCase) - ) - .ToList(); - - if (matchingNodes == null || matchingNodes.Count == 0) - continue; - - if (prop.PropertyType.IsIEnumerable) + if (matchingNodes.Count > 1) { - SetCollectionPropValue(prop, instance, matchingNodes); + throw new KuddleSerializationException( + $"Expected single node '{nodeName}' for property '{mapping.Property.Name}', found {matchingNodes.Count}." + ); } - // CASE B: Scalar (string, int, etc.) -> Map Arg(0) - else if (!prop.PropertyType.IsComplexType) + + SetComplexProperty(mapping.Property, instance, matchingNodes[0]); + } + else + { + // Scalar property - extract from first argument + if (matchingNodes.Count > 1) { - // Expect exactly one node - if (matchingNodes.Count > 1) - throw new KuddleSerializationException( - $"Expected single node '{nodeName}' for scalar property, found {matchingNodes.Count}" - ); - - // Extract Arg 0 - var val = matchingNodes[0].Arg(0); - SetPropValue(prop, instance, val!); + throw new KuddleSerializationException( + $"Expected single node '{nodeName}' for scalar property '{mapping.Property.Name}', found {matchingNodes.Count}." + ); } - // CASE C: Complex Object -> Recursion - else - { - if (matchingNodes.Count > 1) - throw new KuddleSerializationException( - $"Expected single node '{nodeName}' for object property, found {matchingNodes.Count}" - ); - SetSingleComplexPropValue(prop, instance, matchingNodes[0]); + var argValue = matchingNodes[0].Arg(0); + if (argValue is not null) + { + SetPropertyValue(mapping.Property, instance, argValue); } } } } - private static void SetCollectionPropValue( - PropertyInfo collectionProp, + private static void SetPropertyValue(PropertyInfo property, object instance, KdlValue kdlValue) + { + var result = KdlValueConverter.FromKdlOrThrow( + kdlValue, + property.PropertyType, + $"Property: {property.DeclaringType?.Name}.{property.Name}" + ); + + property.SetValue(instance, result); + } + + private static void SetComplexProperty(PropertyInfo property, object instance, KdlNode node) + { + var childInstance = + Activator.CreateInstance(property.PropertyType) + ?? throw new KuddleSerializationException( + $"Failed to create instance of '{property.PropertyType.Name}' for property '{property.Name}'." + ); + + var childMetadata = TypeMetadata.For(property.PropertyType); + MapNodeToObject(node, childInstance, childMetadata); + + property.SetValue(instance, childInstance); + } + + private static void SetCollectionProperty( + PropertyInfo property, object instance, - IEnumerable childNodeGroup + List nodes ) { - var type = collectionProp.PropertyType; - if (!type.IsIEnumerable) - { - throw new NotSupportedException( - $"{nameof(SetCollectionPropValue)} expects a collection type, but received a {type.Name}" - ); - } + var elementType = GetCollectionElementType(property.PropertyType); + var metadata = TypeMetadata.For(property.PropertyType); - var elementType = GetElementType(type); - if (!elementType.IsComplexType) + if (!metadata.IsComplexType) + { throw new KuddleSerializationException( - $"Collection element type '{elementType.Name}' must be a complex type" + $"Collection element type '{elementType.Name}' must be a complex type for property '{property.Name}'." ); + } var listType = typeof(List<>).MakeGenericType(elementType); var list = (IList)Activator.CreateInstance(listType)!; - foreach (var childNode in childNodeGroup) + var elementMetadata = TypeMetadata.For(elementType); + + foreach (var node in nodes) { var element = Activator.CreateInstance(elementType) ?? throw new KuddleSerializationException( - $"Failed to create instance of '{elementType.Name}'" + $"Failed to create instance of '{elementType.Name}'." ); - MapToInstance(childNode, element); - + MapNodeToObject(node, element, elementMetadata); list.Add(element); } - object finalValue = MapCollection(type, elementType, list); - - collectionProp.SetValue(instance, finalValue); + var finalValue = ConvertToTargetCollectionType(property.PropertyType, elementType, list); + property.SetValue(instance, finalValue); } - private static object MapCollection(Type targetType, Type elementType, IList list) - { - if (targetType.IsArray) - { - var array = Array.CreateInstance(elementType, list.Count); - list.CopyTo(array, 0); - return array; - } + #endregion - if (targetType.IsAssignableFrom(list.GetType())) - { - return list; - } - - return list; - } + #region Serialization - static Type GetElementType(Type collectionType) + /// + /// Serializes an object to a KDL string. + /// + public static string Serialize(T instance) { - return collectionType.IsArray ? collectionType.GetElementType()! - : collectionType.IsGenericType ? collectionType.GetGenericArguments()[0] - : throw new KuddleSerializationException( - $"Unsupported collection type '{collectionType.FullName}'" - ); - } + ArgumentNullException.ThrowIfNull(instance); - private static void SetPropValue(PropertyInfo prop, object instance, KdlValue kdlValue) - { - if (!TryConvertFromKdlValue(kdlValue, prop.PropertyType, out var result)) + var type = typeof(T); + var metadata = TypeMetadata.For(); + + if (!metadata.IsComplexType) { throw new KuddleSerializationException( - $"Cannot convert KDL value to property {prop.Name} ({prop.PropertyType})" + $"Cannot serialize primitive type '{type.Name}'. Only complex types are supported." ); } - prop.SetValue(instance, result); - } - - private static bool TryConvertFromKdlValue( - KdlValue kdlValue, - Type targetType, - out object? result - ) - { - result = null; - if (kdlValue is KdlNull) - { - if (!targetType.IsNullable()) - return false; - result = null; - return true; - } - - Type underlying = Nullable.GetUnderlyingType(targetType) ?? targetType; - - if (underlying == typeof(string) && kdlValue.TryGetString(out var stringVal)) - { - result = stringVal; - return true; - } - - if (underlying == typeof(int) && kdlValue.TryGetInt(out var intVal)) - { - result = intVal; - return true; - } - - if (underlying == typeof(double) && kdlValue.TryGetDouble(out var doubleVal)) - { - result = doubleVal; - return true; - } - - if (underlying == typeof(long) && kdlValue.TryGetLong(out var longVal)) - { - result = longVal; - return true; - } - - if (underlying == typeof(decimal) && kdlValue.TryGetDecimal(out var decimalVal)) - { - result = decimalVal; - return true; - } - - if (underlying == typeof(bool) && kdlValue.TryGetBool(out var boolVal)) - { - result = boolVal; - return true; - } + var doc = new KdlDocument(); - if (underlying == typeof(Guid) && kdlValue.TryGetUuid(out var uuid)) - { - result = uuid; - return true; - } - if (underlying == typeof(DateTimeOffset) && kdlValue.TryGetDateTime(out var datetime)) + if (metadata.IsDictionary) { - result = datetime; - return true; + throw new NotSupportedException("Dictionary serialization is not yet supported."); } - return false; - } - - public static string Serialize(T original) - { - var doc = new KdlDocument(); - var type = typeof(T); - if (!type.IsComplexType) - throw new KuddleSerializationException(); - if (type.IsDictionary) - throw new NotSupportedException(); - if (type.IsIEnumerable) + if (metadata.IsIEnumerable) { - var nodes = SerializeCollection(original as IEnumerable); + var nodes = SerializeCollection((IEnumerable)instance); doc.Nodes.AddRange(nodes); } else { - var node = SerializeNode(original); + var node = SerializeToNode(instance); doc.Nodes.Add(node); } return KdlWriter.Write(doc); } - private static KdlNode SerializeNode(object? instance, string? nodeName = null) + private static KdlNode SerializeToNode(object instance, string? overrideNodeName = null) { - var type = instance?.GetType() ?? throw new ArgumentNullException(); - nodeName ??= - type.GetCustomAttribute()?.Name ?? type.Name.ToLowerInvariant(); + var type = instance.GetType(); + var metadata = TypeMetadata.For(type); + var nodeName = overrideNodeName ?? metadata.NodeName; var entries = new List(); - foreach (var argProp in type.GetKdlArgProps()) + + // Serialize arguments (in order) + foreach (var mapping in metadata.Arguments) { - var value = argProp.Item1.GetValue(instance); + var value = mapping.Property.GetValue(instance); + var kdlValue = KdlValueConverter.ToKdlOrThrow( + value, + $"Argument property: {mapping.Property.Name}" + ); - if (!TryConvertToKdlValue(value, out var kdlValue)) - continue; + entries.Add(new KdlArgument(kdlValue)); + } - var arg = new KdlArgument(kdlValue); - entries.Add(arg); + // Serialize properties + foreach (var mapping in metadata.Properties) + { + var value = mapping.Property.GetValue(instance); + + if (!KdlValueConverter.TryToKdl(value, out var kdlValue)) + { + continue; // Skip properties that can't be converted + } + + var key = mapping.GetPropertyKey(); + entries.Add(new KdlProperty(KdlValue.From(key), kdlValue)); } - var propProps = type.GetKdlPropProps(); + // Serialize children + KdlBlock? childBlock = null; - foreach (var (prop, kdlAttr) in propProps) + foreach (var mapping in metadata.Children) { - var key = kdlAttr.Key ?? prop.Name.ToLowerInvariant(); - if (!TryConvertToKdlValue(prop.GetValue(instance), out var value)) + var propValue = mapping.Property.GetValue(instance); + + if (propValue is null) + { continue; + } - var kdlProp = new KdlProperty(KdlValue.From(key), value); - entries.Add(kdlProp); - } + childBlock ??= new KdlBlock(); + var childNodeName = mapping.GetChildNodeName(); - var childProps = type.GetKdlChildProps(); - var block = new KdlBlock(); - foreach (var (prop, childAttr) in childProps) - { - var childNodeName = childAttr.Name ?? prop.Name.ToLowerInvariant(); - if (prop.PropertyType.IsDictionary) - throw new NotSupportedException(); + var propType = mapping.Property.PropertyType; + var childMeta = TypeMetadata.For(propType); - if (prop.PropertyType.IsIEnumerable) + if (childMeta.IsIEnumerable) { - var col = prop.GetValue(instance); - var nodes = SerializeCollection(col as IEnumerable); - block.Nodes.AddRange(nodes); + var childNodes = SerializeCollection((IEnumerable)propValue, childNodeName); + childBlock.Nodes.AddRange(childNodes); + } + else if (childMeta.IsComplexType) + { + var childNode = SerializeToNode(propValue, childNodeName); + childBlock.Nodes.Add(childNode); + } + else + { + // Scalar value as a child node with single argument + var kdlValue = KdlValueConverter.ToKdlOrThrow( + propValue, + $"Child scalar property: {mapping.Property.Name}" + ); + + var scalarNode = new KdlNode(KdlValue.From(childNodeName)) + { + Entries = [new KdlArgument(kdlValue)], + }; + childBlock.Nodes.Add(scalarNode); } } + return new KdlNode(KdlValue.From(nodeName)) { Entries = entries, - Children = block.Nodes.Count > 0 ? block : null, + Children = childBlock?.Nodes.Count > 0 ? childBlock : null, }; } - private static bool TryConvertToKdlValue(object? input, out KdlValue kdlValue) + private static IEnumerable SerializeCollection( + IEnumerable collection, + string? overrideNodeName = null + ) { - kdlValue = KdlValue.Null; - if (input is null) - return true; + foreach (var item in collection) + { + if (item is null) + { + continue; + } + + yield return SerializeToNode(item, overrideNodeName); + } + } - var type = input.GetType(); - if (type.IsComplexType) - throw new KuddleSerializationException("Kdl arguments should be simple scalar values"); + #endregion - kdlValue = input switch + #region Helpers + + private static Type GetCollectionElementType(Type collectionType) + { + if (collectionType.IsArray) { - string s => KdlValue.From(s), - int i => KdlValue.From(i), - double d => KdlValue.From(d), - long l => KdlValue.From(l), - decimal m => KdlValue.From(m), - bool b => KdlValue.From(b), - Guid uuid => KdlValue.From(uuid), - DateTimeOffset dto => KdlValue.From(dto), - _ => null!, - }; - return kdlValue is not null; + return collectionType.GetElementType()!; + } + + if (collectionType.IsGenericType) + { + return collectionType.GetGenericArguments()[0]; + } + + throw new KuddleSerializationException( + $"Unsupported collection type '{collectionType.FullName}'." + ); } - private static IEnumerable SerializeCollection(IEnumerable? original) + private static object ConvertToTargetCollectionType( + Type targetType, + Type elementType, + IList list + ) { - var nodes = new List(); - foreach (var item in original!) + if (targetType.IsArray) + { + var array = Array.CreateInstance(elementType, list.Count); + list.CopyTo(array, 0); + return array; + } + + if (targetType.IsAssignableFrom(list.GetType())) { - var node = SerializeNode(item); - nodes.Add(node); + return list; } - return nodes; + return list; } + + #endregion } +/// +/// Options for KDL serialization (reserved for future use). +/// public record KdlSerializerOptions { } diff --git a/src/Kuddle/Serialization/KdlValueConverter.cs b/src/Kuddle/Serialization/KdlValueConverter.cs new file mode 100644 index 0000000..044efd6 --- /dev/null +++ b/src/Kuddle/Serialization/KdlValueConverter.cs @@ -0,0 +1,159 @@ +using System; +using Kuddle.AST; +using Kuddle.Extensions; + +namespace Kuddle.Serialization; + +/// +/// Provides unified conversion between CLR values and KDL values. +/// +internal static class KdlValueConverter +{ + /// + /// Attempts to convert a KDL value to a CLR type. + /// + public static bool TryFromKdl(KdlValue kdlValue, Type targetType, out object? result) + { + result = null; + + if (kdlValue is KdlNull) + { + if (!IsNullable(targetType)) + { + return false; + } + result = null; + return true; + } + + var underlying = Nullable.GetUnderlyingType(targetType) ?? targetType; + + // String + if (underlying == typeof(string) && kdlValue.TryGetString(out var stringVal)) + { + result = stringVal; + return true; + } + + // Integers + if (underlying == typeof(int) && kdlValue.TryGetInt(out var intVal)) + { + result = intVal; + return true; + } + + if (underlying == typeof(long) && kdlValue.TryGetLong(out var longVal)) + { + result = longVal; + return true; + } + + // Floating point + if (underlying == typeof(double) && kdlValue.TryGetDouble(out var doubleVal)) + { + result = doubleVal; + return true; + } + + if (underlying == typeof(decimal) && kdlValue.TryGetDecimal(out var decimalVal)) + { + result = decimalVal; + return true; + } + + if (underlying == typeof(float) && kdlValue.TryGetDouble(out var floatVal)) + { + result = (float)floatVal; + return true; + } + + // Boolean + if (underlying == typeof(bool) && kdlValue.TryGetBool(out var boolVal)) + { + result = boolVal; + return true; + } + + // Special types with type annotations + if (underlying == typeof(Guid) && kdlValue.TryGetUuid(out var uuid)) + { + result = uuid; + return true; + } + + if (underlying == typeof(DateTimeOffset) && kdlValue.TryGetDateTime(out var dto)) + { + result = dto; + return true; + } + + if (underlying == typeof(DateTime) && kdlValue.TryGetDateTime(out var dt)) + { + result = dt.DateTime; + return true; + } + + return false; + } + + /// + /// Attempts to convert a CLR value to a KDL value. + /// + public static bool TryToKdl(object? input, out KdlValue kdlValue) + { + if (input is null) + { + kdlValue = KdlValue.Null; + return true; + } + + kdlValue = input switch + { + string s => KdlValue.From(s), + int i => KdlValue.From(i), + long l => KdlValue.From(l), + double d => KdlValue.From(d), + float f => KdlValue.From((double)f), + decimal m => KdlValue.From(m), + bool b => KdlValue.From(b), + Guid uuid => KdlValue.From(uuid), + DateTimeOffset dto => KdlValue.From(dto), + DateTime dt => KdlValue.From(new DateTimeOffset(dt)), + _ => null!, + }; + + return kdlValue is not null; + } + + /// + /// Converts a KDL value to a CLR type, throwing on failure. + /// + public static object? FromKdlOrThrow(KdlValue kdlValue, Type targetType, string context) + { + if (!TryFromKdl(kdlValue, targetType, out var result)) + { + throw new KuddleSerializationException( + $"Cannot convert KDL value '{kdlValue}' to {targetType.Name}. {context}" + ); + } + return result; + } + + /// + /// Converts a CLR value to a KDL value, throwing on failure. + /// + public static KdlValue ToKdlOrThrow(object? input, string context) + { + if (!TryToKdl(input, out var kdlValue)) + { + var typeName = input?.GetType().Name ?? "null"; + throw new KuddleSerializationException( + $"Cannot convert CLR value of type '{typeName}' to KDL. {context}" + ); + } + return kdlValue; + } + + private static bool IsNullable(Type type) => + !type.IsValueType || Nullable.GetUnderlyingType(type) != null; +} diff --git a/src/Kuddle/Serialization/TypeMetadata.cs b/src/Kuddle/Serialization/TypeMetadata.cs new file mode 100644 index 0000000..2fc1c07 --- /dev/null +++ b/src/Kuddle/Serialization/TypeMetadata.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; + +namespace Kuddle.Serialization; + +/// +/// Represents a mapping from a C# property to a KDL entry (argument, property, or child node). +/// +internal sealed record PropertyMapping( + PropertyInfo Property, + KdlArgumentAttribute? Argument, + KdlPropertyAttribute? KdlProperty, + KdlNodeAttribute? ChildNode +) +{ + public string GetPropertyKey() => KdlProperty?.Key ?? Property.Name.ToLowerInvariant(); + + public string GetChildNodeName() => ChildNode?.Name ?? Property.Name.ToLowerInvariant(); +} + +/// +/// Cached metadata about how a CLR type maps to/from KDL nodes. +/// +/// +[DebuggerDisplay("Type = {Type.Name,nq}")] +internal sealed class TypeMetadata +{ + private static readonly ConcurrentDictionary s_cache = new(); + + public Type Type { get; } + public string NodeName { get; } + public bool IsNodeDefinition => Arguments.Count > 0 || Properties.Count > 0; + + /// Properties mapped to KDL arguments, sorted by index. + public IReadOnlyList Arguments { get; } + + /// Properties mapped to KDL properties. + public IReadOnlyList Properties { get; } + + /// Properties mapped to child nodes. + public IReadOnlyList Children { get; } + + /// All writable, non-ignored properties. + public IReadOnlyList AllMappings { get; } + + private TypeMetadata(Type type) + { + Type = type; + + // Determine node name from [KdlType] or fall back to type name + var kdlTypeAttr = type.GetCustomAttribute(); + NodeName = kdlTypeAttr?.Name ?? type.Name.ToLowerInvariant(); + + // Gather all writable, non-ignored properties + var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanWrite && p.GetCustomAttribute() == null) + .Select(p => new PropertyMapping( + p, + p.GetCustomAttribute(), + p.GetCustomAttribute(), + p.GetCustomAttribute() + )) + .ToList(); + + AllMappings = props; + + Arguments = props + .Where(m => m.Argument is not null) + .OrderBy(m => m.Argument!.Index) + .ToList(); + + Properties = props.Where(m => m.KdlProperty is not null).ToList(); + + Children = props.Where(m => m.ChildNode is not null).ToList(); + } + + /// + /// Gets or creates cached metadata for a type. + /// + public static TypeMetadata For(Type type) => s_cache.GetOrAdd(type, t => new TypeMetadata(t)); + + /// + /// Gets or creates cached metadata for a type. + /// + public static TypeMetadata For() => For(typeof(T)); + + /// + /// Checks if a type is a complex type (not primitive, not string, not interface/abstract). + /// + public bool IsComplexType => + !Type.IsValueType + && !Type.IsPrimitive + && Type != typeof(string) + && Type != typeof(object) + && !Type.IsInterface + && !Type.IsAbstract; + + /// + /// Checks if a type is enumerable (but not string or dictionary). + /// + public bool IsIEnumerable => + Type != typeof(string) && !IsDictionary && typeof(IEnumerable).IsAssignableFrom(Type); + + /// + /// Checks if a type is a dictionary. + /// + public bool IsDictionary => + Type.IsGenericType + && Type.GetInterfaces() + .Any(i => + i.IsGenericType + && ( + i.GetGenericTypeDefinition() == typeof(IDictionary<,>) + || i.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>) + ) + ); +} From fa3137d3a927874801f11370955bbb9911afc57b Mon Sep 17 00:00:00 2001 From: jamesshenry <79054685+jamesshenry@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:56:57 +0000 Subject: [PATCH 07/17] rename to Kuddle.Net --- .build/targets.cs | 2 +- AGENTS.md | 2 +- Kuddle.slnx | 6 +++--- .../Kuddle.Net.Benchmarks.csproj} | 4 +++- .../ParserBenchmarks.cs | 2 -- .../Program.cs | 0 .../AST/KdlNumberTests.cs | 0 .../AST/KdlStringTests.cs | 0 .../Errors/ErrorHandlingTests.cs | 0 src/{Kuddle.Tests => Kuddle.Net.Tests}/Errors/test.kdl | 0 .../Extensions/DxTests.cs | 0 .../Grammar/CommentParserTests.cs | 0 .../Grammar/NodeParserTests.cs | 0 .../Grammar/NumberParsersTests.cs | 0 .../Grammar/StringParserTests.cs | 0 .../Grammar/WhiteSpaceParsersTests.cs | 0 .../Kuddle.Net.Tests.csproj} | 4 +++- .../Serialization/DocumentToObjectTests.cs | 0 .../Serialization/KuddleParsingTests.cs | 0 .../Serialization/KuddleWriterTests.cs | 0 .../Serialization/NodeToObjectTests.cs | 0 .../Serialization/ObjectMapperTests.cs | 0 .../Validation/ReservedTypeValidatorTests.cs | 0 .../test_cases/expected_kdl/all_escapes.kdl | 0 .../test_cases/expected_kdl/all_node_fields.kdl | 0 .../test_cases/expected_kdl/arg_and_prop_same_name.kdl | 0 .../test_cases/expected_kdl/arg_bare.kdl | 0 .../test_cases/expected_kdl/arg_false_type.kdl | 0 .../test_cases/expected_kdl/arg_float_type.kdl | 0 .../test_cases/expected_kdl/arg_hex_type.kdl | 0 .../test_cases/expected_kdl/arg_null_type.kdl | 0 .../test_cases/expected_kdl/arg_raw_string_type.kdl | 0 .../test_cases/expected_kdl/arg_string_type.kdl | 0 .../test_cases/expected_kdl/arg_true_type.kdl | 0 .../test_cases/expected_kdl/arg_type.kdl | 0 .../test_cases/expected_kdl/arg_zero_type.kdl | 0 .../test_cases/expected_kdl/asterisk_in_block_comment.kdl | 0 .../test_cases/expected_kdl/bare_emoji.kdl | 0 .../test_cases/expected_kdl/bare_ident_dot.kdl | 0 .../test_cases/expected_kdl/bare_ident_sign.kdl | 0 .../test_cases/expected_kdl/bare_ident_sign_dot.kdl | 0 .../test_cases/expected_kdl/binary.kdl | 0 .../test_cases/expected_kdl/binary_trailing_underscore.kdl | 0 .../test_cases/expected_kdl/binary_underscore.kdl | 0 .../test_cases/expected_kdl/blank_arg_type.kdl | 0 .../test_cases/expected_kdl/blank_node_type.kdl | 0 .../test_cases/expected_kdl/blank_prop_type.kdl | 0 .../test_cases/expected_kdl/block_comment.kdl | 0 .../test_cases/expected_kdl/block_comment_after_node.kdl | 0 .../test_cases/expected_kdl/block_comment_before_node.kdl | 0 .../expected_kdl/block_comment_before_node_no_space.kdl | 0 .../test_cases/expected_kdl/block_comment_newline.kdl | 0 .../test_cases/expected_kdl/bom_initial.kdl | 0 .../test_cases/expected_kdl/boolean_arg.kdl | 0 .../test_cases/expected_kdl/boolean_prop.kdl | 0 .../test_cases/expected_kdl/braces_in_bare_id.kdl | 0 .../test_cases/expected_kdl/chevrons_in_bare_id.kdl | 0 .../test_cases/expected_kdl/comma_in_bare_id.kdl | 0 .../test_cases/expected_kdl/comment_after_arg_type.kdl | 0 .../test_cases/expected_kdl/comment_after_node_type.kdl | 0 .../test_cases/expected_kdl/comment_after_prop_type.kdl | 0 .../test_cases/expected_kdl/comment_and_newline.kdl | 0 .../test_cases/expected_kdl/comment_in_arg_type.kdl | 0 .../test_cases/expected_kdl/comment_in_node_type.kdl | 0 .../test_cases/expected_kdl/comment_in_prop_type.kdl | 0 .../test_cases/expected_kdl/commented_arg.kdl | 0 .../test_cases/expected_kdl/commented_child.kdl | 0 .../test_cases/expected_kdl/commented_line.kdl | 0 .../test_cases/expected_kdl/commented_node.kdl | 0 .../test_cases/expected_kdl/commented_prop.kdl | 0 .../test_cases/expected_kdl/crlf_between_nodes.kdl | 0 .../test_cases/expected_kdl/dash_dash.kdl | 0 .../test_cases/expected_kdl/emoji.kdl | 0 .../test_cases/expected_kdl/empty.kdl | 0 .../test_cases/expected_kdl/empty_child.kdl | 0 .../expected_kdl/empty_child_different_lines.kdl | 0 .../test_cases/expected_kdl/empty_child_same_line.kdl | 0 .../test_cases/expected_kdl/empty_child_whitespace.kdl | 0 .../test_cases/expected_kdl/empty_line_comment.kdl | 0 .../test_cases/expected_kdl/empty_quoted_node_id.kdl | 0 .../test_cases/expected_kdl/empty_quoted_prop_key.kdl | 0 .../test_cases/expected_kdl/empty_string_arg.kdl | 0 .../test_cases/expected_kdl/eof_after_escape.kdl | 0 .../test_cases/expected_kdl/esc_multiple_newlines.kdl | 0 .../test_cases/expected_kdl/esc_newline_in_string.kdl | 0 .../test_cases/expected_kdl/esc_unicode_in_string.kdl | 0 .../test_cases/expected_kdl/escaped_whitespace.kdl | 0 .../test_cases/expected_kdl/escline.kdl | 0 .../test_cases/expected_kdl/escline_after_semicolon.kdl | 0 .../test_cases/expected_kdl/escline_alone.kdl | 0 .../test_cases/expected_kdl/escline_empty_line.kdl | 0 .../test_cases/expected_kdl/escline_end_of_node.kdl | 0 .../test_cases/expected_kdl/escline_in_child_block.kdl | 0 .../test_cases/expected_kdl/escline_line_comment.kdl | 0 .../test_cases/expected_kdl/escline_node.kdl | 0 .../test_cases/expected_kdl/escline_node_type.kdl | 0 .../test_cases/expected_kdl/escline_slashdash.kdl | 0 .../test_cases/expected_kdl/false_prefix_in_bare_id.kdl | 0 .../test_cases/expected_kdl/false_prefix_in_prop_key.kdl | 0 .../test_cases/expected_kdl/floating_point_keywords.kdl | 0 .../test_cases/expected_kdl/hex.kdl | 0 .../test_cases/expected_kdl/hex_int.kdl | 0 .../test_cases/expected_kdl/hex_int_underscores.kdl | 0 .../test_cases/expected_kdl/hex_leading_zero.kdl | 0 .../test_cases/expected_kdl/initial_slashdash.kdl | 0 .../test_cases/expected_kdl/int_multiple_underscore.kdl | 0 .../test_cases/expected_kdl/just_block_comment.kdl | 0 .../test_cases/expected_kdl/just_child.kdl | 0 .../test_cases/expected_kdl/just_newline.kdl | 0 .../test_cases/expected_kdl/just_node_id.kdl | 0 .../test_cases/expected_kdl/just_space.kdl | 0 .../test_cases/expected_kdl/leading_newline.kdl | 0 .../test_cases/expected_kdl/leading_zero_binary.kdl | 0 .../test_cases/expected_kdl/leading_zero_int.kdl | 0 .../test_cases/expected_kdl/leading_zero_oct.kdl | 0 .../test_cases/expected_kdl/multiline_comment.kdl | 0 .../test_cases/expected_kdl/multiline_nodes.kdl | 0 .../test_cases/expected_kdl/multiline_raw_string.kdl | 0 .../multiline_raw_string_containing_quotes.kdl | 0 .../test_cases/expected_kdl/multiline_raw_string_empty.kdl | 0 .../expected_kdl/multiline_raw_string_empty_indented.kdl | 0 .../expected_kdl/multiline_raw_string_indented.kdl | 0 .../test_cases/expected_kdl/multiline_string.kdl | 0 .../expected_kdl/multiline_string_containing_quotes.kdl | 0 .../expected_kdl/multiline_string_double_backslash.kdl | 0 .../test_cases/expected_kdl/multiline_string_empty.kdl | 0 .../expected_kdl/multiline_string_empty_indented.kdl | 0 .../expected_kdl/multiline_string_escape_delimiter.kdl | 0 .../multiline_string_escape_in_closing_line.kdl | 0 .../multiline_string_escape_in_closing_line_shallow.kdl | 0 .../multiline_string_escape_newline_at_end.kdl | 0 .../test_cases/expected_kdl/multiline_string_indented.kdl | 0 .../expected_kdl/multiline_string_whitespace_only.kdl | 0 .../expected_kdl/multiline_string_wrapped_binary.kdl | 0 .../test_cases/expected_kdl/negative_exponent.kdl | 0 .../test_cases/expected_kdl/negative_float.kdl | 0 .../test_cases/expected_kdl/negative_int.kdl | 0 .../test_cases/expected_kdl/nested_block_comment.kdl | 0 .../test_cases/expected_kdl/nested_children.kdl | 0 .../test_cases/expected_kdl/nested_comments.kdl | 0 .../expected_kdl/nested_multiline_block_comment.kdl | 0 .../test_cases/expected_kdl/newline_between_nodes.kdl | 0 .../test_cases/expected_kdl/newlines_in_block_comment.kdl | 0 .../test_cases/expected_kdl/no_decimal_exponent.kdl | 0 .../test_cases/expected_kdl/node_false.kdl | 0 .../test_cases/expected_kdl/node_true.kdl | 0 .../test_cases/expected_kdl/node_type.kdl | 0 .../test_cases/expected_kdl/null_arg.kdl | 0 .../test_cases/expected_kdl/null_prefix_in_bare_id.kdl | 0 .../test_cases/expected_kdl/null_prefix_in_prop_key.kdl | 0 .../test_cases/expected_kdl/null_prop.kdl | 0 .../test_cases/expected_kdl/numeric_arg.kdl | 0 .../test_cases/expected_kdl/numeric_prop.kdl | 0 .../test_cases/expected_kdl/octal.kdl | 0 .../test_cases/expected_kdl/only_cr.kdl | 0 .../test_cases/expected_kdl/only_line_comment.kdl | 0 .../test_cases/expected_kdl/only_line_comment_crlf.kdl | 0 .../test_cases/expected_kdl/only_line_comment_newline.kdl | 0 .../test_cases/expected_kdl/optional_child_semicolon.kdl | 0 .../test_cases/expected_kdl/parse_all_arg_types.kdl | 0 .../test_cases/expected_kdl/positive_exponent.kdl | 0 .../test_cases/expected_kdl/positive_int.kdl | 0 .../test_cases/expected_kdl/preserve_duplicate_nodes.kdl | 0 .../test_cases/expected_kdl/preserve_node_order.kdl | 0 .../test_cases/expected_kdl/prop_false_type.kdl | 0 .../test_cases/expected_kdl/prop_float_type.kdl | 0 .../test_cases/expected_kdl/prop_hex_type.kdl | 0 .../test_cases/expected_kdl/prop_identifier_type.kdl | 0 .../test_cases/expected_kdl/prop_null_type.kdl | 0 .../test_cases/expected_kdl/prop_raw_string_type.kdl | 0 .../test_cases/expected_kdl/prop_string_type.kdl | 0 .../test_cases/expected_kdl/prop_true_type.kdl | 0 .../test_cases/expected_kdl/prop_type.kdl | 0 .../test_cases/expected_kdl/prop_zero_type.kdl | 0 .../expected_kdl/question_mark_before_number.kdl | 0 .../test_cases/expected_kdl/quoted_arg_type.kdl | 0 .../test_cases/expected_kdl/quoted_node_name.kdl | 0 .../test_cases/expected_kdl/quoted_node_type.kdl | 0 .../test_cases/expected_kdl/quoted_numeric.kdl | 0 .../test_cases/expected_kdl/quoted_prop_name.kdl | 0 .../test_cases/expected_kdl/quoted_prop_type.kdl | 0 .../test_cases/expected_kdl/r_node.kdl | 0 .../test_cases/expected_kdl/raw_arg_type.kdl | 0 .../test_cases/expected_kdl/raw_node_name.kdl | 0 .../test_cases/expected_kdl/raw_node_type.kdl | 0 .../test_cases/expected_kdl/raw_prop_type.kdl | 0 .../test_cases/expected_kdl/raw_string_arg.kdl | 0 .../test_cases/expected_kdl/raw_string_backslash.kdl | 0 .../test_cases/expected_kdl/raw_string_hash_no_esc.kdl | 0 .../test_cases/expected_kdl/raw_string_just_backslash.kdl | 0 .../test_cases/expected_kdl/raw_string_multiple_hash.kdl | 0 .../test_cases/expected_kdl/raw_string_newline.kdl | 0 .../test_cases/expected_kdl/raw_string_prop.kdl | 0 .../test_cases/expected_kdl/raw_string_quote.kdl | 0 .../test_cases/expected_kdl/repeated_arg.kdl | 0 .../test_cases/expected_kdl/repeated_prop.kdl | 0 .../test_cases/expected_kdl/same_name_nodes.kdl | 0 .../test_cases/expected_kdl/sci_notation_large.kdl | 0 .../test_cases/expected_kdl/sci_notation_small.kdl | 0 .../test_cases/expected_kdl/semicolon_after_child.kdl | 0 .../test_cases/expected_kdl/semicolon_in_child.kdl | 0 .../test_cases/expected_kdl/semicolon_separated.kdl | 0 .../test_cases/expected_kdl/semicolon_separated_nodes.kdl | 0 .../test_cases/expected_kdl/semicolon_terminated.kdl | 0 .../test_cases/expected_kdl/single_arg.kdl | 0 .../test_cases/expected_kdl/single_prop.kdl | 0 .../expected_kdl/slashdash_arg_after_newline_esc.kdl | 0 .../expected_kdl/slashdash_arg_before_newline_esc.kdl | 0 .../test_cases/expected_kdl/slashdash_child.kdl | 0 .../test_cases/expected_kdl/slashdash_empty_child.kdl | 0 .../expected_kdl/slashdash_escline_before_arg_type.kdl | 0 .../expected_kdl/slashdash_escline_before_children.kdl | 0 .../expected_kdl/slashdash_escline_before_node.kdl | 0 .../test_cases/expected_kdl/slashdash_false_node.kdl | 0 .../test_cases/expected_kdl/slashdash_full_node.kdl | 0 .../test_cases/expected_kdl/slashdash_in_slashdash.kdl | 0 .../expected_kdl/slashdash_multi_line_comment_entry.kdl | 0 .../expected_kdl/slashdash_multi_line_comment_inline.kdl | 0 .../expected_kdl/slashdash_multiple_child_blocks.kdl | 0 .../test_cases/expected_kdl/slashdash_negative_number.kdl | 0 .../expected_kdl/slashdash_newline_before_children.kdl | 0 .../expected_kdl/slashdash_newline_before_entry.kdl | 0 .../expected_kdl/slashdash_newline_before_node.kdl | 0 .../test_cases/expected_kdl/slashdash_node_in_child.kdl | 0 .../test_cases/expected_kdl/slashdash_node_with_child.kdl | 0 .../test_cases/expected_kdl/slashdash_only_node.kdl | 0 .../expected_kdl/slashdash_only_node_with_space.kdl | 0 .../test_cases/expected_kdl/slashdash_prop.kdl | 0 .../test_cases/expected_kdl/slashdash_raw_prop_key.kdl | 0 .../test_cases/expected_kdl/slashdash_repeated_prop.kdl | 0 .../expected_kdl/slashdash_single_line_comment_entry.kdl | 0 .../expected_kdl/slashdash_single_line_comment_node.kdl | 0 .../test_cases/expected_kdl/space_after_arg_type.kdl | 0 .../test_cases/expected_kdl/space_after_node_type.kdl | 0 .../test_cases/expected_kdl/space_after_prop_type.kdl | 0 .../test_cases/expected_kdl/space_around_prop_marker.kdl | 0 .../test_cases/expected_kdl/space_in_arg_type.kdl | 0 .../test_cases/expected_kdl/space_in_node_type.kdl | 0 .../test_cases/expected_kdl/space_in_prop_type.kdl | 0 .../test_cases/expected_kdl/string_arg.kdl | 0 .../expected_kdl/string_escaped_literal_whitespace.kdl | 0 .../test_cases/expected_kdl/string_prop.kdl | 0 .../test_cases/expected_kdl/tab_space.kdl | 0 .../test_cases/expected_kdl/trailing_crlf.kdl | 0 .../test_cases/expected_kdl/trailing_underscore_hex.kdl | 0 .../test_cases/expected_kdl/trailing_underscore_octal.kdl | 0 .../test_cases/expected_kdl/true_prefix_in_bare_id.kdl | 0 .../test_cases/expected_kdl/true_prefix_in_prop_key.kdl | 0 .../test_cases/expected_kdl/two_nodes.kdl | 0 .../test_cases/expected_kdl/underscore_before_number.kdl | 0 .../test_cases/expected_kdl/underscore_in_exponent.kdl | 0 .../test_cases/expected_kdl/underscore_in_float.kdl | 0 .../test_cases/expected_kdl/underscore_in_fraction.kdl | 0 .../test_cases/expected_kdl/underscore_in_int.kdl | 0 .../test_cases/expected_kdl/underscore_in_octal.kdl | 0 .../test_cases/expected_kdl/unicode_silly.kdl | 0 .../expected_kdl/unusual_bare_id_chars_in_quoted_id.kdl | 0 .../test_cases/expected_kdl/unusual_chars_in_bare_id.kdl | 0 .../test_cases/expected_kdl/vertical_tab_whitespace.kdl | 0 .../test_cases/expected_kdl/zero_float.kdl | 0 .../test_cases/expected_kdl/zero_int.kdl | 0 .../expected_kdl/zero_space_before_slashdash_arg.kdl | 0 .../expected_kdl/zero_space_before_slashdash_children.kdl | 0 .../expected_kdl/zero_space_before_slashdash_prop.kdl | 0 .../test_cases/input/all_escapes.kdl | 0 .../test_cases/input/all_node_fields.kdl | 0 .../test_cases/input/arg_and_prop_same_name.kdl | 0 .../test_cases/input/arg_bare.kdl | 0 .../test_cases/input/arg_false_type.kdl | 0 .../test_cases/input/arg_float_type.kdl | 0 .../test_cases/input/arg_hex_type.kdl | 0 .../test_cases/input/arg_null_type.kdl | 0 .../test_cases/input/arg_raw_string_type.kdl | 0 .../test_cases/input/arg_string_type.kdl | 0 .../test_cases/input/arg_true_type.kdl | 0 .../test_cases/input/arg_type.kdl | 0 .../test_cases/input/arg_zero_type.kdl | 0 .../test_cases/input/asterisk_in_block_comment.kdl | 0 .../test_cases/input/bare_emoji.kdl | 0 .../test_cases/input/bare_ident_dot.kdl | 0 .../test_cases/input/bare_ident_numeric_dot_fail.kdl | 0 .../test_cases/input/bare_ident_numeric_fail.kdl | 0 .../test_cases/input/bare_ident_numeric_sign_fail.kdl | 0 .../test_cases/input/bare_ident_sign.kdl | 0 .../test_cases/input/bare_ident_sign_dot.kdl | 0 .../test_cases/input/binary.kdl | 0 .../test_cases/input/binary_trailing_underscore.kdl | 0 .../test_cases/input/binary_underscore.kdl | 0 .../test_cases/input/blank_arg_type.kdl | 0 .../test_cases/input/blank_node_type.kdl | 0 .../test_cases/input/blank_prop_type.kdl | 0 .../test_cases/input/block_comment.kdl | 0 .../test_cases/input/block_comment_after_node.kdl | 0 .../test_cases/input/block_comment_before_node.kdl | 0 .../input/block_comment_before_node_no_space.kdl | 0 .../test_cases/input/block_comment_newline.kdl | 0 .../test_cases/input/bom_initial.kdl | 0 .../test_cases/input/bom_later_fail.kdl | 0 .../test_cases/input/boolean_arg.kdl | 0 .../test_cases/input/boolean_prop.kdl | 0 .../test_cases/input/braces_in_bare_id.kdl | 0 .../test_cases/input/chevrons_in_bare_id.kdl | 0 .../test_cases/input/comma_in_bare_id.kdl | 0 .../test_cases/input/comment_after_arg_type.kdl | 0 .../test_cases/input/comment_after_node_type.kdl | 0 .../test_cases/input/comment_after_prop_type.kdl | 0 .../test_cases/input/comment_and_newline.kdl | 0 .../test_cases/input/comment_in_arg_type.kdl | 0 .../test_cases/input/comment_in_node_type.kdl | 0 .../test_cases/input/comment_in_prop_type.kdl | 0 .../test_cases/input/commented_arg.kdl | 0 .../test_cases/input/commented_child.kdl | 0 .../test_cases/input/commented_line.kdl | 0 .../test_cases/input/commented_node.kdl | 0 .../test_cases/input/commented_prop.kdl | 0 .../test_cases/input/crlf_between_nodes.kdl | 0 .../test_cases/input/dash_dash.kdl | 0 .../input/dot_but_no_fraction_before_exponent_fail.kdl | 0 .../test_cases/input/dot_but_no_fraction_fail.kdl | 0 .../test_cases/input/dot_in_exponent_fail.kdl | 0 .../test_cases/input/dot_zero_fail.kdl | 0 .../test_cases/input/emoji.kdl | 0 .../test_cases/input/empty.kdl | 0 .../test_cases/input/empty_arg_type_fail.kdl | 0 .../test_cases/input/empty_child.kdl | 0 .../test_cases/input/empty_child_different_lines.kdl | 0 .../test_cases/input/empty_child_same_line.kdl | 0 .../test_cases/input/empty_child_whitespace.kdl | 0 .../test_cases/input/empty_line_comment.kdl | 0 .../test_cases/input/empty_node_type_fail.kdl | 0 .../test_cases/input/empty_prop_type_fail.kdl | 0 .../test_cases/input/empty_quoted_node_id.kdl | 0 .../test_cases/input/empty_quoted_prop_key.kdl | 0 .../test_cases/input/empty_string_arg.kdl | 0 .../test_cases/input/eof_after_escape.kdl | 0 .../test_cases/input/err_backslash_in_bare_id_fail.kdl | 0 .../test_cases/input/esc_multiple_newlines.kdl | 0 .../test_cases/input/esc_newline_in_string.kdl | 0 .../test_cases/input/esc_unicode_in_string.kdl | 0 .../test_cases/input/escaped_whitespace.kdl | 0 .../test_cases/input/escline.kdl | 0 .../test_cases/input/escline_after_semicolon.kdl | 0 .../test_cases/input/escline_alone.kdl | 0 .../test_cases/input/escline_empty_line.kdl | 0 .../test_cases/input/escline_end_of_node.kdl | 0 .../test_cases/input/escline_in_child_block.kdl | 0 .../test_cases/input/escline_line_comment.kdl | 0 .../test_cases/input/escline_node.kdl | 0 .../test_cases/input/escline_node_type.kdl | 0 .../test_cases/input/escline_slashdash.kdl | 0 .../test_cases/input/false_prefix_in_bare_id.kdl | 0 .../test_cases/input/false_prefix_in_prop_key.kdl | 0 .../test_cases/input/false_prop_key_fail.kdl | 0 .../floating_point_keyword_identifier_strings_fail.kdl | 0 .../test_cases/input/floating_point_keywords.kdl | 0 .../test_cases/input/hash_in_id_fail.kdl | 0 .../test_cases/input/hex.kdl | 0 .../test_cases/input/hex_int.kdl | 0 .../test_cases/input/hex_int_underscores.kdl | 0 .../test_cases/input/hex_leading_zero.kdl | 0 .../test_cases/input/illegal_char_in_binary_fail.kdl | 0 .../test_cases/input/illegal_char_in_hex_fail.kdl | 0 .../test_cases/input/illegal_char_in_octal_fail.kdl | 0 .../test_cases/input/initial_slashdash.kdl | 0 .../test_cases/input/int_multiple_underscore.kdl | 0 .../test_cases/input/just_block_comment.kdl | 0 .../test_cases/input/just_child.kdl | 0 .../test_cases/input/just_newline.kdl | 0 .../test_cases/input/just_node_id.kdl | 0 .../test_cases/input/just_space.kdl | 0 .../test_cases/input/just_space_in_arg_type_fail.kdl | 0 .../test_cases/input/just_space_in_node_type_fail.kdl | 0 .../test_cases/input/just_space_in_prop_type_fail.kdl | 0 .../test_cases/input/just_type_no_arg_fail.kdl | 0 .../test_cases/input/just_type_no_node_id_fail.kdl | 0 .../test_cases/input/just_type_no_prop_fail.kdl | 0 .../test_cases/input/leading_newline.kdl | 0 .../test_cases/input/leading_zero_binary.kdl | 0 .../test_cases/input/leading_zero_int.kdl | 0 .../test_cases/input/leading_zero_oct.kdl | 0 .../test_cases/input/legacy_raw_string_fail.kdl | 0 .../test_cases/input/legacy_raw_string_hash_fail.kdl | 0 .../test_cases/input/multiline_comment.kdl | 0 .../test_cases/input/multiline_nodes.kdl | 0 .../test_cases/input/multiline_raw_string.kdl | 0 .../input/multiline_raw_string_containing_quotes.kdl | 0 .../test_cases/input/multiline_raw_string_empty.kdl | 0 .../input/multiline_raw_string_empty_indented.kdl | 0 .../test_cases/input/multiline_raw_string_indented.kdl | 0 ...raw_string_non_matching_prefix_character_error_fail.kdl | 0 ...ine_raw_string_non_matching_prefix_count_error_fail.kdl | 0 .../input/multiline_raw_string_single_line_err_fail.kdl | 0 .../input/multiline_raw_string_single_quote_err_fail.kdl | 0 .../test_cases/input/multiline_string.kdl | 0 .../input/multiline_string_containing_quotes.kdl | 0 .../test_cases/input/multiline_string_double_backslash.kdl | 0 .../test_cases/input/multiline_string_empty.kdl | 0 .../test_cases/input/multiline_string_empty_indented.kdl | 0 .../test_cases/input/multiline_string_escape_delimiter.kdl | 0 .../input/multiline_string_escape_in_closing_line.kdl | 0 .../multiline_string_escape_in_closing_line_shallow.kdl | 0 .../input/multiline_string_escape_newline_at_end.kdl | 0 .../input/multiline_string_escape_newline_at_end_fail.kdl | 0 .../multiline_string_final_whitespace_escape_fail.kdl | 0 .../test_cases/input/multiline_string_indented.kdl | 0 .../input/multiline_string_non_literal_prefix_fail.kdl | 0 ...ine_string_non_matching_prefix_character_error_fail.kdl | 0 ...ltiline_string_non_matching_prefix_count_error_fail.kdl | 0 .../input/multiline_string_single_line_err_fail.kdl | 0 .../input/multiline_string_single_quote_err_fail.kdl | 0 .../test_cases/input/multiline_string_whitespace_only.kdl | 0 .../test_cases/input/multiline_string_wrapped_binary.kdl | 0 .../input/multiple_dots_in_float_before_exponent_fail.kdl | 0 .../test_cases/input/multiple_dots_in_float_fail.kdl | 0 .../test_cases/input/multiple_es_in_float_fail.kdl | 0 .../test_cases/input/multiple_x_in_hex_fail.kdl | 0 .../test_cases/input/negative_exponent.kdl | 0 .../test_cases/input/negative_float.kdl | 0 .../test_cases/input/negative_int.kdl | 0 .../test_cases/input/nested_block_comment.kdl | 0 .../test_cases/input/nested_children.kdl | 0 .../test_cases/input/nested_comments.kdl | 0 .../test_cases/input/nested_multiline_block_comment.kdl | 0 .../test_cases/input/newline_between_nodes.kdl | 0 .../test_cases/input/newlines_in_block_comment.kdl | 0 .../test_cases/input/no_decimal_exponent.kdl | 0 .../test_cases/input/no_digits_in_hex_fail.kdl | 0 .../test_cases/input/no_integer_digit_fail.kdl | 0 .../test_cases/input/no_solidus_escape_fail.kdl | 0 .../test_cases/input/node_false.kdl | 0 .../test_cases/input/node_true.kdl | 0 .../test_cases/input/node_type.kdl | 0 .../test_cases/input/null_arg.kdl | 0 .../test_cases/input/null_prefix_in_bare_id.kdl | 0 .../test_cases/input/null_prefix_in_prop_key.kdl | 0 .../test_cases/input/null_prop.kdl | 0 .../test_cases/input/null_prop_key_fail.kdl | 0 .../test_cases/input/numeric_arg.kdl | 0 .../test_cases/input/numeric_prop.kdl | 0 .../test_cases/input/octal.kdl | 0 .../test_cases/input/only_cr.kdl | 0 .../test_cases/input/only_line_comment.kdl | 0 .../test_cases/input/only_line_comment_crlf.kdl | 0 .../test_cases/input/only_line_comment_newline.kdl | 0 .../test_cases/input/optional_child_semicolon.kdl | 0 .../test_cases/input/parens_in_bare_id_fail.kdl | 0 .../test_cases/input/parse_all_arg_types.kdl | 0 .../test_cases/input/positive_exponent.kdl | 0 .../test_cases/input/positive_int.kdl | 0 .../test_cases/input/preserve_duplicate_nodes.kdl | 0 .../test_cases/input/preserve_node_order.kdl | 0 .../test_cases/input/prop_false_type.kdl | 0 .../test_cases/input/prop_float_type.kdl | 0 .../test_cases/input/prop_hex_type.kdl | 0 .../test_cases/input/prop_identifier_type.kdl | 0 .../test_cases/input/prop_null_type.kdl | 0 .../test_cases/input/prop_raw_string_type.kdl | 0 .../test_cases/input/prop_string_type.kdl | 0 .../test_cases/input/prop_true_type.kdl | 0 .../test_cases/input/prop_type.kdl | 0 .../test_cases/input/prop_zero_type.kdl | 0 .../test_cases/input/question_mark_before_number.kdl | 0 .../test_cases/input/quote_in_bare_id_fail.kdl | 0 .../test_cases/input/quoted_arg_type.kdl | 0 .../test_cases/input/quoted_node_name.kdl | 0 .../test_cases/input/quoted_node_type.kdl | 0 .../test_cases/input/quoted_numeric.kdl | 0 .../test_cases/input/quoted_prop_name.kdl | 0 .../test_cases/input/quoted_prop_type.kdl | 0 .../test_cases/input/r_node.kdl | 0 .../test_cases/input/raw_arg_type.kdl | 0 .../test_cases/input/raw_node_name.kdl | 0 .../test_cases/input/raw_node_type.kdl | 0 .../test_cases/input/raw_prop_type.kdl | 0 .../test_cases/input/raw_string_arg.kdl | 0 .../test_cases/input/raw_string_backslash.kdl | 0 .../test_cases/input/raw_string_hash_no_esc.kdl | 0 .../test_cases/input/raw_string_just_backslash.kdl | 0 .../test_cases/input/raw_string_just_quote_fail.kdl | 0 .../test_cases/input/raw_string_multiple_hash.kdl | 0 .../test_cases/input/raw_string_newline.kdl | 0 .../test_cases/input/raw_string_prop.kdl | 0 .../test_cases/input/raw_string_quote.kdl | 0 .../test_cases/input/repeated_arg.kdl | 0 .../test_cases/input/repeated_prop.kdl | 0 .../test_cases/input/same_name_nodes.kdl | 0 .../test_cases/input/sci_notation_large.kdl | 0 .../test_cases/input/sci_notation_small.kdl | 0 .../test_cases/input/semicolon_after_child.kdl | 0 .../test_cases/input/semicolon_in_child.kdl | 0 .../input/semicolon_missing_after_children_fail.kdl | 0 .../test_cases/input/semicolon_separated.kdl | 0 .../test_cases/input/semicolon_separated_nodes.kdl | 0 .../test_cases/input/semicolon_terminated.kdl | 0 .../test_cases/input/single_arg.kdl | 0 .../test_cases/input/single_prop.kdl | 0 .../test_cases/input/slash_in_bare_id_fail.kdl | 0 .../test_cases/input/slashdash_after_arg_type_fail.kdl | 0 .../test_cases/input/slashdash_after_node_type_fail.kdl | 0 .../test_cases/input/slashdash_after_prop_key_fail.kdl | 0 .../input/slashdash_after_prop_val_type_fail.kdl | 0 .../test_cases/input/slashdash_after_type_fail.kdl | 0 .../test_cases/input/slashdash_arg_after_newline_esc.kdl | 0 .../test_cases/input/slashdash_arg_before_newline_esc.kdl | 0 .../input/slashdash_before_children_end_fail.kdl | 0 .../test_cases/input/slashdash_before_eof_fail.kdl | 0 .../test_cases/input/slashdash_before_prop_value_fail.kdl | 0 .../test_cases/input/slashdash_before_semicolon_fail.kdl | 0 .../input/slashdash_between_child_blocks_fail.kdl | 0 .../test_cases/input/slashdash_child.kdl | 0 .../input/slashdash_child_block_before_entry_err_fail.kdl | 0 .../test_cases/input/slashdash_empty_child.kdl | 0 .../test_cases/input/slashdash_escline_before_arg_type.kdl | 0 .../test_cases/input/slashdash_escline_before_children.kdl | 0 .../test_cases/input/slashdash_escline_before_node.kdl | 0 .../test_cases/input/slashdash_false_node.kdl | 0 .../test_cases/input/slashdash_full_node.kdl | 0 .../test_cases/input/slashdash_in_slashdash.kdl | 0 .../test_cases/input/slashdash_inside_arg_type_fail.kdl | 0 .../test_cases/input/slashdash_inside_node_type_fail.kdl | 0 .../input/slashdash_multi_line_comment_entry.kdl | 0 .../input/slashdash_multi_line_comment_inline.kdl | 0 .../test_cases/input/slashdash_multiple_child_blocks.kdl | 0 .../test_cases/input/slashdash_negative_number.kdl | 0 .../test_cases/input/slashdash_newline_before_children.kdl | 0 .../test_cases/input/slashdash_newline_before_entry.kdl | 0 .../test_cases/input/slashdash_newline_before_node.kdl | 0 .../test_cases/input/slashdash_node_in_child.kdl | 0 .../test_cases/input/slashdash_node_with_child.kdl | 0 .../test_cases/input/slashdash_only_node.kdl | 0 .../test_cases/input/slashdash_only_node_with_space.kdl | 0 .../test_cases/input/slashdash_prop.kdl | 0 .../test_cases/input/slashdash_raw_prop_key.kdl | 0 .../test_cases/input/slashdash_repeated_prop.kdl | 0 .../input/slashdash_single_line_comment_entry.kdl | 0 .../input/slashdash_single_line_comment_node.kdl | 0 .../test_cases/input/space_after_arg_type.kdl | 0 .../test_cases/input/space_after_node_type.kdl | 0 .../test_cases/input/space_after_prop_type.kdl | 0 .../test_cases/input/space_around_prop_marker.kdl | 0 .../test_cases/input/space_in_arg_type.kdl | 0 .../test_cases/input/space_in_node_type.kdl | 0 .../test_cases/input/space_in_prop_type.kdl | 0 .../test_cases/input/square_bracket_in_bare_id_fail.kdl | 0 .../test_cases/input/string_arg.kdl | 0 .../test_cases/input/string_escaped_literal_whitespace.kdl | 0 .../test_cases/input/string_prop.kdl | 0 .../test_cases/input/tab_space.kdl | 0 .../test_cases/input/trailing_crlf.kdl | 0 .../test_cases/input/trailing_underscore_hex.kdl | 0 .../test_cases/input/trailing_underscore_octal.kdl | 0 .../test_cases/input/true_prefix_in_bare_id.kdl | 0 .../test_cases/input/true_prefix_in_prop_key.kdl | 0 .../test_cases/input/true_prop_key_fail.kdl | 0 .../test_cases/input/two_nodes.kdl | 0 .../test_cases/input/type_before_prop_key_fail.kdl | 0 .../test_cases/input/unbalanced_raw_hashes_fail.kdl | 0 .../input/underscore_at_start_of_fraction_fail.kdl | 0 .../test_cases/input/underscore_at_start_of_hex_fail.kdl | 0 .../test_cases/input/underscore_before_number.kdl | 0 .../test_cases/input/underscore_in_exponent.kdl | 0 .../test_cases/input/underscore_in_float.kdl | 0 .../test_cases/input/underscore_in_fraction.kdl | 0 .../test_cases/input/underscore_in_int.kdl | 0 .../test_cases/input/underscore_in_octal.kdl | 0 .../test_cases/input/unicode_delete_fail.kdl | 0 .../test_cases/input/unicode_escaped_above_max_fail.kdl | 0 .../test_cases/input/unicode_escaped_h1_fail.kdl | 0 .../test_cases/input/unicode_escaped_h2_fail.kdl | 0 .../test_cases/input/unicode_escaped_h3_fail.kdl | 0 .../test_cases/input/unicode_escaped_h4_fail.kdl | 0 .../test_cases/input/unicode_escaped_l1_fail.kdl | 0 .../test_cases/input/unicode_escaped_l2_fail.kdl | 0 .../test_cases/input/unicode_escaped_l3_fail.kdl | 0 .../input/unicode_escaped_too_long_lead0_fail.kdl | 0 .../test_cases/input/unicode_fsi_fail.kdl | 0 .../test_cases/input/unicode_lre_fail.kdl | 0 .../test_cases/input/unicode_lri_fail.kdl | 0 .../test_cases/input/unicode_lrm_fail.kdl | 0 .../test_cases/input/unicode_lro_fail.kdl | 0 .../test_cases/input/unicode_pdf_fail.kdl | 0 .../test_cases/input/unicode_pdi_fail.kdl | 0 .../test_cases/input/unicode_rle_fail.kdl | 0 .../test_cases/input/unicode_rli_fail.kdl | 0 .../test_cases/input/unicode_rlm_fail.kdl | 0 .../test_cases/input/unicode_rlo_fail.kdl | 0 .../test_cases/input/unicode_silly.kdl | 0 .../test_cases/input/unicode_under_0x20_fail.kdl | 0 .../test_cases/input/unterminated_empty_node_fail.kdl | 0 .../input/unusual_bare_id_chars_in_quoted_id.kdl | 0 .../test_cases/input/unusual_chars_in_bare_id.kdl | 0 .../test_cases/input/vertical_tab_whitespace.kdl | 0 .../test_cases/input/zero_float.kdl | 0 .../test_cases/input/zero_int.kdl | 0 .../test_cases/input/zero_space_before_first_arg_fail.kdl | 0 .../test_cases/input/zero_space_before_prop_fail.kdl | 0 .../test_cases/input/zero_space_before_second_arg_fail.kdl | 0 .../test_cases/input/zero_space_before_slashdash_arg.kdl | 0 .../input/zero_space_before_slashdash_children.kdl | 0 .../test_cases/input/zero_space_before_slashdash_prop.kdl | 0 src/{Kuddle => Kuddle.Net}/AST/KdlArgument.cs | 0 src/{Kuddle => Kuddle.Net}/AST/KdlBlock.cs | 0 src/{Kuddle => Kuddle.Net}/AST/KdlBool.cs | 0 src/{Kuddle => Kuddle.Net}/AST/KdlDocument.cs | 0 src/{Kuddle => Kuddle.Net}/AST/KdlEntry.cs | 0 src/{Kuddle => Kuddle.Net}/AST/KdlNode.cs | 0 src/{Kuddle => Kuddle.Net}/AST/KdlNull.cs | 0 src/{Kuddle => Kuddle.Net}/AST/KdlNumber.cs | 0 src/{Kuddle => Kuddle.Net}/AST/KdlObject.cs | 0 src/{Kuddle => Kuddle.Net}/AST/KdlProperty.cs | 0 src/{Kuddle => Kuddle.Net}/AST/KdlSkippedEntry.cs | 0 src/{Kuddle => Kuddle.Net}/AST/KdlString.cs | 0 src/{Kuddle => Kuddle.Net}/AST/KdlTrivia.cs | 0 src/{Kuddle => Kuddle.Net}/AST/KdlValue.cs | 0 src/{Kuddle => Kuddle.Net}/AST/NumberBase.cs | 0 src/{Kuddle => Kuddle.Net}/AST/StringKind.cs | 0 .../Exceptions/KuddleParseException.cs | 0 .../Exceptions/KuddleSerializationException.cs | 0 .../Exceptions/KuddleValidationException.cs | 0 src/{Kuddle => Kuddle.Net}/Extensions/KdlNodeExtensions.cs | 0 .../Extensions/KdlValueExtensions.cs | 0 src/{Kuddle => Kuddle.Net}/Extensions/ParserExtensions.cs | 0 src/{Kuddle => Kuddle.Net}/Extensions/SpanExtensions.cs | 0 src/{Kuddle => Kuddle.Net}/Extensions/TypeExtensions.cs | 0 src/{Kuddle => Kuddle.Net}/GlobalSuppressions.cs | 0 src/{Kuddle/Kuddle.csproj => Kuddle.Net/Kuddle.Net.csproj} | 7 +++++-- src/{Kuddle => Kuddle.Net}/Parser/CharacterSets.cs | 0 src/{Kuddle => Kuddle.Net}/Parser/DebugParser.cs | 0 src/{Kuddle => Kuddle.Net}/Parser/KdlGrammar.cs | 0 src/{Kuddle => Kuddle.Net}/Parser/MultiLineStringParser.cs | 0 src/{Kuddle => Kuddle.Net}/Parser/RawStringParser.cs | 0 .../Serialization/Attributes/KdlArgumentAttribute.cs | 0 .../Serialization/Attributes/KdlIgnoreAttribute.cs | 0 .../Serialization/Attributes/KdlNodeAttribute.cs | 0 .../Serialization/Attributes/KdlPropertyAttribute.cs | 0 src/{Kuddle => Kuddle.Net}/Serialization/KdlReader.cs | 0 .../Serialization/KdlReaderOptions.cs | 0 src/{Kuddle => Kuddle.Net}/Serialization/KdlSerializer.cs | 0 .../Serialization/KdlValueConverter.cs | 0 src/{Kuddle => Kuddle.Net}/Serialization/KdlWriter.cs | 0 .../Serialization/KdlWriterOptions.cs | 0 src/{Kuddle => Kuddle.Net}/Serialization/TypeMetadata.cs | 0 .../Validation/KuddleReservedTypeValidator.cs | 0 643 files changed, 16 insertions(+), 11 deletions(-) rename src/{Kuddle.Benchmarks/Kuddle.Benchmarks.csproj => Kuddle.Net.Benchmarks/Kuddle.Net.Benchmarks.csproj} (72%) rename src/{Kuddle.Benchmarks => Kuddle.Net.Benchmarks}/ParserBenchmarks.cs (98%) rename src/{Kuddle.Benchmarks => Kuddle.Net.Benchmarks}/Program.cs (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/AST/KdlNumberTests.cs (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/AST/KdlStringTests.cs (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/Errors/ErrorHandlingTests.cs (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/Errors/test.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/Extensions/DxTests.cs (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/Grammar/CommentParserTests.cs (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/Grammar/NodeParserTests.cs (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/Grammar/NumberParsersTests.cs (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/Grammar/StringParserTests.cs (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/Grammar/WhiteSpaceParsersTests.cs (100%) rename src/{Kuddle.Tests/Kuddle.Tests.csproj => Kuddle.Net.Tests/Kuddle.Net.Tests.csproj} (78%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/Serialization/DocumentToObjectTests.cs (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/Serialization/KuddleParsingTests.cs (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/Serialization/KuddleWriterTests.cs (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/Serialization/NodeToObjectTests.cs (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/Serialization/ObjectMapperTests.cs (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/Validation/ReservedTypeValidatorTests.cs (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/all_escapes.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/all_node_fields.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/arg_and_prop_same_name.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/arg_bare.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/arg_false_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/arg_float_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/arg_hex_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/arg_null_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/arg_raw_string_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/arg_string_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/arg_true_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/arg_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/arg_zero_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/asterisk_in_block_comment.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/bare_emoji.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/bare_ident_dot.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/bare_ident_sign.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/bare_ident_sign_dot.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/binary.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/binary_trailing_underscore.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/binary_underscore.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/blank_arg_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/blank_node_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/blank_prop_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/block_comment.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/block_comment_after_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/block_comment_before_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/block_comment_before_node_no_space.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/block_comment_newline.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/bom_initial.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/boolean_arg.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/boolean_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/braces_in_bare_id.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/chevrons_in_bare_id.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/comma_in_bare_id.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/comment_after_arg_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/comment_after_node_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/comment_after_prop_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/comment_and_newline.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/comment_in_arg_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/comment_in_node_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/comment_in_prop_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/commented_arg.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/commented_child.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/commented_line.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/commented_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/commented_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/crlf_between_nodes.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/dash_dash.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/emoji.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/empty.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/empty_child.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/empty_child_different_lines.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/empty_child_same_line.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/empty_child_whitespace.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/empty_line_comment.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/empty_quoted_node_id.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/empty_quoted_prop_key.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/empty_string_arg.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/eof_after_escape.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/esc_multiple_newlines.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/esc_newline_in_string.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/esc_unicode_in_string.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/escaped_whitespace.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/escline.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/escline_after_semicolon.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/escline_alone.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/escline_empty_line.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/escline_end_of_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/escline_in_child_block.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/escline_line_comment.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/escline_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/escline_node_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/escline_slashdash.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/false_prefix_in_bare_id.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/false_prefix_in_prop_key.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/floating_point_keywords.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/hex.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/hex_int.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/hex_int_underscores.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/hex_leading_zero.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/initial_slashdash.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/int_multiple_underscore.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/just_block_comment.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/just_child.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/just_newline.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/just_node_id.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/just_space.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/leading_newline.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/leading_zero_binary.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/leading_zero_int.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/leading_zero_oct.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/multiline_comment.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/multiline_nodes.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/multiline_raw_string.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/multiline_raw_string_containing_quotes.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/multiline_raw_string_empty.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/multiline_raw_string_empty_indented.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/multiline_raw_string_indented.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/multiline_string.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/multiline_string_containing_quotes.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/multiline_string_double_backslash.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/multiline_string_empty.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/multiline_string_empty_indented.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/multiline_string_escape_delimiter.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/multiline_string_escape_in_closing_line.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/multiline_string_escape_in_closing_line_shallow.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/multiline_string_escape_newline_at_end.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/multiline_string_indented.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/multiline_string_whitespace_only.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/multiline_string_wrapped_binary.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/negative_exponent.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/negative_float.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/negative_int.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/nested_block_comment.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/nested_children.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/nested_comments.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/nested_multiline_block_comment.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/newline_between_nodes.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/newlines_in_block_comment.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/no_decimal_exponent.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/node_false.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/node_true.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/node_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/null_arg.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/null_prefix_in_bare_id.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/null_prefix_in_prop_key.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/null_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/numeric_arg.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/numeric_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/octal.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/only_cr.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/only_line_comment.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/only_line_comment_crlf.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/only_line_comment_newline.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/optional_child_semicolon.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/parse_all_arg_types.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/positive_exponent.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/positive_int.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/preserve_duplicate_nodes.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/preserve_node_order.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/prop_false_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/prop_float_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/prop_hex_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/prop_identifier_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/prop_null_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/prop_raw_string_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/prop_string_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/prop_true_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/prop_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/prop_zero_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/question_mark_before_number.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/quoted_arg_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/quoted_node_name.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/quoted_node_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/quoted_numeric.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/quoted_prop_name.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/quoted_prop_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/r_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/raw_arg_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/raw_node_name.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/raw_node_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/raw_prop_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/raw_string_arg.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/raw_string_backslash.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/raw_string_hash_no_esc.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/raw_string_just_backslash.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/raw_string_multiple_hash.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/raw_string_newline.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/raw_string_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/raw_string_quote.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/repeated_arg.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/repeated_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/same_name_nodes.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/sci_notation_large.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/sci_notation_small.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/semicolon_after_child.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/semicolon_in_child.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/semicolon_separated.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/semicolon_separated_nodes.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/semicolon_terminated.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/single_arg.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/single_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_arg_after_newline_esc.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_arg_before_newline_esc.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_child.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_empty_child.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_escline_before_arg_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_escline_before_children.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_escline_before_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_false_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_full_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_in_slashdash.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_multi_line_comment_entry.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_multi_line_comment_inline.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_multiple_child_blocks.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_negative_number.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_newline_before_children.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_newline_before_entry.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_newline_before_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_node_in_child.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_node_with_child.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_only_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_only_node_with_space.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_raw_prop_key.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_repeated_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_single_line_comment_entry.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/slashdash_single_line_comment_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/space_after_arg_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/space_after_node_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/space_after_prop_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/space_around_prop_marker.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/space_in_arg_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/space_in_node_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/space_in_prop_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/string_arg.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/string_escaped_literal_whitespace.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/string_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/tab_space.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/trailing_crlf.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/trailing_underscore_hex.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/trailing_underscore_octal.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/true_prefix_in_bare_id.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/true_prefix_in_prop_key.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/two_nodes.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/underscore_before_number.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/underscore_in_exponent.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/underscore_in_float.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/underscore_in_fraction.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/underscore_in_int.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/underscore_in_octal.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/unicode_silly.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/unusual_bare_id_chars_in_quoted_id.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/unusual_chars_in_bare_id.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/vertical_tab_whitespace.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/zero_float.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/zero_int.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/zero_space_before_slashdash_arg.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/zero_space_before_slashdash_children.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/expected_kdl/zero_space_before_slashdash_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/all_escapes.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/all_node_fields.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/arg_and_prop_same_name.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/arg_bare.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/arg_false_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/arg_float_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/arg_hex_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/arg_null_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/arg_raw_string_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/arg_string_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/arg_true_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/arg_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/arg_zero_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/asterisk_in_block_comment.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/bare_emoji.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/bare_ident_dot.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/bare_ident_numeric_dot_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/bare_ident_numeric_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/bare_ident_numeric_sign_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/bare_ident_sign.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/bare_ident_sign_dot.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/binary.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/binary_trailing_underscore.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/binary_underscore.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/blank_arg_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/blank_node_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/blank_prop_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/block_comment.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/block_comment_after_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/block_comment_before_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/block_comment_before_node_no_space.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/block_comment_newline.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/bom_initial.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/bom_later_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/boolean_arg.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/boolean_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/braces_in_bare_id.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/chevrons_in_bare_id.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/comma_in_bare_id.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/comment_after_arg_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/comment_after_node_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/comment_after_prop_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/comment_and_newline.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/comment_in_arg_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/comment_in_node_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/comment_in_prop_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/commented_arg.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/commented_child.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/commented_line.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/commented_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/commented_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/crlf_between_nodes.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/dash_dash.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/dot_but_no_fraction_before_exponent_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/dot_but_no_fraction_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/dot_in_exponent_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/dot_zero_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/emoji.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/empty.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/empty_arg_type_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/empty_child.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/empty_child_different_lines.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/empty_child_same_line.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/empty_child_whitespace.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/empty_line_comment.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/empty_node_type_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/empty_prop_type_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/empty_quoted_node_id.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/empty_quoted_prop_key.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/empty_string_arg.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/eof_after_escape.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/err_backslash_in_bare_id_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/esc_multiple_newlines.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/esc_newline_in_string.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/esc_unicode_in_string.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/escaped_whitespace.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/escline.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/escline_after_semicolon.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/escline_alone.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/escline_empty_line.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/escline_end_of_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/escline_in_child_block.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/escline_line_comment.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/escline_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/escline_node_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/escline_slashdash.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/false_prefix_in_bare_id.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/false_prefix_in_prop_key.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/false_prop_key_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/floating_point_keyword_identifier_strings_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/floating_point_keywords.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/hash_in_id_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/hex.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/hex_int.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/hex_int_underscores.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/hex_leading_zero.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/illegal_char_in_binary_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/illegal_char_in_hex_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/illegal_char_in_octal_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/initial_slashdash.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/int_multiple_underscore.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/just_block_comment.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/just_child.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/just_newline.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/just_node_id.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/just_space.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/just_space_in_arg_type_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/just_space_in_node_type_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/just_space_in_prop_type_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/just_type_no_arg_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/just_type_no_node_id_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/just_type_no_prop_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/leading_newline.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/leading_zero_binary.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/leading_zero_int.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/leading_zero_oct.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/legacy_raw_string_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/legacy_raw_string_hash_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_comment.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_nodes.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_raw_string.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_raw_string_containing_quotes.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_raw_string_empty.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_raw_string_empty_indented.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_raw_string_indented.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_raw_string_non_matching_prefix_character_error_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_raw_string_non_matching_prefix_count_error_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_raw_string_single_line_err_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_raw_string_single_quote_err_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_string.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_string_containing_quotes.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_string_double_backslash.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_string_empty.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_string_empty_indented.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_string_escape_delimiter.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_string_escape_in_closing_line.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_string_escape_in_closing_line_shallow.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_string_escape_newline_at_end.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_string_escape_newline_at_end_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_string_final_whitespace_escape_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_string_indented.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_string_non_literal_prefix_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_string_non_matching_prefix_character_error_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_string_non_matching_prefix_count_error_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_string_single_line_err_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_string_single_quote_err_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_string_whitespace_only.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiline_string_wrapped_binary.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiple_dots_in_float_before_exponent_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiple_dots_in_float_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiple_es_in_float_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/multiple_x_in_hex_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/negative_exponent.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/negative_float.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/negative_int.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/nested_block_comment.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/nested_children.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/nested_comments.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/nested_multiline_block_comment.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/newline_between_nodes.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/newlines_in_block_comment.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/no_decimal_exponent.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/no_digits_in_hex_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/no_integer_digit_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/no_solidus_escape_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/node_false.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/node_true.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/node_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/null_arg.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/null_prefix_in_bare_id.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/null_prefix_in_prop_key.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/null_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/null_prop_key_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/numeric_arg.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/numeric_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/octal.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/only_cr.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/only_line_comment.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/only_line_comment_crlf.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/only_line_comment_newline.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/optional_child_semicolon.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/parens_in_bare_id_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/parse_all_arg_types.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/positive_exponent.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/positive_int.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/preserve_duplicate_nodes.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/preserve_node_order.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/prop_false_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/prop_float_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/prop_hex_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/prop_identifier_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/prop_null_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/prop_raw_string_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/prop_string_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/prop_true_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/prop_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/prop_zero_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/question_mark_before_number.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/quote_in_bare_id_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/quoted_arg_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/quoted_node_name.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/quoted_node_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/quoted_numeric.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/quoted_prop_name.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/quoted_prop_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/r_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/raw_arg_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/raw_node_name.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/raw_node_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/raw_prop_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/raw_string_arg.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/raw_string_backslash.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/raw_string_hash_no_esc.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/raw_string_just_backslash.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/raw_string_just_quote_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/raw_string_multiple_hash.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/raw_string_newline.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/raw_string_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/raw_string_quote.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/repeated_arg.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/repeated_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/same_name_nodes.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/sci_notation_large.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/sci_notation_small.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/semicolon_after_child.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/semicolon_in_child.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/semicolon_missing_after_children_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/semicolon_separated.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/semicolon_separated_nodes.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/semicolon_terminated.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/single_arg.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/single_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slash_in_bare_id_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_after_arg_type_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_after_node_type_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_after_prop_key_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_after_prop_val_type_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_after_type_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_arg_after_newline_esc.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_arg_before_newline_esc.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_before_children_end_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_before_eof_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_before_prop_value_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_before_semicolon_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_between_child_blocks_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_child.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_child_block_before_entry_err_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_empty_child.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_escline_before_arg_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_escline_before_children.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_escline_before_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_false_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_full_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_in_slashdash.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_inside_arg_type_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_inside_node_type_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_multi_line_comment_entry.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_multi_line_comment_inline.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_multiple_child_blocks.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_negative_number.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_newline_before_children.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_newline_before_entry.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_newline_before_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_node_in_child.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_node_with_child.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_only_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_only_node_with_space.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_raw_prop_key.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_repeated_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_single_line_comment_entry.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/slashdash_single_line_comment_node.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/space_after_arg_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/space_after_node_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/space_after_prop_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/space_around_prop_marker.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/space_in_arg_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/space_in_node_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/space_in_prop_type.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/square_bracket_in_bare_id_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/string_arg.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/string_escaped_literal_whitespace.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/string_prop.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/tab_space.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/trailing_crlf.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/trailing_underscore_hex.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/trailing_underscore_octal.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/true_prefix_in_bare_id.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/true_prefix_in_prop_key.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/true_prop_key_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/two_nodes.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/type_before_prop_key_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unbalanced_raw_hashes_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/underscore_at_start_of_fraction_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/underscore_at_start_of_hex_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/underscore_before_number.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/underscore_in_exponent.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/underscore_in_float.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/underscore_in_fraction.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/underscore_in_int.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/underscore_in_octal.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_delete_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_escaped_above_max_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_escaped_h1_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_escaped_h2_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_escaped_h3_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_escaped_h4_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_escaped_l1_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_escaped_l2_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_escaped_l3_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_escaped_too_long_lead0_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_fsi_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_lre_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_lri_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_lrm_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_lro_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_pdf_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_pdi_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_rle_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_rli_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_rlm_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_rlo_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_silly.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unicode_under_0x20_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unterminated_empty_node_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unusual_bare_id_chars_in_quoted_id.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/unusual_chars_in_bare_id.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/vertical_tab_whitespace.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/zero_float.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/zero_int.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/zero_space_before_first_arg_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/zero_space_before_prop_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/zero_space_before_second_arg_fail.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/zero_space_before_slashdash_arg.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/zero_space_before_slashdash_children.kdl (100%) rename src/{Kuddle.Tests => Kuddle.Net.Tests}/test_cases/input/zero_space_before_slashdash_prop.kdl (100%) rename src/{Kuddle => Kuddle.Net}/AST/KdlArgument.cs (100%) rename src/{Kuddle => Kuddle.Net}/AST/KdlBlock.cs (100%) rename src/{Kuddle => Kuddle.Net}/AST/KdlBool.cs (100%) rename src/{Kuddle => Kuddle.Net}/AST/KdlDocument.cs (100%) rename src/{Kuddle => Kuddle.Net}/AST/KdlEntry.cs (100%) rename src/{Kuddle => Kuddle.Net}/AST/KdlNode.cs (100%) rename src/{Kuddle => Kuddle.Net}/AST/KdlNull.cs (100%) rename src/{Kuddle => Kuddle.Net}/AST/KdlNumber.cs (100%) rename src/{Kuddle => Kuddle.Net}/AST/KdlObject.cs (100%) rename src/{Kuddle => Kuddle.Net}/AST/KdlProperty.cs (100%) rename src/{Kuddle => Kuddle.Net}/AST/KdlSkippedEntry.cs (100%) rename src/{Kuddle => Kuddle.Net}/AST/KdlString.cs (100%) rename src/{Kuddle => Kuddle.Net}/AST/KdlTrivia.cs (100%) rename src/{Kuddle => Kuddle.Net}/AST/KdlValue.cs (100%) rename src/{Kuddle => Kuddle.Net}/AST/NumberBase.cs (100%) rename src/{Kuddle => Kuddle.Net}/AST/StringKind.cs (100%) rename src/{Kuddle => Kuddle.Net}/Exceptions/KuddleParseException.cs (100%) rename src/{Kuddle => Kuddle.Net}/Exceptions/KuddleSerializationException.cs (100%) rename src/{Kuddle => Kuddle.Net}/Exceptions/KuddleValidationException.cs (100%) rename src/{Kuddle => Kuddle.Net}/Extensions/KdlNodeExtensions.cs (100%) rename src/{Kuddle => Kuddle.Net}/Extensions/KdlValueExtensions.cs (100%) rename src/{Kuddle => Kuddle.Net}/Extensions/ParserExtensions.cs (100%) rename src/{Kuddle => Kuddle.Net}/Extensions/SpanExtensions.cs (100%) rename src/{Kuddle => Kuddle.Net}/Extensions/TypeExtensions.cs (100%) rename src/{Kuddle => Kuddle.Net}/GlobalSuppressions.cs (100%) rename src/{Kuddle/Kuddle.csproj => Kuddle.Net/Kuddle.Net.csproj} (74%) rename src/{Kuddle => Kuddle.Net}/Parser/CharacterSets.cs (100%) rename src/{Kuddle => Kuddle.Net}/Parser/DebugParser.cs (100%) rename src/{Kuddle => Kuddle.Net}/Parser/KdlGrammar.cs (100%) rename src/{Kuddle => Kuddle.Net}/Parser/MultiLineStringParser.cs (100%) rename src/{Kuddle => Kuddle.Net}/Parser/RawStringParser.cs (100%) rename src/{Kuddle => Kuddle.Net}/Serialization/Attributes/KdlArgumentAttribute.cs (100%) rename src/{Kuddle => Kuddle.Net}/Serialization/Attributes/KdlIgnoreAttribute.cs (100%) rename src/{Kuddle => Kuddle.Net}/Serialization/Attributes/KdlNodeAttribute.cs (100%) rename src/{Kuddle => Kuddle.Net}/Serialization/Attributes/KdlPropertyAttribute.cs (100%) rename src/{Kuddle => Kuddle.Net}/Serialization/KdlReader.cs (100%) rename src/{Kuddle => Kuddle.Net}/Serialization/KdlReaderOptions.cs (100%) rename src/{Kuddle => Kuddle.Net}/Serialization/KdlSerializer.cs (100%) rename src/{Kuddle => Kuddle.Net}/Serialization/KdlValueConverter.cs (100%) rename src/{Kuddle => Kuddle.Net}/Serialization/KdlWriter.cs (100%) rename src/{Kuddle => Kuddle.Net}/Serialization/KdlWriterOptions.cs (100%) rename src/{Kuddle => Kuddle.Net}/Serialization/TypeMetadata.cs (100%) rename src/{Kuddle => Kuddle.Net}/Validation/KuddleReservedTypeValidator.cs (100%) diff --git a/.build/targets.cs b/.build/targets.cs index 1d9a4dd..653fa34 100644 --- a/.build/targets.cs +++ b/.build/targets.cs @@ -37,7 +37,7 @@ { const string configuration = "Release"; const string solution = "Kuddle.slnx"; - const string packProject = "src/Kuddle/Kuddle.csproj"; + const string packProject = "src/Kuddle.Net/Kuddle.Net.csproj"; var root = Directory.GetCurrentDirectory(); diff --git a/AGENTS.md b/AGENTS.md index 1514a95..67bdef9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# Agent Guidelines for Kuddle +# Agent Guidelines for Kuddle.Net ## Build Commands diff --git a/Kuddle.slnx b/Kuddle.slnx index 283aa83..af84070 100644 --- a/Kuddle.slnx +++ b/Kuddle.slnx @@ -4,8 +4,8 @@ - - - + + + diff --git a/src/Kuddle.Benchmarks/Kuddle.Benchmarks.csproj b/src/Kuddle.Net.Benchmarks/Kuddle.Net.Benchmarks.csproj similarity index 72% rename from src/Kuddle.Benchmarks/Kuddle.Benchmarks.csproj rename to src/Kuddle.Net.Benchmarks/Kuddle.Net.Benchmarks.csproj index 8998760..5872336 100644 --- a/src/Kuddle.Benchmarks/Kuddle.Benchmarks.csproj +++ b/src/Kuddle.Net.Benchmarks/Kuddle.Net.Benchmarks.csproj @@ -1,5 +1,7 @@ + Kuddle.Benchmarks + Kuddle.Net.Benchmarks Exe net10.0 preview @@ -11,6 +13,6 @@ - + diff --git a/src/Kuddle.Benchmarks/ParserBenchmarks.cs b/src/Kuddle.Net.Benchmarks/ParserBenchmarks.cs similarity index 98% rename from src/Kuddle.Benchmarks/ParserBenchmarks.cs rename to src/Kuddle.Net.Benchmarks/ParserBenchmarks.cs index 72b9743..73a058b 100644 --- a/src/Kuddle.Benchmarks/ParserBenchmarks.cs +++ b/src/Kuddle.Net.Benchmarks/ParserBenchmarks.cs @@ -1,9 +1,7 @@ -using System; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using Kuddle.AST; using Kuddle.Parser; -using Parlot; using Parlot.Fluent; namespace Kuddle.Benchmarks; diff --git a/src/Kuddle.Benchmarks/Program.cs b/src/Kuddle.Net.Benchmarks/Program.cs similarity index 100% rename from src/Kuddle.Benchmarks/Program.cs rename to src/Kuddle.Net.Benchmarks/Program.cs diff --git a/src/Kuddle.Tests/AST/KdlNumberTests.cs b/src/Kuddle.Net.Tests/AST/KdlNumberTests.cs similarity index 100% rename from src/Kuddle.Tests/AST/KdlNumberTests.cs rename to src/Kuddle.Net.Tests/AST/KdlNumberTests.cs diff --git a/src/Kuddle.Tests/AST/KdlStringTests.cs b/src/Kuddle.Net.Tests/AST/KdlStringTests.cs similarity index 100% rename from src/Kuddle.Tests/AST/KdlStringTests.cs rename to src/Kuddle.Net.Tests/AST/KdlStringTests.cs diff --git a/src/Kuddle.Tests/Errors/ErrorHandlingTests.cs b/src/Kuddle.Net.Tests/Errors/ErrorHandlingTests.cs similarity index 100% rename from src/Kuddle.Tests/Errors/ErrorHandlingTests.cs rename to src/Kuddle.Net.Tests/Errors/ErrorHandlingTests.cs diff --git a/src/Kuddle.Tests/Errors/test.kdl b/src/Kuddle.Net.Tests/Errors/test.kdl similarity index 100% rename from src/Kuddle.Tests/Errors/test.kdl rename to src/Kuddle.Net.Tests/Errors/test.kdl diff --git a/src/Kuddle.Tests/Extensions/DxTests.cs b/src/Kuddle.Net.Tests/Extensions/DxTests.cs similarity index 100% rename from src/Kuddle.Tests/Extensions/DxTests.cs rename to src/Kuddle.Net.Tests/Extensions/DxTests.cs diff --git a/src/Kuddle.Tests/Grammar/CommentParserTests.cs b/src/Kuddle.Net.Tests/Grammar/CommentParserTests.cs similarity index 100% rename from src/Kuddle.Tests/Grammar/CommentParserTests.cs rename to src/Kuddle.Net.Tests/Grammar/CommentParserTests.cs diff --git a/src/Kuddle.Tests/Grammar/NodeParserTests.cs b/src/Kuddle.Net.Tests/Grammar/NodeParserTests.cs similarity index 100% rename from src/Kuddle.Tests/Grammar/NodeParserTests.cs rename to src/Kuddle.Net.Tests/Grammar/NodeParserTests.cs diff --git a/src/Kuddle.Tests/Grammar/NumberParsersTests.cs b/src/Kuddle.Net.Tests/Grammar/NumberParsersTests.cs similarity index 100% rename from src/Kuddle.Tests/Grammar/NumberParsersTests.cs rename to src/Kuddle.Net.Tests/Grammar/NumberParsersTests.cs diff --git a/src/Kuddle.Tests/Grammar/StringParserTests.cs b/src/Kuddle.Net.Tests/Grammar/StringParserTests.cs similarity index 100% rename from src/Kuddle.Tests/Grammar/StringParserTests.cs rename to src/Kuddle.Net.Tests/Grammar/StringParserTests.cs diff --git a/src/Kuddle.Tests/Grammar/WhiteSpaceParsersTests.cs b/src/Kuddle.Net.Tests/Grammar/WhiteSpaceParsersTests.cs similarity index 100% rename from src/Kuddle.Tests/Grammar/WhiteSpaceParsersTests.cs rename to src/Kuddle.Net.Tests/Grammar/WhiteSpaceParsersTests.cs diff --git a/src/Kuddle.Tests/Kuddle.Tests.csproj b/src/Kuddle.Net.Tests/Kuddle.Net.Tests.csproj similarity index 78% rename from src/Kuddle.Tests/Kuddle.Tests.csproj rename to src/Kuddle.Net.Tests/Kuddle.Net.Tests.csproj index 765c78f..af186aa 100644 --- a/src/Kuddle.Tests/Kuddle.Tests.csproj +++ b/src/Kuddle.Net.Tests/Kuddle.Net.Tests.csproj @@ -1,5 +1,7 @@  + Kuddle.Tests + Kuddle.Net.Tests enable enable Exe @@ -10,7 +12,7 @@ - + + Kuddle + Kuddle.Net + Kuddle.Net disable enable net10.0 @@ -15,7 +18,7 @@ - - + + diff --git a/src/Kuddle/Parser/CharacterSets.cs b/src/Kuddle.Net/Parser/CharacterSets.cs similarity index 100% rename from src/Kuddle/Parser/CharacterSets.cs rename to src/Kuddle.Net/Parser/CharacterSets.cs diff --git a/src/Kuddle/Parser/DebugParser.cs b/src/Kuddle.Net/Parser/DebugParser.cs similarity index 100% rename from src/Kuddle/Parser/DebugParser.cs rename to src/Kuddle.Net/Parser/DebugParser.cs diff --git a/src/Kuddle/Parser/KdlGrammar.cs b/src/Kuddle.Net/Parser/KdlGrammar.cs similarity index 100% rename from src/Kuddle/Parser/KdlGrammar.cs rename to src/Kuddle.Net/Parser/KdlGrammar.cs diff --git a/src/Kuddle/Parser/MultiLineStringParser.cs b/src/Kuddle.Net/Parser/MultiLineStringParser.cs similarity index 100% rename from src/Kuddle/Parser/MultiLineStringParser.cs rename to src/Kuddle.Net/Parser/MultiLineStringParser.cs diff --git a/src/Kuddle/Parser/RawStringParser.cs b/src/Kuddle.Net/Parser/RawStringParser.cs similarity index 100% rename from src/Kuddle/Parser/RawStringParser.cs rename to src/Kuddle.Net/Parser/RawStringParser.cs diff --git a/src/Kuddle/Serialization/Attributes/KdlArgumentAttribute.cs b/src/Kuddle.Net/Serialization/Attributes/KdlArgumentAttribute.cs similarity index 100% rename from src/Kuddle/Serialization/Attributes/KdlArgumentAttribute.cs rename to src/Kuddle.Net/Serialization/Attributes/KdlArgumentAttribute.cs diff --git a/src/Kuddle/Serialization/Attributes/KdlIgnoreAttribute.cs b/src/Kuddle.Net/Serialization/Attributes/KdlIgnoreAttribute.cs similarity index 100% rename from src/Kuddle/Serialization/Attributes/KdlIgnoreAttribute.cs rename to src/Kuddle.Net/Serialization/Attributes/KdlIgnoreAttribute.cs diff --git a/src/Kuddle/Serialization/Attributes/KdlNodeAttribute.cs b/src/Kuddle.Net/Serialization/Attributes/KdlNodeAttribute.cs similarity index 100% rename from src/Kuddle/Serialization/Attributes/KdlNodeAttribute.cs rename to src/Kuddle.Net/Serialization/Attributes/KdlNodeAttribute.cs diff --git a/src/Kuddle/Serialization/Attributes/KdlPropertyAttribute.cs b/src/Kuddle.Net/Serialization/Attributes/KdlPropertyAttribute.cs similarity index 100% rename from src/Kuddle/Serialization/Attributes/KdlPropertyAttribute.cs rename to src/Kuddle.Net/Serialization/Attributes/KdlPropertyAttribute.cs diff --git a/src/Kuddle/Serialization/KdlReader.cs b/src/Kuddle.Net/Serialization/KdlReader.cs similarity index 100% rename from src/Kuddle/Serialization/KdlReader.cs rename to src/Kuddle.Net/Serialization/KdlReader.cs diff --git a/src/Kuddle/Serialization/KdlReaderOptions.cs b/src/Kuddle.Net/Serialization/KdlReaderOptions.cs similarity index 100% rename from src/Kuddle/Serialization/KdlReaderOptions.cs rename to src/Kuddle.Net/Serialization/KdlReaderOptions.cs diff --git a/src/Kuddle/Serialization/KdlSerializer.cs b/src/Kuddle.Net/Serialization/KdlSerializer.cs similarity index 100% rename from src/Kuddle/Serialization/KdlSerializer.cs rename to src/Kuddle.Net/Serialization/KdlSerializer.cs diff --git a/src/Kuddle/Serialization/KdlValueConverter.cs b/src/Kuddle.Net/Serialization/KdlValueConverter.cs similarity index 100% rename from src/Kuddle/Serialization/KdlValueConverter.cs rename to src/Kuddle.Net/Serialization/KdlValueConverter.cs diff --git a/src/Kuddle/Serialization/KdlWriter.cs b/src/Kuddle.Net/Serialization/KdlWriter.cs similarity index 100% rename from src/Kuddle/Serialization/KdlWriter.cs rename to src/Kuddle.Net/Serialization/KdlWriter.cs diff --git a/src/Kuddle/Serialization/KdlWriterOptions.cs b/src/Kuddle.Net/Serialization/KdlWriterOptions.cs similarity index 100% rename from src/Kuddle/Serialization/KdlWriterOptions.cs rename to src/Kuddle.Net/Serialization/KdlWriterOptions.cs diff --git a/src/Kuddle/Serialization/TypeMetadata.cs b/src/Kuddle.Net/Serialization/TypeMetadata.cs similarity index 100% rename from src/Kuddle/Serialization/TypeMetadata.cs rename to src/Kuddle.Net/Serialization/TypeMetadata.cs diff --git a/src/Kuddle/Validation/KuddleReservedTypeValidator.cs b/src/Kuddle.Net/Validation/KuddleReservedTypeValidator.cs similarity index 100% rename from src/Kuddle/Validation/KuddleReservedTypeValidator.cs rename to src/Kuddle.Net/Validation/KuddleReservedTypeValidator.cs From dde3fb5055a9d9e3ed564cdc1f2abe286e2c64c6 Mon Sep 17 00:00:00 2001 From: jamesshenry <79054685+jamesshenry@users.noreply.github.com> Date: Wed, 17 Dec 2025 23:28:06 +0000 Subject: [PATCH 08/17] wip --- .../Kuddle.Net.Benchmarks.csproj | 3 +- src/Kuddle.Net.Benchmarks/ParserBenchmarks.cs | 2 +- src/Kuddle.Net.Benchmarks/Program.cs | 2 +- .../Serialization/TypeAnnotationTests.cs | 235 ++++++++++++++++++ .../Attributes/KdlTypeAnnotationAttribute.cs | 29 +++ src/Kuddle.Net/Serialization/KdlSerializer.cs | 42 +++- .../Serialization/KdlValueConverter.cs | 34 ++- src/Kuddle.Net/Serialization/TypeMetadata.cs | 6 +- 8 files changed, 333 insertions(+), 20 deletions(-) create mode 100644 src/Kuddle.Net.Tests/Serialization/TypeAnnotationTests.cs create mode 100644 src/Kuddle.Net/Serialization/Attributes/KdlTypeAnnotationAttribute.cs diff --git a/src/Kuddle.Net.Benchmarks/Kuddle.Net.Benchmarks.csproj b/src/Kuddle.Net.Benchmarks/Kuddle.Net.Benchmarks.csproj index 5872336..3ad6a86 100644 --- a/src/Kuddle.Net.Benchmarks/Kuddle.Net.Benchmarks.csproj +++ b/src/Kuddle.Net.Benchmarks/Kuddle.Net.Benchmarks.csproj @@ -1,7 +1,6 @@ - Kuddle.Benchmarks - Kuddle.Net.Benchmarks + Kuddle Exe net10.0 preview diff --git a/src/Kuddle.Net.Benchmarks/ParserBenchmarks.cs b/src/Kuddle.Net.Benchmarks/ParserBenchmarks.cs index 73a058b..5510673 100644 --- a/src/Kuddle.Net.Benchmarks/ParserBenchmarks.cs +++ b/src/Kuddle.Net.Benchmarks/ParserBenchmarks.cs @@ -4,7 +4,7 @@ using Kuddle.Parser; using Parlot.Fluent; -namespace Kuddle.Benchmarks; +namespace Kuddle.Net.Benchmarks; [SimpleJob(RuntimeMoniker.Net10_0)] [MemoryDiagnoser] diff --git a/src/Kuddle.Net.Benchmarks/Program.cs b/src/Kuddle.Net.Benchmarks/Program.cs index c1ac613..1d8ef97 100644 --- a/src/Kuddle.Net.Benchmarks/Program.cs +++ b/src/Kuddle.Net.Benchmarks/Program.cs @@ -1,4 +1,4 @@ using BenchmarkDotNet.Running; -using Kuddle.Benchmarks; +using Kuddle.Net.Benchmarks; BenchmarkRunner.Run(); diff --git a/src/Kuddle.Net.Tests/Serialization/TypeAnnotationTests.cs b/src/Kuddle.Net.Tests/Serialization/TypeAnnotationTests.cs new file mode 100644 index 0000000..41a4cd4 --- /dev/null +++ b/src/Kuddle.Net.Tests/Serialization/TypeAnnotationTests.cs @@ -0,0 +1,235 @@ +using System; +using Kuddle.Serialization; + +namespace Kuddle.Tests.Serialization; + +public class TypeAnnotationTests +{ + #region Test Models + + public class PersonWithAnnotations + { + [KdlArgument(0)] + [KdlTypeAnnotation("uuid")] + public Guid Id { get; set; } + + [KdlProperty("name")] + public string Name { get; set; } = ""; + + [KdlProperty("created")] + [KdlTypeAnnotation("date-time")] + public DateTimeOffset CreatedAt { get; set; } + + [KdlProperty("age")] + [KdlTypeAnnotation("i32")] + public int Age { get; set; } + } + + public class ProductWithTypeAnnotations + { + [KdlArgument(0)] + public string Name { get; set; } = ""; + + [KdlProperty("sku")] + [KdlTypeAnnotation("uuid")] + public Guid Sku { get; set; } + + [KdlProperty("price")] + [KdlTypeAnnotation("decimal64")] + public decimal Price { get; set; } + + [KdlProperty("stock")] + [KdlTypeAnnotation("u32")] + public int StockCount { get; set; } + } + + public class EventWithChildAnnotations + { + [KdlArgument(0)] + public string Name { get; set; } = ""; + + [KdlNode("timestamp")] + [KdlTypeAnnotation("date-time")] + public DateTimeOffset Timestamp { get; set; } + + [KdlNode("correlation-id")] + [KdlTypeAnnotation("uuid")] + public Guid CorrelationId { get; set; } + } + + #endregion + + [Test] + public async Task Serialize_WithUuidTypeAnnotation_WritesAnnotation() + { + // Arrange + var person = new PersonWithAnnotations + { + Id = Guid.Parse("123e4567-e89b-12d3-a456-426614174000"), + Name = "Alice", + CreatedAt = new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero), + Age = 30, + }; + + // Act + var kdl = KdlSerializer.Serialize(person); + + // Assert + await Assert.That(kdl).Contains("(uuid)\"123e4567-e89b-12d3-a456-426614174000\""); + await Assert.That(kdl).Contains("(date-time)\"2024-01-01T00:00:00.0000000+00:00\""); + await Assert.That(kdl).Contains("(i32)30"); + } + + [Test] + public async Task Deserialize_WithUuidTypeAnnotation_ParsesCorrectly() + { + // Arrange + var kdl = """ + personwithannotations (uuid)"123e4567-e89b-12d3-a456-426614174000" name="Alice" created=(date-time)"2024-01-01T00:00:00.0000000+00:00" age=(i32)30 + """; + + // Act + var person = KdlSerializer.Deserialize(kdl); + + // Assert + await Assert.That(person.Id).IsEqualTo(Guid.Parse("123e4567-e89b-12d3-a456-426614174000")); + await Assert.That(person.Name).IsEqualTo("Alice"); + await Assert + .That(person.CreatedAt) + .IsEqualTo(new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero)); + await Assert.That(person.Age).IsEqualTo(30); + } + + [Test] + public async Task Serialize_WithMixedTypeAnnotations_WritesAllAnnotations() + { + // Arrange + var product = new ProductWithTypeAnnotations + { + Name = "Widget", + Sku = Guid.Parse("a1b2c3d4-e5f6-7890-abcd-ef1234567890"), + Price = 99.99m, + StockCount = 42, + }; + + // Act + var kdl = KdlSerializer.Serialize(product); + + // Assert + await Assert.That(kdl).Contains("\"Widget\""); + await Assert.That(kdl).Contains("(uuid)\"a1b2c3d4-e5f6-7890-abcd-ef1234567890\""); + await Assert.That(kdl).Contains("(decimal64)99.99"); + await Assert.That(kdl).Contains("(u32)42"); + } + + [Test] + public async Task Deserialize_WithMixedTypeAnnotations_ParsesAllValues() + { + // Arrange + var kdl = """ + productwithtypeannotations "Widget" sku=(uuid)"a1b2c3d4-e5f6-7890-abcd-ef1234567890" price=(decimal64)99.99 stock=(u32)42 + """; + + // Act + var product = KdlSerializer.Deserialize(kdl); + + // Assert + await Assert.That(product.Name).IsEqualTo("Widget"); + await Assert + .That(product.Sku) + .IsEqualTo(Guid.Parse("a1b2c3d4-e5f6-7890-abcd-ef1234567890")); + await Assert.That(product.Price).IsEqualTo(99.99m); + await Assert.That(product.StockCount).IsEqualTo(42); + } + + [Test] + public async Task Serialize_WithChildNodeAnnotations_WritesAnnotationsOnChildren() + { + // Arrange + var evt = new EventWithChildAnnotations + { + Name = "UserLoggedIn", + Timestamp = new DateTimeOffset(2024, 12, 17, 10, 30, 0, TimeSpan.Zero), + CorrelationId = Guid.Parse("11111111-2222-3333-4444-555555555555"), + }; + + // Act + var kdl = KdlSerializer.Serialize(evt); + + // Assert + await Assert + .That(kdl) + .Contains("timestamp (date-time)\"2024-12-17T10:30:00.0000000+00:00\""); + await Assert + .That(kdl) + .Contains("correlation-id (uuid)\"11111111-2222-3333-4444-555555555555\""); + } + + [Test] + public async Task Deserialize_WithChildNodeAnnotations_ParsesChildValues() + { + // Arrange + var kdl = """ + eventwitchildannotations "UserLoggedIn" { + timestamp (date-time)"2024-12-17T10:30:00.0000000+00:00" + correlation-id (uuid)"11111111-2222-3333-4444-555555555555" + } + """; + + // Act + var evt = KdlSerializer.Deserialize(kdl); + + // Assert + await Assert.That(evt.Name).IsEqualTo("UserLoggedIn"); + await Assert + .That(evt.Timestamp) + .IsEqualTo(new DateTimeOffset(2024, 12, 17, 10, 30, 0, TimeSpan.Zero)); + await Assert + .That(evt.CorrelationId) + .IsEqualTo(Guid.Parse("11111111-2222-3333-4444-555555555555")); + } + + [Test] + public async Task RoundTrip_WithTypeAnnotations_PreservesValues() + { + // Arrange + var original = new PersonWithAnnotations + { + Id = Guid.NewGuid(), + Name = "Bob", + CreatedAt = DateTimeOffset.UtcNow, + Age = 25, + }; + + // Act + var kdl = KdlSerializer.Serialize(original); + var deserialized = KdlSerializer.Deserialize(kdl); + + // Assert + await Assert.That(deserialized.Id).IsEqualTo(original.Id); + await Assert.That(deserialized.Name).IsEqualTo(original.Name); + await Assert.That(deserialized.CreatedAt).IsEqualTo(original.CreatedAt); + await Assert.That(deserialized.Age).IsEqualTo(original.Age); + } + + [Test] + public async Task Deserialize_WithoutTypeAnnotationInKdl_StillWorks() + { + // Arrange - KDL without type annotation, but C# model has [KdlTypeAnnotation] + // The deserializer should still work because Guid/DateTimeOffset have their own parsing logic + var kdl = """ + personwithannotations "123e4567-e89b-12d3-a456-426614174000" name="Alice" created="2024-01-01T00:00:00.0000000+00:00" age=30 + """; + + // Act + var person = KdlSerializer.Deserialize(kdl); + + // Assert - should parse without type annotations in the KDL + await Assert.That(person.Id).IsEqualTo(Guid.Parse("123e4567-e89b-12d3-a456-426614174000")); + await Assert.That(person.Name).IsEqualTo("Alice"); + await Assert + .That(person.CreatedAt) + .IsEqualTo(new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero)); + await Assert.That(person.Age).IsEqualTo(30); + } +} diff --git a/src/Kuddle.Net/Serialization/Attributes/KdlTypeAnnotationAttribute.cs b/src/Kuddle.Net/Serialization/Attributes/KdlTypeAnnotationAttribute.cs new file mode 100644 index 0000000..5c9672c --- /dev/null +++ b/src/Kuddle.Net/Serialization/Attributes/KdlTypeAnnotationAttribute.cs @@ -0,0 +1,29 @@ +using System; + +namespace Kuddle.Serialization; + +/// +/// Specifies a KDL type annotation to be applied when serializing this property or argument. +/// When deserializing, the type annotation is used to guide conversion (e.g., parsing UUID strings). +/// +/// +/// Examples: +/// +/// [KdlTypeAnnotation("uuid")] +/// public Guid Id { get; set; } +/// +/// [KdlTypeAnnotation("date-time")] +/// public DateTimeOffset CreatedAt { get; set; } +/// +/// [KdlTypeAnnotation("i32")] +/// public int Count { get; set; } +/// +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] +public sealed class KdlTypeAnnotationAttribute(string annotation) : Attribute +{ + /// + /// The type annotation string (e.g., "uuid", "date-time", "i32"). + /// + public string Annotation { get; } = annotation; +} diff --git a/src/Kuddle.Net/Serialization/KdlSerializer.cs b/src/Kuddle.Net/Serialization/KdlSerializer.cs index 976799b..10220b0 100644 --- a/src/Kuddle.Net/Serialization/KdlSerializer.cs +++ b/src/Kuddle.Net/Serialization/KdlSerializer.cs @@ -114,7 +114,12 @@ private static void MapNodeToObject(KdlNode node, object instance, TypeMetadata ); } - SetPropertyValue(mapping.Property, instance, argValue); + SetPropertyValue( + mapping.Property, + instance, + argValue, + mapping.TypeAnnotation?.Annotation + ); } // Map properties @@ -128,7 +133,12 @@ private static void MapNodeToObject(KdlNode node, object instance, TypeMetadata continue; // Optional property, use default } - SetPropertyValue(mapping.Property, instance, propValue); + SetPropertyValue( + mapping.Property, + instance, + propValue, + mapping.TypeAnnotation?.Annotation + ); } // Map child nodes @@ -204,18 +214,29 @@ TypeMetadata metadata var argValue = matchingNodes[0].Arg(0); if (argValue is not null) { - SetPropertyValue(mapping.Property, instance, argValue); + SetPropertyValue( + mapping.Property, + instance, + argValue, + mapping.TypeAnnotation?.Annotation + ); } } } } - private static void SetPropertyValue(PropertyInfo property, object instance, KdlValue kdlValue) + private static void SetPropertyValue( + PropertyInfo property, + object instance, + KdlValue kdlValue, + string? expectedTypeAnnotation = null + ) { var result = KdlValueConverter.FromKdlOrThrow( kdlValue, property.PropertyType, - $"Property: {property.DeclaringType?.Name}.{property.Name}" + $"Property: {property.DeclaringType?.Name}.{property.Name}", + expectedTypeAnnotation ); property.SetValue(instance, result); @@ -326,9 +347,11 @@ private static KdlNode SerializeToNode(object instance, string? overrideNodeName foreach (var mapping in metadata.Arguments) { var value = mapping.Property.GetValue(instance); + var typeAnnotation = mapping.TypeAnnotation?.Annotation; var kdlValue = KdlValueConverter.ToKdlOrThrow( value, - $"Argument property: {mapping.Property.Name}" + $"Argument property: {mapping.Property.Name}", + typeAnnotation ); entries.Add(new KdlArgument(kdlValue)); @@ -338,8 +361,9 @@ private static KdlNode SerializeToNode(object instance, string? overrideNodeName foreach (var mapping in metadata.Properties) { var value = mapping.Property.GetValue(instance); + var typeAnnotation = mapping.TypeAnnotation?.Annotation; - if (!KdlValueConverter.TryToKdl(value, out var kdlValue)) + if (!KdlValueConverter.TryToKdl(value, out var kdlValue, typeAnnotation)) { continue; // Skip properties that can't be converted } @@ -379,9 +403,11 @@ private static KdlNode SerializeToNode(object instance, string? overrideNodeName else { // Scalar value as a child node with single argument + var typeAnnotation = mapping.TypeAnnotation?.Annotation; var kdlValue = KdlValueConverter.ToKdlOrThrow( propValue, - $"Child scalar property: {mapping.Property.Name}" + $"Child scalar property: {mapping.Property.Name}", + typeAnnotation ); var scalarNode = new KdlNode(KdlValue.From(childNodeName)) diff --git a/src/Kuddle.Net/Serialization/KdlValueConverter.cs b/src/Kuddle.Net/Serialization/KdlValueConverter.cs index 044efd6..1df9586 100644 --- a/src/Kuddle.Net/Serialization/KdlValueConverter.cs +++ b/src/Kuddle.Net/Serialization/KdlValueConverter.cs @@ -12,7 +12,12 @@ internal static class KdlValueConverter /// /// Attempts to convert a KDL value to a CLR type. /// - public static bool TryFromKdl(KdlValue kdlValue, Type targetType, out object? result) + public static bool TryFromKdl( + KdlValue kdlValue, + Type targetType, + out object? result, + string? typeAnnotation = null + ) { result = null; @@ -99,7 +104,7 @@ public static bool TryFromKdl(KdlValue kdlValue, Type targetType, out object? re /// /// Attempts to convert a CLR value to a KDL value. /// - public static bool TryToKdl(object? input, out KdlValue kdlValue) + public static bool TryToKdl(object? input, out KdlValue kdlValue, string? typeAnnotation = null) { if (input is null) { @@ -122,15 +127,28 @@ public static bool TryToKdl(object? input, out KdlValue kdlValue) _ => null!, }; + if (kdlValue is not null && typeAnnotation is not null) + { + kdlValue = kdlValue with { TypeAnnotation = typeAnnotation }; + } + return kdlValue is not null; } /// /// Converts a KDL value to a CLR type, throwing on failure. /// - public static object? FromKdlOrThrow(KdlValue kdlValue, Type targetType, string context) + public static object? FromKdlOrThrow( + KdlValue kdlValue, + Type targetType, + string context, + string? expectedTypeAnnotation = null + ) { - if (!TryFromKdl(kdlValue, targetType, out var result)) + // Use the KDL value's type annotation if present, otherwise use the expected one from the attribute + var effectiveAnnotation = kdlValue.TypeAnnotation ?? expectedTypeAnnotation; + + if (!TryFromKdl(kdlValue, targetType, out var result, effectiveAnnotation)) { throw new KuddleSerializationException( $"Cannot convert KDL value '{kdlValue}' to {targetType.Name}. {context}" @@ -142,9 +160,13 @@ public static bool TryToKdl(object? input, out KdlValue kdlValue) /// /// Converts a CLR value to a KDL value, throwing on failure. /// - public static KdlValue ToKdlOrThrow(object? input, string context) + public static KdlValue ToKdlOrThrow( + object? input, + string context, + string? typeAnnotation = null + ) { - if (!TryToKdl(input, out var kdlValue)) + if (!TryToKdl(input, out var kdlValue, typeAnnotation)) { var typeName = input?.GetType().Name ?? "null"; throw new KuddleSerializationException( diff --git a/src/Kuddle.Net/Serialization/TypeMetadata.cs b/src/Kuddle.Net/Serialization/TypeMetadata.cs index 2fc1c07..c40e6da 100644 --- a/src/Kuddle.Net/Serialization/TypeMetadata.cs +++ b/src/Kuddle.Net/Serialization/TypeMetadata.cs @@ -15,7 +15,8 @@ internal sealed record PropertyMapping( PropertyInfo Property, KdlArgumentAttribute? Argument, KdlPropertyAttribute? KdlProperty, - KdlNodeAttribute? ChildNode + KdlNodeAttribute? ChildNode, + KdlTypeAnnotationAttribute? TypeAnnotation ) { public string GetPropertyKey() => KdlProperty?.Key ?? Property.Name.ToLowerInvariant(); @@ -63,7 +64,8 @@ private TypeMetadata(Type type) p, p.GetCustomAttribute(), p.GetCustomAttribute(), - p.GetCustomAttribute() + p.GetCustomAttribute(), + p.GetCustomAttribute() )) .ToList(); From 44ab5f1e7e96f00859dbda633a29be3018b81264 Mon Sep 17 00:00:00 2001 From: jamesshenry <79054685+jamesshenry@users.noreply.github.com> Date: Thu, 18 Dec 2025 03:10:01 +0000 Subject: [PATCH 09/17] feat: add support for type annotations --- .../Serialization/TypeAnnotationTests.cs | 24 ++---- src/Kuddle.Net/Parser/CharacterSets.cs | 83 +++++++++++++++---- .../Attributes/KdlArgumentAttribute.cs | 20 ++++- .../Attributes/KdlNodeAttribute.cs | 2 +- .../Attributes/KdlPropertyAttribute.cs | 3 +- .../Attributes/KdlTypeAnnotationAttribute.cs | 29 ------- src/Kuddle.Net/Serialization/KdlSerializer.cs | 53 +++--------- .../Serialization/KdlValueConverter.cs | 39 ++++----- src/Kuddle.Net/Serialization/TypeMetadata.cs | 57 ++++++------- .../Validation/KuddleReservedTypeValidator.cs | 33 +------- 10 files changed, 158 insertions(+), 185 deletions(-) delete mode 100644 src/Kuddle.Net/Serialization/Attributes/KdlTypeAnnotationAttribute.cs diff --git a/src/Kuddle.Net.Tests/Serialization/TypeAnnotationTests.cs b/src/Kuddle.Net.Tests/Serialization/TypeAnnotationTests.cs index 41a4cd4..25c54cc 100644 --- a/src/Kuddle.Net.Tests/Serialization/TypeAnnotationTests.cs +++ b/src/Kuddle.Net.Tests/Serialization/TypeAnnotationTests.cs @@ -9,19 +9,16 @@ public class TypeAnnotationTests public class PersonWithAnnotations { - [KdlArgument(0)] - [KdlTypeAnnotation("uuid")] + [KdlArgument(0, "uuid")] public Guid Id { get; set; } [KdlProperty("name")] public string Name { get; set; } = ""; - [KdlProperty("created")] - [KdlTypeAnnotation("date-time")] + [KdlProperty("created", "date-time")] public DateTimeOffset CreatedAt { get; set; } - [KdlProperty("age")] - [KdlTypeAnnotation("i32")] + [KdlProperty("age", "i32")] public int Age { get; set; } } @@ -30,16 +27,13 @@ public class ProductWithTypeAnnotations [KdlArgument(0)] public string Name { get; set; } = ""; - [KdlProperty("sku")] - [KdlTypeAnnotation("uuid")] + [KdlProperty("sku", "uuid")] public Guid Sku { get; set; } - [KdlProperty("price")] - [KdlTypeAnnotation("decimal64")] + [KdlProperty("price", "decimal64")] public decimal Price { get; set; } - [KdlProperty("stock")] - [KdlTypeAnnotation("u32")] + [KdlProperty("stock", "u32")] public int StockCount { get; set; } } @@ -49,11 +43,9 @@ public class EventWithChildAnnotations public string Name { get; set; } = ""; [KdlNode("timestamp")] - [KdlTypeAnnotation("date-time")] public DateTimeOffset Timestamp { get; set; } [KdlNode("correlation-id")] - [KdlTypeAnnotation("uuid")] public Guid CorrelationId { get; set; } } @@ -116,7 +108,7 @@ public async Task Serialize_WithMixedTypeAnnotations_WritesAllAnnotations() var kdl = KdlSerializer.Serialize(product); // Assert - await Assert.That(kdl).Contains("\"Widget\""); + await Assert.That(kdl).Contains("Widget"); await Assert.That(kdl).Contains("(uuid)\"a1b2c3d4-e5f6-7890-abcd-ef1234567890\""); await Assert.That(kdl).Contains("(decimal64)99.99"); await Assert.That(kdl).Contains("(u32)42"); @@ -170,7 +162,7 @@ public async Task Deserialize_WithChildNodeAnnotations_ParsesChildValues() { // Arrange var kdl = """ - eventwitchildannotations "UserLoggedIn" { + eventwithchildannotations "UserLoggedIn" { timestamp (date-time)"2024-12-17T10:30:00.0000000+00:00" correlation-id (uuid)"11111111-2222-3333-4444-555555555555" } diff --git a/src/Kuddle.Net/Parser/CharacterSets.cs b/src/Kuddle.Net/Parser/CharacterSets.cs index b25fa3e..e5a5f5c 100644 --- a/src/Kuddle.Net/Parser/CharacterSets.cs +++ b/src/Kuddle.Net/Parser/CharacterSets.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Kuddle.Parser; @@ -71,18 +72,72 @@ internal static bool IsNewline(char c) }; } - // internal static bool IsDisallowedLiteralCodePoint(char c) - // { - // return c switch - // { - // >= '\u0000' and <= '\u0008' => true, // various control characters - // '\u007F' => true, - // >= '\uD800' and <= '\uDFFF' => true, // unicode scalar value - // (>= '\u200E' and <= '\u200F') - // or (>= '\u202A' and <= '\u202E') - // or (>= '\u2066' and <= '\u2069') => true, // unicode 'direction control' characters - // '\uFEFF' => true, // unicode BOM - // _ => false, - // }; - // } + public static readonly HashSet ReservedTypes = + [ + "i8", + "i16", + "i32", + "i64", + "u8", + "u16", + "u32", + "u64", + "f32", + "f64", + "decimal64", + "decimal128", + "date-time", + "time", + "date", + "duration", + "decimal", + "currency", + "country-2", + "country-3", + "ipv4", + "ipv6", + "url", + "uuid", + "regex", + "base64", + ]; + + /// + /// Maps KDL type annotations to their corresponding CLR types. + /// Not all reserved types have CLR equivalents (e.g., country codes, IP addresses as validated strings). + /// + public static readonly Dictionary TypeAnnotationToClrType = new() + { + ["i8"] = typeof(sbyte), + ["i16"] = typeof(short), + ["i32"] = typeof(int), + ["i64"] = typeof(long), + ["u8"] = typeof(byte), + ["u16"] = typeof(ushort), + ["u32"] = typeof(uint), + ["u64"] = typeof(ulong), + ["f32"] = typeof(float), + ["f64"] = typeof(double), + ["decimal64"] = typeof(decimal), + ["decimal128"] = typeof(decimal), + ["decimal"] = typeof(decimal), + ["date-time"] = typeof(DateTimeOffset), + ["time"] = typeof(TimeOnly), + ["date"] = typeof(DateOnly), + ["duration"] = typeof(TimeSpan), + ["uuid"] = typeof(Guid), + ["url"] = typeof(Uri), + ["base64"] = typeof(byte[]), + // These remain as strings with semantic meaning: + // "currency", "country-2", "country-3", "ipv4", "ipv6", "regex" + }; + + /// + /// Gets the CLR type for a KDL type annotation, or null if no mapping exists. + /// + public static Type? GetClrType(string? typeAnnotation) => + typeAnnotation is not null + && TypeAnnotationToClrType.TryGetValue(typeAnnotation, out var type) + ? type + : null; } diff --git a/src/Kuddle.Net/Serialization/Attributes/KdlArgumentAttribute.cs b/src/Kuddle.Net/Serialization/Attributes/KdlArgumentAttribute.cs index ceda7b6..d917eee 100644 --- a/src/Kuddle.Net/Serialization/Attributes/KdlArgumentAttribute.cs +++ b/src/Kuddle.Net/Serialization/Attributes/KdlArgumentAttribute.cs @@ -3,7 +3,25 @@ namespace Kuddle.Serialization; [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] -public sealed class KdlArgumentAttribute(int index) : Attribute +public sealed class KdlArgumentAttribute(int index, string? typeAnnotation = null) + : KdlEntryAttribute(typeAnnotation) { public int Index { get; } = index; } + +/// +/// Base class for KDL entry attributes. Only one entry attribute can be applied per property. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +public abstract class KdlEntryAttribute : Attribute +{ + /// + /// Optional KDL type annotation (e.g., "uuid", "date-time", "i32"). + /// + public string? TypeAnnotation { get; } + + protected KdlEntryAttribute(string? typeAnnotation = null) + { + TypeAnnotation = typeAnnotation; + } +} diff --git a/src/Kuddle.Net/Serialization/Attributes/KdlNodeAttribute.cs b/src/Kuddle.Net/Serialization/Attributes/KdlNodeAttribute.cs index 1ac32ef..8f7febf 100644 --- a/src/Kuddle.Net/Serialization/Attributes/KdlNodeAttribute.cs +++ b/src/Kuddle.Net/Serialization/Attributes/KdlNodeAttribute.cs @@ -3,7 +3,7 @@ namespace Kuddle.Serialization; [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] -public sealed class KdlNodeAttribute(string? name = null) : Attribute +public sealed class KdlNodeAttribute(string? name = null) : KdlEntryAttribute(name) { public string? Name { get; } = name; } diff --git a/src/Kuddle.Net/Serialization/Attributes/KdlPropertyAttribute.cs b/src/Kuddle.Net/Serialization/Attributes/KdlPropertyAttribute.cs index f1b892b..86bd0d5 100644 --- a/src/Kuddle.Net/Serialization/Attributes/KdlPropertyAttribute.cs +++ b/src/Kuddle.Net/Serialization/Attributes/KdlPropertyAttribute.cs @@ -3,7 +3,8 @@ namespace Kuddle.Serialization; [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] -public sealed class KdlPropertyAttribute(string? key = null) : Attribute +public sealed class KdlPropertyAttribute(string? key = null, string? typeAnnotation = null) + : KdlEntryAttribute(typeAnnotation) { public string? Key { get; } = key; } diff --git a/src/Kuddle.Net/Serialization/Attributes/KdlTypeAnnotationAttribute.cs b/src/Kuddle.Net/Serialization/Attributes/KdlTypeAnnotationAttribute.cs deleted file mode 100644 index 5c9672c..0000000 --- a/src/Kuddle.Net/Serialization/Attributes/KdlTypeAnnotationAttribute.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace Kuddle.Serialization; - -/// -/// Specifies a KDL type annotation to be applied when serializing this property or argument. -/// When deserializing, the type annotation is used to guide conversion (e.g., parsing UUID strings). -/// -/// -/// Examples: -/// -/// [KdlTypeAnnotation("uuid")] -/// public Guid Id { get; set; } -/// -/// [KdlTypeAnnotation("date-time")] -/// public DateTimeOffset CreatedAt { get; set; } -/// -/// [KdlTypeAnnotation("i32")] -/// public int Count { get; set; } -/// -/// -[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] -public sealed class KdlTypeAnnotationAttribute(string annotation) : Attribute -{ - /// - /// The type annotation string (e.g., "uuid", "date-time", "i32"). - /// - public string Annotation { get; } = annotation; -} diff --git a/src/Kuddle.Net/Serialization/KdlSerializer.cs b/src/Kuddle.Net/Serialization/KdlSerializer.cs index 10220b0..9a29257 100644 --- a/src/Kuddle.Net/Serialization/KdlSerializer.cs +++ b/src/Kuddle.Net/Serialization/KdlSerializer.cs @@ -93,7 +93,7 @@ public static T Deserialize(string text, KdlSerializerOptions? options = null { // Type maps to a document with child nodes as properties var instance = new T(); - MapDocumentToObject(document.Nodes, instance, metadata); + MapChildNodes(document.Nodes, instance, metadata); return instance; } } @@ -104,59 +104,37 @@ public static T Deserialize(string text, KdlSerializerOptions? options = null private static void MapNodeToObject(KdlNode node, object instance, TypeMetadata metadata) { // Map arguments - foreach (var mapping in metadata.Arguments) + foreach (var mapping in metadata.ArgumentAttributes) { - var argValue = node.Arg(mapping.Argument!.Index); + var argValue = node.Arg(mapping.ArgumentIndex); if (argValue is null) { throw new KuddleSerializationException( - $"Missing required argument at index {mapping.Argument.Index} for property '{mapping.Property.Name}'." + $"Missing required argument at index {mapping.ArgumentIndex}'." ); } - SetPropertyValue( - mapping.Property, - instance, - argValue, - mapping.TypeAnnotation?.Annotation - ); + SetPropertyValue(mapping.Property, instance, argValue, mapping.TypeAnnotation); } // Map properties foreach (var mapping in metadata.Properties) { var propKey = mapping.GetPropertyKey(); - var propValue = node.Prop(propKey); + var kdlValue = node.Prop(propKey); - if (propValue is null) + if (kdlValue is null) { continue; // Optional property, use default } - SetPropertyValue( - mapping.Property, - instance, - propValue, - mapping.TypeAnnotation?.Annotation - ); + SetPropertyValue(mapping.Property, instance, kdlValue, mapping.TypeAnnotation); } // Map child nodes MapChildNodes(node.Children?.Nodes, instance, metadata); } - /// - /// Maps document-level nodes to object properties (for non-node-definition types). - /// - private static void MapDocumentToObject( - List nodes, - object instance, - TypeMetadata metadata - ) - { - MapChildNodes(nodes, instance, metadata); - } - /// /// Maps child KDL nodes to properties marked with [KdlNode]. /// @@ -214,12 +192,7 @@ TypeMetadata metadata var argValue = matchingNodes[0].Arg(0); if (argValue is not null) { - SetPropertyValue( - mapping.Property, - instance, - argValue, - mapping.TypeAnnotation?.Annotation - ); + SetPropertyValue(mapping.Property, instance, argValue, mapping.TypeAnnotation); } } } @@ -344,10 +317,10 @@ private static KdlNode SerializeToNode(object instance, string? overrideNodeName var entries = new List(); // Serialize arguments (in order) - foreach (var mapping in metadata.Arguments) + foreach (var mapping in metadata.ArgumentAttributes) { var value = mapping.Property.GetValue(instance); - var typeAnnotation = mapping.TypeAnnotation?.Annotation; + var typeAnnotation = mapping.TypeAnnotation; var kdlValue = KdlValueConverter.ToKdlOrThrow( value, $"Argument property: {mapping.Property.Name}", @@ -361,7 +334,7 @@ private static KdlNode SerializeToNode(object instance, string? overrideNodeName foreach (var mapping in metadata.Properties) { var value = mapping.Property.GetValue(instance); - var typeAnnotation = mapping.TypeAnnotation?.Annotation; + var typeAnnotation = mapping.TypeAnnotation; if (!KdlValueConverter.TryToKdl(value, out var kdlValue, typeAnnotation)) { @@ -403,7 +376,7 @@ private static KdlNode SerializeToNode(object instance, string? overrideNodeName else { // Scalar value as a child node with single argument - var typeAnnotation = mapping.TypeAnnotation?.Annotation; + var typeAnnotation = mapping.TypeAnnotation; var kdlValue = KdlValueConverter.ToKdlOrThrow( propValue, $"Child scalar property: {mapping.Property.Name}", diff --git a/src/Kuddle.Net/Serialization/KdlValueConverter.cs b/src/Kuddle.Net/Serialization/KdlValueConverter.cs index 1df9586..69a7edc 100644 --- a/src/Kuddle.Net/Serialization/KdlValueConverter.cs +++ b/src/Kuddle.Net/Serialization/KdlValueConverter.cs @@ -1,6 +1,7 @@ using System; using Kuddle.AST; using Kuddle.Extensions; +using Kuddle.Parser; namespace Kuddle.Serialization; @@ -12,14 +13,9 @@ internal static class KdlValueConverter /// /// Attempts to convert a KDL value to a CLR type. /// - public static bool TryFromKdl( - KdlValue kdlValue, - Type targetType, - out object? result, - string? typeAnnotation = null - ) + public static bool TryFromKdl(KdlValue kdlValue, Type targetType, out object? value) { - result = null; + value = default; if (kdlValue is KdlNull) { @@ -27,7 +23,7 @@ public static bool TryFromKdl( { return false; } - result = null; + value = null; return true; } @@ -36,65 +32,65 @@ public static bool TryFromKdl( // String if (underlying == typeof(string) && kdlValue.TryGetString(out var stringVal)) { - result = stringVal; + value = stringVal; return true; } // Integers if (underlying == typeof(int) && kdlValue.TryGetInt(out var intVal)) { - result = intVal; + value = intVal; return true; } if (underlying == typeof(long) && kdlValue.TryGetLong(out var longVal)) { - result = longVal; + value = longVal; return true; } // Floating point if (underlying == typeof(double) && kdlValue.TryGetDouble(out var doubleVal)) { - result = doubleVal; + value = doubleVal; return true; } if (underlying == typeof(decimal) && kdlValue.TryGetDecimal(out var decimalVal)) { - result = decimalVal; + value = decimalVal; return true; } if (underlying == typeof(float) && kdlValue.TryGetDouble(out var floatVal)) { - result = (float)floatVal; + value = (float)floatVal; return true; } // Boolean if (underlying == typeof(bool) && kdlValue.TryGetBool(out var boolVal)) { - result = boolVal; + value = boolVal; return true; } // Special types with type annotations if (underlying == typeof(Guid) && kdlValue.TryGetUuid(out var uuid)) { - result = uuid; + value = uuid; return true; } if (underlying == typeof(DateTimeOffset) && kdlValue.TryGetDateTime(out var dto)) { - result = dto; + value = dto; return true; } if (underlying == typeof(DateTime) && kdlValue.TryGetDateTime(out var dt)) { - result = dt.DateTime; + value = dt.DateTime; return true; } @@ -145,10 +141,11 @@ public static bool TryToKdl(object? input, out KdlValue kdlValue, string? typeAn string? expectedTypeAnnotation = null ) { - // Use the KDL value's type annotation if present, otherwise use the expected one from the attribute - var effectiveAnnotation = kdlValue.TypeAnnotation ?? expectedTypeAnnotation; + var finalTargetType = + CharacterSets.GetClrType(expectedTypeAnnotation ?? kdlValue.TypeAnnotation) + ?? targetType; - if (!TryFromKdl(kdlValue, targetType, out var result, effectiveAnnotation)) + if (!TryFromKdl(kdlValue, finalTargetType, out var result)) { throw new KuddleSerializationException( $"Cannot convert KDL value '{kdlValue}' to {targetType.Name}. {context}" diff --git a/src/Kuddle.Net/Serialization/TypeMetadata.cs b/src/Kuddle.Net/Serialization/TypeMetadata.cs index c40e6da..7f2548a 100644 --- a/src/Kuddle.Net/Serialization/TypeMetadata.cs +++ b/src/Kuddle.Net/Serialization/TypeMetadata.cs @@ -11,17 +11,25 @@ namespace Kuddle.Serialization; /// /// Represents a mapping from a C# property to a KDL entry (argument, property, or child node). /// -internal sealed record PropertyMapping( - PropertyInfo Property, - KdlArgumentAttribute? Argument, - KdlPropertyAttribute? KdlProperty, - KdlNodeAttribute? ChildNode, - KdlTypeAnnotationAttribute? TypeAnnotation -) +internal sealed record KdlEntryMapping(PropertyInfo Property, KdlEntryAttribute? Entry) { - public string GetPropertyKey() => KdlProperty?.Key ?? Property.Name.ToLowerInvariant(); + public bool IsArgument => Entry is KdlArgumentAttribute; + public bool IsProperty => Entry is KdlPropertyAttribute; + public bool IsChildNode => Entry is KdlNodeAttribute; - public string GetChildNodeName() => ChildNode?.Name ?? Property.Name.ToLowerInvariant(); + public int ArgumentIndex => Entry is KdlArgumentAttribute arg ? arg.Index : -1; + + public string GetPropertyKey() => + Entry is KdlPropertyAttribute prop + ? prop.Key ?? Property.Name.ToLowerInvariant() + : Property.Name.ToLowerInvariant(); + + public string GetChildNodeName() => + Entry is KdlNodeAttribute node + ? node.Name ?? Property.Name.ToLowerInvariant() + : Property.Name.ToLowerInvariant(); + + public string? TypeAnnotation => Entry?.TypeAnnotation; } /// @@ -35,50 +43,37 @@ internal sealed class TypeMetadata public Type Type { get; } public string NodeName { get; } - public bool IsNodeDefinition => Arguments.Count > 0 || Properties.Count > 0; + public bool IsNodeDefinition => ArgumentAttributes.Count > 0 || Properties.Count > 0; /// Properties mapped to KDL arguments, sorted by index. - public IReadOnlyList Arguments { get; } + public IReadOnlyList ArgumentAttributes { get; } /// Properties mapped to KDL properties. - public IReadOnlyList Properties { get; } + public IReadOnlyList Properties { get; } /// Properties mapped to child nodes. - public IReadOnlyList Children { get; } + public IReadOnlyList Children { get; } /// All writable, non-ignored properties. - public IReadOnlyList AllMappings { get; } + public IReadOnlyList AllMappings { get; } private TypeMetadata(Type type) { Type = type; - // Determine node name from [KdlType] or fall back to type name var kdlTypeAttr = type.GetCustomAttribute(); NodeName = kdlTypeAttr?.Name ?? type.Name.ToLowerInvariant(); - // Gather all writable, non-ignored properties var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.CanWrite && p.GetCustomAttribute() == null) - .Select(p => new PropertyMapping( - p, - p.GetCustomAttribute(), - p.GetCustomAttribute(), - p.GetCustomAttribute(), - p.GetCustomAttribute() - )) + .Select(p => new KdlEntryMapping(p, p.GetCustomAttribute())) .ToList(); AllMappings = props; - Arguments = props - .Where(m => m.Argument is not null) - .OrderBy(m => m.Argument!.Index) - .ToList(); - - Properties = props.Where(m => m.KdlProperty is not null).ToList(); - - Children = props.Where(m => m.ChildNode is not null).ToList(); + ArgumentAttributes = [.. props.Where(m => m.IsArgument).OrderBy(m => m.ArgumentIndex)]; + Properties = [.. props.Where(m => m.IsProperty)]; + Children = [.. props.Where(m => m.IsChildNode)]; } /// diff --git a/src/Kuddle.Net/Validation/KuddleReservedTypeValidator.cs b/src/Kuddle.Net/Validation/KuddleReservedTypeValidator.cs index ff05cb7..0a44848 100644 --- a/src/Kuddle.Net/Validation/KuddleReservedTypeValidator.cs +++ b/src/Kuddle.Net/Validation/KuddleReservedTypeValidator.cs @@ -5,41 +5,12 @@ using System.Text.RegularExpressions; using Kuddle.AST; using Kuddle.Exceptions; +using Kuddle.Parser; namespace Kuddle.Validation; public static class KdlReservedTypeValidator { - private static readonly HashSet ReservedTypes = - [ - "i8", - "i16", - "i32", - "i64", - "u8", - "u16", - "u32", - "u64", - "f32", - "f64", - "decimal64", - "decimal128", - "date-time", - "time", - "date", - "duration", - "decimal", - "currency", - "country-2", - "country-3", - "ipv4", - "ipv6", - "url", - "uuid", - "regex", - "base64", - ]; - public static void Validate(KdlDocument doc) { var errors = new List(); @@ -82,7 +53,7 @@ private static void ValidateValue(KdlValue val, List erro { if (val.TypeAnnotation == null) return; - if (!ReservedTypes.Contains(val.TypeAnnotation)) + if (!CharacterSets.ReservedTypes.Contains(val.TypeAnnotation)) return; try From b666f83c405ec2c5afd35c5d3a8138ee04c06cf6 Mon Sep 17 00:00:00 2001 From: jamesshenry <79054685+jamesshenry@users.noreply.github.com> Date: Thu, 18 Dec 2025 04:07:18 +0000 Subject: [PATCH 10/17] fix tests --- .../Serialization/TypeAnnotationTests.cs | 8 +- .../Attributes/KdlNodeAttribute.cs | 3 +- src/Kuddle.Net/Serialization/KdlSerializer.cs | 86 +++++++++++++++++-- .../Serialization/KdlValueConverter.cs | 45 +++++++++- 4 files changed, 127 insertions(+), 15 deletions(-) diff --git a/src/Kuddle.Net.Tests/Serialization/TypeAnnotationTests.cs b/src/Kuddle.Net.Tests/Serialization/TypeAnnotationTests.cs index 25c54cc..20a3c6f 100644 --- a/src/Kuddle.Net.Tests/Serialization/TypeAnnotationTests.cs +++ b/src/Kuddle.Net.Tests/Serialization/TypeAnnotationTests.cs @@ -34,7 +34,7 @@ public class ProductWithTypeAnnotations public decimal Price { get; set; } [KdlProperty("stock", "u32")] - public int StockCount { get; set; } + public uint StockCount { get; set; } } public class EventWithChildAnnotations @@ -42,10 +42,10 @@ public class EventWithChildAnnotations [KdlArgument(0)] public string Name { get; set; } = ""; - [KdlNode("timestamp")] + [KdlNode("timestamp", "date-time")] public DateTimeOffset Timestamp { get; set; } - [KdlNode("correlation-id")] + [KdlNode("correlation-id", "uuid")] public Guid CorrelationId { get; set; } } @@ -131,7 +131,7 @@ await Assert .That(product.Sku) .IsEqualTo(Guid.Parse("a1b2c3d4-e5f6-7890-abcd-ef1234567890")); await Assert.That(product.Price).IsEqualTo(99.99m); - await Assert.That(product.StockCount).IsEqualTo(42); + await Assert.That(product.StockCount).IsEqualTo(42u); } [Test] diff --git a/src/Kuddle.Net/Serialization/Attributes/KdlNodeAttribute.cs b/src/Kuddle.Net/Serialization/Attributes/KdlNodeAttribute.cs index 8f7febf..23a03cc 100644 --- a/src/Kuddle.Net/Serialization/Attributes/KdlNodeAttribute.cs +++ b/src/Kuddle.Net/Serialization/Attributes/KdlNodeAttribute.cs @@ -3,7 +3,8 @@ namespace Kuddle.Serialization; [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] -public sealed class KdlNodeAttribute(string? name = null) : KdlEntryAttribute(name) +public sealed class KdlNodeAttribute(string? name = null, string? typeAnnotation = null) + : KdlEntryAttribute(typeAnnotation) { public string? Name { get; } = name; } diff --git a/src/Kuddle.Net/Serialization/KdlSerializer.cs b/src/Kuddle.Net/Serialization/KdlSerializer.cs index 9a29257..388eab8 100644 --- a/src/Kuddle.Net/Serialization/KdlSerializer.cs +++ b/src/Kuddle.Net/Serialization/KdlSerializer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Threading; using Kuddle.AST; using Kuddle.Extensions; @@ -13,6 +14,8 @@ namespace Kuddle.Serialization; /// public static class KdlSerializer { + private const StringComparison NodeNameComparison = StringComparison.OrdinalIgnoreCase; + #region Deserialization /// @@ -20,7 +23,8 @@ public static class KdlSerializer /// public static IEnumerable DeserializeMany( string text, - KdlSerializerOptions? options = null + KdlSerializerOptions? options = null, + CancellationToken cancellationToken = default ) where T : new() { @@ -29,7 +33,9 @@ public static IEnumerable DeserializeMany( foreach (var node in doc.Nodes) { - if (!node.Name.Value.Equals(metadata.NodeName, StringComparison.OrdinalIgnoreCase)) + cancellationToken.ThrowIfCancellationRequested(); + + if (!node.Name.Value.Equals(metadata.NodeName, NodeNameComparison)) { throw new KuddleSerializationException( $"Expected node '{metadata.NodeName}', found '{node.Name.Value}'." @@ -78,7 +84,7 @@ public static T Deserialize(string text, KdlSerializerOptions? options = null } var rootNode = document.Nodes[0]; - if (!rootNode.Name.Value.Equals(metadata.NodeName, StringComparison.OrdinalIgnoreCase)) + if (!rootNode.Name.Value.Equals(metadata.NodeName, NodeNameComparison)) { throw new KuddleSerializationException( $"Expected node '{metadata.NodeName}', found '{rootNode.Name.Value}'." @@ -152,9 +158,7 @@ TypeMetadata metadata foreach (var mapping in metadata.Children) { var nodeName = mapping.GetChildNodeName(); - var matchingNodes = nodes - .Where(n => n.Name.Value.Equals(nodeName, StringComparison.OrdinalIgnoreCase)) - .ToList(); + var matchingNodes = GetMatchingNodes(nodes, nodeName); if (matchingNodes.Count == 0) { @@ -198,6 +202,19 @@ TypeMetadata metadata } } + private static List GetMatchingNodes(IReadOnlyList nodes, string nodeName) + { + var result = new List(); + foreach (var node in nodes) + { + if (node.Name.Value.Equals(nodeName, NodeNameComparison)) + { + result.Add(node); + } + } + return result; + } + private static void SetPropertyValue( PropertyInfo property, object instance, @@ -273,7 +290,7 @@ List nodes /// /// Serializes an object to a KDL string. /// - public static string Serialize(T instance) + public static string Serialize(T instance, KdlSerializerOptions? options = null) { ArgumentNullException.ThrowIfNull(instance); @@ -308,6 +325,36 @@ public static string Serialize(T instance) return KdlWriter.Write(doc); } + /// + /// Serializes multiple objects to a KDL string. + /// + public static string SerializeMany( + IEnumerable items, + KdlSerializerOptions? options = null, + CancellationToken cancellationToken = default + ) + { + ArgumentNullException.ThrowIfNull(items); + + var metadata = TypeMetadata.For(); + var doc = new KdlDocument(); + + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (item is null) + { + continue; + } + + var node = SerializeToNode(item); + doc.Nodes.Add(node); + } + + return KdlWriter.Write(doc); + } + private static KdlNode SerializeToNode(object instance, string? overrideNodeName = null) { var type = instance.GetType(); @@ -460,6 +507,27 @@ IList list } /// -/// Options for KDL serialization (reserved for future use). +/// Options for KDL serialization and deserialization. /// -public record KdlSerializerOptions { } +public record KdlSerializerOptions +{ + /// + /// Whether to ignore null values when serializing. Default is true. + /// + public bool IgnoreNullValues { get; init; } = true; + + /// + /// Whether property/node name comparison is case-insensitive. Default is true. + /// + public bool CaseInsensitiveNames { get; init; } = true; + + /// + /// Whether to include type annotations in output. Default is true. + /// + public bool WriteTypeAnnotations { get; init; } = true; + + /// + /// Default options instance. + /// + public static KdlSerializerOptions Default { get; } = new(); +} diff --git a/src/Kuddle.Net/Serialization/KdlValueConverter.cs b/src/Kuddle.Net/Serialization/KdlValueConverter.cs index 69a7edc..389bbab 100644 --- a/src/Kuddle.Net/Serialization/KdlValueConverter.cs +++ b/src/Kuddle.Net/Serialization/KdlValueConverter.cs @@ -36,7 +36,7 @@ public static bool TryFromKdl(KdlValue kdlValue, Type targetType, out object? va return true; } - // Integers + // Integers (signed) if (underlying == typeof(int) && kdlValue.TryGetInt(out var intVal)) { value = intVal; @@ -49,6 +49,43 @@ public static bool TryFromKdl(KdlValue kdlValue, Type targetType, out object? va return true; } + if (underlying == typeof(short) && kdlValue.TryGetInt(out var shortVal)) + { + value = (short)shortVal; + return true; + } + + if (underlying == typeof(sbyte) && kdlValue.TryGetInt(out var sbyteVal)) + { + value = (sbyte)sbyteVal; + return true; + } + + // Integers (unsigned) + if (underlying == typeof(uint) && kdlValue.TryGetLong(out var uintVal)) + { + value = (uint)uintVal; + return true; + } + + if (underlying == typeof(ulong) && kdlValue.TryGetLong(out var ulongVal)) + { + value = (ulong)ulongVal; + return true; + } + + if (underlying == typeof(ushort) && kdlValue.TryGetInt(out var ushortVal)) + { + value = (ushort)ushortVal; + return true; + } + + if (underlying == typeof(byte) && kdlValue.TryGetInt(out var byteVal)) + { + value = (byte)byteVal; + return true; + } + // Floating point if (underlying == typeof(double) && kdlValue.TryGetDouble(out var doubleVal)) { @@ -113,6 +150,12 @@ public static bool TryToKdl(object? input, out KdlValue kdlValue, string? typeAn string s => KdlValue.From(s), int i => KdlValue.From(i), long l => KdlValue.From(l), + short sh => KdlValue.From(sh), + sbyte sb => KdlValue.From(sb), + uint ui => KdlValue.From(ui), + ulong ul => KdlValue.From((long)ul), + ushort us => KdlValue.From(us), + byte by => KdlValue.From(by), double d => KdlValue.From(d), float f => KdlValue.From((double)f), decimal m => KdlValue.From(m), From f94c3019d8e43fb512a7bba4edf78180eada9364 Mon Sep 17 00:00:00 2001 From: jamesshenry <79054685+jamesshenry@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:07:17 +0000 Subject: [PATCH 11/17] prepare for release --- .build/assets/icon.ico | Bin 4286 -> 15086 bytes .build/assets/icon.png | Bin 41371 -> 7829 bytes LICENSE.md | 21 +++++++++++++++++++++ src/Kuddle.Net/Kuddle.Net.csproj | 23 +++++++++++++++++++++-- 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 LICENSE.md diff --git a/.build/assets/icon.ico b/.build/assets/icon.ico index 60e22b4d9abb2d8bde9d8a869c028efc0db16ef8..fe7b1ff548877b195b5180d55edf710d7e177cd8 100644 GIT binary patch literal 15086 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 literal 41371 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 diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..a56c6b1 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 jamesshenry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/Kuddle.Net/Kuddle.Net.csproj b/src/Kuddle.Net/Kuddle.Net.csproj index b27ab16..883fd4c 100644 --- a/src/Kuddle.Net/Kuddle.Net.csproj +++ b/src/Kuddle.Net/Kuddle.Net.csproj @@ -1,4 +1,4 @@ - + Kuddle Kuddle.Net @@ -8,7 +8,19 @@ net10.0 preview true - + + + jamesshenry + jamesshenry + Kuddle.Net is a .NET implementation of a KDL parser/serializer targeting v2 of the spec. KDL is concise, human-readable language built for configuration and data exchange. + kdl;parser;serializer;configuration + LICENSE.md + https://github.com/jamesshenry/Kuddle.Net + https://github.com/jamesshenry/Kuddle.Net + git + readme.md + icon.png + Initial release of Kuddle.Net KDL parser/serializer. @@ -21,4 +33,11 @@ + + + + + + + From 5e4c0376cbdd284b4343ab2f1bccde1136eaef18 Mon Sep 17 00:00:00 2001 From: jamesshenry <79054685+jamesshenry@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:25:17 +0000 Subject: [PATCH 12/17] tidy up --- docs/serialization-attributes.md | 34 ++--- readme.md | 2 + .../Kuddle.Net.Benchmarks.csproj | 2 +- src/Kuddle.Net.Benchmarks/ParserBenchmarks.cs | 132 +++++++++++++++++- src/Kuddle.Net.Benchmarks/Program.cs | 2 +- .../Attributes/KdlArgumentAttribute.cs | 17 --- .../Attributes/KdlEntryAttribute.cs | 20 +++ .../Attributes/KdlPropertyAttribute.cs | 10 -- .../Attributes/KdlTypeAttribute.cs | 13 ++ 9 files changed, 185 insertions(+), 47 deletions(-) create mode 100644 src/Kuddle.Net/Serialization/Attributes/KdlEntryAttribute.cs create mode 100644 src/Kuddle.Net/Serialization/Attributes/KdlTypeAttribute.cs diff --git a/docs/serialization-attributes.md b/docs/serialization-attributes.md index 44fe741..9b9a544 100644 --- a/docs/serialization-attributes.md +++ b/docs/serialization-attributes.md @@ -1,16 +1,16 @@ -# KDL Serialization Attribute Guide +# Attribute Usage This document describes how to use Kuddle's serialization attributes to map between KDL documents and C# types. ## Quick Reference -| Attribute | Target | Purpose | -|-----------|--------|---------| -| `[KdlType]` | Class | Override the expected node name | -| `[KdlArgument(index)]` | Property | Map to a positional argument | -| `[KdlProperty(key?)]` | Property | Map to a named property | -| `[KdlNode(name?)]` | Property | Map to child nodes | -| `[KdlIgnore]` | Property | Exclude from serialization | +| Attribute | Target | Purpose | +| ---------------------- | -------- | ------------------------------- | +| `[KdlType]` | Class | Override the expected node name | +| `[KdlArgument(index)]` | Property | Map to a positional argument | +| `[KdlProperty(key?)]` | Property | Map to a named property | +| `[KdlNode(name?)]` | Property | Map to child nodes | +| `[KdlIgnore]` | Property | Exclude from serialization | --- @@ -267,15 +267,15 @@ public class User ### Automatic Type Mapping -| C# Type | KDL Representation | -|---------|-------------------| -| `string` | `"value"` or `bare-string` | -| `int`, `long` | `123`, `0xFF`, `0o77`, `0b1010` | -| `double`, `decimal` | `3.14`, `1.5e-10` | -| `bool` | `#true`, `#false` | -| `Guid` | `(uuid)"550e8400-..."` | -| `DateTimeOffset` | `(date-time)"2024-01-15T10:30:00Z"` | -| Nullable `T?` | Value or `#null` | +| C# Type | KDL Representation | +| ------------------- | ----------------------------------- | +| `string` | `"value"` or `bare-string` | +| `int`, `long` | `123`, `0xFF`, `0o77`, `0b1010` | +| `double`, `decimal` | `3.14`, `1.5e-10` | +| `bool` | `#true`, `#false` | +| `Guid` | `(uuid)"550e8400-..."` | +| `DateTimeOffset` | `(date-time)"2024-01-15T10:30:00Z"` | +| Nullable `T?` | Value or `#null` | ### Type Annotations diff --git a/readme.md b/readme.md index df52e51..98a21f3 100644 --- a/readme.md +++ b/readme.md @@ -25,3 +25,5 @@ KDL differs from other configuration languages like yaml or toml in that it is node-based. The top-level document can consist of a collection of nodes, not args (e.g. `#true`, `#false`, `0xAF`) or properties (`key=#false`). To adhere to this, the serialization API depends on the use of several provided attributes to facilitate mapping `KDL` constructs to user defined types: ### Attributes + +[how to use Kdl attributes](docs/serialization-attributes.md) diff --git a/src/Kuddle.Net.Benchmarks/Kuddle.Net.Benchmarks.csproj b/src/Kuddle.Net.Benchmarks/Kuddle.Net.Benchmarks.csproj index 3ad6a86..29658c3 100644 --- a/src/Kuddle.Net.Benchmarks/Kuddle.Net.Benchmarks.csproj +++ b/src/Kuddle.Net.Benchmarks/Kuddle.Net.Benchmarks.csproj @@ -5,7 +5,7 @@ net10.0 preview enable - disable + enable true diff --git a/src/Kuddle.Net.Benchmarks/ParserBenchmarks.cs b/src/Kuddle.Net.Benchmarks/ParserBenchmarks.cs index 5510673..03d2196 100644 --- a/src/Kuddle.Net.Benchmarks/ParserBenchmarks.cs +++ b/src/Kuddle.Net.Benchmarks/ParserBenchmarks.cs @@ -2,13 +2,14 @@ using BenchmarkDotNet.Jobs; using Kuddle.AST; using Kuddle.Parser; +using Kuddle.Serialization; using Parlot.Fluent; namespace Kuddle.Net.Benchmarks; [SimpleJob(RuntimeMoniker.Net10_0)] [MemoryDiagnoser] -public class ParserBenchmarks +public class SerializerBenchmarks { private string _simpleDocument = string.Empty; private string _complexDocument = string.Empty; @@ -17,6 +18,16 @@ public class ParserBenchmarks private Parser _compiledParser = null!; private Parser _nonCompiledParser = null!; + // Serialization benchmarks + private Package _simplePackage = null!; + private string _simplePackageKdl = string.Empty; + + private Project _complexProject = null!; + private string _complexProjectKdl = string.Empty; + + private List _largePackageList = null!; + private string _largePackageListKdl = string.Empty; + [GlobalSetup] public void Setup() { @@ -64,6 +75,47 @@ port 8080 _compiledParser = KdlGrammar.Document.Compile(); _nonCompiledParser = KdlGrammar.Document; + + // Setup serialization objects + _simplePackage = new Package + { + Name = "my-lib", + Version = "1.0.0", + Description = "A library", + }; + _simplePackageKdl = KdlSerializer.Serialize(_simplePackage); + + _complexProject = new Project + { + Name = "my-app", + Version = "2.0.0", + Dependencies = + [ + new Dependency { Package = "lodash", Version = "4.17.21" }, + new Dependency { Package = "react", Version = "18.0.0" }, + new Dependency { Package = "typescript", Version = "4.5.0" }, + ], + DevDependencies = + [ + new Dependency { Package = "jest", Version = "27.0.0" }, + new Dependency { Package = "eslint", Version = "8.0.0" }, + ], + }; + _complexProjectKdl = KdlSerializer.Serialize(_complexProject); + + _largePackageList = []; + for (int i = 0; i < 100; i++) + { + _largePackageList.Add( + new Package + { + Name = $"package{i}", + Version = $"1.{i}.0", + Description = $"Description for package {i}", + } + ); + } + _largePackageListKdl = KdlSerializer.SerializeMany(_largePackageList); } [Benchmark] @@ -101,4 +153,82 @@ port 8080 { return _compiledParser.Parse(_largeDocument); } + + // Serialization benchmarks + [Benchmark] + public string SerializeSimplePackage() + { + return KdlSerializer.Serialize(_simplePackage); + } + + [Benchmark] + public string SerializeComplexProject() + { + return KdlSerializer.Serialize(_complexProject); + } + + [Benchmark] + public string SerializeLargePackageList() + { + return KdlSerializer.SerializeMany(_largePackageList); + } + + // Deserialization benchmarks + [Benchmark] + public Package? DeserializeSimplePackage() + { + return KdlSerializer.Deserialize(_simplePackageKdl); + } + + [Benchmark] + public Project? DeserializeComplexProject() + { + return KdlSerializer.Deserialize(_complexProjectKdl); + } + + [Benchmark] + public List? DeserializeLargePackageList() + { + return KdlSerializer.DeserializeMany(_largePackageListKdl).ToList(); + } + + // Test models + public class Package + { + [KdlArgument(0)] + public string Name { get; set; } = string.Empty; + + [KdlProperty("version")] + public string? Version { get; set; } + + [KdlProperty("description")] + public string? Description { get; set; } + } + + public class Project + { + [KdlArgument(0)] + public string Name { get; set; } = string.Empty; + + [KdlProperty("version")] + public string Version { get; set; } = "1.0.0"; + + [KdlNode("dependency")] + public List Dependencies { get; set; } = []; + + [KdlNode("devDependency")] + public List DevDependencies { get; set; } = []; + } + + public class Dependency + { + [KdlArgument(0)] + public string Package { get; set; } = string.Empty; + + [KdlProperty("version")] + public string Version { get; set; } = "*"; + + [KdlProperty("optional")] + public bool Optional { get; set; } + } } diff --git a/src/Kuddle.Net.Benchmarks/Program.cs b/src/Kuddle.Net.Benchmarks/Program.cs index 1d8ef97..6901321 100644 --- a/src/Kuddle.Net.Benchmarks/Program.cs +++ b/src/Kuddle.Net.Benchmarks/Program.cs @@ -1,4 +1,4 @@ using BenchmarkDotNet.Running; using Kuddle.Net.Benchmarks; -BenchmarkRunner.Run(); +BenchmarkRunner.Run(); diff --git a/src/Kuddle.Net/Serialization/Attributes/KdlArgumentAttribute.cs b/src/Kuddle.Net/Serialization/Attributes/KdlArgumentAttribute.cs index d917eee..988719a 100644 --- a/src/Kuddle.Net/Serialization/Attributes/KdlArgumentAttribute.cs +++ b/src/Kuddle.Net/Serialization/Attributes/KdlArgumentAttribute.cs @@ -8,20 +8,3 @@ public sealed class KdlArgumentAttribute(int index, string? typeAnnotation = nul { public int Index { get; } = index; } - -/// -/// Base class for KDL entry attributes. Only one entry attribute can be applied per property. -/// -[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] -public abstract class KdlEntryAttribute : Attribute -{ - /// - /// Optional KDL type annotation (e.g., "uuid", "date-time", "i32"). - /// - public string? TypeAnnotation { get; } - - protected KdlEntryAttribute(string? typeAnnotation = null) - { - TypeAnnotation = typeAnnotation; - } -} diff --git a/src/Kuddle.Net/Serialization/Attributes/KdlEntryAttribute.cs b/src/Kuddle.Net/Serialization/Attributes/KdlEntryAttribute.cs new file mode 100644 index 0000000..27f0dba --- /dev/null +++ b/src/Kuddle.Net/Serialization/Attributes/KdlEntryAttribute.cs @@ -0,0 +1,20 @@ +using System; + +namespace Kuddle.Serialization; + +/// +/// Base class for KDL entry attributes. Only one entry attribute can be applied per property. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +public abstract class KdlEntryAttribute : Attribute +{ + /// + /// Optional KDL type annotation (e.g., "uuid", "date-time", "i32"). + /// + public string? TypeAnnotation { get; } + + protected KdlEntryAttribute(string? typeAnnotation = null) + { + TypeAnnotation = typeAnnotation; + } +} diff --git a/src/Kuddle.Net/Serialization/Attributes/KdlPropertyAttribute.cs b/src/Kuddle.Net/Serialization/Attributes/KdlPropertyAttribute.cs index 86bd0d5..916208e 100644 --- a/src/Kuddle.Net/Serialization/Attributes/KdlPropertyAttribute.cs +++ b/src/Kuddle.Net/Serialization/Attributes/KdlPropertyAttribute.cs @@ -8,13 +8,3 @@ public sealed class KdlPropertyAttribute(string? key = null, string? typeAnnotat { public string? Key { get; } = key; } - -[AttributeUsage( - AttributeTargets.Class | AttributeTargets.Property, - AllowMultiple = false, - Inherited = false -)] -public sealed class KdlTypeAttribute(string name) : Attribute -{ - public string Name { get; set; } = name; -} diff --git a/src/Kuddle.Net/Serialization/Attributes/KdlTypeAttribute.cs b/src/Kuddle.Net/Serialization/Attributes/KdlTypeAttribute.cs new file mode 100644 index 0000000..535030c --- /dev/null +++ b/src/Kuddle.Net/Serialization/Attributes/KdlTypeAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace Kuddle.Serialization; + +[AttributeUsage( + AttributeTargets.Class | AttributeTargets.Property, + AllowMultiple = false, + Inherited = false +)] +public sealed class KdlTypeAttribute(string name) : Attribute +{ + public string Name { get; set; } = name; +} From 5fccee8ea460c558d9eb2c6901fb1e1e9160f0cd Mon Sep 17 00:00:00 2001 From: jamesshenry <79054685+jamesshenry@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:33:09 +0000 Subject: [PATCH 13/17] tidy up --- src/Kuddle.Net/Extensions/SpanExtensions.cs | 10 ------- src/Kuddle.Net/Parser/KdlGrammar.cs | 21 +++------------ .../Parser/MultiLineStringParser.cs | 26 +------------------ src/Kuddle.Net/Parser/RawStringParser.cs | 21 +-------------- src/Kuddle.Net/Serialization/KdlReader.cs | 13 ---------- 5 files changed, 5 insertions(+), 86 deletions(-) diff --git a/src/Kuddle.Net/Extensions/SpanExtensions.cs b/src/Kuddle.Net/Extensions/SpanExtensions.cs index 80b6f2a..367fe66 100644 --- a/src/Kuddle.Net/Extensions/SpanExtensions.cs +++ b/src/Kuddle.Net/Extensions/SpanExtensions.cs @@ -6,16 +6,6 @@ public static class SpanExtensions { extension(ReadOnlySpan span) { - // public bool Any(Func predicate) - // { - // foreach (var item in span) - // { - // if (predicate(item)) - // return true; - // } - // return false; - // } - public int MaxConsecutive(T target) { int max = 0; diff --git a/src/Kuddle.Net/Parser/KdlGrammar.cs b/src/Kuddle.Net/Parser/KdlGrammar.cs index 7d7d951..cc260f7 100644 --- a/src/Kuddle.Net/Parser/KdlGrammar.cs +++ b/src/Kuddle.Net/Parser/KdlGrammar.cs @@ -136,14 +136,13 @@ static KdlGrammar() ) .Then(s => new TextSpan(s)) ); - // 1. Define a parser for Surrogate Pairs (valid emojis/symbols outside BMP) + var surrogatePair = Capture( Literals .Pattern(char.IsHighSurrogate, 1, 1) .And(Literals.Pattern(char.IsLowSurrogate, 1, 1)) ); - // 2. Define the standard character parser (checks for disallowed chars like lone surrogates) var singleChar = Literals.Pattern( c => c != '\\' && c != '"' && !IsDisallowedLiteralCodePoint(c), 1, @@ -157,20 +156,16 @@ static KdlGrammar() var singleLineStringBody = ZeroOrMany(StringCharacter) .Then(x => { - // Fast path: no characters means empty string if (x.Count == 0) return new TextSpan(string.Empty); - // Fast path: single span can be returned directly if (x.Count == 1) return new TextSpan(x[0].Span.ToString()); - // Calculate total length to avoid StringBuilder resizing int totalLength = 0; for (int i = 0; i < x.Count; i++) totalLength += x[i].Length; - // Use string.Create for efficient concatenation return new TextSpan( string.Create( totalLength, @@ -265,7 +260,6 @@ static KdlGrammar() .Then( (context, span) => { - // Use span-based comparison to avoid allocation for valid identifiers return IsReservedKeyword(span.Span) ? throw new ParseException( $"The keyword '{span}' cannot be used as an unquoted identifier. Wrap it in quotes: \"{span}\".", @@ -313,9 +307,7 @@ static KdlGrammar() .AndSkip(Literals.Text("0b")) .And(Literals.Char('0').Or(Literals.Char('1'))) .And(ZeroOrMany(Literals.Pattern(c => c == '_' || IsBinaryChar(c)))) - ) - // .When((context, x) => x.Span[^1] != '_') - ; + ); Boolean = Literals .Text("#true") .Or(Literals.Text("#false")) @@ -337,7 +329,6 @@ static KdlGrammar() Number = OneOf(KeywordNumber, Hex, Octal, Binary, Decimal) .Then((context, value) => new KdlNumber(value.Span.ToString())); - // Comments var multiLineComment = Deferred(); var openComment = Literals.Text("/*"); @@ -358,7 +349,6 @@ static KdlGrammar() var lineSpace = Deferred(); SlashDash = Capture(Literals.Text("/-").And(ZeroOrMany(lineSpace))).Debug("SlashDash"); - // Whitespace Ws = Literals .Pattern(c => CharacterSets.IsWhiteSpace(c), minSize: 1, maxSize: 1) .Or(MultiLineComment) @@ -379,8 +369,6 @@ static KdlGrammar() lineSpace.Parser = NodeSpace.Or(singleNewLine).Or(SingleLineComment); LineSpace = lineSpace; - // Entries - Type = Between( Literals.Char('('), ZeroOrMany(NodeSpace).SkipAnd(String).AndSkip(ZeroOrMany(NodeSpace)), @@ -468,7 +456,7 @@ static KdlGrammar() { if (result.Item1.HasValue) return null; - // Filter skipped entries - use single pass allocation + var entries = result.Item4; var hasSkipped = false; for (int i = 0; i < entries.Count; i++) @@ -491,7 +479,6 @@ static KdlGrammar() } else { - // No skipped entries - just copy to list filteredEntries = new List(entries.Count); for (int i = 0; i < entries.Count; i++) filteredEntries.Add(entries[i]); @@ -526,14 +513,12 @@ x.Item1 is null .AndSkip(ZeroOrMany(LineSpace)) .Then(list => { - // Single-pass filter to avoid Where().Select().ToList() triple allocation List? filtered = null; for (int i = 0; i < list.Count; i++) { var node = list[i]; if (node is null) { - // First null found - need to build filtered list if (filtered is null) { filtered = new List(list.Count); diff --git a/src/Kuddle.Net/Parser/MultiLineStringParser.cs b/src/Kuddle.Net/Parser/MultiLineStringParser.cs index 962723c..8968089 100644 --- a/src/Kuddle.Net/Parser/MultiLineStringParser.cs +++ b/src/Kuddle.Net/Parser/MultiLineStringParser.cs @@ -57,7 +57,6 @@ public override bool Parse(ParseContext context, ref ParseResult resu int matchIndex = searchOffset + relativeIndex; - // Count preceding backslashes int backslashCount = 0; int backScan = matchIndex - 1; @@ -90,15 +89,12 @@ private static KdlString ProcessMultiLineString( ParseContext context ) { - // Fast path: empty content if (rawInput.IsEmpty) return new KdlString(string.Empty, StringKind.MultiLine); - // Check what processing we need bool hasCR = rawInput.Contains('\r'); bool hasBackslash = rawInput.Contains('\\'); - // If no special characters, we can take a faster path ReadOnlySpan normalized; string? workingString = null; @@ -112,14 +108,12 @@ ParseContext context normalized = rawInput; } - // Handle whitespace escapes if present if (hasBackslash) { workingString = ResolveWsEscapes(workingString ?? normalized.ToString()); normalized = workingString.AsSpan(); } - // Find the last newline to extract the prefix (indentation) int lastNewLine = normalized.LastIndexOf('\n'); ReadOnlySpan prefix; @@ -136,7 +130,6 @@ ParseContext context contentBody = ReadOnlySpan.Empty; } - // Validate prefix contains only whitespace foreach (char c in prefix) { if (!CharacterSets.IsWhiteSpace(c)) @@ -148,10 +141,8 @@ ParseContext context } } - // Build dedented string string dedented = BuildDedentedString(contentBody, prefix, context); - // Unescape if needed string finalValue = hasBackslash ? UnescapeStandardKdl(dedented) : dedented; return new KdlString(finalValue, StringKind.MultiLine); @@ -159,7 +150,6 @@ ParseContext context private static string NormalizeNewlines(ReadOnlySpan input) { - // Count output size first int outputLength = 0; for (int i = 0; i < input.Length; i++) { @@ -207,12 +197,10 @@ ParseContext context if (contentBody.IsEmpty) return string.Empty; - // Skip leading newline int startPos = 0; if (contentBody[0] == '\n') startPos = 1; - // Fast path: no prefix means no dedentation needed if (prefix.IsEmpty) { var body = contentBody.Slice(startPos); @@ -221,7 +209,6 @@ ParseContext context return body.ToString(); } - // First pass: calculate output length and validate int outputLength = 0; int pos = startPos; @@ -256,14 +243,12 @@ ParseContext context pos = nextNewLine + 1; } - // Remove trailing newline if (outputLength > 0) outputLength--; if (outputLength <= 0) return string.Empty; - // Second pass: build string string prefixStr = prefix.ToString(); string contentStr = contentBody.ToString(); @@ -326,12 +311,10 @@ private static bool IsWhitespaceOnlyLine(ReadOnlySpan line) private static string ResolveWsEscapes(string input) { - // Fast path: no backslashes int backslashIdx = input.IndexOf('\\'); if (backslashIdx == -1) return input; - // Check if any backslash is followed by whitespace/newline (ws-escape pattern) bool hasWsEscape = false; for (int i = backslashIdx; i < input.Length - 1; i++) { @@ -349,8 +332,6 @@ private static string ResolveWsEscapes(string input) if (!hasWsEscape) return input; - // Need to process ws-escapes - // First pass: calculate output length int outputLength = 0; for (int i = 0; i < input.Length; i++) { @@ -358,15 +339,13 @@ private static string ResolveWsEscapes(string input) { int scanIdx = i + 1; - // Skip whitespace while (scanIdx < input.Length && (input[scanIdx] == ' ' || input[scanIdx] == '\t')) scanIdx++; - // Check for newline if (scanIdx < input.Length && input[scanIdx] == '\n') { scanIdx++; - // Skip whitespace after newline + while ( scanIdx < input.Length && (input[scanIdx] == ' ' || input[scanIdx] == '\t') ) @@ -377,7 +356,6 @@ private static string ResolveWsEscapes(string input) } else if (scanIdx >= input.Length && scanIdx > i + 1) { - // Trailing ws-escape i = scanIdx - 1; continue; } @@ -428,12 +406,10 @@ private static string ResolveWsEscapes(string input) private static string UnescapeStandardKdl(string input) { - // Fast path: no backslashes int backslashIdx = input.IndexOf('\\'); if (backslashIdx == -1) return input; - // First pass: calculate output length int outputLength = 0; for (int i = 0; i < input.Length; i++) { diff --git a/src/Kuddle.Net/Parser/RawStringParser.cs b/src/Kuddle.Net/Parser/RawStringParser.cs index 961b039..3860a54 100644 --- a/src/Kuddle.Net/Parser/RawStringParser.cs +++ b/src/Kuddle.Net/Parser/RawStringParser.cs @@ -18,7 +18,6 @@ public override bool Parse(ParseContext context, ref ParseResult resu var startPos = cursor.Position; int currentOffset = startPos.Offset; - // Count opening hashes int hashCount = 0; while (currentOffset < bufferSpan.Length && bufferSpan[currentOffset] == '#') { @@ -26,7 +25,6 @@ public override bool Parse(ParseContext context, ref ParseResult resu currentOffset++; } - // Count opening quotes int quoteCount = 0; while (currentOffset < bufferSpan.Length && bufferSpan[currentOffset] == '"') { @@ -48,7 +46,6 @@ public override bool Parse(ParseContext context, ref ParseResult resu bool isMultiline = quoteCount == 3; - // Build closing delimiter: quotes followed by hashes int needleLength = quoteCount + hashCount; Span needle = needleLength <= 256 ? stackalloc char[needleLength] : new char[needleLength]; @@ -70,7 +67,6 @@ public override bool Parse(ParseContext context, ref ParseResult resu var contentSpan = remainingBuffer.Slice(0, matchIndex); int totalLengthParsed = (currentOffset - startPos.Offset) + matchIndex + needleLength; - // Advance cursor for (int i = 0; i < totalLengthParsed; i++) cursor.Advance(); @@ -84,7 +80,6 @@ public override bool Parse(ParseContext context, ref ParseResult resu } else { - // Raw single-line strings have no escape processing content = contentSpan.ToString(); style = StringKind.Quoted | StringKind.Raw; } @@ -99,11 +94,9 @@ public override bool Parse(ParseContext context, ref ParseResult resu /// public static string ProcessMultiLineRawString(ReadOnlySpan rawInput) { - // Fast path: empty content if (rawInput.IsEmpty) return string.Empty; - // Check if we need newline normalization bool hasCR = rawInput.Contains('\r'); ReadOnlySpan normalized; @@ -111,7 +104,6 @@ public static string ProcessMultiLineRawString(ReadOnlySpan rawInput) if (hasCR) { - // Need to normalize - allocate once normalizedString = NormalizeNewlines(rawInput); normalized = normalizedString.AsSpan(); } @@ -120,7 +112,6 @@ public static string ProcessMultiLineRawString(ReadOnlySpan rawInput) normalized = rawInput; } - // Find the last newline to extract the prefix (indentation) int lastNewLine = normalized.LastIndexOf('\n'); ReadOnlySpan prefix; @@ -137,7 +128,6 @@ public static string ProcessMultiLineRawString(ReadOnlySpan rawInput) contentBody = ReadOnlySpan.Empty; } - // Validate prefix contains only whitespace foreach (char c in prefix) { if (c != ' ' && c != '\t') @@ -149,32 +139,27 @@ public static string ProcessMultiLineRawString(ReadOnlySpan rawInput) } } - // If no content body, return empty if (contentBody.IsEmpty) return string.Empty; - // Skip leading newline int pos = 0; if (contentBody[0] == '\n') pos = 1; - // Fast path: no prefix means no dedentation needed if (prefix.IsEmpty) { var body = contentBody.Slice(pos); - // Remove trailing newline if present + if (body.Length > 0 && body[^1] == '\n') body = body.Slice(0, body.Length - 1); return body.ToString(); } - // Need to process with dedentation return BuildDedentedString(contentBody, pos, prefix); } private static string NormalizeNewlines(ReadOnlySpan input) { - // Count output size to avoid StringBuilder resizing int outputLength = 0; for (int i = 0; i < input.Length; i++) { @@ -219,7 +204,6 @@ private static string BuildDedentedString( ReadOnlySpan prefix ) { - // First pass: calculate output length and validate int outputLength = 0; int pos = startPos; @@ -254,14 +238,12 @@ ReadOnlySpan prefix pos = nextNewLine + 1; } - // Remove trailing newline if (outputLength > 0) outputLength--; if (outputLength <= 0) return string.Empty; - // Second pass: build the string string prefixStr = prefix.ToString(); string contentStr = contentBody.ToString(); @@ -314,7 +296,6 @@ ReadOnlySpan prefix private static bool IsWhitespaceOnlyLine(ReadOnlySpan line) { - // Check all chars except the trailing newline for (int i = 0; i < line.Length - 1; i++) { if (!char.IsWhiteSpace(line[i])) diff --git a/src/Kuddle.Net/Serialization/KdlReader.cs b/src/Kuddle.Net/Serialization/KdlReader.cs index a465655..a221251 100644 --- a/src/Kuddle.Net/Serialization/KdlReader.cs +++ b/src/Kuddle.Net/Serialization/KdlReader.cs @@ -44,17 +44,4 @@ public static KdlDocument Read(string text, KdlReaderOptions? options = null) return doc; } - - // /// - // /// Reads a stream assuming UTF-8 encoding. - // /// - // public static async Task ReadAsync( - // Stream stream, - // KuddleReaderOptions? options = null - // ) - // { - // using var reader = new StreamReader(stream); - // var text = await reader.ReadToEndAsync(); - // return await ReadASync(text, options); - // } } From f915af7d01ddf36e74672f8ca4a1140cc71aef8b Mon Sep 17 00:00:00 2001 From: jamesshenry <79054685+jamesshenry@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:45:12 +0000 Subject: [PATCH 14/17] rm todo --- todo.md | 86 --------------------------------------------------------- 1 file changed, 86 deletions(-) delete mode 100644 todo.md diff --git a/todo.md b/todo.md deleted file mode 100644 index 23f7452..0000000 --- a/todo.md +++ /dev/null @@ -1,86 +0,0 @@ -Here is your updated roadmap. - -You have effectively **completed Phase 1** (AST) and the heavy lifting of **Phase 2** (The Grammar/Parser Logic). - -### Phase 1: The High-Fidelity DOM (AST) ✅ - -*Goal: Define immutable data structures that represent the physical text of a KDL file.* - -* **1.1 Define the Base Object** - * [x] Create `KdlObject` with Trivia support. -* **1.2 Implement Semantic Identifiers** - * [x] Create `KdlIdentifier` (supports RawText and Type Annotations). -* **1.3 Implement Value Primitives** - * [x] `KdlValue` base class. - * [x] `KdlString` (Quoted, Raw, Multiline), `KdlBool`, `KdlNull`. - * [x] `KdlNumber` (with lazy parsing logic). -* **1.4 Implement Node Structure** - * [x] Create `KdlEntry` hierarchy (Argument, Property, SkippedEntry). - * [x] Create `KdlNode` (Name, Entries List, Children Block). -* **1.5 Implement Containers** - * [x] Create `KdlBlock` and `KdlDocument`. - -### Phase 2: The Parser Integration 🚧 - -*Goal: Build the engine that populates the AST while enforcing the KDL spec.* - -* **2.1 Tokenizer & Grammar** - * [x] Implement `KuddleGrammar` using Parlot. - * [x] Implement complex String parsing (Raw, Multiline, Escapes). - * [x] Implement Number parsing (Hex, Binary, Octal). -* **2.2 Structural Parsing** - * [x] Implement Node parsing (Name, Type, properties). - * [x] Implement Recursion (Node -> Children -> Nodes). - * [x] Implement "Slash-Dash" `/-` logic for skipping nodes/entries/children. -* **2.3 API Wrapper** - * [x] Create the public static `KdlParser.Parse(string input)` method that invokes `KuddleGrammar.Document.Parse(...)`. - * [x] Add `KdlParseException` wrapping Parlot errors with line/column info. -* **2.4 Reserved Type Validation** - * [x] **(New)** Add a post-parse visitor or validation pass to ensure `(u8)` values fit in bytes, `(uuid)` are valid GUIDs, etc. - -### Phase 3: The Developer Experience (DX) & Extensions - -*Goal: Provide a usable API without polluting the pure AST.* - -* **3.1 Value Extensions (The "TryGet" Pattern)** - * [x] Create `KdlValueExtensions`. - * [x] Implement `TryGetUuid`, `TryGetDateTime`, `TryGetInt`, etc. -* **3.2 Mutation Factories** - * [x] Add `KdlValue.From(Guid)`, `KdlValue.From(int)` helpers. -* **3.3 Navigation Helpers** - * [x] Add indexers: `node["propName"]`. - * [x] Add `node.Arguments` (computed view). - -### Phase 4: Serialization - -*Goal: Output KDL from the AST.* - -* **4.1 Round-Trip Writer** - * [x] Create `KdlWriter` implementation. - * [ ] Logic: Iterate AST, write `LeadingTrivia`, `TypeAnnotation`, `RawText`, `TrailingTrivia`. -* **4.2 Formatting Writer** - * [x] Create `KdlFormatter` (or options for `KdlWriter`). - * [ ] Logic to ignore stored trivia and re-indent based on settings. - -### Phase 5: Verification 🚧 - -*Goal: Ensure correctness.* - -* **5.1 Unit Tests** - * [x] **(In Progress)** AST Structural Tests. - * [x] Grammar Logic Tests (Strings, Numbers). -* **5.2 Integration Tests** - * [x] Run against the official `kdl-org/kdl` test suite. - -### Phase 6: Object Mapping (POCOs) & Source Gen - -*Goal: Map AST to strong C# types.* - -* **6.1 The "Sidecar" Source Generator** - * [ ] Create `[KdlAnnotate]` attribute. - * [ ] Generate partial classes for metadata storage. -* **6.2 The Mapper** - * [ ] Implement `KdlSerializer.Deserialize()`. - * [ ] Map Nodes to Classes, Entries to Properties. -* **6.3 Polymorphism** - * [ ] Implement Discriminator logic using Node Annotations. From 9480610cf57c27f9de82a941336a201bbffb62b1 Mon Sep 17 00:00:00 2001 From: jamesshenry <79054685+jamesshenry@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:45:24 +0000 Subject: [PATCH 15/17] rm spec --- spec.md | 1112 ------------------------------------------------------- 1 file changed, 1112 deletions(-) delete mode 100644 spec.md diff --git a/spec.md b/spec.md deleted file mode 100644 index c34e33f..0000000 --- a/spec.md +++ /dev/null @@ -1,1112 +0,0 @@ ---- -title: "The KDL Document Language" -abbrev: "KDL" -docname: draft-marchan-kdl2-latest -submissionType: independent -category: exp - -ipr: none -area: General -venue: - github: kdl-org/kdl - home: -workgroup: KDL Community -keyword: - -- Document-Language -- Configuration - -stand_alone: yes -smart_quotes: no -pi: [toc, sortrefs, symrefs] - -author: - -- name: Katerina Zoé Marchán Salvá - ins: K. Marchán - organization: Microsoft -- name: The KDL Contributors - ins: KDL Contributors - -normative: - -informative: - ---- abstract - -KDL is a node-oriented document language. Its niche and purpose overlaps with -XML, and as do many of its semantics. You can use KDL both as a configuration -language, and a data exchange or storage format, if you so choose. - -This is the formal specification for KDL, including the intended data model and -the grammar. - -This document describes an unreleased minor change to KDL. For the latest -oficial version of the language, see . - - - ---- note_License - -This work is licensed under Creative Commons Attribution-ShareAlike 4.0 -International. To view a copy of this license, visit - - ---- middle - -# Compatibility - -KDL 2.0 is designed such that for any given KDL document written as [KDL -1.0](./SPEC_v1.md) or KDL 2.0, the parse will either fail completely, or, if the -parse succeeds, the data represented by a v1 or v2 parser will be identical. -This means that it's safe to use a fallback parsing strategy in order to support -both v1 and v2 simultaneously. For example, `node "foo"` is a valid node in both -versions, and should be represented identically by parsers. - -A version marker `/- kdl-version 2` (or `1`) _MAY_ be added to the beginning of -a KDL document, optionally preceded by the BOM, and parsers _MAY_ use that as a -hint as to which version to parse the document as. - -# Introduction - -KDL is a node-oriented document language. Its niche and purpose overlaps with -XML, and as do many of its semantics. You can use KDL both as a configuration -language, and a data exchange or storage format, if you so choose. - -The bulk of this document is dedicated to a long-form description of all -Components ({{components}}) of a KDL document. -There is also a much more terse -Grammar ({{full-grammar}}) at the end of the document that covers most of the -rules, with some semantic exceptions involving the data model. - -KDL is designed to be easy to read _and_ easy to implement. - -In this document, references to "left" or "right" refer to directions in the -_data stream_ towards the beginning or end, respectively; in other words, -the directions if the data stream were only ASCII text. They do not refer -to the writing direction of text, which can flow in either direction, -depending on the characters used. - -# Components - -## Document - -The toplevel concept of KDL is a Document. A Document is composed of zero or -more Nodes ({{node}}), separated by newlines, semicolons, and whitespace, and eventually -terminated by an EOF. - -All KDL documents MUST be encoded in UTF-8 and conform to the specifications in -this document. - -### Example - -The following is a document composed of two toplevel nodes: - -~~~kdl -foo { - bar -} -baz -~~~ - -## Node - -Being a node-oriented language means that the real core component of any KDL -document is the "node". Every node must have a name, which must be a -String ({{string}}). - -The name may be preceded by a Type Annotation ({{type-annotation}}) to further -clarify its type, particularly in relation to its parent node. (For example, -clarifying that a particular `date` child node is for the _publication_ date, -rather than the last-modified date, with `(published)date`.) - -Following the name are zero or more Arguments ({{argument}}) or -Properties ({{property}}), separated by either whitespace ({{whitespace}}) or a -slash-escaped line continuation ({{line-continuation}}). Arguments and Properties -may be interspersed in any order, much like is common with positional arguments -vs options in command line tools. Collectively, Arguments and Properties may be -referred to as "Entries". - -Children ({{children-block}}) can be placed after the name and the optional -Entries, possibly separated by either whitespace or a -slash-escaped line continuation. - -Arguments are ordered relative to each other and that order must be preserved in -order to maintain the semantics. Properties between Arguments do not affect -Argument ordering. - -By contrast, Properties _SHOULD NOT_ be assumed to be presented in a given -order. Children ({{children-block}}) should be used if an order-sensitive -key/value data structure must be represented in KDL. Cf. JSON objects -preserving key order. - -Nodes _MAY_ be prefixed with Slashdash ({{slashdash-comments}}) to "comment out" -the entire node, including its properties, arguments, and children, and make -it act as plain whitespace, even if it spreads across multiple lines. - -Finally, a node is terminated by either a Newline ({{newline}}), a semicolon -(`;`), the end of its parent's child block (`}`) or the end of the file/stream -(an `EOF`). - -### Example - -~~~kdl -// `foo` will have an Argument value list like `[1, 3]`. -foo 1 key=val 3 { - bar - (role)baz 1 2 -} -~~~ - -## Line Continuation - -Line continuations allow Nodes ({{node}}) to be spread across multiple lines. - -A line continuation is a `\` character followed by zero or more whitespace -items (including multiline comments) and an optional single-line comment. It -must be terminated by a Newline ({{newline}}) (including the Newline that is -part of single-line comments). - -Following a line continuation, processing of a Node can continue as usual. - -### Example - -~~~kdl -my-node 1 2 \ // comments are ok after \ - 3 4 // This is the actual end of the Node. -~~~ - -## Property - -A Property is a key/value pair attached to a Node ({{node}}). A Property is -composed of a String ({{string}}), followed immediately by an equals sign (`=`, `U+003D`), -and then a Value ({{value}}). - -Properties should be interpreted left-to-right, with rightmost properties with -identical names overriding earlier properties. That is: - -~~~kdl -node a=1 a=2 -~~~ - -In this example, the node's `a` value must be `2`, not `1`. - -No other guarantees about order should be expected by implementers. -Deserialized representations may iterate over properties in any order and -still be spec-compliant. - -Properties _MAY_ be prefixed with `/-` to "comment out" the entire token and -make it act as plain whitespace, even if it spreads across multiple lines. - -## Argument - -An Argument is a bare Value ({{value}}) attached to a Node ({{node}}), with no -associated key. It shares the same space as Properties ({{property}}), and may be interleaved with them. - -A Node may have any number of Arguments, which should be evaluated left to -right. KDL implementations _MUST_ preserve the order of Arguments relative to -each other (not counting Properties). - -Arguments _MAY_ be prefixed with `/-` to "comment out" the entire token and -make it act as plain whitespace, even if it spreads across multiple lines. - -### Example - -~~~kdl -my-node 1 2 3 a b c -~~~ - -## Children Block - -A children block is a block of Nodes ({{node}}), surrounded by `{` and `}`. They -are an optional part of nodes, and create a hierarchy of KDL nodes. - -Regular node termination rules apply, which means multiple nodes can be -included in a single-line children block, as long as they're all terminated by -`;`. - -### Example - -~~~kdl -parent { - child1 - child2 -} - -parent { child1; child2 } -~~~ - -## Value - -A value is either: a String ({{string}}), a Number ({{number}}), a -Boolean ({{boolean}}), or Null ({{null}}). - -Values _MUST_ be either Arguments ({{argument}}) or values of -Properties ({{property}}). Only String ({{string}}) values may be used as -Node ({{node}}) names or Property ({{property}}) keys. - -Values (both as arguments and in properties) _MAY_ be prefixed by a single -Type Annotation ({{type-annotation}}). - -## Type Annotation - -A type annotation is a prefix to any Node Name ({{node}}) or Value ({{value}}) that -includes a _suggestion_ of what type the value is _intended_ to be treated as, -or as a _context-specific elaboration_ of the more generic type the node name -indicates. - -Type annotations are written as a set of `(` and `)` with a single -String ({{string}}) in it. It may contain Whitespace after the `(` and before -the `)`, and may be separated from its target by Whitespace. - -KDL does not specify any restrictions on what implementations might do with -these annotations. They are free to ignore them, or use them to make decisions -about how to interpret a value. - -Additionally, the following type annotations MAY be recognized by KDL parsers -and, if used, SHOULD interpret these types as follows: - -### Reserved Type Annotations for Numbers Without Decimals - -Signed integers of various sizes (the number is the bit size): - -- `i8` -- `i16` -- `i32` -- `i64` -- `i128` - -Unsigned integers of various sizes (the number is the bit size): - -- `u8` -- `u16` -- `u32` -- `u64` -- `u128` - -Platform-dependent integer types, both signed and unsigned: - -- `isize` -- `usize` - -### Reserved Type Annotations for Numbers With Decimals - -IEEE 754 floating point numbers, both single (32) and double (64) precision: - -- `f32` -- `f64` - -IEEE 754-2008 decimal floating point numbers - -- `decimal64` -- `decimal128` - -### Reserved Type Annotations for Strings - -- `date-time`: ISO8601 date/time format. -- `time`: "Time" section of ISO8601. -- `date`: "Date" section of ISO8601. -- `duration`: ISO8601 duration format. -- `decimal`: IEEE 754-2008 decimal string format. -- `currency`: ISO 4217 currency code. -- `country-2`: ISO 3166-1 alpha-2 country code. -- `country-3`: ISO 3166-1 alpha-3 country code. -- `country-subdivision`: ISO 3166-2 country subdivision code. -- `email`: RFC5322 email address. -- `idn-email`: RFC6531 internationalized email address. -- `hostname`: RFC1123 internet hostname (only ASCII segments) -- `idn-hostname`: RFC5890 internationalized internet hostname - (only `xn--`-prefixed ASCII "punycode" segments, or non-ASCII segments) -- `ipv4`: RFC2673 dotted-quad IPv4 address. -- `ipv6`: RFC2373 IPv6 address. -- `url`: RFC3986 URI. -- `url-reference`: RFC3986 URI Reference. -- `irl`: RFC3987 Internationalized Resource Identifier. -- `irl-reference`: RFC3987 Internationalized Resource Identifier Reference. -- `url-template`: RFC6570 URI Template. -- `uuid`: RFC4122 UUID. -- `regex`: Regular expression. Specific patterns may be implementation-dependent. -- `base64`: A Base64-encoded string, denoting arbitrary binary data. -- `base85`: An [Ascii85](https://en.wikipedia.org/wiki/Ascii85)-encoded string, denoting arbitrary binary data. - -### Examples - -~~~kdl -node (u8)123 -node prop=(regex).* -(published)date "1970-01-01" -(contributor)person name="Foo McBar" -~~~ - -## String - -Strings in KDL represent textual UTF-8 Values ({{value}}). A String is either an -Identifier String ({{identifier-string}}) (like `foo`), a -Quoted String ({{quoted-string}}) (like `"foo"`) -or a Multi-Line String ({{multi-line-string}}). -Both Quoted and Multiline strings come in normal -and Raw String ({{raw-string}}) variants (like `#"foo"#`): - -- Identifier Strings let you write short, "single-word" strings with a - minimum of syntax -- Quoted Strings let you write strings "like normal", with whitespace and escapes. -- Multi-Line Strings let you write strings across multiple lines - and with indentation that's not part of the string value. -- Raw Strings don't allow any escapes, - allowing you to not worry about the string's content containing anything that - might look like an escape. - -Strings _MUST_ be represented as UTF-8 values. - -Strings _MUST NOT_ include the code points for -disallowed literal code points ({{disallowed-literal-code-points}}) directly. -Quoted and Multi-Line Strings may include these code points as _values_ -by representing them with their corresponding `\u{...}` escape. - -## Identifier String - -An Identifier String (sometimes referred to as just an "identifier") is -composed of any [Unicode Scalar -Value](https://unicode.org/glossary/#unicode_scalar_value) other than -non-initial characters ({{non-initial-characters}}), followed by any number of -Unicode Scalar Values other than non-identifier -characters ({{non-identifier-characters}}). - -A handful of patterns are disallowed, to avoid confusion with other values: - -- idents that appear to start with a Number ({{number}}) (like `1.0v2` or - `-1em`) or the "almost a number" pattern of a decimal point without a - leading digit (like `.1`). -- idents that are the language keywords (`inf`, `-inf`, `nan`, `true`, - `false`, and `null`) without their leading `#`. - -Identifiers that match these patterns _MUST_ be treated as a syntax error; such -values can only be written as quoted or raw strings. The precise details of the -identifier syntax is specified in the Full Grammar in {{full-grammar}}. - -### Non-initial characters - -The following characters cannot be the first character in an -Identifier String ({{identifier-string}}): - -- Any decimal digit (0-9) -- Any non-identifier characters ({{non-identifier-characters}}) - -Additionally, the following initial characters impose limitations on subsequent -characters: - -- the `+` and `-` characters can only be used as an initial character if - the second character is _not_ a digit. If the second character is `.`, then - the third character must _not_ be a digit. -- the `.` character can only be used as an initial character if - the second character is _not_ a digit. - -This allows identifiers to look like `--this` or `.md`, and removes the -ambiguity of having an identifier look like a number. - -### Non-identifier characters - -The following characters cannot be used anywhere in a Identifier String ({{identifier-string}}): - -- Any of `(){}[]/\"#;=` -- Any Whitespace ({{whitespace}}) or Newline ({{newline}}). -- Any disallowed literal code points ({{disallowed-literal-code-points}}) in KDL - documents. - -## Quoted String - -A Quoted String is delimited by `"` on either side of any number of literal -string characters except unescaped `"` and `\`. - -Literal Newline ({{newline}}) characters can only be included -if they are Escaped Whitespace ({{escaped-whitespace}}), -which discards them from the string value. -Actually including a newline in the value requires using a newline escape sequence, -like `\n`, -or using a Multi-Line String ({{multi-line-string}}) -which is actually designed for strings stretching across multiple lines. - -Like Identifier Strings, Quoted Strings _MUST NOT_ include any of the -disallowed literal code-points ({{disallowed-literal-code-points}}) as code -points in their body. - -Quoted Strings have a Raw String ({{raw-string}}) variant, -which disallows escapes. - -### Escapes - -In addition to literal code points, a number of "escapes" are supported in Quoted Strings. -"Escapes" are the character `\` followed by another character, and are -interpreted as described in the following table: - -| Name | Escape | Code Pt | -|-------------------------------|--------|----------| -| Line Feed | `\n` | `U+000A` | -| Carriage Return | `\r` | `U+000D` | -| Character Tabulation (Tab) | `\t` | `U+0009` | -| Reverse Solidus (Backslash) | `\\` | `U+005C` | -| Quotation Mark (Double Quote) | `\"` | `U+0022` | -| Backspace | `\b` | `U+0008` | -| Form Feed | `\f` | `U+000C` | -| Space | `\s` | `U+0020` | -| Unicode Escape | `\u{(1-6 hex chars)}` | Code point described by hex characters, as long as it represents a [Unicode Scalar Value](https://unicode.org/glossary/#unicode_scalar_value) | -| Whitespace Escape | See below | N/A | - -#### Escaped Whitespace - -In addition to escaping individual characters, `\` can also escape whitespace. -When a `\` is followed by one or more literal whitespace characters, the `\` -and all of that whitespace are discarded. For example, - -~~~kdl -"Hello World" -~~~ - -and - -~~~kdl -"Hello \ World" -~~~ - -are semantically identical. See whitespace ({{whitespace}}) -and newlines ({{newline}}) for how whitespace is defined. - -Note that only literal whitespace is escaped; whitespace escapes (`\n` and -such) are retained. For example, these strings are all semantically identical: - -~~~kdl -"Hello\ \nWorld" - - "Hello\n\ - World" - -"Hello\nWorld" - -""" - Hello - World - """ -~~~ - -#### Invalid escapes - -Except as described in the escapes table, above, `\` _MUST NOT_ precede any -other characters in a string. - -## Multi-line String - -Multi-Line Strings support multiple lines with literal, non-escaped -Newlines. They must use a special multi-line syntax, and they automatically -"dedent" the string, allowing its value to be indented to a visually matching -level as desired. - -A Multi-Line String is opened and closed by _three_ double-quote characters, -like `"""`. -Its first line _MUST_ immediately start with a Newline ({{newline}}) -after its opening `"""`. -Its final line _MUST_ contain only whitespace -before the closing `"""`. -All in-between lines that contain non-newline, non-whitespace characters -_MUST_ start with _at least_ the exact same whitespace as the final line -(precisely matching codepoints, not merely counting characters or "size"); -they may contain additional whitespace following this prefix. The lines in -between may contain unescaped `"` (but no unescaped `"""` as this would close -the string). - -The value of the Multi-Line String omits the first and last Newline, the -Whitespace of the last line, and the matching Whitespace prefix on all -intermediate lines. The first and last Newline can be the same character (that -is, empty multi-line strings are legal). - -In other words, the final line specifies the whitespace prefix that will be -removed from all other lines. - -Whitespace-only lines (that is, lines containing only literal whitespace -characters, not including whitespace escapes like `\t`) always represent -empty lines in the string value, regardless of what whitespace they -contain (if any). They do not have to start with the same whitespace prefix -that other lines do; all characters on the line are ignored. - -Multi-line Strings that do not immediately start with a Newline and whose final -`"""` is not preceeded by optional whitespace and a Newline are illegal. This -also means that `"""` may not be used for a single-line String (e.g. -`"""foo"""`). - -### Newline Normalization - -Literal Newline sequences in Multi-line Strings must be normalized to a single -`U+000A` (`LF`) during deserialization. This means, for example, that `CR LF` -becomes a single `LF` during parsing. - -This normalization does not apply to non-literal Newlines entered using escape -sequences. That is: - -~~~kdl -multi-line """ - \r\n[CRLF] - foo[CRLF] - """ -~~~ - -becomes: - -~~~kdl -single-line "\r\n\nfoo" -~~~ - -For clarity: this normalization applies to each individual Newline sequence. -That is, the literal sequence `CRLF CRLF` becomes `LF LF`, not `LF`. - -### Examples - -#### Indented multi-line string - -~~~kdl -multi-line """ - foo - This is the base indentation - bar - """ -~~~ - -This example's string value will be: - -~~~ - foo -This is the base indentation - bar -~~~ - -which is equivalent to - -~~~kdl -" foo\nThis is the base indentation\n bar" -~~~ - -when written as a single-line string. - -#### Shorter last-line indent - -If the last line wasn't indented as far, -it won't dedent the rest of the lines as much: - -~~~kdl -multi-line """ - foo - This is no longer on the left edge - bar - """ -~~~ - -This example's string value will be: - -~~~ - foo - This is no longer on the left edge - bar -~~~ - -Equivalent to - -~~~kdl -" foo\n This is no longer on the left edge\n bar" -~~~ - -#### Empty lines - -Empty lines can contain any whitespace, or none at all, and will be reflected as empty in the value: - -~~~kdl -multi-line """ - Indented a bit - - A second indented paragraph. - """ -~~~ - -This example's string value will be: - -~~~ -Indented a bit. - -A second indented paragraph. -~~~ - -Equivalent to - -~~~kdl -"Indented a bit.\n\nA second indented paragraph." -~~~ - -#### Syntax errors - -The following yield **syntax errors**: - -~~~kdl -multi-line """can't be single line""" -~~~ - -~~~kdl -multi-line """ - closing quote with non-whitespace prefix""" -~~~ - -~~~kdl -multi-line """stuff - """ -~~~ - -~~~kdl -// Every line must share the exact same prefix as the closing line. -multi-line """[\n] -[tab]a[\n] -[space][space]b[\n] -[space][tab][\n] -[tab]""" -~~~ - -### Interaction with Whitespace Escapes - -Multi-line strings support the same mechanism for escaping whitespace as Quoted -Strings. - -When processing a Multi-line String, implementations MUST dedent the string -_after_ resolving all whitespace escapes, but _before_ resolving other backslash -escapes. This means a whitespace escape that attempts to escape the final line's -newline and/or whitespace prefix can be invalid: if removing escaped whitespace -places the closing `"""` on a line with non-whitespace characters, this escape -is invalid. - -For example, the following example is illegal: - -~~~kdl - """ - foo - bar\ - """ - - // equivalent to - """ - foo - bar""" -~~~ - -while the following example is allowed - -~~~kdl - """ - foo \ -bar - baz - \ """ - - // equivalent to - """ - foo bar - baz - """ -~~~ - -## Raw String - -Both Quoted ({{quoted-string}}) and Multi-Line Strings ({{multi-line-string}}) have -Raw String variants, which are identical in syntax except they do not support -`\`-escapes. This includes line-continuation escapes (`\` + `ws` collapsing to -nothing). They otherwise share the same properties as far as literal -Newline ({{newline}}) characters go, multi-line rules, and the requirement of -UTF-8 representation. - -The Raw String variants are indicated by preceding the strings's opening quotes -with one or more `#` characters. The string is then closed by its normal closing -quotes, followed by a _matching_ number of `#` characters. This means that the -string may contain any combination of `"` and `#` characters other than its -closing delimiter (e.g., if a raw string starts with `##"`, it can contain `"` -or `"#`, but not `"##` or `"###`). - -Like other Strings, Raw Strings _MUST NOT_ include any of the disallowed -literal code-points ({{disallowed-literal-code-points}}) as code points in their -body. Unlike with Quoted Strings, these cannot simply be escaped, and are thus -unrepresentable when using Raw Strings. - -### Example - -~~~kdl -just-escapes #"\n will be literal"# -~~~ - -The string contains the literal characters `\n will be literal`. - -~~~kdl -quotes-and-escapes ##"hello\n\r\asd"#world"## -~~~ - -The string contains the literal characters `hello\n\r\asd"#world` - -~~~kdl -raw-multi-line #""" - Here's a """ - multiline string - """ - without escapes. - """# -~~~ - -The string contains the value - -~~~ -Here's a """ - multiline string - """ -without escapes. -~~~ - -or equivalently, - -~~~kdl -"Here's a \"\"\"\n multiline string\n \"\"\"\nwithout escapes." -~~~ - -as a Quoted String. - -## Number - -Numbers in KDL represent numerical Values ({{value}}). There is no logical distinction in KDL -between real numbers, integers, and floating point numbers. It's up to -individual implementations to determine how to represent KDL numbers. - -There are five syntaxes for Numbers: Keywords, Decimal, Hexadecimal, Octal, and Binary. - -- All non-Keyword ({{keyword-numbers}}) numbers may optionally start with one of `-` or `+`, which determine whether they'll be positive or negative. -- Binary numbers start with `0b` and only allow `0` and `1` as digits, which may be separated by `_`. They represent numbers in radix 2. -- Octal numbers start with `0o` and only allow digits between `0` and `7`, which may be separated by `_`. They represent numbers in radix 8. -- Hexadecimal numbers start with `0x` and allow digits between `0` and `9`, as well as letters `A` through `F`, in either lower or upper case, which may be separated by `_`. They represent numbers in radix 16. -- Decimal numbers are a bit more special: - - They have no radix prefix. - - They use digits `0` through `9`, which may be separated by `_`. - - They may optionally include a decimal separator `.`, followed by more digits, which may again be separated by `_`. - - They may optionally be followed by `E` or `e`, an optional `-` or `+`, and more digits, to represent an exponent value. - -In all cases where the above says that digits "may be separated by `_`", -this means that between any two digits, or after the digits, any number of -consecutive `_` characters can appear. Underscores are not allowed _before_ the digits. -That is, `1___2` and `12____` are valid (and both equivalent to just `12`), but -`_12` is _not_ a valid number (it will instead parse as an identifier string), -nor is `0x_1a` (it will simply be invalid). - -Note that, similar to JSON and some other languages, -numbers without an integer digit (such as `.1`) are illegal. -They must be written with at least one integer digit, like `0.1`. -(These patterns are also disallowed from Identifier Strings ({{identifier-string}}), to avoid confusion.) - -### Keyword Numbers - -There are three special "keyword" numbers included in KDL to accomodate the -widespread use of [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754) floats: - -- `#inf` - floating point positive infinity. -- `#-inf` - floating point negative infinity. -- `#nan` - floating point NaN/Not a Number. - -To go along with this and prevent foot guns, the bare Identifier -Strings ({{identifier-string}}) `inf`, `-inf`, and `nan` are considered illegal -identifiers and should yield a syntax error. - -The existence of these keywords does not imply that any numbers be represented -as IEEE 754 floats. These are simply for clarity and convenience for any -implementation that chooses to represent their numbers in this way. - -## Boolean - -A boolean Value ({{value}}) is either the symbol `#true` or `#false`. These -_SHOULD_ be represented by implementation as boolean logical values, or some -approximation thereof. - -### Example - -~~~kdl -my-node #true value=#false -~~~ - -## Null - -The symbol `#null` represents a null Value ({{value}}). It's up to the -implementation to decide how to represent this, but it generally signals the -"absence" of a value. - -### Example - -~~~kdl -my-node #null key=#null -~~~ - -## Whitespace - -The following characters should be treated as non-Newline ({{newline}}) [white -space](https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt): - -| Name | Code Pt | -| ------------------------- | -------- | -| Character Tabulation | `U+0009` | -| Space | `U+0020` | -| No-Break Space | `U+00A0` | -| Ogham Space Mark | `U+1680` | -| En Quad | `U+2000` | -| Em Quad | `U+2001` | -| En Space | `U+2002` | -| Em Space | `U+2003` | -| Three-Per-Em Space | `U+2004` | -| Four-Per-Em Space | `U+2005` | -| Six-Per-Em Space | `U+2006` | -| Figure Space | `U+2007` | -| Punctuation Space | `U+2008` | -| Thin Space | `U+2009` | -| Hair Space | `U+200A` | -| Narrow No-Break Space | `U+202F` | -| Medium Mathematical Space | `U+205F` | -| Ideographic Space | `U+3000` | - -### Single-line comments - -Any text after `//`, until the next literal Newline ({{newline}}) is "commented -out", and is considered to be Whitespace ({{whitespace}}). - -### Multi-line comments - -In addition to single-line comments using `//`, comments can also be started -with `/*` and ended with `*/`. These comments can span multiple lines. They -are allowed in all positions where Whitespace ({{whitespace}}) is allowed and -can be nested. - -### Slashdash comments - -Finally, a special kind of comment called a "slashdash", denoted by `/-`, can -be used to comment out entire _components_ of a KDL document logically, and -have those elements not be included as part of the parsed document data. - -Slashdash comments can be used before the following, including before their type -annotations, if present: - -- A Node ({{node}}): the entire Node is treated as Whitespace, including all - props, args, and children. -- An Argument ({{argument}}): the Argument value is treated as Whitespace. -- A Property ({{property}}) key: the entire property, including both key and value, - is treated as Whitespace. A slashdash of just the property value is not allowed. -- A Children Block ({{children-block}}): the entire block, including all - children within, is treated as Whitespace. Only other children blocks, whether - slashdashed or not, may follow a slashdashed children block. - -A slashdash may be be followed by any amount of whitespace, including newlines and -comments (other than other slashdashes), before the element that it comments out. - -## Newline - -The following character sequences [should be treated as new -lines](https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-5/#G41643): - -| Acronym | Name | Code Pt | -| ------- | ----------------------------- | ------------------- | -| CRLF | Carriage Return and Line Feed | `U+000D` + `U+000A` | -| CR | Carriage Return | `U+000D` | -| LF | Line Feed | `U+000A` | -| NEL | Next Line | `U+0085` | -| VT | Vertical tab | `U+000B` | -| FF | Form Feed | `U+000C` | -| LS | Line Separator | `U+2028` | -| PS | Paragraph Separator | `U+2029` | - -Note that for the purpose of new lines, the specific sequence `CRLF` is -considered _a single newline_. - -## Disallowed Literal Code Points - -The following code points may not appear literally anywhere in the document. -They may be represented in Strings (but not Raw Strings) using Unicode Escapes ({{escapes}}) (`\u{...}`, -except for non Unicode Scalar Value, which can't be represented even as escapes). - -- The codepoints `U+0000-0008` or the codepoints `U+000E-001F` (various - control characters). -- `U+007F` (the Delete control character). -- Any codepoint that is not a [Unicode Scalar - Value](https://unicode.org/glossary/#unicode_scalar_value) (`U+D800-DFFF`). -- `U+200E-200F`, `U+202A-202E`, and `U+2066-2069`, the [unicode - "direction control" - characters](https://www.w3.org/International/questions/qa-bidi-unicode-controls) -- `U+FEFF`, aka Zero-width Non-breaking Space (ZWNBSP)/Byte Order Mark (BOM), - except as the first code point in a document. - -# Full Grammar - -This is the full official grammar for KDL and should be considered -authoritative if something seems to disagree with the text above. The grammar -language syntax is defined in {{grammar-language}}. - -~~~abnf -document := bom? version? nodes - -// Nodes -nodes := (line-space* node)* line-space* - -base-node := slashdash? type? node-space* string - (node-space* (node-space | slashdash) node-prop-or-arg)* - // slashdashed node-children must always be after props and args. - (node-space* slashdash node-children)* - (node-space* node-children)? - (node-space* slashdash node-children)* - node-space* -node := base-node node-terminator -final-node := base-node node-terminator? - -// Entries -node-prop-or-arg := prop | value -node-children := '{' nodes final-node? '}' -node-terminator := single-line-comment | newline | ';' | eof - -prop := string node-space* '=' node-space* value -value := type? node-space* (string | number | keyword) -type := '(' node-space* string node-space* ')' - -// Strings -string := identifier-string | quoted-string | raw-string ¶ - -identifier-string := - (unambiguous-ident | signed-ident | dotted-ident) - - disallowed-keyword-identifiers -unambiguous-ident := - (identifier-char - digit - sign - '.') identifier-char* -signed-ident := - sign ((identifier-char - digit - '.') identifier-char*)? -dotted-ident := - sign? '.' ((identifier-char - digit) identifier-char*)? -identifier-char := - unicode - unicode-space - newline - [\\/(){};\[\]"#=] - - disallowed-literal-code-points -disallowed-keyword-identifiers := - 'true' | 'false' | 'null' | 'inf' | '-inf' | 'nan' - -quoted-string := - '"' single-line-string-body '"' | - '"""' newline - (multi-line-string-body newline)? - (unicode-space | ws-escape)* '"""' -single-line-string-body := (string-character - newline)* -multi-line-string-body := (('"' | '""')? string-character)* -string-character := - '\\' (["\\bfnrts] | - 'u{' hex-unicode '}') | - ws-escape | - [^\\"] - disallowed-literal-code-points -ws-escape := '\\' (unicode-space | newline)+ -hex-digit := [0-9a-fA-F] -hex-unicode := hex-digit{1, 6} - surrogate - above-max-scalar -surrogate := [0]{0, 2} [dD] [8-9a-fA-F] hex-digit{2} -// U+D800-DFFF: D 8 00 -// D F FF -above-max-scalar = [2-9a-fA-F] hex-digit{5} | - [1] [1-9a-fA-F] hex-digit{4} - - -raw-string := '#' raw-string-quotes '#' | '#' raw-string '#' -raw-string-quotes := - '"' single-line-raw-string-body '"' | - '"""' newline - (multi-line-raw-string-body newline)? - unicode-space* '"""' -single-line-raw-string-body := - '' | - (single-line-raw-string-char - '"') - single-line-raw-string-char*? | - '"' (single-line-raw-string-char - '"') - single-line-raw-string-char*? -single-line-raw-string-char := - unicode - newline - disallowed-literal-code-points -multi-line-raw-string-body := - (unicode - disallowed-literal-code-points)*? - -// Numbers -number := keyword-number | hex | octal | binary | decimal - -decimal := sign? integer ('.' integer)? exponent? -exponent := ('e' | 'E') sign? integer -integer := digit (digit | '_')* -digit := [0-9] -sign := '+' | '-' - -hex := sign? '0x' hex-digit (hex-digit | '_')* -octal := sign? '0o' [0-7] [0-7_]* -binary := sign? '0b' ('0' | '1') ('0' | '1' | '_')* - -// Keywords and booleans. -keyword := boolean | '#null' -keyword-number := '#inf' | '#-inf' | '#nan' -boolean := '#true' | '#false' - -// Specific code points -bom := '\u{FEFF}' -disallowed-literal-code-points := - See Table (Disallowed Literal Code Points) -unicode := Any Unicode Scalar Value -unicode-space := See Table - (All White_Space unicode characters which are not `newline`) - -// Comments -single-line-comment := '//' ^newline* (newline | eof) -multi-line-comment := '/*' commented-block -commented-block := - '*/' | (multi-line-comment | '*' | '/' | [^*/]+) commented-block -slashdash := '/-' line-space* - -// Whitespace -ws := unicode-space | multi-line-comment -escline := '\\' ws* (single-line-comment | newline | eof) -newline := See Table (All Newline White_Space) -// Whitespace where newlines are allowed. -line-space := node-space | newline | single-line-comment -// Whitespace within nodes, -// where newline-ish things must be esclined. -node-space := ws* escline ws* | ws+ - -// Version marker -version := - '/-' unicode-space* 'kdl-version' unicode-space+ ('1' | '2') - unicode-space* newline -~~~ - -## Grammar language - -The grammar language syntax is a combination of ABNF with some regex spice thrown in. -Specifically: - -- Single quotes (`'`) are used to denote literal text. `\` within a literal - string is used for escaping other single-quotes, for initiating unicode - characters using hex values (`\u{FEFF}`), and for escaping `\` itself - (`\\`). -- `*` is used for "zero or more", `+` is used for "one or more", and `?` is - used for "zero or one". Per standard regex semantics, `*` and `+` are _greedy_; - they match as many instances as possible without failing the match. -- `*?` (used only in raw strings) indicates a _non-greedy_ match; - it matches as _few_ instances as possible without failing the match. -- `¶` is a _cut point_. It always matches and consumes no characters, - but once matched, the parser is not allowed to backtrack past that point in the source. - If a parser would rewind past the cut point, it must instead fail the overall parse, - as if it had run out of options. - (This is only used with the `raw-string` production, - to ensure the first instance of the appropriate closing quote sequence - is guaranteed to be the end of the raw string, - rather than allowing it to potentially consume more of the document unexpectedly.) -- `()` can be used to group matches that must be matched together. -- `a | b` means `a or b`, whichever matches first. If multiple items are before - a `|`, they are a single group. `a b c | d` is equivalent to `(a b c) | d`. -- `[]` are used for regex-style character matches, where any character between - the brackets will be a single match. `\` is used to escape `\`, `[`, and - `]`. They also support character ranges (`0-9`), and negation (`^`) -- `-` is used for "except for" or "minus" whatever follows it. For example, - `a - 'x'` means "any `a`, except something that matches the literal `'x'`". -- The prefix `^` means "something that does not match" whatever follows it. - For example, `^foo` means "must not match `foo`". -- A single definition may be split over multiple lines. Newlines are treated as - spaces. -- `//` followed by text on its own line is used as comment syntax. From c51e85f759bdce30749d5d4ce65747214d8a12a3 Mon Sep 17 00:00:00 2001 From: jamesshenry <79054685+jamesshenry@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:46:22 +0000 Subject: [PATCH 16/17] fix benchmark namespace --- src/Kuddle.Net.Benchmarks/Kuddle.Net.Benchmarks.csproj | 2 +- .../{ParserBenchmarks.cs => SerializerBenchmarks.cs} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/Kuddle.Net.Benchmarks/{ParserBenchmarks.cs => SerializerBenchmarks.cs} (99%) diff --git a/src/Kuddle.Net.Benchmarks/Kuddle.Net.Benchmarks.csproj b/src/Kuddle.Net.Benchmarks/Kuddle.Net.Benchmarks.csproj index 29658c3..9cec005 100644 --- a/src/Kuddle.Net.Benchmarks/Kuddle.Net.Benchmarks.csproj +++ b/src/Kuddle.Net.Benchmarks/Kuddle.Net.Benchmarks.csproj @@ -1,6 +1,6 @@ - Kuddle + Kuddle.Benchmarks Exe net10.0 preview diff --git a/src/Kuddle.Net.Benchmarks/ParserBenchmarks.cs b/src/Kuddle.Net.Benchmarks/SerializerBenchmarks.cs similarity index 99% rename from src/Kuddle.Net.Benchmarks/ParserBenchmarks.cs rename to src/Kuddle.Net.Benchmarks/SerializerBenchmarks.cs index 03d2196..5752f38 100644 --- a/src/Kuddle.Net.Benchmarks/ParserBenchmarks.cs +++ b/src/Kuddle.Net.Benchmarks/SerializerBenchmarks.cs @@ -5,7 +5,7 @@ using Kuddle.Serialization; using Parlot.Fluent; -namespace Kuddle.Net.Benchmarks; +namespace Kuddle.Benchmarks; [SimpleJob(RuntimeMoniker.Net10_0)] [MemoryDiagnoser] From a04467f6c3274cd6ce269d70f183f7ddc6eae397 Mon Sep 17 00:00:00 2001 From: jamesshenry <79054685+jamesshenry@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:50:15 +0000 Subject: [PATCH 17/17] fix again --- src/Kuddle.Net.Benchmarks/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Kuddle.Net.Benchmarks/Program.cs b/src/Kuddle.Net.Benchmarks/Program.cs index 6901321..181057c 100644 --- a/src/Kuddle.Net.Benchmarks/Program.cs +++ b/src/Kuddle.Net.Benchmarks/Program.cs @@ -1,4 +1,4 @@ using BenchmarkDotNet.Running; -using Kuddle.Net.Benchmarks; +using Kuddle.Benchmarks; BenchmarkRunner.Run();