Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Value to eventually replace ValueObject #104

Merged
merged 47 commits into from
Aug 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
e5c8a93
Replace "ValueObject" with simply "object"
atifaziz May 22, 2021
dc53de3
Add custom result building
atifaziz May 27, 2021
b59b26c
Add "Var" to avoid double-boxing
atifaziz May 27, 2021
52f31d1
Bring back "ValueObject"
atifaziz May 27, 2021
90a7683
Rename "Box" to "Boxed" then "Var" to "Box"
atifaziz May 27, 2021
898ca90
Rename builders to accumulators
atifaziz Jun 5, 2021
e522b13
Remove non-generic accumulator
atifaziz Jun 5, 2021
b763c1f
Convert nil case to error
atifaziz Jun 5, 2021
91c51fd
Convert accumulation switch into aggregate via LINQ
atifaziz Jun 5, 2021
a423100
Replace "Parse" with "Application" from "Apply"
atifaziz Jun 5, 2021
895a3f2
Move accumulators to own file
atifaziz Jun 5, 2021
0fe2175
Consolidate duplication across two accumulator implementations
atifaziz Jun 5, 2021
fc624d9
Ensure that object box case is of right type
atifaziz Jul 31, 2021
51f895c
Merge branch 'master' into acc
atifaziz Jul 31, 2021
1cd4c1a
Add box generalization
atifaziz Aug 7, 2021
5af79fb
Add tests for "Box<>"
atifaziz Aug 7, 2021
19b9d97
Revert README changes
atifaziz Aug 7, 2021
b28f2cd
Revert generated code (used in T4) changes
atifaziz Aug 7, 2021
7f1602c
Remove "Stock" prefix form "ApplicationResultAccumulators"
atifaziz Aug 7, 2021
cc73d0d
Remove extra space
atifaziz Aug 7, 2021
0b2045c
Fix leaf pattern string formatting
atifaziz Aug 7, 2021
88c4e5f
Use plain arrays in tests
atifaziz Aug 7, 2021
3884841
Check for "ICollection" instead of "ArrayList"
atifaziz Aug 8, 2021
7b21d9b
Fix stack overflow in value formatting
atifaziz Aug 8, 2021
1963e3f
Simplify value formatting
atifaziz Aug 8, 2021
45b7874
Add value formatting tests
atifaziz Aug 8, 2021
e7d43ee
Inline "LeafPattern.Add" into leaf pattern matcher
atifaziz Aug 8, 2021
423a489
Simplify leaf match for int & list
atifaziz Aug 8, 2021
64268d1
Replace bare object values with "Value"
atifaziz Aug 8, 2021
815a5ba
Remove unused "ValueObject" members
atifaziz Aug 8, 2021
74db6b1
Remove unused imports
atifaziz Aug 8, 2021
0613886
Use a stack/cons to represent string list value
atifaziz Aug 10, 2021
fdb39b8
Replace "Value.Init" with implicit conversions
atifaziz Aug 10, 2021
56c6600
Disconnect "Command" from "Argument"
atifaziz Aug 10, 2021
46bc837
Add debugger display for "Value"
atifaziz Aug 10, 2021
0514563
Specialize stack as string list
atifaziz Aug 10, 2021
6dc9620
Convert "Value.Box" to a property
atifaziz Aug 11, 2021
cbae0c6
Remove unused "StringList" members
atifaziz Aug 11, 2021
407df7c
Merge value kind patterns
atifaziz Aug 12, 2021
1916baf
Rename "Null" value to "None"
atifaziz Aug 12, 2021
3f854d7
Move reversed list formatting into left pattern
atifaziz Aug 13, 2021
ca74d7e
Add tests for "Value"
atifaziz Aug 13, 2021
a8ce20a
Add tests for "StringList"
atifaziz Aug 13, 2021
eac9687
Pass "Value" to "DictionaryAccumulator" subclass for conversion
atifaziz Aug 13, 2021
e24ee6f
Add tests for application result accumulators
atifaziz Aug 13, 2021
20d55cd
Add back distinct cases to "IApplicationResultAccumulator"
atifaziz Aug 14, 2021
ae4a2e3
Apply suggestions from code review
atifaziz Aug 15, 2021
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
72 changes: 72 additions & 0 deletions src/DocoptNet/ApplicationResultAccumulator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#nullable enable

namespace DocoptNet
{
using System.Collections.Generic;

interface IApplicationResultAccumulator<T>
{
T New();
T Command(T state, string name, bool value);
T Command(T state, string name, int value);
T Argument(T state, string name);
T Argument(T state, string name, string value);
T Argument(T state, string name, StringList value);
T Option(T state, string name);
T Option(T state, string name, bool value);
T Option(T state, string name, string value);
T Option(T state, string name, int value);
T Option(T state, string name, StringList value);
T Error(DocoptBaseException exception);
}

static class ApplicationResultAccumulators
{
public static readonly IApplicationResultAccumulator<IDictionary<string, Value>> ValueDictionary = new ValueDictionaryAccumulator();
public static readonly IApplicationResultAccumulator<IDictionary<string, ValueObject>> ValueObjectDictionary = new ValueObjectDictionaryAccumulator();

sealed class ValueDictionaryAccumulator : IApplicationResultAccumulator<IDictionary<string, Value>>
{
public IDictionary<string, Value> New() => new Dictionary<string, Value>();
public IDictionary<string, Value> Command(IDictionary<string, Value> state, string name, bool value) => Adding(state, name, value);
public IDictionary<string, Value> Command(IDictionary<string, Value> state, string name, int value) => Adding(state, name, value);
public IDictionary<string, Value> Argument(IDictionary<string, Value> state, string name) => Adding(state, name, Value.None);
public IDictionary<string, Value> Argument(IDictionary<string, Value> state, string name, string value) => Adding(state, name, value);
public IDictionary<string, Value> Argument(IDictionary<string, Value> state, string name, StringList value) => Adding(state, name, value);
public IDictionary<string, Value> Option(IDictionary<string, Value> state, string name) => Adding(state, name, Value.None);
public IDictionary<string, Value> Option(IDictionary<string, Value> state, string name, bool value) => Adding(state, name, value);
public IDictionary<string, Value> Option(IDictionary<string, Value> state, string name, string value) => Adding(state, name, value);
public IDictionary<string, Value> Option(IDictionary<string, Value> state, string name, int value) => Adding(state, name, value);
public IDictionary<string, Value> Option(IDictionary<string, Value> state, string name, StringList value) => Adding(state, name, value);
public IDictionary<string, Value> Error(DocoptBaseException exception) => null!;

static IDictionary<string, Value> Adding(IDictionary<string, Value> dict, string name, Value value)
{
dict[name] = value;
return dict;
}
}

sealed class ValueObjectDictionaryAccumulator : IApplicationResultAccumulator<IDictionary<string, ValueObject>>
{
public IDictionary<string, ValueObject> New() => new Dictionary<string, ValueObject>();
public IDictionary<string, ValueObject> Command(IDictionary<string, ValueObject> state, string name, bool value) => Adding(state, name, value);
public IDictionary<string, ValueObject> Command(IDictionary<string, ValueObject> state, string name, int value) => Adding(state, name, value);
public IDictionary<string, ValueObject> Argument(IDictionary<string, ValueObject> state, string name) => Adding(state, name, null);
public IDictionary<string, ValueObject> Argument(IDictionary<string, ValueObject> state, string name, string value) => Adding(state, name, value);
public IDictionary<string, ValueObject> Argument(IDictionary<string, ValueObject> state, string name, StringList value) => Adding(state, name, value);
public IDictionary<string, ValueObject> Option(IDictionary<string, ValueObject> state, string name) => Adding(state, name, null);
public IDictionary<string, ValueObject> Option(IDictionary<string, ValueObject> state, string name, bool value) => Adding(state, name, value);
public IDictionary<string, ValueObject> Option(IDictionary<string, ValueObject> state, string name, string value) => Adding(state, name, value);
public IDictionary<string, ValueObject> Option(IDictionary<string, ValueObject> state, string name, int value) => Adding(state, name, value);
public IDictionary<string, ValueObject> Option(IDictionary<string, ValueObject> state, string name, StringList value) => Adding(state, name, value);
public IDictionary<string, ValueObject> Error(DocoptBaseException exception) => null!;

static IDictionary<string, ValueObject> Adding(IDictionary<string, ValueObject> dict, string name, object? value)
{
dict[name] = new ValueObject(value);
return dict;
}
}
}
}
21 changes: 13 additions & 8 deletions src/DocoptNet/Argument.cs
Original file line number Diff line number Diff line change
@@ -1,39 +1,44 @@
namespace DocoptNet
{
using System.Collections;
using System.Globalization;

class Argument: LeafPattern
{
public Argument(string name, ValueObject value = null) : base(name, value)
public Argument(string name) : base(name, Value.None)
{
}

public Argument(string name, string value)
: base(name, new ValueObject(value))
: base(name, value)
{
}

public Argument(string name, ICollection coll)
: base(name, new ValueObject(coll))
public Argument(string name, string[] values)
: base(name, StringList.BottomTop(values))
{
}

/// <remarks>
/// This is only used by tests as a convenience. The instantiated
/// <see cref="Value"/> is a string representation of the integer.
/// </remarks>

public Argument(string name, int value)
: base(name, new ValueObject(value))
: this(name, value.ToString(CultureInfo.InvariantCulture))
{
}

public override Node ToNode()
{
return new ArgumentNode(this.Name, (this.Value != null && this.Value.IsList) ? ValueType.List : ValueType.String);
return new ArgumentNode(this.Name, Value.IsStringList ? ValueType.List : ValueType.String);
}

public override string GenerateCode()
{
var s = Name.Replace("<", "").Replace(">", "").ToLowerInvariant();
s = "Arg" + GenerateCodeHelper.ConvertToPascalCase(s);

if (Value != null && Value.IsList)
if (Value.IsStringList)
{
return $"public ArrayList {s} {{ get {{ return _args[\"{Name}\"].AsList; }} }}";
}
Expand Down
4 changes: 2 additions & 2 deletions src/DocoptNet/Command.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
namespace DocoptNet
{
class Command : Argument
class Command : LeafPattern
{
public Command(string name, ValueObject value = null) : base(name, value ?? new ValueObject(false))
public Command(string name, bool value = false) : base(name, value ? Value.True : Value.False)
{
}

Expand Down
74 changes: 55 additions & 19 deletions src/DocoptNet/Docopt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,27 @@ public IDictionary<string, ValueObject> Apply(string doc, ICollection<string> ar
protected IDictionary<string, ValueObject> Apply(string doc, Tokens tokens,
bool help = true,
object version = null, bool optionsFirst = false, bool exit = false)
{
return Apply(doc, tokens, ApplicationResultAccumulators.ValueObjectDictionary, help, version, optionsFirst, exit);
}

internal T Apply<T>(string doc, IApplicationResultAccumulator<T> accumulator)
{
return Apply(doc, new Tokens(Enumerable.Empty<string>(), typeof (DocoptInputErrorException)), accumulator);
}

internal T Apply<T>(string doc, ICollection<string> argv,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want this to be public? I can't see how a user would inject an accumulator otherwise.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think eventually. For now, I wanted to bring this as an internal design, refactoring and changes that can eventually be added to the public API.

IApplicationResultAccumulator<T> accumulator,
bool help = true, object version = null,
bool optionsFirst = false, bool exit = false)
{
return Apply(doc, new Tokens(argv, typeof (DocoptInputErrorException)), accumulator, help, version, optionsFirst, exit);
}

internal T Apply<T>(string doc, Tokens tokens,
IApplicationResultAccumulator<T> accumulator,
bool help = true, object version = null,
bool optionsFirst = false, bool exit = false)
{
try
{
Expand All @@ -46,18 +67,33 @@ protected IDictionary<string, ValueObject> Apply(string doc, Tokens tokens,
optionsShortcut.Children = docOptions.Distinct().Except(patternOptions).ToList();
}

if (help && arguments.Any(o => o is { Name: "-h" or "--help", Value: { IsNullOrEmpty: false } }))
static bool IsNullOrEmptyString(object obj) => obj is null or string { Length: 0 };

if (help && arguments.Any(o => o is { Name: "-h" or "--help" } && !IsNullOrEmptyString(o.Value)))
OnPrintExit(doc);

if (version is not null && arguments.Any(o => o is { Name: "--version", Value: { IsNullOrEmpty: false } }))
if (version is not null && arguments.Any(o => o is { Name: "--version" } && !IsNullOrEmptyString(o.Value)))
OnPrintExit(version.ToString());

if (pattern.Fix().Match(arguments) is (true, { Count: 0 }, var collected))
{
var dict = new Dictionary<string, ValueObject>();
foreach (var p in pattern.Flat().OfType<LeafPattern>().Concat(collected))
dict[p.Name] = p.Value;
return dict;
return pattern.Flat()
.OfType<LeafPattern>()
.Concat(collected)
.Aggregate(accumulator.New(), (state, p) => (p, p.Value.Box) switch
{
(Command , bool v ) => accumulator.Command(state, p.Name, v),
(Command , int v ) => accumulator.Command(state, p.Name, v),
(Argument, null ) => accumulator.Argument(state, p.Name),
(Argument, string v ) => accumulator.Argument(state, p.Name, v),
(Argument, StringList v ) => accumulator.Argument(state, p.Name, v.Reverse()),
(Option , bool v ) => accumulator.Option(state, p.Name, v),
(Option , int v ) => accumulator.Option(state, p.Name, v),
(Option , string v ) => accumulator.Option(state, p.Name, v),
(Option , null ) => accumulator.Option(state, p.Name),
(Option , StringList v ) => accumulator.Option(state, p.Name, v.Reverse()),
var other => throw new NotSupportedException($"Unsupported pattern: {other}"),
});
}
throw new DocoptInputErrorException(exitUsage);
}
Expand All @@ -68,7 +104,7 @@ protected IDictionary<string, ValueObject> Apply(string doc, Tokens tokens,

OnPrintExit(e.Message, e.ErrorCode);

return null;
return accumulator.Error(e);
}
}

Expand Down Expand Up @@ -154,7 +190,7 @@ internal static IList<LeafPattern> ParseArgv(Tokens tokens, ICollection<Option>
{
if (tokens.Current() == "--")
{
parsed.AddRange(tokens.Select(v => new Argument(null, new ValueObject(v))));
parsed.AddRange(tokens.Select(v => new Argument(null, v)));
return parsed;
}

Expand All @@ -168,12 +204,12 @@ internal static IList<LeafPattern> ParseArgv(Tokens tokens, ICollection<Option>
}
else if (optionsFirst)
{
parsed.AddRange(tokens.Select(v => new Argument(null, new ValueObject(v))));
parsed.AddRange(tokens.Select(v => new Argument(null, v)));
return parsed;
}
else
{
parsed.Add(new Argument(null, new ValueObject(tokens.Move())));
parsed.Add(new Argument(null, tokens.Move()));
}
}
return parsed;
Expand Down Expand Up @@ -334,14 +370,14 @@ private static IEnumerable<Option> ParseShorts(Tokens tokens, ICollection<Option
options.Add(option);
if (tokens.ThrowsInputError)
{
option = new Option(shortName, null, 0, new ValueObject(true));
option = new Option(shortName, null, 0, Value.True);
}
}
else
{
// why is copying necessary here?
option = new Option(shortName, similar[0].LongName, similar[0].ArgCount, similar[0].Value);
ValueObject value = null;
Value? value = null;
if (option.ArgCount != 0)
{
if (left == "")
Expand All @@ -350,16 +386,16 @@ private static IEnumerable<Option> ParseShorts(Tokens tokens, ICollection<Option
{
throw tokens.CreateException(shortName + " requires argument");
}
value = new ValueObject(tokens.Move());
value = tokens.Move();
}
else
{
value = new ValueObject(left);
value = left;
left = "";
}
}
if (tokens.ThrowsInputError)
option.Value = value ?? new ValueObject(true);
option.Value = value ?? Value.True;
}
parsed.Add(option);
}
Expand All @@ -372,7 +408,7 @@ private static IEnumerable<Option> ParseLong(Tokens tokens, ICollection<Option>
var (longName, eq, value) = tokens.Move().Partition("=") switch
{
(var ln, "", _) => (ln, false, null),
var (ln, _, vs) => (ln, true, new ValueObject(vs))
var (ln, _, vs) => (ln, true, vs)
};
Debug.Assert(longName.StartsWith("--"));
var similar = options.Where(o => o.LongName == longName).ToList();
Expand All @@ -394,7 +430,7 @@ private static IEnumerable<Option> ParseLong(Tokens tokens, ICollection<Option>
option = new Option(null, longName, argCount);
options.Add(option);
if (tokens.ThrowsInputError)
option = new Option(null, longName, argCount, argCount != 0 ? value : new ValueObject(true));
option = new Option(null, longName, argCount, value is { } v ? v : Value.True);
}
else
{
Expand All @@ -410,11 +446,11 @@ private static IEnumerable<Option> ParseLong(Tokens tokens, ICollection<Option>
{
if (tokens.Current() == null || tokens.Current() == "--")
throw tokens.CreateException(option.LongName + " requires an argument");
value = new ValueObject(tokens.Move());
value = tokens.Move();
}
}
if (tokens.ThrowsInputError)
option.Value = value ?? new ValueObject(true);
option.Value = value is { } v ? v : Value.True;
}
return new[] {option};
}
Expand Down
8 changes: 6 additions & 2 deletions src/DocoptNet/DocoptNet.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard1.5;netstandard2.0</TargetFrameworks>
<TargetFrameworks>netstandard2.1;netstandard2.0;netstandard1.5</TargetFrameworks>
<DefineConstants>$(DefineConstants);DOCNETOPT_PUBLIC</DefineConstants>
<!--
TODO Remove the suppression of the following warnings after addressing them:
Expand Down Expand Up @@ -42,7 +42,7 @@
<PackageTags>parser;command line argument;option library;syntax;shell;beautiful;posix;python;console;command-line;docopt</PackageTags>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PropertyGroup Condition="'$(TargetFramework)' == 'netstandard2.1' Or '$(TargetFramework)' == 'netstandard2.0'">
<DefineConstants>$(DefineConstants);RUNTIME_SERIALIZATION</DefineConstants>
</PropertyGroup>

Expand Down Expand Up @@ -86,6 +86,10 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Nullable" Version="1.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>

Expand Down
7 changes: 4 additions & 3 deletions src/DocoptNet/LeafPattern.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ internal abstract class LeafPattern: Pattern
{
private readonly string _name;

protected LeafPattern(string name, ValueObject value=null)
protected LeafPattern(string name, Value value = default)
{
_name = name;
Value = value;
Expand All @@ -26,7 +26,7 @@ public override string Name
get { return _name; }
}

public ValueObject Value { get; set; }
public Value Value { get; set; }

public override ICollection<Pattern> Flat(params Type[] types)
{
Expand All @@ -40,7 +40,8 @@ public override ICollection<Pattern> Flat(params Type[] types)

public override string ToString()
{
return $"{GetType().Name}({Name}, {Value})";
var value = Value.TryAsStringList(out var list) ? list.Reverse() : Value;
return $"{GetType().Name}({Name}, {ValueObject.Format(value)})";
}
}
}
Loading