Skip to content

Commit 87063cb

Browse files
committed
Add AttributeConvention and add to default convention set
This convention identifies all attributes on the model type and its members. Attributes that implement IConvention (on the type) or IMemberConvention (on a member) are invoked by default.
1 parent 0ef07a2 commit 87063cb

File tree

11 files changed

+302
-14
lines changed

11 files changed

+302
-14
lines changed

.appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version: 2.2.1.{build}
1+
version: 2.2.2.{build}
22
init:
33
- git config --global core.autocrlf input
44
clone_depth: 1

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## [Unreleased]
4+
5+
Minor improvement:
6+
7+
- Automatically add conventions from attributes that implement IConvention and IMemberConvention
8+
39
## [v2.2.1]
410

511
**April 10, 2018**

releasenotes.props

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,6 @@
22
<PropertyGroup>
33
<PackageReleaseNotes>
44
<![CDATA[
5-
Bug fixes:
6-
7-
- Don't assign option and argument options if no value was provided, preserving the default CLR value unless there is user-input.
8-
- Fix ShowHint() to use ShortName or SymbolName if OptionHelp.LongName is not set
9-
- Fix #85 - lower priority of resolving AdditionalServices after most built-in services
10-
- Fix #79 - OnValidate callbacks invoked before property valueswere assigned
11-
12-
Minor improvements:
13-
14-
- Improve help text generation. Align columns, show top-level command description, and add `protected virtual` API to `DefaultHelpTextGenerator` to make it easier to customize help text
15-
165
See more details here: https://github.com/natemcmaster/CommandLineUtils/blob/master/CHANGELOG.md#v$(VersionPrefix.Replace('.',''))
176
]]>
187
</PackageReleaseNotes>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) Nate McMaster.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
#pragma warning disable CS0649 // this sample uses a field with a setter that runs via reflection
5+
6+
using System;
7+
using System.Reflection;
8+
using McMaster.Extensions.CommandLineUtils;
9+
using McMaster.Extensions.CommandLineUtils.Conventions;
10+
11+
[MyClassConvention]
12+
public class AttributeConventionProgram
13+
{
14+
public static int Main(string[] args) => CommandLineApplication.Execute<AttributeConventionProgram>(args);
15+
16+
[MyFieldConvention]
17+
private string _workingDirectory;
18+
19+
private void OnExecute()
20+
{
21+
Console.WriteLine("cwd = " + _workingDirectory);
22+
}
23+
}
24+
25+
// Custom attributes that apply to the model type should implement IConvention
26+
27+
[AttributeUsage(AttributeTargets.Class)]
28+
internal class MyClassConventionAttribute : Attribute, IConvention
29+
{
30+
public void Apply(ConventionContext context)
31+
{
32+
context.Application.Description = "This command is defined in " + context.ModelType.Assembly.FullName;
33+
}
34+
}
35+
36+
37+
// Custom attributes that apply to fields, events, methods, and properties should implement IMemberConvention
38+
39+
[AttributeUsage(AttributeTargets.Field)]
40+
internal class MyFieldConventionAttribute : Attribute, IMemberConvention
41+
{
42+
public void Apply(ConventionContext context, MemberInfo member)
43+
{
44+
if (member is FieldInfo field)
45+
{
46+
var opt = context.Application.Option("--working-dir", "The working directory", CommandOptionType.SingleOrNoValue);
47+
context.Application.OnParsingComplete(_ =>
48+
{
49+
var cwd = opt.HasValue()
50+
? opt.Value()
51+
: context.Application.WorkingDirectory;
52+
field.SetValue(context.ModelAccessor.GetModel(), cwd);
53+
});
54+
}
55+
}
56+
}
57+
58+
#pragma warning restore CS0649

samples/Conventions/Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,8 @@ static void Main(string[] args)
1010

1111
// This sample shows you how to write your own conventions to bind a type's methods to a subcommand
1212
MethodsAsSubcommandsProgram.Main(args);
13+
14+
// This sample shows you how to write conventions as an attribute.
15+
AttributeConventionProgram.Main(args);
1316
}
1417
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) Nate McMaster.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Linq;
5+
using System.Reflection;
6+
7+
namespace McMaster.Extensions.CommandLineUtils.Conventions
8+
{
9+
/// <summary>
10+
/// Searches the model type and its members for attributes that implement <see cref="IMemberConvention"/> or <see cref="IConvention"/>.
11+
/// </summary>
12+
public class AttributeConvention : IConvention
13+
{
14+
/// <inheritdoc />
15+
public void Apply(ConventionContext context)
16+
{
17+
if (context.ModelType == null)
18+
{
19+
return;
20+
}
21+
22+
foreach (var attr in context.ModelType.GetTypeInfo().GetCustomAttributes().OfType<IConvention>())
23+
{
24+
attr.Apply(context);
25+
}
26+
27+
var members = ReflectionHelper.GetMembers(context.ModelType);
28+
foreach (var member in members)
29+
{
30+
foreach (var attr in member.GetCustomAttributes().OfType<IMemberConvention>())
31+
{
32+
attr.Apply(context, member);
33+
}
34+
}
35+
}
36+
}
37+
}

src/CommandLineUtils/Conventions/ConventionBuilderExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public static IConventionBuilder UseAttributes(this IConventionBuilder builder)
4848
}
4949

5050
return builder
51+
.AddConvention(new AttributeConvention())
5152
.UseCommandAttribute()
5253
.UseVersionOptionFromMemberAttribute()
5354
.UseVersionOptionAttribute()
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) Nate McMaster.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Reflection;
5+
6+
namespace McMaster.Extensions.CommandLineUtils.Conventions
7+
{
8+
/// <summary>
9+
/// Defines a convention that is implemented as an attribute on a model type.
10+
/// </summary>
11+
public interface IMemberConvention
12+
{
13+
/// <summary>
14+
/// Apply the convention given a property or method.
15+
/// </summary>
16+
/// <param name="context">The convention context.</param>
17+
/// <param name="member">A member of the model type to which the attribute is applied.</param>
18+
void Apply(ConventionContext context, MemberInfo member);
19+
}
20+
}

src/CommandLineUtils/Internal/ReflectionHelper.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ public static PropertyInfo[] GetProperties(Type type)
4949
return type.GetTypeInfo().GetProperties(binding);
5050
}
5151

52+
public static MemberInfo[] GetMembers(Type type)
53+
{
54+
const BindingFlags binding = BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public;
55+
return type.GetTypeInfo().GetMembers(binding);
56+
}
57+
5258
public static object[] BindParameters(MethodInfo method, CommandLineApplication command)
5359
{
5460
var methodParams = method.GetParameters();
@@ -87,7 +93,7 @@ public static bool IsNullableType(TypeInfo typeInfo, out Type wrappedType)
8793
{
8894
var result = typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>);
8995
wrappedType = result ? typeInfo.GetGenericArguments().First() : null;
90-
96+
9197
return result;
9298
}
9399
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// Copyright (c) Nate McMaster.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Reflection;
6+
using McMaster.Extensions.CommandLineUtils.Conventions;
7+
using Xunit;
8+
9+
namespace McMaster.Extensions.CommandLineUtils.Tests
10+
{
11+
public class AttributeConventionTests
12+
{
13+
private sealed class MyAttributeConvention : Attribute, IConvention
14+
{
15+
private readonly string _name;
16+
17+
public MyAttributeConvention(string name)
18+
{
19+
_name = name;
20+
}
21+
22+
public void Apply(ConventionContext context)
23+
{
24+
context.Application.Name = _name;
25+
}
26+
}
27+
28+
[Fact]
29+
public void ItFindsAttributesOnType()
30+
{
31+
var app = new CommandLineApplication<Yellow>();
32+
app.Conventions.UseAttributes();
33+
Assert.Equal("yellow", app.Name);
34+
}
35+
36+
[MyAttributeConvention("yellow")]
37+
private class Yellow
38+
{
39+
}
40+
41+
[Fact]
42+
public void ItDoesNotIConventionOnMemberAttribute()
43+
{
44+
var app = new CommandLineApplication<YellowMember>();
45+
app.Conventions.UseAttributes();
46+
Assert.Null(app.Name);
47+
}
48+
49+
private class YellowMember
50+
{
51+
[MyAttributeConvention("yellow")]
52+
private string _color;
53+
54+
public string Color { get => _color; set => _color = value; }
55+
}
56+
57+
[Fact]
58+
public void ItFindsMemberAttributes()
59+
{
60+
var app = new CommandLineApplication<YellowMemberWithDefault>();
61+
app.Conventions.UseAttributes();
62+
app.Parse();
63+
Assert.Equal("yellow", app.Model.Color);
64+
}
65+
66+
private sealed class MyDefaultValue : Attribute, IMemberConvention
67+
{
68+
private readonly string _value;
69+
70+
public MyDefaultValue(string value)
71+
{
72+
_value = value;
73+
}
74+
75+
public void Apply(ConventionContext context, MemberInfo member)
76+
{
77+
if (member is FieldInfo field)
78+
{
79+
context.Application.OnParsingComplete(r =>
80+
{
81+
field.SetValue(context.ModelAccessor.GetModel(), _value);
82+
});
83+
}
84+
}
85+
}
86+
87+
private class YellowMemberWithDefault
88+
{
89+
[MyDefaultValue("yellow")]
90+
private string _color;
91+
92+
public string Color { get => _color; set => _color = value; }
93+
}
94+
95+
[Fact]
96+
public void ItFindsMemberConventionsOnAllMembers()
97+
{
98+
var app = new CommandLineApplication<MemberProgram>();
99+
app.Conventions.UseAttributes();
100+
Assert.Equal(16, app.Commands.Count);
101+
}
102+
103+
#pragma warning disable CS0169, CS0649, CS0067
104+
private class MemberProgram
105+
{
106+
[Custom]
107+
private string _privateField;
108+
109+
[Custom]
110+
public string _publicField;
111+
112+
[Custom]
113+
private static string s_privateStaticField;
114+
115+
[Custom]
116+
public static string s_publicStaticField;
117+
118+
119+
[Custom]
120+
public void Method() { }
121+
122+
[Custom]
123+
private void PrivateMethod() { }
124+
125+
[Custom]
126+
public static void StaticMethod() { }
127+
128+
[Custom]
129+
private static void StaticPrivateMethod() { }
130+
131+
132+
[Custom]
133+
public int Prop { get; set; }
134+
135+
[Custom]
136+
private int PrivateProp { get; set; }
137+
138+
[Custom]
139+
public static int StaticProp { get; set; }
140+
141+
[Custom]
142+
private static int PrivateStaticProp { get; set; }
143+
144+
145+
[Custom]
146+
public event Action Event;
147+
148+
[Custom]
149+
private event Action PrivateEvent;
150+
151+
[Custom]
152+
public static event Action StaticEvent;
153+
154+
[Custom]
155+
private static event Action PrivateStaticEvent;
156+
}
157+
#pragma warning restore CS0169, CS0649, CS0067
158+
159+
160+
private class CustomAttribute : Attribute, IMemberConvention
161+
{
162+
public void Apply(ConventionContext context, MemberInfo member)
163+
{
164+
context.Application.Command(member.Name, _ => { });
165+
}
166+
}
167+
}
168+
}

0 commit comments

Comments
 (0)