From dad5468d17586b745c0972b3f73c36a3456be97b Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Sun, 22 Dec 2024 21:02:48 -0300 Subject: [PATCH] Add legacy TypeConverter support from System.ComponentModel This shouldn't be needed in newer ASP.NET Core since the IParsable/IFormattable is the only requirement nowadays. But might be useful in other scenarios. --- src/StructId.FunctionalTests/Functional.cs | 13 +++++ src/StructId.FunctionalTests/UlidTests.cs | 14 +++++ src/StructId/Templates/TypeConverter.cs | 55 +++++++++++++++++++ src/StructId/Templates/TypeConverterT.cs | 64 ++++++++++++++++++++++ 4 files changed, 146 insertions(+) create mode 100644 src/StructId/Templates/TypeConverter.cs create mode 100644 src/StructId/Templates/TypeConverterT.cs diff --git a/src/StructId.FunctionalTests/Functional.cs b/src/StructId.FunctionalTests/Functional.cs index ac5aa0e..5b1013b 100644 --- a/src/StructId.FunctionalTests/Functional.cs +++ b/src/StructId.FunctionalTests/Functional.cs @@ -24,6 +24,19 @@ public record User(UserId Id, string Name, Wallet Wallet); public class FunctionalTests(ITestOutputHelper output) { + [Fact] + public void TypeConverters() + { + var id = ProductId.New(); + var converter = TypeDescriptor.GetConverter(id); + + Assert.True(converter.CanConvertTo(typeof(string))); + Assert.True(converter.CanConvertFrom(typeof(string))); + + var id2 = (ProductId?)converter.ConvertFromString(converter.ConvertToString(id)!); + Assert.Equal(id, id2); + } + [Fact] public void JsonConversion() { diff --git a/src/StructId.FunctionalTests/UlidTests.cs b/src/StructId.FunctionalTests/UlidTests.cs index 395447f..3a27d62 100644 --- a/src/StructId.FunctionalTests/UlidTests.cs +++ b/src/StructId.FunctionalTests/UlidTests.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.ComponentModel; using System.Data; using System.Text.Json; using Dapper; @@ -69,6 +70,19 @@ public UlidToStringConverter(ConverterMappingHints? mappingHints = null) public class UlidTests { + [Fact] + public void TypeConverters() + { + var id = UlidId.New(); + var converter = TypeDescriptor.GetConverter(id); + + Assert.True(converter.CanConvertTo(typeof(string))); + Assert.True(converter.CanConvertFrom(typeof(string))); + + var id2 = (UlidId?)converter.ConvertFromString(converter.ConvertToString(id)!); + Assert.Equal(id, id2); + } + [Fact] public void JsonConversion() { diff --git a/src/StructId/Templates/TypeConverter.cs b/src/StructId/Templates/TypeConverter.cs new file mode 100644 index 0000000..eb5e0b6 --- /dev/null +++ b/src/StructId/Templates/TypeConverter.cs @@ -0,0 +1,55 @@ +// +#nullable enable + +using System; +using System.ComponentModel; +using System.Globalization; +using StructId; + +[TStructId] +[TypeConverter(typeof(TSelf.StringConverter))] +file readonly partial record struct TSelf(string Value) +{ + partial class StringConverter : TypeConverter + { + /// + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + => sourceType == typeof(string) || sourceType == typeof(TSelf); + + /// + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value == null) + return default(TSelf); + + if (value is string typedValue) + return TSelf.New(typedValue); + + throw new ArgumentException($"Cannot convert '{value}' to {nameof(TSelf)}", nameof(value)); + } + + /// + public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) + => destinationType == typeof(string) || destinationType == typeof(TSelf); + + /// + public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) + { + if (value != null) + { + if (destinationType == typeof(string)) + return ((TSelf)value).Value; + + if (destinationType == typeof(TSelf)) + return value; + } + + throw new InvalidOperationException($"Cannot convert '{value}' to '{destinationType}'"); + } + } +} + +file partial record struct TSelf : INewable +{ + public static TSelf New(string value) => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/src/StructId/Templates/TypeConverterT.cs b/src/StructId/Templates/TypeConverterT.cs new file mode 100644 index 0000000..593edf7 --- /dev/null +++ b/src/StructId/Templates/TypeConverterT.cs @@ -0,0 +1,64 @@ +// +#nullable enable + +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using StructId; + +[TStructId] +[TypeConverter(typeof(TSelf.StringConverter))] +file readonly partial record struct TSelf(/*!string*/ TValue Value) +{ + partial class StringConverter : TypeConverter + { + /// + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + => sourceType == typeof(string) || sourceType == typeof(TSelf); + + /// + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value == null) + return default(TSelf); + + if (value is string typedValue) + return TSelf.New(TValue.Parse(typedValue, culture)); + + throw new ArgumentException($"Cannot convert '{value}' to {nameof(TSelf)}", nameof(value)); + } + + /// + public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) + => destinationType == typeof(string) || destinationType == typeof(TSelf); + + /// + public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) + { + if (value != null) + { + if (destinationType == typeof(string)) + return ((TSelf)value).Value.ToString(null, culture); + + if (destinationType == typeof(TSelf)) + return value; + } + + throw new InvalidOperationException($"Cannot convert '{value}' to '{destinationType}'"); + } + } +} + +file partial record struct TSelf : INewable +{ + public static TSelf New(TValue value) => throw new NotImplementedException(); +} + +// This will be removed when applying the template to each user-defined struct id. +file struct TValue : IParsable, IFormattable +{ + public static TValue Parse(string s, IFormatProvider? provider) => throw new NotImplementedException(); + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out TValue result) => throw new NotImplementedException(); + public string ToString(string? format, IFormatProvider? formatProvider) => throw new NotImplementedException(); +} \ No newline at end of file