diff --git a/CliFx.Tests/Services/CommandOptionInputConverterTests.cs b/CliFx.Tests/Services/CommandOptionInputConverterTests.cs index f42ec7d..3eee3e7 100644 --- a/CliFx.Tests/Services/CommandOptionInputConverterTests.cs +++ b/CliFx.Tests/Services/CommandOptionInputConverterTests.cs @@ -214,6 +214,12 @@ namespace CliFx.Tests.Services new[] {47, 69} ); + yield return new TestCaseData( + new CommandOptionInput("option", new[] {"47"}), + typeof(int[]), + new[] {47} + ); + yield return new TestCaseData( new CommandOptionInput("option", new[] {"value1", "value3"}), typeof(TestEnum[]), @@ -270,6 +276,16 @@ namespace CliFx.Tests.Services typeof(int) ); + yield return new TestCaseData( + new CommandOptionInput("option", new[] {"123", "456"}), + typeof(int) + ); + + yield return new TestCaseData( + new CommandOptionInput("option"), + typeof(int) + ); + yield return new TestCaseData( new CommandOptionInput("option", "123"), typeof(TestNonStringParseable) diff --git a/CliFx/Internal/Extensions.cs b/CliFx/Internal/Extensions.cs index fdedbdd..7018918 100644 --- a/CliFx/Internal/Extensions.cs +++ b/CliFx/Internal/Extensions.cs @@ -36,8 +36,13 @@ namespace CliFx.Internal public static bool Implements(this Type type, Type interfaceType) => type.GetInterfaces().Contains(interfaceType); + public static Type GetNullableUnderlyingType(this Type type) => Nullable.GetUnderlyingType(type); + public static Type GetEnumerableUnderlyingType(this Type type) { + if (type.IsPrimitive) + return null; + if (type == typeof(IEnumerable)) return typeof(object); diff --git a/CliFx/Services/CommandOptionInputConverter.cs b/CliFx/Services/CommandOptionInputConverter.cs index 41508c7..a9f9c89 100644 --- a/CliFx/Services/CommandOptionInputConverter.cs +++ b/CliFx/Services/CommandOptionInputConverter.cs @@ -113,7 +113,7 @@ namespace CliFx.Services return Enum.Parse(targetType, value, true); // Nullable - var nullableUnderlyingType = Nullable.GetUnderlyingType(targetType); + var nullableUnderlyingType = targetType.GetNullableUnderlyingType(); if (nullableUnderlyingType != null) return !value.IsNullOrWhiteSpace() ? ConvertValue(value, nullableUnderlyingType) : null; @@ -134,11 +134,11 @@ namespace CliFx.Services } catch (Exception ex) { - // An exception was thrown when trying to convert the value + // Wrap and rethrow exceptions that occur when trying to convert the value throw new CliFxException($"Can't convert value [{value}] to type [{targetType}].", ex); } - // Couldn't find a way to convert the value + // Throw if we can't find a way to convert the value throw new CliFxException($"Can't convert value [{value}] to type [{targetType}]."); } @@ -148,33 +148,49 @@ namespace CliFx.Services optionInput.GuardNotNull(nameof(optionInput)); targetType.GuardNotNull(nameof(targetType)); - // Single value - if (optionInput.Values.Count <= 1) + // Get the underlying type of IEnumerable if it's implemented by the target type. + // Ignore string type because it's IEnumerable but we don't treat it as such. + var enumerableUnderlyingType = targetType != typeof(string) ? targetType.GetEnumerableUnderlyingType() : null; + + // Convert to a non-enumerable type + if (enumerableUnderlyingType == null) { + // Throw if provided with more than 1 value + if (optionInput.Values.Count > 1) + { + throw new CliFxException( + $"Can't convert a sequence of values [{optionInput.Values.JoinToString(", ")}] " + + $"to non-enumerable type [{targetType}]."); + } + + // Retrieve a single value and convert var value = optionInput.Values.SingleOrDefault(); return ConvertValue(value, targetType); } - // Multiple values + // Convert to an enumerable type else { - // Determine underlying type of elements inside the target collection type - var underlyingType = targetType.GetEnumerableUnderlyingType() ?? typeof(object); + // Convert values to the underlying enumerable type and cast it to dynamic array + var convertedValues = optionInput.Values + .Select(v => ConvertValue(v, enumerableUnderlyingType)) + .ToNonGenericArray(enumerableUnderlyingType); - // Convert values to that type - var convertedValues = optionInput.Values.Select(v => ConvertValue(v, underlyingType)).ToNonGenericArray(underlyingType); + // Get the type of produced array var convertedValuesType = convertedValues.GetType(); - // Assignable from array of values (e.g. T[], IReadOnlyList, IEnumerable) + // Try to assign the array (works for T[], IReadOnlyList, IEnumerable, etc) if (targetType.IsAssignableFrom(convertedValuesType)) return convertedValues; - // Has a constructor that accepts an array of values (e.g. HashSet, List) + // Try to inject the array into the constructor (works for HashSet, List, etc) var arrayConstructor = targetType.GetConstructor(new[] {convertedValuesType}); if (arrayConstructor != null) return arrayConstructor.Invoke(new object[] {convertedValues}); + // Throw if we can't find a way to convert the values throw new CliFxException( - $"Can't convert sequence of values [{optionInput.Values.JoinToString(", ")}] to type [{targetType}]."); + $"Can't convert a sequence of values [{optionInput.Values.JoinToString(", ")}] " + + $"to type [{targetType}]."); } } }