Skip to content

Commit a556e33

Browse files
committed
Implemented auto-binding capability
1 parent 55c4071 commit a556e33

File tree

15 files changed

+490
-52
lines changed

15 files changed

+490
-52
lines changed

FancyHelpError/FancyConsole.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public void AddOption( List<string> keys, string? description = null, string? de
100100
optionLines.Add( description ?? "*** no description provided ***" );
101101

102102
if( defaultText != null )
103-
optionLines.Add( defaultText );
103+
optionLines.Add( $"default: {defaultText}" );
104104

105105
_grid.Children.Add( new Cell( string.Join( "\n", optionLines ) )
106106
{
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System;
2+
using System.Linq;
3+
using FluentAssertions;
4+
using J4JSoftware.CommandLine;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Xunit;
7+
8+
namespace J4JCommandLine.Tests
9+
{
10+
public class AutoBindTests
11+
{
12+
[Theory]
13+
[InlineData("-i 32 -t junk", true, 32, "junk", new string[] { })]
14+
public void working(
15+
string cmdLine,
16+
bool result,
17+
int intValue,
18+
string textValue,
19+
string[] unkeyedValues)
20+
{
21+
var builder = ServiceProvider.Instance.GetRequiredService<BindingTargetBuilder>();
22+
23+
builder.Prefixes("-")
24+
.Quotes('\'', '"')
25+
.HelpKeys("h")
26+
.ProgramName($"{nameof(AutoBindTests.working)}")
27+
.Description("a test program for exercising J4JCommandLine")
28+
.IgnoreUnprocessedUnkeyedParameters( false );
29+
30+
var target = builder.AutoBind<AutoBindProperties>();
31+
target.Should().NotBeNull();
32+
33+
target!.Options.Count().Should().Be( 3 );
34+
35+
target.Parse( new string[] { cmdLine } ).Should().Be( result );
36+
target.Value.IntProperty.Should().Be( intValue );
37+
target.Value.TextProperty.Should().Be( textValue );
38+
target.Value.Unkeyed.Should().BeEquivalentTo( unkeyedValues );
39+
}
40+
41+
[Theory]
42+
[InlineData("-i 32 -t junk")]
43+
public void broken( string cmdLine )
44+
{
45+
var builder = ServiceProvider.Instance.GetRequiredService<BindingTargetBuilder>();
46+
47+
builder.Prefixes("-")
48+
.Quotes('\'', '"')
49+
.HelpKeys("h")
50+
.ProgramName($"{nameof(AutoBindTests.working)}")
51+
.Description("a test program for exercising J4JCommandLine")
52+
.IgnoreUnprocessedUnkeyedParameters(false);
53+
54+
var target = builder.AutoBind<AutoBindPropertiesBroken>();
55+
target.Should().BeNull();
56+
}
57+
}
58+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.Collections.Generic;
2+
using System.ComponentModel;
3+
using J4JSoftware.CommandLine;
4+
5+
namespace J4JCommandLine.Tests
6+
{
7+
public class AutoBindProperties
8+
{
9+
[OptionKeys("i")]
10+
[DefaultValue(-1)]
11+
public int IntProperty { get; set; }
12+
13+
[OptionKeys("t")]
14+
[DefaultValue("abc")]
15+
public string TextProperty { get; set; }
16+
17+
[OptionKeys()]
18+
public List<string> Unkeyed { get; set; }
19+
}
20+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System.Collections.Generic;
2+
using System.ComponentModel;
3+
using J4JSoftware.CommandLine;
4+
5+
namespace J4JCommandLine.Tests
6+
{
7+
public class AutoBindPropertiesBroken
8+
{
9+
[OptionKeys("x")]
10+
[DefaultValue(-1)]
11+
public int IntProperty { get; set; }
12+
13+
[OptionKeys("x")]
14+
[DefaultValue("abc")]
15+
public string TextProperty { get; set; }
16+
17+
[OptionKeys()]
18+
public List<string> Unkeyed1 { get; set; }
19+
20+
[OptionKeys()]
21+
public List<string> Unkeyed2 { get; set; }
22+
}
23+
}

J4JCommandLine.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StaticPropertyExample", "ex
4141
EndProject
4242
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InstancePropertyExample", "examples\InstancePropertyExample\InstancePropertyExample.csproj", "{6DA4542E-A31A-422C-815F-ABEFB4F00295}"
4343
EndProject
44+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoBindExample", "examples\AutoBindExample\AutoBindExample.csproj", "{C5400387-BD0A-4E24-80B7-C532A8D1A216}"
45+
EndProject
4446
Global
4547
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4648
Debug|Any CPU = Debug|Any CPU
@@ -71,6 +73,10 @@ Global
7173
{6DA4542E-A31A-422C-815F-ABEFB4F00295}.Debug|Any CPU.Build.0 = Debug|Any CPU
7274
{6DA4542E-A31A-422C-815F-ABEFB4F00295}.Release|Any CPU.ActiveCfg = Release|Any CPU
7375
{6DA4542E-A31A-422C-815F-ABEFB4F00295}.Release|Any CPU.Build.0 = Release|Any CPU
76+
{C5400387-BD0A-4E24-80B7-C532A8D1A216}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
77+
{C5400387-BD0A-4E24-80B7-C532A8D1A216}.Debug|Any CPU.Build.0 = Debug|Any CPU
78+
{C5400387-BD0A-4E24-80B7-C532A8D1A216}.Release|Any CPU.ActiveCfg = Release|Any CPU
79+
{C5400387-BD0A-4E24-80B7-C532A8D1A216}.Release|Any CPU.Build.0 = Release|Any CPU
7480
EndGlobalSection
7581
GlobalSection(SolutionProperties) = preSolution
7682
HideSolutionNode = FALSE
@@ -79,6 +85,7 @@ Global
7985
{9CABC5BB-C8B6-4BF4-97F2-0B9BC53A88BB} = {473DDAC4-C8B9-4FBC-97C9-51AB11F038EF}
8086
{F6C3D411-BE5F-4B06-9BC7-2C03E6904F29} = {4D3E643D-3FEB-4571-AD1F-8B84313411A2}
8187
{6DA4542E-A31A-422C-815F-ABEFB4F00295} = {4D3E643D-3FEB-4571-AD1F-8B84313411A2}
88+
{C5400387-BD0A-4E24-80B7-C532A8D1A216} = {4D3E643D-3FEB-4571-AD1F-8B84313411A2}
8289
EndGlobalSection
8390
GlobalSection(ExtensibilityGlobals) = postSolution
8491
SolutionGuid = {F40695CC-CF6A-4293-81A1-04D389611E2D}

J4JCommandLine/target/BindingTarget.cs

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ internal BindingTarget()
2525
internal IAllocator Allocator { get; set; }
2626
internal IEnumerable<ITextConverter> Converters { get; set; }
2727
internal ITargetableTypeFactory TypeFactory { get; set; }
28-
internal OptionCollection Options { get; set; }
2928
public CommandLineLogger Logger { get; internal set; }
3029
internal StringComparison TextComparison { get; set; }
3130
internal MasterTextCollection MasterText { get; set; }
@@ -34,8 +33,8 @@ internal BindingTarget()
3433
public bool IsConfigured => Allocator != null && Converters != null && TypeFactory != null && Options != null
3534
&& Logger != null && MasterText != null && ConsoleOutput != null;
3635

36+
public OptionCollection Options { get; internal set; }
3737
public bool HelpRequested { get; private set; }
38-
3938
public bool IgnoreUnkeyedParameters { get; internal set; }
4039

4140
// The instance of TValue being bound to, which was either supplied in the constructor to
@@ -71,24 +70,27 @@ public Option Bind<TProp>(
7170
if( !IsConfigured )
7271
return GetUntargetedOption( keys, null, $"{this.GetType().Name} is not configured" );
7372

74-
// determine whether we were given at least one valid, unique (i.e., so far
75-
// unused) key
76-
keys = Options.GetUniqueKeys(keys);
73+
return GetKeyedOption( propertySelector.GetPropertyPathInfo(), keys );
74+
}
7775

78-
if( keys.Length == 0 )
79-
return GetUntargetedOption( keys, null, $"No unique keys defined" );
76+
internal Option Bind( OptionConfiguration optConfig )
77+
{
78+
if( !IsConfigured )
79+
return GetUntargetedOption( optConfig.Keys, null, $"{this.GetType().Name} is not configured" );
8080

81-
var property = GetTargetedProperty(propertySelector.GetPropertyPathInfo());
81+
var retVal = optConfig.Unkeyed
82+
? GetUnkeyedOption( optConfig.PropertyInfoPath )
83+
: GetKeyedOption( optConfig.PropertyInfoPath, optConfig.Keys! );
8284

83-
if( property.TargetableType.Converter == null )
84-
return GetUntargetedOption(
85-
keys,
86-
property,
87-
$"No converter for {property.TargetableType.SupportedType.Name}" );
88-
89-
var retVal = GetOption( property, true );
85+
if( !string.IsNullOrEmpty( optConfig.Description ) )
86+
retVal.SetDescription( optConfig.Description );
9087

91-
retVal.AddKeys( keys );
88+
if( optConfig.DefaultValue != null )
89+
retVal.SetDefaultValue( optConfig.DefaultValue );
90+
91+
if( optConfig.IsRequired )
92+
retVal.Required();
93+
else retVal.Optional();
9294

9395
return retVal;
9496
}
@@ -106,18 +108,9 @@ public Option Bind<TProp>(
106108
// to create an instance of it. Check the error output after parsing for details.
107109
public Option BindUnkeyed<TProp>( Expression<Func<TValue, TProp>> propertySelector )
108110
{
109-
if( !IsConfigured )
110-
return GetUntargetedOption( null, null, $"{this.GetType().Name} is not configured" );
111-
112-
var property = GetTargetedProperty(propertySelector.GetPropertyPathInfo());
113-
114-
if( property.TargetableType.Converter == null )
115-
return GetUntargetedOption(
116-
null,
117-
property,
118-
$"No converter for {property.TargetableType.SupportedType.Name}" );
119-
120-
return GetOption( property, false );
111+
return !IsConfigured
112+
? GetUntargetedOption( null, null, $"{this.GetType().Name} is not configured" )
113+
: GetUnkeyedOption( propertySelector.GetPropertyPathInfo() );
121114
}
122115

123116
// Parses the command line arguments against the Option objects bound to
@@ -256,6 +249,42 @@ private TargetedProperty GetTargetedProperty( List<PropertyInfo> pathElements )
256249
return retVal;
257250
}
258251

252+
private Option GetKeyedOption(List<PropertyInfo> propertyPath, string[] keys)
253+
{
254+
// determine whether we were given at least one valid, unique (i.e., so far
255+
// unused) key
256+
keys = Options.GetUniqueKeys(keys);
257+
258+
if (keys.Length == 0)
259+
return GetUntargetedOption(keys, null, $"No unique keys defined");
260+
261+
var property = GetTargetedProperty(propertyPath);
262+
263+
if (property.TargetableType.Converter == null)
264+
return GetUntargetedOption(
265+
keys,
266+
property,
267+
$"No converter for {property.TargetableType.SupportedType.Name}");
268+
269+
var retVal = GetOption(property, true);
270+
retVal.AddKeys(keys);
271+
272+
return retVal;
273+
}
274+
275+
private Option GetUnkeyedOption(List<PropertyInfo> propertyPath)
276+
{
277+
var property = GetTargetedProperty(propertyPath);
278+
279+
if (property.TargetableType.Converter == null)
280+
return GetUntargetedOption(
281+
null,
282+
property,
283+
$"No converter for {property.TargetableType.SupportedType.Name}");
284+
285+
return GetOption(property, false);
286+
}
287+
259288
private Option GetOption( TargetedProperty property, bool isKeyed )
260289
{
261290
var style = OptionStyle.SingleValued;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System.Linq;
2+
3+
namespace J4JSoftware.CommandLine
4+
{
5+
public partial class BindingTargetBuilder
6+
{
7+
private class Configuration
8+
{
9+
public bool Initialize<TValue>( BindingTargetBuilder builder, TValue? value = null )
10+
where TValue : class
11+
{
12+
Logger = new CommandLineLogger( builder._textComp );
13+
14+
MasterText = new MasterTextCollection(builder._textComp);
15+
MasterText.AddRange(TextUsageType.Prefix, builder._prefixes);
16+
MasterText.AddRange(TextUsageType.ValueEncloser, builder._enclosers);
17+
18+
if( builder._quotes != null )
19+
MasterText.AddRange(TextUsageType.Quote, builder._quotes.Select(q => q.ToString()));
20+
21+
if ( builder._helpKeys == null || builder._helpKeys.Length == 0 )
22+
{
23+
Logger.LogError( ProcessingPhase.Initializing, $"No help keys defined" );
24+
return false;
25+
}
26+
27+
if (value == null && !typeof(TValue).HasPublicParameterlessConstructor())
28+
{
29+
Logger.LogError(ProcessingPhase.Initializing,
30+
$"{typeof(TValue)} does not have a public parameterless constructor");
31+
32+
return false;
33+
}
34+
35+
MasterText.AddRange( TextUsageType.HelpOptionKey, builder._helpKeys );
36+
37+
return true;
38+
}
39+
40+
public CommandLineLogger Logger { get; private set; }
41+
public MasterTextCollection MasterText { get; private set; }
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)