From f1554fd08af18210b0126cd633b8ce4d9b7cae3c Mon Sep 17 00:00:00 2001 From: Alexey Golub Date: Wed, 14 Aug 2019 17:45:42 +0300 Subject: [PATCH] Add demo project --- CliFx.Demo/CliFx.Demo.csproj | 18 ++++++ CliFx.Demo/Commands/BookAddCommand.cs | 75 ++++++++++++++++++++++ CliFx.Demo/Commands/BookCommand.cs | 36 +++++++++++ CliFx.Demo/Commands/BookListCommand.cs | 39 +++++++++++ CliFx.Demo/Commands/BookRemoveCommand.cs | 37 +++++++++++ CliFx.Demo/Internal/ConsoleExtensions.cs | 30 +++++++++ CliFx.Demo/Models/Book.cs | 23 +++++++ CliFx.Demo/Models/Extensions.cs | 22 +++++++ CliFx.Demo/Models/Isbn.cs | 44 +++++++++++++ CliFx.Demo/Models/Library.cs | 20 ++++++ CliFx.Demo/Program.cs | 33 ++++++++++ CliFx.Demo/Readme.md | 7 ++ CliFx.Demo/Services/LibraryService.cs | 43 +++++++++++++ CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj | 2 +- CliFx.Tests.Dummy/Commands/LogCommand.cs | 2 +- CliFx.Tests.Dummy/Commands/SumCommand.cs | 2 +- CliFx.sln | 16 ++++- CliFx/ICliApplication.cs | 3 +- Readme.md | 1 + 19 files changed, 447 insertions(+), 6 deletions(-) create mode 100644 CliFx.Demo/CliFx.Demo.csproj create mode 100644 CliFx.Demo/Commands/BookAddCommand.cs create mode 100644 CliFx.Demo/Commands/BookCommand.cs create mode 100644 CliFx.Demo/Commands/BookListCommand.cs create mode 100644 CliFx.Demo/Commands/BookRemoveCommand.cs create mode 100644 CliFx.Demo/Internal/ConsoleExtensions.cs create mode 100644 CliFx.Demo/Models/Book.cs create mode 100644 CliFx.Demo/Models/Extensions.cs create mode 100644 CliFx.Demo/Models/Isbn.cs create mode 100644 CliFx.Demo/Models/Library.cs create mode 100644 CliFx.Demo/Program.cs create mode 100644 CliFx.Demo/Readme.md create mode 100644 CliFx.Demo/Services/LibraryService.cs diff --git a/CliFx.Demo/CliFx.Demo.csproj b/CliFx.Demo/CliFx.Demo.csproj new file mode 100644 index 0000000..f6af40b --- /dev/null +++ b/CliFx.Demo/CliFx.Demo.csproj @@ -0,0 +1,18 @@ + + + + Exe + netcoreapp2.2 + latest + + + + + + + + + + + + \ No newline at end of file diff --git a/CliFx.Demo/Commands/BookAddCommand.cs b/CliFx.Demo/Commands/BookAddCommand.cs new file mode 100644 index 0000000..110ca3d --- /dev/null +++ b/CliFx.Demo/Commands/BookAddCommand.cs @@ -0,0 +1,75 @@ +using System; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Demo.Internal; +using CliFx.Demo.Models; +using CliFx.Demo.Services; +using CliFx.Exceptions; +using CliFx.Services; + +namespace CliFx.Demo.Commands +{ + [Command("book add", Description = "Add a book to the library.")] + public partial class BookAddCommand : ICommand + { + private readonly LibraryService _libraryService; + + [CommandOption("title", 't', IsRequired = true, Description = "Book title.")] + public string Title { get; set; } + + [CommandOption("author", 'a', IsRequired = true, Description = "Book author.")] + public string Author { get; set; } + + [CommandOption("published", 'p', Description = "Book publish date.")] + public DateTimeOffset Published { get; set; } + + [CommandOption("isbn", 'n', Description = "Book ISBN.")] + public Isbn Isbn { get; set; } + + public BookAddCommand(LibraryService libraryService) + { + _libraryService = libraryService; + } + + public Task ExecuteAsync(IConsole console) + { + // To make the demo simpler, we will just generate random publish date and ISBN if they were not set + if (Published == default) + Published = CreateRandomDate(); + if (Isbn == default) + Isbn = CreateRandomIsbn(); + + if (_libraryService.GetBook(Title) != null) + throw new CommandErrorException(1, "Book already exists."); + + var book = new Book(Title, Author, Published, Isbn); + _libraryService.AddBook(book); + + console.Output.WriteLine("Book added."); + console.RenderBook(book); + + return Task.CompletedTask; + } + } + + public partial class BookAddCommand + { + private static readonly Random Random = new Random(); + + private static DateTimeOffset CreateRandomDate() => new DateTimeOffset( + Random.Next(1800, 2020), + Random.Next(1, 12), + Random.Next(1, 28), + Random.Next(1, 23), + Random.Next(1, 59), + Random.Next(1, 59), + TimeSpan.Zero); + + public static Isbn CreateRandomIsbn() => new Isbn( + Random.Next(0, 999), + Random.Next(0, 99), + Random.Next(0, 99999), + Random.Next(0, 99), + Random.Next(0, 9)); + } +} \ No newline at end of file diff --git a/CliFx.Demo/Commands/BookCommand.cs b/CliFx.Demo/Commands/BookCommand.cs new file mode 100644 index 0000000..8739d8f --- /dev/null +++ b/CliFx.Demo/Commands/BookCommand.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Demo.Internal; +using CliFx.Demo.Services; +using CliFx.Exceptions; +using CliFx.Services; + +namespace CliFx.Demo.Commands +{ + [Command("book", Description = "View, list, add or remove books.")] + public class BookCommand : ICommand + { + private readonly LibraryService _libraryService; + + [CommandOption("title", 't', IsRequired = true, Description = "Book title.")] + public string Title { get; set; } + + public BookCommand(LibraryService libraryService) + { + _libraryService = libraryService; + } + + public Task ExecuteAsync(IConsole console) + { + var book = _libraryService.GetBook(Title); + + if (book == null) + throw new CommandErrorException(1, "Book not found."); + + console.RenderBook(book); + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/CliFx.Demo/Commands/BookListCommand.cs b/CliFx.Demo/Commands/BookListCommand.cs new file mode 100644 index 0000000..68f3aa4 --- /dev/null +++ b/CliFx.Demo/Commands/BookListCommand.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Demo.Internal; +using CliFx.Demo.Services; +using CliFx.Services; + +namespace CliFx.Demo.Commands +{ + [Command("book list", Description = "List all books in the library.")] + public class BookListCommand : ICommand + { + private readonly LibraryService _libraryService; + + public BookListCommand(LibraryService libraryService) + { + _libraryService = libraryService; + } + + public Task ExecuteAsync(IConsole console) + { + var library = _libraryService.GetLibrary(); + + var isFirst = true; + foreach (var book in library.Books) + { + // Margin + if (!isFirst) + console.Output.WriteLine(); + isFirst = false; + + // Render book + console.RenderBook(book); + } + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/CliFx.Demo/Commands/BookRemoveCommand.cs b/CliFx.Demo/Commands/BookRemoveCommand.cs new file mode 100644 index 0000000..5fa7ca5 --- /dev/null +++ b/CliFx.Demo/Commands/BookRemoveCommand.cs @@ -0,0 +1,37 @@ +using System; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Demo.Services; +using CliFx.Exceptions; +using CliFx.Services; + +namespace CliFx.Demo.Commands +{ + [Command("book remove", Description = "Remove a book from the library.")] + public class BookRemoveCommand : ICommand + { + private readonly LibraryService _libraryService; + + [CommandOption("title", 't', IsRequired = true, Description = "Book title.")] + public string Title { get; set; } + + public BookRemoveCommand(LibraryService libraryService) + { + _libraryService = libraryService; + } + + public Task ExecuteAsync(IConsole console) + { + var book = _libraryService.GetBook(Title); + + if (book == null) + throw new CommandErrorException(1, "Book not found."); + + _libraryService.RemoveBook(book); + + console.Output.WriteLine($"Book {Title} removed."); + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/CliFx.Demo/Internal/ConsoleExtensions.cs b/CliFx.Demo/Internal/ConsoleExtensions.cs new file mode 100644 index 0000000..ff71cae --- /dev/null +++ b/CliFx.Demo/Internal/ConsoleExtensions.cs @@ -0,0 +1,30 @@ +using System; +using CliFx.Demo.Models; +using CliFx.Services; + +namespace CliFx.Demo.Internal +{ + internal static class ConsoleExtensions + { + public static void RenderBook(this IConsole console, Book book) + { + // Title + console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine(book.Title)); + + // Author + console.Output.Write(" "); + console.Output.Write("Author: "); + console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine(book.Author)); + + // Published + console.Output.Write(" "); + console.Output.Write("Published: "); + console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine($"{book.Published:d}")); + + // ISBN + console.Output.Write(" "); + console.Output.Write("ISBN: "); + console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine(book.Isbn)); + } + } +} \ No newline at end of file diff --git a/CliFx.Demo/Models/Book.cs b/CliFx.Demo/Models/Book.cs new file mode 100644 index 0000000..43cbcd1 --- /dev/null +++ b/CliFx.Demo/Models/Book.cs @@ -0,0 +1,23 @@ +using System; + +namespace CliFx.Demo.Models +{ + public class Book + { + public string Title { get; } + + public string Author { get; } + + public DateTimeOffset Published { get; } + + public Isbn Isbn { get; } + + public Book(string title, string author, DateTimeOffset published, Isbn isbn) + { + Title = title; + Author = author; + Published = published; + Isbn = isbn; + } + } +} \ No newline at end of file diff --git a/CliFx.Demo/Models/Extensions.cs b/CliFx.Demo/Models/Extensions.cs new file mode 100644 index 0000000..5b97758 --- /dev/null +++ b/CliFx.Demo/Models/Extensions.cs @@ -0,0 +1,22 @@ +using System.Linq; + +namespace CliFx.Demo.Models +{ + public static class Extensions + { + public static Library WithBook(this Library library, Book book) + { + var books = library.Books.ToList(); + books.Add(book); + + return new Library(books); + } + + public static Library WithoutBook(this Library library, Book book) + { + var books = library.Books.Where(b => b != book).ToArray(); + + return new Library(books); + } + } +} \ No newline at end of file diff --git a/CliFx.Demo/Models/Isbn.cs b/CliFx.Demo/Models/Isbn.cs new file mode 100644 index 0000000..5d2532b --- /dev/null +++ b/CliFx.Demo/Models/Isbn.cs @@ -0,0 +1,44 @@ +using System; +using System.Globalization; + +namespace CliFx.Demo.Models +{ + public partial class Isbn + { + public int EanPrefix { get; } + + public int RegistrationGroup { get; } + + public int Registrant { get; } + + public int Publication { get; } + + public int CheckDigit { get; } + + public Isbn(int eanPrefix, int registrationGroup, int registrant, int publication, int checkDigit) + { + EanPrefix = eanPrefix; + RegistrationGroup = registrationGroup; + Registrant = registrant; + Publication = publication; + CheckDigit = checkDigit; + } + + public override string ToString() => $"{EanPrefix:000}-{RegistrationGroup:00}-{Registrant:00000}-{Publication:00}-{CheckDigit:0}"; + } + + public partial class Isbn + { + public static Isbn Parse(string value) + { + var components = value.Split('-', 5, StringSplitOptions.RemoveEmptyEntries); + + return new Isbn( + int.Parse(components[0], CultureInfo.InvariantCulture), + int.Parse(components[1], CultureInfo.InvariantCulture), + int.Parse(components[2], CultureInfo.InvariantCulture), + int.Parse(components[3], CultureInfo.InvariantCulture), + int.Parse(components[4], CultureInfo.InvariantCulture)); + } + } +} \ No newline at end of file diff --git a/CliFx.Demo/Models/Library.cs b/CliFx.Demo/Models/Library.cs new file mode 100644 index 0000000..1e23f99 --- /dev/null +++ b/CliFx.Demo/Models/Library.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; + +namespace CliFx.Demo.Models +{ + public partial class Library + { + public IReadOnlyList Books { get; } + + public Library(IReadOnlyList books) + { + Books = books; + } + } + + public partial class Library + { + public static Library Empty { get; } = new Library(Array.Empty()); + } +} \ No newline at end of file diff --git a/CliFx.Demo/Program.cs b/CliFx.Demo/Program.cs new file mode 100644 index 0000000..e22e1e1 --- /dev/null +++ b/CliFx.Demo/Program.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using CliFx.Demo.Commands; +using CliFx.Demo.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace CliFx.Demo +{ + public static class Program + { + public static Task Main(string[] args) + { + // We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands + var services = new ServiceCollection(); + + // Register services + services.AddSingleton(); + + // Register commands + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + var serviceProvider = new DefaultServiceProviderFactory().CreateServiceProvider(services); + + return new CliApplicationBuilder() + .WithCommandsFromThisAssembly() + .UseCommandFactory(type => (ICommand) serviceProvider.GetRequiredService(type)) + .Build() + .RunAsync(args); + } + } +} \ No newline at end of file diff --git a/CliFx.Demo/Readme.md b/CliFx.Demo/Readme.md new file mode 100644 index 0000000..9ef1228 --- /dev/null +++ b/CliFx.Demo/Readme.md @@ -0,0 +1,7 @@ +# CliFx Demo Project + +Sample command line interface for managing a library of books. + +This demo project shows basic CliFx functionality such as command routing, option parsing, autogenerated help text, and some other things. + +You can get a list of available commands by running `CliFx.Demo --help`. \ No newline at end of file diff --git a/CliFx.Demo/Services/LibraryService.cs b/CliFx.Demo/Services/LibraryService.cs new file mode 100644 index 0000000..3210093 --- /dev/null +++ b/CliFx.Demo/Services/LibraryService.cs @@ -0,0 +1,43 @@ +using System; +using System.IO; +using System.Linq; +using CliFx.Demo.Models; +using Newtonsoft.Json; + +namespace CliFx.Demo.Services +{ + public class LibraryService + { + private string StorageFilePath => Path.Combine(Directory.GetCurrentDirectory(), "Data.json"); + + private void StoreLibrary(Library library) + { + var data = JsonConvert.SerializeObject(library); + File.WriteAllText(StorageFilePath, data); + } + + public Library GetLibrary() + { + if (!File.Exists(StorageFilePath)) + return Library.Empty; + + var data = File.ReadAllText(StorageFilePath); + + return JsonConvert.DeserializeObject(data); + } + + public Book GetBook(string title) => GetLibrary().Books.FirstOrDefault(b => b.Title == title); + + public void AddBook(Book book) + { + var updatedLibrary = GetLibrary().WithBook(book); + StoreLibrary(updatedLibrary); + } + + public void RemoveBook(Book book) + { + var updatedLibrary = GetLibrary().WithoutBook(book); + StoreLibrary(updatedLibrary); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj b/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj index e71679a..961f3d4 100644 --- a/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj +++ b/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj @@ -3,8 +3,8 @@ Exe net46 - latest 1.2.3.4 + latest diff --git a/CliFx.Tests.Dummy/Commands/LogCommand.cs b/CliFx.Tests.Dummy/Commands/LogCommand.cs index 7786388..ddfb20c 100644 --- a/CliFx.Tests.Dummy/Commands/LogCommand.cs +++ b/CliFx.Tests.Dummy/Commands/LogCommand.cs @@ -5,7 +5,7 @@ using CliFx.Services; namespace CliFx.Tests.Dummy.Commands { - [Command("log", Description = "Calculates the logarithm of a value.")] + [Command("log", Description = "Calculate the logarithm of a value.")] public class LogCommand : ICommand { [CommandOption("value", 'v', IsRequired = true, Description = "Value whose logarithm is to be found.")] diff --git a/CliFx.Tests.Dummy/Commands/SumCommand.cs b/CliFx.Tests.Dummy/Commands/SumCommand.cs index ec106f0..e0d3e59 100644 --- a/CliFx.Tests.Dummy/Commands/SumCommand.cs +++ b/CliFx.Tests.Dummy/Commands/SumCommand.cs @@ -6,7 +6,7 @@ using CliFx.Services; namespace CliFx.Tests.Dummy.Commands { - [Command("sum", Description = "Calculates the sum of all input values.")] + [Command("sum", Description = "Calculate the sum of all input values.")] public class SumCommand : ICommand { [CommandOption("values", 'v', IsRequired = true, Description = "Input values.")] diff --git a/CliFx.sln b/CliFx.sln index 1c10759..62051c1 100644 --- a/CliFx.sln +++ b/CliFx.sln @@ -16,7 +16,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Readme.md = Readme.md EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliFx.Benchmarks", "CliFx.Benchmarks\CliFx.Benchmarks.csproj", "{8ACD6DC2-D768-4850-9223-5B7C83A78513}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Benchmarks", "CliFx.Benchmarks\CliFx.Benchmarks.csproj", "{8ACD6DC2-D768-4850-9223-5B7C83A78513}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliFx.Demo", "CliFx.Demo\CliFx.Demo.csproj", "{AAB6844C-BF71-448F-A11B-89AEE459AB15}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -76,6 +78,18 @@ Global {8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|x64.Build.0 = Release|Any CPU {8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|x86.ActiveCfg = Release|Any CPU {8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|x86.Build.0 = Release|Any CPU + {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|x64.ActiveCfg = Debug|Any CPU + {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|x64.Build.0 = Debug|Any CPU + {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|x86.ActiveCfg = Debug|Any CPU + {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|x86.Build.0 = Debug|Any CPU + {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|Any CPU.Build.0 = Release|Any CPU + {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x64.ActiveCfg = Release|Any CPU + {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x64.Build.0 = Release|Any CPU + {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x86.ActiveCfg = Release|Any CPU + {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/CliFx/ICliApplication.cs b/CliFx/ICliApplication.cs index dd4f34e..573d470 100644 --- a/CliFx/ICliApplication.cs +++ b/CliFx/ICliApplication.cs @@ -9,8 +9,7 @@ namespace CliFx public interface ICliApplication { /// - /// Runs application with specified command line arguments. - /// Returns exit code. + /// Runs application with specified command line arguments and returns an exit code. /// Task RunAsync(IReadOnlyList commandLineArguments); } diff --git a/Readme.md b/Readme.md index 45f854b..9c6ac76 100644 --- a/Readme.md +++ b/Readme.md @@ -276,6 +276,7 @@ public async Task ConcatCommand_Test() - [NUnit](https://github.com/nunit/nunit) - [CliWrap](https://github.com/Tyrrrz/CliWrap) - [FluentAssertions](https://github.com/fluentassertions/fluentassertions) +- [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json) - [BenchmarkDotNet](https://github.com/dotnet/BenchmarkDotNet) - [Coverlet](https://github.com/tonerdo/coverlet)