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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async command support #19

Merged
merged 3 commits into from Mar 8, 2018
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion documentation/documentation/commands.md
Expand Up @@ -7,7 +7,7 @@ It is perfectly legal to use the same input class across multiple commands
Oakton commands consist of two parts:

1. A concrete input class that holds all the argument and flag data inputs
1. A concrete class that inherits from `OaktonCommand<T>` where the "T" is the input class in the first bullet point
1. A concrete class that inherits from `OaktonCommand<T>` or `OaktonAsyncCommand<T>` where the "T" is the input class in the first bullet point

Looking again at the `NameCommand` from the <[linkto:documentation/getting_started]> topic:

Expand All @@ -22,6 +22,10 @@ There's only a couple things to note about a command class:
* The `Usages` syntax in the constructor is explained in a section below
* The `[Description]` attribute on the class is strictly for the purpose of providing help text and is not mandatory

If you want to make use of `async/await`, you can inherit from `OaktonAsyncCommand<T>` instead. The only difference is signature of the `Execute()` method:

<[sample:async-command]>


## Argument Usages

Expand Down
6 changes: 4 additions & 2 deletions src/MultipleCommands/Program.cs
@@ -1,6 +1,7 @@
锘縰sing System;
using System.Reflection;
using System.Reflection.Metadata.Ecma335;
using System.Threading.Tasks;
using Oakton;

namespace MultipleCommands
Expand All @@ -24,10 +25,11 @@ static int Main(string[] args)

// SAMPLE: git-commands
[Description("Switch branches or restore working tree files")]
public class CheckoutCommand : OaktonCommand<CheckoutInput>
public class CheckoutCommand : OaktonAsyncCommand<CheckoutInput>
{
public override bool Execute(CheckoutInput input)
public override async Task<bool> Execute(CheckoutInput input)
{
await Task.CompletedTask;
return true;
}
}
Expand Down
94 changes: 51 additions & 43 deletions src/Oakton.Testing/CommandExecutorTester.cs
@@ -1,6 +1,7 @@
锘縰sing System;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using Baseline;
using Shouldly;
using Xunit;
Expand All @@ -17,37 +18,40 @@ public class CommandExecutorTester
#else
private string directory = AppContext.BaseDirectory;
#endif
private CommandExecutor executor;


public CommandExecutorTester()
{
Console.SetOut(theOutput);


executor = CommandExecutor.For(_ =>
{
_.RegisterCommands(GetType().GetTypeInfo().Assembly);
});
}

[Fact]
public void execute_happy_path()
{
var executor = CommandExecutor.For(_ =>
{
_.RegisterCommands(GetType().GetTypeInfo().Assembly);
});

executor.Execute("say-name Lebron James")
.ShouldBe(0);

theOutput.ToString().ShouldContain("Lebron James");
}

[Fact]
public void no_command_argument_should_display_the_help()
public void execute_async_happy_path()
{
var executor = CommandExecutor.For(_ =>
{
_.RegisterCommands(GetType().GetTypeInfo().Assembly);
});
executor.Execute("say-async-name Lebron James")
.ShouldBe(0);

theOutput.ToString().ShouldContain("Lebron James");
}

[Fact]
public void no_command_argument_should_display_the_help()
{
executor.Execute("").ShouldBe(0);

theOutput.ToString().ShouldContain("Available commands:");
Expand All @@ -57,39 +61,30 @@ public void no_command_argument_should_display_the_help()
[Fact]
public void show_help_for_a_single_command()
{
var executor = CommandExecutor.For(_ =>
{
_.RegisterCommands(GetType().GetTypeInfo().Assembly);
});

executor.Execute("help say-name").ShouldBe(1);



theOutput.ToString().ShouldContain("Usages for 'say-name' (Say my name)");
}

[Fact]
public void run_a_command_that_fails()
{
var executor = CommandExecutor.For(_ =>
{
_.RegisterCommands(GetType().GetTypeInfo().Assembly);
});

executor.Execute("throwup").ShouldBe(1);

theOutput.ToString().ShouldContain("DivideByZeroException");
}

[Fact]
public void run_with_options_if_the_options_file_does_not_exist()
public void run_an_async_command_that_fails()
{
var executor = CommandExecutor.For(_ =>
{
_.RegisterCommands(GetType().GetTypeInfo().Assembly);
});
executor.Execute("throwupasync").ShouldBe(1);

theOutput.ToString().ShouldContain("DivideByZeroException");
}

[Fact]
public void run_with_options_if_the_options_file_does_not_exist()
{
executor.OptionsFile = "exec.opts";

executor.Execute("say-name Lebron James")
Expand All @@ -101,11 +96,6 @@ public void run_with_options_if_the_options_file_does_not_exist()
[Fact]
public void use_options_file_if_it_exists()
{
var executor = CommandExecutor.For(_ =>
{
_.RegisterCommands(GetType().GetTypeInfo().Assembly);
});

var path = directory.AppendPath("good.opts");
new FileSystem().WriteStringToFile(path, "say-name Klay Thompson");

Expand All @@ -120,11 +110,6 @@ public void use_options_file_if_it_exists()
[Fact]
public void can_set_flags_in_combination_with_opts()
{
var executor = CommandExecutor.For(_ =>
{
_.RegisterCommands(GetType().GetTypeInfo().Assembly);
});

var path = directory.AppendPath("override.opts");
new FileSystem().WriteStringToFile(path, "option -b -n 1");

Expand All @@ -133,8 +118,6 @@ public void can_set_flags_in_combination_with_opts()
executor.Execute("--number 5").ShouldBe(0);

theOutput.ToString().ShouldContain("Big is true, Number is 5");


}
}

Expand All @@ -159,7 +142,6 @@ public class SayName
{
public string FirstName;
public string LastName;

}

[Description("Say my name", Name = "say-name")]
Expand All @@ -177,9 +159,27 @@ public override bool Execute(SayName input)
}
}

[Description("Say my name", Name = "say-async-name")]
public class AsyncSayNameCommand : OaktonAsyncCommand<SayName>
{
public AsyncSayNameCommand()
{
Usage("Capture the users name").Arguments(x => x.FirstName, x => x.LastName);
}

public override async Task<bool> Execute(SayName input)
{
await Task.Run(() =>
{
Console.WriteLine($"{input.FirstName} {input.LastName}");
});

return true;
}
}

public class ThrowUp
{

}

public class ThrowUpCommand : OaktonCommand<ThrowUp>
Expand All @@ -189,4 +189,12 @@ public override bool Execute(ThrowUp input)
throw new DivideByZeroException("I threw up!");
}
}
}

public class ThrowUpAsyncCommand : OaktonAsyncCommand<ThrowUp>
{
public override Task<bool> Execute(ThrowUp input)
{
throw new DivideByZeroException("I threw up!");
}
}
}
4 changes: 2 additions & 2 deletions src/Oakton/CommandExecutor.cs
Expand Up @@ -147,7 +147,7 @@ public int Execute(string commandLine)
return execute(() =>
{
var run = _factory.BuildRun(commandLine);
return run.Execute();
return run.Execute().Result;
});

}
Expand All @@ -162,7 +162,7 @@ public int Execute(string[] args)
return execute(() =>
{
var run = _factory.BuildRun(readOptions().Concat(args));
return run.Execute();
return run.Execute().Result;
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Oakton/CommandFactory.cs
Expand Up @@ -171,7 +171,7 @@ public void RegisterCommands(Assembly assembly)
{
assembly
.GetExportedTypes()
.Where(x => x.Closes(typeof(OaktonCommand<>)) && x.IsConcrete())
.Where(x => (x.Closes(typeof(OaktonCommand<>)) || x.Closes(typeof(OaktonAsyncCommand<>))) && x.IsConcrete())
.Each(t => { _commandTypes[CommandNameFor(t)] = t; });
}

Expand Down
6 changes: 4 additions & 2 deletions src/Oakton/CommandRun.cs
@@ -1,11 +1,13 @@
锘縩amespace Oakton
锘縰sing System.Threading.Tasks;

namespace Oakton
{
public class CommandRun
{
public IOaktonCommand Command { get; set; }
public object Input { get; set; }

public bool Execute()
public Task<bool> Execute()
{
return Command.Execute(Input);
}
Expand Down
3 changes: 2 additions & 1 deletion src/Oakton/IOaktonCommand.cs
@@ -1,11 +1,12 @@
using System;
using System.Threading.Tasks;
using Oakton.Help;

namespace Oakton
{
public interface IOaktonCommand
{
bool Execute(object input);
Task<bool> Execute(object input);
Type InputType { get; }
UsageGraph Usages { get; }
}
Expand Down
47 changes: 47 additions & 0 deletions src/Oakton/OaktonAsyncCommand.cs
@@ -0,0 +1,47 @@
锘縰sing System;
using System.Threading.Tasks;
using Oakton.Help;

namespace Oakton
{
/// <summary>
/// Base class for all Oakton commands
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class OaktonAsyncCommand<T> : IOaktonCommand<T>
{
protected OaktonAsyncCommand()
{
Usages = new UsageGraph(GetType());
}

/// <summary>
/// If your command has multiple argument usage patterns ala the Git command line, use
/// this method to define the valid combinations of arguments and optionally limit the flags that are valid
/// for each usage
/// </summary>
/// <param name="description">The description of this usage to be displayed from the CLI help command</param>
/// <returns></returns>
public UsageGraph.UsageExpression<T> Usage(string description)
{
return Usages.AddUsage<T>(description);
}

public UsageGraph Usages { get; }

public Type InputType => typeof(T);

Task<bool> IOaktonCommand.Execute(object input)
{
return Execute((T)input);
}

/// <summary>
/// The actual execution of the command. Return "false" to denote failures
/// or "true" for successes
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public abstract Task<bool> Execute(T input);
}
}
5 changes: 3 additions & 2 deletions src/Oakton/OaktonCommand.cs
@@ -1,4 +1,5 @@
锘縰sing System;
using System.Threading.Tasks;
using Oakton.Help;

namespace Oakton
Expand Down Expand Up @@ -30,9 +31,9 @@ public UsageGraph.UsageExpression<T> Usage(string description)

public Type InputType => typeof (T);

bool IOaktonCommand.Execute(object input)
Task<bool> IOaktonCommand.Execute(object input)
{
return Execute((T)input);
return Task.FromResult(Execute((T)input));
}

/// <summary>
Expand Down