Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public override ICommonOption CreateOption(object source, PropertyInfo propertyI
private static OptionValueRestriction<IList<T>>? CreateValueRestriction(string? data)
{
return data is not null && !string.IsNullOrWhiteSpace(data)
? OptionValueRestrictionParser.ParseForList<T>(data, true)
? new OptionValueRestrictionParser().ParseForList<T>(data, true)
: null;
}
}
2 changes: 1 addition & 1 deletion Core/NetArgumentParser/Attributes/ValueOptionAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ public override ICommonOption CreateOption(object source, PropertyInfo propertyI
private static OptionValueRestriction<T>? CreateValueRestriction(string? data)
{
return data is not null && !string.IsNullOrWhiteSpace(data)
? OptionValueRestrictionParser.Parse<T>(data, true)
? new OptionValueRestrictionParser().Parse<T>(data, true)
: null;
}

Expand Down
2 changes: 1 addition & 1 deletion Core/NetArgumentParser/NetArgumentParser.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<PackageId>NetArgumentParser</PackageId>
<Version>1.0.6</Version>
<Version>1.0.7</Version>
<Product>NetArgumentParser</Product>
<Title>NetArgumentParser</Title>
<Authors>yakovypg</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,34 @@

namespace NetArgumentParser.Options.Configuration;

public static class OptionValueRestrictionParser
public class OptionValueRestrictionParser
{
public const string ExpectedFormat = "f1 p1 ...\\nOP f2 p1 ...\\n ...\\n?msg";
public const char PartsSeparator = '\n';
public const char SubpartsSeparator = ' ';
public const char MessageIndicator = '?';

public OptionValueRestrictionParser(
char partsSeparator = '\n',
char subpartsSeparator = ' ',
char negationIndicator = '!',
char messageIndicator = '?')
{
PartsSeparator = partsSeparator;
SubpartsSeparator = subpartsSeparator;
NegationIndicator = negationIndicator;
MessageIndicator = messageIndicator;
}

private enum LogicalOperator
{
And,
Or
}

public static OptionValueRestriction<T> Parse<T>(string data, bool makePredicatesSafe = false)
public char PartsSeparator { get; }
public char SubpartsSeparator { get; }
public char NegationIndicator { get; }
public char MessageIndicator { get; }

public OptionValueRestriction<T> Parse<T>(string data, bool makePredicatesSafe = false)
{
ExtendedArgumentException.ThrowIfNullOrWhiteSpace(data, nameof(data));

Expand Down Expand Up @@ -69,7 +83,7 @@ public static OptionValueRestriction<T> Parse<T>(string data, bool makePredicate
return new OptionValueRestriction<T>(isValueAllowed, valueNotSatisfyRestrictionMessage);
}

public static OptionValueRestriction<IList<T>> ParseForList<T>(string data, bool makePredicatesSafe = false)
public OptionValueRestriction<IList<T>> ParseForList<T>(string data, bool makePredicatesSafe = false)
{
ExtendedArgumentException.ThrowIfNullOrWhiteSpace(data, nameof(data));

Expand Down Expand Up @@ -97,6 +111,12 @@ private static Predicate<T> MakeSafePredicate<T>(Predicate<T> predicate)
};
}

private static Predicate<T> NegatePredicate<T>(Predicate<T> predicate)
{
ExtendedArgumentNullException.ThrowIfNull(predicate, nameof(predicate));
return t => !predicate(t);
}

private static Predicate<T> CombinePredicates<T>(List<Predicate<T>> predicates, List<LogicalOperator> connections)
{
ExtendedArgumentNullException.ThrowIfNull(predicates, nameof(predicates));
Expand Down Expand Up @@ -145,40 +165,6 @@ private static bool TryParseLogicalOperator(string data, out LogicalOperator log
return result is not null;
}

private static Predicate<T> ParsePredicate<T>(string[] data)
{
ExtendedArgumentNullException.ThrowIfNull(data, nameof(data));

if (data.Length == 0)
{
throw new ArgumentException(
$"Recieved data has incorrect format. Expected: {ExpectedFormat}",
nameof(data));
}

string name = data[0];
string[] parameters = [.. data.Skip(1)];

return name.ToUpper(CultureInfo.CurrentCulture) switch
{
"EQUAL" or "==" or "=" => ParseEqualPredicate<T>(parameters),
"NOTEQUAL" or "!=" or "<>" => ParseNotEqualPredicate<T>(parameters),
"LESS" or "<" => ParseLessPredicate<T>(parameters),
"LESSOREQUAL" or "<=" => ParseLessOrEqualPredicate<T>(parameters),
"GREATER" or ">" => ParseGreaterPredicate<T>(parameters),
"GREATEROREQUAL" or ">=" => ParseGreaterOrEqualPredicate<T>(parameters),
"INRANGE" or "MINMAX" => ParseInRangePredicate<T>(parameters),
"ONEOF" or "INLIST" => ParseOneOfPredicate<T>(parameters),
"MATCH" or "REGEX" => ParseMatchPredicate<T>(parameters),
"DIRECTORYEXISTS" or "DIRECTORY" => ParseDirectoryExistsPredicate<T>(parameters),
"FILEEXISTS" or "FILE" => ParseFileExistsPredicate<T>(parameters),
"MAXFILESIZE" or "MAXSIZE" => ParseMaxFileSizePredicate<T>(parameters),
"EXTENSION" or "EXT" => ParseFileExtensionPredicate<T>(parameters),

_ => throw new ArgumentOutOfRangeException(nameof(data), "Unknown predicate name")
};
}

private static Predicate<T> ParseComparePredicate<T>(
string[] parameters,
Func<dynamic, double, bool> compareFunc)
Expand Down Expand Up @@ -245,15 +231,46 @@ private static Predicate<T> ParseOneOfPredicate<T>(string[] parameters)
return value => parameters.Contains(value?.ToString() ?? string.Empty);
}

private static Predicate<T> ParseMatchPredicate<T>(string[] parameters)
private static Predicate<T> ParseDefaultPredicate<T>(string[] parameters)
{
ExtendedArgumentNullException.ThrowIfNull(parameters, nameof(parameters));
DefaultExceptions.ThrowIfEqual(parameters.Length, 0, nameof(parameters.Length));
DefaultExceptions.ThrowIfNotEqual(parameters.Length, 0, nameof(parameters.Length));

string pattern = string.Join($"{SubpartsSeparator}", parameters);
return value => EqualityComparer<T>.Default.Equals(value, default!);
}

var regex = new Regex(pattern);
return value => value is not null && regex.IsMatch(value.ToString() ?? string.Empty);
private static Predicate<T> ParseNullPredicate<T>(string[] parameters)
{
ExtendedArgumentNullException.ThrowIfNull(parameters, nameof(parameters));
DefaultExceptions.ThrowIfNotEqual(parameters.Length, 0, nameof(parameters.Length));

return value => value is null;
}

private static Predicate<T> ParseNullOrEmptyPredicate<T>(string[] parameters)
{
ExtendedArgumentNullException.ThrowIfNull(parameters, nameof(parameters));
DefaultExceptions.ThrowIfNotEqual(parameters.Length, 0, nameof(parameters.Length));

return value => value is null ||
(value is string text && string.IsNullOrEmpty(text));
}

private static Predicate<T> ParseNullOrWhiteSpacePredicate<T>(string[] parameters)
{
ExtendedArgumentNullException.ThrowIfNull(parameters, nameof(parameters));
DefaultExceptions.ThrowIfNotEqual(parameters.Length, 0, nameof(parameters.Length));

return value => value is null ||
(value is string text && string.IsNullOrWhiteSpace(text));
}

private static Predicate<T> ParseEmptyPredicate<T>(string[] parameters)
{
ExtendedArgumentNullException.ThrowIfNull(parameters, nameof(parameters));
DefaultExceptions.ThrowIfNotEqual(parameters.Length, 0, nameof(parameters.Length));

return value => value is string text && text.Length == 0;
}

private static Predicate<T> ParseDirectoryExistsPredicate<T>(string[] parameters)
Expand Down Expand Up @@ -333,4 +350,77 @@ private static Predicate<T> ParseFileExtensionPredicate<T>(string[] parameters)
}
};
}

private static Predicate<T> ParseFilePredicate<T>(string[] parameters)
{
ExtendedArgumentNullException.ThrowIfNull(parameters, nameof(parameters));

if (parameters.Length == 0)
return ParseFileExistsPredicate<T>(parameters);

Predicate<T> fileExistsPredicate = ParseFileExistsPredicate<T>([]);
Predicate<T> fileExtensionPredicate = ParseFileExtensionPredicate<T>(parameters);

List<Predicate<T>> predicates = [fileExistsPredicate, fileExtensionPredicate];
List<LogicalOperator> connections = [LogicalOperator.And];

return CombinePredicates<T>(predicates, connections);
}

private Predicate<T> ParseMatchPredicate<T>(string[] parameters)
{
ExtendedArgumentNullException.ThrowIfNull(parameters, nameof(parameters));
DefaultExceptions.ThrowIfEqual(parameters.Length, 0, nameof(parameters.Length));

string pattern = string.Join($"{SubpartsSeparator}", parameters);

var regex = new Regex(pattern);
return value => value is not null && regex.IsMatch(value.ToString() ?? string.Empty);
}

private Predicate<T> ParsePredicate<T>(string[] data)
{
ExtendedArgumentNullException.ThrowIfNull(data, nameof(data));

if (data.Length == 0)
{
throw new ArgumentException(
$"Recieved data has incorrect format. Expected: {ExpectedFormat}",
nameof(data));
}

bool shouldNegatePredicate = data.Length > 0 && data[0].StartsWith(NegationIndicator);

string name = shouldNegatePredicate ? data[0].Substring(1) : data[0];
string[] parameters = [.. data.Skip(1)];

Predicate<T> predicate = name.ToUpper(CultureInfo.CurrentCulture) switch
{
"EQUAL" or "==" or "=" => ParseEqualPredicate<T>(parameters),
"NOTEQUAL" or "!=" or "<>" => ParseNotEqualPredicate<T>(parameters),
"LESS" or "<" => ParseLessPredicate<T>(parameters),
"LESSOREQUAL" or "MAX" or "<=" => ParseLessOrEqualPredicate<T>(parameters),
"GREATER" or ">" => ParseGreaterPredicate<T>(parameters),
"GREATEROREQUAL" or "MIN" or ">=" => ParseGreaterOrEqualPredicate<T>(parameters),
"INRANGE" or "MINMAX" => ParseInRangePredicate<T>(parameters),
"ONEOF" or "INLIST" => ParseOneOfPredicate<T>(parameters),
"MATCH" or "REGEX" => ParseMatchPredicate<T>(parameters),
"DEFAULT" => ParseDefaultPredicate<T>(parameters),
"NULL" => ParseNullPredicate<T>(parameters),
"NULLOREMPTY" => ParseNullOrEmptyPredicate<T>(parameters),
"NULLORWHITESPACE" => ParseNullOrWhiteSpacePredicate<T>(parameters),
"EMPTY" => ParseEmptyPredicate<T>(parameters),
"DIRECTORYEXISTS" or "DIRECTORY" => ParseDirectoryExistsPredicate<T>(parameters),
"FILEEXISTS" => ParseFileExistsPredicate<T>(parameters),
"MAXFILESIZE" or "MAXSIZE" => ParseMaxFileSizePredicate<T>(parameters),
"EXTENSION" or "EXT" => ParseFileExtensionPredicate<T>(parameters),
"FILE" => ParseFilePredicate<T>(parameters),

_ => throw new ArgumentOutOfRangeException(nameof(data), "Unknown predicate name")
};

return shouldNegatePredicate
? NegatePredicate(predicate)
: predicate;
}
}
4 changes: 2 additions & 2 deletions Core/NetArgumentParser/Options/IValueOption.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ public interface IValueOption<T> : ICommonOption, IValueOptionDescriptionDesigne
string MetaVariable { get; }

IReadOnlyCollection<T> Choices { get; }
DefaultOptionValue<T>? DefaultValue { get; }
OptionValueRestriction<T>? ValueRestriction { get; }
DefaultOptionValue<T>? DefaultValue { get; set; }
OptionValueRestriction<T>? ValueRestriction { get; set; }
IValueConverter<T>? Converter { get; set; }

void HandleDefaultValue();
Expand Down
42 changes: 30 additions & 12 deletions Documentation/ParserGenerationUsingAttributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,22 +179,30 @@ logical_operator predicateK_name parameter1 ... parameterM

In other words, a string consists of one or more predicates, each separated by a newline character `\n`. Following the predicate name, its parameters are listed. A logical connective (`AND` or `OR`) may precede the predicate name; if omitted, it defaults to `AND`. A line that begins with the `?` character specifies a message to be displayed if the option value doesn't satisfy the restriction. However, this message can be omitted.

In addition, you can negate predicate. To do this, place `!` before the predicate name.

It is also important to note that parentheses are not supported, and logical connectives will be evaluated in the order in which they are specified. Thus, `x OR y AND z` will actually be interpreted as `(x OR y) AND z`. Additionally, logical connectives have aliases: for `OR`, they are `||` and `|`, while for `AND`, they are `&&` and `&`.

The following predicates are available:
1. `equal` (`==`, `=`): takes a single parameter. The option value must equal this parameter. The parameter type must be double, and the option value type must have the overloaded `==` operator.
2. `notequal` (`!=`, `<>`): takes a single parameter. The option value must differ from this parameter. The parameter type must be double, and the option value type must have the overloaded `!=` operator.
3. `less` (`<`): takes a single parameter. The option value must be less than this parameter. The parameter type must be double, and the option value type must have the overloaded `<` operator.
4. `lessorequal` (`<=`): takes a single parameter. The option value must be less than or equal to this parameter. The parameter type must be double, and the option value type must have the overloaded `<=` operator.
4. `lessorequal` (`max`, `<=`): takes a single parameter. The option value must be less than or equal to this parameter. The parameter type must be double, and the option value type must have the overloaded `<=` operator.
5. `greater` (`>`): takes a single parameter. The option value must be greater than this parameter. The parameter type must be double, and the option value type must have the overloaded `>` operator.
6. `greaterorequal` (`>=`): takes a single parameter. The option value must be greater than or equal to this parameter. The parameter type must be double, and the option value type must have the overloaded `>=` operator.
6. `greaterorequal` (`min`, `>=`): takes a single parameter. The option value must be greater than or equal to this parameter. The parameter type must be double, and the option value type must have the overloaded `>=` operator.
7. `inrange` (`minmax`): takes two parameters. The option value must be greater than or equal to the first parameter and less than or equal to the second parameter. The parameter types must be double, and the option value type must have the overloaded `>=` and `<=` operators.
8. `oneof` (`inlist`): takes one or more parameters. The option value, converted to a string, must equal one of the specified parameters.
9. `match` (`regex`): takes a single parameter. The option value, converted to a string, must match the specified parameter representing a regular expression. Anything written after the first space will be avaluated as a regular expression, so it can contain spaces.
10. `directoryexists` (`directory`): takes no parameters. The option value must be a string representing the path to an existing directory.
11. `fileexists` (`file`): takes no parameters. The option value must be a string representing the path to an existing file.
12. `maxfilesize` (`maxsize`): takes a single parameter. The option value must be a string representing the path to a file whose size is less than or equal to this parameter (in bytes).
13. `extension` (`ext`): takes one or more parameters. The option value must be a string representing the path to a file whose extension matches one of the specified parameters. The dot in the file extension is optional, and its case (uppercase or lowercase) doesn't matter.
10. `default`: takes no parameters. The option value must be the default for corresponding type.
11. `null`: takes no parameters. The option value must be null.
12. `nullorempty`: takes no parameters. The option value must be null or empty string.
13. `nullorwhitespace`: takes no parameters. The option value must be null, an empty string, or a string consisting only of whitespace characters.
14. `empty`: takes no parameters. The option value must be an empty string.
15. `directoryexists` (`directory`): takes no parameters. The option value must be a string representing the path to an existing directory.
16. `fileexists`: takes no parameters. The option value must be a string representing the path to an existing file.
17. `maxfilesize` (`maxsize`): takes a single parameter. The option value must be a string representing the path to a file whose size is less than or equal to this parameter (in bytes).
18. `extension` (`ext`): takes one or more parameters. The option value must be a string representing the path to a file whose extension matches one of the specified parameters. The dot in the file extension is optional, and its case (uppercase or lowercase) doesn't matter.
19. `file`: takes zero or more parameters. The option value must be a string representing the path to an existing file whose extension matches one of the specified parameters. If no parameters are provided, only the file's existence is checked. The dot in the file extension is optional, and its case (uppercase or lowercase) doesn't matter.

Examples of simple restrictions are provided below:
1. `== 5`: the option value must be equal to 5.
Expand All @@ -204,15 +212,25 @@ Examples of simple restrictions are provided below:
5. `> 5`: the option value must be greater than 5.
6. `>= 5`: the option value must be greater than or equal to 5.
7. `inrange 0 5`: the option value must be within the range from 0 to 5.
8. `oneof 1 3 6`: the option value must be one of the values: 1, 3 or 6.
8. `oneof 1 3 6`: the option value must be one of: 1, 3 or 6.
9. `match ^[A-Z][a-z]*$`: the option value must match the regular expression `^[A-Z][a-z]*$`.
10. `directoryexists`: the option value must be a string representing the path to an existing directory.
11. `fileexists`: the option value must be a string representing the path to an existing file.
12. `maxfilesize 10240`: the option value must be a string representing the path to a file whose size is less than or equal to 10240 bytes.
13. `extension jpg png`: the option value must be a string representing the path to a file whose extension matches either `jpg` or `png`.
10. `default`: the option value must be the default for corresponding type.
11. `null`: the option value must be null.
12. `nullorempty`: the option value must be null or an empty string.
13. `nullorwhitespace`: the option value must be null, an empty string, or a string consisting only of whitespace characters.
14. `empty`: the option value must be an empty string.
15. `directoryexists`: the option value must be a string representing the path to an existing directory.
16. `fileexists`: the option value must be a string representing the path to an existing file.
17. `maxfilesize 10240`: the option value must be a string representing the path to a file whose size is less than or equal to 10240 bytes.
18. `extension jpg png`: the option value must be a string representing the path to a file whose extension matches either `jpg` or `png`.
19. `file mp4 mkv`: the option value must be a string representing the path to an existing file whose extension matches either `mp4` or `mkv`.

Examples of restrictions with negation are provided below:
1. `!oneof 1 3 6`: the option value mustn't be one of: 1, 3 or 6.
2. `!nullorempty`: the option value mustn't be null or an empty string.

Examples of complex restrictions are provided below:
1. `< -100\nOR > 100\nOR oneof 1 5 7 10\nAND inrange -200 200`.
1. `< -100\nOR > 100\nOR oneof 1 5 7 10\nAND inrange -200 200\nAND !oneof 77 -77 88`.
2. `fileexists\n&& extension jpg png\n?file must exists and be an image`.

Finally, here is an example of creating an option with a restriction using an attribute:
Expand Down
Loading
Loading