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
3 changes: 3 additions & 0 deletions Core/NetArgumentParser/Converters/MultipleValueConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ public class MultipleValueConverter<T> : ValueConverter<IList<T>>
{
public MultipleValueConverter(Func<string, T> singleValueConverter)
: base(t => [singleValueConverter(t)]) { }

public MultipleValueConverter(Func<string, IList<T>> multipleValueConverter)
: base(t => multipleValueConverter(t)) { }
}
116 changes: 115 additions & 1 deletion Documentation/CustomConverters.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ Custom converters allow you to easily work with options whose values are non-sta
## Table of Contents
* [Converter Types](#converter-types)
* [Value Converter](#value-converter)
* [Multiple Value Converter](#multiple-value-converter)
* [Enum Value Converter](#enum-value-converter)
* [Converter Set](#converter-set)

## Converter Types
There are three converter types: value converter, multiple value converter and enum value converter. They are used in value options, multiple value options and enum value options. However, in the vast majority of situations it is sufficient to use only the value converter.
There are three converter types: value converter, multiple value converter and enum value converter. They are used in value options, multiple value options and enum value options. However, in the vast majority of situations it is sufficient to use only the value converter and multiple value converter.

Furthermore, you can create your own converters. To do this you need to inherit your class from the `IValueConverter` interface and implement it. You can also use an existing option class as a base class. See examples of this kind of inheritance, for example, by looking at the implementation of the `MultipleValueConverter` and `EnumValueConverter` classes. Next, you can use this class in the same way as the standard ones.

Expand Down Expand Up @@ -101,6 +103,118 @@ parser.Parse(new string[] { "--first", "name", "subcommand", "--second", "name"
// secondName: NAME
```

### Multiple Value Converter
To create a multiple value converter for type T, you need a function that takes a string and returns T or T[]. Pass this function to the constructor of the appropriate class as shown in the example below.

Here is an example of creating multiple value converter and using it in the parser:

```cs
var ranges = new List<PageRange>();

var rangesOption = new MultipleValueOption<PageRange>("ranges",
afterValueParsingAction: t => ranges.AddRange(t));

var converter = new MultipleValueConverter<PageRange>(PageRange.Parse);
var parser = new ArgumentParser();

parser.AddOptions(rangesOption);
parser.AddConverters(converter);

parser.Parse(new string[] { "--ranges", "1-2", "5-7" });
// ranges: [PageRange { Start = 1, End = 2 }, PageRange { Start = 5, End = 7 }]

record PageRange(int Start, int End)
{
public static PageRange Parse(string data)
{
int[] parts = data.Split('-').Select(int.Parse).ToArray();
return new PageRange(parts[0], parts[1]);
}
}
```

A similar example of using a converter with a function that returns T[] is given in the example below:

```cs
var fontSizes = new List<PageFontSize>();

var fontSizesOption = new MultipleValueOption<PageFontSize>("fonts",
afterValueParsingAction: t => fontSizes = new List<PageFontSize>(t));

var converter = new MultipleValueConverter<PageFontSize>(PageFontSize.ParseMany);
var parser = new ArgumentParser();

parser.AddOptions(fontSizesOption);
parser.AddConverters(converter);

parser.Parse(new string[] { "--fonts", "1-2:12", "5-5:16" });
/* ranges: [
* PageFontSize { PageNumber = 1, FontSize = 12 },
* PageFontSize { PageNumber = 2, FontSize = 12 },
* PageFontSize { PageNumber = 5, FontSize = 16 },
* ]
*/

record PageRange(int Start, int End)
{
public static PageRange Parse(string data)
{
int[] parts = data.Split('-').Select(int.Parse).ToArray();
return new PageRange(parts[0], parts[1]);
}
}

record PageFontSize(int PageNumber, int FontSize)
{
public static PageFontSize Parse(string data)
{
int[] parts = data.Split(';').Select(int.Parse).ToArray();
return new PageFontSize(parts[0], parts[1]);
}

public static PageFontSize[] ParseMany(string data)
{
string[] parts = data.Split(':');

PageRange pageRange = PageRange.Parse(parts[0]);
int fontSize = int.Parse(parts[1]);

IEnumerable<int> pages = Enumerable.Range(
pageRange.Start,
pageRange.End - pageRange.Start + 1);

return pages.Select(t => $"{t};{fontSize}").Select(Parse).ToArray();
}
}
```

All rules regarding the visibility level, adding converters, and using them in subcommands, as well as others, are the same as for the value converter.

### Enum Value Converter
To create an enum value converter for type T, you don't need any special functions. You only need to pass a flag indicating whether the converter should ignore case in the input string.

Here is an example of creating enum value converter and using it in the parser:

```cs
FileMode fileMode = FileMode.Create;

var fileModeOption = new EnumValueOption<FileMode>("mode",
afterValueParsingAction: t => fileMode = t);

var converter = new EnumValueConverter<FileMode>(ignoreCase: true);
var parser = new ArgumentParser();

parser.AddOptions(fileModeOption);
parser.AddConverters(converter);

parser.Parse(new string[] { "--mode", "OpeN" });
// fileMode: Open
```

If we had passed `false` as the value for `ignoreCase`, we would have encountered an error.

All rules regarding the visibility level, adding converters, and using them in subcommands, as well as others, are the same as for the value converter.

## Converter Set
You can put just one converter for each type in converter set.
```cs
Expand Down
72 changes: 70 additions & 2 deletions Examples/NetArgumentParser.Examples.AllUseCases/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,20 @@
ignoreCaseInChoices: true,
ignoreOrderInChoices: true,
contextCapture: new FixedContextCapture(3),
choices: [["Max", "Robert", "Tom"], ["David", "John", "Richard"]]),
choices: [["Max", "Robert", "Tom"], ["David", "John", "Richard"]],
afterValueParsingAction: t => resultValues.Persons = [.. t]),

new MultipleValueOption<PageRange>(
longName: "ranges",
shortName: string.Empty,
description: "ranges of pages that should be processed",
afterValueParsingAction: t => resultValues.PageRanges.AddRange(t)),

new MultipleValueOption<PageFontSize>(
longName: "fonts",
shortName: string.Empty,
description: "font sizes for pages",
afterValueParsingAction: t => resultValues.PageFontSizes = [.. t]),

new ValueOption<int>(
longName: "angle",
Expand Down Expand Up @@ -167,7 +180,10 @@

_ => throw new FormatException()
};
})
}),

new MultipleValueConverter<PageRange>(PageRange.Parse),
new MultipleValueConverter<PageFontSize>(PageFontSize.ParseMany)
};

var descriptionGenerator = new ApplicationDescriptionGenerator(parser)
Expand Down Expand Up @@ -226,12 +242,53 @@
Console.WriteLine($"Time: {resultValues.Time}");
Console.WriteLine($"File mode: {resultValues.FileMode}");
Console.WriteLine($"Input files: {string.Join(" ", resultValues.InputFiles)}");
Console.WriteLine($"Persons: {string.Join(" ", resultValues.Persons)}");
Console.WriteLine($"Page ranges: {string.Join(" ", resultValues.PageRanges)}");
Console.WriteLine($"Page font sizes: {string.Join(" ", resultValues.PageFontSizes)}");
Console.WriteLine($"Date: {resultValues.Date?.ToLongDateString()}");
Console.WriteLine($"Name: {resultValues.Name}");
Console.WriteLine($"Width: {resultValues.Width}");
Console.WriteLine($"Height: {resultValues.Height}");

#pragma warning disable
internal sealed record PageRange(int Start, int End)
{
public static PageRange Parse(string data)
{
int[] parts = data.Split('-').Select(int.Parse).ToArray();

return parts.Length == 2
? new PageRange(parts[0], parts[1])
: throw new ArgumentException("Incorrect format");
}
}

internal sealed record PageFontSize(int PageNumber, int FontSize)
{
public static PageFontSize Parse(string data)
{
int[] parts = data.Split(';').Select(int.Parse).ToArray();

return parts.Length == 2
? new PageFontSize(parts[0], parts[1])
: throw new ArgumentException("Incorrect format");
}

public static PageFontSize[] ParseMany(string data)
{
string[] parts = data.Split(':');

if (parts.Length != 2)
throw new ArgumentException("Incorrect format");

PageRange pageRange = PageRange.Parse(parts[0]);
IEnumerable<int> pages = Enumerable.Range(pageRange.Start, pageRange.End - pageRange.Start + 1);
int fontSize = int.Parse(parts[1]);

return pages.Select(t => $"{t};{fontSize}").Select(Parse).ToArray();
}
}

internal sealed class ResultValues
{
public bool Verbose { get; set; }
Expand All @@ -241,9 +298,20 @@ internal sealed class ResultValues
public TimeSpan? Time { get; set; }
public FileMode? FileMode { get; set; }
public List<string> InputFiles { get; set; } = [];
public List<string> Persons { get; set; } = [];
public List<PageRange> PageRanges { get; set; } = [new PageRange(1, 1)];
public List<PageFontSize> PageFontSizes { get; set; } = [];
public DateTime? Date { get; set; }
public string? Name { get; set; }
public int? Width { get; set; }
public int? Height { get; set; }
}
#pragma warning restore

/*
./NetArgumentParser.Examples.AllUseCases -n Name --verbose -q \
--input ./NetArgumentParser.Examples.AllUseCases ./NetArgumentParser.dll \
--persons David John Richard --ranges 2-5 6-8 --fonts 1-2:12 3-7:16 -a 45 \
-VVV --time 1,2,3,4,5 --file-mode open --date 2026 02 15 \
extra1 --extra2 extra3
*/
107 changes: 107 additions & 0 deletions Tests/NetArgumentParser.Tests/ArgumentParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,113 @@ public void Parse_ValueOptions_SpecialConverterApplied()
Assert.Empty(extraArguments);
}

[Fact]
public void Parse_MultipleValueOptions_SpecialConverterApplied()
{
const int pageMarginLeft = 10;
const int pageMarginTop = -20;
const int pageMarginRight = 30;
const int pageMarginBottom = -40;

const int contentMarginLeft = -15;
const int contentMarginTop = 25;
const int contentMarginRight = -35;
const int contentMarginBottom = 45;

const int firstPageRangeStart = 1;
const int firstPageRangeEnd = 5;
const double firstPageRangeFontSize = 12.5;

const int secondPageRangeStart = 6;
const int secondPageRangeEnd = 6;
const double secondPageRangeFontSize = 16;

const int thirdPageRangeStart = 7;
const int thirdPageRangeEnd = 10;
const double thirdPageRangeFontSize = 24;

List<Margin> margins = [];
List<PageFontSize> pageFontSizes = [];

var arguments = new string[]
{
"-m",
$"{pageMarginLeft},{pageMarginTop},{pageMarginRight},{pageMarginBottom}",
$"{contentMarginLeft},{contentMarginTop},{contentMarginRight},{contentMarginBottom}",
"-s",
$"{firstPageRangeStart}-{firstPageRangeEnd}:{firstPageRangeFontSize}",
$"{secondPageRangeStart}-{secondPageRangeEnd}:{secondPageRangeFontSize}",
$"{thirdPageRangeStart}-{thirdPageRangeEnd}:{thirdPageRangeFontSize}"
};

var options = new ICommonOption[]
{
new MultipleValueOption<Margin>(
string.Empty,
"m",
afterValueParsingAction: t => margins.AddRange(t)),

new MultipleValueOption<PageFontSize>(
string.Empty,
"s",
afterValueParsingAction: t => pageFontSizes.AddRange(t))
};

var converters = new IValueConverter[]
{
new MultipleValueConverter<Margin>(Margin.Parse),
new MultipleValueConverter<PageFontSize>(PageFontSize.ParseFromRange)
};

var parser = new ArgumentParser()
{
UseDefaultHelpOption = false
};

parser.AddOptions(options);
parser.AddConverters(converters);

_ = parser.ParseKnownArguments(arguments, out IList<string> extraArguments);

var expectedPageMargin = new Margin(
pageMarginLeft,
pageMarginTop,
pageMarginRight,
pageMarginBottom);

var expectedContentMargin = new Margin(
contentMarginLeft,
contentMarginTop,
contentMarginRight,
contentMarginBottom);

IEnumerable<Margin> expectedMargins = [expectedPageMargin, expectedContentMargin];

IEnumerable<PageFontSize> expectedPageFontSizesForFirstPageRange = Enumerable
.Range(firstPageRangeStart, firstPageRangeEnd - firstPageRangeStart + 1)
.Select(t => new PageFontSize(t, firstPageRangeFontSize));

IEnumerable<PageFontSize> expectedPageFontSizesForSecondPageRange = Enumerable
.Range(secondPageRangeStart, secondPageRangeEnd - secondPageRangeStart + 1)
.Select(t => new PageFontSize(t, secondPageRangeFontSize));

IEnumerable<PageFontSize> expectedPageFontSizesForThirdPageRange = Enumerable
.Range(thirdPageRangeStart, thirdPageRangeEnd - thirdPageRangeStart + 1)
.Select(t => new PageFontSize(t, thirdPageRangeFontSize));

IEnumerable<PageFontSize> expectedPageFontSizes = expectedPageFontSizesForFirstPageRange
.Concat(expectedPageFontSizesForSecondPageRange)
.Concat(expectedPageFontSizesForThirdPageRange);

bool marginsCorrect = margins.SequenceEqual(expectedMargins);
bool pageFontSizesCorrect = pageFontSizes.SequenceEqual(expectedPageFontSizes);

Assert.True(marginsCorrect);
Assert.True(pageFontSizesCorrect);

Assert.Empty(extraArguments);
}

[Fact]
public void Parse_ValueOptions_DefaultValueApplied()
{
Expand Down
22 changes: 22 additions & 0 deletions Tests/NetArgumentParser.Tests/Models/Margin.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Globalization;

namespace NetArgumentParser.Tests.Models;

Expand All @@ -17,6 +18,27 @@ internal Margin(int left, int top, int right, int bottom)
internal int Right { get; }
internal int Bottom { get; }

public static Margin Parse(string data)
{
ExtendedArgumentException.ThrowIfNullOrWhiteSpace(data, nameof(data));

string[] parts = data.Split(',');

if (parts.Length != 4)
{
throw new ArgumentException(
$"Recieved data has incorrect format. Expected: L,T,R,B",
nameof(data));
}

int left = int.Parse(parts[0], CultureInfo.InvariantCulture);
int top = int.Parse(parts[1], CultureInfo.InvariantCulture);
int right = int.Parse(parts[2], CultureInfo.InvariantCulture);
int bottom = int.Parse(parts[3], CultureInfo.InvariantCulture);

return new Margin(left, top, right, bottom);
}

public bool Equals(Margin? other)
{
return other is not null
Expand Down
Loading