From bb1e2721c15301713fc90627e153f3551db7d82a Mon Sep 17 00:00:00 2001 From: Eric Rodrigues Pires Date: Fri, 14 Nov 2025 08:38:19 -0300 Subject: [PATCH] Finalize tests and publish EpicEric.Duper 0.0.2 --- duper/CHANGELOG.md | 6 + duper/src/parser/temporal.rs | 8 +- duper_uniffi/csharp/CHANGELOG.md | 11 ++ duper_uniffi/csharp/Duper.nuspec | 4 +- duper_uniffi/csharp/DuperSerializer.cs | 39 +++++- duper_uniffi/csharp/README.md | 9 +- duper_uniffi/example/csharp/.gitignore | 2 - duper_uniffi/example/csharp/Example.csproj | 30 ----- duper_uniffi/justfile | 12 +- .../tests/csharp/DuperSerializerTests.cs | 122 ++++++++++++++---- .../docs/guide-dotnet.md | 16 ++- duper_website/docs/quick-start.md | 8 ++ 12 files changed, 188 insertions(+), 79 deletions(-) create mode 100644 duper_uniffi/csharp/CHANGELOG.md delete mode 100644 duper_uniffi/example/csharp/.gitignore delete mode 100644 duper_uniffi/example/csharp/Example.csproj rename duper_uniffi/example/csharp/Example.cs => duper_website/docs/guide-dotnet.md (85%) diff --git a/duper/CHANGELOG.md b/duper/CHANGELOG.md index 45370b4..48d0c18 100644 --- a/duper/CHANGELOG.md +++ b/duper/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixed + +- Missing handling for PlainTime in Temporal parser. + ## 0.4.0 (2025-11-11) ### Added diff --git a/duper/src/parser/temporal.rs b/duper/src/parser/temporal.rs index 6e4eb4c..caa4737 100644 --- a/duper/src/parser/temporal.rs +++ b/duper/src/parser/temporal.rs @@ -322,7 +322,13 @@ pub(crate) fn duration<'a>() -> impl Parser<'a, &'a str, (), extra::Err() -> impl Parser<'a, &'a str, (), extra::Err>> + Clone { - choice((instant(), plain_year_month(), plain_month_day(), duration())) + choice(( + instant(), + plain_year_month(), + plain_month_day(), + plain_time(), + duration(), + )) } // Atoms diff --git a/duper_uniffi/csharp/CHANGELOG.md b/duper_uniffi/csharp/CHANGELOG.md new file mode 100644 index 0000000..6704e8c --- /dev/null +++ b/duper_uniffi/csharp/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +## 0.0.2 (2025-11-14) + +### Fixed + +- Better handling of arrays. + +## 0.0.1 (2025-11-14) + +Initial release. diff --git a/duper_uniffi/csharp/Duper.nuspec b/duper_uniffi/csharp/Duper.nuspec index 769f551..81016db 100644 --- a/duper_uniffi/csharp/Duper.nuspec +++ b/duper_uniffi/csharp/Duper.nuspec @@ -2,7 +2,7 @@ EpicEric.Duper - 0.0.1 + 0.0.2 Duper Eric Rodrigues Pires <eric@eric.dev.br> MIT @@ -11,7 +11,7 @@ false The format that's super! README.md - Initial release. + Better handling of arrays. Copyright 2025 Duper encoding format serialization parsing diff --git a/duper_uniffi/csharp/DuperSerializer.cs b/duper_uniffi/csharp/DuperSerializer.cs index d9a3b4b..45fb0d8 100644 --- a/duper_uniffi/csharp/DuperSerializer.cs +++ b/duper_uniffi/csharp/DuperSerializer.cs @@ -1,5 +1,6 @@ namespace Duper; +using System.Collections; using System.Reflection; using Ffi; @@ -176,6 +177,19 @@ public class DuperSerializer } return list; } + else if (t.IsArray) + { + Type itemType = t.GetElementType() ?? throw new ApplicationException($"No element type found for {t}"); ; + var arrayListType = typeof(List<>).MakeGenericType([itemType]); + var arrayList = Activator.CreateInstance(arrayListType) ?? throw new ApplicationException("No constructor found for List"); + var addMethod = arrayListType.GetMethod("Add") ?? throw new ApplicationException("No Add method found for List"); + foreach (var item in array.value) + { + addMethod.Invoke(arrayList, [DeserializeInner(item, itemType)]); + } + var toArrayMethod = arrayListType.GetMethod("ToArray") ?? throw new ApplicationException("No ToArray method found for List"); + return toArrayMethod.Invoke(arrayList, null); + } foreach (Type interfaceType in t.GetInterfaces()) { if (interfaceType.IsGenericType && @@ -184,7 +198,7 @@ public class DuperSerializer { Type itemType = interfaceType.GetGenericArguments().Single(); var list = Activator.CreateInstance(t) ?? throw new ApplicationException($"No constructor found for {t}"); - var ilist = (list as System.Collections.IList) ?? throw new ApplicationException("IList cast shouldn't fail"); + var ilist = (list as IList) ?? throw new ApplicationException("IList cast shouldn't fail"); foreach (var item in array.value) { ilist.Add(DeserializeInner(item, itemType)); @@ -233,6 +247,19 @@ public class DuperSerializer } return list; } + else if (t.IsArray) + { + Type itemType = t.GetElementType() ?? throw new ApplicationException($"No element type found for {t}"); ; + var arrayListType = typeof(List<>).MakeGenericType([itemType]); + var arrayList = Activator.CreateInstance(arrayListType) ?? throw new ApplicationException("No constructor found for List"); + var addMethod = arrayListType.GetMethod("Add") ?? throw new ApplicationException("No Add method found for List"); + foreach (var item in tuple.value) + { + addMethod.Invoke(arrayList, [DeserializeInner(item, itemType)]); + } + var toArrayMethod = arrayListType.GetMethod("ToArray") ?? throw new ApplicationException("No ToArray method found for List"); + return toArrayMethod.Invoke(arrayList, null); + } foreach (Type interfaceType in t.GetInterfaces()) { if (interfaceType.IsGenericType && @@ -241,10 +268,10 @@ public class DuperSerializer { Type itemType = interfaceType.GetGenericArguments().Single(); var list = Activator.CreateInstance(t) ?? throw new ApplicationException($"No constructor found for {t}"); - var addMethod = interfaceType.GetMethod("Add") ?? throw new ApplicationException("No Add method found for IList"); + var ilist = (list as IList) ?? throw new ApplicationException("IList cast shouldn't fail"); foreach (var item in tuple.value) { - addMethod.Invoke(list, [DeserializeInner(item, itemType)]); + ilist.Add(DeserializeInner(item, itemType)); } return list; } @@ -282,6 +309,8 @@ public class DuperSerializer } throw new ApplicationException($"Cannot cast bytes to {t}"); } + + // Temporal else if (duperValue is DuperValue.Temporal temporal) { if (typeof(string).IsAssignableTo(t)) @@ -525,7 +554,7 @@ public class DuperSerializer else if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IList<>)) { Type itemType = t.GetGenericArguments().Single(); - System.Collections.IList valueList = (value as System.Collections.IList) ?? throw new ApplicationException("IList cast shouldn't fail"); + IList valueList = (value as IList) ?? throw new ApplicationException("IList cast shouldn't fail"); DuperValue[] arrayValue = new DuperValue[valueList.Count]; for (int i = 0; i < valueList.Count; i++) { @@ -543,7 +572,7 @@ public class DuperSerializer } Type valueType = generics[1]; Dictionary objValue = []; - System.Collections.IDictionary valueDict = (value as System.Collections.IDictionary) ?? throw new ApplicationException("IDictionary cast shouldn't fail"); + IDictionary valueDict = (value as IDictionary) ?? throw new ApplicationException("IDictionary cast shouldn't fail"); foreach (var key in valueDict.Keys) { objValue[(string)key] = SerializeInner(valueDict[key], valueType, null); diff --git a/duper_uniffi/csharp/README.md b/duper_uniffi/csharp/README.md index f34cbce..7f2a9c7 100644 --- a/duper_uniffi/csharp/README.md +++ b/duper_uniffi/csharp/README.md @@ -1,11 +1,6 @@ -

- The Duper logo, with a confident spectacled mole wearing a flailing blue cape.
-

-

Duper: The format that's super!

+# Duper: The format that's super! -

- GitHub license -

+![GitHub license](https://img.shields.io/github/license/EpicEric/duper) C# bindings for Duper. diff --git a/duper_uniffi/example/csharp/.gitignore b/duper_uniffi/example/csharp/.gitignore deleted file mode 100644 index cd42ee3..0000000 --- a/duper_uniffi/example/csharp/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -bin/ -obj/ diff --git a/duper_uniffi/example/csharp/Example.csproj b/duper_uniffi/example/csharp/Example.csproj deleted file mode 100644 index 58a7a50..0000000 --- a/duper_uniffi/example/csharp/Example.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - Exe - net8.0 - enable - enable - true - - false - ../../../target/debug - - - - - - - - - PreserveNewest - - - - - - PreserveNewest - - - - diff --git a/duper_uniffi/justfile b/duper_uniffi/justfile index e454d2d..59e7a1f 100644 --- a/duper_uniffi/justfile +++ b/duper_uniffi/justfile @@ -12,18 +12,12 @@ build-release: cargo build --release --target x86_64-pc-windows-gnu [working-directory: 'csharp'] -package-cs: build-release bindgen +package-csharp: build-release bindgen dotnet build dotnet pack -example: build-debug bindgen example-cs - -[working-directory: 'example/csharp'] -example-cs: - dotnet run - -test: build-debug bindgen test-cs +test: build-debug bindgen test-csharp [working-directory: 'tests/csharp'] -test-cs: +test-csharp: dotnet test diff --git a/duper_uniffi/tests/csharp/DuperSerializerTests.cs b/duper_uniffi/tests/csharp/DuperSerializerTests.cs index 716eacd..ae8bf27 100644 --- a/duper_uniffi/tests/csharp/DuperSerializerTests.cs +++ b/duper_uniffi/tests/csharp/DuperSerializerTests.cs @@ -5,66 +5,128 @@ namespace Duper.Tests; public class DuperSerializerTests { [Fact] - public void DuperSerializer_Deserialize_Null() + public void DuperSerializer_Null() { object? output = DuperSerializer.Deserialize("null"); Assert.Null(output); + + Assert.Equal("null", DuperSerializer.Serialize(null)); } [Fact] - public void DuperSerializer_Deserialize_Boolean() + public void DuperSerializer_Boolean() { bool output = DuperSerializer.Deserialize("true"); Assert.True(output); + + Assert.Equal("true", DuperSerializer.Serialize(true)); } [Fact] - public void DuperSerializer_Deserialize_Int() + public void DuperSerializer_Integer() { int output = DuperSerializer.Deserialize("10"); Assert.Equal(10, output); - } + long output2 = DuperSerializer.Deserialize("12345678910"); + Assert.Equal(12345678910, output2); - [Fact] - public void DuperSerializer_Deserialize_Long() - { - long output = DuperSerializer.Deserialize("12345678910"); - Assert.Equal(12345678910, output); + Assert.Equal("10", DuperSerializer.Serialize(10)); + Assert.Equal("12345678910", DuperSerializer.Serialize(12345678910)); } [Fact] - public void DuperSerializer_Deserialize_Float() + public void DuperSerializer_Float() { float output = DuperSerializer.Deserialize("10.2"); Assert.Equal(10.2, output, 0.0001); + double output2 = DuperSerializer.Deserialize("2.7e+100"); + Assert.Equal(2.7e+100, output2); } + [Fact] - public void DuperSerializer_Deserialize_Double() + public void DuperSerializer_String() { - double output = DuperSerializer.Deserialize("2.7e+100"); - Assert.Equal(2.7e+100, output); + string? output = DuperSerializer.Deserialize("r\"super duper\""); + Assert.Equal("super duper", output); + IPAddress? output2 = DuperSerializer.Deserialize("\"2001:12ff::1\""); + Assert.Equal(IPAddress.Parse("2001:12ff::1"), output2); + + Assert.Equal("\"super duper\"", DuperSerializer.Serialize("super duper")); + Assert.Equal("\"2001:12ff::1\"", DuperSerializer.Serialize(IPAddress.Parse("2001:12ff::1"))); } [Fact] - public void DuperSerializer_Deserialize_String() + public void DuperSerializer_Temporal() { - string? output = DuperSerializer.Deserialize(@"r""super duper"""); - Assert.Equal("super duper", output); + string? output = DuperSerializer.Deserialize("'--12-25'"); + Assert.Equal("--12-25", output); + DateOnly? output2 = DuperSerializer.Deserialize("'1970-01-01'"); + Assert.Equal(new DateOnly(1970, 1, 1), output2); + TimeOnly? output3 = DuperSerializer.Deserialize("'12:04:20'"); + Assert.Equal(new TimeOnly(12, 4, 20), output3); + DateTime? output4 = DuperSerializer.Deserialize("'1970-01-01T12:04:20'"); + Assert.Equal(new DateTime(new DateOnly(1970, 1, 1), new TimeOnly(12, 4, 20), DateTimeKind.Unspecified), output4); + DateTimeOffset? output5 = DuperSerializer.Deserialize("'1970-01-01T12:04:20+00:00'"); + Assert.Equal(new DateTimeOffset(new DateOnly(1970, 1, 1), new TimeOnly(12, 4, 20), TimeSpan.Zero), output5); + + Assert.Equal("PlainDate('1970-01-01')", DuperSerializer.Serialize(new DateOnly(1970, 1, 1))); + Assert.Equal("PlainTime('12:04:20.0000000')", DuperSerializer.Serialize(new TimeOnly(12, 4, 20))); + Assert.Equal("PlainDateTime('1970-01-01T12:04:20.0000000')", DuperSerializer.Serialize(new DateTime(new DateOnly(1970, 1, 1), new TimeOnly(12, 4, 20), DateTimeKind.Unspecified))); + Assert.Equal("Instant('1970-01-01T12:04:20.0000000+00:00')", DuperSerializer.Serialize(new DateTimeOffset(new DateOnly(1970, 1, 1), new TimeOnly(12, 4, 20), TimeSpan.Zero))); } [Fact] - public void DuperSerializer_Deserialize_Bytes() + public void DuperSerializer_Bytes() { - byte[]? output = DuperSerializer.Deserialize(@"b""\x1b[0m"""); - Assert.Equal([27, 91, 48, 109], output); + byte[]? output = DuperSerializer.Deserialize(@"b""\x1b[0mabc"""); + Assert.Equal([27, 91, 48, 109, 97, 98, 99], output); + + Assert.Equal(@"b""\x1b[0mabc""", DuperSerializer.Serialize([27, 91, 48, 109, 97, 98, 99])); } [Fact] - public void DuperSerializer_Deserialize_Tuple() + public void DuperSerializer_Tuple() { (string, object?) output = DuperSerializer.Deserialize<(string, object?)>(@"(""hello"", null)"); Assert.Equal(("hello", null), output); + int[]? output2 = DuperSerializer.Deserialize("(12, 34)"); + Assert.Equal([12, 34], output2); + IList? output3 = DuperSerializer.Deserialize>("(true, false)"); + Assert.Equal([true, false], output3); + List? output4 = DuperSerializer.Deserialize>(@"(b""a"", null)"); + Assert.Equal([[0x61], null], output4); + + Assert.Equal(@"(""hello"", null)", DuperSerializer.Serialize<(string, object?)>(("hello", null))); + } + + [Fact] + public void DuperSerializer_Array() + { + (string, object?) output = DuperSerializer.Deserialize<(string, object?)>(@"[""hello"", null]"); + Assert.Equal(("hello", null), output); + int[]? output2 = DuperSerializer.Deserialize(@"[12, 34]"); + Assert.Equal([12, 34], output2); + IList? output3 = DuperSerializer.Deserialize>(@"[true, false]"); + Assert.Equal([true, false], output3); + List? output4 = DuperSerializer.Deserialize>(@"[b""a"", null]"); + Assert.Equal([[0x61], null], output4); + + Assert.Equal("[12, 34]", DuperSerializer.Serialize([12, 34])); + Assert.Equal("[true, false]", DuperSerializer.Serialize>([true, false])); + Assert.Equal(@"[b""a"", null]", DuperSerializer.Serialize>([[0x61], null])); + } + + [Fact] + public void DuperSerializer_Object() + { + IDictionary? output = DuperSerializer.Deserialize>(@"{hello: [null, 14]}"); + Assert.Equivalent(new Dictionary() { { "hello", [null, 14] } }, output); + Dictionary? output2 = DuperSerializer.Deserialize>(@"{""super duper"": (true, ""cool"")}"); + Assert.Equivalent(new Dictionary() { { "super duper", (true, "cool") } }, output2); + + Assert.Equal(@"{hello: [null, 14]}", DuperSerializer.Serialize(new Dictionary() { { "hello", [null, 14] } })); + Assert.Equal(@"{""super duper"": (true, ""cool"")}", DuperSerializer.Serialize(new Dictionary() { { "super duper", (true, "cool") } })); } [Duper("UserProfile")] @@ -81,7 +143,7 @@ public class DuperSerializerTests public required byte[] @avatar { get; set; } public string? @bio { get; set; } [Duper(Key = "last_logins")] - public required IList<(System.Net.IPAddress, DateTimeOffset)> LastLogins { get; set; } + public required IList<(IPAddress, DateTimeOffset)> LastLogins { get; set; } } public class UserSettingsExample @@ -94,7 +156,7 @@ public class DuperSerializerTests } [Fact] - public void DuperSerializer_Deserialize_FullExample() + public void DuperSerializer_FullExample() { UserProfileExample? userProfile = DuperSerializer.Deserialize(""" UserProfile({ @@ -125,5 +187,21 @@ public class DuperSerializerTests Assert.Equal([137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 100], userProfile.avatar); Assert.Equal("Hello! I'm a super \"duper\" user!", userProfile.bio); Assert.Equal([(IPAddress.Parse("192.168.1.100"), DateTimeOffset.Parse("2024-03-20T14:30:00+00:00"))], userProfile.LastLogins); + + string serialized = DuperSerializer.Serialize(userProfile); + Assert.StartsWith("UserProfile({", serialized); + Assert.EndsWith("})", serialized); + Assert.Contains(@"id: Uuid(""f111c275-b4ce-4392-8e5b-19067ce39b53"")", serialized); + Assert.Contains(@"username: ""EpicEric""", serialized); + Assert.Contains(@"email: EmailAddress(""eric@duper.dev.br"")", serialized); + Assert.Contains(@"settings: {", serialized); + Assert.Contains(@"""dark mode"": true", serialized); + Assert.Contains(@"language: Locale(""pt-BR"")", serialized); + Assert.Contains(@"metadata: null", serialized); + Assert.Contains(@"score: 120.25", serialized); + Assert.Contains(@"avatar: Png(b64""iVBORw0KGgoAAAANSUhEUgAAAGQ="")", serialized); + Assert.Contains(@"bio: r#""Hello! I'm a super ""duper"" user!""#", serialized); + Assert.Contains(@"last_logins: [(""192.168.1.100"", Instant('2024-03-20T14:30:00.0000000+00:00'))]", serialized); + Assert.Equal(388, serialized.Length); } } diff --git a/duper_uniffi/example/csharp/Example.cs b/duper_website/docs/guide-dotnet.md similarity index 85% rename from duper_uniffi/example/csharp/Example.cs rename to duper_website/docs/guide-dotnet.md index 4f91940..688b3ba 100644 --- a/duper_uniffi/example/csharp/Example.cs +++ b/duper_website/docs/guide-dotnet.md @@ -1,3 +1,16 @@ +# .NET guide (alpha) + +Get started with Duper in .NET / C# with the [`EpicEric.Duper`](https://www.nuget.org/packages/EpicEric.Duper) package. + +## Installation + +```bash +dotnet add package EpicEric.Duper +``` + +## Usage + +```c# using System.Net; using Duper; @@ -33,7 +46,7 @@ namespace Example { public static void Main(string[] args) { - UserProfile? userProfile = DuperSerializer.Deserialize(""" + UserProfile userProfile = DuperSerializer.Deserialize(""" UserProfile({ id: Uuid("f111c275-b4ce-4392-8e5b-19067ce39b53"), username: "EpicEric", @@ -60,3 +73,4 @@ namespace Example } } } +``` diff --git a/duper_website/docs/quick-start.md b/duper_website/docs/quick-start.md index 5d3678e..2c85977 100644 --- a/duper_website/docs/quick-start.md +++ b/duper_website/docs/quick-start.md @@ -8,6 +8,14 @@ npm install --save @duper-js/wasm See [the JavaScript guide](/guide-javascript). +## .NET / C# (alpha) + +```bash +dotnet add package EpicEric.Duper +``` + +See [the .NET guide](/guide-dotnet). + ## Python ```bash -- 2.51.2