Skip to content

Commit

Permalink
Attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
YuriyDurov committed Jan 9, 2024
1 parent c455c77 commit 4ccd890
Show file tree
Hide file tree
Showing 23 changed files with 278 additions and 261 deletions.
10 changes: 4 additions & 6 deletions sample/BitzArt.Console.TestApp/Menus/0.MainMenu.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
namespace BitzArt.Console;

[AppMenu("Main Menu", IsMain = true)]
internal class MainMenu : ConsoleSelectionMenu
{
public override string Title => "Main Menu";
protected override bool IsMainMenu => true;

public MainMenu()
[MenuSelectionItem("Veggies")]
public async Task SubmenuVeggiesAsync()
{
AddSubmenu<FruitsMenu>("1. Fruits");
AddSubmenu<VeggiesMenu>("2. Veggies");
await RunAsync<VeggiesMenu>();
}
}
20 changes: 0 additions & 20 deletions sample/BitzArt.Console.TestApp/Menus/1.FruitsMenu.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,43 @@

namespace BitzArt.Console;

internal class VeggiesMenu : ConsoleSelectionMenu
[AppMenu("Veggies")]
internal class VeggiesMenu(VeggyOptions options) : ConsoleSelectionMenu
{
public override string Title => "Veggies";
private bool _focusOnPotato = false;

public VeggyOptions _options;
[MenuSelectionItem("Tomato")]
public static void Tomato()
{
}

private bool _focusOnPotato = false;
[MenuSelectionItem("Cucumber")]
public static void Cucumber()
{
}

public VeggiesMenu(VeggyOptions options)
[MenuSelectionItem("Potato")]
public void Potato()
{
AddSelection("tomato", pauseOnComplete: true);
AddSelection("cucumber", pauseOnComplete: true);
AddSelection("is potato a vegetable?", () => _focusOnPotato = true, pauseOnComplete: true);
_options = options;
_focusOnPotato = true;
}

public override void OnBeforeSelectionInvoke(ConsoleSelectionMenuItem selection)
{
_focusOnPotato = false;
AnsiConsole.WriteLine($"You selected '{selection.Name}'");
}

public override void OnSelection(ConsoleSelectionMenuItem selection)
{
AnsiConsole.WriteLine($"You selected '{selection.Name}'");

if (_focusOnPotato)
{
var possibleNegation = _options.IsPotatoAVegetable ? "" : "not ";
var possibleNegation = options.IsPotatoAVegetable ? "" : "not ";
var text = $"Application settings say that potato is {possibleNegation}a vegetable.";
AnsiConsole.WriteLine(text);
return;
}

Pause();
}
}
4 changes: 2 additions & 2 deletions sample/BitzArt.Console.TestApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ static void Main()
{
var builder = ConsoleApp.CreateBuilder();

builder.Services.AddConsoleMenusFromAssemblyContaining<Program>();
builder.AddConsoleMenusFromAssemblyContaining<Program>();
builder.AddVeggyOptions();

var app = builder.Build();

app.Run<MainMenu>();
app.Run();
}
}
14 changes: 13 additions & 1 deletion src/BitzArt.Console/App/ConsoleApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,19 @@ public ConsoleApp(IConsoleAppBuilder builder)
Services = builder.Services.BuildServiceProvider();
}

public void Run<TConsoleMenu>(CancellationToken cancellationToken = default) where TConsoleMenu : IConsoleMenu
public void Run(CancellationToken cancellationToken = default)
{
var navigationManager = Services.GetRequiredService<IConsoleAppNavigationManager>();
navigationManager.NavigateToMainMenuAsync().Wait(cancellationToken);
}

public void Run(Type mainMenuType, CancellationToken cancellationToken = default)
{
var navigationManager = Services.GetRequiredService<IConsoleAppNavigationManager>();
navigationManager.NavigateAsync(mainMenuType).Wait(cancellationToken);
}

public void Run<TConsoleMenu>(CancellationToken cancellationToken = default) where TConsoleMenu : class
{
var navigationManager = Services.GetRequiredService<IConsoleAppNavigationManager>();
navigationManager.NavigateAsync<TConsoleMenu>().Wait(cancellationToken);
Expand Down
5 changes: 5 additions & 0 deletions src/BitzArt.Console/App/ConsoleAppBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ internal class ConsoleAppBuilder : IConsoleAppBuilder
public IServiceCollection Services { get; private set; }
public IConfiguration Configuration { get; private set; }

public MenuMap MenuMap { get; private set; }

public ConsoleAppBuilder()
{
Services = new ServiceCollection();

Services.AddSingleton<IConsoleAppNavigationManager, ConsoleAppNavigationManager>();

MenuMap = new MenuMap();
Services.AddSingleton(MenuMap);

Configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.Build();
Expand Down
12 changes: 0 additions & 12 deletions src/BitzArt.Console/App/IConsoleAppBuilder.cs

This file was deleted.

34 changes: 34 additions & 0 deletions src/BitzArt.Console/Attributes/AppMenuAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Reflection;

namespace BitzArt.Console;

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class AppMenuAttribute : Attribute
{
private readonly string _title = "Menu";

public bool IsMain { get; set; } = false;

public AppMenuAttribute(string title)
{
if (string.IsNullOrWhiteSpace(title))
{
throw new ArgumentException("Menu title cannot be empty.", nameof(title));
}

_title = title;
}

internal string GetTitle() => _title;
}

internal static class AppMenuAttributeExtensions
{
public static AppMenuAttribute GetAppMenuAttribute(this Type type)
{
var attribute = type.GetCustomAttribute<AppMenuAttribute>()
?? throw new InvalidOperationException($"Menu {type.Name} must have {nameof(AppMenuAttribute)}");

return attribute;
}
}
50 changes: 50 additions & 0 deletions src/BitzArt.Console/Attributes/MenuSelectionItemAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Reflection;

namespace BitzArt.Console;

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class MenuSelectionItemAttribute : Attribute
{
private readonly string _name;
public bool PauseOnComplete { get; set; } = false;

public MenuSelectionItemAttribute(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Selection item name cannot be empty.", nameof(name));
}

_name = name;
}

internal string GetName() => _name;
}

internal static class MenuSelectionItemAttributeExtensions
{
internal static IEnumerable<SelectionMethod> GetSelectionMethods(this Type type)
{
var allPublicMethods = type.GetMethods();

var result = new List<SelectionMethod>();

foreach (var method in allPublicMethods)
{
var attribute = method.GetCustomAttribute<MenuSelectionItemAttribute>();
if (attribute is null) continue;

result.Add(new SelectionMethod(attribute.GetName(), method, attribute.PauseOnComplete));
}

return result;
}

internal static MenuSelectionItemAttribute GetMenuSelectionItemAttribute(this MethodInfo method)
{
var attribute = method.GetCustomAttribute<MenuSelectionItemAttribute>()
?? throw new InvalidOperationException($"Method {method.Name} must have {nameof(MenuSelectionItemAttribute)}");

return attribute;
}
}
39 changes: 21 additions & 18 deletions src/BitzArt.Console/Extensions/AddConsoleMenuExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,53 +8,56 @@ public static class AddConsoleMenuExtensions
/// <summary>
/// Registers all console menus defined in the assembly containing this type in the <see cref="IServiceCollection"/>
/// </summary>
public static IServiceCollection AddConsoleMenusFromAssemblyContaining<TAssemblyPointer>(this IServiceCollection services)
=> services.AddConsoleMenusFromAssemblyContaining(typeof(TAssemblyPointer));
public static IConsoleAppBuilder AddConsoleMenusFromAssemblyContaining<TAssemblyPointer>(this IConsoleAppBuilder builder)
=> builder.AddConsoleMenusFromAssemblyContaining(typeof(TAssemblyPointer));

/// <summary>
/// Registers all console menus defined in the assembly containing this type in the <see cref="IServiceCollection"/>
/// </summary>
public static IServiceCollection AddConsoleMenusFromAssemblyContaining(this IServiceCollection services, Type type)
=> services.AddConsoleMenusFromAssembly(type.Assembly);
public static IConsoleAppBuilder AddConsoleMenusFromAssemblyContaining(this IConsoleAppBuilder builder, Type type)
=> builder.AddConsoleMenusFromAssembly(type.Assembly);

/// <summary>
/// Registers all console menus defined in the assembly in the <see cref="IServiceCollection"/>
/// </summary>
public static IServiceCollection AddConsoleMenusFromAssembly(this IServiceCollection services, Assembly assembly)
public static IConsoleAppBuilder AddConsoleMenusFromAssembly(this IConsoleAppBuilder builder, Assembly assembly)
{
var tools = assembly
.DefinedTypes
.Where(x => x.IsAbstract == false)
.Where(x => x.GetInterfaces().Contains(typeof(IConsoleMenu)));
.Where(x => x.GetCustomAttributes<AppMenuAttribute>().Any());

foreach (var tool in tools) services.AddConsoleMenu(tool);
foreach (var tool in tools) builder.AddConsoleMenu(tool);

return services;
return builder;
}

/// <summary>
/// Registers a Console Menu of type <paramref name="TConsoleMenu"/> in the <see cref="IServiceCollection"/>
/// </summary>
public static IServiceCollection AddConsoleMenu<TConsoleMenu>(this IServiceCollection services)
where TConsoleMenu : class, IConsoleMenu
public static IConsoleAppBuilder AddConsoleMenu<TConsoleMenu>(this IConsoleAppBuilder builder)
where TConsoleMenu : class
{
services.AddTransient<TConsoleMenu>();
services.AddTransient<IConsoleMenu, TConsoleMenu>();
builder.Services.AddTransient<TConsoleMenu>();

return services;
var map = builder.MenuMap;
map.Add(typeof(TConsoleMenu));

return builder;
}

/// <summary>
/// Registers a Console Menu of <paramref name="type"/> in the <see cref="IServiceCollection"/>
/// </summary>
public static IServiceCollection AddConsoleMenu(this IServiceCollection services, Type type)
public static IConsoleAppBuilder AddConsoleMenu(this IConsoleAppBuilder builder, Type type)
{
if (type is null) throw new ArgumentException($"{nameof(type)} must not be null");
if (type.IsAssignableTo(typeof(IConsoleMenu)) == false) throw new ArgumentException($"{type.Name} is not assignable to IConsoleMenu");

services.AddTransient(type);
services.AddTransient(x => (IConsoleMenu)x.GetRequiredService(type));
builder.Services.AddTransient(type);

var map = builder.MenuMap;
map.Add(type);

return services;
return builder;
}
}
40 changes: 29 additions & 11 deletions src/BitzArt.Console/Extensions/RunConsoleMenuExtension.cs
Original file line number Diff line number Diff line change
@@ -1,35 +1,53 @@
using Microsoft.Extensions.DependencyInjection;
using System;

namespace BitzArt.Console;

public static class RunConsoleMenuExtension
{
internal static async Task RunConsoleMenuAsync<TConsoleMenu>(this IServiceProvider serviceProvider)
where TConsoleMenu : IConsoleMenu
where TConsoleMenu : class
{
var menu = serviceProvider.GetRequiredService<TConsoleMenu>();

AttachApp(menu, serviceProvider);
if (menu is not ConsoleMenu consoleMenu) return;

consoleMenu.Populate(serviceProvider);

await menu.RunAsync();
await consoleMenu.RunAsync();
}

internal static async Task RunConsoleMenuAsync(this IServiceProvider serviceProvider, Type menuType)
{
if (!menuType.IsAssignableTo(typeof(IConsoleMenu))) throw new ArgumentException($"Menu Type must implement {nameof(IConsoleMenu)}", nameof(menuType));
var menu = serviceProvider.GetRequiredService(menuType);

var menu = (IConsoleMenu)serviceProvider.GetRequiredService(menuType);
if (menu is not ConsoleMenu consoleMenu) return;

AttachApp(menu, serviceProvider);
consoleMenu.Populate(serviceProvider);

await menu.RunAsync();
await consoleMenu.RunAsync();
}

private static void AttachApp(this IConsoleMenu menu, IServiceProvider serviceProvider)
private static void Populate(this ConsoleMenu menu, IServiceProvider serviceProvider)
{
if (menu is not ConsoleMenu consoleMenu) return;

var app = serviceProvider.GetService<ConsoleApp>();
consoleMenu.App = app;
menu.App = app;

var appMenuAttribute = menu.GetType().GetAppMenuAttribute();
menu.Title = appMenuAttribute!.GetTitle();
menu.IsMainMenu = appMenuAttribute.IsMain;

menu.PopulateSelectionFromMethods();
}

private static void PopulateSelectionFromMethods(this ConsoleMenu consoleMenu)
{
if (consoleMenu is not ConsoleSelectionMenu selectionMenu) return;

var selections = consoleMenu.GetType().GetSelectionMethods();
foreach (var selection in selections)
{
selectionMenu.AddSelection(selection.Name, () => selection.Method.Invoke(selectionMenu, null), selection.PauseOnComplete);
}
}
}
Loading

0 comments on commit 4ccd890

Please sign in to comment.