< Summary

Information
Class: Elsa.Api.Client.Extensions.ObjectConverterOptions
Assembly: Elsa.Api.Client
File(s): /home/runner/work/elsa-core/elsa-core/src/clients/Elsa.Api.Client/Extensions/ObjectConverter.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 5
Coverable lines: 5
Total lines: 229
Line coverage: 0%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_SerializerOptions()100%210%
.ctor(...)100%210%

File(s)

/home/runner/work/elsa-core/elsa-core/src/clients/Elsa.Api.Client/Extensions/ObjectConverter.cs

#LineLine coverage
 1using System.Collections;
 2using System.ComponentModel;
 3using System.Dynamic;
 4using System.Globalization;
 5using System.Text.Json;
 6using System.Text.Json.Nodes;
 7using System.Text.Json.Serialization;
 8
 9namespace Elsa.Api.Client.Extensions;
 10
 11/// <summary>
 12/// Provides options to the conversion method.
 13/// </summary>
 014public record ObjectConverterOptions(JsonSerializerOptions? SerializerOptions = default)
 15{
 16    /// <inheritdoc />
 017    public ObjectConverterOptions(Action<JsonSerializerOptions> configureSerializerOptions) : this()
 18    {
 019        SerializerOptions = new JsonSerializerOptions();
 020        configureSerializerOptions(SerializerOptions);
 021    }
 22}
 23
 24/// <summary>
 25/// A helper that attempts many strategies to try and convert the source value into the destination type.
 26/// </summary>
 27public static class ObjectConverter
 28{
 29    /// <summary>
 30    /// Attempts to convert the source value into the destination type.
 31    /// </summary>
 32    public static T? ConvertTo<T>(this object? value, ObjectConverterOptions? serializerOptions = null) => value != null
 33
 34    /// <summary>
 35    /// Attempts to convert the source value into the destination type.
 36    /// </summary>
 37    public static object? ConvertTo(this object? value, Type targetType, ObjectConverterOptions? converterOptions = null
 38    {
 39        if (value == null)
 40            return default!;
 41
 42        var sourceType = value.GetType();
 43
 44        if (sourceType == targetType)
 45            return value;
 46
 47        var options = converterOptions?.SerializerOptions != null ? new JsonSerializerOptions(converterOptions.Serialize
 48        options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
 49        options.ReferenceHandler = ReferenceHandler.Preserve;
 50        options.PropertyNameCaseInsensitive = true;
 51        options.Converters.Add(new JsonStringEnumConverter());
 52
 53        var underlyingTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType;
 54        var underlyingSourceType = Nullable.GetUnderlyingType(sourceType) ?? sourceType;
 55
 56        if (value is JsonElement { ValueKind: JsonValueKind.Number } jsonNumber && underlyingTargetType == typeof(string
 57            return jsonNumber.ToString().ConvertTo(underlyingTargetType);
 58
 59        if (value is JsonElement jsonElement)
 60        {
 61            if (jsonElement.ValueKind == JsonValueKind.String && underlyingTargetType != typeof(string))
 62                return jsonElement.GetString().ConvertTo(underlyingTargetType);
 63
 64            return jsonElement.Deserialize(targetType, options);
 65        }
 66
 67        if (value is JsonNode jsonNode)
 68            return jsonNode.Deserialize(targetType, options);
 69
 70        if (underlyingSourceType == typeof(string) && !underlyingTargetType.IsPrimitive && underlyingTargetType != typeo
 71        {
 72            var stringValue = (string)value;
 73
 74            try
 75            {
 76                var firstChar = stringValue.TrimStart().FirstOrDefault();
 77
 78                if (firstChar is '{' or '[')
 79                    return JsonSerializer.Deserialize(stringValue, underlyingTargetType, options);
 80            }
 81            catch (Exception)
 82            {
 83                throw new Exception($"Failed to deserialize {stringValue} to {underlyingTargetType}");
 84            }
 85        }
 86
 87        if (targetType == typeof(object))
 88            return value;
 89
 90        if (underlyingTargetType.IsInstanceOfType(value))
 91            return value;
 92
 93        if (underlyingSourceType == underlyingTargetType)
 94            return value;
 95
 96        if (IsDateType(underlyingSourceType) && IsDateType(underlyingTargetType))
 97            return ConvertAnyDateType(value, underlyingTargetType);
 98
 99        if (typeof(IDictionary<string, object>).IsAssignableFrom(underlyingSourceType) && underlyingTargetType.IsClass)
 100        {
 101            if (typeof(ExpandoObject) == underlyingTargetType)
 102            {
 103                var expandoJson = JsonSerializer.Serialize(value);
 104                return ConvertTo(expandoJson, underlyingTargetType, converterOptions);
 105            }
 106
 107            if (typeof(IDictionary<string, object>).IsAssignableFrom(underlyingTargetType))
 108                return new Dictionary<string, object>((IDictionary<string, object>)value);
 109
 110            if (typeof(ExpandoObject) == underlyingSourceType)
 111            {
 112                // Parse ExpandoObject into target type.
 113                var expandoObject = (IDictionary<string, object>)value;
 114                var json = JsonSerializer.Serialize(expandoObject);
 115                return ConvertTo(json, underlyingTargetType, converterOptions);
 116            }
 117        }
 118
 119        var targetTypeConverter = TypeDescriptor.GetConverter(underlyingTargetType);
 120
 121        if (targetTypeConverter.CanConvertFrom(underlyingSourceType))
 122            return targetTypeConverter.IsValid(value)
 123                ? targetTypeConverter.ConvertFrom(null!, CultureInfo.InvariantCulture, value)
 124                : targetType.GetDefaultValue();
 125
 126        var sourceTypeConverter = TypeDescriptor.GetConverter(underlyingSourceType);
 127
 128        if (sourceTypeConverter.CanConvertTo(underlyingTargetType))
 129            return sourceTypeConverter.ConvertTo(value, underlyingTargetType);
 130
 131        if (underlyingTargetType.IsEnum)
 132        {
 133            if (underlyingSourceType == typeof(string))
 134                return Enum.Parse(underlyingTargetType, (string)value);
 135
 136            if (underlyingSourceType == typeof(int))
 137                return Enum.ToObject(underlyingTargetType, value);
 138
 139            if (underlyingSourceType == typeof(double))
 140                return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int), CultureInfo.InvariantC
 141        }
 142
 143        if (value is string s)
 144        {
 145            if (string.IsNullOrWhiteSpace(s))
 146                return null;
 147
 148            if (underlyingTargetType == typeof(Type))
 149                return Type.GetType(s);
 150
 151            // Perhaps it's a bit of a leap, but if the input is a string and the target type is IEnumerable<string>, th
 152            if (typeof(IEnumerable<string>).IsAssignableFrom(underlyingTargetType))
 153                return new[] { s };
 154        }
 155
 156        if (value is IEnumerable enumerable)
 157        {
 158            if (underlyingTargetType is { IsGenericType: true })
 159            {
 160                var desiredCollectionItemType = targetType.GenericTypeArguments[0];
 161                var desiredCollectionType = typeof(ICollection<>).MakeGenericType(desiredCollectionItemType);
 162
 163                if (underlyingTargetType.IsAssignableFrom(desiredCollectionType) || desiredCollectionType.IsAssignableFr
 164                {
 165                    var collectionType = typeof(List<>).MakeGenericType(desiredCollectionItemType);
 166                    var collection = (IList)Activator.CreateInstance(collectionType)!;
 167
 168                    foreach (var item in enumerable)
 169                    {
 170                        var convertedItem = ConvertTo(item, desiredCollectionItemType);
 171                        collection.Add(convertedItem);
 172                    }
 173
 174                    return collection;
 175                }
 176            }
 177        }
 178
 179        try
 180        {
 181            return Convert.ChangeType(value, underlyingTargetType, CultureInfo.InvariantCulture);
 182        }
 183        catch (InvalidCastException)
 184        {
 185            throw new Exception($"Failed to convert an object of type {sourceType} to {underlyingTargetType}");
 186        }
 187    }
 188
 189    /// <summary>
 190    /// Returns true if the specified type is date-like type, false otherwise.
 191    /// </summary>
 192    private static bool IsDateType(Type type)
 193    {
 194        var dateTypes = new[]
 195        {
 196            typeof(DateTime),
 197            typeof(DateTimeOffset)
 198        };
 199
 200        return dateTypes.Contains(type);
 201    }
 202
 203    /// <summary>
 204    /// Converts any date type to the specified target type.
 205    /// </summary>
 206    /// <param name="value">Any of <see cref="DateTime"/> or <see cref="DateTimeOffset"/>.</param>
 207    /// <param name="targetType">Any of <see cref="DateTime"/> or <see cref="DateTimeOffset"/>.</param>
 208    /// <returns>The converted value.</returns>
 209    /// <exception cref="ArgumentException">Thrown if <paramref name="value"/> is not of type <see cref="DateTime"/> or 
 210    private static object ConvertAnyDateType(object value, Type targetType)
 211    {
 212        return targetType switch
 213        {
 214            { } t when t == typeof(DateTime) => value switch
 215            {
 216                DateTime dateTime => dateTime,
 217                DateTimeOffset dateTimeOffset => dateTimeOffset.DateTime,
 218                _ => throw new ArgumentException("Invalid value type.")
 219            },
 220            { } t when t == typeof(DateTimeOffset) => value switch
 221            {
 222                DateTime dateTime => new DateTimeOffset(dateTime),
 223                DateTimeOffset dateTimeOffset => dateTimeOffset,
 224                _ => throw new ArgumentException("Invalid value type.")
 225            },
 226            _ => throw new ArgumentException("Invalid target type.")
 227        };
 228    }
 229}