Skip to content

Commit

Permalink
Merge pull request #2 from tarikguney/array-support
Browse files Browse the repository at this point in the history
Adding array property type support
  • Loading branch information
tarikguney committed Jul 25, 2020
2 parents b902bfe + 2a8e93c commit 5e9b275
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 57 deletions.
32 changes: 16 additions & 16 deletions CommandCore.Library.UnitTests/CommandParserTests.cs
Expand Up @@ -21,11 +21,11 @@ public void When_Passed_A_Simple_Command_They_Are_Parsed_Properly()

Assert.NotNull(parsedVerb);
Assert.Equal("add", parsedVerb.VerbName);
Assert.Equal(new Dictionary<string, string>
Assert.Equal(new Dictionary<string, List<string>>
{
{"firstname", "tarik"},
{"lastname", "guney"},
{"zipcode", "55555"}
{"firstname", new List<string> {"tarik"}},
{"lastname", new List<string> {"guney"}},
{"zipcode", new List<string> {"55555"}}
}, parsedVerb.Options);
}

Expand All @@ -41,10 +41,10 @@ public void When_Passed_No_Verb_Default_Verb_Is_Used()
var parsedVerb = commandParser.ParseCommand(arguments);
Assert.NotEmpty(parsedVerb.VerbName);
Assert.Equal("default", parsedVerb.VerbName);
Assert.Equal(new Dictionary<string, string>
Assert.Equal(new Dictionary<string, List<string>>
{
{"firstname", "tarik"},
{"lastname", "guney"},
{"firstname", new List<string> {"tarik"}},
{"lastname", new List<string> {"guney"}},
}, parsedVerb.Options);
}

Expand Down Expand Up @@ -78,11 +78,11 @@ public void When_Passed_Alias_Parsed_Properly()
};
var parsedVerb = commandParser.ParseCommand(arguments);
Assert.Equal("default", parsedVerb.VerbName);
Assert.Equal(new Dictionary<string, string>()
Assert.Equal(new Dictionary<string, List<string>>()
{
{"t", "test"},
{"name", "tarik"},
{"g", "guney"}
{"t", new List<string> {"test"}},
{"name", new List<string> {"tarik"}},
{"g", new List<string> {"guney"}}
}, parsedVerb.Options);
}

Expand All @@ -100,12 +100,12 @@ public void When_Alias_And_Full_Verbs_Randomized_In_Order_They_Are_Parsed_Proper

var parsedVerb = commandParser.ParseCommand(arguments);
Assert.Equal("default", parsedVerb.VerbName);
Assert.Equal(new Dictionary<string, string>()
Assert.Equal(new Dictionary<string, List<string>>()
{
{"t", "test"},
{"name", "tarik"},
{"g", "guney"},
{"hello", "world"}
{"t", new List<string> {"test"}},
{"name", new List<string> {"tarik"}},
{"g", new List<string> {"guney"}},
{"hello", new List<string> {"world"}}
}, parsedVerb.Options);
}
}
Expand Down
4 changes: 2 additions & 2 deletions CommandCore.Library.UnitTests/CommandVerbRunnerTests.cs
Expand Up @@ -58,9 +58,9 @@ public void If_Every_Thing_Passed_Properly_Zero_Return_Code_Returns()
var args = new[] {"--name", "tarik"};
var testVerbInfo = new ParsedVerb()
{
VerbName = "default", Options = new Dictionary<string, string>()
VerbName = "default", Options = new Dictionary<string, List<string>>()
{
{"name", "tarik"}
{"name", new List<string> {"tarik"}}
}
};
_commandParseMock.Setup(a => a.ParseCommand(It.IsAny<string[]>()))
Expand Down
47 changes: 28 additions & 19 deletions CommandCore.Library.UnitTests/OptionsParserTest.cs
Expand Up @@ -14,17 +14,26 @@ public void When_Every_Thing_Is_Simple_Things_Work_As_Expected()
var optionsObject = (TestOptions) parser.CreatePopulatedOptionsObject(typeof(TestVerb), new ParsedVerb()
{
VerbName = "TestVerb",
Options = new Dictionary<string, string>()
Options = new Dictionary<string, List<string>>()
{
{"Name", "tarik"},
{"Age", "33"},
{"ismale", "true"}
{"Name", new List<string> {"tarik"}},
{"Age", new List<string> {"33"}},
{"ismale", new List<string> {"true"}},
{"countries", new List<string> {"usa", "germany", "turkey"}},
{"scores", new List<string> {"2", "3", "4"}},
{"skills", new List<string> {"programming"}},
{"ids", new List<string>()}
}
});
Assert.NotNull(optionsObject);
Assert.Equal("tarik", optionsObject.Name);
Assert.Equal(33, optionsObject.Age);
Assert.True(optionsObject.Male);
Assert.Equal(new List<string> {"usa", "germany", "turkey"}, optionsObject.Countries);
Assert.Equal(new List<int> {2, 3, 4}, optionsObject.Scores);
IReadOnlyList<string> expectedSkills = new List<string>() {"programming"};
Assert.Equal(expectedSkills, optionsObject.Skills);
Assert.Null(optionsObject.Ids);
}

[Fact]
Expand All @@ -34,11 +43,11 @@ public void When_Options_Name_Differ_They_Are_Ignored_During_Parsing()
var optionsObject = (TestOptions) parser.CreatePopulatedOptionsObject(typeof(TestVerb), new ParsedVerb()
{
VerbName = "TestVerb",
Options = new Dictionary<string, string>()
Options = new Dictionary<string, List<string>>()
{
{"Name", "tarik"},
{"age", "33"},
{"ismale", "true"}
{"Name", new List<string> {"tarik"}},
{"age", new List<string> {"33"}},
{"ismale", new List<string> {"true"}}
}
});
Assert.NotNull(optionsObject);
Expand All @@ -54,7 +63,7 @@ public void When_Nothing_Is_Passed_Options_Get_Their_Default_Values()
var optionsObject = (TestOptions) parser.CreatePopulatedOptionsObject(typeof(TestVerb), new ParsedVerb()
{
VerbName = "TestVerb",
Options = new Dictionary<string, string>()
Options = new Dictionary<string, List<string>>()
});
Assert.NotNull(optionsObject);
Assert.Null(optionsObject.Name);
Expand All @@ -69,11 +78,11 @@ public void When_All_Options_Are_Alias_They_Are_Parsed()
var optionsObject = (TestOptions) parser.CreatePopulatedOptionsObject(typeof(TestVerb), new ParsedVerb()
{
VerbName = "TestVerb",
Options = new Dictionary<string, string>()
Options = new Dictionary<string, List<string>>()
{
{"n", "tarik"},
{"a", "33"},
{"m", "true"}
{"n", new List<string> {"tarik"}},
{"a", new List<string> {"33"}},
{"m", new List<string> {"true"}}
}
});
Assert.NotNull(optionsObject);
Expand All @@ -90,9 +99,9 @@ public void When_There_Is_No_Option_Name_Mapping_Then_Property_Name_Is_Used()
new ParsedVerb()
{
VerbName = "TestVerb",
Options = new Dictionary<string, string>
Options = new Dictionary<string, List<string>>
{
{"Money", "12.55"}
{"Money", new List<string> {"12.55"}}
}
});

Expand All @@ -107,11 +116,11 @@ public void When_There_Are_Multiple_Option_Bindings_One_Of_Them_Got_Selected()
var optionsObject = (TestOptions) parser.CreatePopulatedOptionsObject(typeof(TestVerb), new ParsedVerb()
{
VerbName = "TestVerb",
Options = new Dictionary<string, string>()
Options = new Dictionary<string, List<string>>()
{
{"fn", "tarik"},
{"a", "33"},
{"im", "true"}
{"fn", new List<string> {"tarik"}},
{"a", new List<string> {"33"}},
{"im", new List<string> {"true"}}
}
});
Assert.NotNull(optionsObject);
Expand Down
14 changes: 14 additions & 0 deletions CommandCore.Library.UnitTests/TestTypes/TestOptions.cs
@@ -1,3 +1,5 @@
using System.Collections;
using System.Collections.Generic;
using CommandCore.Library.Attributes;
using CommandCore.Library.PublicBase;

Expand All @@ -17,6 +19,18 @@ internal class TestOptions : VerbOptionsBase
public bool Male { get; set; }

public decimal Money { get; set; }

[OptionName("countries")]
public string[] Countries { get; set; }

[OptionName("scores")]
public List<int> Scores { get; set; }

[OptionName("skills")]
public IReadOnlyList<string> Skills { get; set; }

[OptionName("ids")]
public IList<double> Ids { get; set; }
}

}
26 changes: 10 additions & 16 deletions CommandCore.Library/CommandParser.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using CommandCore.Library.Interfaces;

namespace CommandCore.Library
Expand All @@ -11,7 +12,7 @@ public ParsedVerb ParseCommand(string[] arguments)
{
return new ParsedVerb() {VerbName = "default"};
}

var argumentsClone = (string[]) arguments.Clone();
var parsedVerb = new ParsedVerb();

Expand All @@ -33,25 +34,18 @@ public ParsedVerb ParseCommand(string[] arguments)
return parsedVerb;
}

var options = new Dictionary<string, string>();
var options = new Dictionary<string, List<string>>();

for (int i = startingPoint; i < arguments.Length; i++)
{
if (arguments[i].StartsWith("-"))
var arg = arguments[i];
if (arg.StartsWith("-") || arg.StartsWith("--"))
{
options.Add(arg.Trim('-'), new List<string>());
}
else
{
// Checking if the option is with a value like --name "Tarik" or -n "Tarik"
if (arguments.Length - 1 >= i + 1 && !arguments[i + 1].StartsWith("-"))
{
options[arguments[i].TrimStart('-')] = arguments[i + 1];
// We already captured the value which is the next item in the array, so we can skip it.
i++;
}
// Checking if the option is a flag like --visible or -v, which is automatically inferred as --visible
// true. If the --argument is the last item or a value does not follow it, it means it is a flag.
else if (arguments.Length - 1 == i || arguments[i + 1].StartsWith("-"))
{
options[arguments[i].TrimStart('-')] = "true";
}
options[options.Keys.Last()].Add(arg);
}
}

Expand Down
2 changes: 1 addition & 1 deletion CommandCore.Library/Models/ParsedVerb.cs
Expand Up @@ -6,5 +6,5 @@ public class ParsedVerb

// ToDo: Ideally the value should be anything. I don't know how I should design this right now.
// The reason is simple: Some arguments are flag attributes.
public IReadOnlyDictionary<string, string>? Options { get; set; }
public IReadOnlyDictionary<string, List<string>>? Options { get; set; }
}
64 changes: 61 additions & 3 deletions CommandCore.Library/OptionsParser.cs
@@ -1,4 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
Expand Down Expand Up @@ -42,9 +44,65 @@ public VerbOptionsBase CreatePopulatedOptionsObject(Type verbType, ParsedVerb pa

if (parsedVerb.Options!.ContainsKey(parameterName))
{
var argumentValue = parsedVerb.Options[parameterName];
var converter = TypeDescriptor.GetConverter(propertyInfo.PropertyType);
propertyInfo.SetValue(options, converter.ConvertFrom(argumentValue));
var argumentValues = parsedVerb.Options[parameterName];

// I am aware of the relative complexity of the following big ass if conditions. I will simplify
// it one, hopefully. But the idea is simple: A property type may be an array, a collection, or a scalar type.
// And, we are paring them accordingly.
var propType = propertyInfo.PropertyType;

if (propType == typeof(bool))
{
propertyInfo.SetValue(options, argumentValues.Count <= 0 || bool.Parse(argumentValues[0]));
}

// Zero count means something for the boolean properties since what matters is whether the flag is present or not
// But for the other properties, it means nothing, so skipping property set since the user might have
// assigned the properties a default value. We would not want to override it with null or default primitive type value.
if (argumentValues.Count == 0)
{
continue;
}

if (propType.IsArray)
{
// Creating an instance of a new array using the property's array element type, and setting
// the property value with it after filling it with the converted values.
var elementType = propType.GetElementType()!;
var array = Array.CreateInstance(elementType, argumentValues.Count);
for (var i = 0; i < argumentValues.Count; i++)
{
array.SetValue(Convert.ChangeType(argumentValues[i], elementType), i);
}

propertyInfo.SetValue(options, array);
}
else if (propType.IsGenericType &&
(propType.GetGenericTypeDefinition() == typeof(IList<>) ||
propType.GetGenericTypeDefinition() == typeof(IReadOnlyList<>) ||
propType.GetGenericTypeDefinition().GetInterfaces().Any(a =>
a.IsGenericType && (a.GetGenericTypeDefinition() == typeof(IList<>) ||
a.GetGenericTypeDefinition() == typeof(IReadOnlyList<>)))))

{
// Creating an instance of a generic list using the property's generic argument, and setting
// the property value with it after filling it with the converted values.
var elementType = propType.GetGenericArguments()[0];
var listType = typeof(List<>);
var constructedListType = listType.MakeGenericType(elementType);
IList instance = (IList) Activator.CreateInstance(constructedListType)!;
foreach (var arg in argumentValues)
{
instance.Add(Convert.ChangeType(arg, elementType));
}

propertyInfo.SetValue(options, instance);
}
else
{
var converter = TypeDescriptor.GetConverter(propType);
propertyInfo.SetValue(options, converter.ConvertFrom(argumentValues[0]));
}
}
}

Expand Down

0 comments on commit 5e9b275

Please sign in to comment.