feat: add console command framework
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Game.Features.Console.Contracts;
|
||||||
|
using Game.Features.Console.Domain;
|
||||||
|
|
||||||
|
namespace Game.Features.Console.Application;
|
||||||
|
|
||||||
|
public sealed class CommandExecute(ICommandRegistry registry)
|
||||||
|
{
|
||||||
|
public CommandResult Execute(string input)
|
||||||
|
{
|
||||||
|
if (!CommandLineParser.TryParse(input, out string name, out IReadOnlyList<string> arguments))
|
||||||
|
{
|
||||||
|
return CommandResult.Fail("No command entered.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!registry.TryGet(name, out ICommand command))
|
||||||
|
{
|
||||||
|
return CommandResult.Fail($"Unknown command: {name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandResult result = CommandArgValidator.Validate(command.Args, arguments);
|
||||||
|
|
||||||
|
return !result.Success ? result : command.Execute(new CommandArgValues(arguments));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://f1ed8fhpbwik
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Game.Features.Console.Contracts;
|
||||||
|
|
||||||
|
namespace Game.Features.Console.Application.Commands;
|
||||||
|
|
||||||
|
public sealed class ClearCommand : ICommand
|
||||||
|
{
|
||||||
|
public string Name => "clear";
|
||||||
|
public string Description => "Clears the console output.";
|
||||||
|
public IReadOnlyList<CommandArgSpec> Args { get; } = new CommandArgsBuilder().Build();
|
||||||
|
|
||||||
|
public CommandResult Execute(CommandArgValues args) => CommandResult.Ok(clearOutput: true);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cmmw8kn2fc3bb
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Game.Features.Console.Contracts;
|
||||||
|
|
||||||
|
namespace Game.Features.Console.Application.Commands;
|
||||||
|
|
||||||
|
public sealed class EchoCommand : ICommand
|
||||||
|
{
|
||||||
|
public string Name => "echo";
|
||||||
|
public string Description => "Prints the given text back to the console.";
|
||||||
|
public IReadOnlyList<CommandArgSpec> Args { get; } = new CommandArgsBuilder().AddStringArg("text").Build();
|
||||||
|
|
||||||
|
public CommandResult Execute(CommandArgValues args) => CommandResult.Ok(args.GetString(0));
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cy6dkjf4fxkvj
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Game.Features.Console.Contracts;
|
||||||
|
|
||||||
|
namespace Game.Features.Console.Application.Commands;
|
||||||
|
|
||||||
|
public sealed class HelpCommand(ICommandRegistry registry) : ICommand
|
||||||
|
{
|
||||||
|
public string Name => "help";
|
||||||
|
public string Description => "Lists all available commands.";
|
||||||
|
public IReadOnlyList<CommandArgSpec> Args { get; } = new CommandArgsBuilder().Build();
|
||||||
|
|
||||||
|
private static string FormatUsage(ICommand command)
|
||||||
|
{
|
||||||
|
string args = string.Join(" ", command.Args.Select(spec => $"<{spec.Name}:{spec}>"));
|
||||||
|
return $"{command.Name} {args}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandResult Execute(CommandArgValues args)
|
||||||
|
{
|
||||||
|
IEnumerable<string> lines = registry.All
|
||||||
|
.OrderBy(command => command.Name, StringComparer.Ordinal)
|
||||||
|
.Select(command => command.Args.Count == 0
|
||||||
|
? $"{command.Name} - {command.Description}"
|
||||||
|
: $"{command.Name} - {command.Description}\n usage: {FormatUsage(command)}");
|
||||||
|
|
||||||
|
return CommandResult.Ok($"List of available commands:\n{string.Join("\n", lines)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bo7o0t08bb5u5
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Game.Features.Console.Contracts;
|
||||||
|
|
||||||
|
public sealed record CommandArgSpec(string Name, CommandArgType Type, IReadOnlyList<string> Choices = null)
|
||||||
|
{
|
||||||
|
public override string ToString() =>
|
||||||
|
Type == CommandArgType.Choice && Choices is not null
|
||||||
|
? string.Join("|", Choices)
|
||||||
|
: Type.ToString().ToLowerInvariant();
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c3dojuv6775r1
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Game.Features.Console.Contracts;
|
||||||
|
|
||||||
|
public enum CommandArgType
|
||||||
|
{
|
||||||
|
String,
|
||||||
|
Int,
|
||||||
|
Float,
|
||||||
|
Bool,
|
||||||
|
Choice
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dnt7hwlwqtdyq
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace Game.Features.Console.Contracts;
|
||||||
|
|
||||||
|
public sealed class CommandArgValues(IReadOnlyList<string> values)
|
||||||
|
{
|
||||||
|
public string GetString(int index) => values[index];
|
||||||
|
|
||||||
|
public int GetInt(int index) => int.Parse(values[index], CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
public float GetFloat(int index) => float.Parse(values[index], CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
public bool GetBool(int index) => bool.Parse(values[index]);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://carkvp7i86mq4
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Game.Features.Console.Contracts;
|
||||||
|
|
||||||
|
public sealed class CommandArgsBuilder
|
||||||
|
{
|
||||||
|
private readonly List<CommandArgSpec> _specs = [];
|
||||||
|
|
||||||
|
public CommandArgsBuilder AddStringArg(string name)
|
||||||
|
{
|
||||||
|
_specs.Add(new CommandArgSpec(name, CommandArgType.String));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandArgsBuilder AddIntArg(string name)
|
||||||
|
{
|
||||||
|
_specs.Add(new CommandArgSpec(name, CommandArgType.Int));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandArgsBuilder AddFloatArg(string name)
|
||||||
|
{
|
||||||
|
_specs.Add(new CommandArgSpec(name, CommandArgType.Float));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandArgsBuilder AddBoolArg(string name)
|
||||||
|
{
|
||||||
|
_specs.Add(new CommandArgSpec(name, CommandArgType.Bool));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandArgsBuilder AddChoiceArg(string name, IReadOnlyList<string> choices)
|
||||||
|
{
|
||||||
|
_specs.Add(new CommandArgSpec(name, CommandArgType.Choice, choices));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<CommandArgSpec> Build() => _specs.ToArray();
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cyviyspt0gye6
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Game.Features.Console.Contracts;
|
||||||
|
|
||||||
|
public readonly record struct CommandResult(bool Success, string Message, bool ClearOutput = false)
|
||||||
|
{
|
||||||
|
public static CommandResult Ok(string message = "", bool clearOutput = false) => new(true, message, clearOutput);
|
||||||
|
public static CommandResult Fail(string message) => new(false, message);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bfvfjkitb1hq7
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Game.Features.Console.Contracts;
|
||||||
|
|
||||||
|
public interface ICommand
|
||||||
|
{
|
||||||
|
string Name { get; }
|
||||||
|
string Description { get; }
|
||||||
|
IReadOnlyList<CommandArgSpec> Args { get; }
|
||||||
|
CommandResult Execute(CommandArgValues args);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d18ye7rqu75ej
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Game.Features.Console.Contracts;
|
||||||
|
|
||||||
|
public interface ICommandRegistry
|
||||||
|
{
|
||||||
|
IReadOnlyCollection<ICommand> All { get; }
|
||||||
|
void Register(ICommand command);
|
||||||
|
bool TryGet(string name, out ICommand command);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bpw55xoj10toe
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using Game.Features.Console.Contracts;
|
||||||
|
|
||||||
|
namespace Game.Features.Console.Domain;
|
||||||
|
|
||||||
|
public static class CommandArgValidator
|
||||||
|
{
|
||||||
|
private static bool ChoicesContain(IReadOnlyList<string> choices, string value) =>
|
||||||
|
choices.Any(choice => string.Equals(choice, value, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
private static bool MatchesSpec(string rawValue, CommandArgSpec spec) => spec.Type switch
|
||||||
|
{
|
||||||
|
CommandArgType.String => true,
|
||||||
|
CommandArgType.Int => int.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out _),
|
||||||
|
CommandArgType.Float => float.TryParse(rawValue, NumberStyles.Float, CultureInfo.InvariantCulture, out _),
|
||||||
|
CommandArgType.Bool => bool.TryParse(rawValue, out _),
|
||||||
|
CommandArgType.Choice => spec.Choices is not null && ChoicesContain(spec.Choices, rawValue),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(spec), spec.Type, null)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static CommandResult Validate(IReadOnlyList<CommandArgSpec> specs, IReadOnlyList<string> arguments)
|
||||||
|
{
|
||||||
|
if (arguments.Count != specs.Count)
|
||||||
|
{
|
||||||
|
return CommandResult.Fail($"Expected {specs.Count} argument(s), got {arguments.Count}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < specs.Count; i++)
|
||||||
|
{
|
||||||
|
CommandArgSpec spec = specs[i];
|
||||||
|
string rawValue = arguments[i];
|
||||||
|
|
||||||
|
if (MatchesSpec(rawValue, spec)) continue;
|
||||||
|
|
||||||
|
return CommandResult.Fail($"Argument '{spec.Name}' expected {spec}, got '{rawValue}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return CommandResult.Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dtgsw88f1pejl
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Game.Features.Console.Domain;
|
||||||
|
|
||||||
|
public static class CommandLineParser
|
||||||
|
{
|
||||||
|
public static bool TryParse(string input, out string name, out IReadOnlyList<string> arguments)
|
||||||
|
{
|
||||||
|
string[] tokens = input.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (tokens.Length == 0)
|
||||||
|
{
|
||||||
|
name = string.Empty;
|
||||||
|
arguments = [];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
name = tokens[0];
|
||||||
|
arguments = tokens[1..];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://brr37gdqsgb0m
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Game.Features.Console.Contracts;
|
||||||
|
|
||||||
|
namespace Game.Features.Console.Infrastructure;
|
||||||
|
|
||||||
|
public sealed class CommandRegistry : ICommandRegistry
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, ICommand> _commands = new();
|
||||||
|
|
||||||
|
public IReadOnlyCollection<ICommand> All => _commands.Values;
|
||||||
|
|
||||||
|
public void Register(ICommand command)
|
||||||
|
{
|
||||||
|
_commands[command.Name] = command;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGet(string name, out ICommand command) => _commands.TryGetValue(name, out command);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cfcp8d8sh0gf5
|
||||||
Reference in New Issue
Block a user