From d0e4b7002398690baf382c10fe677d6528ddd42f Mon Sep 17 00:00:00 2001 From: Eric Rodrigues Pires Date: Tue, 18 Nov 2025 06:27:27 -0300 Subject: [PATCH] .NET improvements: Handling more types and QoL (#36) * Handle IEnumerable * Handle records * Handle a basic Godot type * Further improvements to Csharp interface * Prepare for .NET release 0.0.3 --- duper_uniffi/dotnet/CHANGELOG.md | 18 + duper_uniffi/dotnet/Duper.nuspec | 4 +- duper_uniffi/dotnet/DuperAttribute.cs | 2 +- .../dotnet/DuperDeserializeException.cs | 28 ++ .../dotnet/DuperSerializeException.cs | 55 +++ duper_uniffi/dotnet/DuperSerializer.cs | 397 ++++++++++++++---- .../tests/dotnet/DuperSerializerTests.cs | 71 ++++ 7 files changed, 495 insertions(+), 80 deletions(-) create mode 100644 duper_uniffi/dotnet/DuperDeserializeException.cs create mode 100644 duper_uniffi/dotnet/DuperSerializeException.cs diff --git a/duper_uniffi/dotnet/CHANGELOG.md b/duper_uniffi/dotnet/CHANGELOG.md index 6704e8c..4c6e4a1 100644 --- a/duper_uniffi/dotnet/CHANGELOG.md +++ b/duper_uniffi/dotnet/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 0.0.3 (2025-11-18) + +### Added + +- Add `DuperSerializationException` and `DuperDeerializationException`. + +### Changed + +- Improve performance when dealing with repeated data. +- Avoid raising non-terminating exceptions. +- Minor improvements. + +### Fixed + +- Handle `IEnumerable`. +- Handle records. +- Handle types with non-default constructors. + ## 0.0.2 (2025-11-14) ### Fixed diff --git a/duper_uniffi/dotnet/Duper.nuspec b/duper_uniffi/dotnet/Duper.nuspec index 81016db..bd68229 100644 --- a/duper_uniffi/dotnet/Duper.nuspec +++ b/duper_uniffi/dotnet/Duper.nuspec @@ -2,7 +2,7 @@ EpicEric.Duper - 0.0.2 + 0.0.3 Duper Eric Rodrigues Pires <eric@eric.dev.br> MIT @@ -11,7 +11,7 @@ false The format that's super! README.md - Better handling of arrays. + Handling of more types and QoL improvements; see the changelog for details. Copyright 2025 Duper encoding format serialization parsing diff --git a/duper_uniffi/dotnet/DuperAttribute.cs b/duper_uniffi/dotnet/DuperAttribute.cs index 49571d2..aa8d5d0 100644 --- a/duper_uniffi/dotnet/DuperAttribute.cs +++ b/duper_uniffi/dotnet/DuperAttribute.cs @@ -1,6 +1,6 @@ namespace Duper; -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property)] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)] public class DuperAttribute : Attribute { public string? Identifier; diff --git a/duper_uniffi/dotnet/DuperDeserializeException.cs b/duper_uniffi/dotnet/DuperDeserializeException.cs new file mode 100644 index 0000000..c242e74 --- /dev/null +++ b/duper_uniffi/dotnet/DuperDeserializeException.cs @@ -0,0 +1,28 @@ +namespace Duper; + +public class DuperDeserializeException : Exception +{ + internal DuperDeserializeException(string message) + : base(message) { } + + internal DuperDeserializeException(string message, Exception exception) + : base(message, exception) { } + + public class ParseException : DuperDeserializeException + { + internal ParseException(string message) + : base(message) { } + + internal ParseException(string message, Exception exception) + : base(message, exception) { } + } + + public class InvalidTypeException : DuperDeserializeException + { + internal InvalidTypeException(string message) + : base(message) { } + + internal InvalidTypeException(string message, Exception exception) + : base(message, exception) { } + } +} \ No newline at end of file diff --git a/duper_uniffi/dotnet/DuperSerializeException.cs b/duper_uniffi/dotnet/DuperSerializeException.cs new file mode 100644 index 0000000..9eb2a4f --- /dev/null +++ b/duper_uniffi/dotnet/DuperSerializeException.cs @@ -0,0 +1,55 @@ +namespace Duper; + +public class DuperSerializeException : Exception +{ + internal DuperSerializeException(string message) + : base(message) { } + + internal DuperSerializeException(string message, Exception exception) + : base(message, exception) { } + + public class SerializeOptionsException : DuperSerializeException + { + internal SerializeOptionsException(string message) + : base(message) { } + + internal SerializeOptionsException(string message, Exception exception) + : base(message, exception) { } + } + + public class InvalidIdentifierException : DuperSerializeException + { + internal InvalidIdentifierException(string message) + : base(message) { } + + internal InvalidIdentifierException(string message, Exception exception) + : base(message, exception) { } + } + + public class InvalidObjectException : DuperSerializeException + { + internal InvalidObjectException(string message) + : base(message) { } + + internal InvalidObjectException(string message, Exception exception) + : base(message, exception) { } + } + + public class InvalidTemporalException : DuperSerializeException + { + internal InvalidTemporalException(string message) + : base(message) { } + + internal InvalidTemporalException(string message, Exception exception) + : base(message, exception) { } + } + + public class InvalidValueException : DuperDeserializeException + { + internal InvalidValueException(string message) + : base(message) { } + + internal InvalidValueException(string message, Exception exception) + : base(message, exception) { } + } +} \ No newline at end of file diff --git a/duper_uniffi/dotnet/DuperSerializer.cs b/duper_uniffi/dotnet/DuperSerializer.cs index a08f85f..e427548 100644 --- a/duper_uniffi/dotnet/DuperSerializer.cs +++ b/duper_uniffi/dotnet/DuperSerializer.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Reflection; +using System.Runtime.CompilerServices; using Ffi; public class DuperSerializer @@ -27,26 +28,45 @@ public class DuperSerializer || t2 == typeof(ValueTuple<,,,,,,,>); } + private enum DeserializeObject + { + Constructor, + Uninit, + } + + /// + /// Parses the provided Duper string into a value of the specified type. + /// + /// The Duper string to deserialize. + /// A parsed value of type T or null. + /// public static T? Deserialize(string @input) { - DuperValue duperValue = Duper.Parse(input, true); - Type t = typeof(T); - object? value = DeserializeInner(duperValue, typeof(T)); - if (value == null) + try { - if (!t.IsValueType || Nullable.GetUnderlyingType(t) != null) + DuperValue duperValue = Duper.Parse(input, true); + Type t = typeof(T); + object? value = DeserializeInner(duperValue, typeof(T), []); + if (value == null) { - return default; + if (!t.IsValueType || Nullable.GetUnderlyingType(t) != null) + { + return default; + } + throw new DuperDeserializeException.InvalidTypeException($"Cannot cast null to non-nullable {t}"); + } + else + { + return (T)value; } - throw new ApplicationException($"Cannot cast null to non-nullable {t}"); } - else + catch (DuperException.Parse exception) { - return (T)value; + throw new DuperDeserializeException.ParseException("Deserialization failed.", exception); } } - private static object? DeserializeInner(DuperValue duperValue, Type t) + private static object? DeserializeInner(DuperValue duperValue, Type t, Dictionary tCache) { // Null if (duperValue is DuperValue.Null) @@ -64,15 +84,15 @@ public class DuperSerializer Type keyType = generics[0]; if (keyType != typeof(string)) { - throw new ApplicationException($"Cannot parse object to dictionary with non-string keys"); + throw new DuperDeserializeException.InvalidTypeException($"Cannot parse object to dictionary with non-string keys"); } Type valueType = generics[1]; var concreteType = typeof(Dictionary<,>).MakeGenericType(generics); var dict = Activator.CreateInstance(concreteType) ?? throw new ApplicationException("No constructor found for Dictionary"); var addMethod = concreteType.GetMethod("Add") ?? throw new ApplicationException("No Add method found for Dictionary"); - foreach (var item in obj.value) + foreach (DuperObjectEntry item in obj.value) { - addMethod.Invoke(dict, [item.key, DeserializeInner(item.value, valueType)]); + addMethod.Invoke(dict, [item.key, DeserializeInner(item.value, valueType, tCache)]); } return dict; } @@ -86,47 +106,127 @@ public class DuperSerializer Type keyType = generics[0]; if (keyType != typeof(string)) { - throw new ApplicationException($"Cannot parse object to dictionary with non-string keys"); + throw new DuperDeserializeException.InvalidTypeException($"Cannot parse object to dictionary with non-string keys"); } Type valueType = generics[1]; var dict = Activator.CreateInstance(t) ?? throw new ApplicationException($"No constructor found for {t}"); var addMethod = interfaceType.GetMethod("Add") ?? throw new ApplicationException("No Add method found for IDictionary"); - foreach (var item in obj.value) + foreach (DuperObjectEntry item in obj.value) { - addMethod.Invoke(dict, [item.key, DeserializeInner(item.value, valueType)]); + addMethod.Invoke(dict, [item.key, DeserializeInner(item.value, valueType, tCache)]); } return dict; } } - // Create class instance - object instance = Activator.CreateInstance(t) ?? throw new ApplicationException($"No constructor found for {t}"); + // Attempt to create class instance Dictionary classFields = new(obj.value.Length); - foreach (var entry in obj.value) + foreach (DuperObjectEntry entry in obj.value) { classFields.Add(entry.key, entry.value); } - foreach (var field in t.GetFields()) + bool cacheHit = tCache.TryGetValue(t, out DeserializeObject deserializeMethod); + // Attempt to create via constructor(s) + if (!cacheHit || deserializeMethod == DeserializeObject.Constructor) { - string key = field.Name; - Attribute[] attrs = Attribute.GetCustomAttributes(field); - foreach (Attribute attr in attrs) + ConstructorInfo? parameterlessConstructor = null; + foreach (ConstructorInfo constructor in t.GetConstructors()) { - if (attr is DuperAttribute a) + var parameters = constructor.GetParameters(); + if (parameters.Length == 0) { - if (a.Key != null) + parameterlessConstructor = constructor; + } + else + { + try { - key = a.Key; + bool badConstructor = false; + List paramArray = new(parameters.Length); + foreach (ParameterInfo param in parameters) + { + string? key = param.Name; + Attribute[] attrs = Attribute.GetCustomAttributes(param); + foreach (Attribute attr in attrs) + { + if (attr is DuperAttribute a) + { + if (a.Key != null) + { + key = a.Key; + } + break; + } + } + if (key == null || !classFields.TryGetValue(key, out DuperValue? classField)) + { + badConstructor = true; + break; + } + paramArray.Add(DeserializeInner(classField, param.ParameterType, tCache)); + } + if (badConstructor) + { + continue; + } + object instance = constructor.Invoke([.. paramArray]); + tCache[t] = DeserializeObject.Constructor; + return instance; } - break; + catch (Exception) + { + continue; + } + } + } + if (parameterlessConstructor != null) + { + object instance = parameterlessConstructor.Invoke([]); + foreach (FieldInfo field in t.GetFields()) + { + string key = field.Name; + Attribute[] attrs = Attribute.GetCustomAttributes(field); + foreach (Attribute attr in attrs) + { + if (attr is DuperAttribute a) + { + if (a.Key != null) + { + key = a.Key; + } + break; + } + } + var item = classFields[key]; + field.SetValue(instance, DeserializeInner(item, field.FieldType, tCache)); } + foreach (PropertyInfo prop in t.GetProperties()) + { + string key = prop.Name; + Attribute[] attrs = Attribute.GetCustomAttributes(prop); + foreach (Attribute attr in attrs) + { + if (attr is DuperAttribute a) + { + if (a.Key != null) + { + key = a.Key; + } + break; + } + } + var item = classFields[key]; + prop.SetValue(instance, DeserializeInner(item, prop.PropertyType, tCache)); + } + tCache[t] = DeserializeObject.Constructor; + return instance; } - var item = classFields[key] ?? throw new ApplicationException($"No key {key} found in Duper object"); - field.SetValue(instance, DeserializeInner(item, field.FieldType)); } - foreach (var prop in t.GetProperties()) + // Last resort: create uninitialized object and fill its fields + object uninitInstance = RuntimeHelpers.GetUninitializedObject(t); + foreach (FieldInfo field in t.GetFields()) { - string key = prop.Name; - Attribute[] attrs = Attribute.GetCustomAttributes(prop); + string key = field.Name; + Attribute[] attrs = Attribute.GetCustomAttributes(field); foreach (Attribute attr in attrs) { if (attr is DuperAttribute a) @@ -138,10 +238,17 @@ public class DuperSerializer break; } } - var item = classFields[key] ?? throw new ApplicationException($"No key {key} found in Duper object"); - prop.SetValue(instance, DeserializeInner(item, prop.PropertyType)); + if (classFields.TryGetValue(key, out var item)) + { + field.SetValue(uninitInstance, DeserializeInner(item, field.FieldType, tCache)); + } + else + { + throw new DuperDeserializeException.InvalidTypeException($"No valid constructors found for {t}"); + } } - return instance; + tCache[t] = DeserializeObject.Uninit; + return uninitInstance; } // Array @@ -153,32 +260,32 @@ public class DuperSerializer var tupleFields = t.GetFields(); if (tupleFields.Length != array.value.Length) { - throw new ApplicationException($"Mismatched tuple sizes: Duper has length {array.value.Length}, target has length {tupleFields.Length}"); + throw new DuperDeserializeException.InvalidTypeException($"Mismatched tuple sizes: Duper has length {array.value.Length}, target has length {tupleFields.Length}"); } object?[] tupleObjects = new object[tupleFields.Length]; for (int i = 0; i < tupleFields.Length; i++) { - tupleObjects[i] = DeserializeInner(array.value[i], tupleFields[i].FieldType); + tupleObjects[i] = DeserializeInner(array.value[i], tupleFields[i].FieldType, tCache); } var constructor = t.GetConstructor(t.GetGenericArguments()); if (constructor == null) { - throw new ApplicationException($"No constructor found for tuple {t}"); + throw new DuperDeserializeException.InvalidTypeException($"No constructor found for tuple {t}"); } else { return constructor.Invoke(tupleObjects); } } - else if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IList<>)) + else if (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(IEnumerable<>) || t.GetGenericTypeDefinition() == typeof(IList<>))) { Type itemType = t.GetGenericArguments().Single(); var concreteType = typeof(List<>).MakeGenericType(t.GetGenericArguments()); var list = Activator.CreateInstance(concreteType) ?? throw new ApplicationException("No constructor found for List"); var addMethod = concreteType.GetMethod("Add") ?? throw new ApplicationException("No Add method found for List"); - foreach (var item in array.value) + foreach (DuperValue item in array.value) { - addMethod.Invoke(list, [DeserializeInner(item, itemType)]); + addMethod.Invoke(list, [DeserializeInner(item, itemType, tCache)]); } return list; } @@ -188,9 +295,9 @@ public class DuperSerializer 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) + foreach (DuperValue item in array.value) { - addMethod.Invoke(arrayList, [DeserializeInner(item, itemType)]); + addMethod.Invoke(arrayList, [DeserializeInner(item, itemType, tCache)]); } var toArrayMethod = arrayListType.GetMethod("ToArray") ?? throw new ApplicationException("No ToArray method found for List"); return toArrayMethod.Invoke(arrayList, null); @@ -204,14 +311,14 @@ 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 IList) ?? throw new ApplicationException("IList cast shouldn't fail"); - foreach (var item in array.value) + foreach (DuperValue item in array.value) { - ilist.Add(DeserializeInner(item, itemType)); + ilist.Add(DeserializeInner(item, itemType, tCache)); } return list; } } - throw new ApplicationException($"Cannot cast array to {t}"); + throw new DuperDeserializeException.InvalidTypeException($"Cannot cast array to {t}"); } // Tuple @@ -223,32 +330,32 @@ public class DuperSerializer var tupleFields = t.GetFields(); if (tupleFields.Length != tuple.value.Length) { - throw new ApplicationException($"Mismatched tuple sizes: Duper has length {tuple.value.Length}, target has length {tupleFields.Length}"); + throw new DuperDeserializeException.InvalidTypeException($"Mismatched tuple sizes: Duper has length {tuple.value.Length}, target has length {tupleFields.Length}"); } object?[] tupleObjects = new object[tupleFields.Length]; for (int i = 0; i < tupleFields.Length; i++) { - tupleObjects[i] = DeserializeInner(tuple.value[i], tupleFields[i].FieldType); + tupleObjects[i] = DeserializeInner(tuple.value[i], tupleFields[i].FieldType, tCache); } var constructor = t.GetConstructor(t.GetGenericArguments()); if (constructor == null) { - throw new ApplicationException($"No constructor found for tuple {t}"); + throw new DuperDeserializeException.InvalidTypeException($"No constructor found for tuple {t}"); } else { return constructor.Invoke(tupleObjects); } } - else if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IList<>)) + else if (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(IEnumerable<>) || t.GetGenericTypeDefinition() == typeof(IList<>))) { Type itemType = t.GetGenericArguments().Single(); var concreteType = typeof(List<>).MakeGenericType(t.GetGenericArguments()); var list = Activator.CreateInstance(concreteType) ?? throw new ApplicationException("No constructor found for List"); var addMethod = concreteType.GetMethod("Add") ?? throw new ApplicationException("No Add method found for List"); - foreach (var item in tuple.value) + foreach (DuperValue item in tuple.value) { - addMethod.Invoke(list, [DeserializeInner(item, itemType)]); + addMethod.Invoke(list, [DeserializeInner(item, itemType, tCache)]); } return list; } @@ -258,9 +365,9 @@ public class DuperSerializer 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) + foreach (DuperValue item in tuple.value) { - addMethod.Invoke(arrayList, [DeserializeInner(item, itemType)]); + addMethod.Invoke(arrayList, [DeserializeInner(item, itemType, tCache)]); } var toArrayMethod = arrayListType.GetMethod("ToArray") ?? throw new ApplicationException("No ToArray method found for List"); return toArrayMethod.Invoke(arrayList, null); @@ -274,14 +381,14 @@ 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 IList) ?? throw new ApplicationException("IList cast shouldn't fail"); - foreach (var item in tuple.value) + foreach (DuperValue item in tuple.value) { - ilist.Add(DeserializeInner(item, itemType)); + ilist.Add(DeserializeInner(item, itemType, tCache)); } return list; } } - throw new ApplicationException($"Cannot cast tuple to {t}"); + throw new DuperDeserializeException.InvalidTypeException($"Cannot cast tuple to {t}"); } // String @@ -302,7 +409,7 @@ public class DuperSerializer return parseMethod.Invoke(null, [str.value]); } } - throw new ApplicationException($"Cannot cast string to {t}"); + throw new DuperDeserializeException.InvalidTypeException($"Cannot cast string to {t}"); } // Bytes @@ -312,7 +419,7 @@ public class DuperSerializer { return bytes.value; } - throw new ApplicationException($"Cannot cast bytes to {t}"); + throw new DuperDeserializeException.InvalidTypeException($"Cannot cast bytes to {t}"); } // Temporal @@ -346,7 +453,7 @@ public class DuperSerializer // TO-DO: Proper conversion from Temporal value return DateTimeOffset.Parse(temporal.value); } - throw new ApplicationException($"Cannot cast temporal to {t}"); + throw new DuperDeserializeException.InvalidTypeException($"Cannot cast temporal to {t}"); } // Integer @@ -368,7 +475,7 @@ public class DuperSerializer { return (float)integer.value; } - throw new ApplicationException($"Cannot cast integer to {t}"); + throw new DuperDeserializeException.InvalidTypeException($"Cannot cast integer to {t}"); } // Float @@ -382,7 +489,7 @@ public class DuperSerializer { return (float)flt.value; } - throw new ApplicationException($"Cannot cast float to {t}"); + throw new DuperDeserializeException.InvalidTypeException($"Cannot cast float to {t}"); } // Boolean @@ -392,13 +499,13 @@ public class DuperSerializer { return boolean.value; } - throw new ApplicationException($"Cannot cast boolean to {t}"); + throw new DuperDeserializeException.InvalidTypeException($"Cannot cast boolean to {t}"); } // Fail-safe else { - throw new ApplicationException($"Unknown Duper value type {duperValue.GetType()}"); + throw new DuperDeserializeException.InvalidTypeException($"Unknown Duper value type {duperValue.GetType()}"); } } @@ -407,6 +514,14 @@ public class DuperSerializer return T.Parse(value, System.Globalization.CultureInfo.InvariantCulture); } + /// + /// Options for serialization via DuperSerializer.Serialize. + /// + /// Optional whitespace string to use as indentation. + /// Whether Duper identifiers should be removed + /// from the stringified value. + /// Whether to minify the value. Not compatible with + /// Indent. public record SerializerOptions( string? Indent, bool StripIdentifiers, @@ -414,6 +529,12 @@ public class DuperSerializer ) { } + /// + /// Converts the provided value into a Duper string. + /// + /// The value to serialize. + /// The Duper string. + /// public static string Serialize(T @value) { Type t = typeof(T); @@ -428,9 +549,31 @@ public class DuperSerializer } } var duperValue = SerializeInner(value, t, identifier); - return Duper.Serialize(duperValue, null); + try + { + return Duper.Serialize(duperValue, null); + } + catch (DuperException.InvalidIdentifier exception) + { + throw new DuperSerializeException.InvalidIdentifierException("Serialization failed.", exception); + } + catch (DuperException.InvalidObject exception) + { + throw new DuperSerializeException.InvalidObjectException("Serialization failed.", exception); + } + catch (DuperException.InvalidTemporal exception) + { + throw new DuperSerializeException.InvalidTemporalException("Serialization failed.", exception); + } } + /// + /// Converts the provided value into a Duper string with the provided options. + /// + /// The value to serialize. + /// Options for serialization. + /// The Duper string. + /// public static string Serialize(T? @value, SerializerOptions @options) { Type t = typeof(T); @@ -445,7 +588,26 @@ public class DuperSerializer } } var duperValue = SerializeInner(value, t, identifier); - return Duper.Serialize(duperValue, new Ffi.SerializeOptions(options.Indent, options.StripIdentifiers, options.Minify)); + try + { + return Duper.Serialize(duperValue, new SerializeOptions(options.Indent, options.StripIdentifiers, options.Minify)); + } + catch (DuperException.SerializeOptions exception) + { + throw new DuperSerializeException.SerializeOptionsException("Serialization failed.", exception); + } + catch (DuperException.InvalidIdentifier exception) + { + throw new DuperSerializeException.InvalidIdentifierException("Serialization failed.", exception); + } + catch (DuperException.InvalidObject exception) + { + throw new DuperSerializeException.InvalidObjectException("Serialization failed.", exception); + } + catch (DuperException.InvalidTemporal exception) + { + throw new DuperSerializeException.InvalidTemporalException("Serialization failed.", exception); + } } private static DuperValue SerializeInner(object? @value, Type t, string? identifier) @@ -551,8 +713,16 @@ public class DuperSerializer for (int i = 0; i < tupleFields.Length; i++) { var field = tupleFields[i]; - // TO-DO: Tuple identifiers - tupleValue[i] = SerializeInner(field.GetValue(value), field.FieldType, null); + string? fieldIdentifier = null; + foreach (Attribute attr in field.GetCustomAttributes()) + { + if (attr is DuperAttribute a) + { + fieldIdentifier = a.Identifier; + break; + } + } + tupleValue[i] = SerializeInner(field.GetValue(value), field.FieldType, fieldIdentifier); } return new DuperValue.Tuple(identifier, tupleValue); } @@ -573,7 +743,7 @@ public class DuperSerializer Type keyType = generics[0]; if (keyType != typeof(string)) { - throw new ApplicationException($"Cannot serialize dictionary with non-string keys to Duper"); + throw new DuperSerializeException.InvalidValueException($"Cannot serialize dictionary with non-string keys to Duper"); } Type valueType = generics[1]; IDictionary valueDict = (value as IDictionary) ?? throw new ApplicationException("IDictionary cast shouldn't fail"); @@ -584,6 +754,17 @@ public class DuperSerializer } return new DuperValue.Object(identifier, [.. objValue]); } + else if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + Type itemType = t.GetGenericArguments().Single(); + IEnumerable valueEnumerable = (value as IEnumerable) ?? throw new ApplicationException("IEnumerable cast shouldn't fail"); + List arrayValue = []; + foreach (var element in valueEnumerable) + { + arrayValue.Add(SerializeInner(element, itemType, null)); + } + return new DuperValue.Array(identifier, [.. arrayValue]); + } Type? iformattable = null; foreach (Type interfaceType in t.GetInterfaces()) @@ -593,7 +774,7 @@ public class DuperSerializer == typeof(IList<>)) { Type itemType = interfaceType.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++) { @@ -609,7 +790,7 @@ public class DuperSerializer Type keyType = generics[0]; if (keyType != typeof(string)) { - throw new ApplicationException($"Cannot serialize dictionary with non-string keys to Duper"); + throw new DuperSerializeException.InvalidValueException($"Cannot serialize dictionary with non-string keys to Duper"); } Type valueType = generics[1]; IDictionary valueDict = (value as IDictionary) ?? throw new ApplicationException("IDictionary cast shouldn't fail"); @@ -620,6 +801,19 @@ public class DuperSerializer } return new DuperValue.Object(identifier, [.. objValue]); } + else if (interfaceType.IsGenericType && + interfaceType.GetGenericTypeDefinition() + == typeof(IEnumerable<>)) + { + Type itemType = t.GetGenericArguments().Single(); + IEnumerable valueEnumerable = (value as IEnumerable) ?? throw new ApplicationException("IEnumerable cast shouldn't fail"); + List arrayValue = []; + foreach (var element in valueEnumerable) + { + arrayValue.Add(SerializeInner(element, itemType, null)); + } + return new DuperValue.Array(identifier, [.. arrayValue]); + } else if (interfaceType == typeof(IFormattable)) { iformattable = interfaceType; @@ -637,12 +831,43 @@ public class DuperSerializer } } - List classDict = []; + List classEntries = []; + Dictionary duperAttributes = []; - foreach (var field in t.GetFields()) + // Records: Check for Duper attribute in constructor parameters + foreach (ConstructorInfo constructor in t.GetConstructors()) + { + foreach (ParameterInfo parameter in constructor.GetParameters()) + { + string? name = parameter.Name; + if (name == null) + { + continue; + } + Attribute[] fieldAttrs = Attribute.GetCustomAttributes(parameter); + foreach (Attribute attr in fieldAttrs) + { + if (attr is DuperAttribute a) + { + duperAttributes.Add(name, a); + break; + } + } + } + } + + foreach (FieldInfo field in t.GetFields()) { string key = field.Name; string? fieldIdentifier = null; + if (duperAttributes.TryGetValue(field.Name, out DuperAttribute? duperAttribute)) + { + fieldIdentifier = duperAttribute.Identifier; + if (duperAttribute.Key != null) + { + key = duperAttribute.Key; + } + } Attribute[] fieldAttrs = Attribute.GetCustomAttributes(field); foreach (Attribute attr in fieldAttrs) { @@ -656,13 +881,31 @@ public class DuperSerializer break; } } - classDict.Add(new DuperObjectEntry(key, SerializeInner(field.GetValue(value), field.FieldType, fieldIdentifier))); + classEntries.Add(new DuperObjectEntry(key, SerializeInner(field.GetValue(value), field.FieldType, fieldIdentifier))); } - foreach (var prop in t.GetProperties()) + foreach (PropertyInfo prop in t.GetProperties()) { + if (prop.GetIndexParameters().Length > 0) + { + continue; + } + // Simple check to prevent infinite recursion on types containing static instances of themselves + // TO-DO: Improve this check + if (prop.PropertyType == t) + { + continue; + } string key = prop.Name; string? propIdentifier = null; + if (duperAttributes.TryGetValue(prop.Name, out DuperAttribute? duperAttribute)) + { + propIdentifier = duperAttribute.Identifier; + if (duperAttribute.Key != null) + { + key = duperAttribute.Key; + } + } Attribute[] fieldAttrs = Attribute.GetCustomAttributes(prop); foreach (Attribute attr in fieldAttrs) { @@ -676,10 +919,10 @@ public class DuperSerializer break; } } - classDict.Add(new DuperObjectEntry(key, SerializeInner(prop.GetValue(value), prop.PropertyType, propIdentifier))); + classEntries.Add(new DuperObjectEntry(key, SerializeInner(prop.GetValue(value), prop.PropertyType, propIdentifier))); } - return new DuperValue.Object(identifier, [.. classDict]); + return new DuperValue.Object(identifier, [.. classEntries]); } private static string FormatViaGeneric(T value) where T : IFormattable diff --git a/duper_uniffi/tests/dotnet/DuperSerializerTests.cs b/duper_uniffi/tests/dotnet/DuperSerializerTests.cs index ae8bf27..db2ee49 100644 --- a/duper_uniffi/tests/dotnet/DuperSerializerTests.cs +++ b/duper_uniffi/tests/dotnet/DuperSerializerTests.cs @@ -96,6 +96,9 @@ public class DuperSerializerTests Assert.Equal([true, false], output3); List? output4 = DuperSerializer.Deserialize>(@"(b""a"", null)"); Assert.Equal([[0x61], null], output4); + IEnumerable? output5 = DuperSerializer.Deserialize>(@"(""foo"", ""bar"")"); + Assert.NotNull(output5); + Assert.Equal(["foo", "bar"], output5); Assert.Equal(@"(""hello"", null)", DuperSerializer.Serialize<(string, object?)>(("hello", null))); } @@ -111,12 +114,18 @@ public class DuperSerializerTests Assert.Equal([true, false], output3); List? output4 = DuperSerializer.Deserialize>(@"[b""a"", null]"); Assert.Equal([[0x61], null], output4); + IEnumerable? output5 = DuperSerializer.Deserialize>(@"[""foo"", ""bar""]"); + Assert.NotNull(output5); + Assert.Equal(["foo", "bar"], output5); 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])); + Assert.Equal(@"[""foo"", ""bar""]", DuperSerializer.Serialize>(["foo", "bar"])); } + public record Person(string FirstName, [Duper(Key = "last_name")] string LastName); + [Fact] public void DuperSerializer_Object() { @@ -124,9 +133,12 @@ public class DuperSerializerTests 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); + Person? output3 = DuperSerializer.Deserialize(@"{""FirstName"": ""John"", ""last_name"": ""Doe""}"); + Assert.Equal(new Person("John", "Doe"), output3); 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") } })); + Assert.Equal(@"{FirstName: ""John"", last_name: ""Doe""}", DuperSerializer.Serialize(new Person("John", "Doe"))); } [Duper("UserProfile")] @@ -204,4 +216,63 @@ public class DuperSerializerTests Assert.Contains(@"last_logins: [(""192.168.1.100"", Instant('2024-03-20T14:30:00.0000000+00:00'))]", serialized); Assert.Equal(388, serialized.Length); } + + public struct Vector2 + { + public enum Axis + { + X, + Y + } + + public float X; + public float Y; + + private static readonly Vector2 _zero = new(0f, 0f); + + public float this[int index] + { + readonly get + { + return index switch + { + 0 => X, + 1 => Y, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + } + set + { + switch (index) + { + case 0: + X = value; + break; + case 1: + Y = value; + break; + default: + throw new ArgumentOutOfRangeException("index"); + } + } + } + + public static Vector2 Zero => _zero; + + public Vector2(float x, float y) + { + X = x; + Y = y; + } + } + + [Fact] + public void DuperSerializer_Vector2() + { + string serialized = DuperSerializer.Serialize(new Vector2(1f, 2f)); + Assert.Equal("{X: 1.0, Y: 2.0}", serialized); + + Vector2? deserialized = DuperSerializer.Deserialize("{Y: 3.0, X: 0.5}"); + Assert.Equal(new Vector2(0.5f, 3f), deserialized); + } } -- 2.51.2