Добавьте файлы проекта.
This commit is contained in:
30
ArgumentsToolkit.Core/ArgumentsToolkit.Core.csproj
Normal file
30
ArgumentsToolkit.Core/ArgumentsToolkit.Core.csproj
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<!-- Целевая платформа -->
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
|
||||||
|
<!-- Информация о пакете -->
|
||||||
|
<PackageId>ArgumentsToolkit.Core</PackageId>
|
||||||
|
<Version>0.1.0</Version>
|
||||||
|
<Authors>FrigaT</Authors>
|
||||||
|
<Company>FrigaT</Company>
|
||||||
|
<Description>Ядро библиотеки ArgumentsToolkit: парсинг аргументов, генерация строки, обязательные параметры, базовые конвертеры.</Description>
|
||||||
|
<PackageTags>cli arguments parser toolkit core</PackageTags>
|
||||||
|
<RepositoryUrl>https://git.frigat.duckdns.org/FrigaT/ArgumentsToolkit</RepositoryUrl>
|
||||||
|
<PackageProjectUrl>https://git.frigat.duckdns.org/FrigaT/ArgumentsToolkit</PackageProjectUrl>
|
||||||
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
|
||||||
|
<!-- Сборка и публикация -->
|
||||||
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
|
<IncludeSymbols>true</IncludeSymbols>
|
||||||
|
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||||
|
|
||||||
|
<!-- Документация и качество кода -->
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
49
ArgumentsToolkit.Core/Attributes/OptionAttribute.cs
Normal file
49
ArgumentsToolkit.Core/Attributes/OptionAttribute.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
namespace ArgumentsToolkit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Атрибут для описания параметра командной строки.
|
||||||
|
/// Позволяет задать имя, короткое имя, описание, обязательность и значение по умолчанию.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class OptionAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Поолное название для --Name
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Краткое название для -ShortName
|
||||||
|
/// </summary>
|
||||||
|
public string? ShortName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Описание параметра
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Обязательный параметр
|
||||||
|
/// </summary>
|
||||||
|
public bool Required { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Значение по умолчанию
|
||||||
|
/// </summary>
|
||||||
|
public object? DefaultValue { get; }
|
||||||
|
|
||||||
|
public OptionAttribute(
|
||||||
|
string name,
|
||||||
|
string? shortName = null,
|
||||||
|
string? description = null,
|
||||||
|
bool required = false,
|
||||||
|
object? defaultValue = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
ShortName = shortName;
|
||||||
|
Description = description;
|
||||||
|
Required = required;
|
||||||
|
DefaultValue = defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
ArgumentsToolkit.Core/Attributes/OptionConverterAttribute.cs
Normal file
15
ArgumentsToolkit.Core/Attributes/OptionConverterAttribute.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace ArgumentsToolkit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Атрибут для указания кастомного конвертера для свойства.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
|
||||||
|
public sealed class OptionConverterAttribute : Attribute
|
||||||
|
{
|
||||||
|
public Type ConverterType { get; }
|
||||||
|
|
||||||
|
public OptionConverterAttribute(Type converterType)
|
||||||
|
{
|
||||||
|
ConverterType = converterType;
|
||||||
|
}
|
||||||
|
}
|
||||||
93
ArgumentsToolkit.Core/Converters/Converters.cs
Normal file
93
ArgumentsToolkit.Core/Converters/Converters.cs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace ArgumentsToolkit;
|
||||||
|
|
||||||
|
public class DateTimeOptionConverter : IOptionConverter
|
||||||
|
{
|
||||||
|
public bool CanConvert(Type targetType) => targetType == typeof(DateTime);
|
||||||
|
public object Convert(Type targetType, string value) => DateTime.Parse(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TimeSpanOptionConverter : IOptionConverter
|
||||||
|
{
|
||||||
|
public bool CanConvert(Type targetType) => targetType == typeof(TimeSpan);
|
||||||
|
public object Convert(Type targetType, string value) => TimeSpan.Parse(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class JsonOptionConverter : IOptionConverter
|
||||||
|
{
|
||||||
|
public bool CanConvert(Type targetType) => true;
|
||||||
|
|
||||||
|
public object Convert(Type targetType, string value)
|
||||||
|
{
|
||||||
|
return System.Text.Json.JsonSerializer.Deserialize(value, targetType)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BoolOptionConverter : IOptionConverter
|
||||||
|
{
|
||||||
|
public bool CanConvert(Type targetType) => targetType == typeof(bool);
|
||||||
|
|
||||||
|
public object Convert(Type targetType, string value)
|
||||||
|
{
|
||||||
|
// когда указали просто флаг без значения
|
||||||
|
if (string.IsNullOrEmpty(value)) { return true; }
|
||||||
|
|
||||||
|
if (bool.TryParse(value, out var b)) { return b; }
|
||||||
|
|
||||||
|
if (value == "0" || value.Equals("false", StringComparison.OrdinalIgnoreCase)) { return true; }
|
||||||
|
if (value == "1" || value.Equals("true", StringComparison.OrdinalIgnoreCase)) { return true; }
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EnumOptionConverter : IOptionConverter
|
||||||
|
{
|
||||||
|
public bool CanConvert(Type targetType) => targetType.IsEnum;
|
||||||
|
|
||||||
|
public object Convert(Type targetType, string value)
|
||||||
|
{
|
||||||
|
return Enum.Parse(targetType, value, ignoreCase: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IntOptionConverter : IOptionConverter
|
||||||
|
{
|
||||||
|
public bool CanConvert(Type targetType) => targetType == typeof(int);
|
||||||
|
|
||||||
|
public object Convert(Type targetType, string value)
|
||||||
|
{
|
||||||
|
return int.Parse(value, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LongOptionConverter : IOptionConverter
|
||||||
|
{
|
||||||
|
public bool CanConvert(Type targetType) => targetType == typeof(long);
|
||||||
|
|
||||||
|
public object Convert(Type targetType, string value)
|
||||||
|
{
|
||||||
|
return long.Parse(value, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DoubleOptionConverter : IOptionConverter
|
||||||
|
{
|
||||||
|
public bool CanConvert(Type targetType) => targetType == typeof(double);
|
||||||
|
|
||||||
|
public object Convert(Type targetType, string value)
|
||||||
|
{
|
||||||
|
return double.Parse(value, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DecimalOptionConverter : IOptionConverter
|
||||||
|
{
|
||||||
|
public bool CanConvert(Type targetType) => targetType == typeof(decimal);
|
||||||
|
|
||||||
|
public object Convert(Type targetType, string value)
|
||||||
|
{
|
||||||
|
return decimal.Parse(value, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
ArgumentsToolkit.Core/Converters/IOptionConverter.cs
Normal file
17
ArgumentsToolkit.Core/Converters/IOptionConverter.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace ArgumentsToolkit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Интерфейс для конвертера значений аргументов.
|
||||||
|
/// </summary>
|
||||||
|
public interface IOptionConverter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Проверяет, может ли данный конвертер обработать указанный тип.
|
||||||
|
/// </summary>
|
||||||
|
bool CanConvert(Type targetType);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Преобразует строковое значение в указанный тип.
|
||||||
|
/// </summary>
|
||||||
|
object Convert(Type targetType, string value);
|
||||||
|
}
|
||||||
43
ArgumentsToolkit.Core/Converters/TypeConverters.cs
Normal file
43
ArgumentsToolkit.Core/Converters/TypeConverters.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace ArgumentsToolkit;
|
||||||
|
|
||||||
|
internal static class TypeConverters
|
||||||
|
{
|
||||||
|
public static bool TryConvert(string input, Type targetType, out object? value)
|
||||||
|
{
|
||||||
|
value = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (targetType == typeof(string)) { value = input; return true; }
|
||||||
|
if (targetType == typeof(bool))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input)) { value = true; return true; }
|
||||||
|
if (bool.TryParse(input, out var b)) { value = b; return true; }
|
||||||
|
if (input == "0" || input.Equals("false", StringComparison.OrdinalIgnoreCase)) { value = false; return true; }
|
||||||
|
if (input == "1" || input.Equals("true", StringComparison.OrdinalIgnoreCase)) { value = true; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (targetType.IsEnum) { value = Enum.Parse(targetType, input, ignoreCase: true); return true; }
|
||||||
|
if (targetType == typeof(int)) { value = int.Parse(input, CultureInfo.InvariantCulture); return true; }
|
||||||
|
if (targetType == typeof(long)) { value = long.Parse(input, CultureInfo.InvariantCulture); return true; }
|
||||||
|
if (targetType == typeof(double)) { value = double.Parse(input, CultureInfo.InvariantCulture); return true; }
|
||||||
|
if (targetType == typeof(decimal)) { value = decimal.Parse(input, CultureInfo.InvariantCulture); return true; }
|
||||||
|
if (targetType == typeof(Uri)) { value = new Uri(input, UriKind.RelativeOrAbsolute); return true; }
|
||||||
|
|
||||||
|
// Nullable<T>
|
||||||
|
if (Nullable.GetUnderlyingType(targetType) is Type underlying)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input)) { value = null; return true; }
|
||||||
|
return TryConvert(input, underlying, out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom types with string ctor
|
||||||
|
var ctor = targetType.GetConstructor(new[] { typeof(string) });
|
||||||
|
if (ctor != null) { value = ctor.Invoke(new object[] { input }); return true; }
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
3
ArgumentsToolkit.Core/Errors/ArgumentError.cs
Normal file
3
ArgumentsToolkit.Core/Errors/ArgumentError.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace ArgumentsToolkit;
|
||||||
|
|
||||||
|
public record ArgumentError(ErrorCode Code, string Message, string? Option = null);
|
||||||
14
ArgumentsToolkit.Core/Errors/ErrorCode.cs
Normal file
14
ArgumentsToolkit.Core/Errors/ErrorCode.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace ArgumentsToolkit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ошибки парсера
|
||||||
|
/// </summary>
|
||||||
|
public enum ErrorCode
|
||||||
|
{
|
||||||
|
UnknownOption,
|
||||||
|
MissingValue,
|
||||||
|
InvalidValue,
|
||||||
|
MissingRequired,
|
||||||
|
DuplicateOption,
|
||||||
|
ConversionFailed,
|
||||||
|
}
|
||||||
10
ArgumentsToolkit.Core/Exceptions.cs
Normal file
10
ArgumentsToolkit.Core/Exceptions.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace ArgumentsToolkit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Исключение для отсутствующих обязательных параметров.
|
||||||
|
/// </summary>
|
||||||
|
public class MissingRequiredOptionException : Exception
|
||||||
|
{
|
||||||
|
public MissingRequiredOptionException(string optionName)
|
||||||
|
: base($"Обязательный параметр '--{optionName}' не указан.") { }
|
||||||
|
}
|
||||||
308
ArgumentsToolkit.Core/Parsers/ArgumentsParser.cs
Normal file
308
ArgumentsToolkit.Core/Parsers/ArgumentsParser.cs
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace ArgumentsToolkit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Основной парсер аргументов командной строки.
|
||||||
|
/// Преобразует массив строковых аргументов в объект указанного типа.
|
||||||
|
/// </summary>
|
||||||
|
public static class ArgumentsParser
|
||||||
|
{
|
||||||
|
private static readonly List<IOptionConverter> _converters = new()
|
||||||
|
{
|
||||||
|
new DateTimeOptionConverter(),
|
||||||
|
new TimeSpanOptionConverter(),
|
||||||
|
new BoolOptionConverter(),
|
||||||
|
new IntOptionConverter(),
|
||||||
|
new LongOptionConverter(),
|
||||||
|
new DoubleOptionConverter(),
|
||||||
|
new DecimalOptionConverter(),
|
||||||
|
new EnumOptionConverter(),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static void RegisterConverter(IOptionConverter converter) => _converters.Add(converter);
|
||||||
|
public static void RegisterConverter<TConverter>()
|
||||||
|
where TConverter : IOptionConverter, new()
|
||||||
|
=> _converters.Add(new TConverter());
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Парсит массив аргументов <paramref name="args"/> в объект типа <typeparamref name="T"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Тип модели опций, содержащий свойства с атрибутами <see cref="OptionAttribute"/>.</typeparam>
|
||||||
|
/// <param name="args">Массив аргументов командной строки.</param>
|
||||||
|
/// <returns>Результат парсинга, содержащий объект и список ошибок.</returns>
|
||||||
|
|
||||||
|
public static ParseResult<T> Parse<T>(string[] args)
|
||||||
|
where T : class, new()
|
||||||
|
{
|
||||||
|
var result = new ParseResult<T> { Success = true, Value = new T() };
|
||||||
|
|
||||||
|
var props = typeof(T).GetProperties();
|
||||||
|
|
||||||
|
var optionMap = BuildOptionMap(typeof(T));
|
||||||
|
var valuesByProperty = new Dictionary<PropertyInfo, object?>();
|
||||||
|
|
||||||
|
|
||||||
|
for (int i = 0; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
var token = args[i];
|
||||||
|
|
||||||
|
if (!IsOptionToken(token))
|
||||||
|
{
|
||||||
|
result.Errors.Add(new ArgumentError(ErrorCode.UnknownOption, $"Не является токеном '{token}'"));
|
||||||
|
result.Success = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!optionMap.TryGetValue(token, out var prop))
|
||||||
|
{
|
||||||
|
result.Errors.Add(new ArgumentError(ErrorCode.UnknownOption, $"Неизвестный токен '{token}'", token));
|
||||||
|
result.Success = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetType = prop.PropertyType;
|
||||||
|
|
||||||
|
// Значение аргумента
|
||||||
|
string? raw = null;
|
||||||
|
if (targetType == typeof(bool) || targetType == typeof(bool?))
|
||||||
|
{
|
||||||
|
// Флаг: --flag или --flag=true
|
||||||
|
if (i + 1 < args.Length && !IsOptionToken(args[i + 1]))
|
||||||
|
raw = args[++i];
|
||||||
|
else
|
||||||
|
raw = string.Empty; // true
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (i + 1 >= args.Length || IsOptionToken(args[i + 1]))
|
||||||
|
{
|
||||||
|
result.Errors.Add(new ArgumentError(ErrorCode.MissingValue, $"Токен '{token}' ожидает значение"));
|
||||||
|
result.Success = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
raw = args[++i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выбор конвертера
|
||||||
|
var converterAttr = prop.GetCustomAttribute<OptionConverterAttribute>();
|
||||||
|
var converter = converterAttr != null ? (IOptionConverter)Activator.CreateInstance(converterAttr.ConverterType)! : null;
|
||||||
|
|
||||||
|
|
||||||
|
if (TryParse(targetType, raw, converter, out var converted))
|
||||||
|
{
|
||||||
|
//TODO: Generic List / Dictionary
|
||||||
|
if (valuesByProperty.ContainsKey(prop))
|
||||||
|
{
|
||||||
|
result.Errors.Add(new ArgumentError(ErrorCode.DuplicateOption, $"Токен '{token}' указан несколько раз"));
|
||||||
|
result.Success = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
valuesByProperty[prop] = converted;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.Errors.Add(new ArgumentError(ErrorCode.ConversionFailed, $"Не найден конвертер для {targetType.Name}", token));
|
||||||
|
result.Success = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Применение значений по умолчанию и проверка обязательных
|
||||||
|
foreach (var prop in optionMap.Values.Distinct())
|
||||||
|
{
|
||||||
|
var opt = prop.GetCustomAttribute<OptionAttribute>()!;
|
||||||
|
if (!valuesByProperty.TryGetValue(prop, out var value))
|
||||||
|
{
|
||||||
|
if (opt.Required)
|
||||||
|
{
|
||||||
|
result.Errors.Add(new ArgumentError(ErrorCode.MissingRequired, $"Отсутствует обязательный токен '--{opt.Name}'", opt.Name));
|
||||||
|
result.Success = false;
|
||||||
|
}
|
||||||
|
else if (opt.DefaultValue is not null)
|
||||||
|
{
|
||||||
|
valuesByProperty[prop] = opt.DefaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заполнение объекта
|
||||||
|
foreach (var kv in valuesByProperty)
|
||||||
|
{
|
||||||
|
kv.Key.SetValue(result.Value, kv.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToArguments<T>(T options)
|
||||||
|
{
|
||||||
|
var props = typeof(T).GetProperties();
|
||||||
|
var parts = new List<string>();
|
||||||
|
|
||||||
|
foreach (var prop in props)
|
||||||
|
{
|
||||||
|
var opt = prop.GetCustomAttribute<OptionAttribute>();
|
||||||
|
if (opt == null) continue;
|
||||||
|
|
||||||
|
var value = prop.GetValue(options);
|
||||||
|
if (value == null) continue;
|
||||||
|
|
||||||
|
string prefix = "--" + opt.Name;
|
||||||
|
|
||||||
|
if (prop.PropertyType == typeof(bool))
|
||||||
|
{
|
||||||
|
if ((bool)value)
|
||||||
|
parts.Add(prefix);
|
||||||
|
}
|
||||||
|
else if (prop.PropertyType.IsGenericType &&
|
||||||
|
prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>))
|
||||||
|
{
|
||||||
|
var list = (System.Collections.IList)value;
|
||||||
|
if (list.Count > 0)
|
||||||
|
parts.Add($"{prefix} {string.Join(",", list.Cast<object>())}");
|
||||||
|
}
|
||||||
|
else if (prop.PropertyType.IsGenericType &&
|
||||||
|
prop.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||||
|
{
|
||||||
|
var dict = (System.Collections.IDictionary)value;
|
||||||
|
var items = new List<string>();
|
||||||
|
foreach (var k in dict.Keys)
|
||||||
|
items.Add($"{k}={dict[k]}");
|
||||||
|
if (items.Count > 0)
|
||||||
|
parts.Add($"{prefix} {string.Join(",", items)}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
parts.Add($"{prefix} {value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(" ", parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParse(Type targetType, string input, IOptionConverter? converterAttr, out object? value)
|
||||||
|
{
|
||||||
|
if (converterAttr != null && converterAttr.CanConvert(targetType))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
value = converterAttr.Convert(targetType, input);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
value = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nullable<T>
|
||||||
|
if (Nullable.GetUnderlyingType(targetType) is Type underlying)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input)) { value = input; return true; }
|
||||||
|
return TryParse(underlying, input, converterAttr, out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var converter = _converters.LastOrDefault(c => c.CanConvert(targetType));
|
||||||
|
if (converter != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
value = converter.Convert(targetType, input);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
value = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пользовательские типы с string ctor
|
||||||
|
var ctor = targetType.GetConstructor([typeof(string)]);
|
||||||
|
if (ctor != null) { value = ctor.Invoke([input]); return true; }
|
||||||
|
|
||||||
|
|
||||||
|
//TODO: Generic types
|
||||||
|
/*
|
||||||
|
if (targetType.IsGenericType)
|
||||||
|
{
|
||||||
|
var genType = targetType.GetGenericTypeDefinition();
|
||||||
|
|
||||||
|
if (genType == typeof(List<>))
|
||||||
|
{
|
||||||
|
var elementType = targetType.GetGenericArguments()[0];
|
||||||
|
var list = (System.Collections.IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType))!;
|
||||||
|
foreach (var item in value.Split(','))
|
||||||
|
list.Add(Convert.ChangeType(item.Trim(), elementType));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (genType == typeof(Dictionary<,>))
|
||||||
|
{
|
||||||
|
var keyType = targetType.GetGenericArguments()[0];
|
||||||
|
var valueType = targetType.GetGenericArguments()[1];
|
||||||
|
var dict = (System.Collections.IDictionary)Activator.CreateInstance(targetType)!;
|
||||||
|
|
||||||
|
foreach (var pair in value.Split(','))
|
||||||
|
{
|
||||||
|
var kv = pair.Split('=');
|
||||||
|
if (kv.Length == 2)
|
||||||
|
{
|
||||||
|
var k = Convert.ChangeType(kv[0].Trim(), keyType);
|
||||||
|
var v = Convert.ChangeType(kv[1].Trim(), valueType);
|
||||||
|
dict.Add(k, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
value = Convert.ChangeType(input, targetType);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
value = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Проверяет, является ли токен аргументом (начинается с '-' или '--').
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsOptionToken(string token) => token.StartsWith("-");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Строит карту имён аргументов к свойствам модели.
|
||||||
|
/// </summary>
|
||||||
|
private static Dictionary<string, PropertyInfo> BuildOptionMap(Type optionsType)
|
||||||
|
{
|
||||||
|
var map = new Dictionary<string, PropertyInfo>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var prop in optionsType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||||
|
{
|
||||||
|
var opt = prop.GetCustomAttribute<OptionAttribute>();
|
||||||
|
if (opt == null) continue;
|
||||||
|
|
||||||
|
void add(string key)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(key))
|
||||||
|
map[key] = prop;
|
||||||
|
}
|
||||||
|
|
||||||
|
add("--" + opt.Name);
|
||||||
|
add(("-" + opt.ShortName) ?? string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
9
ArgumentsToolkit.Core/Parsers/ParseResult.cs
Normal file
9
ArgumentsToolkit.Core/Parsers/ParseResult.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ArgumentsToolkit;
|
||||||
|
|
||||||
|
public class ParseResult<T> where T : class, new()
|
||||||
|
{
|
||||||
|
public T? Value { get; internal set; }
|
||||||
|
public bool Success { get; internal set; }
|
||||||
|
public List<ArgumentError> Errors { get; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
25
ArgumentsToolkit.Help/ArgumentsToolkit.Help.csproj
Normal file
25
ArgumentsToolkit.Help/ArgumentsToolkit.Help.csproj
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<PackageId>ArgumentsToolkit.Help</PackageId>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<Authors>FrigaT</Authors>
|
||||||
|
<Company>FrigaT</Company>
|
||||||
|
<Description>Расширение для ArgumentsToolkit.Core: генерация справки (--help), вывод в консоль, Markdown/HTML.</Description>
|
||||||
|
<PackageTags>cli arguments parser help toolkit</PackageTags>
|
||||||
|
<RepositoryUrl>https://git.frigat.duckdns.org/FrigaT/ArgumentsToolkit</RepositoryUrl>
|
||||||
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
|
||||||
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ArgumentsToolkit.Core\ArgumentsToolkit.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
36
ArgumentsToolkit.Help/Collectors/HelpCollector.cs
Normal file
36
ArgumentsToolkit.Help/Collectors/HelpCollector.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace ArgumentsToolkit.Help;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сборщик информации о параметрах из модели Options.
|
||||||
|
/// </summary>
|
||||||
|
public static class HelpCollector
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Собирает метаданные о параметрах из указанной модели Options.
|
||||||
|
/// </summary>
|
||||||
|
public static HelpModel Collect<T>()
|
||||||
|
{
|
||||||
|
var model = new HelpModel();
|
||||||
|
var props = typeof(T).GetProperties();
|
||||||
|
|
||||||
|
foreach (var prop in props)
|
||||||
|
{
|
||||||
|
var opt = prop.GetCustomAttribute<OptionAttribute>();
|
||||||
|
if (opt == null) continue;
|
||||||
|
|
||||||
|
model.Entries.Add(new HelpEntry
|
||||||
|
{
|
||||||
|
Name = opt.Name,
|
||||||
|
ShortName = opt.ShortName,
|
||||||
|
Description = opt.Description,
|
||||||
|
TypeName = prop.PropertyType.Name,
|
||||||
|
Required = opt.Required,
|
||||||
|
DefaultValue = opt.DefaultValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
ArgumentsToolkit.Help/Exceptions/HelpGenerationException.cs
Normal file
13
ArgumentsToolkit.Help/Exceptions/HelpGenerationException.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace ArgumentsToolkit.Help;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Исключение, выбрасываемое при ошибках генерации справки.
|
||||||
|
/// </summary>
|
||||||
|
public class HelpGenerationException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт новое исключение генерации справки.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">Сообщение об ошибке.</param>
|
||||||
|
public HelpGenerationException(string message) : base(message) { }
|
||||||
|
}
|
||||||
29
ArgumentsToolkit.Help/Extensions/HelpExtensions.cs
Normal file
29
ArgumentsToolkit.Help/Extensions/HelpExtensions.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
namespace ArgumentsToolkit.Help;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Расширенные методы для удобного форматирования справки.
|
||||||
|
/// </summary>
|
||||||
|
public static class HelpExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Форматирует справку в Markdown.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="model">Модель справки.</param>
|
||||||
|
/// <returns>Строка в формате Markdown.</returns>
|
||||||
|
public static string AsMarkdown(this HelpModel model)
|
||||||
|
{
|
||||||
|
IHelpFormatter formatter = new MarkdownHelpFormatter();
|
||||||
|
return formatter.Format(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Форматирует справку в HTML.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="model">Модель справки.</param>
|
||||||
|
/// <returns>Строка в формате HTML.</returns>
|
||||||
|
public static string AsHtml(this HelpModel model)
|
||||||
|
{
|
||||||
|
IHelpFormatter formatter = new HtmlHelpFormatter();
|
||||||
|
return formatter.Format(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
ArgumentsToolkit.Help/Formatters/HtmlHelpFormatter.cs
Normal file
21
ArgumentsToolkit.Help/Formatters/HtmlHelpFormatter.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
namespace ArgumentsToolkit.Help;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Форматтер для генерации справки в формате HTML.
|
||||||
|
/// </summary>
|
||||||
|
public class HtmlHelpFormatter : IHelpFormatter
|
||||||
|
{
|
||||||
|
public string Format(HelpModel model)
|
||||||
|
{
|
||||||
|
var lines = new List<string> { $"<h2>{model.Title}</h2>", "<ul>" };
|
||||||
|
|
||||||
|
foreach (var entry in model.Entries)
|
||||||
|
{
|
||||||
|
string required = entry.Required ? " (обязательный)" : "";
|
||||||
|
lines.Add($"<li><b>--{entry.Name} / -{entry.ShortName}</b>: {entry.Description} ({entry.TypeName}{required})</li>");
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.Add("</ul>");
|
||||||
|
return string.Join(Environment.NewLine, lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
ArgumentsToolkit.Help/Formatters/IHelpFormatter.cs
Normal file
9
ArgumentsToolkit.Help/Formatters/IHelpFormatter.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ArgumentsToolkit.Help;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Интерфейс для форматирования справки в разные форматы.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHelpFormatter
|
||||||
|
{
|
||||||
|
string Format(HelpModel model);
|
||||||
|
}
|
||||||
20
ArgumentsToolkit.Help/Formatters/MarkdownHelpFormatter.cs
Normal file
20
ArgumentsToolkit.Help/Formatters/MarkdownHelpFormatter.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
namespace ArgumentsToolkit.Help;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Форматтер для генерации справки в формате Markdown.
|
||||||
|
/// </summary>
|
||||||
|
public class MarkdownHelpFormatter : IHelpFormatter
|
||||||
|
{
|
||||||
|
public string Format(HelpModel model)
|
||||||
|
{
|
||||||
|
var lines = new List<string> { $"## {model.Title}" };
|
||||||
|
|
||||||
|
foreach (var entry in model.Entries)
|
||||||
|
{
|
||||||
|
string required = entry.Required ? " (обязательный)" : "";
|
||||||
|
lines.Add($"- **--{entry.Name} / -{entry.ShortName}**: {entry.Description} ({entry.TypeName}{required})");
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(Environment.NewLine, lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
ArgumentsToolkit.Help/Models/HelpEntry.cs
Normal file
14
ArgumentsToolkit.Help/Models/HelpEntry.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace ArgumentsToolkit.Help;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Описание одного аргумента для справки.
|
||||||
|
/// </summary>
|
||||||
|
public class HelpEntry
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string? ShortName { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public string TypeName { get; set; }
|
||||||
|
public bool Required { get; set; }
|
||||||
|
public object? DefaultValue { get; set; }
|
||||||
|
}
|
||||||
10
ArgumentsToolkit.Help/Models/HelpModel.cs
Normal file
10
ArgumentsToolkit.Help/Models/HelpModel.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace ArgumentsToolkit.Help;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Справка по всей модели Options.
|
||||||
|
/// </summary>
|
||||||
|
public class HelpModel
|
||||||
|
{
|
||||||
|
public string Title { get; set; } = "Доступные аргументы";
|
||||||
|
public List<HelpEntry> Entries { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<PackageId>ArgumentsToolkit.Validation</PackageId>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<Authors>FrigaT</Authors>
|
||||||
|
<Company>FrigaT</Company>
|
||||||
|
<Description>Расширение для ArgumentsToolkit.Core: атрибуты и правила валидации аргументов.</Description>
|
||||||
|
<PackageTags>cli arguments parser validation toolkit</PackageTags>
|
||||||
|
<RepositoryUrl>https://git.frigat.duckdns.org/FrigaT/ArgumentsToolkit</RepositoryUrl>
|
||||||
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
|
||||||
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ArgumentsToolkit.Core\ArgumentsToolkit.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
13
ArgumentsToolkit.Validation/ValidationException.cs
Normal file
13
ArgumentsToolkit.Validation/ValidationException.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace ArgumentsToolkit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Исключение, выбрасываемое при нарушении правил валидации аргументов.
|
||||||
|
/// </summary>
|
||||||
|
public class ValidationException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт новое исключение валидации.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">Сообщение об ошибке.</param>
|
||||||
|
public ValidationException(string message) : base(message) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
namespace ArgumentsToolkit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Атрибут для проверки строкового значения на соответствие списку допустимых значений.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class AllowedValuesAttribute : ValidationAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Список допустимых значений.
|
||||||
|
/// </summary>
|
||||||
|
public string[] Values { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт новый атрибут допустимых значений.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="values">Массив допустимых строковых значений.</param>
|
||||||
|
public AllowedValuesAttribute(params string[] values)
|
||||||
|
{
|
||||||
|
Values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт новый атрибут допустимых значений.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="en">Enum допустимых значений.</param>
|
||||||
|
public AllowedValuesAttribute(Enum en)
|
||||||
|
{
|
||||||
|
var enums = Enum.GetValues(en.GetType());
|
||||||
|
List<string> values = new();
|
||||||
|
|
||||||
|
foreach (var e in enums)
|
||||||
|
{
|
||||||
|
values.Add(e.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
Values = values.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ErrorTemplate { get; set; } = "--{0}: значение {1} не входит в диапазон [{2}]";
|
||||||
|
|
||||||
|
public override bool Validate(object? value)
|
||||||
|
{
|
||||||
|
if (value is string s)
|
||||||
|
return Values.Contains(s);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string GetErrorMessage(string optionName, object? value)
|
||||||
|
{
|
||||||
|
return string.Format(ErrorTemplate, optionName, value, string.Join(", ", Values));
|
||||||
|
}
|
||||||
|
}
|
||||||
48
ArgumentsToolkit.Validation/Validations/RangeAttribute.cs
Normal file
48
ArgumentsToolkit.Validation/Validations/RangeAttribute.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
namespace ArgumentsToolkit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Атрибут для проверки числового значения на соответствие диапазону.
|
||||||
|
/// Применяется к свойствам модели Options.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public partial class RangeAttribute : ValidationAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Минимально допустимое значение.
|
||||||
|
/// </summary>
|
||||||
|
public double Min { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Максимально допустимое значение.
|
||||||
|
/// </summary>
|
||||||
|
public double Max { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт новый атрибут диапазона.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="min">Минимальное значение.</param>
|
||||||
|
/// <param name="max">Максимальное значение.</param>
|
||||||
|
public RangeAttribute(double min, double max)
|
||||||
|
{
|
||||||
|
Min = min;
|
||||||
|
Max = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ErrorTemplate { get; set; } = "--{0}: значение {1} выходит за диапазон {2}..{3}";
|
||||||
|
|
||||||
|
public override bool Validate(object? value)
|
||||||
|
{
|
||||||
|
if (value is IConvertible c)
|
||||||
|
{
|
||||||
|
var d = c.ToDouble(System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
if (d < Min || d > Max)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string GetErrorMessage(string optionName, object? value)
|
||||||
|
{
|
||||||
|
return string.Format(ErrorTemplate, optionName, value, Min, Max);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
ArgumentsToolkit.Validation/Validations/RegexAttribute.cs
Normal file
43
ArgumentsToolkit.Validation/Validations/RegexAttribute.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace ArgumentsToolkit;
|
||||||
|
|
||||||
|
public partial class RangeAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Атрибут для проверки строкового значения по регулярному выражению.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class RegexAttribute : ValidationAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Шаблон регулярного выражения.
|
||||||
|
/// </summary>
|
||||||
|
public string Pattern { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт новый атрибут регулярного выражения.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pattern">Регулярное выражение для проверки.</param>
|
||||||
|
public RegexAttribute(string pattern)
|
||||||
|
{
|
||||||
|
Pattern = pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ErrorTemplate { get; set; } = "--{0}: значение {1} не соответствует регулярному выражению '{2}'";
|
||||||
|
|
||||||
|
public override bool Validate(object? value)
|
||||||
|
{
|
||||||
|
if (value is string s)
|
||||||
|
return Regex.IsMatch(s, Pattern);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string GetErrorMessage(string optionName, object? value)
|
||||||
|
{
|
||||||
|
return string.Format(ErrorTemplate, optionName, value, Pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
namespace ArgumentsToolkit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Базовый атрибут для всех правил валидации.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
|
||||||
|
public abstract class ValidationAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>Кастомное сообщение об ошибке.</summary>
|
||||||
|
public abstract string ErrorTemplate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Проверяет значение свойства.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">Значение свойства.</param>
|
||||||
|
public abstract bool Validate(object? value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает сообщение об ошибке для указанного значения.
|
||||||
|
/// </summary>
|
||||||
|
public abstract string GetErrorMessage(string optionName, object? value);
|
||||||
|
|
||||||
|
}
|
||||||
45
ArgumentsToolkit.Validation/Validator.cs
Normal file
45
ArgumentsToolkit.Validation/Validator.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace ArgumentsToolkit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Класс для выполнения валидации модели Options на основе атрибутов.
|
||||||
|
/// </summary>
|
||||||
|
public static class Validator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Проверяет объект <paramref name="options"/> на соответствие правилам валидации.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Тип модели опций.</typeparam>
|
||||||
|
/// <param name="options">Экземпляр модели опций.</param>
|
||||||
|
/// <param name="errors">Список ошибок валидации.</param>
|
||||||
|
/// <returns>true, если ошибок нет; иначе false.</returns>
|
||||||
|
public static bool Validate<T>(T options, out string[] errors)
|
||||||
|
{
|
||||||
|
var list = new List<string>();
|
||||||
|
var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
|
||||||
|
foreach (var p in props)
|
||||||
|
{
|
||||||
|
// Проверяем только свойства с [Option]
|
||||||
|
var optionAttr = p.GetCustomAttribute<OptionAttribute>();
|
||||||
|
if (optionAttr == null) continue;
|
||||||
|
|
||||||
|
var val = p.GetValue(options);
|
||||||
|
|
||||||
|
// Берём все атрибуты, которые реализуют IValidationAttribute
|
||||||
|
foreach (var attr in p.GetCustomAttributes().OfType<ValidationAttribute>())
|
||||||
|
{
|
||||||
|
if (!attr.Validate(val))
|
||||||
|
{
|
||||||
|
var error = attr.GetErrorMessage(optionAttr.Name, val);
|
||||||
|
list.Add(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errors = list.ToArray();
|
||||||
|
return list.Count == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
7
ArgumentsToolkit.slnx
Normal file
7
ArgumentsToolkit.slnx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="ArgumentsToolkit.Core/ArgumentsToolkit.Core.csproj" />
|
||||||
|
<Project Path="ArgumentsToolkit.Help/ArgumentsToolkit.Help.csproj" />
|
||||||
|
<Project Path="ArgumentsToolkit.Validation/ArgumentsToolkit.Validation.csproj" />
|
||||||
|
<Project Path="ArgumentsToolkit/ArgumentsToolkit.csproj" />
|
||||||
|
<Project Path="Demo/Demo.csproj" />
|
||||||
|
</Solution>
|
||||||
15
ArgumentsToolkit/ArgumentsToolkit.csproj
Normal file
15
ArgumentsToolkit/ArgumentsToolkit.csproj
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ArgumentsToolkit.Core\ArgumentsToolkit.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\ArgumentsToolkit.Help\ArgumentsToolkit.Help.csproj" />
|
||||||
|
<ProjectReference Include="..\ArgumentsToolkit.Validation\ArgumentsToolkit.Validation.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
14
Demo/Demo.csproj
Normal file
14
Demo/Demo.csproj
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ArgumentsToolkit\ArgumentsToolkit.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
88
Demo/Program.cs
Normal file
88
Demo/Program.cs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
using ArgumentsToolkit;
|
||||||
|
using ArgumentsToolkit.Help;
|
||||||
|
|
||||||
|
namespace Demo;
|
||||||
|
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
static int Main(string[] args)
|
||||||
|
{ // Пример запуска:
|
||||||
|
// dotnet run --server myhost --port 8080 --env staging --mode Incremental --config "{\"Author\":\"FrigaT\",\"Timeout\":60}"
|
||||||
|
|
||||||
|
|
||||||
|
var result = ArgumentsParser.Parse<DeployOptions>(args);
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
Console.ForegroundColor = ConsoleColor.Red;
|
||||||
|
foreach (var e in result.Errors)
|
||||||
|
Console.WriteLine($"{e.Code}: {e.Message}");
|
||||||
|
Console.ResetColor();
|
||||||
|
|
||||||
|
var help = HelpCollector.Collect<DeployOptions>();
|
||||||
|
Console.WriteLine(help.AsMarkdown());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Validator.Validate(result.Value!, out var vErrors))
|
||||||
|
{
|
||||||
|
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||||
|
foreach (var e in vErrors)
|
||||||
|
Console.WriteLine($"Validation: {e}");
|
||||||
|
Console.ResetColor();
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.ForegroundColor = ConsoleColor.Green;
|
||||||
|
Console.WriteLine("Аргументы успешно разобраны и прошли валидацию:");
|
||||||
|
Console.WriteLine($"Server: {result.Value!.Server}");
|
||||||
|
Console.WriteLine($"Port: {result.Value.Port}");
|
||||||
|
Console.WriteLine($"Environment: {result.Value.Environment}");
|
||||||
|
Console.WriteLine($"DryRun: {result.Value.DryRun}");
|
||||||
|
Console.WriteLine($"Mode: {result.Value.Mode}");
|
||||||
|
Console.WriteLine($"Config.Author: {result.Value.Config.Author}");
|
||||||
|
Console.WriteLine($"Config.Timeout: {result.Value.Config.Timeout}");
|
||||||
|
Console.ResetColor();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class DeployOptions
|
||||||
|
{
|
||||||
|
[Option("server", "s", "Адрес сервера", required: true)]
|
||||||
|
public string Server { get; set; } = default!;
|
||||||
|
|
||||||
|
[Option("port", "p", "Порт подключения", defaultValue: 22)]
|
||||||
|
[Range(1, 65535)]
|
||||||
|
public int Port { get; set; }
|
||||||
|
|
||||||
|
[Option("env", "e", "Среда деплоя")]
|
||||||
|
[AllowedValues("dev", "staging", "prod")]
|
||||||
|
public string Environment { get; set; } = "dev";
|
||||||
|
|
||||||
|
[Option("dry-run", "d", "Пробный запуск без изменений")]
|
||||||
|
public bool DryRun { get; set; }
|
||||||
|
|
||||||
|
[Option("config", "c", "JSON‑конфиг для деплоя")]
|
||||||
|
[OptionConverter(typeof(JsonOptionConverter))]
|
||||||
|
public MyConfig Config { get; set; } = new();
|
||||||
|
|
||||||
|
[Option("mode", "m", "Режим деплоя")]
|
||||||
|
public DeployMode Mode { get; set; } = DeployMode.Full;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MyConfig
|
||||||
|
{
|
||||||
|
public string Author { get; set; } = "unknown";
|
||||||
|
public int Timeout { get; set; } = 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DeployMode
|
||||||
|
{
|
||||||
|
Full,
|
||||||
|
Incremental,
|
||||||
|
DryRun
|
||||||
|
}
|
||||||
12
Demo/Properties/launchSettings.json
Normal file
12
Demo/Properties/launchSettings.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"Demo": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"commandLineArgs": "--server myhost --port 8080 --env staging --mode Incremental --config \"{\\\"Author\\\":\\\"FrigaT\\\",\\\"Timeout\\\":60}"
|
||||||
|
},
|
||||||
|
"Demo (Error)": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"commandLineArgs": "--server myhost --port \"128080\" --env staging --mode Incremental --config \"{\\\"Author\\\":\\\"FrigaT\\\",\\\"Timeout\\\":60}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user