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

Commandline Interface #167

Merged
merged 22 commits into from
Jul 7, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/EvoSC.CLI/Attributes/CliCommandAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
namespace EvoSC.CLI.Attributes;

/// <summary>
/// Define a class as a CLI command handler.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class CliCommandAttribute : Attribute
{
/// <summary>
/// The name of the command.
/// The name of the CLI command. Must alphanumeric.
/// </summary>
public required string Name { get; init; }

/// <summary>
/// Description of the command.
/// A short summary describing what this command does.
/// </summary>
public required string Description { get; init; }
}
23 changes: 0 additions & 23 deletions src/EvoSC.CLI/Attributes/CliOptionAttribute.cs

This file was deleted.

29 changes: 29 additions & 0 deletions src/EvoSC.CLI/Attributes/RequiredFeaturesAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using EvoSC.Common.Application;

namespace EvoSC.CLI.Attributes;

/// <summary>
/// Define a required Application Feature for a command.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class RequiredFeaturesAttribute : Attribute
{
public AppFeature[] Features { get; init; }

public RequiredFeaturesAttribute(AppFeature feature, params AppFeature[] features)
{
var requiredFeatures = new List<AppFeature>();

if (feature == AppFeature.All)
{
requiredFeatures.AddRange(Enum.GetValues<AppFeature>().Where(f => f != AppFeature.All));
}
else
{
requiredFeatures.Add(feature);
requiredFeatures.AddRange(features);
}

Features = requiredFeatures.ToArray();
}
}
9 changes: 0 additions & 9 deletions src/EvoSC.CLI/CliCommandContext.cs

This file was deleted.

88 changes: 0 additions & 88 deletions src/EvoSC.CLI/CliHandler.cs

This file was deleted.

202 changes: 202 additions & 0 deletions src/EvoSC.CLI/CliManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;
using System.ComponentModel;
using System.Reflection;
using EvoSC.CLI.Attributes;
using EvoSC.CLI.Exceptions;
using EvoSC.CLI.Interfaces;
using EvoSC.CLI.Models;
using EvoSC.Common.Application;
using EvoSC.Common.Config;
using EvoSC.Common.Services;
using EvoSC.Common.Util;
using EvoSC.Common.Util.EnumIdentifier;
using Microsoft.Extensions.DependencyInjection;

namespace EvoSC.CLI;

public class CliManager : ICliManager
{
private readonly RootCommand _rootCommand;
private readonly Parser _cliParser;
private readonly Option<IEnumerable<string>> _configOption;
private readonly List<ICliCommandInfo> _registeredCommands = new();

public CliManager()
{
_rootCommand = new RootCommand("EvoSC# TrackMania server controller.");

_configOption =
new Option<IEnumerable<string>>(new[] {"--option", "-o"},
description: "Override configuration options. Format is key:value");

_rootCommand.AddGlobalOption(_configOption);

_cliParser = new CommandLineBuilder(_rootCommand)
.UseVersionOption()
.UseHelp()
.UseEnvironmentVariableDirective()
.UseParseDirective()
.UseSuggestDirective()
.RegisterWithDotnetSuggest()
.UseTypoCorrections()
.UseParseErrorReporting()
.CancelOnProcessTermination()
.Build();
}

private async Task ExecuteHandlerAsync(ICliCommandInfo command, InvocationContext context)
{
var paramValues = new List<object?>();

foreach (var option in command.Options)
{
if (option == _configOption)
{
continue;
}

if (option.ValueType == typeof(InvocationContext))
{
paramValues.Add(context);
continue;
}

var optionValue = context.BindingContext.ParseResult.GetValueForOption(option);
paramValues.Add(optionValue);
}

var cliConfig = context.BindingContext.ParseResult.GetValueForOption(_configOption);
var cliConfigOptions = new Dictionary<string, string> ();

if (cliConfig != null)
{
foreach (var cliConfigOption in cliConfig)
{
var kv = cliConfigOption.Split(':', 2);

if (kv.Length < 2)
{
throw new InvalidOperationException(
$"The provided config option {cliConfigOption} is in an invalid format.");
}

cliConfigOptions[kv[0]] = kv[1];
}
}

var config = Configuration.GetBaseConfig(cliConfigOptions);

var startupPipeline = new StartupPipeline(config);
startupPipeline.ServiceContainer.ConfigureServiceContainerForEvoSc();
startupPipeline.SetupBasePipeline(config);

await startupPipeline.ExecuteAsync(command.RequiredFeatures
.Select(feature => feature.ToString())
.ToArray()
);

var cmdObject = ActivatorUtilities.CreateInstance(startupPipeline.ServiceContainer, command.CommandClass);

var methodArgs = paramValues?.ToArray() ?? Array.Empty<object>();
var task = ReflectionUtils.CallMethod(cmdObject, "ExecuteAsync", methodArgs) as Task;

if (task == null)
{
throw new InvalidOperationException("Failed to call CLI command handler as the task is null.");
}

await task;
}

private Option? CreateOption(ParameterInfo param)
{
var cmdOptionType = typeof(Option<>).MakeGenericType(param.ParameterType);
var aliases = param
.GetCustomAttributes<AliasAttribute>()
.Select(a => a.Name)
.ToList();

aliases.Add($"--{param.Name}");

var description = param.GetCustomAttribute<DescriptionAttribute>();

var option = Activator.CreateInstance(cmdOptionType, aliases.ToArray(), description?.Description ?? "") as Option;

return option;
}

public ICliManager RegisterCommands(Assembly assembly)
{
foreach (var cmdClass in assembly.AssemblyTypesWithAttribute<CliCommandAttribute>())
{
RegisterCommand(cmdClass);
}

return this;
}

public ICliManager RegisterCommand(Type cmdClass)
{
var handlerMethod = cmdClass.GetMethod("ExecuteAsync");

if (handlerMethod == null)
{
throw new InvalidCommandClassFormatException(
$"Did not find the ExecuteAsync method in CLI command: {cmdClass.Name}");
}

var cmdAttr = cmdClass.GetCustomAttribute<CliCommandAttribute>();

if (cmdAttr == null)
{
throw new InvalidCommandClassFormatException("Missing CliCommand");
}

var requiredFeatures = cmdClass.GetCustomAttribute<RequiredFeaturesAttribute>();

var command = new Command(cmdAttr!.Name, cmdAttr.Description);

var options = new List<Option>();
foreach (var param in handlerMethod.GetParameters())
{
var option = CreateOption(param);

if (option == null)
{
throw new InvalidOperationException($"Failed to create option for CLI command {cmdClass.Name}");
}

options.Add(option);
command.AddOption(option);
}

options.Add(_configOption);

var commandInfo = new CliCommandInfo
{
Command = command,
CommandClass = cmdClass,
HandlerMethod = handlerMethod,
Options = options.ToArray(),
RequiredFeatures = requiredFeatures?.Features ?? Array.Empty<AppFeature>()
};

command.SetHandler(async context =>
{
await ExecuteHandlerAsync(commandInfo, context);
});

_registeredCommands.Add(commandInfo);
_rootCommand.AddCommand(command);

return this;
}

public Task<int> ExecuteAsync(string[] args)
{
return _cliParser.InvokeAsync(args);
}
}
Loading
Loading