diff --git a/src/CommandLineUtils/Abstractions/IValueParser.cs b/src/CommandLineUtils/Abstractions/IValueParser.cs new file mode 100644 index 00000000..b2bb7169 --- /dev/null +++ b/src/CommandLineUtils/Abstractions/IValueParser.cs @@ -0,0 +1,27 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace McMaster.Extensions.CommandLineUtils.Abstractions +{ + using System; + + /// + /// The interface for defining a value parser. + /// + public interface IValueParser + { + /// + /// Gets the Type that this value parser is defined for. + /// + Type TargetType { get; } + + /// + /// Parses the raw string value. + /// + /// The name of the argument this value will be bound to. + /// The raw string value to parse. + /// The parsed value object. + /// When the value cannot be parsed. + object Parse(string argName, string value); + } +} diff --git a/src/CommandLineUtils/Abstractions/ValueParserProvider.cs b/src/CommandLineUtils/Abstractions/ValueParserProvider.cs new file mode 100644 index 00000000..b0c50e34 --- /dev/null +++ b/src/CommandLineUtils/Abstractions/ValueParserProvider.cs @@ -0,0 +1,149 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace McMaster.Extensions.CommandLineUtils.Abstractions +{ + using System; + using System.Collections.Generic; + using System.Reflection; + + /// + /// A store of value parsers that are used to convert argument values from strings to types. + /// + public class ValueParserProvider + { + private readonly Dictionary _parsers = new Dictionary(10); + + internal ValueParserProvider() + { + this.AddRange( + new IValueParser[] + { + StringValueParser.Singleton, + BooleanValueParser.Singleton, + ByteValueParser.Singleton, + Int16ValueParser.Singleton, + Int32ValueParser.Singleton, + Int64ValueParser.Singleton, + UInt16ValueParser.Singleton, + UInt32ValueParser.Singleton, + UInt64ValueParser.Singleton, + FloatValueParser.Singleton, + DoubleValueParser.Singleton, + }); + } + + internal IValueParser GetParser(Type type) + { + if (this._parsers.TryGetValue(type, out var parser)) + { + return parser; + } + + var typeInfo = type.GetTypeInfo(); + + if (typeInfo.IsEnum) + { + return new EnumParser(type); + } + + if (ReflectionHelper.IsNullableType(typeInfo, out var wrappedType)) + { + if (wrappedType.GetTypeInfo().IsEnum) + { + return new NullableValueParser(new EnumParser(wrappedType)); + } + + if (this._parsers.TryGetValue(wrappedType, out parser)) + { + return new NullableValueParser(parser); + } + } + + return parser; + } + + + /// + /// Add a new value parser to the provider. + /// + /// An instance of the parser that is used to convert an argument from a string. + /// + /// A value parser with the same is already registered. + /// + /// is null. + public void Add(IValueParser parser) + { + this.SafeAdd(parser); + } + + /// + /// Add collection of a new value parsers to the provider. + /// + /// The collection whose parsers should be added. + /// + /// A value parser with the same is already registered. + /// + /// is null. + public void AddRange(IEnumerable parsers) + { + if (parsers == null) + { + throw new ArgumentNullException(nameof(parsers)); + } + + foreach (var parser in parsers) + { + this.SafeAdd(parser); + } + } + + /// + /// Add a new value parser to the provider, or if a value provider already exists for + /// then replaces it with . + /// + /// An instance of the parser that is used to convert an argument from a string. + /// is null. + public void AddOrReplace(IValueParser parser) + { + this.SafeAdd(parser, andReplace: true); + } + + private void SafeAdd(IValueParser parser, bool andReplace = false) + { + if (parser == null) + { + throw new ArgumentNullException(nameof(parser)); + } + + var targetType = parser.TargetType; + + if (targetType == null) + { + throw new ArgumentNullException( + nameof(IValueParser.TargetType), + "The value parser must have a target type set"); + } + + // strip nullable wrappers since we have a dedicated nullable value parser + targetType = ReflectionHelper.IsNullableType(targetType.GetTypeInfo(), out var wrappedType) + ? wrappedType + : targetType; + + if (this._parsers.ContainsKey(targetType)) + { + if (andReplace) + { + this._parsers.Remove(targetType); + } + else + { + throw new ArgumentException( + $"Value parser provider for type '{targetType}' already exists."); + } + } + + this._parsers.Add(targetType, parser); + } + } +} diff --git a/src/CommandLineUtils/Attributes/OptionAttribute.cs b/src/CommandLineUtils/Attributes/OptionAttribute.cs index aa3f8459..fb43fb78 100755 --- a/src/CommandLineUtils/Attributes/OptionAttribute.cs +++ b/src/CommandLineUtils/Attributes/OptionAttribute.cs @@ -3,6 +3,7 @@ using System; using System.Reflection; +using McMaster.Extensions.CommandLineUtils.Abstractions; namespace McMaster.Extensions.CommandLineUtils { @@ -66,7 +67,7 @@ public OptionAttribute(string template, string description, CommandOptionType op internal CommandOption Configure(CommandLineApplication app, PropertyInfo prop) { - var optionType = GetOptionType(prop); + var optionType = GetOptionType(prop, app.ValueParsers); CommandOption option; if (Template != null) { @@ -94,14 +95,14 @@ internal CommandOption Configure(CommandLineApplication app, PropertyInfo prop) return option; } - private CommandOptionType GetOptionType(PropertyInfo prop) + private CommandOptionType GetOptionType(PropertyInfo prop, ValueParserProvider valueParsers) { CommandOptionType optionType; if (OptionType.HasValue) { optionType = OptionType.Value; } - else if (!CommandOptionTypeMapper.Default.TryGetOptionType(prop.PropertyType, out optionType)) + else if (!CommandOptionTypeMapper.Default.TryGetOptionType(prop.PropertyType, valueParsers, out optionType)) { throw new InvalidOperationException(Strings.CannotDetermineOptionType(prop)); } diff --git a/src/CommandLineUtils/CommandLineApplication.Execute.cs b/src/CommandLineUtils/CommandLineApplication.Execute.cs index 9ab3d575..16e1813b 100644 --- a/src/CommandLineUtils/CommandLineApplication.Execute.cs +++ b/src/CommandLineUtils/CommandLineApplication.Execute.cs @@ -57,7 +57,7 @@ public static int Execute(CommandLineContext context) return app.Execute(context.Arguments); } } - catch (CommandParsingException ex) + catch (Exception ex) when (ex is CommandParsingException || ex is FormatException) { context.Console.Error.WriteLine(ex.Message); return ValidationErrorExitCode; diff --git a/src/CommandLineUtils/CommandLineApplication.cs b/src/CommandLineUtils/CommandLineApplication.cs index df22dc79..1ec81010 100644 --- a/src/CommandLineUtils/CommandLineApplication.cs +++ b/src/CommandLineUtils/CommandLineApplication.cs @@ -94,6 +94,7 @@ internal CommandLineApplication(CommandLineApplication parent, ValidationErrorHandler = DefaultValidationErrorHandler; SetContext(context); _services = new Lazy(() => new ServiceProvider(this)); + ValueParsers = new ValueParserProvider(); _conventionContext = CreateConventionContext(); @@ -247,6 +248,19 @@ public CommandOption OptionHelp /// public StringComparison OptionsComparison { get; set; } + /// + /// Gets the default value parser provider. + /// + /// The value parsers control how argument values are converted from strings to other types. Additional value + /// parsers can be added so that domain specific types can converted. In-built value parsers can also be replaced + /// for precise control of all type conversion. + /// + /// + /// Value parsers are currently only used by the Attribute API. + /// + /// + public ValueParserProvider ValueParsers { get; private set; } + /// /// /// Defines the working directory of the application. Defaults to . diff --git a/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs b/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs index 3db79b0a..17836f3f 100644 --- a/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs +++ b/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs @@ -99,7 +99,9 @@ private void AddArgument(PropertyInfo prop, if (argument.MultipleValues) { - var collectionParser = CollectionParserProvider.Default.GetParser(prop.PropertyType); + var collectionParser = CollectionParserProvider.Default.GetParser( + prop.PropertyType, + convention.Application.ValueParsers); if (collectionParser == null) { throw new InvalidOperationException(Strings.CannotDetermineParserType(prop)); @@ -115,7 +117,7 @@ private void AddArgument(PropertyInfo prop, } else { - var parser = ValueParserProvider.Default.GetParser(prop.PropertyType); + var parser = convention.Application.ValueParsers.GetParser(prop.PropertyType); if (parser == null) { throw new InvalidOperationException(Strings.CannotDetermineParserType(prop)); diff --git a/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs b/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs index 2aac251f..f535fa68 100644 --- a/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs +++ b/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs @@ -53,7 +53,7 @@ private protected void AddOption(ConventionContext context, CommandOption option switch (option.OptionType) { case CommandOptionType.MultipleValue: - var collectionParser = CollectionParserProvider.Default.GetParser(prop.PropertyType); + var collectionParser = CollectionParserProvider.Default.GetParser(prop.PropertyType, context.Application.ValueParsers); if (collectionParser == null) { throw new InvalidOperationException(Strings.CannotDetermineParserType(prop)); @@ -62,7 +62,7 @@ private protected void AddOption(ConventionContext context, CommandOption option setter.Invoke(context.ModelAccessor.GetModel(), collectionParser.Parse(option.LongName, option.Values))); break; case CommandOptionType.SingleOrNoValue: - var valueTupleParser = ValueTupleParserProvider.Default.GetParser(prop.PropertyType); + var valueTupleParser = ValueTupleParserProvider.Default.GetParser(prop.PropertyType, context.Application.ValueParsers); if (valueTupleParser == null) { throw new InvalidOperationException(Strings.CannotDetermineParserType(prop)); @@ -71,7 +71,7 @@ private protected void AddOption(ConventionContext context, CommandOption option setter.Invoke(context.ModelAccessor.GetModel(), valueTupleParser.Parse(option.HasValue(), option.LongName, option.Value()))); break; case CommandOptionType.SingleValue: - var parser = ValueParserProvider.Default.GetParser(prop.PropertyType); + var parser = context.Application.ValueParsers.GetParser(prop.PropertyType); if (parser == null) { throw new InvalidOperationException(Strings.CannotDetermineParserType(prop)); diff --git a/src/CommandLineUtils/Internal/CollectionParserProvider.cs b/src/CommandLineUtils/Internal/CollectionParserProvider.cs index 2dcdb480..1f03f5a7 100644 --- a/src/CommandLineUtils/Internal/CollectionParserProvider.cs +++ b/src/CommandLineUtils/Internal/CollectionParserProvider.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using McMaster.Extensions.CommandLineUtils.ValueParsers; +using McMaster.Extensions.CommandLineUtils.Abstractions; namespace McMaster.Extensions.CommandLineUtils { @@ -16,12 +16,12 @@ private CollectionParserProvider() public static CollectionParserProvider Default { get; } = new CollectionParserProvider(); - public ICollectionParser GetParser(Type type) + public ICollectionParser GetParser(Type type, ValueParserProvider valueParsers) { if (type.IsArray) { var elementType = type.GetElementType(); - var elementParser = ValueParserProvider.Default.GetParser(elementType); + var elementParser = valueParsers.GetParser(elementType); if (elementParser == null) { return null; @@ -35,7 +35,7 @@ public ICollectionParser GetParser(Type type) { var typeDef = type.GetGenericTypeDefinition(); var elementType = typeInfo.GetGenericArguments().First(); - var elementParser = ValueParserProvider.Default.GetParser(elementType); + var elementParser = valueParsers.GetParser(elementType); if (typeof(IList<>) == typeDef || typeof(IEnumerable<>) == typeDef diff --git a/src/CommandLineUtils/Internal/CommandOptionTypeMapper.cs b/src/CommandLineUtils/Internal/CommandOptionTypeMapper.cs index 26f80c1f..138a7233 100644 --- a/src/CommandLineUtils/Internal/CommandOptionTypeMapper.cs +++ b/src/CommandLineUtils/Internal/CommandOptionTypeMapper.cs @@ -5,6 +5,7 @@ using System.Collections; using System.Linq; using System.Reflection; +using McMaster.Extensions.CommandLineUtils.Abstractions; namespace McMaster.Extensions.CommandLineUtils { @@ -15,11 +16,14 @@ private CommandOptionTypeMapper() public static CommandOptionTypeMapper Default { get; } = new CommandOptionTypeMapper(); - public bool TryGetOptionType(Type clrType, out CommandOptionType optionType) + public bool TryGetOptionType( + Type clrType, + ValueParserProvider valueParsers, + out CommandOptionType optionType) { try { - optionType = GetOptionType(clrType); + optionType = GetOptionType(clrType, valueParsers); return true; } catch @@ -29,7 +33,7 @@ public bool TryGetOptionType(Type clrType, out CommandOptionType optionType) } } - public CommandOptionType GetOptionType(Type clrType) + public CommandOptionType GetOptionType(Type clrType, ValueParserProvider valueParsers = null) { if (clrType == typeof(bool)) { @@ -57,12 +61,12 @@ public CommandOptionType GetOptionType(Type clrType) var typeDef = typeInfo.GetGenericTypeDefinition(); if (typeDef == typeof(Nullable<>)) { - return GetOptionType(typeInfo.GetGenericArguments().First()); + return GetOptionType(typeInfo.GetGenericArguments().First(), valueParsers); } if (typeDef == typeof(Tuple<,>) && typeInfo.GenericTypeArguments[0] == typeof(bool)) { - if (GetOptionType(typeInfo.GenericTypeArguments[1]) == CommandOptionType.SingleValue) + if (GetOptionType(typeInfo.GenericTypeArguments[1], valueParsers) == CommandOptionType.SingleValue) { return CommandOptionType.SingleOrNoValue; } @@ -70,7 +74,7 @@ public CommandOptionType GetOptionType(Type clrType) if (typeDef == typeof(ValueTuple<,>) && typeInfo.GenericTypeArguments[0] == typeof(bool)) { - if (GetOptionType(typeInfo.GenericTypeArguments[1]) == CommandOptionType.SingleValue) + if (GetOptionType(typeInfo.GenericTypeArguments[1], valueParsers) == CommandOptionType.SingleValue) { return CommandOptionType.SingleOrNoValue; } @@ -90,7 +94,12 @@ public CommandOptionType GetOptionType(Type clrType) return CommandOptionType.SingleValue; } - throw new ArgumentException("Could not determine CommandOptionType", nameof(clrType)); + if (valueParsers?.GetParser(clrType) != null) + { + return CommandOptionType.SingleValue; + } + + throw new ArgumentException("Could not determine CommandOptionType", clrType.Name); } } } diff --git a/src/CommandLineUtils/Internal/IValueParser.cs b/src/CommandLineUtils/Internal/IValueParser.cs deleted file mode 100644 index c70a7596..00000000 --- a/src/CommandLineUtils/Internal/IValueParser.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Nate McMaster. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace McMaster.Extensions.CommandLineUtils -{ - internal interface IValueParser - { - object Parse(string argName, string value); - } -} diff --git a/src/CommandLineUtils/Internal/ReflectionHelper.cs b/src/CommandLineUtils/Internal/ReflectionHelper.cs index 6cf482f2..5dd3ad72 100644 --- a/src/CommandLineUtils/Internal/ReflectionHelper.cs +++ b/src/CommandLineUtils/Internal/ReflectionHelper.cs @@ -82,5 +82,13 @@ public static object[] BindParameters(MethodInfo method, CommandLineApplication return arguments; } + + public static bool IsNullableType(TypeInfo typeInfo, out Type wrappedType) + { + var result = typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>); + wrappedType = result ? typeInfo.GetGenericArguments().First() : null; + + return result; + } } } diff --git a/src/CommandLineUtils/Internal/ValueParserProvider.cs b/src/CommandLineUtils/Internal/ValueParserProvider.cs deleted file mode 100644 index 24e8dca6..00000000 --- a/src/CommandLineUtils/Internal/ValueParserProvider.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Nate McMaster. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using McMaster.Extensions.CommandLineUtils.ValueParsers; - -namespace McMaster.Extensions.CommandLineUtils -{ - internal class ValueParserProvider - { - private Dictionary _parsers = new Dictionary - { - { typeof(string), StringValueParser.Singleton }, - { typeof(bool), BooleanValueParser.Singleton }, - { typeof(byte), ByteValueParser.Singleton }, - { typeof(short), Int16ValueParser.Singleton }, - { typeof(int), Int32ValueParser.Singleton }, - { typeof(long), Int64ValueParser.Singleton }, - { typeof(ushort), UInt16ValueParser.Singleton }, - { typeof(uint), UInt32ValueParser.Singleton }, - { typeof(ulong), UInt64ValueParser.Singleton }, - { typeof(float), FloatValueParser.Singleton }, - { typeof(double), DoubleValueParser.Singleton }, - }; - - private ValueParserProvider() - { } - - public static ValueParserProvider Default { get; } = new ValueParserProvider(); - - public IValueParser GetParser(Type type) - { - if (_parsers.TryGetValue(type, out var parser)) - { - return parser; - } - - var typeInfo = type.GetTypeInfo(); - - if (typeInfo.IsEnum) - { - return new EnumParser(type); - } - - if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - var wrappedType = type.GetTypeInfo().GetGenericArguments().First(); - - if (wrappedType.GetTypeInfo().IsEnum) - { - return new NullableValueParser(new EnumParser(wrappedType)); - } - - if (_parsers.TryGetValue(wrappedType, out parser)) - { - return new NullableValueParser(parser); - } - } - - return parser; - } - } -} diff --git a/src/CommandLineUtils/Internal/ValueParsers/ArrayParser.cs b/src/CommandLineUtils/Internal/ValueParsers/ArrayParser.cs index f2e3b595..27a12a1b 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/ArrayParser.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/ArrayParser.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; -namespace McMaster.Extensions.CommandLineUtils.ValueParsers +namespace McMaster.Extensions.CommandLineUtils.Abstractions { internal class ArrayParser : ICollectionParser { diff --git a/src/CommandLineUtils/Internal/ValueParsers/BooleanValueParser.cs b/src/CommandLineUtils/Internal/ValueParsers/BooleanValueParser.cs index 183bf416..49598358 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/BooleanValueParser.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/BooleanValueParser.cs @@ -1,8 +1,10 @@ // Copyright (c) Nate McMaster. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -namespace McMaster.Extensions.CommandLineUtils.ValueParsers +namespace McMaster.Extensions.CommandLineUtils.Abstractions { + using System; + internal class BooleanValueParser : IValueParser { private BooleanValueParser() @@ -10,11 +12,13 @@ private BooleanValueParser() public static BooleanValueParser Singleton { get; } = new BooleanValueParser(); + public Type TargetType { get; } = typeof(bool); + public object Parse(string argName, string value) { if (!bool.TryParse(value, out var result)) { - throw new CommandParsingException(null, $"Invalid value specified for {argName}. Cannot convert '{value}' to a boolean."); + throw new FormatException($"Invalid value specified for {argName}. Cannot convert '{value}' to a boolean."); } return result; } diff --git a/src/CommandLineUtils/Internal/ValueParsers/ByteValueParser.cs b/src/CommandLineUtils/Internal/ValueParsers/ByteValueParser.cs index 16d3ff40..b0951993 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/ByteValueParser.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/ByteValueParser.cs @@ -1,8 +1,10 @@ // Copyright (c) Nate McMaster. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -namespace McMaster.Extensions.CommandLineUtils.ValueParsers +namespace McMaster.Extensions.CommandLineUtils.Abstractions { + using System; + internal class ByteValueParser : IValueParser { private ByteValueParser() @@ -10,11 +12,13 @@ private ByteValueParser() public static ByteValueParser Singleton { get; } = new ByteValueParser(); + public Type TargetType { get; } = typeof(byte); + public object Parse(string argName, string value) { if (!byte.TryParse(value, out var result)) { - throw new CommandParsingException(null, $"Invalid value specified for {argName}. '{value}' is not a valid number."); + throw new FormatException($"Invalid value specified for {argName}. '{value}' is not a valid number."); } return result; } diff --git a/src/CommandLineUtils/Internal/ValueParsers/DoubleValueParser.cs b/src/CommandLineUtils/Internal/ValueParsers/DoubleValueParser.cs index 66127b10..716200fb 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/DoubleValueParser.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/DoubleValueParser.cs @@ -1,8 +1,10 @@ // Copyright (c) Nate McMaster. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -namespace McMaster.Extensions.CommandLineUtils.ValueParsers +namespace McMaster.Extensions.CommandLineUtils.Abstractions { + using System; + internal class DoubleValueParser : IValueParser { private DoubleValueParser() @@ -10,11 +12,13 @@ private DoubleValueParser() public static DoubleValueParser Singleton { get; } = new DoubleValueParser(); + public Type TargetType { get; } = typeof(double); + public object Parse(string argName, string value) { if (!double.TryParse(value, out var result)) { - throw new CommandParsingException(null, $"Invalid value specified for {argName}. '{value}' is not a valid floating-point number."); + throw new FormatException($"Invalid value specified for {argName}. '{value}' is not a valid floating-point number."); } return result; } diff --git a/src/CommandLineUtils/Internal/ValueParsers/EnumParser.cs b/src/CommandLineUtils/Internal/ValueParsers/EnumParser.cs index f02510e7..6503a65b 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/EnumParser.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/EnumParser.cs @@ -3,7 +3,7 @@ using System; -namespace McMaster.Extensions.CommandLineUtils.ValueParsers +namespace McMaster.Extensions.CommandLineUtils.Abstractions { internal class EnumParser : IValueParser { @@ -14,6 +14,15 @@ public EnumParser(Type enumType) _enumType = enumType; } + public Type TargetType + { + get + { + // Note: Because Enum's are a special case, this value is never used + return _enumType; + } + } + public object Parse(string argName, string value) { try @@ -22,7 +31,7 @@ public object Parse(string argName, string value) } catch { - throw new CommandParsingException(null, $"Invalid value specified for {argName}. Allowed values are: {string.Join(", ", Enum.GetNames(_enumType))}."); + throw new FormatException($"Invalid value specified for {argName}. Allowed values are: {string.Join(", ", Enum.GetNames(_enumType))}."); } } } diff --git a/src/CommandLineUtils/Internal/ValueParsers/FloatValueParser.cs b/src/CommandLineUtils/Internal/ValueParsers/FloatValueParser.cs index c1528737..7a898de5 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/FloatValueParser.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/FloatValueParser.cs @@ -1,8 +1,10 @@ // Copyright (c) Nate McMaster. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -namespace McMaster.Extensions.CommandLineUtils.ValueParsers +namespace McMaster.Extensions.CommandLineUtils.Abstractions { + using System; + internal class FloatValueParser : IValueParser { private FloatValueParser() @@ -10,11 +12,13 @@ private FloatValueParser() public static FloatValueParser Singleton { get; } = new FloatValueParser(); + public Type TargetType { get; } = typeof(float); + public object Parse(string argName, string value) { if (!float.TryParse(value, out var result)) { - throw new CommandParsingException(null, $"Invalid value specified for {argName}. '{value}' is not a valid floating-point number."); + throw new FormatException($"Invalid value specified for {argName}. '{value}' is not a valid floating-point number."); } return result; } diff --git a/src/CommandLineUtils/Internal/ValueParsers/HashSetParser.cs b/src/CommandLineUtils/Internal/ValueParsers/HashSetParser.cs index f047e554..4ed37b40 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/HashSetParser.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/HashSetParser.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Reflection; -namespace McMaster.Extensions.CommandLineUtils.ValueParsers +namespace McMaster.Extensions.CommandLineUtils.Abstractions { internal class HashSetParser : ICollectionParser { diff --git a/src/CommandLineUtils/Internal/ValueParsers/Int16ValueParser.cs b/src/CommandLineUtils/Internal/ValueParsers/Int16ValueParser.cs index bbfc35de..522c1b78 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/Int16ValueParser.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/Int16ValueParser.cs @@ -1,8 +1,10 @@ // Copyright (c) Nate McMaster. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -namespace McMaster.Extensions.CommandLineUtils.ValueParsers +namespace McMaster.Extensions.CommandLineUtils.Abstractions { + using System; + internal class Int16ValueParser : IValueParser { private Int16ValueParser() @@ -10,11 +12,13 @@ private Int16ValueParser() public static Int16ValueParser Singleton { get; } = new Int16ValueParser(); + public Type TargetType { get; } = typeof(short); + public object Parse(string argName, string value) { if (!short.TryParse(value, out var result)) { - throw new CommandParsingException(null, $"Invalid value specified for {argName}. '{value}' is not a valid number."); + throw new FormatException($"Invalid value specified for {argName}. '{value}' is not a valid number."); } return result; diff --git a/src/CommandLineUtils/Internal/ValueParsers/Int32ValueParser.cs b/src/CommandLineUtils/Internal/ValueParsers/Int32ValueParser.cs index e8b2144e..d9f4ef20 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/Int32ValueParser.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/Int32ValueParser.cs @@ -1,8 +1,10 @@ // Copyright (c) Nate McMaster. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -namespace McMaster.Extensions.CommandLineUtils.ValueParsers +namespace McMaster.Extensions.CommandLineUtils.Abstractions { + using System; + internal class Int32ValueParser : IValueParser { private Int32ValueParser() @@ -10,11 +12,13 @@ private Int32ValueParser() public static Int32ValueParser Singleton { get; } = new Int32ValueParser(); + public Type TargetType { get; } = typeof(int); + public object Parse(string argName, string value) { if (!int.TryParse(value, out var result)) { - throw new CommandParsingException(null, $"Invalid value specified for {argName}. '{value}' is not a valid number."); + throw new FormatException($"Invalid value specified for {argName}. '{value}' is not a valid number."); } return result; } diff --git a/src/CommandLineUtils/Internal/ValueParsers/Int64ValueParser.cs b/src/CommandLineUtils/Internal/ValueParsers/Int64ValueParser.cs index b5e9554c..a00908a9 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/Int64ValueParser.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/Int64ValueParser.cs @@ -1,8 +1,10 @@ // Copyright (c) Nate McMaster. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -namespace McMaster.Extensions.CommandLineUtils.ValueParsers +namespace McMaster.Extensions.CommandLineUtils.Abstractions { + using System; + internal class Int64ValueParser : IValueParser { private Int64ValueParser() @@ -10,11 +12,13 @@ private Int64ValueParser() public static Int64ValueParser Singleton { get; } = new Int64ValueParser(); + public Type TargetType { get; } = typeof(long); + public object Parse(string argName, string value) { if (!long.TryParse(value, out var result)) { - throw new CommandParsingException(null, $"Invalid value specified for {argName}. '{value}' is not a valid number."); + throw new FormatException($"Invalid value specified for {argName}. '{value}' is not a valid number."); } return result; } diff --git a/src/CommandLineUtils/Internal/ValueParsers/ListParser.cs b/src/CommandLineUtils/Internal/ValueParsers/ListParser.cs index beb1d2e0..0b1ab932 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/ListParser.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/ListParser.cs @@ -5,7 +5,7 @@ using System.Collections; using System.Collections.Generic; -namespace McMaster.Extensions.CommandLineUtils.ValueParsers +namespace McMaster.Extensions.CommandLineUtils.Abstractions { internal class ListParser : ICollectionParser { diff --git a/src/CommandLineUtils/Internal/ValueParsers/NullableValueParser.cs b/src/CommandLineUtils/Internal/ValueParsers/NullableValueParser.cs index a5091844..b28a9cbd 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/NullableValueParser.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/NullableValueParser.cs @@ -1,8 +1,10 @@ // Copyright (c) Nate McMaster. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -namespace McMaster.Extensions.CommandLineUtils.ValueParsers +namespace McMaster.Extensions.CommandLineUtils.Abstractions { + using System; + internal class NullableValueParser : IValueParser { private readonly IValueParser _wrapped; @@ -12,6 +14,15 @@ public NullableValueParser(IValueParser boxedParser) _wrapped = boxedParser; } + public Type TargetType + { + get + { + throw new InvalidOperationException($"{nameof(NullableValueParser)} does not have a target type"); + } + } + + public object Parse(string argName, string value) { if (string.IsNullOrWhiteSpace(value)) diff --git a/src/CommandLineUtils/Internal/ValueParsers/StringValueParser.cs b/src/CommandLineUtils/Internal/ValueParsers/StringValueParser.cs index 57e1a285..9b6dd980 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/StringValueParser.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/StringValueParser.cs @@ -2,8 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -namespace McMaster.Extensions.CommandLineUtils.ValueParsers +namespace McMaster.Extensions.CommandLineUtils.Abstractions { + using System; + internal class StringValueParser : IValueParser { private StringValueParser() @@ -11,6 +13,8 @@ private StringValueParser() public static StringValueParser Singleton { get; } = new StringValueParser(); + public Type TargetType { get; } = typeof(string); + public object Parse(string argName, string value) => value; } } diff --git a/src/CommandLineUtils/Internal/ValueParsers/TupleValueParser.cs b/src/CommandLineUtils/Internal/ValueParsers/TupleValueParser.cs index e8980fea..2795821c 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/TupleValueParser.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/TupleValueParser.cs @@ -3,7 +3,7 @@ using System; -namespace McMaster.Extensions.CommandLineUtils.ValueParsers +namespace McMaster.Extensions.CommandLineUtils.Abstractions { internal class TupleValueParser : ITupleValueParser { diff --git a/src/CommandLineUtils/Internal/ValueParsers/UInt16ValueParser.cs b/src/CommandLineUtils/Internal/ValueParsers/UInt16ValueParser.cs index b7ed3c0b..10378f4a 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/UInt16ValueParser.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/UInt16ValueParser.cs @@ -1,8 +1,10 @@ // Copyright (c) Nate McMaster. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -namespace McMaster.Extensions.CommandLineUtils.ValueParsers +namespace McMaster.Extensions.CommandLineUtils.Abstractions { + using System; + internal class UInt16ValueParser : IValueParser { private UInt16ValueParser() @@ -10,11 +12,13 @@ private UInt16ValueParser() public static UInt16ValueParser Singleton { get; } = new UInt16ValueParser(); + public Type TargetType { get; } = typeof(ushort); + public object Parse(string argName, string value) { if (!ushort.TryParse(value, out var result)) { - throw new CommandParsingException(null, $"Invalid value specified for {argName}. '{value}' is not a valid, non-negative number."); + throw new FormatException($"Invalid value specified for {argName}. '{value}' is not a valid, non-negative number."); } return result; } diff --git a/src/CommandLineUtils/Internal/ValueParsers/UInt32ValueParser.cs b/src/CommandLineUtils/Internal/ValueParsers/UInt32ValueParser.cs index 190688a5..fd049c21 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/UInt32ValueParser.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/UInt32ValueParser.cs @@ -1,8 +1,10 @@ // Copyright (c) Nate McMaster. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -namespace McMaster.Extensions.CommandLineUtils.ValueParsers +namespace McMaster.Extensions.CommandLineUtils.Abstractions { + using System; + internal class UInt32ValueParser : IValueParser { private UInt32ValueParser() @@ -10,11 +12,13 @@ private UInt32ValueParser() public static UInt32ValueParser Singleton { get; } = new UInt32ValueParser(); + public Type TargetType { get; } = typeof(uint); + public object Parse(string argName, string value) { if (!uint.TryParse(value, out var result)) { - throw new CommandParsingException(null, $"Invalid value specified for {argName}. '{value}' is not a valid, non-negative number."); + throw new FormatException($"Invalid value specified for {argName}. '{value}' is not a valid, non-negative number."); } return result; } diff --git a/src/CommandLineUtils/Internal/ValueParsers/UInt64ValueParser.cs b/src/CommandLineUtils/Internal/ValueParsers/UInt64ValueParser.cs index c3b5e5ae..3683f3d9 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/UInt64ValueParser.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/UInt64ValueParser.cs @@ -1,8 +1,10 @@ // Copyright (c) Nate McMaster. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -namespace McMaster.Extensions.CommandLineUtils.ValueParsers +namespace McMaster.Extensions.CommandLineUtils.Abstractions { + using System; + internal class UInt64ValueParser : IValueParser { private UInt64ValueParser() @@ -10,11 +12,13 @@ private UInt64ValueParser() public static UInt64ValueParser Singleton { get; } = new UInt64ValueParser(); + public Type TargetType { get; } = typeof(ulong); + public object Parse(string argName, string value) { if (!ulong.TryParse(value, out var result)) { - throw new CommandParsingException(null, $"Invalid value specified for {argName}. '{value}' is not a valid, non-negative number."); + throw new FormatException($"Invalid value specified for {argName}. '{value}' is not a valid, non-negative number."); } return result; } diff --git a/src/CommandLineUtils/Internal/ValueParsers/ValueTupleValueParser.cs b/src/CommandLineUtils/Internal/ValueParsers/ValueTupleValueParser.cs index 583d0180..fea05086 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/ValueTupleValueParser.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/ValueTupleValueParser.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -namespace McMaster.Extensions.CommandLineUtils.ValueParsers +namespace McMaster.Extensions.CommandLineUtils.Abstractions { internal class ValueTupleValueParser : ITupleValueParser { diff --git a/src/CommandLineUtils/Internal/ValueTupleParserProvider.cs b/src/CommandLineUtils/Internal/ValueTupleParserProvider.cs index f9cd4db4..89e1b336 100644 --- a/src/CommandLineUtils/Internal/ValueTupleParserProvider.cs +++ b/src/CommandLineUtils/Internal/ValueTupleParserProvider.cs @@ -3,7 +3,7 @@ using System; using System.Reflection; -using McMaster.Extensions.CommandLineUtils.ValueParsers; +using McMaster.Extensions.CommandLineUtils.Abstractions; namespace McMaster.Extensions.CommandLineUtils { @@ -12,7 +12,7 @@ internal class ValueTupleParserProvider private ValueTupleParserProvider() { } public static ValueTupleParserProvider Default { get; } = new ValueTupleParserProvider(); - public ITupleValueParser GetParser(Type type) + public ITupleValueParser GetParser(Type type, ValueParserProvider valueParsers) { var typeInfo = type.GetTypeInfo(); if (!typeInfo.IsGenericType) @@ -22,7 +22,7 @@ public ITupleValueParser GetParser(Type type) var typeDef = typeInfo.GetGenericTypeDefinition(); if (typeDef == typeof(Tuple<,>) && typeInfo.GenericTypeArguments[0] == typeof(bool)) { - var innerParser = ValueParserProvider.Default.GetParser(typeInfo.GenericTypeArguments[1]); + var innerParser = valueParsers.GetParser(typeInfo.GenericTypeArguments[1]); if (innerParser == null) { return null; @@ -33,7 +33,7 @@ public ITupleValueParser GetParser(Type type) if (typeDef == typeof(ValueTuple<,>) && typeInfo.GenericTypeArguments[0] == typeof(bool)) { - var innerParser = ValueParserProvider.Default.GetParser(typeInfo.GenericTypeArguments[1]); + var innerParser = valueParsers.GetParser(typeInfo.GenericTypeArguments[1]); if (innerParser == null) { return null; diff --git a/test/CommandLineUtils.Tests/ValueParserProviderCustomTests.cs b/test/CommandLineUtils.Tests/ValueParserProviderCustomTests.cs new file mode 100644 index 00000000..745116f1 --- /dev/null +++ b/test/CommandLineUtils.Tests/ValueParserProviderCustomTests.cs @@ -0,0 +1,247 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using System.Collections.Generic; +using Xunit; +using McMaster.Extensions.CommandLineUtils.Abstractions; + +namespace McMaster.Extensions.CommandLineUtils.Tests +{ + public class ValueParserProviderCustomTests + { + + private class MyDateTimeOffsetParser : IValueParser + { + public Type TargetType { get; } = typeof(DateTimeOffset); + + public object Parse(string argName, string value) + { + if (!DateTimeOffset.TryParse(value, out var result)) + { + throw new FormatException($"Invalid value specified for {argName}. '{value}' is not a valid date time (with offset)"); + } + + return result; + } + } + + // scenario: specialized domain value in the format of 1=123.456=abc + private class ComplexTupleParser : IValueParser + { + public Type TargetType { get; } = typeof(ValueTuple?); + + public object Parse(string argName, string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return default(ValueTuple?); + } + + var fragments = value.Split('='); + + try + { + var item1 = double.Parse(fragments[0]); + var item2 = double.Parse(fragments[1]); + var item3 = fragments[2]; + return (ValueTuple?)(item1, item2, item3); + } + catch(Exception ex) + { + throw new FormatException( + $"Invalid value specified for {argName}. '{value} is not a valid time span (with offset)", + ex); + } + } + } + + // scenario: for some reason I insist on using thin spaces instead of commas for the thousands delimitters + private class MyDoubleParser : IValueParser + { + // This is a trivial example but rooted in a real standard + // https://en.wikipedia.org/wiki/ISO_31-0#Numbers + private readonly NumberFormatInfo _iso80000NumberFormatInfo; + + public MyDoubleParser() + { + this._iso80000NumberFormatInfo = (NumberFormatInfo)CultureInfo.InvariantCulture.NumberFormat.Clone(); + + // a thin space + this._iso80000NumberFormatInfo.NumberGroupSeparator = "\u2009"; + } + + public Type TargetType { get; } = typeof(double); + + public object Parse(string argName, string value) + { + if (!double.TryParse(value, NumberStyles.Number, _iso80000NumberFormatInfo, out var result)) + { + throw new FormatException($"Invalid value specified for {argName}. '{value}' is not a valid ISO80000 double"); + } + + return result; + } + } + + private class CustomParserProgram + { + [Argument(0)] + public DateTimeOffset DateTimeOffset { get; } + + [Argument(1)] + public double Double { get; } + + [Argument(2)] + public ValueTuple? ComplexValue { get; } + } + + [Fact] + public void CustomParsersCanBeAdded() + { + var expectedDate = new DateTimeOffset(2018, 02, 16, 21, 30, 33, 45, TimeSpan.FromHours(10)); + var expectedDouble = 123456.789; + ValueTuple? expectedComplexValue = null; + + var app = new CommandLineApplication(); + + app.ValueParsers.AddRange(new IValueParser[] { new MyDateTimeOffsetParser(), new ComplexTupleParser() }); + app.ValueParsers.AddOrReplace(new MyDoubleParser()); + + app.Conventions.UseAttributes(); + + // We're omitting the third argument to test nullable-ness. The ComplexValue type (with a value) is tested + // in the next test + var args = new[] { expectedDate.ToString("O"), "123 456.789" }; + app.Parse(args); + var model = app.Model; + + Assert.Equal(expectedDate, model.DateTimeOffset); + Assert.Equal(expectedDouble, model.Double); + Assert.Equal(expectedComplexValue, model.ComplexValue); + } + + private class CustomParserProgramOptions + { + [Option] + public ValueTuple? ComplexValue { get; } + } + + [Fact] + public void CustomParsersSupportComplexGenericTypes() + { + ValueTuple? expectedComplexValue = (1, 123.456, "abc"); + + var app = new CommandLineApplication(); + + app.ValueParsers.Add(new ComplexTupleParser()); + + app.Conventions.UseAttributes(); + + var args = $"-c=1=123.456=abc"; + app.Parse(args); + var model = app.Model; + + Assert.Equal(expectedComplexValue, model.ComplexValue); + } + + [Fact] + public void CustomParsersAreAutomaticallySingleValues() + { + var app = new CommandLineApplication(); + + app.ValueParsers.AddRange(new IValueParser[] { new MyDateTimeOffsetParser(), new ComplexTupleParser() }); + app.ValueParsers.AddOrReplace(new MyDoubleParser()); + + var optionMapper = CommandOptionTypeMapper.Default; + Assert.Equal( + CommandOptionType.SingleValue, + optionMapper.GetOptionType(typeof(DateTimeOffset), app.ValueParsers)); + Assert.Equal( + CommandOptionType.SingleValue, + optionMapper.GetOptionType(typeof(ValueTuple?), app.ValueParsers)); + Assert.Equal( + CommandOptionType.SingleValue, + optionMapper.GetOptionType(typeof(double), app.ValueParsers)); + } + + private class BadValueParser : IValueParser + { + public Type TargetType { get; } = null; + + public object Parse(string argName, string value) + { + throw new NotImplementedException(); + } + } + + [Fact] + public void ThrowsIfNoType() + { + var ex = Assert.Throws( + () => + { + var app = new CommandLineApplication(); + app.ValueParsers.Add(new BadValueParser()); + }); + + Assert.Contains("TargetType", ex.Message); + } + + [Fact] + public void ThrowsIfAlreadyRegistered() + { + var ex = Assert.Throws( + () => + { + var app = new CommandLineApplication(); + app.ValueParsers.Add(new ComplexTupleParser()); + app.ValueParsers.Add(new ComplexTupleParser()); + }); + + Assert.Contains( + "Value parser provider for type 'System.ValueTuple`3[System.Int32,System.Double,System.String]' already exists.", + ex.Message); + } + + [Fact] + public void AddThrowsIfNullParser() + { + var ex = Assert.Throws( + () => + { + var app = new CommandLineApplication(); + app.ValueParsers.Add(null); + }); + + Assert.Contains("parser", ex.Message); + } + + [Fact] + public void AddRangeThrowsIfNullCollection() + { + var ex = Assert.Throws( + () => + { + var app = new CommandLineApplication(); + app.ValueParsers.AddRange(null); + }); + + Assert.Contains("parsers", ex.Message); + } + + [Fact] + public void AddOrReplaceThrowsIfNullparser() + { + var ex = Assert.Throws( + () => + { + var app = new CommandLineApplication(); + app.ValueParsers.AddOrReplace(null); + }); + + Assert.Contains("parser", ex.Message); + } + } +}