From e318ef3c3e8f7ad0980951bc1ed14fd08fa39081 Mon Sep 17 00:00:00 2001 From: Stanislav Osipov Date: Sat, 15 Aug 2020 01:14:42 +0300 Subject: [PATCH] feat: TypeConversion API --- Runtime/Utilities/Conversion.meta | 3 + .../Utilities/Conversion/ITypeConverter.cs | 21 ++ .../Conversion/ITypeConverter.cs.meta | 11 + .../Utilities/Conversion/TypeConversion.cs | 324 ++++++++++++++++++ .../Conversion/TypeConversion.cs.meta | 11 + Runtime/Utilities/Conversion/TypeConverter.cs | 52 +++ .../Conversion/TypeConverter.cs.meta | 3 + 7 files changed, 425 insertions(+) create mode 100644 Runtime/Utilities/Conversion.meta create mode 100644 Runtime/Utilities/Conversion/ITypeConverter.cs create mode 100644 Runtime/Utilities/Conversion/ITypeConverter.cs.meta create mode 100644 Runtime/Utilities/Conversion/TypeConversion.cs create mode 100644 Runtime/Utilities/Conversion/TypeConversion.cs.meta create mode 100644 Runtime/Utilities/Conversion/TypeConverter.cs create mode 100644 Runtime/Utilities/Conversion/TypeConverter.cs.meta diff --git a/Runtime/Utilities/Conversion.meta b/Runtime/Utilities/Conversion.meta new file mode 100644 index 0000000..f7fea74 --- /dev/null +++ b/Runtime/Utilities/Conversion.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e8fd8e9383f6443cbde104c574d0c368 +timeCreated: 1597441554 \ No newline at end of file diff --git a/Runtime/Utilities/Conversion/ITypeConverter.cs b/Runtime/Utilities/Conversion/ITypeConverter.cs new file mode 100644 index 0000000..952cc10 --- /dev/null +++ b/Runtime/Utilities/Conversion/ITypeConverter.cs @@ -0,0 +1,21 @@ +namespace StansAssets.Foundation +{ + /// + /// Base interface to for a strongly typed conversion. + /// + /// Conversion Destination type. + public interface ITypeConverter + { + TDestination Convert(object value); + } + + /// + /// + /// Interface to map a strong conversion between two types. + /// + /// Conversion Source type. + /// Conversion Destination type. + public interface ITypeConverter : ITypeConverter + { + } +} diff --git a/Runtime/Utilities/Conversion/ITypeConverter.cs.meta b/Runtime/Utilities/Conversion/ITypeConverter.cs.meta new file mode 100644 index 0000000..4cef846 --- /dev/null +++ b/Runtime/Utilities/Conversion/ITypeConverter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f23753c63ed345339a81994c7c91f802 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Utilities/Conversion/TypeConversion.cs b/Runtime/Utilities/Conversion/TypeConversion.cs new file mode 100644 index 0000000..789f6e9 --- /dev/null +++ b/Runtime/Utilities/Conversion/TypeConversion.cs @@ -0,0 +1,324 @@ +using System; +using System.Collections.Generic; + +namespace StansAssets.Foundation +{ + static class TypeConversion + { + static readonly Dictionary> s_Converters = new Dictionary>(); + + /// + /// Registers a new type converter + /// + /// + /// + internal static void Register(ITypeConverter converter) + { + var type = typeof(TSource); + if (s_Converters.ContainsKey(type)) + { + throw new Exception($"TypeConverter<{typeof(TSource)}, {typeof(TDestination)}> has already been registered"); + } + + s_Converters[typeof(TSource)] = converter; + } + + /// + /// Unregisters the converter from TSource to TDestination. + /// + internal static void Unregister() + { + s_Converters.Remove(typeof(TSource)); + } + + /// + /// Unregister all type converters + /// + internal static void UnregisterAll() + { + s_Converters.Clear(); + } + + /// + /// Generic conversion path where destination type is known ahead of time and source type must be resolved + /// + /// + /// + public static TDestination Convert(object value) + { + var type = value.GetType(); + var converter = GetTypeConverter(type); + + if (null == converter) + { + throw new NullReferenceException($"No TypeConverter<,> has been registered to convert '{type}' to '{typeof(TDestination)}'"); + } + + return converter.Convert(value); + } + + /// + /// Generic conversion path where destination type is known ahead of time and source type must be resolved + /// + /// + /// + /// + public static bool TryConvert(object value, out TDestination result) + { + var type = value.GetType(); + var converter = GetTypeConverter(type); + + if (null == converter) + { + result = default(TDestination); + return false; + } + + result = converter.Convert(value); + return true; + } + + /// + /// Enumerates the class hierarchy and returns the first converter found + /// + /// @NOTE This method will resolve base classes before interfaces + /// + /// + /// + static ITypeConverter GetTypeConverter(Type type) + { + var t = type; + while (null != t) + { + if (s_Converters.TryGetValue(t, out var converter)) + { + return converter; + } + + t = t.BaseType; + } + + var interfaces = type.GetInterfaces(); + foreach (var i in interfaces) + { + if (s_Converters.TryGetValue(i, out var converter)) + { + return converter; + } + } + + return null; + } + } + + /// + /// TypeConversion API + /// + public static class TypeConversion + { + static readonly HashSet s_UnregisterMethods = new HashSet(); + + /// + /// Registers a new type conversion from the given source type to the given destination type. + /// + /// Conversion delegate method. + /// Input type. + /// Output type. + public static void Register(Func conversion) + { + if (typeof(TSource) == typeof(TDestination)) + { + throw new ArgumentException( + $"Failed to register {nameof(TSource)} conversion method, source type and destination are the same."); + } + + var converter = new TypeConverter(conversion); + + TypeConversion.Register(converter); + TypeConversion.Register(converter); + + s_UnregisterMethods.Add(TypeConversion.Unregister); + s_UnregisterMethods.Add(TypeConversion.UnregisterAll); + } + + /// + /// Unregisters the converter for the given (TSource, TDestination) pair. + /// + public static void Unregister() + { + // remove specific converter if any + TypeConversion.Unregister(); + + // remove generic converter if any + TypeConversion.Unregister(); + } + + static bool IsNull(TValue value) + { + if (ReferenceEquals(value, null)) + { + return true; + } + + // Handle fake nulls + var obj = value as UnityEngine.Object; + return null != obj && !obj; + } + + /// + /// Converts given the value to the destination type + /// + /// @NOTE Fastest conversion method + /// + /// + /// + /// + /// + public static TValue Convert(TSource value) + { + return TypeConversion.Convert(value); + } + + /// + /// Try convert the given value to the destination type + /// + /// + /// + /// + /// + public static bool TryConvert(object value, out TValue result) + { + try + { + result = Convert(value); + return true; + } + catch + { + // ignored + } + + result = default; + return false; + } + + /// + /// Converts given the value to the destination type + /// + /// + /// + /// + /// + public static TDestination Convert(object source) + { + // Try a straightforward cast, this is always the best case scenario + if (source is TDestination) + { + return (TDestination) source; + } + + if (typeof(TDestination).IsValueType) + { + // There is no elegant default behaviour we can do here, we must throw in this case + if (source == null) + { + throw new Exception($"Failed to convert from 'null' to '{typeof(TDestination)}' value is null."); + } + } + else if (IsNull(source)) + { + return default(TDestination); + } + + // Try to forward to a user defined implementation for conversion + TDestination result; + if (TypeConversion.TryConvert(source, out result)) + { + return result; + } + + // At this point we can try our best to convert the value + + // Special handling of enum types + if (typeof(TDestination).IsEnum) + { + var s = source as string; + if (s != null) + { + return (TDestination) Enum.Parse(typeof(TDestination), s, true); + } + + // Try to convert to the underlying type + var v = System.Convert.ChangeType(source, Enum.GetUnderlyingType(typeof(TDestination))); + return (TDestination) v; + } + + if (source is IConvertible) + { + return (TDestination) System.Convert.ChangeType(source, typeof(TDestination)); + } + + throw new Exception($"Failed to convert from '{source?.GetType()}' to '{typeof(TDestination)}'."); + } + + /// + /// Converts given the value to the destination type + /// + /// + /// + /// + public static object Convert(object source, Type type) + { + if (type == source?.GetType()) + { + return source; + } + + if (type.IsValueType) + { + // There is no elegant default behaviour we can do here, we must throw in this case + if (source == null) + { + throw new Exception($"Failed to convert from 'null' to '{type}' value is null."); + } + } + else if (IsNull(source)) + { + return null; + } + + + // Try to forward to a user defined implementation for conversion + // object result; + + var converter = typeof(TypeConversion<>).MakeGenericType(type); + var method = converter.GetMethod("TryConvert"); + + if (null != method) + { + var parameters = new [] {source, null}; + var result = method.Invoke(null, parameters); + if ((bool) result) + { + return parameters[1]; + } + } + + // At this point we can try our best to convert the value + + // Special handling of enum types + if (type.IsEnum) + { + var s = source as string; + return s != null ? Enum.Parse(type, s) : System.Convert.ChangeType(source, Enum.GetUnderlyingType(type)); + } + + if (source is IConvertible) + { + return System.Convert.ChangeType(source, type); + } + + throw new Exception($"Failed to convert from '{source?.GetType()}' to '{type}'."); + } + } +} \ No newline at end of file diff --git a/Runtime/Utilities/Conversion/TypeConversion.cs.meta b/Runtime/Utilities/Conversion/TypeConversion.cs.meta new file mode 100644 index 0000000..bec4db1 --- /dev/null +++ b/Runtime/Utilities/Conversion/TypeConversion.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0199251da17f6426697005e571965186 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Utilities/Conversion/TypeConverter.cs b/Runtime/Utilities/Conversion/TypeConverter.cs new file mode 100644 index 0000000..378462a --- /dev/null +++ b/Runtime/Utilities/Conversion/TypeConverter.cs @@ -0,0 +1,52 @@ +using System; + +namespace StansAssets.Foundation +{ + class TypeConverter : ITypeConverter + { + readonly Func m_Conversion; + + public TypeConverter(Func conversion) + { + m_Conversion = conversion; + } + + public TDestination Convert(object value) + { + return m_Conversion((TSource) value); + } + } + + static class TypeConversion + { + static ITypeConverter s_Converter; + + /// + /// Registers a strongly typed converter + /// + /// + /// + internal static void Register(ITypeConverter converter) + { + s_Converter = converter; + } + + /// + /// Unregister the given type converter + /// + internal static void Unregister() + { + s_Converter = null; + } + + /// + /// Fast conversion path where types are known ahead of time + /// + /// + /// + public static TDestination Convert(TSource value) + { + return s_Converter.Convert(value); + } + } +} diff --git a/Runtime/Utilities/Conversion/TypeConverter.cs.meta b/Runtime/Utilities/Conversion/TypeConverter.cs.meta new file mode 100644 index 0000000..732b722 --- /dev/null +++ b/Runtime/Utilities/Conversion/TypeConverter.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2e3881b5154943d390e9b18c566f2d17 +timeCreated: 1597442423 \ No newline at end of file