From 96414f4b65e0fd35de2532cf9e9a5d8004c47d23 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Fri, 19 Jan 2024 17:57:21 +0100 Subject: [PATCH 01/40] Execution logic modifications --- .../CSF.Samples.Console.csproj | 2 +- .../RequireOperationSystemAttribute.cs | 2 +- .../CSF.Samples.Hosting.csproj | 2 +- .../Components/IComponent.cs | 0 .../Components/IConditionalComponent.cs | 0 .../Components/IParameterComponent.cs | 0 .../Components/IParameterContainer.cs | 0 .../Contexts/ICommandContext.cs | 9 +- .../ExecutionException.cs} | 6 +- .../{Entities => Abstractions}/ModuleBase.cs | 23 ++++ .../PreconditionAttribute.cs | 10 +- src/CSF.Core/Abstractions/Results/IResult.cs | 12 ++ src/CSF.Core/Abstractions/TypeReader.cs | 50 +++++++ .../CommandAttribute.cs | 0 .../ComplexAttribute.cs | 0 .../DescriptionAttribute.cs | 0 .../DontRegisterAttribute.cs | 0 .../GroupAttribute.cs | 0 .../PrimaryConstructorAttribute.cs | 0 .../PriorityAttribute.cs | 0 .../RemainderAttribute.cs | 0 .../RequireContextAttribute.cs | 2 +- src/CSF.Core/CSF.Core.csproj | 4 +- src/CSF.Core/CommandBuildingConfiguration.cs | 63 +++++---- src/CSF.Core/CommandManager.cs | 128 +++++++++++++++--- src/CSF.Core/CommandManagerHelper.cs | 95 +++++++++++++ .../Implementation => Components}/Command.cs | 3 +- .../CommandCell.cs | 0 .../ComplexParameter.cs | 0 .../Constructor.cs | 0 .../Implementation => Components}/Module.cs | 2 +- .../Parameter.cs | 0 src/CSF.Core/Contexts/CommandContext.cs | 11 ++ .../Entities/Contexts/IStringBasedContext.cs | 18 --- .../Contexts/Implementation/CommandContext.cs | 33 ----- src/CSF.Core/Entities/Results/FailureCode.cs | 33 ----- src/CSF.Core/Entities/Results/IResult.cs | 18 --- .../Results/Implementations/FailedResult.cs | 59 -------- .../Results/Implementations/SuccessResult.cs | 36 ----- .../Entities/TypeReaders/TypeReader.cs | 117 ---------------- .../CheckException.cs | 4 +- .../CommandException.cs} | 6 +- src/CSF.Core/Exceptions/MatchException.cs | 16 +++ .../ReadException.cs | 4 +- .../SearchException.cs | 4 +- ...xecutionOptions.cs => ExecutionOptions.cs} | 18 ++- .../Extensions/Internal/BuildHelper.cs | 8 +- src/CSF.Core/Manager/Check.cs | 35 ----- src/CSF.Core/Manager/Execute.cs | 86 ------------ src/CSF.Core/Manager/Match.cs | 67 --------- src/CSF.Core/Manager/Read.cs | 89 ------------ src/CSF.Core/Manager/Search.cs | 72 ---------- .../Parsing/Implementation/TextParser.cs | 0 src/CSF.Core/{Entities => }/Parsing/Parser.cs | 0 .../{Entities => }/Parsing/ParserCell.cs | 0 src/CSF.Core/Results/CheckResult.cs | 15 ++ src/CSF.Core/Results/MatchResult.cs | 29 ++++ src/CSF.Core/Results/ReadResult.cs | 24 ++++ src/CSF.Core/Results/RunResult.cs | 27 ++++ src/CSF.Core/Results/SearchResult.cs | 24 ++++ .../BaseTypeReader.cs | 0 .../ColorTypeReader.cs | 0 .../EnumTypeReader.cs | 0 .../TimeSpanTypeReader.cs | 0 src/CSF.Hosting/CSF.Hosting.csproj | 2 +- src/CSF.Hosting/HostedCommandManager.cs | 2 +- .../CSF.Tests.Benchmarks.csproj | 2 +- 67 files changed, 520 insertions(+), 752 deletions(-) rename src/CSF.Core/{Entities => Abstractions}/Components/IComponent.cs (100%) rename src/CSF.Core/{Entities => Abstractions}/Components/IConditionalComponent.cs (100%) rename src/CSF.Core/{Entities => Abstractions}/Components/IParameterComponent.cs (100%) rename src/CSF.Core/{Entities => Abstractions}/Components/IParameterContainer.cs (100%) rename src/CSF.Core/{Entities => Abstractions}/Contexts/ICommandContext.cs (54%) rename src/CSF.Core/{Entities/Exceptions/PipelineException.cs => Abstractions/ExecutionException.cs} (51%) rename src/CSF.Core/{Entities => Abstractions}/ModuleBase.cs (70%) rename src/CSF.Core/{Entities/Attributes => Abstractions}/PreconditionAttribute.cs (85%) create mode 100644 src/CSF.Core/Abstractions/Results/IResult.cs create mode 100644 src/CSF.Core/Abstractions/TypeReader.cs rename src/CSF.Core/{Entities/Attributes/Implementation => Attributes}/CommandAttribute.cs (100%) rename src/CSF.Core/{Entities/Attributes/Implementation => Attributes}/ComplexAttribute.cs (100%) rename src/CSF.Core/{Entities/Attributes/Implementation => Attributes}/DescriptionAttribute.cs (100%) rename src/CSF.Core/{Entities/Attributes/Implementation => Attributes}/DontRegisterAttribute.cs (100%) rename src/CSF.Core/{Entities/Attributes/Implementation => Attributes}/GroupAttribute.cs (100%) rename src/CSF.Core/{Entities/Attributes/Implementation => Attributes}/PrimaryConstructorAttribute.cs (100%) rename src/CSF.Core/{Entities/Attributes/Implementation => Attributes}/PriorityAttribute.cs (100%) rename src/CSF.Core/{Entities/Attributes/Implementation => Attributes}/RemainderAttribute.cs (100%) rename src/CSF.Core/{Entities/Attributes/Implementation => Attributes}/RequireContextAttribute.cs (89%) create mode 100644 src/CSF.Core/CommandManagerHelper.cs rename src/CSF.Core/{Entities/Components/Implementation => Components}/Command.cs (98%) rename src/CSF.Core/{Entities/Components/Implementation => Components}/CommandCell.cs (100%) rename src/CSF.Core/{Entities/Components/Implementation => Components}/ComplexParameter.cs (100%) rename src/CSF.Core/{Entities/Components/Implementation => Components}/Constructor.cs (100%) rename src/CSF.Core/{Entities/Components/Implementation => Components}/Module.cs (97%) rename src/CSF.Core/{Entities/Components/Implementation => Components}/Parameter.cs (100%) create mode 100644 src/CSF.Core/Contexts/CommandContext.cs delete mode 100644 src/CSF.Core/Entities/Contexts/IStringBasedContext.cs delete mode 100644 src/CSF.Core/Entities/Contexts/Implementation/CommandContext.cs delete mode 100644 src/CSF.Core/Entities/Results/FailureCode.cs delete mode 100644 src/CSF.Core/Entities/Results/IResult.cs delete mode 100644 src/CSF.Core/Entities/Results/Implementations/FailedResult.cs delete mode 100644 src/CSF.Core/Entities/Results/Implementations/SuccessResult.cs delete mode 100644 src/CSF.Core/Entities/TypeReaders/TypeReader.cs rename src/CSF.Core/{Entities/Exceptions/Implementation => Exceptions}/CheckException.cs (63%) rename src/CSF.Core/{Entities/Exceptions/Implementation/ExecuteException.cs => Exceptions/CommandException.cs} (51%) create mode 100644 src/CSF.Core/Exceptions/MatchException.cs rename src/CSF.Core/{Entities/Exceptions/Implementation => Exceptions}/ReadException.cs (63%) rename src/CSF.Core/{Entities/Exceptions/Implementation => Exceptions}/SearchException.cs (66%) rename src/CSF.Core/{CommandExecutionOptions.cs => ExecutionOptions.cs} (53%) delete mode 100644 src/CSF.Core/Manager/Check.cs delete mode 100644 src/CSF.Core/Manager/Execute.cs delete mode 100644 src/CSF.Core/Manager/Match.cs delete mode 100644 src/CSF.Core/Manager/Read.cs delete mode 100644 src/CSF.Core/Manager/Search.cs rename src/CSF.Core/{Entities => }/Parsing/Implementation/TextParser.cs (100%) rename src/CSF.Core/{Entities => }/Parsing/Parser.cs (100%) rename src/CSF.Core/{Entities => }/Parsing/ParserCell.cs (100%) create mode 100644 src/CSF.Core/Results/CheckResult.cs create mode 100644 src/CSF.Core/Results/MatchResult.cs create mode 100644 src/CSF.Core/Results/ReadResult.cs create mode 100644 src/CSF.Core/Results/RunResult.cs create mode 100644 src/CSF.Core/Results/SearchResult.cs rename src/CSF.Core/{Entities/TypeReaders/Implementation => TypeReaders}/BaseTypeReader.cs (100%) rename src/CSF.Core/{Entities/TypeReaders/Implementation => TypeReaders}/ColorTypeReader.cs (100%) rename src/CSF.Core/{Entities/TypeReaders/Implementation => TypeReaders}/EnumTypeReader.cs (100%) rename src/CSF.Core/{Entities/TypeReaders/Implementation => TypeReaders}/TimeSpanTypeReader.cs (100%) diff --git a/examples/CSF.Samples.Console/CSF.Samples.Console.csproj b/examples/CSF.Samples.Console/CSF.Samples.Console.csproj index d455298..e875e9f 100644 --- a/examples/CSF.Samples.Console/CSF.Samples.Console.csproj +++ b/examples/CSF.Samples.Console/CSF.Samples.Console.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0 enable enable diff --git a/examples/CSF.Samples.Console/Preconditions/RequireOperationSystemAttribute.cs b/examples/CSF.Samples.Console/Preconditions/RequireOperationSystemAttribute.cs index 61a18ab..eabaa42 100644 --- a/examples/CSF.Samples.Console/Preconditions/RequireOperationSystemAttribute.cs +++ b/examples/CSF.Samples.Console/Preconditions/RequireOperationSystemAttribute.cs @@ -11,7 +11,7 @@ public RequireOperatingSystemAttribute(PlatformID platform) Platform = platform; } - public override Result Evaluate(ICommandContext context, Command command, IServiceProvider provider) + public override Result EvaluateAsync(ICommandContext context, Command command, IServiceProvider provider) { if (Environment.OSVersion.Platform == Platform) return Success(); diff --git a/examples/CSF.Samples.Hosting/CSF.Samples.Hosting.csproj b/examples/CSF.Samples.Hosting/CSF.Samples.Hosting.csproj index 6ce426b..fa2c405 100644 --- a/examples/CSF.Samples.Hosting/CSF.Samples.Hosting.csproj +++ b/examples/CSF.Samples.Hosting/CSF.Samples.Hosting.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 enable enable diff --git a/src/CSF.Core/Entities/Components/IComponent.cs b/src/CSF.Core/Abstractions/Components/IComponent.cs similarity index 100% rename from src/CSF.Core/Entities/Components/IComponent.cs rename to src/CSF.Core/Abstractions/Components/IComponent.cs diff --git a/src/CSF.Core/Entities/Components/IConditionalComponent.cs b/src/CSF.Core/Abstractions/Components/IConditionalComponent.cs similarity index 100% rename from src/CSF.Core/Entities/Components/IConditionalComponent.cs rename to src/CSF.Core/Abstractions/Components/IConditionalComponent.cs diff --git a/src/CSF.Core/Entities/Components/IParameterComponent.cs b/src/CSF.Core/Abstractions/Components/IParameterComponent.cs similarity index 100% rename from src/CSF.Core/Entities/Components/IParameterComponent.cs rename to src/CSF.Core/Abstractions/Components/IParameterComponent.cs diff --git a/src/CSF.Core/Entities/Components/IParameterContainer.cs b/src/CSF.Core/Abstractions/Components/IParameterContainer.cs similarity index 100% rename from src/CSF.Core/Entities/Components/IParameterContainer.cs rename to src/CSF.Core/Abstractions/Components/IParameterContainer.cs diff --git a/src/CSF.Core/Entities/Contexts/ICommandContext.cs b/src/CSF.Core/Abstractions/Contexts/ICommandContext.cs similarity index 54% rename from src/CSF.Core/Entities/Contexts/ICommandContext.cs rename to src/CSF.Core/Abstractions/Contexts/ICommandContext.cs index f07e79c..1688d0a 100644 --- a/src/CSF.Core/Entities/Contexts/ICommandContext.cs +++ b/src/CSF.Core/Abstractions/Contexts/ICommandContext.cs @@ -6,13 +6,8 @@ public interface ICommandContext { /// - /// The command parameters. + /// The command options. /// - public string[] Parameters { get; set; } - - /// - /// The command name. - /// - public string Name { get; set; } + public IExecutionOptions Options { get; } } } diff --git a/src/CSF.Core/Entities/Exceptions/PipelineException.cs b/src/CSF.Core/Abstractions/ExecutionException.cs similarity index 51% rename from src/CSF.Core/Entities/Exceptions/PipelineException.cs rename to src/CSF.Core/Abstractions/ExecutionException.cs index 6edcdc1..461453f 100644 --- a/src/CSF.Core/Entities/Exceptions/PipelineException.cs +++ b/src/CSF.Core/Abstractions/ExecutionException.cs @@ -1,14 +1,14 @@ namespace CSF { - public abstract class PipelineException : Exception + public abstract class ExecutionException : Exception { - public PipelineException(string message) + public ExecutionException(string message) : base(message) { } - public PipelineException(string message, Exception innerException = null) + public ExecutionException(string message, Exception innerException = null) : base(message, innerException) { diff --git a/src/CSF.Core/Entities/ModuleBase.cs b/src/CSF.Core/Abstractions/ModuleBase.cs similarity index 70% rename from src/CSF.Core/Entities/ModuleBase.cs rename to src/CSF.Core/Abstractions/ModuleBase.cs index d1a0fd3..9da3e48 100644 --- a/src/CSF.Core/Entities/ModuleBase.cs +++ b/src/CSF.Core/Abstractions/ModuleBase.cs @@ -40,5 +40,28 @@ public abstract class ModuleBase /// The message to send. public virtual void Respond(string message) => Console.WriteLine(message); + + public virtual CommandManager.RunResult ReturnTypeHandle(object value) + { + switch (value) + { + case Task task: + return new(Command, task); + case null: + return new(Command, null); + default: + throw new NotSupportedException("The return value of this command is not supported."); + } + } + + public virtual async ValueTask BeforeExecuteAsync() + { + + } + + public virtual async ValueTask AfterExecuteAsync() + { + + } } } diff --git a/src/CSF.Core/Entities/Attributes/PreconditionAttribute.cs b/src/CSF.Core/Abstractions/PreconditionAttribute.cs similarity index 85% rename from src/CSF.Core/Entities/Attributes/PreconditionAttribute.cs rename to src/CSF.Core/Abstractions/PreconditionAttribute.cs index 172bc65..34b509d 100644 --- a/src/CSF.Core/Entities/Attributes/PreconditionAttribute.cs +++ b/src/CSF.Core/Abstractions/PreconditionAttribute.cs @@ -13,7 +13,7 @@ public abstract class PreconditionAttribute : Attribute /// The command that is to be executed if this and all other precondition evaluations succeed. /// The services in scope for the current command execution. /// A result that represents the outcome of the evaluation. - public abstract Result Evaluate(ICommandContext context, Command command, IServiceProvider services); + public abstract async Task EvaluateAsync(ICommandContext context, Command command); /// /// Returns that the evaluation has failed. @@ -28,14 +28,6 @@ protected static Result Failure(string reason) protected static Result Success() => new(); - internal void EvalInternal(ICommandContext context, Command command, IServiceProvider services) - { - var result = Evaluate(context, command, services); - - if (!result.IsSuccess) - throw new CheckException(result.Reason); - } - /// /// Represents the result structure that displays the returned state of the evaluation. /// diff --git a/src/CSF.Core/Abstractions/Results/IResult.cs b/src/CSF.Core/Abstractions/Results/IResult.cs new file mode 100644 index 0000000..6135ab6 --- /dev/null +++ b/src/CSF.Core/Abstractions/Results/IResult.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSF +{ + public interface IResult + { + } +} diff --git a/src/CSF.Core/Abstractions/TypeReader.cs b/src/CSF.Core/Abstractions/TypeReader.cs new file mode 100644 index 0000000..a6e206c --- /dev/null +++ b/src/CSF.Core/Abstractions/TypeReader.cs @@ -0,0 +1,50 @@ +namespace CSF +{ + /// + /// Represents a generic to use for parsing provided types into the targetted type. + /// + /// The targetted type for this typereader. + public abstract class TypeReader : TypeReader + { + /// + public override Type Type { get; } = typeof(T); + + /// + public override abstract Result Evaluate(ICommandContext context, IParameterComponent parameter, IServiceProvider services, string value); + } + + public abstract class TypeReader + { + /// + /// The type that this reader intends to return. + /// + public abstract Type Type { get; } + + /// + /// Evaluates an input and tries to parse it into a value that matches the expected parameter type. + /// + /// The command context used to execute the command currently in scope. + /// The parameter this input evaluation is targetting. + /// The services in scope for the current command execution. + /// The input that this evaluation intends to convert into the expected parameter type. + /// A result that represents the outcome of the evaluation. + public abstract ValueTask EvaluateAsync(ICommandContext context, IParameterComponent parameter, object value); + + /// + /// Gets a range of default s. + /// + /// A range of s that are defined in the library by default. + public static TypeReader[] CreateDefaultReaders() + { + var range = BaseTypeReader.CreateBaseReaders(); + + int length = range.Length; + Array.Resize(ref range, length + 2); + + range[length++] = new TimeSpanTypeReader(); + range[length++] = new ColorTypeReader(); + + return range; + } + } +} diff --git a/src/CSF.Core/Entities/Attributes/Implementation/CommandAttribute.cs b/src/CSF.Core/Attributes/CommandAttribute.cs similarity index 100% rename from src/CSF.Core/Entities/Attributes/Implementation/CommandAttribute.cs rename to src/CSF.Core/Attributes/CommandAttribute.cs diff --git a/src/CSF.Core/Entities/Attributes/Implementation/ComplexAttribute.cs b/src/CSF.Core/Attributes/ComplexAttribute.cs similarity index 100% rename from src/CSF.Core/Entities/Attributes/Implementation/ComplexAttribute.cs rename to src/CSF.Core/Attributes/ComplexAttribute.cs diff --git a/src/CSF.Core/Entities/Attributes/Implementation/DescriptionAttribute.cs b/src/CSF.Core/Attributes/DescriptionAttribute.cs similarity index 100% rename from src/CSF.Core/Entities/Attributes/Implementation/DescriptionAttribute.cs rename to src/CSF.Core/Attributes/DescriptionAttribute.cs diff --git a/src/CSF.Core/Entities/Attributes/Implementation/DontRegisterAttribute.cs b/src/CSF.Core/Attributes/DontRegisterAttribute.cs similarity index 100% rename from src/CSF.Core/Entities/Attributes/Implementation/DontRegisterAttribute.cs rename to src/CSF.Core/Attributes/DontRegisterAttribute.cs diff --git a/src/CSF.Core/Entities/Attributes/Implementation/GroupAttribute.cs b/src/CSF.Core/Attributes/GroupAttribute.cs similarity index 100% rename from src/CSF.Core/Entities/Attributes/Implementation/GroupAttribute.cs rename to src/CSF.Core/Attributes/GroupAttribute.cs diff --git a/src/CSF.Core/Entities/Attributes/Implementation/PrimaryConstructorAttribute.cs b/src/CSF.Core/Attributes/PrimaryConstructorAttribute.cs similarity index 100% rename from src/CSF.Core/Entities/Attributes/Implementation/PrimaryConstructorAttribute.cs rename to src/CSF.Core/Attributes/PrimaryConstructorAttribute.cs diff --git a/src/CSF.Core/Entities/Attributes/Implementation/PriorityAttribute.cs b/src/CSF.Core/Attributes/PriorityAttribute.cs similarity index 100% rename from src/CSF.Core/Entities/Attributes/Implementation/PriorityAttribute.cs rename to src/CSF.Core/Attributes/PriorityAttribute.cs diff --git a/src/CSF.Core/Entities/Attributes/Implementation/RemainderAttribute.cs b/src/CSF.Core/Attributes/RemainderAttribute.cs similarity index 100% rename from src/CSF.Core/Entities/Attributes/Implementation/RemainderAttribute.cs rename to src/CSF.Core/Attributes/RemainderAttribute.cs diff --git a/src/CSF.Core/Entities/Attributes/Implementation/RequireContextAttribute.cs b/src/CSF.Core/Attributes/RequireContextAttribute.cs similarity index 89% rename from src/CSF.Core/Entities/Attributes/Implementation/RequireContextAttribute.cs rename to src/CSF.Core/Attributes/RequireContextAttribute.cs index 62dc795..92f4fe9 100644 --- a/src/CSF.Core/Entities/Attributes/Implementation/RequireContextAttribute.cs +++ b/src/CSF.Core/Attributes/RequireContextAttribute.cs @@ -19,7 +19,7 @@ public RequireContextAttribute(Type contextType) ContextType = contextType; } - public override Result Evaluate(ICommandContext context, Command command, IServiceProvider provider) + public override Result EvaluateAsync(ICommandContext context, Command command, IServiceProvider provider) { var providedType = context.GetType(); diff --git a/src/CSF.Core/CSF.Core.csproj b/src/CSF.Core/CSF.Core.csproj index cea5f4e..c7b9756 100644 --- a/src/CSF.Core/CSF.Core.csproj +++ b/src/CSF.Core/CSF.Core.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable CSF @@ -42,7 +42,7 @@ - + diff --git a/src/CSF.Core/CommandBuildingConfiguration.cs b/src/CSF.Core/CommandBuildingConfiguration.cs index 6ee2774..d9ca317 100644 --- a/src/CSF.Core/CommandBuildingConfiguration.cs +++ b/src/CSF.Core/CommandBuildingConfiguration.cs @@ -2,30 +2,45 @@ namespace CSF { - /// - /// Represents a context used to set up the command manager and its child containers. - /// - public sealed class CommandBuildingConfiguration + public sealed class FrameworkBuilder { - /// - /// The assemblies to be used to register modules and typereaders automatically. Items will be registered when any of the following conditions are true: - /// - /// - /// - /// The type inherits or , does not contain undeclared generic parameters and is public. - /// The type inherits or , does not contain undeclared generic parameters and is public. - /// - /// - public Assembly[] RegistrationAssemblies { get; set; } = new[] { Assembly.GetEntryAssembly() }; - - /// - /// A range of typereaders that are to be manually registered to all existing readers. - /// - public TypeReader[] TypeReaders { get; set; } = Array.Empty(); - - /// - /// Gets the default configuration that is used when no is provided at manager creation. - /// - public static CommandBuildingConfiguration Default { get; } = new(); + public List Assemblies { get; set; } = [ Assembly.GetEntryAssembly() ]; + + public List TypeReaders { get; set; } = []; + + public FrameworkBuilder AddAssembly() + { + Assemblies.Add(Assembly.GetEntryAssembly()); + return this; + } + + public FrameworkBuilder AddAssembly(Assembly assembly) + { + Assemblies.Add(assembly); + return this; + } + + public FrameworkBuilder WithAssemblies(params Assembly[] assemblies) + { + Assemblies.AddRange(assemblies); + return this; + } + + public FrameworkBuilder AddTypeReader(TypeReader typeReader) + { + TypeReaders.Add(typeReader); + return this; + } + + public FrameworkBuilder WithTypeReaders(params TypeReader[] typeReaders) + { + TypeReaders.AddRange(typeReaders); + return this; + } + + public CommandManager Build() + { + + } } } diff --git a/src/CSF.Core/CommandManager.cs b/src/CSF.Core/CommandManager.cs index 095d243..e10ecf5 100644 --- a/src/CSF.Core/CommandManager.cs +++ b/src/CSF.Core/CommandManager.cs @@ -1,4 +1,5 @@ [assembly: CLSCompliant(true)] + namespace CSF { /// @@ -7,35 +8,128 @@ namespace CSF /// /// Guides and documentation can be found at: /// - public partial class CommandManager + public class CommandManager { - private readonly IServiceProvider _services; - /// /// Gets the components registered to this manager. /// public HashSet Components { get; } - /// - /// Creates a new with default configuration. - /// - /// The services to use to handle command execution and module registration. - public CommandManager(IServiceProvider services) - : this(services, CommandBuildingConfiguration.Default) + public async Task ExecuteAsync(ICommandContext context, object[] args) { + // search all relevant commands. + var searches = Search(args); + + // define a fallback for unsuccesful execution. + MatchResult? fallback = default; + + // order searches by descending for priority definitions. + foreach (var search in searches.OrderByDescending(x => x.Command.Priority)) + { + var match = await MatchAsync(context, search, args); + if (fallback is not null) + fallback = match; + + if (match.Success) + return await RunAsync(context, match); + } + + if (!fallback.HasValue) + return new SearchResult(new SearchException("")); + + return fallback; } - /// - /// Creates a new with provided configuration. - /// - /// The services used to handle command execution and module registration. - /// The configuration that should be used to construct the manager. - public CommandManager(IServiceProvider services, CommandBuildingConfiguration configuration) + public IEnumerable Search(object[] args) { - _services = services; + // recursively search for commands in the execution. + return Components.RecursiveSearch(args, 0); + } + + public static async Task MatchAsync(ICommandContext context, SearchResult search, object[] args) + { + // check command preconditions. + var check = await CheckAsync(context, search.Command); + + // verify check success, if not, return the failure. + if (!check.Success) + return new(search.Command, new MatchException("", check.Exception)); + + // read the command parameters in right order. + var readResult = await ReadAsync(context, search, args); + + // exchange the reads for result, verifying successes in the process. + var reads = new object[readResult.Length]; + for (int i = 0; i < readResult.Length; i++) + { + // check for read success. + if (!readResult[i].Success) + return new(search.Command, readResult[i].Exception); + + reads[i] = readResult[i]; + } + + // return successful match if execution reaches here. + return new(search.Command, reads); + } + + public static async Task ReadAsync(ICommandContext context, SearchResult search, object[] args) + { + // skip if no parameters exist. + if (!search.Command.HasParameters) + return []; + + // determine height of search to discover command name. + var length = args.Length - search.SearchHeight; + + // check if input equals command length. + if (search.Command.MaxLength == length) + return await search.Command.Parameters.RecursiveReadAsync(context, args[length..], 0); + + // check if input is longer than command, but remainder to concatenate. + if (search.Command.MaxLength <= length && search.Command.HasRemainder) + return await search.Command.Parameters.RecursiveReadAsync(context, args[length..], 0); + + // check if input is shorter than command, but optional parameters to replace. + if (search.Command.MaxLength > length && search.Command.MinLength <= length) + return await search.Command.Parameters.RecursiveReadAsync(context, args[length..], 0); + + // input is too long or too short. + return []; + } + + public static async Task CheckAsync(ICommandContext context, Command command) + { + foreach (var precon in command.Preconditions) + { + if (!await precon.EvaluateAsync(context, command)) + return new(new CheckException("")); + } + return new(); + } + + public static async Task RunAsync(ICommandContext context, MatchResult match) + { + try + { + var module = context.Options.Scope.ServiceProvider.GetService(match.Command.Module.Type) as ModuleBase; + + module.Context = context; + module.Command = match.Command; + + await module.BeforeExecuteAsync(); + + var value = match.Command.Target.Invoke(module, match.Reads); + + await module.AfterExecuteAsync(); - Components = configuration.Build(); + return module.ReturnTypeHandle(value); + } + catch (Exception exception) + { + return new(match.Command, exception); + } } } } diff --git a/src/CSF.Core/CommandManagerHelper.cs b/src/CSF.Core/CommandManagerHelper.cs new file mode 100644 index 0000000..3321442 --- /dev/null +++ b/src/CSF.Core/CommandManagerHelper.cs @@ -0,0 +1,95 @@ +namespace CSF +{ + internal static class CommandManagerHelper + { + public static IEnumerable RecursiveSearch(this IEnumerable components, object[] args, int searchHeight) + { + List discovered = []; + + // select command by name or alias. + var selection = components.Where(command => command.Aliases.Any(x => x == (string)args[searchHeight])); + + foreach (var component in selection) + { + if (component is Module module) + { + // add the cluster found in the next iteration, if any. + var nested = module.Components.RecursiveSearch(args, searchHeight + 1); + discovered.AddRange(nested); + } + else + // add the top level matches immediately. + discovered.Add(new(component as Command, searchHeight)); + + // when the ranges fail, no results should return. + } + + return discovered; + } + + public static async Task RecursiveReadAsync(this IParameterComponent[] param, ICommandContext context, object[] args, int index) + { + + static async ValueTask ReadAsync(IParameterComponent param, ICommandContext context, object arg) + { + if (arg.GetType() == param.Type) + return new(arg); + + if (param.IsNullable && arg is null or "null" or "nothing") + return new(arg); + + return await param.TypeReader.EvaluateAsync(context, param, arg); + } + + var results = new ReadResult[param.Length]; + + for (int i = 0; i < param.Length; i++) + { + var parameter = param[i]; + + if (parameter.IsRemainder) + { + var input = string.Join(" ", args.Skip(index)); + if (parameter.Type == typeof(string)) + results[i] = new(input); + else + results[i] = await ReadAsync(parameter, context, input); + + break; + } + + if (parameter.IsOptional && args.Length <= index) + { + results[i] = new(Type.Missing); + continue; + } + + if (parameter is ComplexParameter complex) + { + var result = await complex.Parameters.RecursiveReadAsync(context, args, index); + + index += result.Length; + + if (result.Any(x => !x.Success)) + { + try + { + var obj = complex.Constructor.Target.Invoke(result.Select(x => x.Value).ToArray()); + results[i] = new(obj); + } + catch (Exception ex) + { + results[i] = new(ex); + } + } + continue; + } + + results[i] = await ReadAsync(parameter, context, args[index]); + index++; + } + + return results; + } + } +} diff --git a/src/CSF.Core/Entities/Components/Implementation/Command.cs b/src/CSF.Core/Components/Command.cs similarity index 98% rename from src/CSF.Core/Entities/Components/Implementation/Command.cs rename to src/CSF.Core/Components/Command.cs index ec4e521..94bce24 100644 --- a/src/CSF.Core/Entities/Components/Implementation/Command.cs +++ b/src/CSF.Core/Components/Command.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System.Dynamic; +using System.Reflection; namespace CSF { diff --git a/src/CSF.Core/Entities/Components/Implementation/CommandCell.cs b/src/CSF.Core/Components/CommandCell.cs similarity index 100% rename from src/CSF.Core/Entities/Components/Implementation/CommandCell.cs rename to src/CSF.Core/Components/CommandCell.cs diff --git a/src/CSF.Core/Entities/Components/Implementation/ComplexParameter.cs b/src/CSF.Core/Components/ComplexParameter.cs similarity index 100% rename from src/CSF.Core/Entities/Components/Implementation/ComplexParameter.cs rename to src/CSF.Core/Components/ComplexParameter.cs diff --git a/src/CSF.Core/Entities/Components/Implementation/Constructor.cs b/src/CSF.Core/Components/Constructor.cs similarity index 100% rename from src/CSF.Core/Entities/Components/Implementation/Constructor.cs rename to src/CSF.Core/Components/Constructor.cs diff --git a/src/CSF.Core/Entities/Components/Implementation/Module.cs b/src/CSF.Core/Components/Module.cs similarity index 97% rename from src/CSF.Core/Entities/Components/Implementation/Module.cs rename to src/CSF.Core/Components/Module.cs index 958e4a1..59fd6e1 100644 --- a/src/CSF.Core/Entities/Components/Implementation/Module.cs +++ b/src/CSF.Core/Components/Module.cs @@ -53,7 +53,7 @@ internal Module(Type type, IDictionary typeReaders, Module roo Components = this.Build(typeReaders); Name = expectedName ?? type.Name; - Aliases = aliases ?? new string[] { Name }; + Aliases = aliases ?? [ Name ]; } /// diff --git a/src/CSF.Core/Entities/Components/Implementation/Parameter.cs b/src/CSF.Core/Components/Parameter.cs similarity index 100% rename from src/CSF.Core/Entities/Components/Implementation/Parameter.cs rename to src/CSF.Core/Components/Parameter.cs diff --git a/src/CSF.Core/Contexts/CommandContext.cs b/src/CSF.Core/Contexts/CommandContext.cs new file mode 100644 index 0000000..547851d --- /dev/null +++ b/src/CSF.Core/Contexts/CommandContext.cs @@ -0,0 +1,11 @@ +namespace CSF +{ + /// + /// Represents a class that's used to describe data from the command. + /// + public class CommandContext(T options) : ICommandContext + where T : IExecutionOptions + { + public IExecutionOptions Options { get; } = options; + } +} diff --git a/src/CSF.Core/Entities/Contexts/IStringBasedContext.cs b/src/CSF.Core/Entities/Contexts/IStringBasedContext.cs deleted file mode 100644 index 265a27f..0000000 --- a/src/CSF.Core/Entities/Contexts/IStringBasedContext.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace CSF -{ - /// - /// Represents an abstract context for text commands. - /// - public interface IStringBasedContext : ICommandContext - { - /// - /// The raw input of the command. - /// - public string RawInput { get; set; } - - /// - /// The flags present on the command input. - /// - public IReadOnlyDictionary NamedParameters { get; } - } -} diff --git a/src/CSF.Core/Entities/Contexts/Implementation/CommandContext.cs b/src/CSF.Core/Entities/Contexts/Implementation/CommandContext.cs deleted file mode 100644 index 68bf9b2..0000000 --- a/src/CSF.Core/Entities/Contexts/Implementation/CommandContext.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace CSF -{ - /// - /// Represents a class that's used to describe data from the command. - /// - public class CommandContext : IStringBasedContext - { - /// - public string Name { get; set; } - - /// - public string[] Parameters { get; set; } - - /// - public IReadOnlyDictionary NamedParameters { get; } - - /// - public string RawInput { get; set; } - - public CommandContext(string rawInput, Parser parser = null) - { - parser ??= Parser.Text; - - var result = parser.Parse(rawInput); - - Name = result.Name; - Parameters = result.Parameters; - NamedParameters = result.NamedParameters; - - RawInput = rawInput; - } - } -} diff --git a/src/CSF.Core/Entities/Results/FailureCode.cs b/src/CSF.Core/Entities/Results/FailureCode.cs deleted file mode 100644 index 0af3609..0000000 --- a/src/CSF.Core/Entities/Results/FailureCode.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace CSF -{ - /// - /// Represents the error code that occurred during command execution. means there was no negative result. - /// - public enum FailureCode : int - { - /// - /// Represents no negative result. - /// - Success, - - /// - /// Represents a result tied to . Thrown when no command could be found. - /// - Search, - - /// - /// Represents a result tied to . Thrown when no matched command succeeded its precondition checks. - /// - Check, - - /// - /// Represents a result tied to . Thrown when no matched command succeeded parsing its parameters. - /// - Read, - - /// - /// Represents a result tied to . Thrown when the command being executed failed to run its body. - /// - Execute, - } -} diff --git a/src/CSF.Core/Entities/Results/IResult.cs b/src/CSF.Core/Entities/Results/IResult.cs deleted file mode 100644 index 998b45a..0000000 --- a/src/CSF.Core/Entities/Results/IResult.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace CSF -{ - /// - /// Represents a - /// - public interface IResult - { - public TaskAwaiter GetAwaiter(); - - /// - /// Checks if the command from which this was formatted failed to execute or not. - /// - /// if the command failed to execute. if it succeeded. - public bool Failed(out FailedResult result); - } -} diff --git a/src/CSF.Core/Entities/Results/Implementations/FailedResult.cs b/src/CSF.Core/Entities/Results/Implementations/FailedResult.cs deleted file mode 100644 index e45c49f..0000000 --- a/src/CSF.Core/Entities/Results/Implementations/FailedResult.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace CSF -{ - /// - /// Represents a result that was produced by a command pipeline. - /// - public readonly struct FailedResult : IResult - { - /// - /// Gets the code that represents where the pipeline failed. - /// - public FailureCode Code { get; } = FailureCode.Execute; - - /// - /// The exception thrown by the pipeline in case of failure. In most cases, this is a . The is associated with the following exceptions: - /// - /// - /// - /// - . Thrown when no command could be found. - /// - . Thrown when no matched command succeeded its precondition checks. - /// - . Thrown when no matched command succeeded parsing its parameters. - /// - . Thrown when the command being executed failed to run its body. - /// - /// This range is determined by the execution flow. Exceptions will occur in this order. - /// - public Exception Exception { get; } = null; - - /// - /// Creates a new from the provided result code and exception. - /// - /// - /// - public FailedResult(FailureCode code, Exception exception = null) - { - Code = code; - Exception = exception; - } - - /// - public bool Failed([NotNullWhen(true)] out FailedResult result) - { - result = this; - return true; - } - - /// - public TaskAwaiter GetAwaiter() - => Task.CompletedTask.GetAwaiter(); - - /// - /// Formats a readable output from this . - /// - /// - public override string ToString() - => $"[{Code}] " + Exception; - } -} diff --git a/src/CSF.Core/Entities/Results/Implementations/SuccessResult.cs b/src/CSF.Core/Entities/Results/Implementations/SuccessResult.cs deleted file mode 100644 index c71ce22..0000000 --- a/src/CSF.Core/Entities/Results/Implementations/SuccessResult.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace CSF -{ - public readonly struct SuccessResult : IResult - { - public Task CommandTask { get; } - - public SuccessResult(Task t) - { - CommandTask = t; - } - - public SuccessResult() - { - CommandTask = Task.CompletedTask; - } - - /// - public bool Failed([NotNullWhen(true)] out FailedResult result) - { - result = default; - return false; - } - - /// - public TaskAwaiter GetAwaiter() - => CommandTask.GetAwaiter(); - - public static implicit operator Task(SuccessResult result) - { - return result.CommandTask; - } - } -} diff --git a/src/CSF.Core/Entities/TypeReaders/TypeReader.cs b/src/CSF.Core/Entities/TypeReaders/TypeReader.cs deleted file mode 100644 index 8e30142..0000000 --- a/src/CSF.Core/Entities/TypeReaders/TypeReader.cs +++ /dev/null @@ -1,117 +0,0 @@ -namespace CSF -{ - /// - /// Represents a generic to use for parsing provided types into the targetted type. - /// - /// The targetted type for this typereader. - public abstract class TypeReader : TypeReader - { - /// - public override Type Type { get; } = typeof(T); - - /// - public override abstract Result Evaluate(ICommandContext context, IParameterComponent parameter, IServiceProvider services, string value); - } - - public abstract class TypeReader - { - /// - /// The type that this reader intends to return. - /// - public abstract Type Type { get; } - - /// - /// Evaluates an input and tries to parse it into a value that matches the expected parameter type. - /// - /// The command context used to execute the command currently in scope. - /// The parameter this input evaluation is targetting. - /// The services in scope for the current command execution. - /// The input that this evaluation intends to convert into the expected parameter type. - /// A result that represents the outcome of the evaluation. - public abstract Result Evaluate(ICommandContext context, IParameterComponent parameter, IServiceProvider services, string value); - - /// - /// Returns that the evaluation has failed. - /// - /// The reason why the evaluation failed. - protected static Result Failure(string reason) - => new(reason); - - /// - /// Returns that the evaluation has succeeded. - /// - /// The value the evaluation returns upon success. - protected static Result Success(object value) - => new(value); - - internal object EvalInternal(ICommandContext context, IParameterComponent parameter, IServiceProvider services, string value) - { - var result = Evaluate(context, parameter, services, value); - - if (!result.IsSuccess) - throw new ReadException(result.Reason); - - return result.Value; - } - - /// - /// Represents the result structure that displays the returned state of the evaluation. - /// - public readonly struct Result - { - /// - /// Gets if the evaluation was successful or not. - /// - public bool IsSuccess { get; } - - /// - /// Gets the value the evaluation returned if it succeeded. - /// - public object Value { get; } - - /// - /// Gets the reason of failure in case it has. - /// - public string Reason { get; } - - /// - /// Creates a new successful evaluation result. - /// - /// The value that the evaluation is supposed to return upon success. - public Result(object value) - { - IsSuccess = true; - Reason = null; - Value = value; - } - - /// - /// Creates a new failed evaluation result. - /// - /// The reason why the evaluation has failed. - public Result(string reason) - { - IsSuccess = false; - Reason = reason; - Value = null; - } - } - - /// - /// Gets a range of default s. - /// - /// A range of s that are defined in the library by default. - public static TypeReader[] CreateDefaultReaders() - { - var range = BaseTypeReader.CreateBaseReaders(); - - int length = range.Length; - Array.Resize(ref range, length + 2); - - range[length++] = new TimeSpanTypeReader(); - range[length++] = new ColorTypeReader(); - - return range; - } - } -} diff --git a/src/CSF.Core/Entities/Exceptions/Implementation/CheckException.cs b/src/CSF.Core/Exceptions/CheckException.cs similarity index 63% rename from src/CSF.Core/Entities/Exceptions/Implementation/CheckException.cs rename to src/CSF.Core/Exceptions/CheckException.cs index 4a14afe..fbed9b3 100644 --- a/src/CSF.Core/Entities/Exceptions/Implementation/CheckException.cs +++ b/src/CSF.Core/Exceptions/CheckException.cs @@ -1,9 +1,9 @@ namespace CSF { /// - /// Represents a that is thrown when no matched command succeeded its precondition checks. + /// Represents a that is thrown when no matched command succeeded its precondition checks. /// - public sealed class CheckException : PipelineException + public sealed class CheckException : ExecutionException { public CheckException(string message, Exception innerException = null) : base(message, innerException) diff --git a/src/CSF.Core/Entities/Exceptions/Implementation/ExecuteException.cs b/src/CSF.Core/Exceptions/CommandException.cs similarity index 51% rename from src/CSF.Core/Entities/Exceptions/Implementation/ExecuteException.cs rename to src/CSF.Core/Exceptions/CommandException.cs index f0b6363..5657a9f 100644 --- a/src/CSF.Core/Entities/Exceptions/Implementation/ExecuteException.cs +++ b/src/CSF.Core/Exceptions/CommandException.cs @@ -1,11 +1,11 @@ namespace CSF { /// - /// Represents a that is thrown when the command being executed failed to run its body. + /// Represents a that is thrown when the command being executed failed to run its body. /// - public sealed class ExecuteException : PipelineException + public sealed class CommandException : ExecutionException { - public ExecuteException(string message, Exception innerException = null) + public CommandException(string message, Exception innerException = null) : base(message, innerException) { diff --git a/src/CSF.Core/Exceptions/MatchException.cs b/src/CSF.Core/Exceptions/MatchException.cs new file mode 100644 index 0000000..3bf1414 --- /dev/null +++ b/src/CSF.Core/Exceptions/MatchException.cs @@ -0,0 +1,16 @@ +namespace CSF +{ + public class MatchException : ExecutionException + { + public MatchException(string message, Exception innerException = null) + : base(message, innerException) + { + + } + + public override FailedResult AsResult() + { + return new(FailureCode.Execute, this); + } + } +} diff --git a/src/CSF.Core/Entities/Exceptions/Implementation/ReadException.cs b/src/CSF.Core/Exceptions/ReadException.cs similarity index 63% rename from src/CSF.Core/Entities/Exceptions/Implementation/ReadException.cs rename to src/CSF.Core/Exceptions/ReadException.cs index bf9ab51..d6f1802 100644 --- a/src/CSF.Core/Entities/Exceptions/Implementation/ReadException.cs +++ b/src/CSF.Core/Exceptions/ReadException.cs @@ -1,9 +1,9 @@ namespace CSF { /// - /// Represents a that is thrown when no matched command succeeded parsing its parameters. + /// Represents a that is thrown when no matched command succeeded parsing its parameters. /// - public sealed class ReadException : PipelineException + public sealed class ReadException : ExecutionException { public ReadException(string message, Exception innerException = null) : base(message, innerException) diff --git a/src/CSF.Core/Entities/Exceptions/Implementation/SearchException.cs b/src/CSF.Core/Exceptions/SearchException.cs similarity index 66% rename from src/CSF.Core/Entities/Exceptions/Implementation/SearchException.cs rename to src/CSF.Core/Exceptions/SearchException.cs index 18a2fbb..517040a 100644 --- a/src/CSF.Core/Entities/Exceptions/Implementation/SearchException.cs +++ b/src/CSF.Core/Exceptions/SearchException.cs @@ -1,9 +1,9 @@ namespace CSF { /// - /// Represents a that is thrown when no command could be found. + /// Represents a that is thrown when no command could be found. /// - public sealed class SearchException : PipelineException + public sealed class SearchException : ExecutionException { public SearchException(string message, Exception innerException = null) : base(message, innerException) diff --git a/src/CSF.Core/CommandExecutionOptions.cs b/src/CSF.Core/ExecutionOptions.cs similarity index 53% rename from src/CSF.Core/CommandExecutionOptions.cs rename to src/CSF.Core/ExecutionOptions.cs index 9ab25d4..e4fd49c 100644 --- a/src/CSF.Core/CommandExecutionOptions.cs +++ b/src/CSF.Core/ExecutionOptions.cs @@ -2,19 +2,27 @@ namespace CSF { + public interface IExecutionOptions + { + /// + /// Gets or sets the service scope to be used in command execution. The services used for executing commands will be defaulted to the globally defined if this property is not set. + /// + public IServiceScope Scope { get; set; } + } + /// /// Represents a set of options that change the execution flow of the command handler. /// - public class CommandExecutionOptions + public class ExecutionOptions : IExecutionOptions { /// /// Gets or sets the service scope to be used in command execution. The services used for executing commands will be defaulted to the globally defined if this property is not set. /// public IServiceScope Scope { get; set; } = null; - /// - /// Gets the default options that are used across all executions that don't provide customized . - /// - public static CommandExecutionOptions Default { get; } = new(); + public ExecutionOptions(IServiceProvider provider) + { + Scope = provider.CreateScope(); + } } } diff --git a/src/CSF.Core/Extensions/Internal/BuildHelper.cs b/src/CSF.Core/Extensions/Internal/BuildHelper.cs index 1e077f1..7b26ac6 100644 --- a/src/CSF.Core/Extensions/Internal/BuildHelper.cs +++ b/src/CSF.Core/Extensions/Internal/BuildHelper.cs @@ -4,19 +4,19 @@ namespace CSF { internal static class BuildHelper { - public static HashSet Build(this CommandBuildingConfiguration context) + public static HashSet Build(this FrameworkBuilder context) { var modules = context.BuildModules(); return modules.SelectMany(x => x.Components).ToHashSet(); } - public static IEnumerable BuildModules(this CommandBuildingConfiguration context) + public static IEnumerable BuildModules(this FrameworkBuilder context) { var typeReaders = TypeReader.CreateDefaultReaders().UnionBy(context.TypeReaders, x => x.Type).ToDictionary(x => x.Type, x => x); var rootReader = typeof(TypeReader); - foreach (var assembly in context.RegistrationAssemblies) + foreach (var assembly in context.Assemblies) foreach (var type in assembly.GetTypes()) if (rootReader.IsAssignableFrom(type) && !type.IsAbstract && !type.ContainsGenericParameters) { @@ -30,7 +30,7 @@ public static IEnumerable BuildModules(this CommandBuildingConfiguration } var rootType = typeof(ModuleBase); - foreach (var assembly in context.RegistrationAssemblies) + foreach (var assembly in context.Assemblies) foreach (var type in assembly.GetTypes()) if (rootType.IsAssignableFrom(type) && !type.IsAbstract && !type.ContainsGenericParameters) yield return new Module(type, typeReaders); diff --git a/src/CSF.Core/Manager/Check.cs b/src/CSF.Core/Manager/Check.cs deleted file mode 100644 index 3158d4c..0000000 --- a/src/CSF.Core/Manager/Check.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace CSF -{ - public partial class CommandManager - { - /// - /// Checks all existing preconditions on the specified passed into the method. - /// - /// The command to check all preconditions for. - /// The context containing information about the command input. - /// The services in scope to execute the pipeline in relation to the provided context. - /// An empty if the precondition evaluations succeeded. If any failed, the result of the first failed precondition. - protected virtual FailedResult Check(Command command, ICommandContext context, IServiceProvider services) - { - try - { - command.Check(context, services); - - return new(); - } - catch (PipelineException e) - { - return e.AsResult(); - } - } - } - - internal static class CheckOperations - { - public static void Check(this Command command, ICommandContext context, IServiceProvider services) - { - if (command.HasPreconditions) - Parallel.ForEach(command.Preconditions, x => x.EvalInternal(context, command, services)); - } - } -} diff --git a/src/CSF.Core/Manager/Execute.cs b/src/CSF.Core/Manager/Execute.cs deleted file mode 100644 index 6b77552..0000000 --- a/src/CSF.Core/Manager/Execute.cs +++ /dev/null @@ -1,86 +0,0 @@ -namespace CSF -{ - public partial class CommandManager - { - /// - /// Executes a command based on the input provided through the and options passed into this method. - /// - /// The context containing information about the command input. - /// A range of options to customize command execution flow. - /// An that can be awaited in asynchronous context. - /// - public virtual IResult ExecuteAsync(ICommandContext context, CommandExecutionOptions options = null) - { - options ??= CommandExecutionOptions.Default; - - var services = options.Scope?.ServiceProvider ?? _services; - - return ExecuteAsync(context, services); - } - - /// - /// Runs the command pipeline based on the input provided through the and options passed into this method. - /// - /// The context containing information about the command input. - /// The services in scope to execute the pipeline in relation to the provided context. - /// The result of the command, containing the state of failure or success. - protected virtual IResult ExecuteAsync(ICommandContext context, IServiceProvider services) - { - try - { - var cell = Search(context, services); - - return ExecuteAsync(context, services, cell); - } - catch (PipelineException ex) - { - return ex.AsResult(); - } - catch (Exception ex) - { - return new ExecuteException(ex.Message, ex).AsResult(); - } - } - - /// - /// Executes a resolved commandcell that succeeded through all relevant handles in the search and match pipeline. - /// - /// The context containing information about the command input. - /// The services in scope to execute the pipeline in relation to the provided context. - /// The resolved command that was chosen as the result out of the pipeline. - /// The result of the command, containing the state of failure or success. - /// - /// Thrown when the return type of a command cannot be resolved. - /// Consider overriding this method and adding the custom return type in the resolver. - /// - protected virtual IResult ExecuteAsync(ICommandContext context, IServiceProvider services, CommandCell cell) - { - var value = cell.Execute(context, services); - - switch (value) - { - case Task task: - return new SuccessResult(task); - case null: - return new SuccessResult(); - default: - throw new NotSupportedException("The return value of this command is not supported."); - } - } - } - - internal static class ExecuteOperations - { - public static object Execute(this CommandCell cell, ICommandContext context, IServiceProvider services) - { - var module = services.GetService(cell.Command.Module.Type) as ModuleBase; - - module.Context = context; - module.Command = cell.Command; - - var value = cell.Command.Target.Invoke(module, cell.Arguments); - - return value; - } - } -} diff --git a/src/CSF.Core/Manager/Match.cs b/src/CSF.Core/Manager/Match.cs deleted file mode 100644 index e7d8253..0000000 --- a/src/CSF.Core/Manager/Match.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace CSF -{ - public partial class CommandManager - { - /// - /// Runs through a found commands and attempts to match the amount of parameters to the represented amount in the context. - /// - /// The command - /// The context containing information about the command input. - /// The services in scope to execute the pipeline in relation to the provided context. - /// A resolved match from the provided command, containing failure or result depending on the outcome of the evaluation. - protected virtual CommandCell Match(Command command, ICommandContext context, IServiceProvider services) - => command.Match(context, services); - - /// - /// Runs through a set of found commands and tests them for the availability - /// - /// The range of components to attempt to match. - /// The context containing information about the command input. - /// The services in scope to execute the pipeline in relation to the provided context. - /// All relevant matches to the provided context. - protected virtual CommandCell[] Match(IEnumerable components, ICommandContext context, IServiceProvider services) - => components.Match(context, services) - .ToArray(); - } - - internal static class MatchOperations - { - public static IEnumerable Match(this IEnumerable components, ICommandContext context, IServiceProvider services) - { - foreach (var component in components) - { - if (component is not Command command) - continue; - - var length = context.Parameters.Length; - - if (command.MaxLength == length) - yield return command.Match(context, services); - - if (command.MaxLength <= length && command.HasRemainder) - yield return command.Match(context, services); - - if (command.MaxLength > length && command.MinLength <= length) - yield return command.Match(context, services); - } - } - - public static CommandCell Match(this Command command, ICommandContext context, IServiceProvider services) - { - try - { - command.Check(context, services); - - var arguments = command.HasParameters - ? command.Parameters.Read(context, services) - : Array.Empty(); - - return new(command, arguments); - } - catch (PipelineException ex) - { - return new(ex); - } - } - } -} diff --git a/src/CSF.Core/Manager/Read.cs b/src/CSF.Core/Manager/Read.cs deleted file mode 100644 index 49e541e..0000000 --- a/src/CSF.Core/Manager/Read.cs +++ /dev/null @@ -1,89 +0,0 @@ -namespace CSF -{ - public partial class CommandManager - { - /// - /// Reads the parameters of the current command and returns the parsed input to execute it. - /// - /// The command of which the parameters should be evaluated. - /// The context containing information about the command input. - /// The services in scope to execute the pipeline in relation to the provided context. - /// An array of the values returned by the read parameters within the provided command. - protected virtual object[] Read(Command command, ICommandContext context, IServiceProvider services) - { - if (command.HasParameters) - return command.Parameters.Read(context, services); - return Array.Empty(); - } - - /// - /// Reads the provided range of components and returns the resulting typereader information. - /// - /// The components of which the parameters should be evaluated. - /// The context containing information about the command input. - /// The services in scope to execute the pipeline in relation to the provided context. - /// An array of the values returned by the read parameters within the provided range. - protected virtual object[] Read(IParameterComponent[] components, ICommandContext context, IServiceProvider services) - => components.Read(context, services); - } - - internal static class ReadOperations - { - public static object[] Read(this IParameterComponent[] parameters, ICommandContext context, IServiceProvider services, int index = 0) - { - var args = new object[parameters.Length]; - - for (int i = 0; i < parameters.Length; i++) - { - var parameter = parameters[i]; - - if (parameter.IsRemainder) - { - var input = string.Join(" ", context.Parameters.Skip(index)); - if (parameter.Type == typeof(string)) - args[i] = input; - else - args[i] = parameter.Read(context, services, input); - - break; - } - - if (parameter.IsOptional && context.Parameters.Length <= index) - { - args[i] = Type.Missing; - continue; - } - - if (parameter.Type == typeof(string) || parameter.Type == typeof(object)) - { - args[i] = context.Parameters[index]; - index++; - continue; - } - - if (parameter.IsNullable && context.Parameters[index] is "null" or "nothing") - { - args[i] = null; - index++; - continue; - } - - if (parameter is ComplexParameter complex) - { - var result = complex.Parameters.Read(context, services, index); - - index += result.Length; - args[i] = complex.Constructor.Target.Invoke(result); - continue; - } - - args[i] = parameter.Read(context, services, context.Parameters[index]); - index++; - } - return args; - } - - public static object Read(this IParameterComponent parameter, ICommandContext context, IServiceProvider services, string value) - => parameter.TypeReader.EvalInternal(context, parameter, services, value); - } -} diff --git a/src/CSF.Core/Manager/Search.cs b/src/CSF.Core/Manager/Search.cs deleted file mode 100644 index 1a39bbc..0000000 --- a/src/CSF.Core/Manager/Search.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace CSF -{ - public partial class CommandManager - { - /// - /// Searches all available components for the most relevant match. - /// - /// The context containing information about the command input. - /// The services in scope to execute the pipeline in relation to the provided context. - /// A command cell containing the best possible result for the provided input. - /// Thrown when no command was found accepting the provided input. - protected virtual CommandCell Search(ICommandContext context, IServiceProvider services) - { - var commands = Components.Search(context, services); - - if (commands.Length == 0) - throw new SearchException("Failed to find any commands that accept the provided input."); - - CommandCell? result = null; - - foreach (var command in commands) - if (!command.IsInvalid) - { - if (!result.HasValue) - result = command; - - if (command.Command?.Priority > result.Value.Command?.Priority) - result = command; - } - - if (!result.HasValue) - throw commands[0].Exception; - - return result.Value; - } - - /// - /// Searches a range of modules for the most relevant match. - /// - /// The components to search. - /// The context containing information about the command input. - /// The services in scope to execute the pipeline in relation to the provided context. - /// An array of command cells that best match the provided input. - protected virtual CommandCell[] Search(IEnumerable components, ICommandContext context, IServiceProvider services) - => components.Search(context, services); - } - - internal static class SearchOperations - { - public static CommandCell[] Search(this IEnumerable components, ICommandContext context, IServiceProvider services) - { - var matches = components.Where(command => command.Aliases.Any(x => x == context.Name)); - - var cells = matches.Match(context, services).ToArray(); - - if (cells.All(x => x.IsInvalid)) - { - var module = matches.SelectFirstOrDefault(); - - if (module is null) - return cells; - - context.Name = context.Parameters[0]; - context.Parameters = context.Parameters[1..]; - - return module.Components.Search(context, services); - } - - return cells; - } - } -} diff --git a/src/CSF.Core/Entities/Parsing/Implementation/TextParser.cs b/src/CSF.Core/Parsing/Implementation/TextParser.cs similarity index 100% rename from src/CSF.Core/Entities/Parsing/Implementation/TextParser.cs rename to src/CSF.Core/Parsing/Implementation/TextParser.cs diff --git a/src/CSF.Core/Entities/Parsing/Parser.cs b/src/CSF.Core/Parsing/Parser.cs similarity index 100% rename from src/CSF.Core/Entities/Parsing/Parser.cs rename to src/CSF.Core/Parsing/Parser.cs diff --git a/src/CSF.Core/Entities/Parsing/ParserCell.cs b/src/CSF.Core/Parsing/ParserCell.cs similarity index 100% rename from src/CSF.Core/Entities/Parsing/ParserCell.cs rename to src/CSF.Core/Parsing/ParserCell.cs diff --git a/src/CSF.Core/Results/CheckResult.cs b/src/CSF.Core/Results/CheckResult.cs new file mode 100644 index 0000000..e1fe927 --- /dev/null +++ b/src/CSF.Core/Results/CheckResult.cs @@ -0,0 +1,15 @@ +namespace CSF +{ + public readonly struct CheckResult : IResult + { + public Exception Exception { get; } = null; + + public bool Success { get; } = true; + + internal CheckResult(Exception exception) + { + Exception = exception; + Success = false; + } + } +} diff --git a/src/CSF.Core/Results/MatchResult.cs b/src/CSF.Core/Results/MatchResult.cs new file mode 100644 index 0000000..83bf250 --- /dev/null +++ b/src/CSF.Core/Results/MatchResult.cs @@ -0,0 +1,29 @@ +namespace CSF +{ + public readonly struct MatchResult : IResult + { + public Exception Exception { get; } = null; + + public Command Command { get; } + + public object[] Reads { get; } + + public bool Success { get; } + + internal MatchResult(Command command, object[] reads) + { + Command = command; + Reads = reads; + Success = true; + } + + internal MatchResult(Command command, Exception exception) + { + Command = command; + Reads = null; + Success = false; + + Exception = exception; + } + } +} diff --git a/src/CSF.Core/Results/ReadResult.cs b/src/CSF.Core/Results/ReadResult.cs new file mode 100644 index 0000000..263841d --- /dev/null +++ b/src/CSF.Core/Results/ReadResult.cs @@ -0,0 +1,24 @@ +namespace CSF +{ + public readonly struct ReadResult : IResult + { + public Exception Exception { get; } = null; + + public object Value { get; } + + public bool Success { get; } + + internal ReadResult(object value) + { + Value = value; + Success = true; + } + + internal ReadResult(Exception exception) + { + Exception = exception; + Value = null; + Success = false; + } + } +} diff --git a/src/CSF.Core/Results/RunResult.cs b/src/CSF.Core/Results/RunResult.cs new file mode 100644 index 0000000..688e9a4 --- /dev/null +++ b/src/CSF.Core/Results/RunResult.cs @@ -0,0 +1,27 @@ +namespace CSF +{ + public readonly struct RunResult : IResult + { + public Exception Exception { get; } = null; + + public object ReturnType { get; } = null; + + public Command Command { get; } + + public bool Success { get; } + + internal RunResult(Command command, Exception exception) + { + Exception = exception; + Command = command; + Success = false; + } + + internal RunResult(Command command, object returnValue) + { + ReturnType = returnValue; + Command = command; + Success = true; + } + } +} diff --git a/src/CSF.Core/Results/SearchResult.cs b/src/CSF.Core/Results/SearchResult.cs new file mode 100644 index 0000000..6bf7f47 --- /dev/null +++ b/src/CSF.Core/Results/SearchResult.cs @@ -0,0 +1,24 @@ +namespace CSF +{ + public readonly struct SearchResult : IResult + { + public Exception Exception { get; } = null; + + public Command Command { get; } + + internal int SearchHeight { get; } + + internal SearchResult(Command command, int srcHeight) + { + Command = command; + SearchHeight = srcHeight; + } + + internal SearchResult(Exception exception) + { + Exception = exception; + Command = null; + SearchHeight = 0; + } + } +} diff --git a/src/CSF.Core/Entities/TypeReaders/Implementation/BaseTypeReader.cs b/src/CSF.Core/TypeReaders/BaseTypeReader.cs similarity index 100% rename from src/CSF.Core/Entities/TypeReaders/Implementation/BaseTypeReader.cs rename to src/CSF.Core/TypeReaders/BaseTypeReader.cs diff --git a/src/CSF.Core/Entities/TypeReaders/Implementation/ColorTypeReader.cs b/src/CSF.Core/TypeReaders/ColorTypeReader.cs similarity index 100% rename from src/CSF.Core/Entities/TypeReaders/Implementation/ColorTypeReader.cs rename to src/CSF.Core/TypeReaders/ColorTypeReader.cs diff --git a/src/CSF.Core/Entities/TypeReaders/Implementation/EnumTypeReader.cs b/src/CSF.Core/TypeReaders/EnumTypeReader.cs similarity index 100% rename from src/CSF.Core/Entities/TypeReaders/Implementation/EnumTypeReader.cs rename to src/CSF.Core/TypeReaders/EnumTypeReader.cs diff --git a/src/CSF.Core/Entities/TypeReaders/Implementation/TimeSpanTypeReader.cs b/src/CSF.Core/TypeReaders/TimeSpanTypeReader.cs similarity index 100% rename from src/CSF.Core/Entities/TypeReaders/Implementation/TimeSpanTypeReader.cs rename to src/CSF.Core/TypeReaders/TimeSpanTypeReader.cs diff --git a/src/CSF.Hosting/CSF.Hosting.csproj b/src/CSF.Hosting/CSF.Hosting.csproj index 40ee9ff..09baf1e 100644 --- a/src/CSF.Hosting/CSF.Hosting.csproj +++ b/src/CSF.Hosting/CSF.Hosting.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable 2.1 diff --git a/src/CSF.Hosting/HostedCommandManager.cs b/src/CSF.Hosting/HostedCommandManager.cs index af7723c..cb565c1 100644 --- a/src/CSF.Hosting/HostedCommandManager.cs +++ b/src/CSF.Hosting/HostedCommandManager.cs @@ -51,7 +51,7 @@ public virtual async Task RunAsync(CancellationToken cancellationToken) { var context = new CommandContext(Console.ReadLine(), Parser); - var result = ExecuteAsync(context, new CommandExecutionOptions()); + var result = ExecuteAsync(context, new ExecutionOptions()); if (result.Failed(out var failure)) Logger.LogError(failure.Exception, "Command execution returned an exception."); diff --git a/src/CSF.Tests.Benchmarks/CSF.Tests.Benchmarks.csproj b/src/CSF.Tests.Benchmarks/CSF.Tests.Benchmarks.csproj index 1c39a4f..3c2ed5e 100644 --- a/src/CSF.Tests.Benchmarks/CSF.Tests.Benchmarks.csproj +++ b/src/CSF.Tests.Benchmarks/CSF.Tests.Benchmarks.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0 enable enable true From 88449a1991a5cc19abe889b9dbd16573254f41ed Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Mon, 22 Jan 2024 13:04:11 +0100 Subject: [PATCH 02/40] Update package inclusions & versions --- src/CSF.Core/CommandManager.cs | 4 ++-- src/CSF.Hosting/CSF.Hosting.csproj | 4 ++-- src/CSF.Tests.Console/CSF.Tests.Console.csproj | 2 +- src/CSF.Tests.Hosting/CSF.Tests.Hosting.csproj | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/CSF.Core/CommandManager.cs b/src/CSF.Core/CommandManager.cs index e10ecf5..2e83f6b 100644 --- a/src/CSF.Core/CommandManager.cs +++ b/src/CSF.Core/CommandManager.cs @@ -36,7 +36,7 @@ public async Task ExecuteAsync(ICommandContext context, object[] args) } if (!fallback.HasValue) - return new SearchResult(new SearchException("")); + return new SearchResult(new SearchException("")); // TODO return fallback; } @@ -54,7 +54,7 @@ public static async Task MatchAsync(ICommandContext context, Search // verify check success, if not, return the failure. if (!check.Success) - return new(search.Command, new MatchException("", check.Exception)); + return new(search.Command, new MatchException("", check.Exception)); // TODO // read the command parameters in right order. var readResult = await ReadAsync(context, search, args); diff --git a/src/CSF.Hosting/CSF.Hosting.csproj b/src/CSF.Hosting/CSF.Hosting.csproj index 09baf1e..6ceddc3 100644 --- a/src/CSF.Hosting/CSF.Hosting.csproj +++ b/src/CSF.Hosting/CSF.Hosting.csproj @@ -45,8 +45,8 @@ - - + + diff --git a/src/CSF.Tests.Console/CSF.Tests.Console.csproj b/src/CSF.Tests.Console/CSF.Tests.Console.csproj index 537db1d..37030f7 100644 --- a/src/CSF.Tests.Console/CSF.Tests.Console.csproj +++ b/src/CSF.Tests.Console/CSF.Tests.Console.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0 enable enable diff --git a/src/CSF.Tests.Hosting/CSF.Tests.Hosting.csproj b/src/CSF.Tests.Hosting/CSF.Tests.Hosting.csproj index e4187b0..e3f1fc0 100644 --- a/src/CSF.Tests.Hosting/CSF.Tests.Hosting.csproj +++ b/src/CSF.Tests.Hosting/CSF.Tests.Hosting.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 enable enable From 5a2cb047b82d0aa11b9f1b7d532a28e4e96f6317 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Wed, 24 Jan 2024 21:47:00 +0100 Subject: [PATCH 03/40] Logic implementations, refactors, finalize design --- .../CSF.Samples.Hosting.csproj | 2 +- .../Abstractions/ExecutionException.cs | 2 - src/CSF.Core/Abstractions/ModuleBase.cs | 11 ++- .../Abstractions/PreconditionAttribute.cs | 66 +++++-------- src/CSF.Core/Abstractions/TypeReader.cs | 52 ++++++++-- src/CSF.Core/Attributes/CommandAttribute.cs | 28 +++--- .../Attributes/DescriptionAttribute.cs | 9 +- .../Attributes/DontRegisterAttribute.cs | 2 +- src/CSF.Core/Attributes/GroupAttribute.cs | 26 +++-- src/CSF.Core/Attributes/PriorityAttribute.cs | 23 ++--- .../Attributes/RequireContextAttribute.cs | 22 ++--- src/CSF.Core/CMBuilder.cs | 87 +++++++++++++++++ src/CSF.Core/CommandBuildingConfiguration.cs | 46 --------- src/CSF.Core/CommandManager.cs | 54 +++++++++-- src/CSF.Core/CommandManagerHelper.cs | 2 +- src/CSF.Core/Components/Command.cs | 9 +- src/CSF.Core/Components/ComplexParameter.cs | 7 +- src/CSF.Core/Components/Module.cs | 2 +- src/CSF.Core/Components/Parameter.cs | 5 +- src/CSF.Core/Exceptions/CheckException.cs | 5 - src/CSF.Core/Exceptions/CommandException.cs | 5 - src/CSF.Core/Exceptions/MatchException.cs | 5 - src/CSF.Core/Exceptions/ReadException.cs | 5 - src/CSF.Core/Exceptions/SearchException.cs | 5 - src/CSF.Core/Extensions/Internal/Assert.cs | 37 -------- src/CSF.Core/Extensions/ServiceHelper.cs | 72 -------------- .../Internal/LinqHelpers.cs} | 2 +- .../Internal/ReflectionHelpers.cs} | 94 +++++++++---------- src/CSF.Core/Helpers/Internal/ThrowHelpers.cs | 21 +++++ src/CSF.Core/Helpers/ServiceHelper.cs | 51 ++++++++++ src/CSF.Core/TypeReaders/BaseTypeReader.cs | 6 +- src/CSF.Core/TypeReaders/ColorTypeReader.cs | 8 +- src/CSF.Core/TypeReaders/EnumTypeReader.cs | 24 ++--- .../TypeReaders/TimeSpanTypeReader.cs | 6 +- src/CSF.Hosting/CSF.Hosting.csproj | 2 +- .../CSF.Tests.Hosting.csproj | 2 +- 36 files changed, 409 insertions(+), 396 deletions(-) create mode 100644 src/CSF.Core/CMBuilder.cs delete mode 100644 src/CSF.Core/CommandBuildingConfiguration.cs delete mode 100644 src/CSF.Core/Extensions/Internal/Assert.cs delete mode 100644 src/CSF.Core/Extensions/ServiceHelper.cs rename src/CSF.Core/{Extensions/Internal/LinqHelper.cs => Helpers/Internal/LinqHelpers.cs} (97%) rename src/CSF.Core/{Extensions/Internal/BuildHelper.cs => Helpers/Internal/ReflectionHelpers.cs} (51%) create mode 100644 src/CSF.Core/Helpers/Internal/ThrowHelpers.cs create mode 100644 src/CSF.Core/Helpers/ServiceHelper.cs diff --git a/examples/CSF.Samples.Hosting/CSF.Samples.Hosting.csproj b/examples/CSF.Samples.Hosting/CSF.Samples.Hosting.csproj index fa2c405..fb80b99 100644 --- a/examples/CSF.Samples.Hosting/CSF.Samples.Hosting.csproj +++ b/examples/CSF.Samples.Hosting/CSF.Samples.Hosting.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/CSF.Core/Abstractions/ExecutionException.cs b/src/CSF.Core/Abstractions/ExecutionException.cs index 461453f..05bffdc 100644 --- a/src/CSF.Core/Abstractions/ExecutionException.cs +++ b/src/CSF.Core/Abstractions/ExecutionException.cs @@ -13,7 +13,5 @@ public ExecutionException(string message, Exception innerException = null) { } - - public abstract FailedResult AsResult(); } } diff --git a/src/CSF.Core/Abstractions/ModuleBase.cs b/src/CSF.Core/Abstractions/ModuleBase.cs index 9da3e48..35066c7 100644 --- a/src/CSF.Core/Abstractions/ModuleBase.cs +++ b/src/CSF.Core/Abstractions/ModuleBase.cs @@ -29,6 +29,15 @@ public abstract class ModuleBase /// public ICommandContext Context { get; internal set; } + /// + /// Gets the command execution services as provided by the command scope. + /// + public IServiceProvider Services + { + get + => Context.Options.Scope.ServiceProvider; + } + /// /// Gets the component that displays all information about the command thats currently in scope. /// @@ -41,7 +50,7 @@ public abstract class ModuleBase public virtual void Respond(string message) => Console.WriteLine(message); - public virtual CommandManager.RunResult ReturnTypeHandle(object value) + public virtual RunResult ReturnTypeHandle(object value) { switch (value) { diff --git a/src/CSF.Core/Abstractions/PreconditionAttribute.cs b/src/CSF.Core/Abstractions/PreconditionAttribute.cs index 34b509d..db05341 100644 --- a/src/CSF.Core/Abstractions/PreconditionAttribute.cs +++ b/src/CSF.Core/Abstractions/PreconditionAttribute.cs @@ -1,4 +1,6 @@ -namespace CSF +using System.Diagnostics.CodeAnalysis; + +namespace CSF { /// /// Defines a precondition attribute. @@ -6,6 +8,7 @@ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] public abstract class PreconditionAttribute : Attribute { + private static readonly string _exHeader = "Precondition result halted further command execution. View inner exception for more details."; /// /// Evaluates a condition to handle a command and returns the result. /// @@ -13,54 +16,31 @@ public abstract class PreconditionAttribute : Attribute /// The command that is to be executed if this and all other precondition evaluations succeed. /// The services in scope for the current command execution. /// A result that represents the outcome of the evaluation. - public abstract async Task EvaluateAsync(ICommandContext context, Command command); + public abstract ValueTask EvaluateAsync(ICommandContext context, Command command); - /// - /// Returns that the evaluation has failed. - /// - /// The reason why the evaluation failed. - protected static Result Failure(string reason) - => new(reason); - - /// - /// Returns that the evaluation has succeeded. - /// - protected static Result Success() - => new(); - - /// - /// Represents the result structure that displays the returned state of the evaluation. - /// - public readonly struct Result + public static CheckResult Error([DisallowNull] Exception exception) { - /// - /// Gets if the evaluation was successful or not. - /// - public bool IsSuccess { get; } + if (exception == null) + ThrowHelpers.InvalidArg(exception); - /// - /// Gets the reason of failure in case it has. - /// - public string Reason { get; } - - /// - /// Creates a new successful evaluation result. - /// - public Result() + if (exception is CheckException checkEx) { - IsSuccess = true; - Reason = null; + return new(checkEx); } + return new(new CheckException(_exHeader, exception)); + } - /// - /// Creates a new failed evaluation result. - /// - /// The reason why the evaluation has failed. - public Result(string reason) - { - IsSuccess = false; - Reason = reason; - } + public virtual CheckResult Error([DisallowNull] string error) + { + if (string.IsNullOrEmpty(error)) + ThrowHelpers.InvalidArg(error); + + return new(new CheckException(error)); + } + + public virtual CheckResult Success() + { + return new(); } } } diff --git a/src/CSF.Core/Abstractions/TypeReader.cs b/src/CSF.Core/Abstractions/TypeReader.cs index a6e206c..8811e0d 100644 --- a/src/CSF.Core/Abstractions/TypeReader.cs +++ b/src/CSF.Core/Abstractions/TypeReader.cs @@ -1,4 +1,6 @@ -namespace CSF +using System.Diagnostics.CodeAnalysis; + +namespace CSF { /// /// Represents a generic to use for parsing provided types into the targetted type. @@ -10,11 +12,13 @@ public abstract class TypeReader : TypeReader public override Type Type { get; } = typeof(T); /// - public override abstract Result Evaluate(ICommandContext context, IParameterComponent parameter, IServiceProvider services, string value); + public override abstract ValueTask EvaluateAsync(ICommandContext context, IParameterComponent parameter, string value); } public abstract class TypeReader { + private static readonly string _exHeader = "TypeReader failed to parse provided value as '{0}'. View inner exception for more details."; + /// /// The type that this reader intends to return. /// @@ -28,13 +32,45 @@ public abstract class TypeReader /// The services in scope for the current command execution. /// The input that this evaluation intends to convert into the expected parameter type. /// A result that represents the outcome of the evaluation. - public abstract ValueTask EvaluateAsync(ICommandContext context, IParameterComponent parameter, object value); + public abstract ValueTask EvaluateAsync(ICommandContext context, IParameterComponent parameter, string value); - /// - /// Gets a range of default s. - /// - /// A range of s that are defined in the library by default. - public static TypeReader[] CreateDefaultReaders() + internal ValueTask ObjectEvaluateAsync(ICommandContext context, IParameterComponent parameter, object value) + { + if (value.GetType() == Type) + return ValueTask.FromResult(new ReadResult(value)); + + if (value is string str) + return EvaluateAsync(context, parameter, str); + + return EvaluateAsync(context, parameter, value.ToString()); + } + + public virtual ReadResult Error([DisallowNull] Exception exception) + { + if (exception == null) + ThrowHelpers.InvalidArg(exception); + + if (exception is ReadException readEx) + { + return new(readEx); + } + return new(new ReadException(string.Format(_exHeader, Type.Name), exception)); + } + + public virtual ReadResult Error([DisallowNull] string error) + { + if (string.IsNullOrEmpty(error)) + ThrowHelpers.InvalidArg(error); + + return new(new ReadException(error)); + } + + public virtual ReadResult Success(object value) + { + return new(value); + } + + internal static TypeReader[] CreateDefaultReaders() { var range = BaseTypeReader.CreateBaseReaders(); diff --git a/src/CSF.Core/Attributes/CommandAttribute.cs b/src/CSF.Core/Attributes/CommandAttribute.cs index 2e7962b..0764f11 100644 --- a/src/CSF.Core/Attributes/CommandAttribute.cs +++ b/src/CSF.Core/Attributes/CommandAttribute.cs @@ -1,4 +1,6 @@ -namespace CSF +using System.Diagnostics.CodeAnalysis; + +namespace CSF { /// /// An attribute that represents the required info to map a command. @@ -20,8 +22,8 @@ public sealed class CommandAttribute : Attribute /// Sets up a new command attribute with the provided name. /// /// - public CommandAttribute(string name) - : this(name, Array.Empty()) + public CommandAttribute([DisallowNull] string name) + : this(name, []) { } @@ -31,25 +33,19 @@ public CommandAttribute(string name) /// /// [CLSCompliant(false)] - public CommandAttribute(string name, params string[] aliases) + public CommandAttribute([DisallowNull] string name, params string[] aliases) { - static void Assign(ref string[] arr, string value, int pos) - { - Assert.IsNotEmpty(value); - arr[pos] = value; - pos++; - } + if (string.IsNullOrWhiteSpace(name)) + ThrowHelpers.InvalidArg(name); - var i = 0; - var array = new string[aliases.Length + 1]; + var arr = new string[aliases.Length + 1]; - Assign(ref array, name, i); + arr[0] = name; - foreach (var value in aliases) - Assign(ref array, value, i); + Array.Copy(aliases, 0, arr, 1, aliases.Length); Name = name; - Aliases = array; + Aliases = arr; } } } diff --git a/src/CSF.Core/Attributes/DescriptionAttribute.cs b/src/CSF.Core/Attributes/DescriptionAttribute.cs index bf3b280..0def0ec 100644 --- a/src/CSF.Core/Attributes/DescriptionAttribute.cs +++ b/src/CSF.Core/Attributes/DescriptionAttribute.cs @@ -1,4 +1,6 @@ -namespace CSF +using System.Diagnostics.CodeAnalysis; + +namespace CSF { /// /// Represents the description of a command. @@ -15,9 +17,10 @@ public sealed class DescriptionAttribute : Attribute /// Sets up a new with provided value. /// /// - public DescriptionAttribute(string description) + public DescriptionAttribute([DisallowNull] string description) { - Assert.IsNotEmpty(description); + if (string.IsNullOrWhiteSpace(description)) + ThrowHelpers.InvalidArg(description); Description = description; } diff --git a/src/CSF.Core/Attributes/DontRegisterAttribute.cs b/src/CSF.Core/Attributes/DontRegisterAttribute.cs index ef1f903..a2f8f42 100644 --- a/src/CSF.Core/Attributes/DontRegisterAttribute.cs +++ b/src/CSF.Core/Attributes/DontRegisterAttribute.cs @@ -3,7 +3,7 @@ /// /// Represents an attribute that forces the registration to not register provided member. /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] public sealed class DontRegisterAttribute : Attribute { diff --git a/src/CSF.Core/Attributes/GroupAttribute.cs b/src/CSF.Core/Attributes/GroupAttribute.cs index 0ce4dc7..525e4b2 100644 --- a/src/CSF.Core/Attributes/GroupAttribute.cs +++ b/src/CSF.Core/Attributes/GroupAttribute.cs @@ -1,4 +1,6 @@ -namespace CSF +using System.Diagnostics.CodeAnalysis; + +namespace CSF { /// /// Represents a command group, functioning much like subcommands. @@ -21,7 +23,7 @@ public sealed class GroupAttribute : Attribute /// /// public GroupAttribute(string name) - : this(name, Array.Empty()) + : this(name, []) { } @@ -32,25 +34,19 @@ public GroupAttribute(string name) /// /// [CLSCompliant(false)] - public GroupAttribute(string name, params string[] aliases) + public GroupAttribute([DisallowNull] string name, params string[] aliases) { - static void Assign(ref string[] arr, string value, int pos) - { - Assert.IsNotEmpty(value); - arr[pos] = value; - pos++; - } + if (string.IsNullOrWhiteSpace(name)) + ThrowHelpers.InvalidArg(name); - var i = 0; - var array = new string[aliases.Length + 1]; + var arr = new string[aliases.Length + 1]; - Assign(ref array, name, i); + arr[0] = name; - foreach (var value in aliases) - Assign(ref array, value, i); + Array.Copy(aliases, 0, arr, 1, aliases.Length); Name = name; - Aliases = array; + Aliases = arr; } } } diff --git a/src/CSF.Core/Attributes/PriorityAttribute.cs b/src/CSF.Core/Attributes/PriorityAttribute.cs index bdc6591..210853c 100644 --- a/src/CSF.Core/Attributes/PriorityAttribute.cs +++ b/src/CSF.Core/Attributes/PriorityAttribute.cs @@ -1,4 +1,6 @@ -namespace CSF +using System.Diagnostics.CodeAnalysis; + +namespace CSF { /// /// Represents an attribute that can prioritize one result over another when multiple matches were found. @@ -6,23 +8,16 @@ /// /// By default, a command has a priority of 0. Higher values take priority, meaning a command with a priority of 1 will execute first if other commands have a priority of 0. /// + /// + /// Creates a new with provided priority. + /// + /// The priority of this command, which can be between 0 and 255. [AttributeUsage(AttributeTargets.Method)] - public sealed class PriorityAttribute : Attribute + public sealed class PriorityAttribute([DisallowNull] byte priority) : Attribute { /// /// Gets the priority of a command, where higher values take priority over lower ones. /// - public byte Priority { get; } - - /// - /// Creates a new with provided priority. - /// - /// The priority of this command, which can be between 0 and 255. - public PriorityAttribute(byte priority) - { - Assert.IsNotNull(priority); - - Priority = priority; - } + public byte Priority { get; } = priority; } } diff --git a/src/CSF.Core/Attributes/RequireContextAttribute.cs b/src/CSF.Core/Attributes/RequireContextAttribute.cs index 92f4fe9..380ae2f 100644 --- a/src/CSF.Core/Attributes/RequireContextAttribute.cs +++ b/src/CSF.Core/Attributes/RequireContextAttribute.cs @@ -3,30 +3,24 @@ /// /// Represents a precondition that checks if the provided context is valid. /// - public sealed class RequireContextAttribute : PreconditionAttribute + /// + /// Compiles a new from provided . + /// + public sealed class RequireContextAttribute() : PreconditionAttribute { /// /// The context type to compare against. /// - public Type ContextType { get; } + public Type ContextType { get; } = typeof(T); - /// - /// Compiles a new from provided . - /// - /// - public RequireContextAttribute(Type contextType) - { - ContextType = contextType; - } - - public override Result EvaluateAsync(ICommandContext context, Command command, IServiceProvider provider) + public override ValueTask EvaluateAsync(ICommandContext context, Command command) { var providedType = context.GetType(); if (providedType != ContextType) - return Failure($"Invalid context was passed into the command. Expected: '{ContextType.FullName}', got '{providedType.FullName}'"); + return ValueTask.FromResult(new CheckResult(new CheckException($"Invalid context was passed into the command. Expected: '{ContextType.FullName}', got '{providedType.FullName}'"))); - return Success(); + return ValueTask.FromResult(new CheckResult()); } } } diff --git a/src/CSF.Core/CMBuilder.cs b/src/CSF.Core/CMBuilder.cs new file mode 100644 index 0000000..625e07b --- /dev/null +++ b/src/CSF.Core/CMBuilder.cs @@ -0,0 +1,87 @@ +using System.Reflection; + +namespace CSF +{ + public sealed class CMBuilder + { + private bool _disposed = false; + + public HashSet Assemblies { get; set; } = [ Assembly.GetEntryAssembly() ]; + + public HashSet TypeReaders { get; set; } = []; + + public CMBuilder AddEntryAssembly() + { + if (_disposed) + ThrowHelpers.InvalidOp("This builder cannot be reused."); + + Assemblies.Add(Assembly.GetEntryAssembly()); + return this; + } + + public CMBuilder AddAssembly(Assembly assembly) + { + if (_disposed) + ThrowHelpers.InvalidOp("This builder cannot be reused."); + + Assemblies.Add(assembly); + return this; + } + + public CMBuilder AddTypeReader(TypeReader typeReader) + { + if (_disposed) + ThrowHelpers.InvalidOp("This builder cannot be reused."); + + TypeReaders.Add(typeReader); + return this; + } + + public CommandManager Build() + { + var typeReaders = TypeReader.CreateDefaultReaders().UnionBy(TypeReaders, x => x.Type).ToDictionary(x => x.Type, x => x); + + if (Assemblies.Count == 0) + ThrowHelpers.InvalidOp("An assembly has to be present in the builder prior to building the CommandManager."); + + IEnumerable BuildComponents() + { + var rootReader = typeof(TypeReader); + foreach (var assembly in Assemblies) + { + foreach (var type in assembly.GetTypes()) + { + if (rootReader.IsAssignableFrom(type) + && !type.IsAbstract + && !type.ContainsGenericParameters) + { + var reader = Activator.CreateInstance(type) as TypeReader; + + // replace existing typereader with replacement handler + if (!typeReaders.TryAdd(reader.Type, reader)) + typeReaders[reader.Type] = reader; + } + } + } + + var rootType = typeof(ModuleBase); + foreach (var assembly in Assemblies) + { + foreach (var type in assembly.GetTypes()) + { + if (rootType.IsAssignableFrom(type) + && !type.IsAbstract + && !type.ContainsGenericParameters) + { + yield return new Module(type, typeReaders); + } + } + } + } + + _disposed = true; + + return new(BuildComponents().SelectMany(x => x.Components), [.. Assemblies]); + } + } +} diff --git a/src/CSF.Core/CommandBuildingConfiguration.cs b/src/CSF.Core/CommandBuildingConfiguration.cs deleted file mode 100644 index d9ca317..0000000 --- a/src/CSF.Core/CommandBuildingConfiguration.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Reflection; - -namespace CSF -{ - public sealed class FrameworkBuilder - { - public List Assemblies { get; set; } = [ Assembly.GetEntryAssembly() ]; - - public List TypeReaders { get; set; } = []; - - public FrameworkBuilder AddAssembly() - { - Assemblies.Add(Assembly.GetEntryAssembly()); - return this; - } - - public FrameworkBuilder AddAssembly(Assembly assembly) - { - Assemblies.Add(assembly); - return this; - } - - public FrameworkBuilder WithAssemblies(params Assembly[] assemblies) - { - Assemblies.AddRange(assemblies); - return this; - } - - public FrameworkBuilder AddTypeReader(TypeReader typeReader) - { - TypeReaders.Add(typeReader); - return this; - } - - public FrameworkBuilder WithTypeReaders(params TypeReader[] typeReaders) - { - TypeReaders.AddRange(typeReaders); - return this; - } - - public CommandManager Build() - { - - } - } -} diff --git a/src/CSF.Core/CommandManager.cs b/src/CSF.Core/CommandManager.cs index 2e83f6b..0e46f04 100644 --- a/src/CSF.Core/CommandManager.cs +++ b/src/CSF.Core/CommandManager.cs @@ -1,4 +1,6 @@ -[assembly: CLSCompliant(true)] +using System.Reflection; + +[assembly: CLSCompliant(true)] namespace CSF { @@ -8,14 +10,31 @@ namespace CSF /// /// Guides and documentation can be found at: /// - public class CommandManager + public sealed class CommandManager { /// /// Gets the components registered to this manager. /// public HashSet Components { get; } - public async Task ExecuteAsync(ICommandContext context, object[] args) + /// + /// Gets the assemblies used to register to this manager. + /// + public Assembly[] Assemblies { get; } + + public CommandManager(IEnumerable components, Assembly[] assemblies) + { + Components = components.ToHashSet(); + Assemblies = assemblies; + } + + /// + /// + /// + /// + /// + /// + public async ValueTask ExecuteAsync(ICommandContext context, params object[] args) { // search all relevant commands. var searches = Search(args); @@ -41,13 +60,19 @@ public async Task ExecuteAsync(ICommandContext context, object[] args) return fallback; } + /// + /// + /// + /// + /// public IEnumerable Search(object[] args) { // recursively search for commands in the execution. return Components.RecursiveSearch(args, 0); } - public static async Task MatchAsync(ICommandContext context, SearchResult search, object[] args) + #region Matching + internal static async ValueTask MatchAsync(ICommandContext context, SearchResult search, object[] args) { // check command preconditions. var check = await CheckAsync(context, search.Command); @@ -73,8 +98,10 @@ public static async Task MatchAsync(ICommandContext context, Search // return successful match if execution reaches here. return new(search.Command, reads); } + #endregion - public static async Task ReadAsync(ICommandContext context, SearchResult search, object[] args) + #region Reading + internal static async ValueTask ReadAsync(ICommandContext context, SearchResult search, object[] args) { // skip if no parameters exist. if (!search.Command.HasParameters) @@ -98,18 +125,24 @@ public static async Task ReadAsync(ICommandContext context, Search // input is too long or too short. return []; } - - public static async Task CheckAsync(ICommandContext context, Command command) + #endregion + + #region Checking + internal static async ValueTask CheckAsync(ICommandContext context, Command command) { foreach (var precon in command.Preconditions) { - if (!await precon.EvaluateAsync(context, command)) - return new(new CheckException("")); + var result = await precon.EvaluateAsync(context, command); + + if (!result.Success) + return result; } return new(); } + #endregion - public static async Task RunAsync(ICommandContext context, MatchResult match) + #region Running + internal static async ValueTask RunAsync(ICommandContext context, MatchResult match) { try { @@ -131,5 +164,6 @@ public static async Task RunAsync(ICommandContext context, MatchResul return new(match.Command, exception); } } + #endregion } } diff --git a/src/CSF.Core/CommandManagerHelper.cs b/src/CSF.Core/CommandManagerHelper.cs index 3321442..250a93b 100644 --- a/src/CSF.Core/CommandManagerHelper.cs +++ b/src/CSF.Core/CommandManagerHelper.cs @@ -38,7 +38,7 @@ static async ValueTask ReadAsync(IParameterComponent param, ICommand if (param.IsNullable && arg is null or "null" or "nothing") return new(arg); - return await param.TypeReader.EvaluateAsync(context, param, arg); + return await param.TypeReader.ObjectEvaluateAsync(context, param, arg); } var results = new ReadResult[param.Length]; diff --git a/src/CSF.Core/Components/Command.cs b/src/CSF.Core/Components/Command.cs index 94bce24..2f570b1 100644 --- a/src/CSF.Core/Components/Command.cs +++ b/src/CSF.Core/Components/Command.cs @@ -57,12 +57,17 @@ internal Command(Module module, MethodInfo method, string[] aliases, IDictionary { var attributes = method.GetAttributes(true); var preconditions = attributes.GetPreconditions(); - var parameters = method.BuildParameters(typeReaders); + var parameters = method.GetParameters(typeReaders); var (minLength, maxLength) = parameters.GetLength(); if (parameters.Any(x => x.Attributes.Contains(false))) - Assert.IsTrue(parameters[^1].IsRemainder, $"{nameof(RemainderAttribute)} can only exist on the last parameter of a method."); + { + if (parameters[^1].IsRemainder) + { + ThrowHelpers.InvalidOp($"{nameof(RemainderAttribute)} can only exist on the last parameter of a method."); + } + } Priority = attributes.SelectFirstOrDefault()?.Priority ?? 0; diff --git a/src/CSF.Core/Components/ComplexParameter.cs b/src/CSF.Core/Components/ComplexParameter.cs index 3a3d40a..a1051ab 100644 --- a/src/CSF.Core/Components/ComplexParameter.cs +++ b/src/CSF.Core/Components/ComplexParameter.cs @@ -70,9 +70,12 @@ internal ComplexParameter(ParameterInfo parameterInfo, IDictionary 0, "Complex types are expected to have at least 1 constructor parameter."); + if (parameters.Length > 0) + { + ThrowHelpers.InvalidOp("Complex types are expected to have at least 1 constructor parameter."); + } var (minLength, maxLength) = parameters.GetLength(); diff --git a/src/CSF.Core/Components/Module.cs b/src/CSF.Core/Components/Module.cs index 59fd6e1..d10f978 100644 --- a/src/CSF.Core/Components/Module.cs +++ b/src/CSF.Core/Components/Module.cs @@ -50,7 +50,7 @@ internal Module(Type type, IDictionary typeReaders, Module roo Preconditions = preconditions; HasPreconditions = preconditions.Length > 0; - Components = this.Build(typeReaders); + Components = this.GetComponents(typeReaders); Name = expectedName ?? type.Name; Aliases = aliases ?? [ Name ]; diff --git a/src/CSF.Core/Components/Parameter.cs b/src/CSF.Core/Components/Parameter.cs index 1520923..d0851d6 100644 --- a/src/CSF.Core/Components/Parameter.cs +++ b/src/CSF.Core/Components/Parameter.cs @@ -57,7 +57,10 @@ internal Parameter(ParameterInfo parameterInfo, IDictionary ty else IsRemainder = false; - if (Type != typeof(string) && Type != typeof(object)) + if (Type.IsEnum) + TypeReader = new EnumTypeReader(Type); + + else if (Type != typeof(string) && Type != typeof(object)) TypeReader = typeReaders[Type]; Attributes = attributes; diff --git a/src/CSF.Core/Exceptions/CheckException.cs b/src/CSF.Core/Exceptions/CheckException.cs index fbed9b3..13e8269 100644 --- a/src/CSF.Core/Exceptions/CheckException.cs +++ b/src/CSF.Core/Exceptions/CheckException.cs @@ -10,10 +10,5 @@ public CheckException(string message, Exception innerException = null) { } - - public override FailedResult AsResult() - { - return new(FailureCode.Check, this); - } } } diff --git a/src/CSF.Core/Exceptions/CommandException.cs b/src/CSF.Core/Exceptions/CommandException.cs index 5657a9f..125404b 100644 --- a/src/CSF.Core/Exceptions/CommandException.cs +++ b/src/CSF.Core/Exceptions/CommandException.cs @@ -10,10 +10,5 @@ public CommandException(string message, Exception innerException = null) { } - - public override FailedResult AsResult() - { - return new(FailureCode.Execute, this); - } } } diff --git a/src/CSF.Core/Exceptions/MatchException.cs b/src/CSF.Core/Exceptions/MatchException.cs index 3bf1414..cf6b1d0 100644 --- a/src/CSF.Core/Exceptions/MatchException.cs +++ b/src/CSF.Core/Exceptions/MatchException.cs @@ -7,10 +7,5 @@ public MatchException(string message, Exception innerException = null) { } - - public override FailedResult AsResult() - { - return new(FailureCode.Execute, this); - } } } diff --git a/src/CSF.Core/Exceptions/ReadException.cs b/src/CSF.Core/Exceptions/ReadException.cs index d6f1802..2406df5 100644 --- a/src/CSF.Core/Exceptions/ReadException.cs +++ b/src/CSF.Core/Exceptions/ReadException.cs @@ -10,10 +10,5 @@ public ReadException(string message, Exception innerException = null) { } - - public override FailedResult AsResult() - { - return new(FailureCode.Read, this); - } } } diff --git a/src/CSF.Core/Exceptions/SearchException.cs b/src/CSF.Core/Exceptions/SearchException.cs index 517040a..ec01158 100644 --- a/src/CSF.Core/Exceptions/SearchException.cs +++ b/src/CSF.Core/Exceptions/SearchException.cs @@ -10,10 +10,5 @@ public SearchException(string message, Exception innerException = null) { } - - public override FailedResult AsResult() - { - return new(FailureCode.Search, this); - } } } diff --git a/src/CSF.Core/Extensions/Internal/Assert.cs b/src/CSF.Core/Extensions/Internal/Assert.cs deleted file mode 100644 index 84c66eb..0000000 --- a/src/CSF.Core/Extensions/Internal/Assert.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace CSF -{ - internal static class Assert - { - public static void IsTrue(bool condition, string failureMessage) - { - if (!condition) - throw new InvalidOperationException(failureMessage); - } - - public static void IsFalse(bool condition, string failureMessage) - { - if (condition) - throw new InvalidOperationException(failureMessage); - } - - public static void IsNotNull(object value, [CallerArgumentExpression("value")] string caller = null) - { - if (value == null) - throw new ArgumentNullException(paramName: caller, "Argument is null."); - } - - public static void IsNotEmpty(string value, [CallerArgumentExpression("value")] string caller = null) - { - if (string.IsNullOrEmpty(value)) - throw new ArgumentNullException(paramName: caller, "Argument is null or empty."); - } - - public static void IsNotWhitespace(string value, [CallerArgumentExpression("value")] string caller = null) - { - if (string.IsNullOrWhiteSpace(value)) - throw new ArgumentNullException(paramName: caller, "Argument is null or whitespace."); - } - } -} diff --git a/src/CSF.Core/Extensions/ServiceHelper.cs b/src/CSF.Core/Extensions/ServiceHelper.cs deleted file mode 100644 index 61e855f..0000000 --- a/src/CSF.Core/Extensions/ServiceHelper.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using System.ComponentModel; - -namespace CSF -{ - public static class ServiceHelper - { - /// - /// Adds a range of command components to the collection based on the passed into this method. - /// - /// - /// The building configuration by which commands are searched and registered. - /// The same for chained calls. - public static IServiceCollection AddComponents(this IServiceCollection collection, CommandBuildingConfiguration configuration) - { - var rootType = typeof(ModuleBase); - - foreach (var assembly in configuration.RegistrationAssemblies) - foreach (var type in assembly.GetTypes()) - if (rootType.IsAssignableFrom(type) && !type.IsAbstract && !type.ContainsGenericParameters) - collection.TryAddTransient(type); - - return collection; - } - - /// - /// Includes a into the this method is called on. - /// - /// - /// The configuration required to set up a new instance of . - /// The same for chained calls. - public static IServiceCollection WithCommandManager(this IServiceCollection collection, Action action = null) - { - collection.WithCommandManager(action); - - return collection; - } - - /// - /// Includes inheriting into the this method is called on. - /// - /// The type inheriting to include in the collection. - /// - /// The configuration required to set up a new instance of . - /// The same for chained calls. - public static IServiceCollection WithCommandManager(this IServiceCollection collection, Action action = null) - where T : CommandManager - { - var context = new CommandBuildingConfiguration(); - - action?.Invoke(context); - - collection.AddCommandManager(context); - - return collection; - } - - [EditorBrowsable(EditorBrowsableState.Never)] - public static IServiceCollection AddCommandManager(this IServiceCollection collection, CommandBuildingConfiguration configuration) - where T : CommandManager - { - collection.AddComponents(configuration); - - collection.AddSingleton(configuration); - - collection.TryAddSingleton(); - - return collection; - } - } -} diff --git a/src/CSF.Core/Extensions/Internal/LinqHelper.cs b/src/CSF.Core/Helpers/Internal/LinqHelpers.cs similarity index 97% rename from src/CSF.Core/Extensions/Internal/LinqHelper.cs rename to src/CSF.Core/Helpers/Internal/LinqHelpers.cs index 48529ad..b692268 100644 --- a/src/CSF.Core/Extensions/Internal/LinqHelper.cs +++ b/src/CSF.Core/Helpers/Internal/LinqHelpers.cs @@ -2,7 +2,7 @@ namespace CSF { - internal static class LinqHelper + internal static class LinqHelpers { public static IEnumerable CastWhere(this IEnumerable input) { diff --git a/src/CSF.Core/Extensions/Internal/BuildHelper.cs b/src/CSF.Core/Helpers/Internal/ReflectionHelpers.cs similarity index 51% rename from src/CSF.Core/Extensions/Internal/BuildHelper.cs rename to src/CSF.Core/Helpers/Internal/ReflectionHelpers.cs index 7b26ac6..eac4ced 100644 --- a/src/CSF.Core/Extensions/Internal/BuildHelper.cs +++ b/src/CSF.Core/Helpers/Internal/ReflectionHelpers.cs @@ -2,79 +2,65 @@ namespace CSF { - internal static class BuildHelper + internal static class ReflectionHelpers { - public static HashSet Build(this FrameworkBuilder context) - { - var modules = context.BuildModules(); - - return modules.SelectMany(x => x.Components).ToHashSet(); - } - - public static IEnumerable BuildModules(this FrameworkBuilder context) - { - var typeReaders = TypeReader.CreateDefaultReaders().UnionBy(context.TypeReaders, x => x.Type).ToDictionary(x => x.Type, x => x); - - var rootReader = typeof(TypeReader); - foreach (var assembly in context.Assemblies) - foreach (var type in assembly.GetTypes()) - if (rootReader.IsAssignableFrom(type) && !type.IsAbstract && !type.ContainsGenericParameters) - { - var reader = Activator.CreateInstance(type) as TypeReader; - - // replace existing typereader with replacement handler - if (typeReaders.ContainsKey(reader.Type)) - typeReaders[reader.Type] = reader; - else - typeReaders.Add(reader.Type, reader); - } - - var rootType = typeof(ModuleBase); - foreach (var assembly in context.Assemblies) - foreach (var type in assembly.GetTypes()) - if (rootType.IsAssignableFrom(type) && !type.IsAbstract && !type.ContainsGenericParameters) - yield return new Module(type, typeReaders); - } - - public static IConditionalComponent[] Build(this Module module, IDictionary typeReaders) - { - var commands = (IEnumerable)module.BuildCommands(typeReaders) - .OrderBy(x => x.Parameters.Length); - - var modules = (IEnumerable)module.BuildModules(typeReaders) - .OrderBy(x => x.Components.Length); - - return commands.Concat(modules) - .ToArray(); - } - - public static IEnumerable BuildModules(this Module module, IDictionary typeReaders) + private static IEnumerable GetModules(Module module, IDictionary typeReaders) { foreach (var group in module.Type.GetNestedTypes()) + { foreach (var attribute in group.GetCustomAttributes(true)) + { if (attribute is GroupAttribute gattribute) + { yield return new Module(group, typeReaders, module, gattribute.Name, gattribute.Aliases); + } + } + } } - public static IEnumerable BuildCommands(this Module module, IDictionary typeReaders) + private static IEnumerable GetCommands(Module module, IDictionary typeReaders) { foreach (var method in module.Type.GetMethods()) { var attributes = method.GetCustomAttributes(true); - string[] aliases = Array.Empty(); + string[] aliases = []; foreach (var attribute in attributes) - if (attribute is CommandAttribute commandAttribute) - aliases = commandAttribute.Aliases; + { + if (attribute is CommandAttribute cmd) + { + aliases = cmd.Aliases; + } + + // if set to noReg, dont build the command. + if (attribute is DontRegisterAttribute noReg) + { + continue; + } + } - if (!aliases.Any()) + if (aliases.Length == 0) + { continue; + } yield return new Command(module, method, aliases, typeReaders); } } - public static IParameterComponent[] BuildParameters(this MethodBase method, IDictionary typeReaders) + public static IConditionalComponent[] GetComponents(this Module module, IDictionary typeReaders) + { + var commands = (IEnumerable)GetCommands(module, typeReaders) + .OrderBy(x => x.Parameters.Length); + + var modules = (IEnumerable)GetModules(module, typeReaders) + .OrderBy(x => x.Components.Length); + + return commands.Concat(modules) + .ToArray(); + } + + public static IParameterComponent[] GetParameters(this MethodBase method, IDictionary typeReaders) { var parameters = method.GetParameters(); @@ -82,9 +68,13 @@ public static IParameterComponent[] BuildParameters(this MethodBase method, IDic for (int i = 0; i < parameters.Length; i++) { if (parameters[i].GetCustomAttributes().Any(x => x is ComplexAttribute)) + { arr[i] = new ComplexParameter(parameters[i], typeReaders); + } else + { arr[i] = new Parameter(parameters[i], typeReaders); + } } return arr; diff --git a/src/CSF.Core/Helpers/Internal/ThrowHelpers.cs b/src/CSF.Core/Helpers/Internal/ThrowHelpers.cs new file mode 100644 index 0000000..ec35eaa --- /dev/null +++ b/src/CSF.Core/Helpers/Internal/ThrowHelpers.cs @@ -0,0 +1,21 @@ +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace CSF +{ + internal static class ThrowHelpers + { + [DoesNotReturn] + public static void InvalidOp([DisallowNull] string failureMessage) + { + throw new InvalidOperationException(failureMessage); + } + + [DoesNotReturn] + public static void InvalidArg(object value, [CallerArgumentExpression(nameof(value))] string arg = null) + { + throw new ArgumentException("Argument is not in valid state, being null, empty or whitespace.", paramName: arg); + } + } +} diff --git a/src/CSF.Core/Helpers/ServiceHelper.cs b/src/CSF.Core/Helpers/ServiceHelper.cs new file mode 100644 index 0000000..465577a --- /dev/null +++ b/src/CSF.Core/Helpers/ServiceHelper.cs @@ -0,0 +1,51 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System.ComponentModel; + +namespace CSF +{ + public static class ServiceHelper + { + /// + /// Includes inheriting into the this method is called on. + /// + /// The type inheriting to include in the collection. + /// + /// The configuration required to set up a new instance of . + /// The same for chained calls. + public static IServiceCollection WithCommandManager(this IServiceCollection collection, Action action = null) + { + var context = new CMBuilder(); + + action?.Invoke(context); + + collection.AddCommandManager(context); + + return collection; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public static IServiceCollection AddCommandManager(this IServiceCollection collection, CMBuilder configuration) + { + ModulesAddTransient(collection, configuration); + + var implementor = configuration.Build(); + + collection.TryAddSingleton(implementor); + + return collection; + } + + private static IServiceCollection ModulesAddTransient(IServiceCollection collection, CMBuilder configuration) + { + var rootType = typeof(ModuleBase); + + foreach (var assembly in configuration.Assemblies) + foreach (var type in assembly.GetTypes()) + if (rootType.IsAssignableFrom(type) && !type.IsAbstract && !type.ContainsGenericParameters) + collection.TryAddTransient(type); + + return collection; + } + } +} diff --git a/src/CSF.Core/TypeReaders/BaseTypeReader.cs b/src/CSF.Core/TypeReaders/BaseTypeReader.cs index fa3fc4e..410ef7f 100644 --- a/src/CSF.Core/TypeReaders/BaseTypeReader.cs +++ b/src/CSF.Core/TypeReaders/BaseTypeReader.cs @@ -6,14 +6,14 @@ internal class BaseTypeReader : TypeReader private readonly static Lazy> _container = new(ValueGenerator); - public override Result Evaluate(ICommandContext context, IParameterComponent parameter, IServiceProvider services, string value) + public override ValueTask EvaluateAsync(ICommandContext context, IParameterComponent parameter, string value) { var parser = _container.Value[Type] as Tpd; if (parser(value, out var result)) - return Success(result); + return ValueTask.FromResult(Success(result)); - return Failure($"The provided value does not match the expected type. Expected {typeof(T).Name}, got {value}. At: '{parameter.Name}'"); + return ValueTask.FromResult(Error($"The provided value does not match the expected type. Expected {typeof(T).Name}, got {value}. At: '{parameter.Name}'")); } private static IReadOnlyDictionary ValueGenerator() diff --git a/src/CSF.Core/TypeReaders/ColorTypeReader.cs b/src/CSF.Core/TypeReaders/ColorTypeReader.cs index 4b12cf4..7c09f20 100644 --- a/src/CSF.Core/TypeReaders/ColorTypeReader.cs +++ b/src/CSF.Core/TypeReaders/ColorTypeReader.cs @@ -45,19 +45,19 @@ public ColorTypeReader() _spacedColors = spacedNames; } - public override Result Evaluate(ICommandContext context, IParameterComponent parameter, IServiceProvider services, string value) + public override ValueTask EvaluateAsync(ICommandContext context, IParameterComponent parameter, string value) { if (int.TryParse(value.Replace("#", "").Replace("0x", ""), NumberStyles.HexNumber, null, out var hexNumber)) - return Success(Color.FromArgb(hexNumber)); + return ValueTask.FromResult(Success(Color.FromArgb(hexNumber))); var name = value; _spacedColors.TryGetValue(name, out name); if (_colors.TryGetValue(name, out var color)) - return Success(color); + return ValueTask.FromResult(Success(color)); - return Failure($"The provided value is not a color. Got: '{value}'. At: '{parameter.Name}'"); + return ValueTask.FromResult(Error($"The provided value is not a color. Got: '{value}'. At: '{parameter.Name}'")); } } } diff --git a/src/CSF.Core/TypeReaders/EnumTypeReader.cs b/src/CSF.Core/TypeReaders/EnumTypeReader.cs index e192fc2..698fc31 100644 --- a/src/CSF.Core/TypeReaders/EnumTypeReader.cs +++ b/src/CSF.Core/TypeReaders/EnumTypeReader.cs @@ -1,23 +1,15 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace CSF +namespace CSF { - /// - /// Defines the default for enums. - /// - /// - /// To implement this typereader, you must first define it with the associated enum and add it to the . - /// - /// The enum this parser belongs to. - public class EnumTypeReader : TypeReader - where T : struct, Enum + internal class EnumTypeReader(Type targetEnumType) : TypeReader { - public override Result Evaluate(ICommandContext context, IParameterComponent parameter, IServiceProvider services, string value) + public override Type Type { get; } = targetEnumType; + + public override ValueTask EvaluateAsync(ICommandContext context, IParameterComponent parameter, string value) { - if (Enum.TryParse(value, true, out var result)) - return Success(result); + if (Enum.TryParse(Type, value, true, out var result)) + return ValueTask.FromResult(Success(result)); - return Failure($"The provided value is not a part the enum specified. Expected: '{typeof(T).Name}', got: '{value}'. At: '{parameter.Name}'"); + return ValueTask.FromResult(Error($"The provided value is not a part the enum specified. Expected: '{Type.Name}', got: '{value}'. At: '{parameter.Name}'")); } } } diff --git a/src/CSF.Core/TypeReaders/TimeSpanTypeReader.cs b/src/CSF.Core/TypeReaders/TimeSpanTypeReader.cs index d97e683..9d4efdc 100644 --- a/src/CSF.Core/TypeReaders/TimeSpanTypeReader.cs +++ b/src/CSF.Core/TypeReaders/TimeSpanTypeReader.cs @@ -33,7 +33,7 @@ public TimeSpanTypeReader() }; } - public override Result Evaluate(ICommandContext context, IParameterComponent parameter, IServiceProvider services, string value) + public override ValueTask EvaluateAsync(ICommandContext context, IParameterComponent parameter, string value) { if (!TimeSpan.TryParse(value, out TimeSpan span)) { @@ -48,10 +48,10 @@ public override Result Evaluate(ICommandContext context, IParameterComponent par #pragma warning restore IDE0220 // Add explicit cast } else - return Failure($"The provided value is no timespan. Got: '{value}'. At: '{parameter.Name}'"); + return ValueTask.FromResult(Error($"The provided value is no timespan. Got: '{value}'. At: '{parameter.Name}'")); } - return Success(span); + return ValueTask.FromResult(Success(span)); } private static TimeSpan Seconds(string match) diff --git a/src/CSF.Hosting/CSF.Hosting.csproj b/src/CSF.Hosting/CSF.Hosting.csproj index 6ceddc3..7cb0313 100644 --- a/src/CSF.Hosting/CSF.Hosting.csproj +++ b/src/CSF.Hosting/CSF.Hosting.csproj @@ -46,7 +46,7 @@ - + diff --git a/src/CSF.Tests.Hosting/CSF.Tests.Hosting.csproj b/src/CSF.Tests.Hosting/CSF.Tests.Hosting.csproj index e3f1fc0..c686f43 100644 --- a/src/CSF.Tests.Hosting/CSF.Tests.Hosting.csproj +++ b/src/CSF.Tests.Hosting/CSF.Tests.Hosting.csproj @@ -8,7 +8,7 @@ - + From 72578431a7d30c81dc691e1298372d8949a2eaa2 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Thu, 25 Jan 2024 18:40:30 +0100 Subject: [PATCH 04/40] Improve typereader gen, value duplicate checks, throwhelpers --- src/CSF.Core/Abstractions/ModuleBase.cs | 12 ++++++------ .../Abstractions/PreconditionAttribute.cs | 4 ++-- src/CSF.Core/Abstractions/Results/IResult.cs | 3 +++ src/CSF.Core/Abstractions/TypeReader.cs | 4 ++-- src/CSF.Core/Attributes/CommandAttribute.cs | 17 ++++++++++++++--- .../Attributes/DescriptionAttribute.cs | 2 +- src/CSF.Core/Attributes/GroupAttribute.cs | 19 +++++++++++++++---- src/CSF.Core/Attributes/PriorityAttribute.cs | 2 +- src/CSF.Core/CSF.Core.csproj | 1 + src/CSF.Core/CommandManager.cs | 2 +- src/CSF.Core/Components/Parameter.cs | 2 +- src/CSF.Core/Contexts/CommandContext.cs | 12 ++++++++---- src/CSF.Core/Exceptions/CheckException.cs | 8 ++------ src/CSF.Core/Exceptions/CommandException.cs | 8 ++------ .../Internal/ArgumentMissingException.cs | 14 ++++++++++++++ .../Internal/RangeDuplicateException.cs | 14 ++++++++++++++ src/CSF.Core/Exceptions/MatchException.cs | 7 ++----- src/CSF.Core/Exceptions/ReadException.cs | 8 ++------ src/CSF.Core/Exceptions/SearchException.cs | 8 ++------ src/CSF.Core/Helpers/Internal/ThrowHelpers.cs | 13 ++++++++++--- src/CSF.Core/TypeReaders/EnumTypeReader.cs | 12 ++++++++++++ .../TypeReaders/TimeSpanTypeReader.cs | 7 +++++-- 22 files changed, 120 insertions(+), 59 deletions(-) create mode 100644 src/CSF.Core/Exceptions/Internal/ArgumentMissingException.cs create mode 100644 src/CSF.Core/Exceptions/Internal/RangeDuplicateException.cs diff --git a/src/CSF.Core/Abstractions/ModuleBase.cs b/src/CSF.Core/Abstractions/ModuleBase.cs index 35066c7..09df4bb 100644 --- a/src/CSF.Core/Abstractions/ModuleBase.cs +++ b/src/CSF.Core/Abstractions/ModuleBase.cs @@ -50,7 +50,7 @@ public IServiceProvider Services public virtual void Respond(string message) => Console.WriteLine(message); - public virtual RunResult ReturnTypeHandle(object value) + public virtual RunResult ReturnTypeResolve(object value) { switch (value) { @@ -59,18 +59,18 @@ public virtual RunResult ReturnTypeHandle(object value) case null: return new(Command, null); default: - throw new NotSupportedException("The return value of this command is not supported."); + throw new NotSupportedException($"The return value of the command in question is not supported. Consider overriding {nameof(ReturnTypeResolve)} to add your own return type resolver."); } } - public virtual async ValueTask BeforeExecuteAsync() + public virtual ValueTask BeforeExecuteAsync() { - + return ValueTask.CompletedTask; } - public virtual async ValueTask AfterExecuteAsync() + public virtual ValueTask AfterExecuteAsync() { - + return ValueTask.CompletedTask; } } } diff --git a/src/CSF.Core/Abstractions/PreconditionAttribute.cs b/src/CSF.Core/Abstractions/PreconditionAttribute.cs index db05341..61e71bd 100644 --- a/src/CSF.Core/Abstractions/PreconditionAttribute.cs +++ b/src/CSF.Core/Abstractions/PreconditionAttribute.cs @@ -21,7 +21,7 @@ public abstract class PreconditionAttribute : Attribute public static CheckResult Error([DisallowNull] Exception exception) { if (exception == null) - ThrowHelpers.InvalidArg(exception); + ThrowHelpers.ArgMissing(exception); if (exception is CheckException checkEx) { @@ -33,7 +33,7 @@ public static CheckResult Error([DisallowNull] Exception exception) public virtual CheckResult Error([DisallowNull] string error) { if (string.IsNullOrEmpty(error)) - ThrowHelpers.InvalidArg(error); + ThrowHelpers.ArgMissing(error); return new(new CheckException(error)); } diff --git a/src/CSF.Core/Abstractions/Results/IResult.cs b/src/CSF.Core/Abstractions/Results/IResult.cs index 6135ab6..3d64251 100644 --- a/src/CSF.Core/Abstractions/Results/IResult.cs +++ b/src/CSF.Core/Abstractions/Results/IResult.cs @@ -8,5 +8,8 @@ namespace CSF { public interface IResult { + public Exception Exception { get; } + + public bool Success { get; } } } diff --git a/src/CSF.Core/Abstractions/TypeReader.cs b/src/CSF.Core/Abstractions/TypeReader.cs index 8811e0d..8b7cf28 100644 --- a/src/CSF.Core/Abstractions/TypeReader.cs +++ b/src/CSF.Core/Abstractions/TypeReader.cs @@ -48,7 +48,7 @@ internal ValueTask ObjectEvaluateAsync(ICommandContext context, IPar public virtual ReadResult Error([DisallowNull] Exception exception) { if (exception == null) - ThrowHelpers.InvalidArg(exception); + ThrowHelpers.ArgMissing(exception); if (exception is ReadException readEx) { @@ -60,7 +60,7 @@ public virtual ReadResult Error([DisallowNull] Exception exception) public virtual ReadResult Error([DisallowNull] string error) { if (string.IsNullOrEmpty(error)) - ThrowHelpers.InvalidArg(error); + ThrowHelpers.ArgMissing(error); return new(new ReadException(error)); } diff --git a/src/CSF.Core/Attributes/CommandAttribute.cs b/src/CSF.Core/Attributes/CommandAttribute.cs index 0764f11..fc01fc2 100644 --- a/src/CSF.Core/Attributes/CommandAttribute.cs +++ b/src/CSF.Core/Attributes/CommandAttribute.cs @@ -36,13 +36,24 @@ public CommandAttribute([DisallowNull] string name) public CommandAttribute([DisallowNull] string name, params string[] aliases) { if (string.IsNullOrWhiteSpace(name)) - ThrowHelpers.InvalidArg(name); + ThrowHelpers.ArgMissing(name); var arr = new string[aliases.Length + 1]; + for (int i = 0; i < aliases.Length; i++) + { + if (string.IsNullOrWhiteSpace(aliases[i])) + ThrowHelpers.ArgMissing(aliases); - arr[0] = name; + if (arr.Contains(aliases[i])) + ThrowHelpers.RangeDuplicate(aliases); + + arr[i + 1] = aliases[i]; + } - Array.Copy(aliases, 0, arr, 1, aliases.Length); + if (arr.Contains(name)) + ThrowHelpers.RangeDuplicate(aliases); + + arr[0] = name; Name = name; Aliases = arr; diff --git a/src/CSF.Core/Attributes/DescriptionAttribute.cs b/src/CSF.Core/Attributes/DescriptionAttribute.cs index 0def0ec..a183501 100644 --- a/src/CSF.Core/Attributes/DescriptionAttribute.cs +++ b/src/CSF.Core/Attributes/DescriptionAttribute.cs @@ -20,7 +20,7 @@ public sealed class DescriptionAttribute : Attribute public DescriptionAttribute([DisallowNull] string description) { if (string.IsNullOrWhiteSpace(description)) - ThrowHelpers.InvalidArg(description); + ThrowHelpers.ArgMissing(description); Description = description; } diff --git a/src/CSF.Core/Attributes/GroupAttribute.cs b/src/CSF.Core/Attributes/GroupAttribute.cs index 525e4b2..f9a15ca 100644 --- a/src/CSF.Core/Attributes/GroupAttribute.cs +++ b/src/CSF.Core/Attributes/GroupAttribute.cs @@ -22,7 +22,7 @@ public sealed class GroupAttribute : Attribute /// Creates a new with defined name. /// /// - public GroupAttribute(string name) + public GroupAttribute([DisallowNull] string name) : this(name, []) { @@ -37,13 +37,24 @@ public GroupAttribute(string name) public GroupAttribute([DisallowNull] string name, params string[] aliases) { if (string.IsNullOrWhiteSpace(name)) - ThrowHelpers.InvalidArg(name); + ThrowHelpers.ArgMissing(name); var arr = new string[aliases.Length + 1]; + for (int i = 0; i < aliases.Length; i++) + { + if (string.IsNullOrWhiteSpace(aliases[i])) + ThrowHelpers.ArgMissing(aliases); - arr[0] = name; + if (arr.Contains(aliases[i])) + ThrowHelpers.RangeDuplicate(aliases); + + arr[i + 1] = aliases[i]; + } - Array.Copy(aliases, 0, arr, 1, aliases.Length); + if (arr.Contains(name)) + ThrowHelpers.RangeDuplicate(aliases); + + arr[0] = name; Name = name; Aliases = arr; diff --git a/src/CSF.Core/Attributes/PriorityAttribute.cs b/src/CSF.Core/Attributes/PriorityAttribute.cs index 210853c..b33d60b 100644 --- a/src/CSF.Core/Attributes/PriorityAttribute.cs +++ b/src/CSF.Core/Attributes/PriorityAttribute.cs @@ -12,7 +12,7 @@ namespace CSF /// Creates a new with provided priority. /// /// The priority of this command, which can be between 0 and 255. - [AttributeUsage(AttributeTargets.Method)] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] public sealed class PriorityAttribute([DisallowNull] byte priority) : Attribute { /// diff --git a/src/CSF.Core/CSF.Core.csproj b/src/CSF.Core/CSF.Core.csproj index c7b9756..c8dece9 100644 --- a/src/CSF.Core/CSF.Core.csproj +++ b/src/CSF.Core/CSF.Core.csproj @@ -4,6 +4,7 @@ net8.0 enable CSF + true 2.1 2.1 diff --git a/src/CSF.Core/CommandManager.cs b/src/CSF.Core/CommandManager.cs index 0e46f04..ed842c3 100644 --- a/src/CSF.Core/CommandManager.cs +++ b/src/CSF.Core/CommandManager.cs @@ -157,7 +157,7 @@ internal static async ValueTask RunAsync(ICommandContext context, Mat await module.AfterExecuteAsync(); - return module.ReturnTypeHandle(value); + return module.ReturnTypeResolve(value); } catch (Exception exception) { diff --git a/src/CSF.Core/Components/Parameter.cs b/src/CSF.Core/Components/Parameter.cs index d0851d6..0312fee 100644 --- a/src/CSF.Core/Components/Parameter.cs +++ b/src/CSF.Core/Components/Parameter.cs @@ -58,7 +58,7 @@ internal Parameter(ParameterInfo parameterInfo, IDictionary ty IsRemainder = false; if (Type.IsEnum) - TypeReader = new EnumTypeReader(Type); + TypeReader = EnumTypeReader.GetOrCreate(Type); else if (Type != typeof(string) && Type != typeof(object)) TypeReader = typeReaders[Type]; diff --git a/src/CSF.Core/Contexts/CommandContext.cs b/src/CSF.Core/Contexts/CommandContext.cs index 547851d..adc03fe 100644 --- a/src/CSF.Core/Contexts/CommandContext.cs +++ b/src/CSF.Core/Contexts/CommandContext.cs @@ -1,10 +1,14 @@ namespace CSF { - /// - /// Represents a class that's used to describe data from the command. - /// - public class CommandContext(T options) : ICommandContext + public class CommandContext(T options) : CommandContext(options), ICommandContext where T : IExecutionOptions + { + public new T Options { get; } = options; + + IExecutionOptions ICommandContext.Options { get; } = options; + } + + public class CommandContext(IExecutionOptions options) : ICommandContext { public IExecutionOptions Options { get; } = options; } diff --git a/src/CSF.Core/Exceptions/CheckException.cs b/src/CSF.Core/Exceptions/CheckException.cs index 13e8269..6f7d1ac 100644 --- a/src/CSF.Core/Exceptions/CheckException.cs +++ b/src/CSF.Core/Exceptions/CheckException.cs @@ -3,12 +3,8 @@ /// /// Represents a that is thrown when no matched command succeeded its precondition checks. /// - public sealed class CheckException : ExecutionException + public sealed class CheckException(string message, Exception innerException = null) + : ExecutionException(message, innerException) { - public CheckException(string message, Exception innerException = null) - : base(message, innerException) - { - - } } } diff --git a/src/CSF.Core/Exceptions/CommandException.cs b/src/CSF.Core/Exceptions/CommandException.cs index 125404b..bf72715 100644 --- a/src/CSF.Core/Exceptions/CommandException.cs +++ b/src/CSF.Core/Exceptions/CommandException.cs @@ -3,12 +3,8 @@ /// /// Represents a that is thrown when the command being executed failed to run its body. /// - public sealed class CommandException : ExecutionException + public sealed class CommandException(string message, Exception innerException = null) + : ExecutionException(message, innerException) { - public CommandException(string message, Exception innerException = null) - : base(message, innerException) - { - - } } } diff --git a/src/CSF.Core/Exceptions/Internal/ArgumentMissingException.cs b/src/CSF.Core/Exceptions/Internal/ArgumentMissingException.cs new file mode 100644 index 0000000..61b4403 --- /dev/null +++ b/src/CSF.Core/Exceptions/Internal/ArgumentMissingException.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSF +{ + internal sealed class ArgumentMissingException(string paramName, string message) + : ArgumentException(message, paramName) + { + + } +} diff --git a/src/CSF.Core/Exceptions/Internal/RangeDuplicateException.cs b/src/CSF.Core/Exceptions/Internal/RangeDuplicateException.cs new file mode 100644 index 0000000..5f75b2b --- /dev/null +++ b/src/CSF.Core/Exceptions/Internal/RangeDuplicateException.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSF +{ + internal sealed class RangeDuplicateException(string paramName, string message) + : ArgumentException(paramName, message) + { + + } +} \ No newline at end of file diff --git a/src/CSF.Core/Exceptions/MatchException.cs b/src/CSF.Core/Exceptions/MatchException.cs index cf6b1d0..c5df10b 100644 --- a/src/CSF.Core/Exceptions/MatchException.cs +++ b/src/CSF.Core/Exceptions/MatchException.cs @@ -1,11 +1,8 @@ namespace CSF { - public class MatchException : ExecutionException + public class MatchException(string message, Exception innerException = null) + : ExecutionException(message, innerException) { - public MatchException(string message, Exception innerException = null) - : base(message, innerException) - { - } } } diff --git a/src/CSF.Core/Exceptions/ReadException.cs b/src/CSF.Core/Exceptions/ReadException.cs index 2406df5..543ca25 100644 --- a/src/CSF.Core/Exceptions/ReadException.cs +++ b/src/CSF.Core/Exceptions/ReadException.cs @@ -3,12 +3,8 @@ /// /// Represents a that is thrown when no matched command succeeded parsing its parameters. /// - public sealed class ReadException : ExecutionException + public sealed class ReadException(string message, Exception innerException = null) + : ExecutionException(message, innerException) { - public ReadException(string message, Exception innerException = null) - : base(message, innerException) - { - - } } } diff --git a/src/CSF.Core/Exceptions/SearchException.cs b/src/CSF.Core/Exceptions/SearchException.cs index ec01158..7f3726a 100644 --- a/src/CSF.Core/Exceptions/SearchException.cs +++ b/src/CSF.Core/Exceptions/SearchException.cs @@ -3,12 +3,8 @@ /// /// Represents a that is thrown when no command could be found. /// - public sealed class SearchException : ExecutionException + public sealed class SearchException(string message, Exception innerException = null) + : ExecutionException(message, innerException) { - public SearchException(string message, Exception innerException = null) - : base(message, innerException) - { - - } } } diff --git a/src/CSF.Core/Helpers/Internal/ThrowHelpers.cs b/src/CSF.Core/Helpers/Internal/ThrowHelpers.cs index ec35eaa..5ad3bfa 100644 --- a/src/CSF.Core/Helpers/Internal/ThrowHelpers.cs +++ b/src/CSF.Core/Helpers/Internal/ThrowHelpers.cs @@ -1,4 +1,5 @@ -using System.Collections; +using CSF.Exceptions; +using System.Collections; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -13,9 +14,15 @@ public static void InvalidOp([DisallowNull] string failureMessage) } [DoesNotReturn] - public static void InvalidArg(object value, [CallerArgumentExpression(nameof(value))] string arg = null) + public static void ArgMissing(object value, [CallerArgumentExpression(nameof(value))] string arg = null) { - throw new ArgumentException("Argument is not in valid state, being null, empty or whitespace.", paramName: arg); + throw new ArgumentMissingException(arg, "Argument is not in valid state, being null, empty or whitespace."); + } + + [DoesNotReturn] + public static void RangeDuplicate(object value, [CallerArgumentExpression(nameof(value))] string arg = null) + { + throw new RangeDuplicateException(arg, "Range contains a duplicate value, which is not supported by the implementation."); } } } diff --git a/src/CSF.Core/TypeReaders/EnumTypeReader.cs b/src/CSF.Core/TypeReaders/EnumTypeReader.cs index 698fc31..161bf35 100644 --- a/src/CSF.Core/TypeReaders/EnumTypeReader.cs +++ b/src/CSF.Core/TypeReaders/EnumTypeReader.cs @@ -2,6 +2,8 @@ { internal class EnumTypeReader(Type targetEnumType) : TypeReader { + private static readonly Dictionary _readers = []; + public override Type Type { get; } = targetEnumType; public override ValueTask EvaluateAsync(ICommandContext context, IParameterComponent parameter, string value) @@ -11,5 +13,15 @@ public override ValueTask EvaluateAsync(ICommandContext context, IPa return ValueTask.FromResult(Error($"The provided value is not a part the enum specified. Expected: '{Type.Name}', got: '{value}'. At: '{parameter.Name}'")); } + + internal static EnumTypeReader GetOrCreate(Type type) + { + if (_readers.TryGetValue(type, out var reader)) + return reader; + + _readers.Add(type, reader = new EnumTypeReader(type)); + + return reader; + } } } diff --git a/src/CSF.Core/TypeReaders/TimeSpanTypeReader.cs b/src/CSF.Core/TypeReaders/TimeSpanTypeReader.cs index 9d4efdc..4a018f5 100644 --- a/src/CSF.Core/TypeReaders/TimeSpanTypeReader.cs +++ b/src/CSF.Core/TypeReaders/TimeSpanTypeReader.cs @@ -2,10 +2,10 @@ namespace CSF { - internal class TimeSpanTypeReader : TypeReader + internal partial class TimeSpanTypeReader : TypeReader { private readonly IReadOnlyDictionary> _callback; - private readonly Regex _regex = new(@"(\d*)\s*([a-zA-Z]*)\s*(?:and|,)?\s*", RegexOptions.Compiled); + private readonly Regex _regex = GenTSRegex(); public TimeSpanTypeReader() { @@ -71,5 +71,8 @@ private static TimeSpan Weeks(string match) private static TimeSpan Months(string match) => new(((int)(int.Parse(match) * 30.437)), 0, 0, 0); + + [GeneratedRegex(@"(\d*)\s*([a-zA-Z]*)\s*(?:and|,)?\s*", RegexOptions.Compiled)] + private static partial Regex GenTSRegex(); } } From f0fa0bd8c095f266b45204f3d1031a6e331d792b Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Thu, 25 Jan 2024 18:40:45 +0100 Subject: [PATCH 05/40] Simple cleanup --- src/CSF.Core/Abstractions/Results/IResult.cs | 12 +++--------- src/CSF.Core/CMBuilder.cs | 2 +- src/CSF.Core/CommandManager.cs | 2 +- src/CSF.Core/CommandManagerHelper.cs | 2 +- src/CSF.Core/Components/Command.cs | 3 +-- src/CSF.Core/Components/Module.cs | 2 +- src/CSF.Core/Exceptions/CheckException.cs | 2 +- src/CSF.Core/Exceptions/CommandException.cs | 2 +- .../Exceptions/Internal/ArgumentMissingException.cs | 10 ++-------- .../Exceptions/Internal/RangeDuplicateException.cs | 10 ++-------- src/CSF.Core/Exceptions/MatchException.cs | 2 +- src/CSF.Core/Exceptions/ReadException.cs | 2 +- src/CSF.Core/Exceptions/SearchException.cs | 2 +- src/CSF.Core/Helpers/Internal/ThrowHelpers.cs | 4 +--- src/CSF.Core/TypeReaders/EnumTypeReader.cs | 2 +- src/CSF.Tests.Console/Modules/AsyncModule.cs | 8 +------- 16 files changed, 20 insertions(+), 47 deletions(-) diff --git a/src/CSF.Core/Abstractions/Results/IResult.cs b/src/CSF.Core/Abstractions/Results/IResult.cs index 3d64251..5cda7ce 100644 --- a/src/CSF.Core/Abstractions/Results/IResult.cs +++ b/src/CSF.Core/Abstractions/Results/IResult.cs @@ -1,15 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CSF +namespace CSF { public interface IResult { - public Exception Exception { get; } + public Exception Exception { get; } - public bool Success { get; } + public bool Success { get; } } } diff --git a/src/CSF.Core/CMBuilder.cs b/src/CSF.Core/CMBuilder.cs index 625e07b..c9a9547 100644 --- a/src/CSF.Core/CMBuilder.cs +++ b/src/CSF.Core/CMBuilder.cs @@ -6,7 +6,7 @@ public sealed class CMBuilder { private bool _disposed = false; - public HashSet Assemblies { get; set; } = [ Assembly.GetEntryAssembly() ]; + public HashSet Assemblies { get; set; } = [Assembly.GetEntryAssembly()]; public HashSet TypeReaders { get; set; } = []; diff --git a/src/CSF.Core/CommandManager.cs b/src/CSF.Core/CommandManager.cs index ed842c3..256d13b 100644 --- a/src/CSF.Core/CommandManager.cs +++ b/src/CSF.Core/CommandManager.cs @@ -41,7 +41,7 @@ public async ValueTask ExecuteAsync(ICommandContext context, params obj // define a fallback for unsuccesful execution. MatchResult? fallback = default; - + // order searches by descending for priority definitions. foreach (var search in searches.OrderByDescending(x => x.Command.Priority)) { diff --git a/src/CSF.Core/CommandManagerHelper.cs b/src/CSF.Core/CommandManagerHelper.cs index 250a93b..6bd1f61 100644 --- a/src/CSF.Core/CommandManagerHelper.cs +++ b/src/CSF.Core/CommandManagerHelper.cs @@ -17,7 +17,7 @@ public static IEnumerable RecursiveSearch(this IEnumerable typeReaders, Module roo Components = this.GetComponents(typeReaders); Name = expectedName ?? type.Name; - Aliases = aliases ?? [ Name ]; + Aliases = aliases ?? [Name]; } /// diff --git a/src/CSF.Core/Exceptions/CheckException.cs b/src/CSF.Core/Exceptions/CheckException.cs index 6f7d1ac..8b97621 100644 --- a/src/CSF.Core/Exceptions/CheckException.cs +++ b/src/CSF.Core/Exceptions/CheckException.cs @@ -3,7 +3,7 @@ /// /// Represents a that is thrown when no matched command succeeded its precondition checks. /// - public sealed class CheckException(string message, Exception innerException = null) + public sealed class CheckException(string message, Exception innerException = null) : ExecutionException(message, innerException) { } diff --git a/src/CSF.Core/Exceptions/CommandException.cs b/src/CSF.Core/Exceptions/CommandException.cs index bf72715..8ae1c6e 100644 --- a/src/CSF.Core/Exceptions/CommandException.cs +++ b/src/CSF.Core/Exceptions/CommandException.cs @@ -3,7 +3,7 @@ /// /// Represents a that is thrown when the command being executed failed to run its body. /// - public sealed class CommandException(string message, Exception innerException = null) + public sealed class CommandException(string message, Exception innerException = null) : ExecutionException(message, innerException) { } diff --git a/src/CSF.Core/Exceptions/Internal/ArgumentMissingException.cs b/src/CSF.Core/Exceptions/Internal/ArgumentMissingException.cs index 61b4403..e57be6e 100644 --- a/src/CSF.Core/Exceptions/Internal/ArgumentMissingException.cs +++ b/src/CSF.Core/Exceptions/Internal/ArgumentMissingException.cs @@ -1,12 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CSF +namespace CSF { - internal sealed class ArgumentMissingException(string paramName, string message) + internal sealed class ArgumentMissingException(string paramName, string message) : ArgumentException(message, paramName) { diff --git a/src/CSF.Core/Exceptions/Internal/RangeDuplicateException.cs b/src/CSF.Core/Exceptions/Internal/RangeDuplicateException.cs index 5f75b2b..1a799ab 100644 --- a/src/CSF.Core/Exceptions/Internal/RangeDuplicateException.cs +++ b/src/CSF.Core/Exceptions/Internal/RangeDuplicateException.cs @@ -1,12 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CSF +namespace CSF { - internal sealed class RangeDuplicateException(string paramName, string message) + internal sealed class RangeDuplicateException(string paramName, string message) : ArgumentException(paramName, message) { diff --git a/src/CSF.Core/Exceptions/MatchException.cs b/src/CSF.Core/Exceptions/MatchException.cs index c5df10b..8fd43e6 100644 --- a/src/CSF.Core/Exceptions/MatchException.cs +++ b/src/CSF.Core/Exceptions/MatchException.cs @@ -1,6 +1,6 @@ namespace CSF { - public class MatchException(string message, Exception innerException = null) + public class MatchException(string message, Exception innerException = null) : ExecutionException(message, innerException) { diff --git a/src/CSF.Core/Exceptions/ReadException.cs b/src/CSF.Core/Exceptions/ReadException.cs index 543ca25..b0a3320 100644 --- a/src/CSF.Core/Exceptions/ReadException.cs +++ b/src/CSF.Core/Exceptions/ReadException.cs @@ -3,7 +3,7 @@ /// /// Represents a that is thrown when no matched command succeeded parsing its parameters. /// - public sealed class ReadException(string message, Exception innerException = null) + public sealed class ReadException(string message, Exception innerException = null) : ExecutionException(message, innerException) { } diff --git a/src/CSF.Core/Exceptions/SearchException.cs b/src/CSF.Core/Exceptions/SearchException.cs index 7f3726a..ac55443 100644 --- a/src/CSF.Core/Exceptions/SearchException.cs +++ b/src/CSF.Core/Exceptions/SearchException.cs @@ -3,7 +3,7 @@ /// /// Represents a that is thrown when no command could be found. /// - public sealed class SearchException(string message, Exception innerException = null) + public sealed class SearchException(string message, Exception innerException = null) : ExecutionException(message, innerException) { } diff --git a/src/CSF.Core/Helpers/Internal/ThrowHelpers.cs b/src/CSF.Core/Helpers/Internal/ThrowHelpers.cs index 5ad3bfa..e039d61 100644 --- a/src/CSF.Core/Helpers/Internal/ThrowHelpers.cs +++ b/src/CSF.Core/Helpers/Internal/ThrowHelpers.cs @@ -1,6 +1,4 @@ -using CSF.Exceptions; -using System.Collections; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace CSF diff --git a/src/CSF.Core/TypeReaders/EnumTypeReader.cs b/src/CSF.Core/TypeReaders/EnumTypeReader.cs index 161bf35..8a881ab 100644 --- a/src/CSF.Core/TypeReaders/EnumTypeReader.cs +++ b/src/CSF.Core/TypeReaders/EnumTypeReader.cs @@ -18,7 +18,7 @@ internal static EnumTypeReader GetOrCreate(Type type) { if (_readers.TryGetValue(type, out var reader)) return reader; - + _readers.Add(type, reader = new EnumTypeReader(type)); return reader; diff --git a/src/CSF.Tests.Console/Modules/AsyncModule.cs b/src/CSF.Tests.Console/Modules/AsyncModule.cs index bc72969..c2b2915 100644 --- a/src/CSF.Tests.Console/Modules/AsyncModule.cs +++ b/src/CSF.Tests.Console/Modules/AsyncModule.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CSF.Tests.Console.Modules +namespace CSF.Tests.Console.Modules { public sealed class AsyncModule : ModuleBase { From 2b635570be583a5e053563b86682d7cb92efa7fe Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Thu, 25 Jan 2024 20:56:42 +0100 Subject: [PATCH 06/40] Attention to namespaces, naming convention system, parsing start --- .../Abstractions/Contexts/ICommandContext.cs | 13 --- .../Attributes/DontRegisterAttribute.cs | 11 -- .../Attributes/RequireContextAttribute.cs | 26 ----- src/CSF.Core/CMBuilder.cs | 87 --------------- src/CSF.Core/Components/CommandCell.cs | 59 ---------- src/CSF.Core/Components/Constructor.cs | 37 ------- src/CSF.Core/Contexts/CommandContext.cs | 15 --- .../{ => Core}/Attributes/CommandAttribute.cs | 11 +- .../{ => Core}/Attributes/ComplexAttribute.cs | 0 .../Attributes/DescriptionAttribute.cs | 3 +- .../{ => Core}/Attributes/GroupAttribute.cs | 3 +- .../Attributes/PrimaryConstructorAttribute.cs | 0 .../Attributes/PriorityAttribute.cs | 0 .../Attributes/RemainderAttribute.cs | 0 src/CSF.Core/Core/CommandConfiguration.cs | 12 +++ src/CSF.Core/{ => Core}/CommandManager.cs | 101 ++++++++++++------ .../Core/Execution/ICommandContext.cs | 8 ++ .../Core/Execution/Impl/CommandContext.cs | 7 ++ .../Execution}/ModuleBase.cs | 12 +-- .../{Abstractions => Core}/Results/IResult.cs | 0 .../Results/Impl}/CheckResult.cs | 0 .../Results/Impl}/MatchResult.cs | 10 +- .../Results/Impl}/ReadResult.cs | 0 .../Results/Impl}/RunResult.cs | 10 +- .../Results/Impl}/SearchResult.cs | 12 ++- .../ArgumentMissingException.cs | 2 +- src/CSF.Core/Exceptions/CheckException.cs | 2 +- src/CSF.Core/Exceptions/CommandException.cs | 2 +- .../ExecutionException.cs | 4 +- src/CSF.Core/Exceptions/MatchException.cs | 2 +- .../{Internal => }/RangeDuplicateException.cs | 2 +- src/CSF.Core/Exceptions/ReadException.cs | 2 +- src/CSF.Core/Exceptions/SearchException.cs | 2 +- src/CSF.Core/ExecutionOptions.cs | 28 ----- .../LinqHelpers.cs => CollectionHelpers.cs} | 4 +- .../ExecutionHelpers.cs} | 21 ++-- .../{Internal => }/ReflectionHelpers.cs | 41 ++++--- src/CSF.Core/Helpers/ServiceHelper.cs | 51 --------- src/CSF.Core/Helpers/ServiceHelpers.cs | 30 ++++++ .../Helpers/{Internal => }/ThrowHelpers.cs | 5 +- src/CSF.Core/Parsing/Impl/CharSpanParser.cs | 16 +++ src/CSF.Core/Parsing/Impl/StringParser.cs | 18 ++++ src/CSF.Core/Parsing/Parser.cs | 9 +- src/CSF.Core/Parsing/ParserCell.cs | 31 ------ src/CSF.Core/Parsing/SpanParser.cs | 21 ++++ .../PreconditionAttribute.cs | 10 +- .../IArgument.cs} | 6 +- .../IArgumentBucket.cs} | 6 +- .../IConditional.cs} | 6 +- .../IComponent.cs => Reflection/INameable.cs} | 4 +- .../Impl/ArgumentInfo.cs} | 10 +- .../Impl/CommandInfo.cs} | 17 +-- .../Impl/ComplexArgumentInfo.cs} | 20 ++-- .../Impl/ModuleInfo.cs} | 14 ++- .../TypeReaders/{ => Impl}/BaseTypeReader.cs | 6 +- .../TypeReaders/{ => Impl}/ColorTypeReader.cs | 7 +- .../TypeReaders/{ => Impl}/EnumTypeReader.cs | 6 +- .../{ => Impl}/TimeSpanTypeReader.cs | 7 +- .../TypeReader.cs | 30 ++---- .../TypeReaders/TimeOnlyTypeReader.cs | 10 -- 60 files changed, 339 insertions(+), 550 deletions(-) delete mode 100644 src/CSF.Core/Abstractions/Contexts/ICommandContext.cs delete mode 100644 src/CSF.Core/Attributes/DontRegisterAttribute.cs delete mode 100644 src/CSF.Core/Attributes/RequireContextAttribute.cs delete mode 100644 src/CSF.Core/CMBuilder.cs delete mode 100644 src/CSF.Core/Components/CommandCell.cs delete mode 100644 src/CSF.Core/Components/Constructor.cs delete mode 100644 src/CSF.Core/Contexts/CommandContext.cs rename src/CSF.Core/{ => Core}/Attributes/CommandAttribute.cs (79%) rename src/CSF.Core/{ => Core}/Attributes/ComplexAttribute.cs (100%) rename src/CSF.Core/{ => Core}/Attributes/DescriptionAttribute.cs (93%) rename src/CSF.Core/{ => Core}/Attributes/GroupAttribute.cs (96%) rename src/CSF.Core/{ => Core}/Attributes/PrimaryConstructorAttribute.cs (100%) rename src/CSF.Core/{ => Core}/Attributes/PriorityAttribute.cs (100%) rename src/CSF.Core/{ => Core}/Attributes/RemainderAttribute.cs (100%) create mode 100644 src/CSF.Core/Core/CommandConfiguration.cs rename src/CSF.Core/{ => Core}/CommandManager.cs (61%) create mode 100644 src/CSF.Core/Core/Execution/ICommandContext.cs create mode 100644 src/CSF.Core/Core/Execution/Impl/CommandContext.cs rename src/CSF.Core/{Abstractions => Core/Execution}/ModuleBase.cs (91%) rename src/CSF.Core/{Abstractions => Core}/Results/IResult.cs (100%) rename src/CSF.Core/{Results => Core/Results/Impl}/CheckResult.cs (100%) rename src/CSF.Core/{Results => Core/Results/Impl}/MatchResult.cs (66%) rename src/CSF.Core/{Results => Core/Results/Impl}/ReadResult.cs (100%) rename src/CSF.Core/{Results => Core/Results/Impl}/RunResult.cs (66%) rename src/CSF.Core/{Results => Core/Results/Impl}/SearchResult.cs (63%) rename src/CSF.Core/Exceptions/{Internal => }/ArgumentMissingException.cs (84%) rename src/CSF.Core/{Abstractions => Exceptions}/ExecutionException.cs (77%) rename src/CSF.Core/Exceptions/{Internal => }/RangeDuplicateException.cs (84%) delete mode 100644 src/CSF.Core/ExecutionOptions.cs rename src/CSF.Core/Helpers/{Internal/LinqHelpers.cs => CollectionHelpers.cs} (94%) rename src/CSF.Core/{CommandManagerHelper.cs => Helpers/ExecutionHelpers.cs} (80%) rename src/CSF.Core/Helpers/{Internal => }/ReflectionHelpers.cs (60%) delete mode 100644 src/CSF.Core/Helpers/ServiceHelper.cs create mode 100644 src/CSF.Core/Helpers/ServiceHelpers.cs rename src/CSF.Core/Helpers/{Internal => }/ThrowHelpers.cs (90%) create mode 100644 src/CSF.Core/Parsing/Impl/CharSpanParser.cs create mode 100644 src/CSF.Core/Parsing/Impl/StringParser.cs delete mode 100644 src/CSF.Core/Parsing/ParserCell.cs create mode 100644 src/CSF.Core/Parsing/SpanParser.cs rename src/CSF.Core/{Abstractions => Preconditions}/PreconditionAttribute.cs (89%) rename src/CSF.Core/{Abstractions/Components/IParameterComponent.cs => Reflection/IArgument.cs} (91%) rename src/CSF.Core/{Abstractions/Components/IParameterContainer.cs => Reflection/IArgumentBucket.cs} (86%) rename src/CSF.Core/{Abstractions/Components/IConditionalComponent.cs => Reflection/IConditional.cs} (85%) rename src/CSF.Core/{Abstractions/Components/IComponent.cs => Reflection/INameable.cs} (87%) rename src/CSF.Core/{Components/Parameter.cs => Reflection/Impl/ArgumentInfo.cs} (88%) rename src/CSF.Core/{Components/Command.cs => Reflection/Impl/CommandInfo.cs} (85%) rename src/CSF.Core/{Components/ComplexParameter.cs => Reflection/Impl/ComplexArgumentInfo.cs} (81%) rename src/CSF.Core/{Components/Module.cs => Reflection/Impl/ModuleInfo.cs} (81%) rename src/CSF.Core/TypeReaders/{ => Impl}/BaseTypeReader.cs (97%) rename src/CSF.Core/TypeReaders/{ => Impl}/ColorTypeReader.cs (94%) rename src/CSF.Core/TypeReaders/{ => Impl}/EnumTypeReader.cs (89%) rename src/CSF.Core/TypeReaders/{ => Impl}/TimeSpanTypeReader.cs (94%) rename src/CSF.Core/{Abstractions => TypeReaders}/TypeReader.cs (61%) delete mode 100644 src/CSF.Tests.Console/TypeReaders/TimeOnlyTypeReader.cs diff --git a/src/CSF.Core/Abstractions/Contexts/ICommandContext.cs b/src/CSF.Core/Abstractions/Contexts/ICommandContext.cs deleted file mode 100644 index 1688d0a..0000000 --- a/src/CSF.Core/Abstractions/Contexts/ICommandContext.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace CSF -{ - /// - /// Represents an interface that supports all implementations of command context classes. - /// - public interface ICommandContext - { - /// - /// The command options. - /// - public IExecutionOptions Options { get; } - } -} diff --git a/src/CSF.Core/Attributes/DontRegisterAttribute.cs b/src/CSF.Core/Attributes/DontRegisterAttribute.cs deleted file mode 100644 index a2f8f42..0000000 --- a/src/CSF.Core/Attributes/DontRegisterAttribute.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace CSF -{ - /// - /// Represents an attribute that forces the registration to not register provided member. - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public sealed class DontRegisterAttribute : Attribute - { - - } -} diff --git a/src/CSF.Core/Attributes/RequireContextAttribute.cs b/src/CSF.Core/Attributes/RequireContextAttribute.cs deleted file mode 100644 index 380ae2f..0000000 --- a/src/CSF.Core/Attributes/RequireContextAttribute.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace CSF -{ - /// - /// Represents a precondition that checks if the provided context is valid. - /// - /// - /// Compiles a new from provided . - /// - public sealed class RequireContextAttribute() : PreconditionAttribute - { - /// - /// The context type to compare against. - /// - public Type ContextType { get; } = typeof(T); - - public override ValueTask EvaluateAsync(ICommandContext context, Command command) - { - var providedType = context.GetType(); - - if (providedType != ContextType) - return ValueTask.FromResult(new CheckResult(new CheckException($"Invalid context was passed into the command. Expected: '{ContextType.FullName}', got '{providedType.FullName}'"))); - - return ValueTask.FromResult(new CheckResult()); - } - } -} diff --git a/src/CSF.Core/CMBuilder.cs b/src/CSF.Core/CMBuilder.cs deleted file mode 100644 index c9a9547..0000000 --- a/src/CSF.Core/CMBuilder.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.Reflection; - -namespace CSF -{ - public sealed class CMBuilder - { - private bool _disposed = false; - - public HashSet Assemblies { get; set; } = [Assembly.GetEntryAssembly()]; - - public HashSet TypeReaders { get; set; } = []; - - public CMBuilder AddEntryAssembly() - { - if (_disposed) - ThrowHelpers.InvalidOp("This builder cannot be reused."); - - Assemblies.Add(Assembly.GetEntryAssembly()); - return this; - } - - public CMBuilder AddAssembly(Assembly assembly) - { - if (_disposed) - ThrowHelpers.InvalidOp("This builder cannot be reused."); - - Assemblies.Add(assembly); - return this; - } - - public CMBuilder AddTypeReader(TypeReader typeReader) - { - if (_disposed) - ThrowHelpers.InvalidOp("This builder cannot be reused."); - - TypeReaders.Add(typeReader); - return this; - } - - public CommandManager Build() - { - var typeReaders = TypeReader.CreateDefaultReaders().UnionBy(TypeReaders, x => x.Type).ToDictionary(x => x.Type, x => x); - - if (Assemblies.Count == 0) - ThrowHelpers.InvalidOp("An assembly has to be present in the builder prior to building the CommandManager."); - - IEnumerable BuildComponents() - { - var rootReader = typeof(TypeReader); - foreach (var assembly in Assemblies) - { - foreach (var type in assembly.GetTypes()) - { - if (rootReader.IsAssignableFrom(type) - && !type.IsAbstract - && !type.ContainsGenericParameters) - { - var reader = Activator.CreateInstance(type) as TypeReader; - - // replace existing typereader with replacement handler - if (!typeReaders.TryAdd(reader.Type, reader)) - typeReaders[reader.Type] = reader; - } - } - } - - var rootType = typeof(ModuleBase); - foreach (var assembly in Assemblies) - { - foreach (var type in assembly.GetTypes()) - { - if (rootType.IsAssignableFrom(type) - && !type.IsAbstract - && !type.ContainsGenericParameters) - { - yield return new Module(type, typeReaders); - } - } - } - } - - _disposed = true; - - return new(BuildComponents().SelectMany(x => x.Components), [.. Assemblies]); - } - } -} diff --git a/src/CSF.Core/Components/CommandCell.cs b/src/CSF.Core/Components/CommandCell.cs deleted file mode 100644 index be5a263..0000000 --- a/src/CSF.Core/Components/CommandCell.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace CSF -{ - /// - /// Represents a command resolved through the match and search pipeline. - /// - public readonly struct CommandCell - { - /// - /// Gets the command that this cell represents. - /// - public Command Command { get; } - - /// - /// Gets the arguments that are to be used to execute the method of this command. - /// - public object[] Arguments { get; } - - /// - /// Gets the exception that occurred while resolving this command. - /// - /// - /// if no exception occurred. - /// - public Exception Exception { get; } - - /// - /// Gets if the cell is invalid and cannot be executed. - /// - public bool IsInvalid { get; } - - /// - /// Creates a new that is constructed when resolvement of a command failed. - /// - /// - public CommandCell(Exception exception) - : this(null, null) - { - Command = null; - Arguments = null; - - Exception = exception; - IsInvalid = true; - } - - /// - /// Creates a new that is constructed when resolvement of a command succeeded. - /// - /// - /// - public CommandCell(Command match, object[] arguments) - { - Command = match; - Arguments = arguments; - - Exception = null; - IsInvalid = false; - } - } -} diff --git a/src/CSF.Core/Components/Constructor.cs b/src/CSF.Core/Components/Constructor.cs deleted file mode 100644 index ca971e1..0000000 --- a/src/CSF.Core/Components/Constructor.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Reflection; - -namespace CSF -{ - /// - /// Represents information about the primary constructor of a component. - /// - public class Constructor : IComponent - { - /// - public string Name { get; } - - /// - public Attribute[] Attributes { get; } - - /// - /// Gets the target constructor information that this constructor should call. - /// - public ConstructorInfo Target { get; } - - internal Constructor(Type type) - { - var target = type.GetConstructors()[0]; - - Attributes = target.GetAttributes(true); - Name = target.Name; - Target = target; - } - - /// - /// Formats the type into a readable signature. - /// - /// A string containing a readable signature. - public override string ToString() - => $"{Name}"; - } -} \ No newline at end of file diff --git a/src/CSF.Core/Contexts/CommandContext.cs b/src/CSF.Core/Contexts/CommandContext.cs deleted file mode 100644 index adc03fe..0000000 --- a/src/CSF.Core/Contexts/CommandContext.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace CSF -{ - public class CommandContext(T options) : CommandContext(options), ICommandContext - where T : IExecutionOptions - { - public new T Options { get; } = options; - - IExecutionOptions ICommandContext.Options { get; } = options; - } - - public class CommandContext(IExecutionOptions options) : ICommandContext - { - public IExecutionOptions Options { get; } = options; - } -} diff --git a/src/CSF.Core/Attributes/CommandAttribute.cs b/src/CSF.Core/Core/Attributes/CommandAttribute.cs similarity index 79% rename from src/CSF.Core/Attributes/CommandAttribute.cs rename to src/CSF.Core/Core/Attributes/CommandAttribute.cs index fc01fc2..3be9948 100644 --- a/src/CSF.Core/Attributes/CommandAttribute.cs +++ b/src/CSF.Core/Core/Attributes/CommandAttribute.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using CSF.Helpers; +using System.Diagnostics.CodeAnalysis; namespace CSF { @@ -18,20 +19,12 @@ public sealed class CommandAttribute : Attribute /// public string[] Aliases { get; } - /// - /// Sets up a new command attribute with the provided name. - /// - /// public CommandAttribute([DisallowNull] string name) : this(name, []) { } - /// - /// Sets up a new command attribute with the provided name and aliases. - /// - /// [CLSCompliant(false)] public CommandAttribute([DisallowNull] string name, params string[] aliases) { diff --git a/src/CSF.Core/Attributes/ComplexAttribute.cs b/src/CSF.Core/Core/Attributes/ComplexAttribute.cs similarity index 100% rename from src/CSF.Core/Attributes/ComplexAttribute.cs rename to src/CSF.Core/Core/Attributes/ComplexAttribute.cs diff --git a/src/CSF.Core/Attributes/DescriptionAttribute.cs b/src/CSF.Core/Core/Attributes/DescriptionAttribute.cs similarity index 93% rename from src/CSF.Core/Attributes/DescriptionAttribute.cs rename to src/CSF.Core/Core/Attributes/DescriptionAttribute.cs index a183501..89c3064 100644 --- a/src/CSF.Core/Attributes/DescriptionAttribute.cs +++ b/src/CSF.Core/Core/Attributes/DescriptionAttribute.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using CSF.Helpers; +using System.Diagnostics.CodeAnalysis; namespace CSF { diff --git a/src/CSF.Core/Attributes/GroupAttribute.cs b/src/CSF.Core/Core/Attributes/GroupAttribute.cs similarity index 96% rename from src/CSF.Core/Attributes/GroupAttribute.cs rename to src/CSF.Core/Core/Attributes/GroupAttribute.cs index f9a15ca..39506e2 100644 --- a/src/CSF.Core/Attributes/GroupAttribute.cs +++ b/src/CSF.Core/Core/Attributes/GroupAttribute.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using CSF.Helpers; +using System.Diagnostics.CodeAnalysis; namespace CSF { diff --git a/src/CSF.Core/Attributes/PrimaryConstructorAttribute.cs b/src/CSF.Core/Core/Attributes/PrimaryConstructorAttribute.cs similarity index 100% rename from src/CSF.Core/Attributes/PrimaryConstructorAttribute.cs rename to src/CSF.Core/Core/Attributes/PrimaryConstructorAttribute.cs diff --git a/src/CSF.Core/Attributes/PriorityAttribute.cs b/src/CSF.Core/Core/Attributes/PriorityAttribute.cs similarity index 100% rename from src/CSF.Core/Attributes/PriorityAttribute.cs rename to src/CSF.Core/Core/Attributes/PriorityAttribute.cs diff --git a/src/CSF.Core/Attributes/RemainderAttribute.cs b/src/CSF.Core/Core/Attributes/RemainderAttribute.cs similarity index 100% rename from src/CSF.Core/Attributes/RemainderAttribute.cs rename to src/CSF.Core/Core/Attributes/RemainderAttribute.cs diff --git a/src/CSF.Core/Core/CommandConfiguration.cs b/src/CSF.Core/Core/CommandConfiguration.cs new file mode 100644 index 0000000..20af470 --- /dev/null +++ b/src/CSF.Core/Core/CommandConfiguration.cs @@ -0,0 +1,12 @@ +using CSF.TypeReaders; +using System.Reflection; + +namespace CSF +{ + public sealed class CommandConfiguration + { + public Assembly[] Assemblies { get; set; } = [ Assembly.GetEntryAssembly() ]; + + public TypeReader[] TypeReaders { get; set; } = []; + } +} diff --git a/src/CSF.Core/CommandManager.cs b/src/CSF.Core/Core/CommandManager.cs similarity index 61% rename from src/CSF.Core/CommandManager.cs rename to src/CSF.Core/Core/CommandManager.cs index 256d13b..14ab88f 100644 --- a/src/CSF.Core/CommandManager.cs +++ b/src/CSF.Core/Core/CommandManager.cs @@ -1,39 +1,41 @@ -using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using CSF.TypeReaders; +using CSF.Helpers; +using CSF.Reflection; +using CSF.Exceptions; [assembly: CLSCompliant(true)] namespace CSF { - /// - /// The root type of the Command Standardization Framework (CSF). This type is responsible for setting up the execution pipeline, handling command input and managing modules. - /// - /// - /// Guides and documentation can be found at: - /// public sealed class CommandManager { - /// - /// Gets the components registered to this manager. - /// - public HashSet Components { get; } + public IServiceProvider Services { get; } - /// - /// Gets the assemblies used to register to this manager. - /// - public Assembly[] Assemblies { get; } + public IReadOnlySet Components { get; } - public CommandManager(IEnumerable components, Assembly[] assemblies) + public TypeReader[] TypeReaders { get; } + + public CommandConfiguration Configuration { get; } + + public CommandManager(IServiceProvider services, CommandConfiguration configuration) { - Components = components.ToHashSet(); - Assemblies = assemblies; + TypeReaders = configuration.TypeReaders.Distinct().ToArray(); + + if (configuration.Assemblies == null || configuration.Assemblies.Length == 0) + { + ThrowHelpers.ArgMissing(nameof(configuration.Assemblies)); + } + + Components = BuildComponents(configuration) + .SelectMany(x => x.Components) + .ToHashSet(); + + Services = services; + + Configuration = configuration; } - /// - /// - /// - /// - /// - /// public async ValueTask ExecuteAsync(ICommandContext context, params object[] args) { // search all relevant commands. @@ -60,11 +62,6 @@ public async ValueTask ExecuteAsync(ICommandContext context, params obj return fallback; } - /// - /// - /// - /// - /// public IEnumerable Search(object[] args) { // recursively search for commands in the execution. @@ -72,7 +69,7 @@ public IEnumerable Search(object[] args) } #region Matching - internal static async ValueTask MatchAsync(ICommandContext context, SearchResult search, object[] args) + private static async ValueTask MatchAsync(ICommandContext context, SearchResult search, object[] args) { // check command preconditions. var check = await CheckAsync(context, search.Command); @@ -101,7 +98,7 @@ internal static async ValueTask MatchAsync(ICommandContext context, #endregion #region Reading - internal static async ValueTask ReadAsync(ICommandContext context, SearchResult search, object[] args) + private static async ValueTask ReadAsync(ICommandContext context, SearchResult search, object[] args) { // skip if no parameters exist. if (!search.Command.HasParameters) @@ -128,7 +125,7 @@ internal static async ValueTask ReadAsync(ICommandContext context, #endregion #region Checking - internal static async ValueTask CheckAsync(ICommandContext context, Command command) + private static async ValueTask CheckAsync(ICommandContext context, CommandInfo command) { foreach (var precon in command.Preconditions) { @@ -142,14 +139,15 @@ internal static async ValueTask CheckAsync(ICommandContext context, #endregion #region Running - internal static async ValueTask RunAsync(ICommandContext context, MatchResult match) + private async ValueTask RunAsync(ICommandContext context, MatchResult match) { try { - var module = context.Options.Scope.ServiceProvider.GetService(match.Command.Module.Type) as ModuleBase; + var module = Services.GetService(match.Command.Module.Type) as ModuleBase; module.Context = context; module.Command = match.Command; + module.Services = Services; await module.BeforeExecuteAsync(); @@ -165,5 +163,40 @@ internal static async ValueTask RunAsync(ICommandContext context, Mat } } #endregion + + #region Building + private IEnumerable BuildComponents(CommandConfiguration configuration) + { + var typeReaders = TypeReader.CreateDefaultReaders().UnionBy(TypeReaders, x => x.Type).ToDictionary(x => x.Type, x => x); + + var rootType = typeof(ModuleBase); + foreach (var assembly in configuration.Assemblies) + { + foreach (var type in assembly.GetTypes()) + { + if (rootType.IsAssignableFrom(type) + && !type.IsAbstract + && !type.ContainsGenericParameters) + { + yield return new ModuleInfo(type, typeReaders); + } + } + } + } + #endregion + } + + public static class CollectionExtensions + { + public static IServiceCollection WithCommandManager(this IServiceCollection collection, Action action = null) + { + var context = new CommandConfiguration(); + + action?.Invoke(context); + + collection.AddCommandManager(context); + + return collection; + } } } diff --git a/src/CSF.Core/Core/Execution/ICommandContext.cs b/src/CSF.Core/Core/Execution/ICommandContext.cs new file mode 100644 index 0000000..b02d152 --- /dev/null +++ b/src/CSF.Core/Core/Execution/ICommandContext.cs @@ -0,0 +1,8 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace CSF +{ + public interface ICommandContext + { + } +} diff --git a/src/CSF.Core/Core/Execution/Impl/CommandContext.cs b/src/CSF.Core/Core/Execution/Impl/CommandContext.cs new file mode 100644 index 0000000..e0b04e7 --- /dev/null +++ b/src/CSF.Core/Core/Execution/Impl/CommandContext.cs @@ -0,0 +1,7 @@ +namespace CSF +{ + public class CommandContext : ICommandContext + { + + } +} diff --git a/src/CSF.Core/Abstractions/ModuleBase.cs b/src/CSF.Core/Core/Execution/ModuleBase.cs similarity index 91% rename from src/CSF.Core/Abstractions/ModuleBase.cs rename to src/CSF.Core/Core/Execution/ModuleBase.cs index 09df4bb..2640f64 100644 --- a/src/CSF.Core/Abstractions/ModuleBase.cs +++ b/src/CSF.Core/Core/Execution/ModuleBase.cs @@ -1,4 +1,6 @@ -namespace CSF +using CSF.Reflection; + +namespace CSF { /// /// Represents a registration and execution tool for modules. @@ -32,16 +34,12 @@ public abstract class ModuleBase /// /// Gets the command execution services as provided by the command scope. /// - public IServiceProvider Services - { - get - => Context.Options.Scope.ServiceProvider; - } + public IServiceProvider Services { get; internal set; } /// /// Gets the component that displays all information about the command thats currently in scope. /// - public Command Command { get; internal set; } + public CommandInfo Command { get; internal set; } /// /// Responds to the command with a message. diff --git a/src/CSF.Core/Abstractions/Results/IResult.cs b/src/CSF.Core/Core/Results/IResult.cs similarity index 100% rename from src/CSF.Core/Abstractions/Results/IResult.cs rename to src/CSF.Core/Core/Results/IResult.cs diff --git a/src/CSF.Core/Results/CheckResult.cs b/src/CSF.Core/Core/Results/Impl/CheckResult.cs similarity index 100% rename from src/CSF.Core/Results/CheckResult.cs rename to src/CSF.Core/Core/Results/Impl/CheckResult.cs diff --git a/src/CSF.Core/Results/MatchResult.cs b/src/CSF.Core/Core/Results/Impl/MatchResult.cs similarity index 66% rename from src/CSF.Core/Results/MatchResult.cs rename to src/CSF.Core/Core/Results/Impl/MatchResult.cs index 83bf250..89b26ff 100644 --- a/src/CSF.Core/Results/MatchResult.cs +++ b/src/CSF.Core/Core/Results/Impl/MatchResult.cs @@ -1,23 +1,25 @@ -namespace CSF +using CSF.Reflection; + +namespace CSF { public readonly struct MatchResult : IResult { public Exception Exception { get; } = null; - public Command Command { get; } + public CommandInfo Command { get; } public object[] Reads { get; } public bool Success { get; } - internal MatchResult(Command command, object[] reads) + internal MatchResult(CommandInfo command, object[] reads) { Command = command; Reads = reads; Success = true; } - internal MatchResult(Command command, Exception exception) + internal MatchResult(CommandInfo command, Exception exception) { Command = command; Reads = null; diff --git a/src/CSF.Core/Results/ReadResult.cs b/src/CSF.Core/Core/Results/Impl/ReadResult.cs similarity index 100% rename from src/CSF.Core/Results/ReadResult.cs rename to src/CSF.Core/Core/Results/Impl/ReadResult.cs diff --git a/src/CSF.Core/Results/RunResult.cs b/src/CSF.Core/Core/Results/Impl/RunResult.cs similarity index 66% rename from src/CSF.Core/Results/RunResult.cs rename to src/CSF.Core/Core/Results/Impl/RunResult.cs index 688e9a4..cac745c 100644 --- a/src/CSF.Core/Results/RunResult.cs +++ b/src/CSF.Core/Core/Results/Impl/RunResult.cs @@ -1,4 +1,6 @@ -namespace CSF +using CSF.Reflection; + +namespace CSF { public readonly struct RunResult : IResult { @@ -6,18 +8,18 @@ public object ReturnType { get; } = null; - public Command Command { get; } + public CommandInfo Command { get; } public bool Success { get; } - internal RunResult(Command command, Exception exception) + internal RunResult(CommandInfo command, Exception exception) { Exception = exception; Command = command; Success = false; } - internal RunResult(Command command, object returnValue) + internal RunResult(CommandInfo command, object returnValue) { ReturnType = returnValue; Command = command; diff --git a/src/CSF.Core/Results/SearchResult.cs b/src/CSF.Core/Core/Results/Impl/SearchResult.cs similarity index 63% rename from src/CSF.Core/Results/SearchResult.cs rename to src/CSF.Core/Core/Results/Impl/SearchResult.cs index 6bf7f47..a0605e0 100644 --- a/src/CSF.Core/Results/SearchResult.cs +++ b/src/CSF.Core/Core/Results/Impl/SearchResult.cs @@ -1,17 +1,22 @@ -namespace CSF +using CSF.Reflection; + +namespace CSF { public readonly struct SearchResult : IResult { public Exception Exception { get; } = null; - public Command Command { get; } + public CommandInfo Command { get; } + + public bool Success { get; } internal int SearchHeight { get; } - internal SearchResult(Command command, int srcHeight) + internal SearchResult(CommandInfo command, int srcHeight) { Command = command; SearchHeight = srcHeight; + Success = true; } internal SearchResult(Exception exception) @@ -19,6 +24,7 @@ internal SearchResult(Exception exception) Exception = exception; Command = null; SearchHeight = 0; + Success = false; } } } diff --git a/src/CSF.Core/Exceptions/Internal/ArgumentMissingException.cs b/src/CSF.Core/Exceptions/ArgumentMissingException.cs similarity index 84% rename from src/CSF.Core/Exceptions/Internal/ArgumentMissingException.cs rename to src/CSF.Core/Exceptions/ArgumentMissingException.cs index e57be6e..a78d6ed 100644 --- a/src/CSF.Core/Exceptions/Internal/ArgumentMissingException.cs +++ b/src/CSF.Core/Exceptions/ArgumentMissingException.cs @@ -1,4 +1,4 @@ -namespace CSF +namespace CSF.Exceptions { internal sealed class ArgumentMissingException(string paramName, string message) : ArgumentException(message, paramName) diff --git a/src/CSF.Core/Exceptions/CheckException.cs b/src/CSF.Core/Exceptions/CheckException.cs index 8b97621..7ed48d0 100644 --- a/src/CSF.Core/Exceptions/CheckException.cs +++ b/src/CSF.Core/Exceptions/CheckException.cs @@ -1,4 +1,4 @@ -namespace CSF +namespace CSF.Exceptions { /// /// Represents a that is thrown when no matched command succeeded its precondition checks. diff --git a/src/CSF.Core/Exceptions/CommandException.cs b/src/CSF.Core/Exceptions/CommandException.cs index 8ae1c6e..52b9658 100644 --- a/src/CSF.Core/Exceptions/CommandException.cs +++ b/src/CSF.Core/Exceptions/CommandException.cs @@ -1,4 +1,4 @@ -namespace CSF +namespace CSF.Exceptions { /// /// Represents a that is thrown when the command being executed failed to run its body. diff --git a/src/CSF.Core/Abstractions/ExecutionException.cs b/src/CSF.Core/Exceptions/ExecutionException.cs similarity index 77% rename from src/CSF.Core/Abstractions/ExecutionException.cs rename to src/CSF.Core/Exceptions/ExecutionException.cs index 05bffdc..609dc83 100644 --- a/src/CSF.Core/Abstractions/ExecutionException.cs +++ b/src/CSF.Core/Exceptions/ExecutionException.cs @@ -1,6 +1,6 @@ -namespace CSF +namespace CSF.Exceptions { - public abstract class ExecutionException : Exception + public class ExecutionException : Exception { public ExecutionException(string message) : base(message) diff --git a/src/CSF.Core/Exceptions/MatchException.cs b/src/CSF.Core/Exceptions/MatchException.cs index 8fd43e6..6dd0fae 100644 --- a/src/CSF.Core/Exceptions/MatchException.cs +++ b/src/CSF.Core/Exceptions/MatchException.cs @@ -1,4 +1,4 @@ -namespace CSF +namespace CSF.Exceptions { public class MatchException(string message, Exception innerException = null) : ExecutionException(message, innerException) diff --git a/src/CSF.Core/Exceptions/Internal/RangeDuplicateException.cs b/src/CSF.Core/Exceptions/RangeDuplicateException.cs similarity index 84% rename from src/CSF.Core/Exceptions/Internal/RangeDuplicateException.cs rename to src/CSF.Core/Exceptions/RangeDuplicateException.cs index 1a799ab..9d3d4ba 100644 --- a/src/CSF.Core/Exceptions/Internal/RangeDuplicateException.cs +++ b/src/CSF.Core/Exceptions/RangeDuplicateException.cs @@ -1,4 +1,4 @@ -namespace CSF +namespace CSF.Exceptions { internal sealed class RangeDuplicateException(string paramName, string message) : ArgumentException(paramName, message) diff --git a/src/CSF.Core/Exceptions/ReadException.cs b/src/CSF.Core/Exceptions/ReadException.cs index b0a3320..1901f6f 100644 --- a/src/CSF.Core/Exceptions/ReadException.cs +++ b/src/CSF.Core/Exceptions/ReadException.cs @@ -1,4 +1,4 @@ -namespace CSF +namespace CSF.Exceptions { /// /// Represents a that is thrown when no matched command succeeded parsing its parameters. diff --git a/src/CSF.Core/Exceptions/SearchException.cs b/src/CSF.Core/Exceptions/SearchException.cs index ac55443..64cbff9 100644 --- a/src/CSF.Core/Exceptions/SearchException.cs +++ b/src/CSF.Core/Exceptions/SearchException.cs @@ -1,4 +1,4 @@ -namespace CSF +namespace CSF.Exceptions { /// /// Represents a that is thrown when no command could be found. diff --git a/src/CSF.Core/ExecutionOptions.cs b/src/CSF.Core/ExecutionOptions.cs deleted file mode 100644 index e4fd49c..0000000 --- a/src/CSF.Core/ExecutionOptions.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace CSF -{ - public interface IExecutionOptions - { - /// - /// Gets or sets the service scope to be used in command execution. The services used for executing commands will be defaulted to the globally defined if this property is not set. - /// - public IServiceScope Scope { get; set; } - } - - /// - /// Represents a set of options that change the execution flow of the command handler. - /// - public class ExecutionOptions : IExecutionOptions - { - /// - /// Gets or sets the service scope to be used in command execution. The services used for executing commands will be defaulted to the globally defined if this property is not set. - /// - public IServiceScope Scope { get; set; } = null; - - public ExecutionOptions(IServiceProvider provider) - { - Scope = provider.CreateScope(); - } - } -} diff --git a/src/CSF.Core/Helpers/Internal/LinqHelpers.cs b/src/CSF.Core/Helpers/CollectionHelpers.cs similarity index 94% rename from src/CSF.Core/Helpers/Internal/LinqHelpers.cs rename to src/CSF.Core/Helpers/CollectionHelpers.cs index b692268..5527935 100644 --- a/src/CSF.Core/Helpers/Internal/LinqHelpers.cs +++ b/src/CSF.Core/Helpers/CollectionHelpers.cs @@ -1,8 +1,8 @@ using System.Collections; -namespace CSF +namespace CSF.Helpers { - internal static class LinqHelpers + internal static class CollectionHelpers { public static IEnumerable CastWhere(this IEnumerable input) { diff --git a/src/CSF.Core/CommandManagerHelper.cs b/src/CSF.Core/Helpers/ExecutionHelpers.cs similarity index 80% rename from src/CSF.Core/CommandManagerHelper.cs rename to src/CSF.Core/Helpers/ExecutionHelpers.cs index 6bd1f61..8048168 100644 --- a/src/CSF.Core/CommandManagerHelper.cs +++ b/src/CSF.Core/Helpers/ExecutionHelpers.cs @@ -1,8 +1,10 @@ -namespace CSF +using CSF.Reflection; + +namespace CSF.Helpers { - internal static class CommandManagerHelper + internal static class ExecutionHelpers { - public static IEnumerable RecursiveSearch(this IEnumerable components, object[] args, int searchHeight) + public static IEnumerable RecursiveSearch(this IEnumerable components, object[] args, int searchHeight) { List discovered = []; @@ -11,7 +13,7 @@ public static IEnumerable RecursiveSearch(this IEnumerable RecursiveSearch(this IEnumerable RecursiveSearch(this IEnumerable RecursiveReadAsync(this IParameterComponent[] param, ICommandContext context, object[] args, int index) + public static async Task RecursiveReadAsync(this IArgument[] param, ICommandContext context, object[] args, int index) { - - static async ValueTask ReadAsync(IParameterComponent param, ICommandContext context, object arg) + static async ValueTask ReadAsync(IArgument param, ICommandContext context, object arg) { if (arg.GetType() == param.Type) return new(arg); @@ -64,7 +65,7 @@ static async ValueTask ReadAsync(IParameterComponent param, ICommand continue; } - if (parameter is ComplexParameter complex) + if (parameter is ComplexArgumentInfo complex) { var result = await complex.Parameters.RecursiveReadAsync(context, args, index); @@ -74,7 +75,7 @@ static async ValueTask ReadAsync(IParameterComponent param, ICommand { try { - var obj = complex.Constructor.Target.Invoke(result.Select(x => x.Value).ToArray()); + var obj = complex.Constructor.Invoke(result.Select(x => x.Value).ToArray()); results[i] = new(obj); } catch (Exception ex) diff --git a/src/CSF.Core/Helpers/Internal/ReflectionHelpers.cs b/src/CSF.Core/Helpers/ReflectionHelpers.cs similarity index 60% rename from src/CSF.Core/Helpers/Internal/ReflectionHelpers.cs rename to src/CSF.Core/Helpers/ReflectionHelpers.cs index eac4ced..b753049 100644 --- a/src/CSF.Core/Helpers/Internal/ReflectionHelpers.cs +++ b/src/CSF.Core/Helpers/ReflectionHelpers.cs @@ -1,10 +1,13 @@ -using System.Reflection; +using CSF.TypeReaders; +using CSF.Reflection; +using System.Reflection; +using CSF.Preconditions; -namespace CSF +namespace CSF.Helpers { internal static class ReflectionHelpers { - private static IEnumerable GetModules(Module module, IDictionary typeReaders) + private static IEnumerable GetModules(ModuleInfo module, IDictionary typeReaders) { foreach (var group in module.Type.GetNestedTypes()) { @@ -12,13 +15,13 @@ private static IEnumerable GetModules(Module module, IDictionary GetCommands(Module module, IDictionary typeReaders) + private static IEnumerable GetCommands(ModuleInfo module, IDictionary typeReaders) { foreach (var method in module.Type.GetMethods()) { @@ -31,12 +34,6 @@ private static IEnumerable GetCommands(Module module, IDictionary GetCommands(Module module, IDictionary typeReaders) + public static IConditional[] GetComponents(this ModuleInfo module, IDictionary typeReaders) { - var commands = (IEnumerable)GetCommands(module, typeReaders) + var commands = (IEnumerable)GetCommands(module, typeReaders) .OrderBy(x => x.Parameters.Length); - var modules = (IEnumerable)GetModules(module, typeReaders) + var modules = (IEnumerable)GetModules(module, typeReaders) .OrderBy(x => x.Components.Length); return commands.Concat(modules) .ToArray(); } - public static IParameterComponent[] GetParameters(this MethodBase method, IDictionary typeReaders) + public static IArgument[] GetParameters(this MethodBase method, IDictionary typeReaders) { var parameters = method.GetParameters(); - var arr = new IParameterComponent[parameters.Length]; + var arr = new IArgument[parameters.Length]; for (int i = 0; i < parameters.Length; i++) { if (parameters[i].GetCustomAttributes().Any(x => x is ComplexAttribute)) { - arr[i] = new ComplexParameter(parameters[i], typeReaders); + arr[i] = new ComplexArgumentInfo(parameters[i], typeReaders); } else { - arr[i] = new Parameter(parameters[i], typeReaders); + arr[i] = new ArgumentInfo(parameters[i], typeReaders); } } @@ -86,20 +83,20 @@ public static PreconditionAttribute[] GetPreconditions(this Attribute[] attribut public static Attribute[] GetAttributes(this ICustomAttributeProvider provider, bool inherit) => provider.GetCustomAttributes(inherit).CastWhere().ToArray(); - public static Tuple GetLength(this IParameterComponent[] parameters) + public static Tuple GetLength(this IArgument[] parameters) { var minLength = 0; var maxLength = 0; foreach (var parameter in parameters) { - if (parameter is ComplexParameter complexParam) + if (parameter is ComplexArgumentInfo complexParam) { maxLength += complexParam.MaxLength; minLength += complexParam.MinLength; } - if (parameter is Parameter defaultParam) + if (parameter is ArgumentInfo defaultParam) { maxLength++; if (!defaultParam.IsOptional) diff --git a/src/CSF.Core/Helpers/ServiceHelper.cs b/src/CSF.Core/Helpers/ServiceHelper.cs deleted file mode 100644 index 465577a..0000000 --- a/src/CSF.Core/Helpers/ServiceHelper.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using System.ComponentModel; - -namespace CSF -{ - public static class ServiceHelper - { - /// - /// Includes inheriting into the this method is called on. - /// - /// The type inheriting to include in the collection. - /// - /// The configuration required to set up a new instance of . - /// The same for chained calls. - public static IServiceCollection WithCommandManager(this IServiceCollection collection, Action action = null) - { - var context = new CMBuilder(); - - action?.Invoke(context); - - collection.AddCommandManager(context); - - return collection; - } - - [EditorBrowsable(EditorBrowsableState.Never)] - public static IServiceCollection AddCommandManager(this IServiceCollection collection, CMBuilder configuration) - { - ModulesAddTransient(collection, configuration); - - var implementor = configuration.Build(); - - collection.TryAddSingleton(implementor); - - return collection; - } - - private static IServiceCollection ModulesAddTransient(IServiceCollection collection, CMBuilder configuration) - { - var rootType = typeof(ModuleBase); - - foreach (var assembly in configuration.Assemblies) - foreach (var type in assembly.GetTypes()) - if (rootType.IsAssignableFrom(type) && !type.IsAbstract && !type.ContainsGenericParameters) - collection.TryAddTransient(type); - - return collection; - } - } -} diff --git a/src/CSF.Core/Helpers/ServiceHelpers.cs b/src/CSF.Core/Helpers/ServiceHelpers.cs new file mode 100644 index 0000000..b928f64 --- /dev/null +++ b/src/CSF.Core/Helpers/ServiceHelpers.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace CSF.Helpers +{ + internal static class ServiceHelpers + { + public static IServiceCollection AddCommandManager(this IServiceCollection collection, CommandConfiguration configuration) + { + collection.ModulesAddTransient(configuration); + + collection.TryAddSingleton(configuration); + collection.TryAddSingleton(); + + return collection; + } + + public static IServiceCollection ModulesAddTransient(this IServiceCollection collection, CommandConfiguration configuration) + { + var rootType = typeof(ModuleBase); + + foreach (var assembly in configuration.Assemblies) + foreach (var type in assembly.GetTypes()) + if (rootType.IsAssignableFrom(type) && !type.IsAbstract && !type.ContainsGenericParameters) + collection.TryAddTransient(type); + + return collection; + } + } +} diff --git a/src/CSF.Core/Helpers/Internal/ThrowHelpers.cs b/src/CSF.Core/Helpers/ThrowHelpers.cs similarity index 90% rename from src/CSF.Core/Helpers/Internal/ThrowHelpers.cs rename to src/CSF.Core/Helpers/ThrowHelpers.cs index e039d61..2617c61 100644 --- a/src/CSF.Core/Helpers/Internal/ThrowHelpers.cs +++ b/src/CSF.Core/Helpers/ThrowHelpers.cs @@ -1,7 +1,8 @@ -using System.Diagnostics.CodeAnalysis; +using CSF.Exceptions; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -namespace CSF +namespace CSF.Helpers { internal static class ThrowHelpers { diff --git a/src/CSF.Core/Parsing/Impl/CharSpanParser.cs b/src/CSF.Core/Parsing/Impl/CharSpanParser.cs new file mode 100644 index 0000000..869679c --- /dev/null +++ b/src/CSF.Core/Parsing/Impl/CharSpanParser.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSF.Parsing +{ + public sealed class CharSpanParser : SpanParser + { + public override object[] Parse(Span value) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/CSF.Core/Parsing/Impl/StringParser.cs b/src/CSF.Core/Parsing/Impl/StringParser.cs new file mode 100644 index 0000000..df52e51 --- /dev/null +++ b/src/CSF.Core/Parsing/Impl/StringParser.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSF.Parsing +{ + public class StringParser : Parser + { + private static readonly SpanParser _spanParser = new CharSpanParser(); + + public override object[] Parse(string value) + { + + } + } +} diff --git a/src/CSF.Core/Parsing/Parser.cs b/src/CSF.Core/Parsing/Parser.cs index 9ee15c1..dbbed4f 100644 --- a/src/CSF.Core/Parsing/Parser.cs +++ b/src/CSF.Core/Parsing/Parser.cs @@ -1,9 +1,8 @@ -namespace CSF +namespace CSF.Parsing { - public abstract class Parser + public abstract class Parser + where T : IEquatable { - public static Parser Text { get; } = new TextParser(); - - public abstract ParserCell Parse(string rawInput); + public abstract object[] Parse(T value); } } diff --git a/src/CSF.Core/Parsing/ParserCell.cs b/src/CSF.Core/Parsing/ParserCell.cs deleted file mode 100644 index 618f089..0000000 --- a/src/CSF.Core/Parsing/ParserCell.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace CSF -{ - /// - /// Represents the information received from a successful operation. - /// - public readonly struct ParserCell - { - /// - /// The name of the command. - /// - public string Name { get; } - - /// - /// The parameters found through parsing. - /// - public string[] Parameters { get; } - - /// - /// The named parameters found through parsing. - /// - public IReadOnlyDictionary NamedParameters { get; } - - public ParserCell(string[] param, IReadOnlyDictionary namedParam = null) - { - Parameters = param[1..]; - NamedParameters = namedParam; - - Name = param[0]; - } - } -} diff --git a/src/CSF.Core/Parsing/SpanParser.cs b/src/CSF.Core/Parsing/SpanParser.cs new file mode 100644 index 0000000..239209e --- /dev/null +++ b/src/CSF.Core/Parsing/SpanParser.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSF.Parsing +{ + public abstract class SpanParser : Parser + where T : struct, IEquatable + { + [EditorBrowsable(EditorBrowsableState.Never)] + public override object[] Parse(T value) + { + throw new NotImplementedException(); + } + + public abstract object[] Parse(Span value); + } +} diff --git a/src/CSF.Core/Abstractions/PreconditionAttribute.cs b/src/CSF.Core/Preconditions/PreconditionAttribute.cs similarity index 89% rename from src/CSF.Core/Abstractions/PreconditionAttribute.cs rename to src/CSF.Core/Preconditions/PreconditionAttribute.cs index 61e71bd..b2c48e3 100644 --- a/src/CSF.Core/Abstractions/PreconditionAttribute.cs +++ b/src/CSF.Core/Preconditions/PreconditionAttribute.cs @@ -1,6 +1,9 @@ -using System.Diagnostics.CodeAnalysis; +using CSF.Reflection; +using CSF.Exceptions; +using CSF.Helpers; +using System.Diagnostics.CodeAnalysis; -namespace CSF +namespace CSF.Preconditions { /// /// Defines a precondition attribute. @@ -14,9 +17,8 @@ public abstract class PreconditionAttribute : Attribute /// /// The command context used to execute the command currently in scope. /// The command that is to be executed if this and all other precondition evaluations succeed. - /// The services in scope for the current command execution. /// A result that represents the outcome of the evaluation. - public abstract ValueTask EvaluateAsync(ICommandContext context, Command command); + public abstract ValueTask EvaluateAsync(ICommandContext context, CommandInfo command); public static CheckResult Error([DisallowNull] Exception exception) { diff --git a/src/CSF.Core/Abstractions/Components/IParameterComponent.cs b/src/CSF.Core/Reflection/IArgument.cs similarity index 91% rename from src/CSF.Core/Abstractions/Components/IParameterComponent.cs rename to src/CSF.Core/Reflection/IArgument.cs index 34453f0..da78714 100644 --- a/src/CSF.Core/Abstractions/Components/IParameterComponent.cs +++ b/src/CSF.Core/Reflection/IArgument.cs @@ -1,9 +1,11 @@ -namespace CSF +using CSF.TypeReaders; + +namespace CSF.Reflection { /// /// Represents a constructor or method parameter. /// - public interface IParameterComponent : IComponent + public interface IArgument : INameable { /// /// Gets the type of the parameter. diff --git a/src/CSF.Core/Abstractions/Components/IParameterContainer.cs b/src/CSF.Core/Reflection/IArgumentBucket.cs similarity index 86% rename from src/CSF.Core/Abstractions/Components/IParameterContainer.cs rename to src/CSF.Core/Reflection/IArgumentBucket.cs index 2c0e04e..87d9c46 100644 --- a/src/CSF.Core/Abstractions/Components/IParameterContainer.cs +++ b/src/CSF.Core/Reflection/IArgumentBucket.cs @@ -1,14 +1,14 @@ -namespace CSF +namespace CSF.Reflection { /// /// Represents a container that holds and handles parameters. /// - public interface IParameterContainer + public interface IArgumentBucket { /// /// Gets a list of parameters for this container. /// - public IParameterComponent[] Parameters { get; } + public IArgument[] Parameters { get; } /// /// Gets if this container contains any parameters or not. diff --git a/src/CSF.Core/Abstractions/Components/IConditionalComponent.cs b/src/CSF.Core/Reflection/IConditional.cs similarity index 85% rename from src/CSF.Core/Abstractions/Components/IConditionalComponent.cs rename to src/CSF.Core/Reflection/IConditional.cs index 9a226c3..4171dcd 100644 --- a/src/CSF.Core/Abstractions/Components/IConditionalComponent.cs +++ b/src/CSF.Core/Reflection/IConditional.cs @@ -1,9 +1,11 @@ -namespace CSF +using CSF.Preconditions; + +namespace CSF.Reflection { /// /// Represents a component with preconditions available. /// - public interface IConditionalComponent : IComponent + public interface IConditional : INameable { /// /// Gets the aliases of this component. diff --git a/src/CSF.Core/Abstractions/Components/IComponent.cs b/src/CSF.Core/Reflection/INameable.cs similarity index 87% rename from src/CSF.Core/Abstractions/Components/IComponent.cs rename to src/CSF.Core/Reflection/INameable.cs index 8098c7a..dfee656 100644 --- a/src/CSF.Core/Abstractions/Components/IComponent.cs +++ b/src/CSF.Core/Reflection/INameable.cs @@ -1,9 +1,9 @@ -namespace CSF +namespace CSF.Reflection { /// /// Represents any part of a command. /// - public interface IComponent + public interface INameable { /// /// Gets the name of the component in question. diff --git a/src/CSF.Core/Components/Parameter.cs b/src/CSF.Core/Reflection/Impl/ArgumentInfo.cs similarity index 88% rename from src/CSF.Core/Components/Parameter.cs rename to src/CSF.Core/Reflection/Impl/ArgumentInfo.cs index 0312fee..10ab12a 100644 --- a/src/CSF.Core/Components/Parameter.cs +++ b/src/CSF.Core/Reflection/Impl/ArgumentInfo.cs @@ -1,11 +1,13 @@ -using System.Reflection; +using CSF.Helpers; +using CSF.TypeReaders; +using System.Reflection; -namespace CSF +namespace CSF.Reflection { /// /// Represents a single parameter for the method. /// - public sealed class Parameter : IParameterComponent + public sealed class ArgumentInfo : IArgument { /// public string Name { get; } @@ -31,7 +33,7 @@ public sealed class Parameter : IParameterComponent /// public TypeReader TypeReader { get; } - internal Parameter(ParameterInfo parameterInfo, IDictionary typeReaders) + internal ArgumentInfo(ParameterInfo parameterInfo, IDictionary typeReaders) { var underlying = Nullable.GetUnderlyingType(parameterInfo.ParameterType); var attributes = parameterInfo.GetAttributes(false); diff --git a/src/CSF.Core/Components/Command.cs b/src/CSF.Core/Reflection/Impl/CommandInfo.cs similarity index 85% rename from src/CSF.Core/Components/Command.cs rename to src/CSF.Core/Reflection/Impl/CommandInfo.cs index b7f2aa5..40cee11 100644 --- a/src/CSF.Core/Components/Command.cs +++ b/src/CSF.Core/Reflection/Impl/CommandInfo.cs @@ -1,11 +1,14 @@ -using System.Reflection; +using CSF.Helpers; +using CSF.Preconditions; +using CSF.TypeReaders; +using System.Reflection; -namespace CSF +namespace CSF.Reflection { /// /// Represents the information required to execute commands. /// - public sealed class Command : IConditionalComponent, IParameterContainer + public sealed class CommandInfo : IConditional, IArgumentBucket { /// public string Name { get; } @@ -20,7 +23,7 @@ public sealed class Command : IConditionalComponent, IParameterContainer public bool HasPreconditions { get; } /// - public IParameterComponent[] Parameters { get; } + public IArgument[] Parameters { get; } /// public bool HasParameters { get; } @@ -45,14 +48,14 @@ public sealed class Command : IConditionalComponent, IParameterContainer /// /// Gets the module this command is declared in. /// - public Module Module { get; } + public ModuleInfo Module { get; } /// /// Gets the target method this command is aimed to execute. /// public MethodInfo Target { get; } - internal Command(Module module, MethodInfo method, string[] aliases, IDictionary typeReaders) + internal CommandInfo(ModuleInfo module, MethodInfo method, string[] aliases, IDictionary typeReaders) { var attributes = method.GetAttributes(true); var preconditions = attributes.GetPreconditions(); @@ -93,6 +96,6 @@ internal Command(Module module, MethodInfo method, string[] aliases, IDictionary /// /// A string containing a readable signature. public override string ToString() - => $"{Module}.{Target.Name}['{Name}']({string.Join(", ", Parameters)})"; + => $"{Module}.{Target.Name}['{Name}']({string.Join(", ", Parameters)})"; } } diff --git a/src/CSF.Core/Components/ComplexParameter.cs b/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs similarity index 81% rename from src/CSF.Core/Components/ComplexParameter.cs rename to src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs index a1051ab..ed70bff 100644 --- a/src/CSF.Core/Components/ComplexParameter.cs +++ b/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs @@ -1,11 +1,13 @@ -using System.Reflection; +using CSF.Helpers; +using CSF.TypeReaders; +using System.Reflection; -namespace CSF +namespace CSF.Reflection { /// /// Represents a complex parameter, containing a number of its own parameters. /// - public class ComplexParameter : IParameterComponent, IParameterContainer + public class ComplexArgumentInfo : IArgument, IArgumentBucket { /// public string Name { get; } @@ -29,7 +31,7 @@ public class ComplexParameter : IParameterComponent, IParameterContainer public Attribute[] Attributes { get; } /// - public IParameterComponent[] Parameters { get; } + public IArgument[] Parameters { get; } /// public bool HasParameters { get; } @@ -46,9 +48,9 @@ public class ComplexParameter : IParameterComponent, IParameterContainer /// /// Gets the constructor that constructs this complex parameter. /// - public Constructor Constructor { get; } + public ConstructorInfo Constructor { get; } - internal ComplexParameter(ParameterInfo parameterInfo, IDictionary typeReaders) + internal ComplexArgumentInfo(ParameterInfo parameterInfo, IDictionary typeReaders) { var underlying = Nullable.GetUnderlyingType(parameterInfo.ParameterType); var attributes = parameterInfo.GetAttributes(false); @@ -69,8 +71,8 @@ internal ComplexParameter(ParameterInfo parameterInfo, IDictionary 0) { @@ -100,6 +102,6 @@ internal ComplexParameter(ParameterInfo parameterInfo, IDictionary /// A string containing a readable signature. public override string ToString() - => $"{Type.Name} ({string.Join(", ", Parameters)}) {Name}"; + => $"{Type.Name} ({string.Join(", ", Parameters)}) {Name}"; } } diff --git a/src/CSF.Core/Components/Module.cs b/src/CSF.Core/Reflection/Impl/ModuleInfo.cs similarity index 81% rename from src/CSF.Core/Components/Module.cs rename to src/CSF.Core/Reflection/Impl/ModuleInfo.cs index e8016cb..b8ccde2 100644 --- a/src/CSF.Core/Components/Module.cs +++ b/src/CSF.Core/Reflection/Impl/ModuleInfo.cs @@ -1,9 +1,13 @@ -namespace CSF +using CSF.Helpers; +using CSF.Preconditions; +using CSF.TypeReaders; + +namespace CSF.Reflection { /// /// Represents information about the module this command is executed in. /// - public sealed class Module : IConditionalComponent + public sealed class ModuleInfo : IConditional { /// public string Name { get; } @@ -23,7 +27,7 @@ public sealed class Module : IConditionalComponent /// /// The components of this module. /// - public IConditionalComponent[] Components { get; } + public IConditional[] Components { get; } /// /// Gets the type of this module. @@ -36,9 +40,9 @@ public sealed class Module : IConditionalComponent /// /// This property is if no root is specified for this module. /// - public Module Root { get; } + public ModuleInfo Root { get; } - internal Module(Type type, IDictionary typeReaders, Module root = null, string expectedName = null, string[] aliases = null) + internal ModuleInfo(Type type, IDictionary typeReaders, ModuleInfo root = null, string expectedName = null, string[] aliases = null) { var attributes = type.GetAttributes(true); var preconditions = attributes.GetPreconditions(); diff --git a/src/CSF.Core/TypeReaders/BaseTypeReader.cs b/src/CSF.Core/TypeReaders/Impl/BaseTypeReader.cs similarity index 97% rename from src/CSF.Core/TypeReaders/BaseTypeReader.cs rename to src/CSF.Core/TypeReaders/Impl/BaseTypeReader.cs index 410ef7f..a1570b4 100644 --- a/src/CSF.Core/TypeReaders/BaseTypeReader.cs +++ b/src/CSF.Core/TypeReaders/Impl/BaseTypeReader.cs @@ -1,4 +1,6 @@ -namespace CSF +using CSF.Reflection; + +namespace CSF.TypeReaders { internal class BaseTypeReader : TypeReader { @@ -6,7 +8,7 @@ internal class BaseTypeReader : TypeReader private readonly static Lazy> _container = new(ValueGenerator); - public override ValueTask EvaluateAsync(ICommandContext context, IParameterComponent parameter, string value) + public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value) { var parser = _container.Value[Type] as Tpd; diff --git a/src/CSF.Core/TypeReaders/ColorTypeReader.cs b/src/CSF.Core/TypeReaders/Impl/ColorTypeReader.cs similarity index 94% rename from src/CSF.Core/TypeReaders/ColorTypeReader.cs rename to src/CSF.Core/TypeReaders/Impl/ColorTypeReader.cs index 7c09f20..f53cbd2 100644 --- a/src/CSF.Core/TypeReaders/ColorTypeReader.cs +++ b/src/CSF.Core/TypeReaders/Impl/ColorTypeReader.cs @@ -1,9 +1,10 @@ -using System.Drawing; +using CSF.Reflection; +using System.Drawing; using System.Globalization; using System.Reflection; using System.Text; -namespace CSF +namespace CSF.TypeReaders { internal class ColorTypeReader : TypeReader { @@ -45,7 +46,7 @@ public ColorTypeReader() _spacedColors = spacedNames; } - public override ValueTask EvaluateAsync(ICommandContext context, IParameterComponent parameter, string value) + public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value) { if (int.TryParse(value.Replace("#", "").Replace("0x", ""), NumberStyles.HexNumber, null, out var hexNumber)) return ValueTask.FromResult(Success(Color.FromArgb(hexNumber))); diff --git a/src/CSF.Core/TypeReaders/EnumTypeReader.cs b/src/CSF.Core/TypeReaders/Impl/EnumTypeReader.cs similarity index 89% rename from src/CSF.Core/TypeReaders/EnumTypeReader.cs rename to src/CSF.Core/TypeReaders/Impl/EnumTypeReader.cs index 8a881ab..3f1d258 100644 --- a/src/CSF.Core/TypeReaders/EnumTypeReader.cs +++ b/src/CSF.Core/TypeReaders/Impl/EnumTypeReader.cs @@ -1,4 +1,6 @@ -namespace CSF +using CSF.Reflection; + +namespace CSF.TypeReaders { internal class EnumTypeReader(Type targetEnumType) : TypeReader { @@ -6,7 +8,7 @@ internal class EnumTypeReader(Type targetEnumType) : TypeReader public override Type Type { get; } = targetEnumType; - public override ValueTask EvaluateAsync(ICommandContext context, IParameterComponent parameter, string value) + public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value) { if (Enum.TryParse(Type, value, true, out var result)) return ValueTask.FromResult(Success(result)); diff --git a/src/CSF.Core/TypeReaders/TimeSpanTypeReader.cs b/src/CSF.Core/TypeReaders/Impl/TimeSpanTypeReader.cs similarity index 94% rename from src/CSF.Core/TypeReaders/TimeSpanTypeReader.cs rename to src/CSF.Core/TypeReaders/Impl/TimeSpanTypeReader.cs index 4a018f5..8587351 100644 --- a/src/CSF.Core/TypeReaders/TimeSpanTypeReader.cs +++ b/src/CSF.Core/TypeReaders/Impl/TimeSpanTypeReader.cs @@ -1,6 +1,7 @@ -using System.Text.RegularExpressions; +using CSF.Reflection; +using System.Text.RegularExpressions; -namespace CSF +namespace CSF.TypeReaders { internal partial class TimeSpanTypeReader : TypeReader { @@ -33,7 +34,7 @@ public TimeSpanTypeReader() }; } - public override ValueTask EvaluateAsync(ICommandContext context, IParameterComponent parameter, string value) + public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value) { if (!TimeSpan.TryParse(value, out TimeSpan span)) { diff --git a/src/CSF.Core/Abstractions/TypeReader.cs b/src/CSF.Core/TypeReaders/TypeReader.cs similarity index 61% rename from src/CSF.Core/Abstractions/TypeReader.cs rename to src/CSF.Core/TypeReaders/TypeReader.cs index 8b7cf28..98a0a03 100644 --- a/src/CSF.Core/Abstractions/TypeReader.cs +++ b/src/CSF.Core/TypeReaders/TypeReader.cs @@ -1,40 +1,28 @@ -using System.Diagnostics.CodeAnalysis; +using CSF.Reflection; +using CSF.Exceptions; +using CSF.Helpers; +using System.Diagnostics.CodeAnalysis; -namespace CSF +namespace CSF.TypeReaders { - /// - /// Represents a generic to use for parsing provided types into the targetted type. - /// - /// The targetted type for this typereader. public abstract class TypeReader : TypeReader { /// public override Type Type { get; } = typeof(T); /// - public override abstract ValueTask EvaluateAsync(ICommandContext context, IParameterComponent parameter, string value); + public override abstract ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value); } public abstract class TypeReader { private static readonly string _exHeader = "TypeReader failed to parse provided value as '{0}'. View inner exception for more details."; - /// - /// The type that this reader intends to return. - /// public abstract Type Type { get; } - /// - /// Evaluates an input and tries to parse it into a value that matches the expected parameter type. - /// - /// The command context used to execute the command currently in scope. - /// The parameter this input evaluation is targetting. - /// The services in scope for the current command execution. - /// The input that this evaluation intends to convert into the expected parameter type. - /// A result that represents the outcome of the evaluation. - public abstract ValueTask EvaluateAsync(ICommandContext context, IParameterComponent parameter, string value); - - internal ValueTask ObjectEvaluateAsync(ICommandContext context, IParameterComponent parameter, object value) + public abstract ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value); + + internal ValueTask ObjectEvaluateAsync(ICommandContext context, IArgument parameter, object value) { if (value.GetType() == Type) return ValueTask.FromResult(new ReadResult(value)); diff --git a/src/CSF.Tests.Console/TypeReaders/TimeOnlyTypeReader.cs b/src/CSF.Tests.Console/TypeReaders/TimeOnlyTypeReader.cs deleted file mode 100644 index 2b6d1b0..0000000 --- a/src/CSF.Tests.Console/TypeReaders/TimeOnlyTypeReader.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace CSF.Tests -{ - public class TimeOnlyTypeReader : TypeReader - { - public override Result Evaluate(ICommandContext context, IParameterComponent parameter, IServiceProvider services, string value) - { - return Success(TimeOnly.MinValue); - } - } -} From c415fcb2cb8335347245c27b8f42c567b2e83147 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Thu, 25 Jan 2024 22:15:59 +0100 Subject: [PATCH 07/40] new ignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9491a2f..5fccc8a 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,5 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd +/Visual Studio 2022/Visualizers From 0ed35f3652bcb5a58f1ee861d07295856edc7ff3 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Sat, 27 Jan 2024 20:33:52 +0100 Subject: [PATCH 08/40] Introduce functional parsing, stringgen --- .../CSF.Benchmark.Parsing.csproj | 18 ++++ CSF.Benchmark.Parsing/Program.cs | 22 +++++ CSF.sln | 24 ++--- src/CSF.Core/Parsing/Impl/CharSpanParser.cs | 16 ---- src/CSF.Core/Parsing/Impl/StringParser.cs | 93 +++++++++++++++++-- .../Parsing/Implementation/TextParser.cs | 85 ----------------- src/CSF.Core/Parsing/SpanParser.cs | 21 ----- src/CSF.Tests.Console/Program.cs | 38 +++++--- 8 files changed, 161 insertions(+), 156 deletions(-) create mode 100644 CSF.Benchmark.Parsing/CSF.Benchmark.Parsing.csproj create mode 100644 CSF.Benchmark.Parsing/Program.cs delete mode 100644 src/CSF.Core/Parsing/Impl/CharSpanParser.cs delete mode 100644 src/CSF.Core/Parsing/Implementation/TextParser.cs delete mode 100644 src/CSF.Core/Parsing/SpanParser.cs diff --git a/CSF.Benchmark.Parsing/CSF.Benchmark.Parsing.csproj b/CSF.Benchmark.Parsing/CSF.Benchmark.Parsing.csproj new file mode 100644 index 0000000..5508c60 --- /dev/null +++ b/CSF.Benchmark.Parsing/CSF.Benchmark.Parsing.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + diff --git a/CSF.Benchmark.Parsing/Program.cs b/CSF.Benchmark.Parsing/Program.cs new file mode 100644 index 0000000..ed3ebae --- /dev/null +++ b/CSF.Benchmark.Parsing/Program.cs @@ -0,0 +1,22 @@ + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; +using CSF.Parsing; + +[MemoryDiagnoser] +public class Program +{ + private static readonly StringParser _parser = new(); + + [Params("command", "a larger command with context", "a massive command \"with quotes\" and several additional 1 22 333 4444 5555")] + public string Text { get; set; } + + static void Main() + => BenchmarkRunner.Run(); + + [Benchmark] + public void ParseText() + { + _parser.Parse(Text); + } +} \ No newline at end of file diff --git a/CSF.sln b/CSF.sln index 996467e..56979a7 100644 --- a/CSF.sln +++ b/CSF.sln @@ -21,7 +21,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSF.Tests.Hosting", "src\CS EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSF.Samples.Hosting", "examples\CSF.Samples.Hosting\CSF.Samples.Hosting.csproj", "{D99DCD40-8A42-42F5-8385-88EDF87057CC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSF.Tests.Benchmarks", "src\CSF.Tests.Benchmarks\CSF.Tests.Benchmarks.csproj", "{0A3A4351-DD9A-4E25-9CA7-8CFFA5A3EE7B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSF.Tests.Benchmarks", "src\CSF.Tests.Benchmarks\CSF.Tests.Benchmarks.csproj", "{0A3A4351-DD9A-4E25-9CA7-8CFFA5A3EE7B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Benchmarks", "Benchmarks", "{7EB2ED0C-26A8-4F51-81CD-A0AB13B739C9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSF.Benchmark.Parsing", "CSF.Benchmark.Parsing\CSF.Benchmark.Parsing.csproj", "{6ABD1982-481C-4006-B830-997837E2B24D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -41,22 +45,10 @@ Global {10058F39-52CB-44D6-AD8B-597F34B50B45}.Debug|Any CPU.Build.0 = Debug|Any CPU {10058F39-52CB-44D6-AD8B-597F34B50B45}.Release|Any CPU.ActiveCfg = Release|Any CPU {10058F39-52CB-44D6-AD8B-597F34B50B45}.Release|Any CPU.Build.0 = Release|Any CPU - {B868198E-67C3-4AFB-8D19-48034E1156FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B868198E-67C3-4AFB-8D19-48034E1156FC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B868198E-67C3-4AFB-8D19-48034E1156FC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B868198E-67C3-4AFB-8D19-48034E1156FC}.Release|Any CPU.Build.0 = Release|Any CPU {19F7A07B-7349-4AA5-A9CC-58F1002DFED6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {19F7A07B-7349-4AA5-A9CC-58F1002DFED6}.Debug|Any CPU.Build.0 = Debug|Any CPU {19F7A07B-7349-4AA5-A9CC-58F1002DFED6}.Release|Any CPU.ActiveCfg = Release|Any CPU {19F7A07B-7349-4AA5-A9CC-58F1002DFED6}.Release|Any CPU.Build.0 = Release|Any CPU - {CA734A9C-2E3A-4E61-8CD0-0476AD88F94C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CA734A9C-2E3A-4E61-8CD0-0476AD88F94C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CA734A9C-2E3A-4E61-8CD0-0476AD88F94C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CA734A9C-2E3A-4E61-8CD0-0476AD88F94C}.Release|Any CPU.Build.0 = Release|Any CPU - {924925E1-0AE5-4B5A-B736-EB09EF5BDB60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {924925E1-0AE5-4B5A-B736-EB09EF5BDB60}.Debug|Any CPU.Build.0 = Debug|Any CPU - {924925E1-0AE5-4B5A-B736-EB09EF5BDB60}.Release|Any CPU.ActiveCfg = Release|Any CPU - {924925E1-0AE5-4B5A-B736-EB09EF5BDB60}.Release|Any CPU.Build.0 = Release|Any CPU {BD637AD8-B1B6-4C4E-9C28-FEF0B2824B20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BD637AD8-B1B6-4C4E-9C28-FEF0B2824B20}.Debug|Any CPU.Build.0 = Debug|Any CPU {BD637AD8-B1B6-4C4E-9C28-FEF0B2824B20}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -73,6 +65,10 @@ Global {0A3A4351-DD9A-4E25-9CA7-8CFFA5A3EE7B}.Debug|Any CPU.Build.0 = Debug|Any CPU {0A3A4351-DD9A-4E25-9CA7-8CFFA5A3EE7B}.Release|Any CPU.ActiveCfg = Release|Any CPU {0A3A4351-DD9A-4E25-9CA7-8CFFA5A3EE7B}.Release|Any CPU.Build.0 = Release|Any CPU + {6ABD1982-481C-4006-B830-997837E2B24D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6ABD1982-481C-4006-B830-997837E2B24D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6ABD1982-481C-4006-B830-997837E2B24D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6ABD1982-481C-4006-B830-997837E2B24D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -80,11 +76,11 @@ Global GlobalSection(NestedProjects) = preSolution {A70E8C5D-0209-433B-9AB7-9C05C0DA04E3} = {DA6771C2-541A-46E0-AF1A-B4256FF4CB5E} {10058F39-52CB-44D6-AD8B-597F34B50B45} = {DA6771C2-541A-46E0-AF1A-B4256FF4CB5E} - {B868198E-67C3-4AFB-8D19-48034E1156FC} = {01CCF11A-2D95-44C9-81EA-EE7D6A36FE7A} {19F7A07B-7349-4AA5-A9CC-58F1002DFED6} = {01CCF11A-2D95-44C9-81EA-EE7D6A36FE7A} {DC2807A7-F45F-422F-A9CB-75E1CC0A9978} = {DA6771C2-541A-46E0-AF1A-B4256FF4CB5E} {D99DCD40-8A42-42F5-8385-88EDF87057CC} = {01CCF11A-2D95-44C9-81EA-EE7D6A36FE7A} {0A3A4351-DD9A-4E25-9CA7-8CFFA5A3EE7B} = {DA6771C2-541A-46E0-AF1A-B4256FF4CB5E} + {6ABD1982-481C-4006-B830-997837E2B24D} = {7EB2ED0C-26A8-4F51-81CD-A0AB13B739C9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C9CE5996-C6A9-442C-8808-490D2FA61458} diff --git a/src/CSF.Core/Parsing/Impl/CharSpanParser.cs b/src/CSF.Core/Parsing/Impl/CharSpanParser.cs deleted file mode 100644 index 869679c..0000000 --- a/src/CSF.Core/Parsing/Impl/CharSpanParser.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CSF.Parsing -{ - public sealed class CharSpanParser : SpanParser - { - public override object[] Parse(Span value) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/CSF.Core/Parsing/Impl/StringParser.cs b/src/CSF.Core/Parsing/Impl/StringParser.cs index df52e51..b5b4bbc 100644 --- a/src/CSF.Core/Parsing/Impl/StringParser.cs +++ b/src/CSF.Core/Parsing/Impl/StringParser.cs @@ -1,18 +1,95 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Text; namespace CSF.Parsing { public class StringParser : Parser { - private static readonly SpanParser _spanParser = new CharSpanParser(); + const char quote = '"'; + const char whitespace = ' '; - public override object[] Parse(string value) + public override object[] Parse(string toParse) { - + var arr = Array.Empty(); + var sb = new StringBuilder(0, toParse.Length); + var quoted = false; + + // Adds SB content to array & resets + void SAddReset() + { + // if anything exists, otherwise skip + if (sb.Length > 0) + { + var size = arr.Length; + Array.Resize(ref arr, size + 1); + + arr[size] = sb.ToString(); + + // clear for next range + sb.Clear(); + } + } + + // enter loop for string inner char[] + for (int i = 0; i < toParse.Length; i++) + { + // if startquote found, skip space check & continue until next occurrence of quote + if (quoted) + { + // next quote occurrence + if (toParse[i] is quote) + { + // add discovered until now, skipping quote itself + SAddReset(); + + // set quoted to false, quoted range is handled + quoted = false; + + // next loop step + continue; + } + + // add char in quote range + sb.Append(toParse[i]); + + // dont allow the checks below this statement, next loop step + continue; + } + + // check for startquote + if (toParse[i] is quote) + { + // check end of loop, skipping add + if (i + 1 == toParse.Length) + { + break; + } + + // add all before quote + SAddReset(); + + // set startquote discovery to true + quoted = true; + + continue; + } + + // check for whitespace + if (toParse[i] is whitespace) + { + // add all before whitespace, skip whitespace itself + SAddReset(); + + continue; + } + + // nomatch for above, add character to current range + sb.Append(toParse[i]); + } + + // if loop ended, do final add + SAddReset(); + + return arr; } } } diff --git a/src/CSF.Core/Parsing/Implementation/TextParser.cs b/src/CSF.Core/Parsing/Implementation/TextParser.cs deleted file mode 100644 index 6087543..0000000 --- a/src/CSF.Core/Parsing/Implementation/TextParser.cs +++ /dev/null @@ -1,85 +0,0 @@ -namespace CSF -{ - /// - /// Represents a parser for text command input. - /// - public class TextParser : Parser - { - public override ParserCell Parse(string rawInput) - { - var splitInput = rawInput.Split(' '); - - var hasName = false; - - var param = new List(); - var partial = new List(); - - var paramName = ""; - var namedParam = new Dictionary(); - - foreach (var part in splitInput) - { - if (!hasName) - { - param.Add(part); - continue; - } - - if (partial.Any()) - { - if (part.EndsWith("\"")) - { - partial.Add(part.Replace("\"", "")); - - if (paramName is "") - param.Add(string.Join(" ", partial)); - else - { - namedParam.Add(paramName, string.Join(" ", partial)); - paramName = ""; - } - - partial.Clear(); - continue; - } - partial.Add(part); - continue; - } - - if (part.StartsWith('"')) - { - if (part.EndsWith('"')) - { - if (paramName is "") - param.Add(part.Replace("\"", "")); - else - { - namedParam.Add(paramName, part.Replace("\"", "")); - paramName = ""; - } - } - else - partial.Add(part.Replace("\"", "")); - continue; - } - - if (part.StartsWith("-")) - foreach (var c in part[1..]) - namedParam.Add(c.ToString(), null); - - if (part.StartsWith("--")) - { - if (!part.EndsWith(":")) - namedParam.Add(part[1..], null!); - else - paramName = part[1..^1]; - continue; - } - - param.Add(part); - } - - return new(param.ToArray(), namedParam); - } - } -} diff --git a/src/CSF.Core/Parsing/SpanParser.cs b/src/CSF.Core/Parsing/SpanParser.cs deleted file mode 100644 index 239209e..0000000 --- a/src/CSF.Core/Parsing/SpanParser.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CSF.Parsing -{ - public abstract class SpanParser : Parser - where T : struct, IEquatable - { - [EditorBrowsable(EditorBrowsableState.Never)] - public override object[] Parse(T value) - { - throw new NotImplementedException(); - } - - public abstract object[] Parse(Span value); - } -} diff --git a/src/CSF.Tests.Console/Program.cs b/src/CSF.Tests.Console/Program.cs index 601ec70..68ba3ad 100644 --- a/src/CSF.Tests.Console/Program.cs +++ b/src/CSF.Tests.Console/Program.cs @@ -1,20 +1,20 @@ -using CSF; -using Microsoft.Extensions.DependencyInjection; +//using CSF; +//using Microsoft.Extensions.DependencyInjection; -var collection = new ServiceCollection() - .WithCommandManager(); +//var collection = new ServiceCollection() +// .WithCommandManager(); -var services = collection.BuildServiceProvider(); +//var services = collection.BuildServiceProvider(); -var framework = services.GetRequiredService(); +//var framework = services.GetRequiredService(); -var delayed = new CommandContext("delayed"); -var direct = new CommandContext("direct"); +//var delayed = new CommandContext("delayed"); +//var direct = new CommandContext("direct"); -framework.ExecuteAsync(delayed); -framework.ExecuteAsync(direct); +//framework.ExecuteAsync(delayed); +//framework.ExecuteAsync(direct); -await Task.Delay(Timeout.Infinite); +//await Task.Delay(Timeout.Infinite); //while (true) //{ @@ -24,4 +24,18 @@ // if (result.Failed(out var failure)) // Console.WriteLine(failure.Exception); -//} \ No newline at end of file +//} + +using CSF.Parsing; + +var parser = new StringParser(); + +while (true) +{ + var text = Console.ReadLine(); + + var value = parser.Parse(text); + + foreach (var item in value) + Console.WriteLine("-> " + item); +} \ No newline at end of file From ed71ed813788fb5f943a0bf35ac150d7fc06e69f Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Sat, 27 Jan 2024 22:37:57 +0100 Subject: [PATCH 09/40] Upgrades to hosting extensions, logging support --- src/CSF.Core/Core/CommandManager.cs | 39 +++++------ src/CSF.Core/Core/CommandManagerResolver.cs | 41 ++++++++++++ .../Core/Execution/ICommandContext.cs | 11 ++++ .../Core/Execution/Impl/CommandContext.cs | 34 +++++++++- src/CSF.Core/Core/Execution/ModuleBase.cs | 47 +++++-------- src/CSF.Core/Exceptions/MatchException.cs | 2 +- src/CSF.Core/Helpers/ServiceHelpers.cs | 10 --- .../Preconditions/PreconditionAttribute.cs | 10 +-- src/CSF.Core/Reflection/IArgument.cs | 28 ++------ src/CSF.Core/Reflection/IArgumentBucket.cs | 20 ++---- src/CSF.Core/Reflection/IConditional.cs | 16 ++--- src/CSF.Core/Reflection/INameable.cs | 12 +--- src/CSF.Core/Reflection/Impl/ArgumentInfo.cs | 16 +---- src/CSF.Core/Reflection/Impl/CommandInfo.cs | 41 +++++------- .../Reflection/Impl/ComplexArgumentInfo.cs | 37 +++++------ src/CSF.Core/Reflection/Impl/ModuleInfo.cs | 34 +++------- src/CSF.Core/TypeReaders/TypeReader.cs | 3 +- .../Core/Execution/IContextFactory.cs | 24 +++++++ .../Execution/Impl/HostedCommandContext.cs | 55 ++++++++++++++++ .../Execution/Impl/HostedContextFactory.cs | 34 ++++++++++ .../Core/Execution/Impl/HostedModuleBase.cs | 19 ++++++ src/CSF.Hosting/Core/HostedCommandManager.cs | 31 +++++++++ .../Core/HostedCommandManagerResolver.cs | 44 +++++++++++++ src/CSF.Hosting/Helpers/HostBuilderHelper.cs | 48 -------------- src/CSF.Hosting/Helpers/ServiceHelpers.cs | 12 ++++ src/CSF.Hosting/HostedCommandManager.cs | 66 ------------------- 26 files changed, 399 insertions(+), 335 deletions(-) create mode 100644 src/CSF.Core/Core/CommandManagerResolver.cs create mode 100644 src/CSF.Hosting/Core/Execution/IContextFactory.cs create mode 100644 src/CSF.Hosting/Core/Execution/Impl/HostedCommandContext.cs create mode 100644 src/CSF.Hosting/Core/Execution/Impl/HostedContextFactory.cs create mode 100644 src/CSF.Hosting/Core/Execution/Impl/HostedModuleBase.cs create mode 100644 src/CSF.Hosting/Core/HostedCommandManager.cs create mode 100644 src/CSF.Hosting/Core/HostedCommandManagerResolver.cs delete mode 100644 src/CSF.Hosting/Helpers/HostBuilderHelper.cs create mode 100644 src/CSF.Hosting/Helpers/ServiceHelpers.cs delete mode 100644 src/CSF.Hosting/HostedCommandManager.cs diff --git a/src/CSF.Core/Core/CommandManager.cs b/src/CSF.Core/Core/CommandManager.cs index 14ab88f..70f5908 100644 --- a/src/CSF.Core/Core/CommandManager.cs +++ b/src/CSF.Core/Core/CommandManager.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; -using CSF.TypeReaders; +using CSF.TypeReaders; using CSF.Helpers; using CSF.Reflection; using CSF.Exceptions; @@ -8,7 +7,7 @@ namespace CSF { - public sealed class CommandManager + public class CommandManager { public IServiceProvider Services { get; } @@ -36,7 +35,7 @@ public CommandManager(IServiceProvider services, CommandConfiguration configurat Configuration = configuration; } - public async ValueTask ExecuteAsync(ICommandContext context, params object[] args) + public virtual async Task ExecuteAsync(ICommandContext context, params object[] args) { // search all relevant commands. var searches = Search(args); @@ -57,26 +56,26 @@ public async ValueTask ExecuteAsync(ICommandContext context, params obj } if (!fallback.HasValue) - return new SearchResult(new SearchException("")); // TODO + return new SearchResult(new SearchException("No command was found with the provided input.")); return fallback; } - public IEnumerable Search(object[] args) + public virtual IEnumerable Search(object[] args) { // recursively search for commands in the execution. return Components.RecursiveSearch(args, 0); } #region Matching - private static async ValueTask MatchAsync(ICommandContext context, SearchResult search, object[] args) + private async ValueTask MatchAsync(ICommandContext context, SearchResult search, object[] args) { // check command preconditions. var check = await CheckAsync(context, search.Command); // verify check success, if not, return the failure. if (!check.Success) - return new(search.Command, new MatchException("", check.Exception)); // TODO + return new(search.Command, new MatchException("Command failed to reach execution state. View inner exception for more details.", check.Exception)); // read the command parameters in right order. var readResult = await ReadAsync(context, search, args); @@ -98,8 +97,10 @@ private static async ValueTask MatchAsync(ICommandContext context, #endregion #region Reading - private static async ValueTask ReadAsync(ICommandContext context, SearchResult search, object[] args) + private async ValueTask ReadAsync(ICommandContext context, SearchResult search, object[] args) { + context.LogDebug("Attempting argument conversion for {}", search.Command); + // skip if no parameters exist. if (!search.Command.HasParameters) return []; @@ -125,8 +126,10 @@ private static async ValueTask ReadAsync(ICommandContext context, #endregion #region Checking - private static async ValueTask CheckAsync(ICommandContext context, CommandInfo command) + private async ValueTask CheckAsync(ICommandContext context, CommandInfo command) { + context.LogDebug("Attempting validations for {}", command); + foreach (var precon in command.Preconditions) { var result = await precon.EvaluateAsync(context, command); @@ -143,6 +146,8 @@ private async ValueTask RunAsync(ICommandContext context, MatchResult { try { + context.LogInformation("Executing {} with {} resolved arguments.", match.Command, match.Reads.Length); + var module = Services.GetService(match.Command.Module.Type) as ModuleBase; module.Context = context; @@ -185,18 +190,4 @@ private IEnumerable BuildComponents(CommandConfiguration configurati } #endregion } - - public static class CollectionExtensions - { - public static IServiceCollection WithCommandManager(this IServiceCollection collection, Action action = null) - { - var context = new CommandConfiguration(); - - action?.Invoke(context); - - collection.AddCommandManager(context); - - return collection; - } - } } diff --git a/src/CSF.Core/Core/CommandManagerResolver.cs b/src/CSF.Core/Core/CommandManagerResolver.cs new file mode 100644 index 0000000..a17f189 --- /dev/null +++ b/src/CSF.Core/Core/CommandManagerResolver.cs @@ -0,0 +1,41 @@ +using CSF.Helpers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + +namespace CSF.Core +{ + public static class CommandManagerResolver + { + public static IServiceCollection WithCommands(this IServiceCollection collection, [DisallowNull] Action action) + { + collection.WithCommands(action); + + return collection; + } + + public static IServiceCollection WithCommands(this IServiceCollection collection, [DisallowNull] Action action) + where T : CommandManager + { + var cmdConf = new CommandConfiguration(); + + action(cmdConf); + + collection.WithCommands(cmdConf); + + return collection; + } + + public static IServiceCollection WithCommands(this IServiceCollection collection, CommandConfiguration configuration) + where T : CommandManager + { + collection.ModulesAddTransient(configuration); + + collection.TryAddSingleton(configuration); + collection.TryAddSingleton(); + + return collection; + } + } +} diff --git a/src/CSF.Core/Core/Execution/ICommandContext.cs b/src/CSF.Core/Core/Execution/ICommandContext.cs index b02d152..0bed802 100644 --- a/src/CSF.Core/Core/Execution/ICommandContext.cs +++ b/src/CSF.Core/Core/Execution/ICommandContext.cs @@ -4,5 +4,16 @@ namespace CSF { public interface ICommandContext { + public void LogTrace(string message, params object[] args); + + public void LogDebug(string message, params object[] args); + + public void LogInformation(string message, params object[] args); + + public void LogWarning(string message, params object[] args); + + public void LogError(string message, params object[] args); + + public void LogCritical(string message, params object[] args); } } diff --git a/src/CSF.Core/Core/Execution/Impl/CommandContext.cs b/src/CSF.Core/Core/Execution/Impl/CommandContext.cs index e0b04e7..6ba3685 100644 --- a/src/CSF.Core/Core/Execution/Impl/CommandContext.cs +++ b/src/CSF.Core/Core/Execution/Impl/CommandContext.cs @@ -1,7 +1,37 @@ -namespace CSF +using CSF.Reflection; + +namespace CSF { - public class CommandContext : ICommandContext + public abstract class CommandContext : ICommandContext { + public virtual void LogCritical(string message, params object[] args) + { + + } + + public virtual void LogDebug(string message, params object[] args) + { + + } + + public virtual void LogError(string message, params object[] args) + { + + } + + public virtual void LogInformation(string message, params object[] args) + { + + } + + public virtual void LogTrace(string message, params object[] args) + { + + } + public virtual void LogWarning(string message, params object[] args) + { + + } } } diff --git a/src/CSF.Core/Core/Execution/ModuleBase.cs b/src/CSF.Core/Core/Execution/ModuleBase.cs index 2640f64..ce96cd8 100644 --- a/src/CSF.Core/Core/Execution/ModuleBase.cs +++ b/src/CSF.Core/Core/Execution/ModuleBase.cs @@ -2,18 +2,11 @@ namespace CSF { - /// - /// Represents a registration and execution tool for modules. - /// - /// The expected to use for this module. public abstract class ModuleBase : ModuleBase where T : ICommandContext { private T _context; - /// - /// Gets the command execution context containing value about the currently executed command. - /// public new T Context { get @@ -21,46 +14,42 @@ public abstract class ModuleBase : ModuleBase } } - /// - /// Represents a registration and execution tool for modules. - /// public abstract class ModuleBase { - /// - /// Gets the command execution context containing value about the currently executed command. - /// public ICommandContext Context { get; internal set; } - /// - /// Gets the command execution services as provided by the command scope. - /// public IServiceProvider Services { get; internal set; } - /// - /// Gets the component that displays all information about the command thats currently in scope. - /// public CommandInfo Command { get; internal set; } - /// - /// Responds to the command with a message. - /// - /// The message to send. - public virtual void Respond(string message) - => Console.WriteLine(message); - - public virtual RunResult ReturnTypeResolve(object value) + internal virtual RunResult ReturnTypeResolve(object value) { switch (value) { case Task task: return new(Command, task); case null: - return new(Command, null); + return new(Command, returnValue: null); default: - throw new NotSupportedException($"The return value of the command in question is not supported. Consider overriding {nameof(ReturnTypeResolve)} to add your own return type resolver."); + { + var result = HandleUnknownReturnType(value); + + if (!result.Success) + { + Context.LogWarning("{} returned unknown type. Consider overriding {} to resolve this message.", Command, nameof(HandleUnknownReturnType)); + return new(Command, returnValue: value); + } + + return result; + } } } + public virtual RunResult HandleUnknownReturnType(object value) + { + return new RunResult(Command, exception: null); + } + public virtual ValueTask BeforeExecuteAsync() { return ValueTask.CompletedTask; diff --git a/src/CSF.Core/Exceptions/MatchException.cs b/src/CSF.Core/Exceptions/MatchException.cs index 6dd0fae..cea2c86 100644 --- a/src/CSF.Core/Exceptions/MatchException.cs +++ b/src/CSF.Core/Exceptions/MatchException.cs @@ -3,6 +3,6 @@ public class MatchException(string message, Exception innerException = null) : ExecutionException(message, innerException) { - + private const string _exHeader = "Command failed to reach execution state. View inner exception for more details."; } } diff --git a/src/CSF.Core/Helpers/ServiceHelpers.cs b/src/CSF.Core/Helpers/ServiceHelpers.cs index b928f64..c1c0cdb 100644 --- a/src/CSF.Core/Helpers/ServiceHelpers.cs +++ b/src/CSF.Core/Helpers/ServiceHelpers.cs @@ -5,16 +5,6 @@ namespace CSF.Helpers { internal static class ServiceHelpers { - public static IServiceCollection AddCommandManager(this IServiceCollection collection, CommandConfiguration configuration) - { - collection.ModulesAddTransient(configuration); - - collection.TryAddSingleton(configuration); - collection.TryAddSingleton(); - - return collection; - } - public static IServiceCollection ModulesAddTransient(this IServiceCollection collection, CommandConfiguration configuration) { var rootType = typeof(ModuleBase); diff --git a/src/CSF.Core/Preconditions/PreconditionAttribute.cs b/src/CSF.Core/Preconditions/PreconditionAttribute.cs index b2c48e3..0ea3453 100644 --- a/src/CSF.Core/Preconditions/PreconditionAttribute.cs +++ b/src/CSF.Core/Preconditions/PreconditionAttribute.cs @@ -5,19 +5,11 @@ namespace CSF.Preconditions { - /// - /// Defines a precondition attribute. - /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] public abstract class PreconditionAttribute : Attribute { private static readonly string _exHeader = "Precondition result halted further command execution. View inner exception for more details."; - /// - /// Evaluates a condition to handle a command and returns the result. - /// - /// The command context used to execute the command currently in scope. - /// The command that is to be executed if this and all other precondition evaluations succeed. - /// A result that represents the outcome of the evaluation. + public abstract ValueTask EvaluateAsync(ICommandContext context, CommandInfo command); public static CheckResult Error([DisallowNull] Exception exception) diff --git a/src/CSF.Core/Reflection/IArgument.cs b/src/CSF.Core/Reflection/IArgument.cs index da78714..d264599 100644 --- a/src/CSF.Core/Reflection/IArgument.cs +++ b/src/CSF.Core/Reflection/IArgument.cs @@ -2,39 +2,25 @@ namespace CSF.Reflection { - /// - /// Represents a constructor or method parameter. - /// + public interface IArgument : INameable { - /// - /// Gets the type of the parameter. - /// + public Type Type { get; } - /// - /// Gets the type that is exposed to the runtime directly, potentially being nullable. - /// + public Type ExposedType { get; } - /// - /// Gets if the parameter is nullable. - /// + public bool IsNullable { get; } - /// - /// Gets if the parameter is optional. - /// + public bool IsOptional { get; } - /// - /// Gets if the parameter is remainder. - /// + public bool IsRemainder { get; } - /// - /// Gets the typereader responsible for parsing this type. - /// + public TypeReader TypeReader { get; } } } diff --git a/src/CSF.Core/Reflection/IArgumentBucket.cs b/src/CSF.Core/Reflection/IArgumentBucket.cs index 87d9c46..0662874 100644 --- a/src/CSF.Core/Reflection/IArgumentBucket.cs +++ b/src/CSF.Core/Reflection/IArgumentBucket.cs @@ -1,28 +1,18 @@ namespace CSF.Reflection { - /// - /// Represents a container that holds and handles parameters. - /// + public interface IArgumentBucket { - /// - /// Gets a list of parameters for this container. - /// + public IArgument[] Parameters { get; } - /// - /// Gets if this container contains any parameters or not. - /// + public bool HasParameters { get; } - /// - /// Gets the minimum required length to use a command. - /// + public int MinLength { get; } - /// - /// Gets the optimal length to use a command. If remainder is specified, the count will be set to infinity. - /// + public int MaxLength { get; } } } diff --git a/src/CSF.Core/Reflection/IConditional.cs b/src/CSF.Core/Reflection/IConditional.cs index 4171dcd..1c9ac1e 100644 --- a/src/CSF.Core/Reflection/IConditional.cs +++ b/src/CSF.Core/Reflection/IConditional.cs @@ -2,24 +2,16 @@ namespace CSF.Reflection { - /// - /// Represents a component with preconditions available. - /// + public interface IConditional : INameable { - /// - /// Gets the aliases of this component. - /// + public string[] Aliases { get; } - /// - /// Gets the preconditions of this component. - /// + public PreconditionAttribute[] Preconditions { get; } - /// - /// Gets if this component has any preconditions. - /// + public bool HasPreconditions { get; } } } diff --git a/src/CSF.Core/Reflection/INameable.cs b/src/CSF.Core/Reflection/INameable.cs index dfee656..5d46b9a 100644 --- a/src/CSF.Core/Reflection/INameable.cs +++ b/src/CSF.Core/Reflection/INameable.cs @@ -1,18 +1,12 @@ namespace CSF.Reflection { - /// - /// Represents any part of a command. - /// + public interface INameable { - /// - /// Gets the name of the component in question. - /// + public string Name { get; } - /// - /// Gets the attribute collection for this component. - /// + public Attribute[] Attributes { get; } } } diff --git a/src/CSF.Core/Reflection/Impl/ArgumentInfo.cs b/src/CSF.Core/Reflection/Impl/ArgumentInfo.cs index 10ab12a..cc41ec9 100644 --- a/src/CSF.Core/Reflection/Impl/ArgumentInfo.cs +++ b/src/CSF.Core/Reflection/Impl/ArgumentInfo.cs @@ -4,33 +4,23 @@ namespace CSF.Reflection { - /// - /// Represents a single parameter for the method. - /// + public sealed class ArgumentInfo : IArgument { - /// public string Name { get; } - /// public Type Type { get; } - /// public Type ExposedType { get; } - /// public bool IsNullable { get; } - /// public bool IsOptional { get; } - /// public bool IsRemainder { get; } - /// public Attribute[] Attributes { get; } - /// public TypeReader TypeReader { get; } internal ArgumentInfo(ParameterInfo parameterInfo, IDictionary typeReaders) @@ -70,10 +60,6 @@ internal ArgumentInfo(ParameterInfo parameterInfo, IDictionary Name = parameterInfo.Name; } - /// - /// Formats the type into a readable signature. - /// - /// A string containing a readable signature. public override string ToString() => $"{Type.Name} {Name}"; } diff --git a/src/CSF.Core/Reflection/Impl/CommandInfo.cs b/src/CSF.Core/Reflection/Impl/CommandInfo.cs index 40cee11..a27207a 100644 --- a/src/CSF.Core/Reflection/Impl/CommandInfo.cs +++ b/src/CSF.Core/Reflection/Impl/CommandInfo.cs @@ -5,54 +5,46 @@ namespace CSF.Reflection { - /// - /// Represents the information required to execute commands. - /// + public sealed class CommandInfo : IConditional, IArgumentBucket { - /// + public string Name { get; } - /// + public Attribute[] Attributes { get; } - /// + public PreconditionAttribute[] Preconditions { get; } - /// + public bool HasPreconditions { get; } - /// + public IArgument[] Parameters { get; } - /// + public bool HasParameters { get; } - /// + public bool HasRemainder { get; } - /// + public int MinLength { get; } - /// + public int MaxLength { get; } - /// + public string[] Aliases { get; } - /// - /// Represents the priority of a command. - /// + public byte Priority { get; } - /// - /// Gets the module this command is declared in. - /// + public ModuleInfo Module { get; } - /// - /// Gets the target method this command is aimed to execute. - /// + public MethodInfo Target { get; } internal CommandInfo(ModuleInfo module, MethodInfo method, string[] aliases, IDictionary typeReaders) @@ -91,10 +83,7 @@ internal CommandInfo(ModuleInfo module, MethodInfo method, string[] aliases, IDi MaxLength = maxLength; } - /// - /// Formats the type into a readable signature. - /// - /// A string containing a readable signature. + public override string ToString() => $"{Module}.{Target.Name}['{Name}']({string.Join(", ", Parameters)})"; } diff --git a/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs b/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs index ed70bff..2c2e3f6 100644 --- a/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs +++ b/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs @@ -4,50 +4,46 @@ namespace CSF.Reflection { - /// - /// Represents a complex parameter, containing a number of its own parameters. - /// + public class ComplexArgumentInfo : IArgument, IArgumentBucket { - /// + public string Name { get; } - /// + public Type Type { get; } - /// + public Type ExposedType { get; } - /// + public bool IsNullable { get; } - /// + public bool IsOptional { get; } - /// + public bool IsRemainder { get; } - /// + public Attribute[] Attributes { get; } - /// + public IArgument[] Parameters { get; } - /// + public bool HasParameters { get; } - /// + public int MinLength { get; } - /// + public int MaxLength { get; } - /// + public TypeReader TypeReader { get; } - /// - /// Gets the constructor that constructs this complex parameter. - /// + public ConstructorInfo Constructor { get; } internal ComplexArgumentInfo(ParameterInfo parameterInfo, IDictionary typeReaders) @@ -97,10 +93,7 @@ internal ComplexArgumentInfo(ParameterInfo parameterInfo, IDictionary - /// Formats the type into a readable signature. - /// - /// A string containing a readable signature. + public override string ToString() => $"{Type.Name} ({string.Join(", ", Parameters)}) {Name}"; } diff --git a/src/CSF.Core/Reflection/Impl/ModuleInfo.cs b/src/CSF.Core/Reflection/Impl/ModuleInfo.cs index b8ccde2..723ac77 100644 --- a/src/CSF.Core/Reflection/Impl/ModuleInfo.cs +++ b/src/CSF.Core/Reflection/Impl/ModuleInfo.cs @@ -4,42 +4,31 @@ namespace CSF.Reflection { - /// - /// Represents information about the module this command is executed in. - /// + public sealed class ModuleInfo : IConditional { - /// + public string Name { get; } - /// + public string[] Aliases { get; } - /// + public Attribute[] Attributes { get; } - /// + public PreconditionAttribute[] Preconditions { get; } - /// + public bool HasPreconditions { get; } - /// - /// The components of this module. - /// + public IConditional[] Components { get; } - /// - /// Gets the type of this module. - /// + public Type Type { get; } - /// - /// Gets the root module of the current module. - /// - /// - /// This property is if no root is specified for this module. - /// + public ModuleInfo Root { get; } internal ModuleInfo(Type type, IDictionary typeReaders, ModuleInfo root = null, string expectedName = null, string[] aliases = null) @@ -60,10 +49,7 @@ internal ModuleInfo(Type type, IDictionary typeReaders, Module Aliases = aliases ?? [Name]; } - /// - /// Formats the type into a readable signature. - /// - /// A string containing a readable signature. + public override string ToString() => $"{(Root != null ? $"{Root}." : "")}{(Type.Name != Name ? $"{Type.Name}['{Name}']" : $"{Name}")}"; } diff --git a/src/CSF.Core/TypeReaders/TypeReader.cs b/src/CSF.Core/TypeReaders/TypeReader.cs index 98a0a03..951d76f 100644 --- a/src/CSF.Core/TypeReaders/TypeReader.cs +++ b/src/CSF.Core/TypeReaders/TypeReader.cs @@ -2,15 +2,14 @@ using CSF.Exceptions; using CSF.Helpers; using System.Diagnostics.CodeAnalysis; +using System.Diagnostics; namespace CSF.TypeReaders { public abstract class TypeReader : TypeReader { - /// public override Type Type { get; } = typeof(T); - /// public override abstract ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value); } diff --git a/src/CSF.Hosting/Core/Execution/IContextFactory.cs b/src/CSF.Hosting/Core/Execution/IContextFactory.cs new file mode 100644 index 0000000..5d0a360 --- /dev/null +++ b/src/CSF.Hosting/Core/Execution/IContextFactory.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSF.Hosting +{ + public interface IContextFactory : IDisposable + { + public ICommandContext CreateContext(); + } + + public interface IContextFactory : IContextFactory + where T : ICommandContext + { + public new T CreateContext(); + + ICommandContext IContextFactory.CreateContext() + { + return CreateContext(); + } + } +} diff --git a/src/CSF.Hosting/Core/Execution/Impl/HostedCommandContext.cs b/src/CSF.Hosting/Core/Execution/Impl/HostedCommandContext.cs new file mode 100644 index 0000000..5f6d458 --- /dev/null +++ b/src/CSF.Hosting/Core/Execution/Impl/HostedCommandContext.cs @@ -0,0 +1,55 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSF.Hosting +{ + public class HostedCommandContext : ICommandContext + { + internal Guid Id { get; } + + public ILogger Logger { get; } + + public HostedCommandContext(Guid id, ILogger logger) + { + Id = id; + Logger = logger; + } + + public void LogTrace(string message, params object[] args) + { + Logger.Log(LogLevel.Trace, message, args); + } + + public void LogDebug(string message, params object[] args) + { + Logger.Log(LogLevel.Debug, message, args); + } + + public void LogInformation(string message, params object[] args) + { + Logger.Log(LogLevel.Information, message, args); + } + + public void LogWarning(string message, params object[] args) + { + Logger.Log(LogLevel.Warning, message, args); + } + + public void LogError(string message, params object[] args) + { + Logger.Log(LogLevel.Error, message, args); + } + + public void LogCritical(string message, params object[] args) + { + Logger.Log(LogLevel.Critical, message, args); + } + + public override string ToString() + => $"HostedCommandContext[{Id}]"; + } +} diff --git a/src/CSF.Hosting/Core/Execution/Impl/HostedContextFactory.cs b/src/CSF.Hosting/Core/Execution/Impl/HostedContextFactory.cs new file mode 100644 index 0000000..4afb4cd --- /dev/null +++ b/src/CSF.Hosting/Core/Execution/Impl/HostedContextFactory.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSF.Hosting +{ + public class HostedContextFactory : IContextFactory + { + public ILoggerFactory LoggerFactory { get; } + + public HostedContextFactory(ILoggerFactory loggerFactory) + { + LoggerFactory = loggerFactory; + } + + public virtual HostedCommandContext CreateContext() + { + var guid = Guid.NewGuid(); + var logger = LoggerFactory.CreateLogger($"CSF.Command.Pipeline[{Guid.NewGuid()}]"); + + logger.LogTrace("Generating context with ID {}", guid); + + return new(guid, logger); + } + + public void Dispose() + { + + } + } +} diff --git a/src/CSF.Hosting/Core/Execution/Impl/HostedModuleBase.cs b/src/CSF.Hosting/Core/Execution/Impl/HostedModuleBase.cs new file mode 100644 index 0000000..96f8f23 --- /dev/null +++ b/src/CSF.Hosting/Core/Execution/Impl/HostedModuleBase.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSF.Hosting +{ + public class HostedModuleBase : ModuleBase + where T : HostedCommandContext + { + public ILogger Logger + { + get + => Context.Logger; + } + } +} diff --git a/src/CSF.Hosting/Core/HostedCommandManager.cs b/src/CSF.Hosting/Core/HostedCommandManager.cs new file mode 100644 index 0000000..2a0cb51 --- /dev/null +++ b/src/CSF.Hosting/Core/HostedCommandManager.cs @@ -0,0 +1,31 @@ +using CSF.Reflection; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSF.Hosting +{ + public class HostedCommandManager : CommandManager + { + public ILogger Logger { get; } + + public IContextFactory ContextFactory { get; } + + public HostedCommandManager(ILogger logger, IContextFactory factory, IServiceProvider services, CommandConfiguration configuration) + : base(services, configuration) + { + ContextFactory = factory; + Logger = logger; + } + + public Task ExecuteAsync(params object[] args) + { + var context = ContextFactory.CreateContext(); + + return base.ExecuteAsync(context, args); + } + } +} diff --git a/src/CSF.Hosting/Core/HostedCommandManagerResolver.cs b/src/CSF.Hosting/Core/HostedCommandManagerResolver.cs new file mode 100644 index 0000000..d3dc957 --- /dev/null +++ b/src/CSF.Hosting/Core/HostedCommandManagerResolver.cs @@ -0,0 +1,44 @@ +using CSF.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSF.Hosting +{ + public static class HostedCommandManagerResolver + { + public static IHostBuilder WithCommands(this IHostBuilder builder, [DisallowNull] Action configureDelegate) + { + return builder.WithCommands(configureDelegate); + } + + public static IHostBuilder WithCommands(this IHostBuilder builder, [DisallowNull] Action configureDelegate) + where T : HostedCommandManager + { + return builder.WithCommands(configureDelegate); + } + + public static IHostBuilder WithCommands(this IHostBuilder builder, [DisallowNull] Action configureDelegate) + where TManager : HostedCommandManager where TFactory : class, IContextFactory + { + builder.ConfigureServices((context, services) => + { + var config = new CommandConfiguration(); + + configureDelegate(context, config); + + services.WithCommands(config); + + services.AddScoped(); + }); + + return builder; + } + } +} diff --git a/src/CSF.Hosting/Helpers/HostBuilderHelper.cs b/src/CSF.Hosting/Helpers/HostBuilderHelper.cs deleted file mode 100644 index 71ea6c6..0000000 --- a/src/CSF.Hosting/Helpers/HostBuilderHelper.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace CSF.Hosting -{ - /// - /// Represents helper method for the . - /// - public static class HostBuilderHelper - { - /// - /// Configures the with a . - /// - /// - /// Configuration action for the host and manager. - /// The same for chaining calls. - public static IHostBuilder ConfigureCommandManager(this IHostBuilder hostBuilder, Action action = null) - { - hostBuilder.ConfigureCommandManager(action); - - return hostBuilder; - } - - /// - /// Configures the with a customized . - /// - /// The manager to bind to. - /// - /// Configuration action for the host and manager. - /// The same for chaining calls. - public static IHostBuilder ConfigureCommandManager(this IHostBuilder hostBuilder, Action action = null) - where T : HostedCommandManager - { - hostBuilder.ConfigureServices((hostContext, services) => - { - var fxContext = new CommandBuildingConfiguration(); - - action?.Invoke(hostContext, fxContext); - - services.AddComponents(fxContext); - - services.AddHostedService(); - }); - - return hostBuilder; - } - } -} diff --git a/src/CSF.Hosting/Helpers/ServiceHelpers.cs b/src/CSF.Hosting/Helpers/ServiceHelpers.cs new file mode 100644 index 0000000..71ed31c --- /dev/null +++ b/src/CSF.Hosting/Helpers/ServiceHelpers.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSF.Hosting.Helpers +{ + internal class ServiceHelpers + { + } +} diff --git a/src/CSF.Hosting/HostedCommandManager.cs b/src/CSF.Hosting/HostedCommandManager.cs deleted file mode 100644 index cb565c1..0000000 --- a/src/CSF.Hosting/HostedCommandManager.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace CSF -{ - /// - /// Represents a host-managed command manager. - /// - public class HostedCommandManager : CommandManager, IHostedService - { - /// - /// The logger used by the hosted manager. - /// - public ILogger Logger { get; } - - /// - /// The parser used by the hosted manager. - /// - public TextParser Parser { get; } - - public HostedCommandManager(IServiceProvider serviceProvider) - : this(serviceProvider.GetRequiredService>(), serviceProvider) - { - - } - - public HostedCommandManager(ILogger logger, IServiceProvider serviceProvider) : base(serviceProvider) - { - Logger = logger; - Parser = new TextParser(); - } - - public virtual Task StartAsync(CancellationToken cancellationToken) - { - Task.Run(async () => await RunAsync(cancellationToken), cancellationToken) - .ContinueWith(async _ => await StopAsync(cancellationToken)); - - return Task.CompletedTask; - } - - /// - /// Enters a loop through which commands are read and ran. - /// - /// - /// - /// - public virtual async Task RunAsync(CancellationToken cancellationToken) - { - while (true) - { - var context = new CommandContext(Console.ReadLine(), Parser); - - var result = ExecuteAsync(context, new ExecutionOptions()); - - if (result.Failed(out var failure)) - Logger.LogError(failure.Exception, "Command execution returned an exception."); - - await result; - } - } - - public virtual Task StopAsync(CancellationToken cancellationToken) - => Task.CompletedTask; - } -} From f658c68a5e458631935f32bc9dcacf8f26e26928 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Mon, 29 Jan 2024 21:04:09 +0100 Subject: [PATCH 10/40] Improvements to asynchronous execution approach --- .../Core/Attributes/CommandAttribute.cs | 2 +- .../Core/Attributes/ComplexAttribute.cs | 2 +- .../Core/Attributes/DescriptionAttribute.cs | 2 +- .../Core/Attributes/GroupAttribute.cs | 2 +- .../Attributes/PrimaryConstructorAttribute.cs | 2 +- .../Core/Attributes/PriorityAttribute.cs | 2 +- .../Core/Attributes/RemainderAttribute.cs | 2 +- src/CSF.Core/Core/CommandConfiguration.cs | 4 +- src/CSF.Core/Core/CommandManager.cs | 129 +++++++++++++----- src/CSF.Core/Core/CommandManagerResolver.cs | 1 - .../Core/Configuration/TaskAwaitOptions.cs | 17 +++ .../Core/Execution/ICommandContext.cs | 8 +- src/CSF.Core/Core/Execution/IResultHandler.cs | 13 ++ .../Core/Execution/Impl/CommandContext.cs | 32 ++++- src/CSF.Core/Core/Execution/ModuleBase.cs | 6 +- .../Results/{IResult.cs => ICommandResult.cs} | 6 +- src/CSF.Core/Core/Results/Impl/CheckResult.cs | 2 +- src/CSF.Core/Core/Results/Impl/MatchResult.cs | 2 +- src/CSF.Core/Core/Results/Impl/ReadResult.cs | 2 +- src/CSF.Core/Core/Results/Impl/RunResult.cs | 2 +- .../Core/Results/Impl/SearchResult.cs | 2 +- src/CSF.Core/Helpers/ExecutionHelpers.cs | 15 +- src/CSF.Core/Helpers/ReflectionHelpers.cs | 5 +- src/CSF.Core/Helpers/ServiceHelpers.cs | 5 +- src/CSF.Core/Helpers/ThrowHelpers.cs | 2 +- .../Preconditions/PreconditionAttribute.cs | 5 +- src/CSF.Core/Reflection/Impl/ArgumentInfo.cs | 3 +- src/CSF.Core/Reflection/Impl/CommandInfo.cs | 21 +-- .../TypeReaders/Impl/BaseTypeReader.cs | 5 +- .../TypeReaders/Impl/ColorTypeReader.cs | 5 +- .../TypeReaders/Impl/EnumTypeReader.cs | 5 +- .../TypeReaders/Impl/TimeSpanTypeReader.cs | 5 +- src/CSF.Core/TypeReaders/TypeReader.cs | 14 +- src/CSF.Hosting/CSF.Hosting.csproj | 3 + .../Core/Execution/IContextFactory.cs | 24 ---- .../Execution/Impl/HostedCommandContext.cs | 55 -------- .../Execution/Impl/HostedContextFactory.cs | 34 ----- src/CSF.Hosting/Core/HostedCommandManager.cs | 31 ----- src/CSF.Hosting/Helpers/ServiceHelpers.cs | 8 +- .../Hosting/Execution/IActionFactory.cs | 22 +++ .../Execution/Impl/HostedCommandContext.cs | 42 ++++++ .../Execution/Impl/HostedModuleBase.cs | 8 +- .../Hosting/HostedCommandManager.cs | 72 ++++++++++ .../HostedCommandManagerResolver.cs | 28 ++-- src/CSF.Tests.Console/Complex/ComplexType.cs | 4 +- .../Complex/ComplexerType.cs | 4 +- src/CSF.Tests.Console/Modules/AsyncModule.cs | 8 +- src/CSF.Tests.Console/Modules/Module.cs | 35 ++--- src/CSF.Tests.Console/Program.cs | 40 ++---- src/CSF.Tests.Hosting/Factory.cs | 53 +++++++ src/CSF.Tests.Hosting/Module.cs | 9 +- src/CSF.Tests.Hosting/Program.cs | 12 +- 52 files changed, 486 insertions(+), 336 deletions(-) create mode 100644 src/CSF.Core/Core/Configuration/TaskAwaitOptions.cs create mode 100644 src/CSF.Core/Core/Execution/IResultHandler.cs rename src/CSF.Core/Core/Results/{IResult.cs => ICommandResult.cs} (51%) delete mode 100644 src/CSF.Hosting/Core/Execution/IContextFactory.cs delete mode 100644 src/CSF.Hosting/Core/Execution/Impl/HostedCommandContext.cs delete mode 100644 src/CSF.Hosting/Core/Execution/Impl/HostedContextFactory.cs delete mode 100644 src/CSF.Hosting/Core/HostedCommandManager.cs create mode 100644 src/CSF.Hosting/Hosting/Execution/IActionFactory.cs create mode 100644 src/CSF.Hosting/Hosting/Execution/Impl/HostedCommandContext.cs rename src/CSF.Hosting/{Core => Hosting}/Execution/Impl/HostedModuleBase.cs (59%) create mode 100644 src/CSF.Hosting/Hosting/HostedCommandManager.cs rename src/CSF.Hosting/{Core => Hosting}/HostedCommandManagerResolver.cs (50%) create mode 100644 src/CSF.Tests.Hosting/Factory.cs diff --git a/src/CSF.Core/Core/Attributes/CommandAttribute.cs b/src/CSF.Core/Core/Attributes/CommandAttribute.cs index 3be9948..d89ef0f 100644 --- a/src/CSF.Core/Core/Attributes/CommandAttribute.cs +++ b/src/CSF.Core/Core/Attributes/CommandAttribute.cs @@ -1,7 +1,7 @@ using CSF.Helpers; using System.Diagnostics.CodeAnalysis; -namespace CSF +namespace CSF.Core { /// /// An attribute that represents the required info to map a command. diff --git a/src/CSF.Core/Core/Attributes/ComplexAttribute.cs b/src/CSF.Core/Core/Attributes/ComplexAttribute.cs index 346132d..9cc14e0 100644 --- a/src/CSF.Core/Core/Attributes/ComplexAttribute.cs +++ b/src/CSF.Core/Core/Attributes/ComplexAttribute.cs @@ -1,4 +1,4 @@ -namespace CSF +namespace CSF.Core { /// /// Marks a parameter as complex, which will attempt to fetch the primary constructor values and use those as command parameters. diff --git a/src/CSF.Core/Core/Attributes/DescriptionAttribute.cs b/src/CSF.Core/Core/Attributes/DescriptionAttribute.cs index 89c3064..d622fef 100644 --- a/src/CSF.Core/Core/Attributes/DescriptionAttribute.cs +++ b/src/CSF.Core/Core/Attributes/DescriptionAttribute.cs @@ -1,7 +1,7 @@ using CSF.Helpers; using System.Diagnostics.CodeAnalysis; -namespace CSF +namespace CSF.Core { /// /// Represents the description of a command. diff --git a/src/CSF.Core/Core/Attributes/GroupAttribute.cs b/src/CSF.Core/Core/Attributes/GroupAttribute.cs index 39506e2..09b3fb0 100644 --- a/src/CSF.Core/Core/Attributes/GroupAttribute.cs +++ b/src/CSF.Core/Core/Attributes/GroupAttribute.cs @@ -1,7 +1,7 @@ using CSF.Helpers; using System.Diagnostics.CodeAnalysis; -namespace CSF +namespace CSF.Core { /// /// Represents a command group, functioning much like subcommands. diff --git a/src/CSF.Core/Core/Attributes/PrimaryConstructorAttribute.cs b/src/CSF.Core/Core/Attributes/PrimaryConstructorAttribute.cs index e38a26d..3cdd919 100644 --- a/src/CSF.Core/Core/Attributes/PrimaryConstructorAttribute.cs +++ b/src/CSF.Core/Core/Attributes/PrimaryConstructorAttribute.cs @@ -1,4 +1,4 @@ -namespace CSF +namespace CSF.Core { /// /// Represents an attribute that sets a specified constructor as the dependency injection constructor. diff --git a/src/CSF.Core/Core/Attributes/PriorityAttribute.cs b/src/CSF.Core/Core/Attributes/PriorityAttribute.cs index b33d60b..a6a55c5 100644 --- a/src/CSF.Core/Core/Attributes/PriorityAttribute.cs +++ b/src/CSF.Core/Core/Attributes/PriorityAttribute.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; -namespace CSF +namespace CSF.Core { /// /// Represents an attribute that can prioritize one result over another when multiple matches were found. diff --git a/src/CSF.Core/Core/Attributes/RemainderAttribute.cs b/src/CSF.Core/Core/Attributes/RemainderAttribute.cs index 4e644dc..b1e95ff 100644 --- a/src/CSF.Core/Core/Attributes/RemainderAttribute.cs +++ b/src/CSF.Core/Core/Attributes/RemainderAttribute.cs @@ -1,4 +1,4 @@ -namespace CSF +namespace CSF.Core { /// /// Defines that this parameter should be the remainder of the command phrase. diff --git a/src/CSF.Core/Core/CommandConfiguration.cs b/src/CSF.Core/Core/CommandConfiguration.cs index 20af470..333bcd2 100644 --- a/src/CSF.Core/Core/CommandConfiguration.cs +++ b/src/CSF.Core/Core/CommandConfiguration.cs @@ -1,12 +1,14 @@ using CSF.TypeReaders; using System.Reflection; -namespace CSF +namespace CSF.Core { public sealed class CommandConfiguration { public Assembly[] Assemblies { get; set; } = [ Assembly.GetEntryAssembly() ]; public TypeReader[] TypeReaders { get; set; } = []; + + public TaskAwaitOptions ExecutionPattern { get; set; } } } diff --git a/src/CSF.Core/Core/CommandManager.cs b/src/CSF.Core/Core/CommandManager.cs index 70f5908..0cd4141 100644 --- a/src/CSF.Core/Core/CommandManager.cs +++ b/src/CSF.Core/Core/CommandManager.cs @@ -1,18 +1,24 @@ -using CSF.TypeReaders; +using CSF.Exceptions; using CSF.Helpers; using CSF.Reflection; -using CSF.Exceptions; +using CSF.TypeReaders; +using Microsoft.Extensions.DependencyInjection; +using System.ComponentModel; [assembly: CLSCompliant(true)] -namespace CSF +namespace CSF.Core { - public class CommandManager + public class CommandManager : IDisposable { - public IServiceProvider Services { get; } + private readonly object _searchLock = new(); public IReadOnlySet Components { get; } + public IServiceProvider Services { get; } + + public IResultHandler ResultHandler { get; } + public TypeReader[] TypeReaders { get; } public CommandConfiguration Configuration { get; } @@ -32,53 +38,96 @@ public CommandManager(IServiceProvider services, CommandConfiguration configurat Services = services; + ResultHandler = services.GetService(); + Configuration = configuration; } - public virtual async Task ExecuteAsync(ICommandContext context, params object[] args) + public void Execute(ICommandContext context, params object[] args) + => ExecuteAsync(context, args).Wait(); + + public Task ExecuteAsync(ICommandContext context, params object[] args) + => ExecuteAsync(context, args, cancellationToken: default); + + public async Task ExecuteAsync(ICommandContext context, object[] args, CancellationToken cancellationToken = default) + { + switch (Configuration.ExecutionPattern) + { + case TaskAwaitOptions.Await: + { + context.LogDebug("Starting execution. Execution pattern = [Await]."); + await ExecuteInternalAsync(context, args, cancellationToken); + } + return; + case TaskAwaitOptions.Discard: + { + context.LogDebug("Starting execution. Execution pattern = [Discard]."); + _ = ExecuteInternalAsync(context, args, cancellationToken); + } + return; + } + } + + public IEnumerable Search(object[] args) + { + // recursively search for commands in the execution. + lock (_searchLock) + { + return Components.RecursiveSearch(args, 0); + } + } + + #region Executing + private async Task ExecuteInternalAsync(ICommandContext context, object[] args, CancellationToken cancellationToken) { - // search all relevant commands. var searches = Search(args); - // define a fallback for unsuccesful execution. - MatchResult? fallback = default; + var c = 0; - // order searches by descending for priority definitions. foreach (var search in searches.OrderByDescending(x => x.Command.Priority)) { - var match = await MatchAsync(context, search, args); + c++; - if (fallback is not null) - fallback = match; + var match = await MatchAsync(context, search, args, cancellationToken); + // enter the invocation logic when a match is succesful. if (match.Success) - return await RunAsync(context, match); - } + { + var result = await RunAsync(context, match, cancellationToken); + await ResultHandler.HandleAsync(context, result, cancellationToken); - if (!fallback.HasValue) - return new SearchResult(new SearchException("No command was found with the provided input.")); + return; + } - return fallback; - } + context.TrySetFallback(match); + } - public virtual IEnumerable Search(object[] args) - { - // recursively search for commands in the execution. - return Components.RecursiveSearch(args, 0); + // if no searches were found, we send searchfailure. + if (c is 0) + { + await ResultHandler.HandleAsync(context, new SearchResult(new SearchException("No commands were found with the provided input.")), cancellationToken); + } + + // if there is a fallback present, we send matchfailure. + if (context.TryGetFallback(out var fallback)) + { + await ResultHandler.HandleAsync(context, fallback, cancellationToken); + } } + #endregion #region Matching - private async ValueTask MatchAsync(ICommandContext context, SearchResult search, object[] args) + private async ValueTask MatchAsync(ICommandContext context, SearchResult search, object[] args, CancellationToken cancellationToken) { // check command preconditions. - var check = await CheckAsync(context, search.Command); + var check = await CheckAsync(context, search.Command, cancellationToken); // verify check success, if not, return the failure. if (!check.Success) return new(search.Command, new MatchException("Command failed to reach execution state. View inner exception for more details.", check.Exception)); // read the command parameters in right order. - var readResult = await ReadAsync(context, search, args); + var readResult = await ReadAsync(context, search, args, cancellationToken); // exchange the reads for result, verifying successes in the process. var reads = new object[readResult.Length]; @@ -97,7 +146,7 @@ private async ValueTask MatchAsync(ICommandContext context, SearchR #endregion #region Reading - private async ValueTask ReadAsync(ICommandContext context, SearchResult search, object[] args) + private async ValueTask ReadAsync(ICommandContext context, SearchResult search, object[] args, CancellationToken cancellationToken) { context.LogDebug("Attempting argument conversion for {}", search.Command); @@ -110,15 +159,15 @@ private async ValueTask ReadAsync(ICommandContext context, SearchR // check if input equals command length. if (search.Command.MaxLength == length) - return await search.Command.Parameters.RecursiveReadAsync(context, args[length..], 0); + return await search.Command.Parameters.RecursiveReadAsync(context, args[length..], 0, cancellationToken); // check if input is longer than command, but remainder to concatenate. if (search.Command.MaxLength <= length && search.Command.HasRemainder) - return await search.Command.Parameters.RecursiveReadAsync(context, args[length..], 0); + return await search.Command.Parameters.RecursiveReadAsync(context, args[length..], 0, cancellationToken); // check if input is shorter than command, but optional parameters to replace. if (search.Command.MaxLength > length && search.Command.MinLength <= length) - return await search.Command.Parameters.RecursiveReadAsync(context, args[length..], 0); + return await search.Command.Parameters.RecursiveReadAsync(context, args[length..], 0, cancellationToken); // input is too long or too short. return []; @@ -126,13 +175,13 @@ private async ValueTask ReadAsync(ICommandContext context, SearchR #endregion #region Checking - private async ValueTask CheckAsync(ICommandContext context, CommandInfo command) + private async ValueTask CheckAsync(ICommandContext context, CommandInfo command, CancellationToken cancellationToken) { context.LogDebug("Attempting validations for {}", command); foreach (var precon in command.Preconditions) { - var result = await precon.EvaluateAsync(context, command); + var result = await precon.EvaluateAsync(context, command, cancellationToken); if (!result.Success) return result; @@ -142,7 +191,7 @@ private async ValueTask CheckAsync(ICommandContext context, Command #endregion #region Running - private async ValueTask RunAsync(ICommandContext context, MatchResult match) + private async ValueTask RunAsync(ICommandContext context, MatchResult match, CancellationToken cancellationToken) { try { @@ -154,11 +203,11 @@ private async ValueTask RunAsync(ICommandContext context, MatchResult module.Command = match.Command; module.Services = Services; - await module.BeforeExecuteAsync(); + await module.BeforeExecuteAsync(cancellationToken); var value = match.Command.Target.Invoke(module, match.Reads); - await module.AfterExecuteAsync(); + await module.AfterExecuteAsync(cancellationToken); return module.ReturnTypeResolve(value); } @@ -189,5 +238,15 @@ private IEnumerable BuildComponents(CommandConfiguration configurati } } #endregion + + public void Dispose() + { + + } + + public override string ToString() + { + + } } } diff --git a/src/CSF.Core/Core/CommandManagerResolver.cs b/src/CSF.Core/Core/CommandManagerResolver.cs index a17f189..db13886 100644 --- a/src/CSF.Core/Core/CommandManagerResolver.cs +++ b/src/CSF.Core/Core/CommandManagerResolver.cs @@ -1,7 +1,6 @@ using CSF.Helpers; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using System.ComponentModel; using System.Diagnostics.CodeAnalysis; namespace CSF.Core diff --git a/src/CSF.Core/Core/Configuration/TaskAwaitOptions.cs b/src/CSF.Core/Core/Configuration/TaskAwaitOptions.cs new file mode 100644 index 0000000..3f98ee6 --- /dev/null +++ b/src/CSF.Core/Core/Configuration/TaskAwaitOptions.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSF.Core +{ + public enum TaskAwaitOptions + { + Default = Await, + + Await = 0, + + Discard = 1, + } +} diff --git a/src/CSF.Core/Core/Execution/ICommandContext.cs b/src/CSF.Core/Core/Execution/ICommandContext.cs index 0bed802..bc20df2 100644 --- a/src/CSF.Core/Core/Execution/ICommandContext.cs +++ b/src/CSF.Core/Core/Execution/ICommandContext.cs @@ -1,6 +1,6 @@ -using Microsoft.Extensions.DependencyInjection; +using System.Diagnostics.CodeAnalysis; -namespace CSF +namespace CSF.Core { public interface ICommandContext { @@ -15,5 +15,9 @@ public interface ICommandContext public void LogError(string message, params object[] args); public void LogCritical(string message, params object[] args); + + internal bool TryGetFallback([NotNullWhen(true)] out ICommandResult result); + + internal void TrySetFallback(ICommandResult result); } } diff --git a/src/CSF.Core/Core/Execution/IResultHandler.cs b/src/CSF.Core/Core/Execution/IResultHandler.cs new file mode 100644 index 0000000..0a5c932 --- /dev/null +++ b/src/CSF.Core/Core/Execution/IResultHandler.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSF.Core +{ + public interface IResultHandler : IDisposable + { + public Task HandleAsync(ICommandContext context, ICommandResult result, CancellationToken cancellationToken); + } +} diff --git a/src/CSF.Core/Core/Execution/Impl/CommandContext.cs b/src/CSF.Core/Core/Execution/Impl/CommandContext.cs index 6ba3685..d7fbec2 100644 --- a/src/CSF.Core/Core/Execution/Impl/CommandContext.cs +++ b/src/CSF.Core/Core/Execution/Impl/CommandContext.cs @@ -1,9 +1,10 @@ -using CSF.Reflection; - -namespace CSF +namespace CSF.Core { - public abstract class CommandContext : ICommandContext + public class CommandContext : ICommandContext { + private readonly object _lock = new(); + private ICommandResult _fallback; + public virtual void LogCritical(string message, params object[] args) { @@ -31,7 +32,28 @@ public virtual void LogTrace(string message, params object[] args) public virtual void LogWarning(string message, params object[] args) { - + + } + + bool ICommandContext.TryGetFallback(out ICommandResult result) + { + lock (_lock) + { + result = _fallback; + + return _fallback != null; + } + } + + void ICommandContext.TrySetFallback(ICommandResult result) + { + lock (_lock) + { + if (_fallback != null) + { + _fallback = result; + } + } } } } diff --git a/src/CSF.Core/Core/Execution/ModuleBase.cs b/src/CSF.Core/Core/Execution/ModuleBase.cs index ce96cd8..2b34f93 100644 --- a/src/CSF.Core/Core/Execution/ModuleBase.cs +++ b/src/CSF.Core/Core/Execution/ModuleBase.cs @@ -1,6 +1,6 @@ using CSF.Reflection; -namespace CSF +namespace CSF.Core { public abstract class ModuleBase : ModuleBase where T : ICommandContext @@ -50,12 +50,12 @@ public virtual RunResult HandleUnknownReturnType(object value) return new RunResult(Command, exception: null); } - public virtual ValueTask BeforeExecuteAsync() + public virtual ValueTask BeforeExecuteAsync(CancellationToken cancellationToken) { return ValueTask.CompletedTask; } - public virtual ValueTask AfterExecuteAsync() + public virtual ValueTask AfterExecuteAsync(CancellationToken cancellationToken) { return ValueTask.CompletedTask; } diff --git a/src/CSF.Core/Core/Results/IResult.cs b/src/CSF.Core/Core/Results/ICommandResult.cs similarity index 51% rename from src/CSF.Core/Core/Results/IResult.cs rename to src/CSF.Core/Core/Results/ICommandResult.cs index 5cda7ce..a241e95 100644 --- a/src/CSF.Core/Core/Results/IResult.cs +++ b/src/CSF.Core/Core/Results/ICommandResult.cs @@ -1,6 +1,8 @@ -namespace CSF +using System.Diagnostics.CodeAnalysis; + +namespace CSF { - public interface IResult + public interface ICommandResult { public Exception Exception { get; } diff --git a/src/CSF.Core/Core/Results/Impl/CheckResult.cs b/src/CSF.Core/Core/Results/Impl/CheckResult.cs index e1fe927..09e5ac0 100644 --- a/src/CSF.Core/Core/Results/Impl/CheckResult.cs +++ b/src/CSF.Core/Core/Results/Impl/CheckResult.cs @@ -1,6 +1,6 @@ namespace CSF { - public readonly struct CheckResult : IResult + public readonly struct CheckResult : ICommandResult { public Exception Exception { get; } = null; diff --git a/src/CSF.Core/Core/Results/Impl/MatchResult.cs b/src/CSF.Core/Core/Results/Impl/MatchResult.cs index 89b26ff..725f9dc 100644 --- a/src/CSF.Core/Core/Results/Impl/MatchResult.cs +++ b/src/CSF.Core/Core/Results/Impl/MatchResult.cs @@ -2,7 +2,7 @@ namespace CSF { - public readonly struct MatchResult : IResult + public readonly struct MatchResult : ICommandResult { public Exception Exception { get; } = null; diff --git a/src/CSF.Core/Core/Results/Impl/ReadResult.cs b/src/CSF.Core/Core/Results/Impl/ReadResult.cs index 263841d..159bfc9 100644 --- a/src/CSF.Core/Core/Results/Impl/ReadResult.cs +++ b/src/CSF.Core/Core/Results/Impl/ReadResult.cs @@ -1,6 +1,6 @@ namespace CSF { - public readonly struct ReadResult : IResult + public readonly struct ReadResult : ICommandResult { public Exception Exception { get; } = null; diff --git a/src/CSF.Core/Core/Results/Impl/RunResult.cs b/src/CSF.Core/Core/Results/Impl/RunResult.cs index cac745c..36f8edf 100644 --- a/src/CSF.Core/Core/Results/Impl/RunResult.cs +++ b/src/CSF.Core/Core/Results/Impl/RunResult.cs @@ -2,7 +2,7 @@ namespace CSF { - public readonly struct RunResult : IResult + public readonly struct RunResult : ICommandResult { public Exception Exception { get; } = null; diff --git a/src/CSF.Core/Core/Results/Impl/SearchResult.cs b/src/CSF.Core/Core/Results/Impl/SearchResult.cs index a0605e0..710d3a4 100644 --- a/src/CSF.Core/Core/Results/Impl/SearchResult.cs +++ b/src/CSF.Core/Core/Results/Impl/SearchResult.cs @@ -2,7 +2,7 @@ namespace CSF { - public readonly struct SearchResult : IResult + public readonly struct SearchResult : ICommandResult { public Exception Exception { get; } = null; diff --git a/src/CSF.Core/Helpers/ExecutionHelpers.cs b/src/CSF.Core/Helpers/ExecutionHelpers.cs index 8048168..c36a617 100644 --- a/src/CSF.Core/Helpers/ExecutionHelpers.cs +++ b/src/CSF.Core/Helpers/ExecutionHelpers.cs @@ -1,4 +1,5 @@ -using CSF.Reflection; +using CSF.Core; +using CSF.Reflection; namespace CSF.Helpers { @@ -29,9 +30,9 @@ public static IEnumerable RecursiveSearch(this IEnumerable RecursiveReadAsync(this IArgument[] param, ICommandContext context, object[] args, int index) + public static async Task RecursiveReadAsync(this IArgument[] param, ICommandContext context, object[] args, int index, CancellationToken cancellationToken) { - static async ValueTask ReadAsync(IArgument param, ICommandContext context, object arg) + static async ValueTask ReadAsync(IArgument param, ICommandContext context, object arg, CancellationToken cancellationToken) { if (arg.GetType() == param.Type) return new(arg); @@ -39,7 +40,7 @@ static async ValueTask ReadAsync(IArgument param, ICommandContext co if (param.IsNullable && arg is null or "null" or "nothing") return new(arg); - return await param.TypeReader.ObjectEvaluateAsync(context, param, arg); + return await param.TypeReader.ObjectEvaluateAsync(context, param, arg, cancellationToken); } var results = new ReadResult[param.Length]; @@ -54,7 +55,7 @@ static async ValueTask ReadAsync(IArgument param, ICommandContext co if (parameter.Type == typeof(string)) results[i] = new(input); else - results[i] = await ReadAsync(parameter, context, input); + results[i] = await ReadAsync(parameter, context, input, cancellationToken); break; } @@ -67,7 +68,7 @@ static async ValueTask ReadAsync(IArgument param, ICommandContext co if (parameter is ComplexArgumentInfo complex) { - var result = await complex.Parameters.RecursiveReadAsync(context, args, index); + var result = await complex.Parameters.RecursiveReadAsync(context, args, index, cancellationToken); index += result.Length; @@ -86,7 +87,7 @@ static async ValueTask ReadAsync(IArgument param, ICommandContext co continue; } - results[i] = await ReadAsync(parameter, context, args[index]); + results[i] = await ReadAsync(parameter, context, args[index], cancellationToken); index++; } diff --git a/src/CSF.Core/Helpers/ReflectionHelpers.cs b/src/CSF.Core/Helpers/ReflectionHelpers.cs index b753049..5ea638f 100644 --- a/src/CSF.Core/Helpers/ReflectionHelpers.cs +++ b/src/CSF.Core/Helpers/ReflectionHelpers.cs @@ -1,7 +1,8 @@ -using CSF.TypeReaders; +using CSF.Core; +using CSF.Preconditions; using CSF.Reflection; +using CSF.TypeReaders; using System.Reflection; -using CSF.Preconditions; namespace CSF.Helpers { diff --git a/src/CSF.Core/Helpers/ServiceHelpers.cs b/src/CSF.Core/Helpers/ServiceHelpers.cs index c1c0cdb..670617f 100644 --- a/src/CSF.Core/Helpers/ServiceHelpers.cs +++ b/src/CSF.Core/Helpers/ServiceHelpers.cs @@ -1,9 +1,10 @@ -using Microsoft.Extensions.DependencyInjection; +using CSF.Core; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; namespace CSF.Helpers { - internal static class ServiceHelpers + public static class ServiceHelpers { public static IServiceCollection ModulesAddTransient(this IServiceCollection collection, CommandConfiguration configuration) { diff --git a/src/CSF.Core/Helpers/ThrowHelpers.cs b/src/CSF.Core/Helpers/ThrowHelpers.cs index 2617c61..1a651ac 100644 --- a/src/CSF.Core/Helpers/ThrowHelpers.cs +++ b/src/CSF.Core/Helpers/ThrowHelpers.cs @@ -4,7 +4,7 @@ namespace CSF.Helpers { - internal static class ThrowHelpers + public static class ThrowHelpers { [DoesNotReturn] public static void InvalidOp([DisallowNull] string failureMessage) diff --git a/src/CSF.Core/Preconditions/PreconditionAttribute.cs b/src/CSF.Core/Preconditions/PreconditionAttribute.cs index 0ea3453..0b14fe6 100644 --- a/src/CSF.Core/Preconditions/PreconditionAttribute.cs +++ b/src/CSF.Core/Preconditions/PreconditionAttribute.cs @@ -1,6 +1,7 @@ -using CSF.Reflection; +using CSF.Core; using CSF.Exceptions; using CSF.Helpers; +using CSF.Reflection; using System.Diagnostics.CodeAnalysis; namespace CSF.Preconditions @@ -10,7 +11,7 @@ public abstract class PreconditionAttribute : Attribute { private static readonly string _exHeader = "Precondition result halted further command execution. View inner exception for more details."; - public abstract ValueTask EvaluateAsync(ICommandContext context, CommandInfo command); + public abstract ValueTask EvaluateAsync(ICommandContext context, CommandInfo command, CancellationToken cancellationToken); public static CheckResult Error([DisallowNull] Exception exception) { diff --git a/src/CSF.Core/Reflection/Impl/ArgumentInfo.cs b/src/CSF.Core/Reflection/Impl/ArgumentInfo.cs index cc41ec9..a802ca4 100644 --- a/src/CSF.Core/Reflection/Impl/ArgumentInfo.cs +++ b/src/CSF.Core/Reflection/Impl/ArgumentInfo.cs @@ -1,4 +1,5 @@ -using CSF.Helpers; +using CSF.Core; +using CSF.Helpers; using CSF.TypeReaders; using System.Reflection; diff --git a/src/CSF.Core/Reflection/Impl/CommandInfo.cs b/src/CSF.Core/Reflection/Impl/CommandInfo.cs index a27207a..ee92d3c 100644 --- a/src/CSF.Core/Reflection/Impl/CommandInfo.cs +++ b/src/CSF.Core/Reflection/Impl/CommandInfo.cs @@ -1,4 +1,5 @@ -using CSF.Helpers; +using CSF.Core; +using CSF.Helpers; using CSF.Preconditions; using CSF.TypeReaders; using System.Reflection; @@ -8,43 +9,30 @@ namespace CSF.Reflection public sealed class CommandInfo : IConditional, IArgumentBucket { - public string Name { get; } - public Attribute[] Attributes { get; } - public PreconditionAttribute[] Preconditions { get; } - public bool HasPreconditions { get; } - public IArgument[] Parameters { get; } - public bool HasParameters { get; } - public bool HasRemainder { get; } - public int MinLength { get; } - public int MaxLength { get; } - public string[] Aliases { get; } - public byte Priority { get; } - public ModuleInfo Module { get; } - public MethodInfo Target { get; } internal CommandInfo(ModuleInfo module, MethodInfo method, string[] aliases, IDictionary typeReaders) @@ -63,6 +51,11 @@ internal CommandInfo(ModuleInfo module, MethodInfo method, string[] aliases, IDi } } + if (method.ReturnType == typeof(Task)) + { + + } + Priority = attributes.SelectFirstOrDefault()?.Priority ?? 0; Target = method; diff --git a/src/CSF.Core/TypeReaders/Impl/BaseTypeReader.cs b/src/CSF.Core/TypeReaders/Impl/BaseTypeReader.cs index a1570b4..41a3c16 100644 --- a/src/CSF.Core/TypeReaders/Impl/BaseTypeReader.cs +++ b/src/CSF.Core/TypeReaders/Impl/BaseTypeReader.cs @@ -1,4 +1,5 @@ -using CSF.Reflection; +using CSF.Core; +using CSF.Reflection; namespace CSF.TypeReaders { @@ -8,7 +9,7 @@ internal class BaseTypeReader : TypeReader private readonly static Lazy> _container = new(ValueGenerator); - public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value) + public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value, CancellationToken cancellationToken) { var parser = _container.Value[Type] as Tpd; diff --git a/src/CSF.Core/TypeReaders/Impl/ColorTypeReader.cs b/src/CSF.Core/TypeReaders/Impl/ColorTypeReader.cs index f53cbd2..3f5fe66 100644 --- a/src/CSF.Core/TypeReaders/Impl/ColorTypeReader.cs +++ b/src/CSF.Core/TypeReaders/Impl/ColorTypeReader.cs @@ -1,4 +1,5 @@ -using CSF.Reflection; +using CSF.Core; +using CSF.Reflection; using System.Drawing; using System.Globalization; using System.Reflection; @@ -46,7 +47,7 @@ public ColorTypeReader() _spacedColors = spacedNames; } - public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value) + public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value, CancellationToken cancellationToken) { if (int.TryParse(value.Replace("#", "").Replace("0x", ""), NumberStyles.HexNumber, null, out var hexNumber)) return ValueTask.FromResult(Success(Color.FromArgb(hexNumber))); diff --git a/src/CSF.Core/TypeReaders/Impl/EnumTypeReader.cs b/src/CSF.Core/TypeReaders/Impl/EnumTypeReader.cs index 3f1d258..73688b1 100644 --- a/src/CSF.Core/TypeReaders/Impl/EnumTypeReader.cs +++ b/src/CSF.Core/TypeReaders/Impl/EnumTypeReader.cs @@ -1,4 +1,5 @@ -using CSF.Reflection; +using CSF.Core; +using CSF.Reflection; namespace CSF.TypeReaders { @@ -8,7 +9,7 @@ internal class EnumTypeReader(Type targetEnumType) : TypeReader public override Type Type { get; } = targetEnumType; - public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value) + public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value, CancellationToken cancellationToken) { if (Enum.TryParse(Type, value, true, out var result)) return ValueTask.FromResult(Success(result)); diff --git a/src/CSF.Core/TypeReaders/Impl/TimeSpanTypeReader.cs b/src/CSF.Core/TypeReaders/Impl/TimeSpanTypeReader.cs index 8587351..0435a31 100644 --- a/src/CSF.Core/TypeReaders/Impl/TimeSpanTypeReader.cs +++ b/src/CSF.Core/TypeReaders/Impl/TimeSpanTypeReader.cs @@ -1,4 +1,5 @@ -using CSF.Reflection; +using CSF.Core; +using CSF.Reflection; using System.Text.RegularExpressions; namespace CSF.TypeReaders @@ -34,7 +35,7 @@ public TimeSpanTypeReader() }; } - public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value) + public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value, CancellationToken cancellationToken) { if (!TimeSpan.TryParse(value, out TimeSpan span)) { diff --git a/src/CSF.Core/TypeReaders/TypeReader.cs b/src/CSF.Core/TypeReaders/TypeReader.cs index 951d76f..5210d3d 100644 --- a/src/CSF.Core/TypeReaders/TypeReader.cs +++ b/src/CSF.Core/TypeReaders/TypeReader.cs @@ -1,8 +1,8 @@ -using CSF.Reflection; +using CSF.Core; using CSF.Exceptions; using CSF.Helpers; +using CSF.Reflection; using System.Diagnostics.CodeAnalysis; -using System.Diagnostics; namespace CSF.TypeReaders { @@ -10,7 +10,7 @@ public abstract class TypeReader : TypeReader { public override Type Type { get; } = typeof(T); - public override abstract ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value); + public override abstract ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value, CancellationToken cancellationToken); } public abstract class TypeReader @@ -19,17 +19,17 @@ public abstract class TypeReader public abstract Type Type { get; } - public abstract ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value); + public abstract ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value, CancellationToken cancellationToken); - internal ValueTask ObjectEvaluateAsync(ICommandContext context, IArgument parameter, object value) + internal ValueTask ObjectEvaluateAsync(ICommandContext context, IArgument parameter, object value, CancellationToken cancellationToken) { if (value.GetType() == Type) return ValueTask.FromResult(new ReadResult(value)); if (value is string str) - return EvaluateAsync(context, parameter, str); + return EvaluateAsync(context, parameter, str, cancellationToken); - return EvaluateAsync(context, parameter, value.ToString()); + return EvaluateAsync(context, parameter, value.ToString(), cancellationToken); } public virtual ReadResult Error([DisallowNull] Exception exception) diff --git a/src/CSF.Hosting/CSF.Hosting.csproj b/src/CSF.Hosting/CSF.Hosting.csproj index 7cb0313..99d036a 100644 --- a/src/CSF.Hosting/CSF.Hosting.csproj +++ b/src/CSF.Hosting/CSF.Hosting.csproj @@ -3,12 +3,15 @@ net8.0 enable + CSF 2.1 2.1 2.1.1.0 2.1 + true + Armano den Boef and CSF contributors. https://github.com/Rozen4334/CSF.NET diff --git a/src/CSF.Hosting/Core/Execution/IContextFactory.cs b/src/CSF.Hosting/Core/Execution/IContextFactory.cs deleted file mode 100644 index 5d0a360..0000000 --- a/src/CSF.Hosting/Core/Execution/IContextFactory.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CSF.Hosting -{ - public interface IContextFactory : IDisposable - { - public ICommandContext CreateContext(); - } - - public interface IContextFactory : IContextFactory - where T : ICommandContext - { - public new T CreateContext(); - - ICommandContext IContextFactory.CreateContext() - { - return CreateContext(); - } - } -} diff --git a/src/CSF.Hosting/Core/Execution/Impl/HostedCommandContext.cs b/src/CSF.Hosting/Core/Execution/Impl/HostedCommandContext.cs deleted file mode 100644 index 5f6d458..0000000 --- a/src/CSF.Hosting/Core/Execution/Impl/HostedCommandContext.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CSF.Hosting -{ - public class HostedCommandContext : ICommandContext - { - internal Guid Id { get; } - - public ILogger Logger { get; } - - public HostedCommandContext(Guid id, ILogger logger) - { - Id = id; - Logger = logger; - } - - public void LogTrace(string message, params object[] args) - { - Logger.Log(LogLevel.Trace, message, args); - } - - public void LogDebug(string message, params object[] args) - { - Logger.Log(LogLevel.Debug, message, args); - } - - public void LogInformation(string message, params object[] args) - { - Logger.Log(LogLevel.Information, message, args); - } - - public void LogWarning(string message, params object[] args) - { - Logger.Log(LogLevel.Warning, message, args); - } - - public void LogError(string message, params object[] args) - { - Logger.Log(LogLevel.Error, message, args); - } - - public void LogCritical(string message, params object[] args) - { - Logger.Log(LogLevel.Critical, message, args); - } - - public override string ToString() - => $"HostedCommandContext[{Id}]"; - } -} diff --git a/src/CSF.Hosting/Core/Execution/Impl/HostedContextFactory.cs b/src/CSF.Hosting/Core/Execution/Impl/HostedContextFactory.cs deleted file mode 100644 index 4afb4cd..0000000 --- a/src/CSF.Hosting/Core/Execution/Impl/HostedContextFactory.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CSF.Hosting -{ - public class HostedContextFactory : IContextFactory - { - public ILoggerFactory LoggerFactory { get; } - - public HostedContextFactory(ILoggerFactory loggerFactory) - { - LoggerFactory = loggerFactory; - } - - public virtual HostedCommandContext CreateContext() - { - var guid = Guid.NewGuid(); - var logger = LoggerFactory.CreateLogger($"CSF.Command.Pipeline[{Guid.NewGuid()}]"); - - logger.LogTrace("Generating context with ID {}", guid); - - return new(guid, logger); - } - - public void Dispose() - { - - } - } -} diff --git a/src/CSF.Hosting/Core/HostedCommandManager.cs b/src/CSF.Hosting/Core/HostedCommandManager.cs deleted file mode 100644 index 2a0cb51..0000000 --- a/src/CSF.Hosting/Core/HostedCommandManager.cs +++ /dev/null @@ -1,31 +0,0 @@ -using CSF.Reflection; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CSF.Hosting -{ - public class HostedCommandManager : CommandManager - { - public ILogger Logger { get; } - - public IContextFactory ContextFactory { get; } - - public HostedCommandManager(ILogger logger, IContextFactory factory, IServiceProvider services, CommandConfiguration configuration) - : base(services, configuration) - { - ContextFactory = factory; - Logger = logger; - } - - public Task ExecuteAsync(params object[] args) - { - var context = ContextFactory.CreateContext(); - - return base.ExecuteAsync(context, args); - } - } -} diff --git a/src/CSF.Hosting/Helpers/ServiceHelpers.cs b/src/CSF.Hosting/Helpers/ServiceHelpers.cs index 71ed31c..2d6fcd6 100644 --- a/src/CSF.Hosting/Helpers/ServiceHelpers.cs +++ b/src/CSF.Hosting/Helpers/ServiceHelpers.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CSF.Hosting.Helpers +namespace CSF.Hosting.Helpers { internal class ServiceHelpers { diff --git a/src/CSF.Hosting/Hosting/Execution/IActionFactory.cs b/src/CSF.Hosting/Hosting/Execution/IActionFactory.cs new file mode 100644 index 0000000..3712048 --- /dev/null +++ b/src/CSF.Hosting/Hosting/Execution/IActionFactory.cs @@ -0,0 +1,22 @@ +using CSF.Core; + +namespace CSF.Hosting +{ + public interface IActionFactory : IDisposable + { + public ValueTask CreateContextAsync(CancellationToken cancellationToken); + + public ValueTask CreateArgsAsync(CancellationToken cancellationToken); + } + + public interface IActionFactory : IActionFactory + where T : ICommandContext + { + public new ValueTask CreateContextAsync(CancellationToken cancellationToken); + + async ValueTask IActionFactory.CreateContextAsync(CancellationToken cancellationToken) + { + return await CreateContextAsync(cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/src/CSF.Hosting/Hosting/Execution/Impl/HostedCommandContext.cs b/src/CSF.Hosting/Hosting/Execution/Impl/HostedCommandContext.cs new file mode 100644 index 0000000..90ccc58 --- /dev/null +++ b/src/CSF.Hosting/Hosting/Execution/Impl/HostedCommandContext.cs @@ -0,0 +1,42 @@ +using CSF.Core; +using Microsoft.Extensions.Logging; + +namespace CSF.Hosting +{ +#pragma warning disable CA2254 // Template should be a static expression + public class HostedCommandContext(ILogger logger) : CommandContext + { + public ILogger Logger { get; } = logger; + + public override void LogTrace(string message, params object[] args) + { + Logger.Log(logLevel: LogLevel.Trace, message: message, args: args); + } + + public override void LogDebug(string message, params object[] args) + { + Logger.Log(LogLevel.Debug, message, args); + } + + public override void LogInformation(string message, params object[] args) + { + Logger.Log(LogLevel.Information, message, args); + } + + public override void LogWarning(string message, params object[] args) + { + Logger.Log(LogLevel.Warning, message, args); + } + + public override void LogError(string message, params object[] args) + { + Logger.Log(LogLevel.Error, message, args); + } + + public override void LogCritical(string message, params object[] args) + { + Logger.Log(LogLevel.Critical, message, args); + } + } +#pragma warning restore CA2254 // Template should be a static expression +} diff --git a/src/CSF.Hosting/Core/Execution/Impl/HostedModuleBase.cs b/src/CSF.Hosting/Hosting/Execution/Impl/HostedModuleBase.cs similarity index 59% rename from src/CSF.Hosting/Core/Execution/Impl/HostedModuleBase.cs rename to src/CSF.Hosting/Hosting/Execution/Impl/HostedModuleBase.cs index 96f8f23..ca26279 100644 --- a/src/CSF.Hosting/Core/Execution/Impl/HostedModuleBase.cs +++ b/src/CSF.Hosting/Hosting/Execution/Impl/HostedModuleBase.cs @@ -1,9 +1,5 @@ -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using CSF.Core; +using Microsoft.Extensions.Logging; namespace CSF.Hosting { diff --git a/src/CSF.Hosting/Hosting/HostedCommandManager.cs b/src/CSF.Hosting/Hosting/HostedCommandManager.cs new file mode 100644 index 0000000..e28a894 --- /dev/null +++ b/src/CSF.Hosting/Hosting/HostedCommandManager.cs @@ -0,0 +1,72 @@ +using CSF.Core; +using CSF.Helpers; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace CSF.Hosting +{ + public class HostedCommandManager : CommandManager, IHostedService + { + public ILogger Logger { get; } + + public IActionFactory ActionFactory { get; } + + public HostedCommandManager(ILogger logger, IActionFactory factory, IServiceProvider services, CommandConfiguration configuration) + : base(services, configuration) + { + ActionFactory = factory; + Logger = logger; + } + + public async Task ExecuteAsync(object[] args, CancellationToken cancellationToken) + { + var context = await ActionFactory.CreateContextAsync(cancellationToken).ConfigureAwait(false); + + await ExecuteAsync(context, args, cancellationToken); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _ = RunAsync(cancellationToken); + + return Task.CompletedTask; + } + + internal async Task RunAsync(CancellationToken cancellationToken) + { + try + { + while (cancellationToken.IsCancellationRequested) + { + var args = await ActionFactory.CreateArgsAsync(cancellationToken).ConfigureAwait(false); + + if (args == null) + { + ThrowHelpers.ArgMissing(args); + } + + if (args.Length == 0) + { + ThrowHelpers.ArgMissing(args); + } + + await ExecuteAsync(args, cancellationToken).ConfigureAwait(false); + } + } + catch + { + // WIP + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + + } + } +} diff --git a/src/CSF.Hosting/Core/HostedCommandManagerResolver.cs b/src/CSF.Hosting/Hosting/HostedCommandManagerResolver.cs similarity index 50% rename from src/CSF.Hosting/Core/HostedCommandManagerResolver.cs rename to src/CSF.Hosting/Hosting/HostedCommandManagerResolver.cs index d3dc957..87be036 100644 --- a/src/CSF.Hosting/Core/HostedCommandManagerResolver.cs +++ b/src/CSF.Hosting/Hosting/HostedCommandManagerResolver.cs @@ -1,39 +1,33 @@ using CSF.Core; +using CSF.Helpers; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; -using System; -using System.Collections.Generic; -using System.ComponentModel; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace CSF.Hosting { public static class HostedCommandManagerResolver { - public static IHostBuilder WithCommands(this IHostBuilder builder, [DisallowNull] Action configureDelegate) + public static IHostBuilder WithCommands(this IHostBuilder builder, [DisallowNull] Action configureDelegate) + where TFactory : class, IActionFactory { - return builder.WithCommands(configureDelegate); - } - - public static IHostBuilder WithCommands(this IHostBuilder builder, [DisallowNull] Action configureDelegate) - where T : HostedCommandManager - { - return builder.WithCommands(configureDelegate); + return builder.WithCommands(configureDelegate); } public static IHostBuilder WithCommands(this IHostBuilder builder, [DisallowNull] Action configureDelegate) - where TManager : HostedCommandManager where TFactory : class, IContextFactory + where TManager : HostedCommandManager where TFactory : class, IActionFactory { builder.ConfigureServices((context, services) => { var config = new CommandConfiguration(); - configureDelegate(context, config); - services.WithCommands(config); + services.ModulesAddTransient(config); + + services.TryAddSingleton(config); + services.TryAddSingleton(); services.AddScoped(); }); diff --git a/src/CSF.Tests.Console/Complex/ComplexType.cs b/src/CSF.Tests.Console/Complex/ComplexType.cs index ea73b5e..42486f4 100644 --- a/src/CSF.Tests.Console/Complex/ComplexType.cs +++ b/src/CSF.Tests.Console/Complex/ComplexType.cs @@ -1,4 +1,6 @@ -namespace CSF.Tests.Complex +using CSF.Core; + +namespace CSF.Tests.Complex { public class ComplexType { diff --git a/src/CSF.Tests.Console/Complex/ComplexerType.cs b/src/CSF.Tests.Console/Complex/ComplexerType.cs index 0fd9dee..fd88feb 100644 --- a/src/CSF.Tests.Console/Complex/ComplexerType.cs +++ b/src/CSF.Tests.Console/Complex/ComplexerType.cs @@ -1,4 +1,6 @@ -namespace CSF.Tests.Complex +using CSF.Core; + +namespace CSF.Tests.Complex { public class ComplexerType { diff --git a/src/CSF.Tests.Console/Modules/AsyncModule.cs b/src/CSF.Tests.Console/Modules/AsyncModule.cs index c2b2915..6ecc9de 100644 --- a/src/CSF.Tests.Console/Modules/AsyncModule.cs +++ b/src/CSF.Tests.Console/Modules/AsyncModule.cs @@ -1,4 +1,6 @@ -namespace CSF.Tests.Console.Modules +using CSF.Core; + +namespace CSF.Tests.Console.Modules { public sealed class AsyncModule : ModuleBase { @@ -7,13 +9,13 @@ public async Task AsyncRunDelayed() { await Task.Delay(5000); - Respond("Success. (Delayed)."); + System.Console.WriteLine("Success. (Delayed)."); } [Command("direct")] #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously public async Task AsyncRunDirect() #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - => Respond("Success. (Direct)."); + => System.Console.WriteLine("Success. (Direct)."); } } diff --git a/src/CSF.Tests.Console/Modules/Module.cs b/src/CSF.Tests.Console/Modules/Module.cs index f2de5d2..9d45bdb 100644 --- a/src/CSF.Tests.Console/Modules/Module.cs +++ b/src/CSF.Tests.Console/Modules/Module.cs @@ -1,4 +1,5 @@ -using CSF.Tests.Complex; +using CSF.Core; +using CSF.Tests.Complex; namespace CSF.Tests { @@ -8,63 +9,63 @@ public class Module : ModuleBase [Priority(1)] public void Priority1(bool optional = true) { - Respond($"Success: {Command.Priority}"); + System.Console.WriteLine($"Success: {Command.Priority}"); } [Command("priority")] [Priority(2)] public Task Priority2(bool optional = false) { - Respond($"Success: {Command.Priority}"); + System.Console.WriteLine($"Success: {Command.Priority}"); return Task.CompletedTask; } [Command("remainder")] public void Remainder([Remainder] string values) { - Respond($"Success: {values}"); + System.Console.WriteLine($"Success: {values}"); } [Command("time")] public void TimeOnly(TimeOnly time) { - Respond($"Success: {time}"); + System.Console.WriteLine($"Success: {time}"); } [Command("multiple")] public void Test(bool truee, bool falsee) { - Respond($"Success: {truee}, {falsee}"); + System.Console.WriteLine($"Success: {truee}, {falsee}"); } [Command("multiple")] public void Test(int i1, int i2) { - Respond($"Success: {i1}, {i2}"); + System.Console.WriteLine($"Success: {i1}, {i2}"); } [Command("optional")] public void Test(int i = 0, string str = "") { - Respond($"Success: {i}, {str}"); + System.Console.WriteLine($"Success: {i}, {str}"); } [Command("nullable")] public void Nullable(long? l) { - Respond($"Success: {l}"); + System.Console.WriteLine($"Success: {l}"); } [Command("complex")] public void Complex([Complex] ComplexType complex) { - Respond($"({complex.X}, {complex.Y}, {complex.Z}) {complex.Complexer}: {complex.Complexer.X}, {complex.Complexer.Y}, {complex.Complexer.Z}"); + System.Console.WriteLine($"({complex.X}, {complex.Y}, {complex.Z}) {complex.Complexer}: {complex.Complexer.X}, {complex.Complexer.Y}, {complex.Complexer.Z}"); } [Command("complexnullable")] public void Complex([Complex] ComplexerType? complex) { - Respond($"({complex?.X}, {complex?.Y}, {complex?.Z})"); + System.Console.WriteLine($"({complex?.X}, {complex?.Y}, {complex?.Z})"); } [Group("nested")] @@ -73,37 +74,37 @@ public class NestedModule : ModuleBase [Command("multiple")] public void Test(bool truee, bool falsee) { - Respond($"Success: {truee}, {falsee}"); + System.Console.WriteLine($"Success: {truee}, {falsee}"); } [Command("multiple")] public void Test(int i1, int i2) { - Respond($"Success: {i1}, {i2}"); + System.Console.WriteLine($"Success: {i1}, {i2}"); } [Command("optional")] public void Test(int i = 0, string str = "") { - Respond($"Success: {i}, {str}"); + System.Console.WriteLine($"Success: {i}, {str}"); } [Command("nullable")] public void Nullable(long? l) { - Respond($"Success: {l}"); + System.Console.WriteLine($"Success: {l}"); } [Command("complex")] public void Complex([Complex] ComplexType complex) { - Respond($"({complex.X}, {complex.Y}, {complex.Z}) {complex.Complexer}: {complex.Complexer.X}, {complex.Complexer.Y}, {complex.Complexer.Z}"); + System.Console.WriteLine($"({complex.X}, {complex.Y}, {complex.Z}) {complex.Complexer}: {complex.Complexer.X}, {complex.Complexer.Y}, {complex.Complexer.Z}"); } [Command("complexnullable")] public void Complex([Complex] ComplexerType? complex) { - Respond($"({complex?.X}, {complex?.Y}, {complex?.Z})"); + System.Console.WriteLine($"({complex?.X}, {complex?.Y}, {complex?.Z})"); } } } diff --git a/src/CSF.Tests.Console/Program.cs b/src/CSF.Tests.Console/Program.cs index 68ba3ad..4ae2010 100644 --- a/src/CSF.Tests.Console/Program.cs +++ b/src/CSF.Tests.Console/Program.cs @@ -1,41 +1,19 @@ -//using CSF; -//using Microsoft.Extensions.DependencyInjection; - -//var collection = new ServiceCollection() -// .WithCommandManager(); - -//var services = collection.BuildServiceProvider(); - -//var framework = services.GetRequiredService(); - -//var delayed = new CommandContext("delayed"); -//var direct = new CommandContext("direct"); - -//framework.ExecuteAsync(delayed); -//framework.ExecuteAsync(direct); - -//await Task.Delay(Timeout.Infinite); +using CSF.Core; +using CSF.Parsing; +using Microsoft.Extensions.DependencyInjection; -//while (true) -//{ -// var context = new CommandContext(Console.ReadLine()!); +var collection = new ServiceCollection() + .WithCommands(_ => { }); -// var result = framework.ExecuteAsync(context); +var services = collection.BuildServiceProvider(); -// if (result.Failed(out var failure)) -// Console.WriteLine(failure.Exception); -//} - -using CSF.Parsing; +var framework = services.GetRequiredService(); var parser = new StringParser(); while (true) { - var text = Console.ReadLine(); - - var value = parser.Parse(text); + var input = parser.Parse(Console.ReadLine()!); - foreach (var item in value) - Console.WriteLine("-> " + item); + await framework.ExecuteAsync(null, input); } \ No newline at end of file diff --git a/src/CSF.Tests.Hosting/Factory.cs b/src/CSF.Tests.Hosting/Factory.cs new file mode 100644 index 0000000..7207d6d --- /dev/null +++ b/src/CSF.Tests.Hosting/Factory.cs @@ -0,0 +1,53 @@ +using CSF.Helpers; +using CSF.Hosting; +using CSF.Parsing; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSF.Tests.Hosting +{ + public class Factory : IActionFactory + { + private readonly ILoggerFactory _loggerFactory; + private readonly StringParser _stringParser; + + public Factory(ILoggerFactory loggerFactory) + { + _loggerFactory = loggerFactory; + _stringParser = new StringParser(); + } + + public ValueTask CreateArgsAsync(CancellationToken cancellationToken) + { + var input = Console.ReadLine(); + + var args = _stringParser.Parse(input); + + if (args.Length == 0) + { + ThrowHelpers.ArgMissing(args); + } + + return ValueTask.FromResult(args); + } + + public ValueTask CreateContextAsync(CancellationToken cancellationToken) + { + var guid = Guid.NewGuid(); + var logger = _loggerFactory.CreateLogger($"CSF.Command.Pipeline[{Guid.NewGuid()}]"); + + logger.LogTrace("Generating context with ID {}", guid); + + return ValueTask.FromResult(new HostedCommandContext(logger)); + } + + public void Dispose() + { + + } + } +} diff --git a/src/CSF.Tests.Hosting/Module.cs b/src/CSF.Tests.Hosting/Module.cs index 2dc2875..df0a874 100644 --- a/src/CSF.Tests.Hosting/Module.cs +++ b/src/CSF.Tests.Hosting/Module.cs @@ -1,6 +1,9 @@ -namespace CSF.Tests.Hosting +using CSF.Core; +using CSF.Hosting; + +namespace CSF.Tests.Hosting { - public sealed class Module : ModuleBase + public sealed class Module : HostedModuleBase { #pragma warning disable IDE0052 // Remove unread private members private readonly IServiceProvider _provider; @@ -13,6 +16,6 @@ public Module(IServiceProvider provider) [Command("help")] public void Help() - => Respond("Helped"); + => Console.WriteLine("Helped"); } } diff --git a/src/CSF.Tests.Hosting/Program.cs b/src/CSF.Tests.Hosting/Program.cs index 0cdb2d7..514cad0 100644 --- a/src/CSF.Tests.Hosting/Program.cs +++ b/src/CSF.Tests.Hosting/Program.cs @@ -1,6 +1,16 @@ using CSF.Hosting; +using CSF.Tests.Hosting; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Reflection; await Host.CreateDefaultBuilder(args) - .ConfigureCommandManager() + .WithCommands((context, config) => + { + config.Assemblies = [ Assembly.GetEntryAssembly() ]; + }) + .ConfigureLogging(x => + { + x.AddSimpleConsole(); + }) .RunConsoleAsync(); \ No newline at end of file From b513d063adc9db886b733e5732b21dbf392381d8 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Mon, 29 Jan 2024 21:13:12 +0100 Subject: [PATCH 11/40] Update csproj's, change helpers design --- src/CSF.Core/CSF.Core.csproj | 10 ++--- src/CSF.Core/Core/CommandManagerResolver.cs | 40 ------------------- src/CSF.Core/Helpers/CollectionHelpers.cs | 7 +++- src/CSF.Core/Helpers/ExecutionHelpers.cs | 6 ++- src/CSF.Core/Helpers/ReflectionHelpers.cs | 10 +++-- src/CSF.Core/Helpers/ServiceHelpers.cs | 37 ++++++++++++++++- src/CSF.Core/Helpers/ThrowHelpers.cs | 5 +++ src/CSF.Hosting/CSF.Hosting.csproj | 12 +++--- src/CSF.Hosting/Helpers/ServiceHelpers.cs | 37 ++++++++++++++++- .../Hosting/HostedCommandManagerResolver.cs | 38 ------------------ src/CSF.Tests.Console/Program.cs | 1 + src/CSF.Tests.Hosting/Program.cs | 3 +- 12 files changed, 108 insertions(+), 98 deletions(-) delete mode 100644 src/CSF.Core/Core/CommandManagerResolver.cs delete mode 100644 src/CSF.Hosting/Hosting/HostedCommandManagerResolver.cs diff --git a/src/CSF.Core/CSF.Core.csproj b/src/CSF.Core/CSF.Core.csproj index c8dece9..17f8f86 100644 --- a/src/CSF.Core/CSF.Core.csproj +++ b/src/CSF.Core/CSF.Core.csproj @@ -6,10 +6,10 @@ CSF true - 2.1 - 2.1 - 2.1.1.0 - 2.1 + 3.0 + 3.0 + 3.0.0.0 + 3.0 Armano den Boef and CSF contributors. @@ -17,7 +17,7 @@ https://github.com/Rozen4334/CSF.NET git - Version 2.1; Made for net6+. + V3. Made for net8+. Text, Console, Commands, Framework Command Standardization Framework diff --git a/src/CSF.Core/Core/CommandManagerResolver.cs b/src/CSF.Core/Core/CommandManagerResolver.cs deleted file mode 100644 index db13886..0000000 --- a/src/CSF.Core/Core/CommandManagerResolver.cs +++ /dev/null @@ -1,40 +0,0 @@ -using CSF.Helpers; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using System.Diagnostics.CodeAnalysis; - -namespace CSF.Core -{ - public static class CommandManagerResolver - { - public static IServiceCollection WithCommands(this IServiceCollection collection, [DisallowNull] Action action) - { - collection.WithCommands(action); - - return collection; - } - - public static IServiceCollection WithCommands(this IServiceCollection collection, [DisallowNull] Action action) - where T : CommandManager - { - var cmdConf = new CommandConfiguration(); - - action(cmdConf); - - collection.WithCommands(cmdConf); - - return collection; - } - - public static IServiceCollection WithCommands(this IServiceCollection collection, CommandConfiguration configuration) - where T : CommandManager - { - collection.ModulesAddTransient(configuration); - - collection.TryAddSingleton(configuration); - collection.TryAddSingleton(); - - return collection; - } - } -} diff --git a/src/CSF.Core/Helpers/CollectionHelpers.cs b/src/CSF.Core/Helpers/CollectionHelpers.cs index 5527935..a1eae6f 100644 --- a/src/CSF.Core/Helpers/CollectionHelpers.cs +++ b/src/CSF.Core/Helpers/CollectionHelpers.cs @@ -1,9 +1,12 @@ using System.Collections; +using System.ComponentModel; namespace CSF.Helpers { - internal static class CollectionHelpers + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static class CollectionHelpers { + [EditorBrowsable(EditorBrowsableState.Never)] public static IEnumerable CastWhere(this IEnumerable input) { foreach (var @in in input) @@ -11,6 +14,7 @@ public static IEnumerable CastWhere(this IEnumerable input) yield return @out; } + [EditorBrowsable(EditorBrowsableState.Never)] public static T SelectFirstOrDefault(this IEnumerable input, T defaultValue = default) { foreach (var @in in input) @@ -20,6 +24,7 @@ public static T SelectFirstOrDefault(this IEnumerable input, T defaultValue = return defaultValue; } + [EditorBrowsable(EditorBrowsableState.Never)] public static bool Contains(this IEnumerable input, bool allowMultipleMatches) where T : Attribute { diff --git a/src/CSF.Core/Helpers/ExecutionHelpers.cs b/src/CSF.Core/Helpers/ExecutionHelpers.cs index c36a617..608d13f 100644 --- a/src/CSF.Core/Helpers/ExecutionHelpers.cs +++ b/src/CSF.Core/Helpers/ExecutionHelpers.cs @@ -1,10 +1,13 @@ using CSF.Core; using CSF.Reflection; +using System.ComponentModel; namespace CSF.Helpers { - internal static class ExecutionHelpers + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static class ExecutionHelpers { + [EditorBrowsable(EditorBrowsableState.Never)] public static IEnumerable RecursiveSearch(this IEnumerable components, object[] args, int searchHeight) { List discovered = []; @@ -30,6 +33,7 @@ public static IEnumerable RecursiveSearch(this IEnumerable RecursiveReadAsync(this IArgument[] param, ICommandContext context, object[] args, int index, CancellationToken cancellationToken) { static async ValueTask ReadAsync(IArgument param, ICommandContext context, object arg, CancellationToken cancellationToken) diff --git a/src/CSF.Core/Helpers/ReflectionHelpers.cs b/src/CSF.Core/Helpers/ReflectionHelpers.cs index 5ea638f..6ace7a6 100644 --- a/src/CSF.Core/Helpers/ReflectionHelpers.cs +++ b/src/CSF.Core/Helpers/ReflectionHelpers.cs @@ -2,13 +2,16 @@ using CSF.Preconditions; using CSF.Reflection; using CSF.TypeReaders; +using System.ComponentModel; using System.Reflection; namespace CSF.Helpers { - internal static class ReflectionHelpers + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static class ReflectionHelpers { - private static IEnumerable GetModules(ModuleInfo module, IDictionary typeReaders) + [EditorBrowsable(EditorBrowsableState.Never)] + public static IEnumerable GetModules(ModuleInfo module, IDictionary typeReaders) { foreach (var group in module.Type.GetNestedTypes()) { @@ -22,7 +25,8 @@ private static IEnumerable GetModules(ModuleInfo module, IDictionary } } - private static IEnumerable GetCommands(ModuleInfo module, IDictionary typeReaders) + [EditorBrowsable(EditorBrowsableState.Never)] + public static IEnumerable GetCommands(ModuleInfo module, IDictionary typeReaders) { foreach (var method in module.Type.GetMethods()) { diff --git a/src/CSF.Core/Helpers/ServiceHelpers.cs b/src/CSF.Core/Helpers/ServiceHelpers.cs index 670617f..882368b 100644 --- a/src/CSF.Core/Helpers/ServiceHelpers.cs +++ b/src/CSF.Core/Helpers/ServiceHelpers.cs @@ -1,12 +1,47 @@ using CSF.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; namespace CSF.Helpers { + [EditorBrowsable(EditorBrowsableState.Advanced)] public static class ServiceHelpers { - public static IServiceCollection ModulesAddTransient(this IServiceCollection collection, CommandConfiguration configuration) + public static IServiceCollection WithCommands(this IServiceCollection collection, [DisallowNull] Action action) + { + collection.WithCommands(action); + + return collection; + } + + public static IServiceCollection WithCommands(this IServiceCollection collection, [DisallowNull] Action action) + where T : CommandManager + { + var cmdConf = new CommandConfiguration(); + + action(cmdConf); + + collection.WithCommands(cmdConf); + + return collection; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public static IServiceCollection WithCommands(this IServiceCollection collection, CommandConfiguration configuration) + where T : CommandManager + { + collection.AddModules(configuration); + + collection.TryAddSingleton(configuration); + collection.TryAddSingleton(); + + return collection; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public static IServiceCollection AddModules(this IServiceCollection collection, CommandConfiguration configuration) { var rootType = typeof(ModuleBase); diff --git a/src/CSF.Core/Helpers/ThrowHelpers.cs b/src/CSF.Core/Helpers/ThrowHelpers.cs index 1a651ac..f942b3b 100644 --- a/src/CSF.Core/Helpers/ThrowHelpers.cs +++ b/src/CSF.Core/Helpers/ThrowHelpers.cs @@ -1,24 +1,29 @@ using CSF.Exceptions; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace CSF.Helpers { + [EditorBrowsable(EditorBrowsableState.Advanced)] public static class ThrowHelpers { [DoesNotReturn] + [EditorBrowsable(EditorBrowsableState.Never)] public static void InvalidOp([DisallowNull] string failureMessage) { throw new InvalidOperationException(failureMessage); } [DoesNotReturn] + [EditorBrowsable(EditorBrowsableState.Never)] public static void ArgMissing(object value, [CallerArgumentExpression(nameof(value))] string arg = null) { throw new ArgumentMissingException(arg, "Argument is not in valid state, being null, empty or whitespace."); } [DoesNotReturn] + [EditorBrowsable(EditorBrowsableState.Never)] public static void RangeDuplicate(object value, [CallerArgumentExpression(nameof(value))] string arg = null) { throw new RangeDuplicateException(arg, "Range contains a duplicate value, which is not supported by the implementation."); diff --git a/src/CSF.Hosting/CSF.Hosting.csproj b/src/CSF.Hosting/CSF.Hosting.csproj index 99d036a..b877e8c 100644 --- a/src/CSF.Hosting/CSF.Hosting.csproj +++ b/src/CSF.Hosting/CSF.Hosting.csproj @@ -5,10 +5,10 @@ enable CSF - 2.1 - 2.1 - 2.1.1.0 - 2.1 + 3.0 + 3.0 + 3.0.0.0 + 3.0 true @@ -18,10 +18,10 @@ https://github.com/Rozen4334/CSF.NET git - Version 2.1; Made for net6+. + V3. Made for net8+. Text, Console, Commands, Framework, Hosting - Command Standardization Framework Extension; Microsoft.Extensions.Hosting + CSF Extension: Microsoft.Extensions.Hosting An extension for CSF that adds support for Microsoft.Extensions.Hosting. en-US diff --git a/src/CSF.Hosting/Helpers/ServiceHelpers.cs b/src/CSF.Hosting/Helpers/ServiceHelpers.cs index 2d6fcd6..2c45ec0 100644 --- a/src/CSF.Hosting/Helpers/ServiceHelpers.cs +++ b/src/CSF.Hosting/Helpers/ServiceHelpers.cs @@ -1,6 +1,39 @@ -namespace CSF.Hosting.Helpers +using CSF.Core; +using CSF.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + +namespace CSF.Helpers { - internal class ServiceHelpers + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static class ServiceHelpers { + public static IHostBuilder WithCommands(this IHostBuilder builder, [DisallowNull] Action configureDelegate) + where TFactory : class, IActionFactory + { + return builder.WithCommands(configureDelegate); + } + + public static IHostBuilder WithCommands(this IHostBuilder builder, [DisallowNull] Action configureDelegate) + where TManager : HostedCommandManager where TFactory : class, IActionFactory + { + builder.ConfigureServices((context, services) => + { + var config = new CommandConfiguration(); + configureDelegate(context, config); + + services.AddModules(config); + + services.TryAddSingleton(config); + services.TryAddSingleton(); + + services.AddScoped(); + }); + + return builder; + } } } diff --git a/src/CSF.Hosting/Hosting/HostedCommandManagerResolver.cs b/src/CSF.Hosting/Hosting/HostedCommandManagerResolver.cs deleted file mode 100644 index 87be036..0000000 --- a/src/CSF.Hosting/Hosting/HostedCommandManagerResolver.cs +++ /dev/null @@ -1,38 +0,0 @@ -using CSF.Core; -using CSF.Helpers; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Hosting; -using System.Diagnostics.CodeAnalysis; - -namespace CSF.Hosting -{ - public static class HostedCommandManagerResolver - { - public static IHostBuilder WithCommands(this IHostBuilder builder, [DisallowNull] Action configureDelegate) - where TFactory : class, IActionFactory - { - return builder.WithCommands(configureDelegate); - } - - public static IHostBuilder WithCommands(this IHostBuilder builder, [DisallowNull] Action configureDelegate) - where TManager : HostedCommandManager where TFactory : class, IActionFactory - { - builder.ConfigureServices((context, services) => - { - var config = new CommandConfiguration(); - configureDelegate(context, config); - - services.ModulesAddTransient(config); - - services.TryAddSingleton(config); - services.TryAddSingleton(); - - services.AddScoped(); - }); - - return builder; - } - } -} diff --git a/src/CSF.Tests.Console/Program.cs b/src/CSF.Tests.Console/Program.cs index 4ae2010..48f1dff 100644 --- a/src/CSF.Tests.Console/Program.cs +++ b/src/CSF.Tests.Console/Program.cs @@ -1,5 +1,6 @@ using CSF.Core; using CSF.Parsing; +using CSF.Helpers; using Microsoft.Extensions.DependencyInjection; var collection = new ServiceCollection() diff --git a/src/CSF.Tests.Hosting/Program.cs b/src/CSF.Tests.Hosting/Program.cs index 514cad0..57c4f70 100644 --- a/src/CSF.Tests.Hosting/Program.cs +++ b/src/CSF.Tests.Hosting/Program.cs @@ -1,4 +1,5 @@ -using CSF.Hosting; +using CSF.Helpers; +using CSF.Hosting; using CSF.Tests.Hosting; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; From 6a67db60f6851199a13a70b9d7a13fc9687cf71e Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Tue, 30 Jan 2024 15:22:32 +0100 Subject: [PATCH 12/40] Improve result handling & configuration --- .../Core/Attributes/CommandAttribute.cs | 8 +- .../Core/Attributes/DescriptionAttribute.cs | 2 +- .../Core/Attributes/GroupAttribute.cs | 8 +- src/CSF.Core/Core/CommandConfiguration.cs | 149 +++++++++++++++++- src/CSF.Core/Core/CommandManager.cs | 16 +- src/CSF.Core/Core/Execution/IResultHandler.cs | 13 -- src/CSF.Core/Core/Results/ResultResolver.cs | 35 ++++ src/CSF.Core/Helpers/ThrowHelpers.cs | 39 ++++- .../Preconditions/PreconditionAttribute.cs | 4 +- src/CSF.Core/TypeReaders/TypeReader.cs | 4 +- .../TypeReaders/TypeReaderEqualityComparer.cs | 38 +++++ .../Hosting/HostedCommandManager.cs | 4 +- src/CSF.Tests.Hosting/Factory.cs | 2 +- src/CSF.Tests.Hosting/Program.cs | 16 +- 14 files changed, 291 insertions(+), 47 deletions(-) delete mode 100644 src/CSF.Core/Core/Execution/IResultHandler.cs create mode 100644 src/CSF.Core/Core/Results/ResultResolver.cs create mode 100644 src/CSF.Core/TypeReaders/TypeReaderEqualityComparer.cs diff --git a/src/CSF.Core/Core/Attributes/CommandAttribute.cs b/src/CSF.Core/Core/Attributes/CommandAttribute.cs index d89ef0f..0568b44 100644 --- a/src/CSF.Core/Core/Attributes/CommandAttribute.cs +++ b/src/CSF.Core/Core/Attributes/CommandAttribute.cs @@ -29,22 +29,22 @@ public CommandAttribute([DisallowNull] string name) public CommandAttribute([DisallowNull] string name, params string[] aliases) { if (string.IsNullOrWhiteSpace(name)) - ThrowHelpers.ArgMissing(name); + ThrowHelpers.InvalidArg(name); var arr = new string[aliases.Length + 1]; for (int i = 0; i < aliases.Length; i++) { if (string.IsNullOrWhiteSpace(aliases[i])) - ThrowHelpers.ArgMissing(aliases); + ThrowHelpers.InvalidArg(aliases); if (arr.Contains(aliases[i])) - ThrowHelpers.RangeDuplicate(aliases); + ThrowHelpers.NotDistinct(aliases); arr[i + 1] = aliases[i]; } if (arr.Contains(name)) - ThrowHelpers.RangeDuplicate(aliases); + ThrowHelpers.NotDistinct(aliases); arr[0] = name; diff --git a/src/CSF.Core/Core/Attributes/DescriptionAttribute.cs b/src/CSF.Core/Core/Attributes/DescriptionAttribute.cs index d622fef..99bd1e2 100644 --- a/src/CSF.Core/Core/Attributes/DescriptionAttribute.cs +++ b/src/CSF.Core/Core/Attributes/DescriptionAttribute.cs @@ -21,7 +21,7 @@ public sealed class DescriptionAttribute : Attribute public DescriptionAttribute([DisallowNull] string description) { if (string.IsNullOrWhiteSpace(description)) - ThrowHelpers.ArgMissing(description); + ThrowHelpers.InvalidArg(description); Description = description; } diff --git a/src/CSF.Core/Core/Attributes/GroupAttribute.cs b/src/CSF.Core/Core/Attributes/GroupAttribute.cs index 09b3fb0..ddc39f7 100644 --- a/src/CSF.Core/Core/Attributes/GroupAttribute.cs +++ b/src/CSF.Core/Core/Attributes/GroupAttribute.cs @@ -38,22 +38,22 @@ public GroupAttribute([DisallowNull] string name) public GroupAttribute([DisallowNull] string name, params string[] aliases) { if (string.IsNullOrWhiteSpace(name)) - ThrowHelpers.ArgMissing(name); + ThrowHelpers.InvalidArg(name); var arr = new string[aliases.Length + 1]; for (int i = 0; i < aliases.Length; i++) { if (string.IsNullOrWhiteSpace(aliases[i])) - ThrowHelpers.ArgMissing(aliases); + ThrowHelpers.InvalidArg(aliases); if (arr.Contains(aliases[i])) - ThrowHelpers.RangeDuplicate(aliases); + ThrowHelpers.NotDistinct(aliases); arr[i + 1] = aliases[i]; } if (arr.Contains(name)) - ThrowHelpers.RangeDuplicate(aliases); + ThrowHelpers.NotDistinct(aliases); arr[0] = name; diff --git a/src/CSF.Core/Core/CommandConfiguration.cs b/src/CSF.Core/Core/CommandConfiguration.cs index 333bcd2..c1baa06 100644 --- a/src/CSF.Core/Core/CommandConfiguration.cs +++ b/src/CSF.Core/Core/CommandConfiguration.cs @@ -1,14 +1,155 @@ -using CSF.TypeReaders; +using CSF.Helpers; +using CSF.TypeReaders; +using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Reflection.Metadata.Ecma335; namespace CSF.Core { - public sealed class CommandConfiguration + public class CommandConfiguration { - public Assembly[] Assemblies { get; set; } = [ Assembly.GetEntryAssembly() ]; + private Assembly[] _assemblies = [ Assembly.GetExecutingAssembly() ]; - public TypeReader[] TypeReaders { get; set; } = []; + public Assembly[] Assemblies + { + get + { + return _assemblies; + } + } + + private TypeReader[] _typeReaders = []; + + public TypeReader[] TypeReaders + { + get + { + return _typeReaders; + } + } + + private ResultResolver _resultResolver = ResultResolver.Default; + + public ResultResolver ResultResolver + { + get + { + return _resultResolver; + } + } public TaskAwaitOptions ExecutionPattern { get; set; } + + public CommandConfiguration WithAssemblies(params Assembly[] assemblies) + { + if (assemblies == null) + { + ThrowHelpers.InvalidArg(assemblies); + } + + _assemblies = assemblies; + + return this; + } + + public CommandConfiguration AddAssembly(Assembly assembly) + { + if (assembly == null) + { + ThrowHelpers.InvalidArg(assembly); + } + + if (_assemblies.Contains(assembly)) + { + ThrowHelpers.NotDistinct(assembly); + } + + AddAsm(assembly); + + return this; + } + + public CommandConfiguration TryAddAssembly(Assembly assembly) + { + if (assembly != null) + { + if (!_assemblies.Contains(assembly)) + { + AddAsm(assembly); + } + } + + return this; + } + + public CommandConfiguration WithTypeReaders(params TypeReader[] typeReaders) + { + if (typeReaders == null) + { + ThrowHelpers.InvalidArg(typeReaders); + } + + _typeReaders = typeReaders.Distinct(TypeReaderEqualityComparer.Default).ToArray(); + + return this; + } + + public CommandConfiguration AddTypeReader(TypeReader typeReader) + { + if (typeReader == null) + { + ThrowHelpers.InvalidArg(typeReader); + } + + if (_typeReaders.Contains(typeReader, TypeReaderEqualityComparer.Default)) + { + ThrowHelpers.NotDistinct(typeReader); + } + + AddTr(typeReader); + + return this; + } + + public CommandConfiguration TryAddTypeReader(TypeReader typeReader) + { + if (typeReader != null) + { + if (!_typeReaders.Contains(typeReader, TypeReaderEqualityComparer.Default)) + { + AddTr(typeReader); + } + } + + return this; + } + + public CommandConfiguration ConfigureResultAction([DisallowNull] Func configureDelegate) + { + if (configureDelegate == null) + { + ThrowHelpers.InvalidArg(configureDelegate); + } + + _resultResolver = new ResultResolver(configureDelegate); + + return this; + } + + private void AddAsm(Assembly assembly) + { + var oLen = _assemblies.Length; + Array.Resize(ref _assemblies, oLen); + + _assemblies[oLen] = assembly; + } + + private void AddTr(TypeReader typeReader) + { + var oLen = _typeReaders.Length; + Array.Resize(ref _typeReaders, oLen); + + _typeReaders[oLen] = typeReader; + } } } diff --git a/src/CSF.Core/Core/CommandManager.cs b/src/CSF.Core/Core/CommandManager.cs index 0cd4141..ea90e4c 100644 --- a/src/CSF.Core/Core/CommandManager.cs +++ b/src/CSF.Core/Core/CommandManager.cs @@ -12,24 +12,23 @@ namespace CSF.Core public class CommandManager : IDisposable { private readonly object _searchLock = new(); + private readonly ResultResolver _resultHandle; public IReadOnlySet Components { get; } public IServiceProvider Services { get; } - public IResultHandler ResultHandler { get; } - public TypeReader[] TypeReaders { get; } public CommandConfiguration Configuration { get; } public CommandManager(IServiceProvider services, CommandConfiguration configuration) { - TypeReaders = configuration.TypeReaders.Distinct().ToArray(); + TypeReaders = configuration.TypeReaders.Distinct(TypeReaderEqualityComparer.Default).ToArray(); if (configuration.Assemblies == null || configuration.Assemblies.Length == 0) { - ThrowHelpers.ArgMissing(nameof(configuration.Assemblies)); + ThrowHelpers.InvalidArg(nameof(configuration.Assemblies)); } Components = BuildComponents(configuration) @@ -38,7 +37,7 @@ public CommandManager(IServiceProvider services, CommandConfiguration configurat Services = services; - ResultHandler = services.GetService(); + _resultHandle = services.GetService() ?? ResultResolver.Default; Configuration = configuration; } @@ -94,7 +93,8 @@ private async Task ExecuteInternalAsync(ICommandContext context, object[] args, if (match.Success) { var result = await RunAsync(context, match, cancellationToken); - await ResultHandler.HandleAsync(context, result, cancellationToken); + + await _resultHandle.TryHandleAsync(context, result, Services); return; } @@ -105,13 +105,13 @@ private async Task ExecuteInternalAsync(ICommandContext context, object[] args, // if no searches were found, we send searchfailure. if (c is 0) { - await ResultHandler.HandleAsync(context, new SearchResult(new SearchException("No commands were found with the provided input.")), cancellationToken); + await _resultHandle.TryHandleAsync(context, new SearchResult(new SearchException("No commands were found with the provided input.")), Services); } // if there is a fallback present, we send matchfailure. if (context.TryGetFallback(out var fallback)) { - await ResultHandler.HandleAsync(context, fallback, cancellationToken); + await _resultHandle.TryHandleAsync(context, fallback, Services); } } #endregion diff --git a/src/CSF.Core/Core/Execution/IResultHandler.cs b/src/CSF.Core/Core/Execution/IResultHandler.cs deleted file mode 100644 index 0a5c932..0000000 --- a/src/CSF.Core/Core/Execution/IResultHandler.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CSF.Core -{ - public interface IResultHandler : IDisposable - { - public Task HandleAsync(ICommandContext context, ICommandResult result, CancellationToken cancellationToken); - } -} diff --git a/src/CSF.Core/Core/Results/ResultResolver.cs b/src/CSF.Core/Core/Results/ResultResolver.cs new file mode 100644 index 0000000..d41d124 --- /dev/null +++ b/src/CSF.Core/Core/Results/ResultResolver.cs @@ -0,0 +1,35 @@ +using CSF.Helpers; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSF.Core +{ + public sealed class ResultResolver(Func handler) + { + private static readonly ResultResolver _i = new(null); + + public Func Handler { get; } = handler; + + public Task TryHandleAsync(ICommandContext context, ICommandResult result, IServiceProvider services) + { + if (Handler == null) + { + return Task.CompletedTask; + } + + return Handler(context, result, services); + } + + public static ResultResolver Default + { + get + { + return _i; + } + } + } +} diff --git a/src/CSF.Core/Helpers/ThrowHelpers.cs b/src/CSF.Core/Helpers/ThrowHelpers.cs index f942b3b..10e6707 100644 --- a/src/CSF.Core/Helpers/ThrowHelpers.cs +++ b/src/CSF.Core/Helpers/ThrowHelpers.cs @@ -1,4 +1,5 @@ using CSF.Exceptions; +using System.Collections; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -17,16 +18,46 @@ public static void InvalidOp([DisallowNull] string failureMessage) [DoesNotReturn] [EditorBrowsable(EditorBrowsableState.Never)] - public static void ArgMissing(object value, [CallerArgumentExpression(nameof(value))] string arg = null) + public static void InvalidArg(object value, [CallerArgumentExpression(nameof(value))] string arg = null) { - throw new ArgumentMissingException(arg, "Argument is not in valid state, being null, empty or whitespace."); + // should resolve overload when a collection is null or empty. + if (value is ICollection or IEnumerable) + { + // if invalidarg is called on not null collection, it must mean it needs a value. + if (value != null) + { + throw new ArgumentException( + message: "Provided collection must be initialized with at least one argument. It is not allowed to be null or empty.", + paramName: arg); + } + + // thrown when collection is null. + throw new ArgumentException( + message: "Provided collection must be initialized. It is not allowed to be null.", + paramName: arg); + } + + // if value is a string, this check ensures that more data is provided at throw, regarding the state of the string. + if (value is string) + { + throw new ArgumentMissingException( + message: "Provided string must carry at least a character that is not whitespace. It is not allowed to be null or empty.", + paramName: arg); + } + + // throw argument null with custom message in the situation where none of the above overloads are relevant. + throw new ArgumentException( + message: "Provided argument must carry a value. It is not allowed to be null.", + paramName: arg); } [DoesNotReturn] [EditorBrowsable(EditorBrowsableState.Never)] - public static void RangeDuplicate(object value, [CallerArgumentExpression(nameof(value))] string arg = null) + public static void NotDistinct(object value, [CallerArgumentExpression(nameof(value))] string arg = null) { - throw new RangeDuplicateException(arg, "Range contains a duplicate value, which is not supported by the implementation."); + throw new ArgumentException( + message: "Provided collection cannot contain duplicate values.", + paramName: arg); } } } diff --git a/src/CSF.Core/Preconditions/PreconditionAttribute.cs b/src/CSF.Core/Preconditions/PreconditionAttribute.cs index 0b14fe6..df6e72e 100644 --- a/src/CSF.Core/Preconditions/PreconditionAttribute.cs +++ b/src/CSF.Core/Preconditions/PreconditionAttribute.cs @@ -16,7 +16,7 @@ public abstract class PreconditionAttribute : Attribute public static CheckResult Error([DisallowNull] Exception exception) { if (exception == null) - ThrowHelpers.ArgMissing(exception); + ThrowHelpers.InvalidArg(exception); if (exception is CheckException checkEx) { @@ -28,7 +28,7 @@ public static CheckResult Error([DisallowNull] Exception exception) public virtual CheckResult Error([DisallowNull] string error) { if (string.IsNullOrEmpty(error)) - ThrowHelpers.ArgMissing(error); + ThrowHelpers.InvalidArg(error); return new(new CheckException(error)); } diff --git a/src/CSF.Core/TypeReaders/TypeReader.cs b/src/CSF.Core/TypeReaders/TypeReader.cs index 5210d3d..65e076d 100644 --- a/src/CSF.Core/TypeReaders/TypeReader.cs +++ b/src/CSF.Core/TypeReaders/TypeReader.cs @@ -35,7 +35,7 @@ internal ValueTask ObjectEvaluateAsync(ICommandContext context, IArg public virtual ReadResult Error([DisallowNull] Exception exception) { if (exception == null) - ThrowHelpers.ArgMissing(exception); + ThrowHelpers.InvalidArg(exception); if (exception is ReadException readEx) { @@ -47,7 +47,7 @@ public virtual ReadResult Error([DisallowNull] Exception exception) public virtual ReadResult Error([DisallowNull] string error) { if (string.IsNullOrEmpty(error)) - ThrowHelpers.ArgMissing(error); + ThrowHelpers.InvalidArg(error); return new(new ReadException(error)); } diff --git a/src/CSF.Core/TypeReaders/TypeReaderEqualityComparer.cs b/src/CSF.Core/TypeReaders/TypeReaderEqualityComparer.cs new file mode 100644 index 0000000..579003c --- /dev/null +++ b/src/CSF.Core/TypeReaders/TypeReaderEqualityComparer.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSF.TypeReaders +{ + public sealed class TypeReaderEqualityComparer : IEqualityComparer + { + private static readonly TypeReaderEqualityComparer _i = new(); + + public bool Equals(TypeReader x, TypeReader y) + { + if (x == y) + return true; + + if (x.Type == y.Type) + return true; + + return false; + } + + public int GetHashCode([DisallowNull] TypeReader obj) + { + return obj.GetHashCode(); + } + + public static TypeReaderEqualityComparer Default + { + get + { + return _i; + } + } + } +} diff --git a/src/CSF.Hosting/Hosting/HostedCommandManager.cs b/src/CSF.Hosting/Hosting/HostedCommandManager.cs index e28a894..8ae4071 100644 --- a/src/CSF.Hosting/Hosting/HostedCommandManager.cs +++ b/src/CSF.Hosting/Hosting/HostedCommandManager.cs @@ -42,12 +42,12 @@ internal async Task RunAsync(CancellationToken cancellationToken) if (args == null) { - ThrowHelpers.ArgMissing(args); + ThrowHelpers.InvalidArg(args); } if (args.Length == 0) { - ThrowHelpers.ArgMissing(args); + ThrowHelpers.InvalidArg(args); } await ExecuteAsync(args, cancellationToken).ConfigureAwait(false); diff --git a/src/CSF.Tests.Hosting/Factory.cs b/src/CSF.Tests.Hosting/Factory.cs index 7207d6d..267dccd 100644 --- a/src/CSF.Tests.Hosting/Factory.cs +++ b/src/CSF.Tests.Hosting/Factory.cs @@ -29,7 +29,7 @@ public ValueTask CreateArgsAsync(CancellationToken cancellationToken) if (args.Length == 0) { - ThrowHelpers.ArgMissing(args); + ThrowHelpers.InvalidArg(args); } return ValueTask.FromResult(args); diff --git a/src/CSF.Tests.Hosting/Program.cs b/src/CSF.Tests.Hosting/Program.cs index 57c4f70..fd59567 100644 --- a/src/CSF.Tests.Hosting/Program.cs +++ b/src/CSF.Tests.Hosting/Program.cs @@ -6,9 +6,21 @@ using System.Reflection; await Host.CreateDefaultBuilder(args) - .WithCommands((context, config) => + .WithCommands((context, configuration) => { - config.Assemblies = [ Assembly.GetEntryAssembly() ]; + configuration.TryAddAssembly(Assembly.GetEntryAssembly()); + + configuration.ConfigureResultAction(async (context, result, services) => + { + if (result.Success) + { + + } + else + { + + } + }); }) .ConfigureLogging(x => { From 4e43d0ef751a46b3ac57429364e235bf5bc130b2 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Tue, 30 Jan 2024 15:33:35 +0100 Subject: [PATCH 13/40] Finalize configuration rework --- src/CSF.Core/Core/CommandConfiguration.cs | 3 +-- src/CSF.Core/Core/CommandManager.cs | 6 +++--- .../{TaskAwaitOptions.cs => AsyncApproach.cs} | 2 +- src/CSF.Core/Helpers/ServiceHelpers.cs | 12 +++++------ src/CSF.Hosting/Helpers/ServiceHelpers.cs | 6 +++--- src/CSF.Tests.Console/Program.cs | 20 +++++++++++++++++-- src/CSF.Tests.Hosting/Program.cs | 9 +++++---- 7 files changed, 37 insertions(+), 21 deletions(-) rename src/CSF.Core/Core/Configuration/{TaskAwaitOptions.cs => AsyncApproach.cs} (87%) diff --git a/src/CSF.Core/Core/CommandConfiguration.cs b/src/CSF.Core/Core/CommandConfiguration.cs index c1baa06..9d82528 100644 --- a/src/CSF.Core/Core/CommandConfiguration.cs +++ b/src/CSF.Core/Core/CommandConfiguration.cs @@ -2,7 +2,6 @@ using CSF.TypeReaders; using System.Diagnostics.CodeAnalysis; using System.Reflection; -using System.Reflection.Metadata.Ecma335; namespace CSF.Core { @@ -38,7 +37,7 @@ public ResultResolver ResultResolver } } - public TaskAwaitOptions ExecutionPattern { get; set; } + public AsyncApproach AsyncApproach { get; set; } public CommandConfiguration WithAssemblies(params Assembly[] assemblies) { diff --git a/src/CSF.Core/Core/CommandManager.cs b/src/CSF.Core/Core/CommandManager.cs index ea90e4c..fabe7b8 100644 --- a/src/CSF.Core/Core/CommandManager.cs +++ b/src/CSF.Core/Core/CommandManager.cs @@ -50,15 +50,15 @@ public Task ExecuteAsync(ICommandContext context, params object[] args) public async Task ExecuteAsync(ICommandContext context, object[] args, CancellationToken cancellationToken = default) { - switch (Configuration.ExecutionPattern) + switch (Configuration.AsyncApproach) { - case TaskAwaitOptions.Await: + case AsyncApproach.Await: { context.LogDebug("Starting execution. Execution pattern = [Await]."); await ExecuteInternalAsync(context, args, cancellationToken); } return; - case TaskAwaitOptions.Discard: + case AsyncApproach.Discard: { context.LogDebug("Starting execution. Execution pattern = [Discard]."); _ = ExecuteInternalAsync(context, args, cancellationToken); diff --git a/src/CSF.Core/Core/Configuration/TaskAwaitOptions.cs b/src/CSF.Core/Core/Configuration/AsyncApproach.cs similarity index 87% rename from src/CSF.Core/Core/Configuration/TaskAwaitOptions.cs rename to src/CSF.Core/Core/Configuration/AsyncApproach.cs index 3f98ee6..8200292 100644 --- a/src/CSF.Core/Core/Configuration/TaskAwaitOptions.cs +++ b/src/CSF.Core/Core/Configuration/AsyncApproach.cs @@ -6,7 +6,7 @@ namespace CSF.Core { - public enum TaskAwaitOptions + public enum AsyncApproach { Default = Await, diff --git a/src/CSF.Core/Helpers/ServiceHelpers.cs b/src/CSF.Core/Helpers/ServiceHelpers.cs index 882368b..da4cc33 100644 --- a/src/CSF.Core/Helpers/ServiceHelpers.cs +++ b/src/CSF.Core/Helpers/ServiceHelpers.cs @@ -9,21 +9,21 @@ namespace CSF.Helpers [EditorBrowsable(EditorBrowsableState.Advanced)] public static class ServiceHelpers { - public static IServiceCollection WithCommands(this IServiceCollection collection, [DisallowNull] Action action) + public static IServiceCollection ConfigureCommands(this IServiceCollection collection, [DisallowNull] Action configureDelegate) { - collection.WithCommands(action); + collection.WithCommands(configureDelegate); return collection; } - public static IServiceCollection WithCommands(this IServiceCollection collection, [DisallowNull] Action action) + public static IServiceCollection WithCommands(this IServiceCollection collection, [DisallowNull] Action configureDelegate) where T : CommandManager { - var cmdConf = new CommandConfiguration(); + var configuration = new CommandConfiguration(); - action(cmdConf); + configureDelegate(configuration); - collection.WithCommands(cmdConf); + collection.WithCommands(configuration); return collection; } diff --git a/src/CSF.Hosting/Helpers/ServiceHelpers.cs b/src/CSF.Hosting/Helpers/ServiceHelpers.cs index 2c45ec0..bfc386d 100644 --- a/src/CSF.Hosting/Helpers/ServiceHelpers.cs +++ b/src/CSF.Hosting/Helpers/ServiceHelpers.cs @@ -11,13 +11,13 @@ namespace CSF.Helpers [EditorBrowsable(EditorBrowsableState.Advanced)] public static class ServiceHelpers { - public static IHostBuilder WithCommands(this IHostBuilder builder, [DisallowNull] Action configureDelegate) + public static IHostBuilder ConfigureCommands(this IHostBuilder builder, [DisallowNull] Action configureDelegate) where TFactory : class, IActionFactory { - return builder.WithCommands(configureDelegate); + return builder.ConfigureCommands(configureDelegate); } - public static IHostBuilder WithCommands(this IHostBuilder builder, [DisallowNull] Action configureDelegate) + public static IHostBuilder ConfigureCommands(this IHostBuilder builder, [DisallowNull] Action configureDelegate) where TManager : HostedCommandManager where TFactory : class, IActionFactory { builder.ConfigureServices((context, services) => diff --git a/src/CSF.Tests.Console/Program.cs b/src/CSF.Tests.Console/Program.cs index 48f1dff..7dd48fd 100644 --- a/src/CSF.Tests.Console/Program.cs +++ b/src/CSF.Tests.Console/Program.cs @@ -1,10 +1,26 @@ using CSF.Core; -using CSF.Parsing; using CSF.Helpers; +using CSF.Parsing; using Microsoft.Extensions.DependencyInjection; +using System.Reflection; var collection = new ServiceCollection() - .WithCommands(_ => { }); + .ConfigureCommands(configuration => + { + configuration.AsyncApproach = AsyncApproach.Await; + configuration.ConfigureResultAction(async (context, result, services) => + { + if (result.Success) + { + + } + else + { + + } + }); + configuration.WithAssemblies(Assembly.GetEntryAssembly()); + }); var services = collection.BuildServiceProvider(); diff --git a/src/CSF.Tests.Hosting/Program.cs b/src/CSF.Tests.Hosting/Program.cs index fd59567..5b676c7 100644 --- a/src/CSF.Tests.Hosting/Program.cs +++ b/src/CSF.Tests.Hosting/Program.cs @@ -1,15 +1,16 @@ -using CSF.Helpers; -using CSF.Hosting; +using CSF.Core; +using CSF.Helpers; using CSF.Tests.Hosting; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Reflection; await Host.CreateDefaultBuilder(args) - .WithCommands((context, configuration) => + .ConfigureCommands((context, configuration) => { - configuration.TryAddAssembly(Assembly.GetEntryAssembly()); + configuration.AsyncApproach = AsyncApproach.Await; + configuration.TryAddAssembly(Assembly.GetEntryAssembly()); configuration.ConfigureResultAction(async (context, result, services) => { if (result.Success) From c6da199430fc43e837d8d43fd8ae1d776cd200ed Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Tue, 30 Jan 2024 15:34:02 +0100 Subject: [PATCH 14/40] Analyzers yippee --- src/CSF.Core/Core/CommandConfiguration.cs | 2 +- src/CSF.Core/Core/CommandManager.cs | 5 ++--- src/CSF.Core/Core/Configuration/AsyncApproach.cs | 8 +------- src/CSF.Core/Core/Results/ICommandResult.cs | 4 +--- src/CSF.Core/Core/Results/ResultResolver.cs | 10 +--------- src/CSF.Core/Helpers/ThrowHelpers.cs | 6 +++--- src/CSF.Core/TypeReaders/TypeReaderEqualityComparer.cs | 7 +------ src/CSF.Hosting/Hosting/Execution/IActionFactory.cs | 2 +- src/CSF.Hosting/Hosting/HostedCommandManager.cs | 2 +- src/CSF.Tests.Hosting/Factory.cs | 7 +------ src/CSF.Tests.Hosting/Program.cs | 2 +- 11 files changed, 14 insertions(+), 41 deletions(-) diff --git a/src/CSF.Core/Core/CommandConfiguration.cs b/src/CSF.Core/Core/CommandConfiguration.cs index 9d82528..e0627f5 100644 --- a/src/CSF.Core/Core/CommandConfiguration.cs +++ b/src/CSF.Core/Core/CommandConfiguration.cs @@ -7,7 +7,7 @@ namespace CSF.Core { public class CommandConfiguration { - private Assembly[] _assemblies = [ Assembly.GetExecutingAssembly() ]; + private Assembly[] _assemblies = [Assembly.GetExecutingAssembly()]; public Assembly[] Assemblies { diff --git a/src/CSF.Core/Core/CommandManager.cs b/src/CSF.Core/Core/CommandManager.cs index fabe7b8..55e6a69 100644 --- a/src/CSF.Core/Core/CommandManager.cs +++ b/src/CSF.Core/Core/CommandManager.cs @@ -3,7 +3,6 @@ using CSF.Reflection; using CSF.TypeReaders; using Microsoft.Extensions.DependencyInjection; -using System.ComponentModel; [assembly: CLSCompliant(true)] @@ -37,7 +36,7 @@ public CommandManager(IServiceProvider services, CommandConfiguration configurat Services = services; - _resultHandle = services.GetService() ?? ResultResolver.Default; + _resultHandle = services.GetService() ?? ResultResolver.Default; Configuration = configuration; } @@ -246,7 +245,7 @@ public void Dispose() public override string ToString() { - + } } } diff --git a/src/CSF.Core/Core/Configuration/AsyncApproach.cs b/src/CSF.Core/Core/Configuration/AsyncApproach.cs index 8200292..352c749 100644 --- a/src/CSF.Core/Core/Configuration/AsyncApproach.cs +++ b/src/CSF.Core/Core/Configuration/AsyncApproach.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CSF.Core +namespace CSF.Core { public enum AsyncApproach { diff --git a/src/CSF.Core/Core/Results/ICommandResult.cs b/src/CSF.Core/Core/Results/ICommandResult.cs index a241e95..87f8742 100644 --- a/src/CSF.Core/Core/Results/ICommandResult.cs +++ b/src/CSF.Core/Core/Results/ICommandResult.cs @@ -1,6 +1,4 @@ -using System.Diagnostics.CodeAnalysis; - -namespace CSF +namespace CSF { public interface ICommandResult { diff --git a/src/CSF.Core/Core/Results/ResultResolver.cs b/src/CSF.Core/Core/Results/ResultResolver.cs index d41d124..e0e3d98 100644 --- a/src/CSF.Core/Core/Results/ResultResolver.cs +++ b/src/CSF.Core/Core/Results/ResultResolver.cs @@ -1,12 +1,4 @@ -using CSF.Helpers; -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CSF.Core +namespace CSF.Core { public sealed class ResultResolver(Func handler) { diff --git a/src/CSF.Core/Helpers/ThrowHelpers.cs b/src/CSF.Core/Helpers/ThrowHelpers.cs index 10e6707..9331ee9 100644 --- a/src/CSF.Core/Helpers/ThrowHelpers.cs +++ b/src/CSF.Core/Helpers/ThrowHelpers.cs @@ -33,7 +33,7 @@ public static void InvalidArg(object value, [CallerArgumentExpression(nameof(val // thrown when collection is null. throw new ArgumentException( - message: "Provided collection must be initialized. It is not allowed to be null.", + message: "Provided collection must be initialized. It is not allowed to be null.", paramName: arg); } @@ -41,7 +41,7 @@ public static void InvalidArg(object value, [CallerArgumentExpression(nameof(val if (value is string) { throw new ArgumentMissingException( - message: "Provided string must carry at least a character that is not whitespace. It is not allowed to be null or empty.", + message: "Provided string must carry at least a character that is not whitespace. It is not allowed to be null or empty.", paramName: arg); } @@ -56,7 +56,7 @@ public static void InvalidArg(object value, [CallerArgumentExpression(nameof(val public static void NotDistinct(object value, [CallerArgumentExpression(nameof(value))] string arg = null) { throw new ArgumentException( - message: "Provided collection cannot contain duplicate values.", + message: "Provided collection cannot contain duplicate values.", paramName: arg); } } diff --git a/src/CSF.Core/TypeReaders/TypeReaderEqualityComparer.cs b/src/CSF.Core/TypeReaders/TypeReaderEqualityComparer.cs index 579003c..b79f3d4 100644 --- a/src/CSF.Core/TypeReaders/TypeReaderEqualityComparer.cs +++ b/src/CSF.Core/TypeReaders/TypeReaderEqualityComparer.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; namespace CSF.TypeReaders { diff --git a/src/CSF.Hosting/Hosting/Execution/IActionFactory.cs b/src/CSF.Hosting/Hosting/Execution/IActionFactory.cs index 3712048..7d56b7d 100644 --- a/src/CSF.Hosting/Hosting/Execution/IActionFactory.cs +++ b/src/CSF.Hosting/Hosting/Execution/IActionFactory.cs @@ -12,7 +12,7 @@ public interface IActionFactory : IDisposable public interface IActionFactory : IActionFactory where T : ICommandContext { - public new ValueTask CreateContextAsync(CancellationToken cancellationToken); + public new ValueTask CreateContextAsync(CancellationToken cancellationToken); async ValueTask IActionFactory.CreateContextAsync(CancellationToken cancellationToken) { diff --git a/src/CSF.Hosting/Hosting/HostedCommandManager.cs b/src/CSF.Hosting/Hosting/HostedCommandManager.cs index 8ae4071..cb052f3 100644 --- a/src/CSF.Hosting/Hosting/HostedCommandManager.cs +++ b/src/CSF.Hosting/Hosting/HostedCommandManager.cs @@ -66,7 +66,7 @@ public Task StopAsync(CancellationToken cancellationToken) public void Dispose() { - + } } } diff --git a/src/CSF.Tests.Hosting/Factory.cs b/src/CSF.Tests.Hosting/Factory.cs index 267dccd..aea1496 100644 --- a/src/CSF.Tests.Hosting/Factory.cs +++ b/src/CSF.Tests.Hosting/Factory.cs @@ -2,11 +2,6 @@ using CSF.Hosting; using CSF.Parsing; using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace CSF.Tests.Hosting { @@ -47,7 +42,7 @@ public ValueTask CreateContextAsync(CancellationToken canc public void Dispose() { - + } } } diff --git a/src/CSF.Tests.Hosting/Program.cs b/src/CSF.Tests.Hosting/Program.cs index 5b676c7..f4e5cbd 100644 --- a/src/CSF.Tests.Hosting/Program.cs +++ b/src/CSF.Tests.Hosting/Program.cs @@ -19,7 +19,7 @@ await Host.CreateDefaultBuilder(args) } else { - + } }); }) From a50c42eb8d93f10edf66a3f705fe840f2041cd58 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Tue, 30 Jan 2024 15:58:45 +0100 Subject: [PATCH 15/40] Document configuration --- src/CSF.Core/Core/CommandConfiguration.cs | 86 ++++++++++++++++++++++- src/CSF.Core/Core/CommandManager.cs | 5 -- 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/src/CSF.Core/Core/CommandConfiguration.cs b/src/CSF.Core/Core/CommandConfiguration.cs index e0627f5..5d58cb4 100644 --- a/src/CSF.Core/Core/CommandConfiguration.cs +++ b/src/CSF.Core/Core/CommandConfiguration.cs @@ -5,10 +5,19 @@ namespace CSF.Core { + /// + /// Represents the configuration to be implemented by a new . + /// public class CommandConfiguration { private Assembly[] _assemblies = [Assembly.GetExecutingAssembly()]; + /// + /// Gets a collection of assemblies that the will use to register commands. + /// + /// + /// If the collection has not been altered, it will include as the default value. + /// public Assembly[] Assemblies { get @@ -19,6 +28,13 @@ public Assembly[] Assemblies private TypeReader[] _typeReaders = []; + /// + /// Gets a collection of 's that the will use to handle unknown argument types. + /// + /// + /// It is adviced not to create new implementations of without first confirming if the target type is not already supported. + /// All valuetypes and time/date types are already supported out of the box. + /// public TypeReader[] TypeReaders { get @@ -29,6 +45,9 @@ public TypeReader[] TypeReaders private ResultResolver _resultResolver = ResultResolver.Default; + /// + /// Gets a resolver that contains a handler for handling command post-execution data. + /// public ResultResolver ResultResolver { get @@ -37,8 +56,24 @@ public ResultResolver ResultResolver } } - public AsyncApproach AsyncApproach { get; set; } - + /// + /// Gets or sets the approach to asynchronousity in commands. + /// + /// + /// If set to , the manager will wait for a command to finish before allowing another to be executed. + /// If set to , the manager will seperate the command execution from the entry stack, and slip it to another thread. + /// Only change this value if you have read the documentation of and understand the definitions. + /// + public AsyncApproach AsyncApproach { get; set; } = AsyncApproach.Default; + + /// + /// Replaces the existing values in with a new collection. + /// + /// + /// To prevent duplicate value recognition, is called to remove duplicates from . + /// + /// A collection of targets to be used to register commands. + /// The same for call chaining. public CommandConfiguration WithAssemblies(params Assembly[] assemblies) { if (assemblies == null) @@ -46,11 +81,19 @@ public CommandConfiguration WithAssemblies(params Assembly[] assemblies) ThrowHelpers.InvalidArg(assemblies); } - _assemblies = assemblies; + _assemblies = assemblies.Distinct().ToArray(); return this; } + /// + /// Adds an to . + /// + /// + /// This call will throw if already contains a value for . + /// + /// An target to be used to register commands. + /// The same for call chaining. public CommandConfiguration AddAssembly(Assembly assembly) { if (assembly == null) @@ -68,6 +111,14 @@ public CommandConfiguration AddAssembly(Assembly assembly) return this; } + /// + /// Attempts to add an to . + /// + /// + /// Will not add to if it already contains the target assembly. + /// + /// An target to be used to register commands. + /// The same for call chaining. public CommandConfiguration TryAddAssembly(Assembly assembly) { if (assembly != null) @@ -81,6 +132,14 @@ public CommandConfiguration TryAddAssembly(Assembly assembly) return this; } + /// + /// Replaces the existing values in with a new collection. + /// + /// + /// To prevent duplicate value recognition, is called to remove duplicates from . + /// + /// A collection of 's to parse unknown argument types. + /// The same for call chaining. public CommandConfiguration WithTypeReaders(params TypeReader[] typeReaders) { if (typeReaders == null) @@ -93,6 +152,14 @@ public CommandConfiguration WithTypeReaders(params TypeReader[] typeReaders) return this; } + /// + /// Adds a to . + /// + /// + /// This call will throw if already contains an implementation of the target type, validated by a custom equality comparer. + /// + /// A to parse unknown argument types. + /// The same for call chaining. public CommandConfiguration AddTypeReader(TypeReader typeReader) { if (typeReader == null) @@ -110,6 +177,14 @@ public CommandConfiguration AddTypeReader(TypeReader typeReader) return this; } + /// + /// Attempts to add a to . + /// + /// + /// Will not add to if it already contains an implementation of the target type, validated by a custom equality comparer. + /// + /// A to parse unknown argument types. + /// The same for call chaining. public CommandConfiguration TryAddTypeReader(TypeReader typeReader) { if (typeReader != null) @@ -123,6 +198,11 @@ public CommandConfiguration TryAddTypeReader(TypeReader typeReader) return this; } + /// + /// + /// + /// + /// The same for call chaining. public CommandConfiguration ConfigureResultAction([DisallowNull] Func configureDelegate) { if (configureDelegate == null) diff --git a/src/CSF.Core/Core/CommandManager.cs b/src/CSF.Core/Core/CommandManager.cs index 55e6a69..cd93010 100644 --- a/src/CSF.Core/Core/CommandManager.cs +++ b/src/CSF.Core/Core/CommandManager.cs @@ -242,10 +242,5 @@ public void Dispose() { } - - public override string ToString() - { - - } } } From 16708a8102ed2d22d1edebcd4d2dbd7b61c7766a Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Tue, 30 Jan 2024 17:05:12 +0100 Subject: [PATCH 16/40] lazy's, manager XML --- src/CSF.Core/Core/CommandConfiguration.cs | 36 +++++- src/CSF.Core/Core/CommandManager.cs | 114 +++++++++++++++--- src/CSF.Core/Core/Results/ResultResolver.cs | 4 +- src/CSF.Core/TypeReaders/TypeReader.cs | 29 +++++ .../TypeReaders/TypeReaderEqualityComparer.cs | 33 ----- .../Hosting/HostedCommandManager.cs | 2 +- src/CSF.Tests.Console/Program.cs | 2 +- 7 files changed, 159 insertions(+), 61 deletions(-) delete mode 100644 src/CSF.Core/TypeReaders/TypeReaderEqualityComparer.cs diff --git a/src/CSF.Core/Core/CommandConfiguration.cs b/src/CSF.Core/Core/CommandConfiguration.cs index 5d58cb4..b715ab3 100644 --- a/src/CSF.Core/Core/CommandConfiguration.cs +++ b/src/CSF.Core/Core/CommandConfiguration.cs @@ -46,7 +46,7 @@ public TypeReader[] TypeReaders private ResultResolver _resultResolver = ResultResolver.Default; /// - /// Gets a resolver that contains a handler for handling command post-execution data. + /// Gets or sets a resolver that contains a handler for handling command post-execution data. /// public ResultResolver ResultResolver { @@ -54,8 +54,19 @@ public ResultResolver ResultResolver { return _resultResolver; } + set + { + if (value == null) + { + ThrowHelpers.InvalidArg(value); + } + + _resultResolver = value; + } } + private AsyncApproach _asyncApproach = AsyncApproach.Default; + /// /// Gets or sets the approach to asynchronousity in commands. /// @@ -64,7 +75,22 @@ public ResultResolver ResultResolver /// If set to , the manager will seperate the command execution from the entry stack, and slip it to another thread. /// Only change this value if you have read the documentation of and understand the definitions. /// - public AsyncApproach AsyncApproach { get; set; } = AsyncApproach.Default; + public AsyncApproach AsyncApproach + { + get + { + return _asyncApproach; + } + set + { + if (value is not AsyncApproach.Await or AsyncApproach.Discard) + { + ThrowHelpers.InvalidOp("AsyncApproach does not support values that exceed the provided options, ranging between 0 and 1."); + } + + _asyncApproach = value; + } + } /// /// Replaces the existing values in with a new collection. @@ -147,7 +173,7 @@ public CommandConfiguration WithTypeReaders(params TypeReader[] typeReaders) ThrowHelpers.InvalidArg(typeReaders); } - _typeReaders = typeReaders.Distinct(TypeReaderEqualityComparer.Default).ToArray(); + _typeReaders = typeReaders.Distinct(TypeReader.EqualityComparer.Default).ToArray(); return this; } @@ -167,7 +193,7 @@ public CommandConfiguration AddTypeReader(TypeReader typeReader) ThrowHelpers.InvalidArg(typeReader); } - if (_typeReaders.Contains(typeReader, TypeReaderEqualityComparer.Default)) + if (_typeReaders.Contains(typeReader, TypeReader.EqualityComparer.Default)) { ThrowHelpers.NotDistinct(typeReader); } @@ -189,7 +215,7 @@ public CommandConfiguration TryAddTypeReader(TypeReader typeReader) { if (typeReader != null) { - if (!_typeReaders.Contains(typeReader, TypeReaderEqualityComparer.Default)) + if (!_typeReaders.Contains(typeReader, TypeReader.EqualityComparer.Default)) { AddTr(typeReader); } diff --git a/src/CSF.Core/Core/CommandManager.cs b/src/CSF.Core/Core/CommandManager.cs index cd93010..9615b7a 100644 --- a/src/CSF.Core/Core/CommandManager.cs +++ b/src/CSF.Core/Core/CommandManager.cs @@ -8,46 +8,95 @@ namespace CSF.Core { + /// + /// The root type serving as a basis for all operations and functionality as provided by the Command Standardization Framework. + /// + /// + /// This API is completely CLS compliant where it is supported, always implementing an overload that is CLS compliant, where it otherwise would not be. + /// public class CommandManager : IDisposable { private readonly object _searchLock = new(); private readonly ResultResolver _resultHandle; - public IReadOnlySet Components { get; } + /// + /// Gets the collection containing all commands, groups and subcommands as implemented by the assemblies that were registered in the provided when creating the manager. + /// + public IReadOnlySet Commands { get; } + /// + /// Gets the services used to create transient instances of modules that host command execution, with full support for dependency injection in mind. + /// public IServiceProvider Services { get; } - public TypeReader[] TypeReaders { get; } - + /// + /// Gets the configuration used to configure execution operations and registration options. + /// public CommandConfiguration Configuration { get; } + /// + /// Creates a new instance of with provided arguments. + /// + /// + /// It is suggested to configure and create the by calling . + /// Creating the manager manually will have a negative impact on performance, unless each is manually added to the as provided. + /// + /// A built collection of services that hosts services to be injected or received at request. + /// A configuration to be used to configure the execution and registration of commands. public CommandManager(IServiceProvider services, CommandConfiguration configuration) { - TypeReaders = configuration.TypeReaders.Distinct(TypeReaderEqualityComparer.Default).ToArray(); - if (configuration.Assemblies == null || configuration.Assemblies.Length == 0) { ThrowHelpers.InvalidArg(nameof(configuration.Assemblies)); } - Components = BuildComponents(configuration) + Commands = BuildComponents(configuration) .SelectMany(x => x.Components) .ToHashSet(); - Services = services; - - _resultHandle = services.GetService() ?? ResultResolver.Default; + services ??= ServiceProvider.Default; + Services = services; Configuration = configuration; - } - - public void Execute(ICommandContext context, params object[] args) - => ExecuteAsync(context, args).Wait(); - public Task ExecuteAsync(ICommandContext context, params object[] args) - => ExecuteAsync(context, args, cancellationToken: default); + _resultHandle = services.GetService() + ?? ResultResolver.Default; + } - public async Task ExecuteAsync(ICommandContext context, object[] args, CancellationToken cancellationToken = default) + /// + /// Makes an attempt at executing a command from provided . + /// + /// + /// The arguments intended for searching for a target need to be , as and store their aliases this way also. + /// + /// A command context that persist for the duration of the execution pipeline, serving as a metadata and logging container. + /// A set of arguments that are expected to discover, populate and invoke a target command. + public void TryExecute(ICommandContext context, params object[] args) + => TryExecuteAsync(context, args).Wait(); + + /// + /// Makes an attempt at executing a command from provided . + /// + /// + /// The arguments intended for searching for a target need to be , as and store their aliases this way also. + /// + /// A command context that persist for the duration of the execution pipeline, serving as a metadata and logging container. + /// A set of arguments that are expected to discover, populate and invoke a target command. + /// An awaitable hosting the state of execution. This task should be awaited, even if is set to . + public Task TryExecuteAsync(ICommandContext context, params object[] args) + => TryExecuteAsync(context, args, cancellationToken: default); + + /// + /// Makes an attempt at executing a command from provided . + /// + /// + /// The arguments intended for searching for a target need to be , as and store their aliases this way also. + /// + /// A command context that persist for the duration of the execution pipeline, serving as a metadata and logging container. + /// A set of arguments that are expected to discover, populate and invoke a target command. + /// A token that can be provided from a and later used to cancel asynchronous execution. + /// An awaitable hosting the state of execution. This task should be awaited, even if is set to . + public async Task TryExecuteAsync(ICommandContext context, object[] args, CancellationToken cancellationToken = default) { switch (Configuration.AsyncApproach) { @@ -66,12 +115,17 @@ public async Task ExecuteAsync(ICommandContext context, object[] args, Cancellat } } + /// + /// Searches all commands for any matches of . + /// + /// A set of arguments intended to discover commands as a query. + /// A lazily evaluated that holds the results of the search query. public IEnumerable Search(object[] args) { // recursively search for commands in the execution. lock (_searchLock) { - return Components.RecursiveSearch(args, 0); + return Commands.RecursiveSearch(args, 0); } } @@ -196,7 +250,11 @@ private async ValueTask RunAsync(ICommandContext context, MatchResult { context.LogInformation("Executing {} with {} resolved arguments.", match.Command, match.Reads.Length); - var module = Services.GetService(match.Command.Module.Type) as ModuleBase; + var targetInstance = Services.GetService(match.Command.Module.Type); + + var module = targetInstance != null + ? targetInstance as ModuleBase + : ActivatorUtilities.CreateInstance(Services, match.Command.Module.Type) as ModuleBase; module.Context = context; module.Command = match.Command; @@ -220,7 +278,7 @@ private async ValueTask RunAsync(ICommandContext context, MatchResult #region Building private IEnumerable BuildComponents(CommandConfiguration configuration) { - var typeReaders = TypeReader.CreateDefaultReaders().UnionBy(TypeReaders, x => x.Type).ToDictionary(x => x.Type, x => x); + var typeReaders = TypeReader.CreateDefaultReaders().UnionBy(configuration.TypeReaders, x => x.Type).ToDictionary(x => x.Type, x => x); var rootType = typeof(ModuleBase); foreach (var assembly in configuration.Assemblies) @@ -238,6 +296,24 @@ private IEnumerable BuildComponents(CommandConfiguration configurati } #endregion + internal class ServiceProvider : IServiceProvider + { + private static readonly Lazy _i = new(); + + public object GetService(Type serviceType) + { + return null; + } + + public static ServiceProvider Default + { + get + { + return _i.Value; + } + } + } + public void Dispose() { diff --git a/src/CSF.Core/Core/Results/ResultResolver.cs b/src/CSF.Core/Core/Results/ResultResolver.cs index e0e3d98..e29ab1d 100644 --- a/src/CSF.Core/Core/Results/ResultResolver.cs +++ b/src/CSF.Core/Core/Results/ResultResolver.cs @@ -2,7 +2,7 @@ { public sealed class ResultResolver(Func handler) { - private static readonly ResultResolver _i = new(null); + private static readonly Lazy _i = new(() => new ResultResolver(null)); public Func Handler { get; } = handler; @@ -20,7 +20,7 @@ public static ResultResolver Default { get { - return _i; + return _i.Value; } } } diff --git a/src/CSF.Core/TypeReaders/TypeReader.cs b/src/CSF.Core/TypeReaders/TypeReader.cs index 65e076d..527759d 100644 --- a/src/CSF.Core/TypeReaders/TypeReader.cs +++ b/src/CSF.Core/TypeReaders/TypeReader.cs @@ -69,5 +69,34 @@ internal static TypeReader[] CreateDefaultReaders() return range; } + + internal class EqualityComparer : IEqualityComparer + { + private static readonly Lazy _i = new(); + + public bool Equals(TypeReader x, TypeReader y) + { + if (x == y) + return true; + + if (x.Type == y.Type) + return true; + + return false; + } + + public int GetHashCode([DisallowNull] TypeReader obj) + { + return obj.GetHashCode(); + } + + public static EqualityComparer Default + { + get + { + return _i.Value; + } + } + } } } diff --git a/src/CSF.Core/TypeReaders/TypeReaderEqualityComparer.cs b/src/CSF.Core/TypeReaders/TypeReaderEqualityComparer.cs deleted file mode 100644 index b79f3d4..0000000 --- a/src/CSF.Core/TypeReaders/TypeReaderEqualityComparer.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace CSF.TypeReaders -{ - public sealed class TypeReaderEqualityComparer : IEqualityComparer - { - private static readonly TypeReaderEqualityComparer _i = new(); - - public bool Equals(TypeReader x, TypeReader y) - { - if (x == y) - return true; - - if (x.Type == y.Type) - return true; - - return false; - } - - public int GetHashCode([DisallowNull] TypeReader obj) - { - return obj.GetHashCode(); - } - - public static TypeReaderEqualityComparer Default - { - get - { - return _i; - } - } - } -} diff --git a/src/CSF.Hosting/Hosting/HostedCommandManager.cs b/src/CSF.Hosting/Hosting/HostedCommandManager.cs index cb052f3..39a3a50 100644 --- a/src/CSF.Hosting/Hosting/HostedCommandManager.cs +++ b/src/CSF.Hosting/Hosting/HostedCommandManager.cs @@ -22,7 +22,7 @@ public async Task ExecuteAsync(object[] args, CancellationToken cancellationToke { var context = await ActionFactory.CreateContextAsync(cancellationToken).ConfigureAwait(false); - await ExecuteAsync(context, args, cancellationToken); + await TryExecuteAsync(context, args, cancellationToken); } public Task StartAsync(CancellationToken cancellationToken) diff --git a/src/CSF.Tests.Console/Program.cs b/src/CSF.Tests.Console/Program.cs index 7dd48fd..2275fa5 100644 --- a/src/CSF.Tests.Console/Program.cs +++ b/src/CSF.Tests.Console/Program.cs @@ -32,5 +32,5 @@ { var input = parser.Parse(Console.ReadLine()!); - await framework.ExecuteAsync(null, input); + await framework.TryExecuteAsync(null, input); } \ No newline at end of file From c35e6f103be2ebd97b5964ab1169058115f7458e Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Tue, 30 Jan 2024 19:13:14 +0100 Subject: [PATCH 17/40] Exception & helper XML, small renames & clearing unused --- .../Exceptions/ArgumentMissingException.cs | 8 ---- src/CSF.Core/Exceptions/CheckException.cs | 4 +- src/CSF.Core/Exceptions/CommandException.cs | 10 ----- src/CSF.Core/Exceptions/ExecutionException.cs | 12 ++++++ src/CSF.Core/Exceptions/MatchException.cs | 5 +++ .../Exceptions/RangeDuplicateException.cs | 8 ---- src/CSF.Core/Exceptions/ReadException.cs | 4 +- src/CSF.Core/Exceptions/RunException.cs | 13 +++++++ src/CSF.Core/Exceptions/SearchException.cs | 4 +- src/CSF.Core/Helpers/CollectionHelpers.cs | 6 +-- src/CSF.Core/Helpers/ExecutionHelpers.cs | 5 +-- src/CSF.Core/Helpers/ReflectionHelpers.cs | 5 +-- src/CSF.Core/Helpers/ServiceHelpers.cs | 37 +++++++++++++++++-- src/CSF.Core/Helpers/ThrowHelpers.cs | 29 ++++++++++++--- .../Hosting/HostedCommandManager.cs | 2 + .../{Factory.cs => ActionFactory.cs} | 4 +- src/CSF.Tests.Hosting/Program.cs | 2 +- 17 files changed, 104 insertions(+), 54 deletions(-) delete mode 100644 src/CSF.Core/Exceptions/ArgumentMissingException.cs delete mode 100644 src/CSF.Core/Exceptions/CommandException.cs delete mode 100644 src/CSF.Core/Exceptions/RangeDuplicateException.cs create mode 100644 src/CSF.Core/Exceptions/RunException.cs rename src/CSF.Tests.Hosting/{Factory.cs => ActionFactory.cs} (90%) diff --git a/src/CSF.Core/Exceptions/ArgumentMissingException.cs b/src/CSF.Core/Exceptions/ArgumentMissingException.cs deleted file mode 100644 index a78d6ed..0000000 --- a/src/CSF.Core/Exceptions/ArgumentMissingException.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace CSF.Exceptions -{ - internal sealed class ArgumentMissingException(string paramName, string message) - : ArgumentException(message, paramName) - { - - } -} diff --git a/src/CSF.Core/Exceptions/CheckException.cs b/src/CSF.Core/Exceptions/CheckException.cs index 7ed48d0..d8955e1 100644 --- a/src/CSF.Core/Exceptions/CheckException.cs +++ b/src/CSF.Core/Exceptions/CheckException.cs @@ -1,8 +1,10 @@ namespace CSF.Exceptions { /// - /// Represents a that is thrown when no matched command succeeded its precondition checks. + /// Represents an that is thrown when a command failed precondition validation. /// + /// The message that represents the reason of the exception being thrown. + /// An exception thrown by an inner operation, if present. public sealed class CheckException(string message, Exception innerException = null) : ExecutionException(message, innerException) { diff --git a/src/CSF.Core/Exceptions/CommandException.cs b/src/CSF.Core/Exceptions/CommandException.cs deleted file mode 100644 index 52b9658..0000000 --- a/src/CSF.Core/Exceptions/CommandException.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace CSF.Exceptions -{ - /// - /// Represents a that is thrown when the command being executed failed to run its body. - /// - public sealed class CommandException(string message, Exception innerException = null) - : ExecutionException(message, innerException) - { - } -} diff --git a/src/CSF.Core/Exceptions/ExecutionException.cs b/src/CSF.Core/Exceptions/ExecutionException.cs index 609dc83..48281cc 100644 --- a/src/CSF.Core/Exceptions/ExecutionException.cs +++ b/src/CSF.Core/Exceptions/ExecutionException.cs @@ -1,13 +1,25 @@ namespace CSF.Exceptions { + /// + /// Represents an exception thrown anywhere in the command execution pipeline. + /// public class ExecutionException : Exception { + /// + /// Creates a new . + /// + /// The message that represents the reason of the exception being thrown. public ExecutionException(string message) : base(message) { } + /// + /// Creates a new . + /// + /// The message that represents the reason of the exception being thrown. + /// An exception thrown by an inner operation, if present. public ExecutionException(string message, Exception innerException = null) : base(message, innerException) { diff --git a/src/CSF.Core/Exceptions/MatchException.cs b/src/CSF.Core/Exceptions/MatchException.cs index cea2c86..9950bf0 100644 --- a/src/CSF.Core/Exceptions/MatchException.cs +++ b/src/CSF.Core/Exceptions/MatchException.cs @@ -1,5 +1,10 @@ namespace CSF.Exceptions { + /// + /// Represents an that is thrown when precondition validation or argument conversion failed. + /// + /// The message that represents the reason of the exception being thrown. + /// An exception thrown by an inner operation, if present. public class MatchException(string message, Exception innerException = null) : ExecutionException(message, innerException) { diff --git a/src/CSF.Core/Exceptions/RangeDuplicateException.cs b/src/CSF.Core/Exceptions/RangeDuplicateException.cs deleted file mode 100644 index 9d3d4ba..0000000 --- a/src/CSF.Core/Exceptions/RangeDuplicateException.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace CSF.Exceptions -{ - internal sealed class RangeDuplicateException(string paramName, string message) - : ArgumentException(paramName, message) - { - - } -} \ No newline at end of file diff --git a/src/CSF.Core/Exceptions/ReadException.cs b/src/CSF.Core/Exceptions/ReadException.cs index 1901f6f..f53533e 100644 --- a/src/CSF.Core/Exceptions/ReadException.cs +++ b/src/CSF.Core/Exceptions/ReadException.cs @@ -1,8 +1,10 @@ namespace CSF.Exceptions { /// - /// Represents a that is thrown when no matched command succeeded parsing its parameters. + /// Represents an that is thrown when no matched command succeeded parsing its parameters. /// + /// The message that represents the reason of the exception being thrown. + /// An exception thrown by an inner operation, if present. public sealed class ReadException(string message, Exception innerException = null) : ExecutionException(message, innerException) { diff --git a/src/CSF.Core/Exceptions/RunException.cs b/src/CSF.Core/Exceptions/RunException.cs new file mode 100644 index 0000000..7a0fea4 --- /dev/null +++ b/src/CSF.Core/Exceptions/RunException.cs @@ -0,0 +1,13 @@ +namespace CSF.Exceptions +{ + /// + /// Represents an that is thrown when the command being executed failed to finish invocation. + /// + /// The message that represents the reason of the exception being thrown. + /// An exception thrown by an inner operation, if present. + public sealed class RunException(string message, Exception innerException = null) + : ExecutionException(message, innerException) + { + + } +} diff --git a/src/CSF.Core/Exceptions/SearchException.cs b/src/CSF.Core/Exceptions/SearchException.cs index 64cbff9..7925e52 100644 --- a/src/CSF.Core/Exceptions/SearchException.cs +++ b/src/CSF.Core/Exceptions/SearchException.cs @@ -1,8 +1,10 @@ namespace CSF.Exceptions { /// - /// Represents a that is thrown when no command could be found. + /// Represents an that is thrown when no command could be found. /// + /// The message that represents the reason of the exception being thrown. + /// An exception thrown by an inner operation, if present. public sealed class SearchException(string message, Exception innerException = null) : ExecutionException(message, innerException) { diff --git a/src/CSF.Core/Helpers/CollectionHelpers.cs b/src/CSF.Core/Helpers/CollectionHelpers.cs index a1eae6f..a710403 100644 --- a/src/CSF.Core/Helpers/CollectionHelpers.cs +++ b/src/CSF.Core/Helpers/CollectionHelpers.cs @@ -3,10 +3,8 @@ namespace CSF.Helpers { - [EditorBrowsable(EditorBrowsableState.Advanced)] - public static class CollectionHelpers + internal static class CollectionHelpers { - [EditorBrowsable(EditorBrowsableState.Never)] public static IEnumerable CastWhere(this IEnumerable input) { foreach (var @in in input) @@ -14,7 +12,6 @@ public static IEnumerable CastWhere(this IEnumerable input) yield return @out; } - [EditorBrowsable(EditorBrowsableState.Never)] public static T SelectFirstOrDefault(this IEnumerable input, T defaultValue = default) { foreach (var @in in input) @@ -24,7 +21,6 @@ public static T SelectFirstOrDefault(this IEnumerable input, T defaultValue = return defaultValue; } - [EditorBrowsable(EditorBrowsableState.Never)] public static bool Contains(this IEnumerable input, bool allowMultipleMatches) where T : Attribute { diff --git a/src/CSF.Core/Helpers/ExecutionHelpers.cs b/src/CSF.Core/Helpers/ExecutionHelpers.cs index 608d13f..f332592 100644 --- a/src/CSF.Core/Helpers/ExecutionHelpers.cs +++ b/src/CSF.Core/Helpers/ExecutionHelpers.cs @@ -4,10 +4,8 @@ namespace CSF.Helpers { - [EditorBrowsable(EditorBrowsableState.Advanced)] - public static class ExecutionHelpers + internal static class ExecutionHelpers { - [EditorBrowsable(EditorBrowsableState.Never)] public static IEnumerable RecursiveSearch(this IEnumerable components, object[] args, int searchHeight) { List discovered = []; @@ -33,7 +31,6 @@ public static IEnumerable RecursiveSearch(this IEnumerable RecursiveReadAsync(this IArgument[] param, ICommandContext context, object[] args, int index, CancellationToken cancellationToken) { static async ValueTask ReadAsync(IArgument param, ICommandContext context, object arg, CancellationToken cancellationToken) diff --git a/src/CSF.Core/Helpers/ReflectionHelpers.cs b/src/CSF.Core/Helpers/ReflectionHelpers.cs index 6ace7a6..043d0c3 100644 --- a/src/CSF.Core/Helpers/ReflectionHelpers.cs +++ b/src/CSF.Core/Helpers/ReflectionHelpers.cs @@ -7,10 +7,8 @@ namespace CSF.Helpers { - [EditorBrowsable(EditorBrowsableState.Advanced)] - public static class ReflectionHelpers + internal static class ReflectionHelpers { - [EditorBrowsable(EditorBrowsableState.Never)] public static IEnumerable GetModules(ModuleInfo module, IDictionary typeReaders) { foreach (var group in module.Type.GetNestedTypes()) @@ -25,7 +23,6 @@ public static IEnumerable GetModules(ModuleInfo module, IDictionary< } } - [EditorBrowsable(EditorBrowsableState.Never)] public static IEnumerable GetCommands(ModuleInfo module, IDictionary typeReaders) { foreach (var method in module.Type.GetMethods()) diff --git a/src/CSF.Core/Helpers/ServiceHelpers.cs b/src/CSF.Core/Helpers/ServiceHelpers.cs index da4cc33..8a03c25 100644 --- a/src/CSF.Core/Helpers/ServiceHelpers.cs +++ b/src/CSF.Core/Helpers/ServiceHelpers.cs @@ -6,30 +6,53 @@ namespace CSF.Helpers { + /// + /// Represents a set of methods to populate and configure a . + /// [EditorBrowsable(EditorBrowsableState.Advanced)] public static class ServiceHelpers { + /// + /// Configures the to support use of a . + /// + /// + /// A delegate to configure the used to set up the to be generated by this . + /// The same for call chaining. public static IServiceCollection ConfigureCommands(this IServiceCollection collection, [DisallowNull] Action configureDelegate) { - collection.WithCommands(configureDelegate); + collection.ConfigureCommands(configureDelegate); return collection; } - public static IServiceCollection WithCommands(this IServiceCollection collection, [DisallowNull] Action configureDelegate) + /// + /// Configures the to support use of , an implementation of . + /// + /// The implementation to be generated by this . + /// + /// A delegate to configure the for setting up . + /// The same for call chaining. + public static IServiceCollection ConfigureCommands(this IServiceCollection collection, [DisallowNull] Action configureDelegate) where T : CommandManager { var configuration = new CommandConfiguration(); configureDelegate(configuration); - collection.WithCommands(configuration); + collection.AddCommandManager(configuration); return collection; } + /// + /// Adds the provided , all discovered 's and to the . + /// + /// The implementation to be generated by this . + /// + /// The configuration for setting up . + /// The same for call chaining. [EditorBrowsable(EditorBrowsableState.Never)] - public static IServiceCollection WithCommands(this IServiceCollection collection, CommandConfiguration configuration) + public static IServiceCollection AddCommandManager(this IServiceCollection collection, CommandConfiguration configuration) where T : CommandManager { collection.AddModules(configuration); @@ -40,6 +63,12 @@ public static IServiceCollection WithCommands(this IServiceCollection collect return collection; } + /// + /// Adds all discovered 's based on the collection of in . + /// + /// + /// The configuration for discovering 's + /// The same for call chaining. [EditorBrowsable(EditorBrowsableState.Never)] public static IServiceCollection AddModules(this IServiceCollection collection, CommandConfiguration configuration) { diff --git a/src/CSF.Core/Helpers/ThrowHelpers.cs b/src/CSF.Core/Helpers/ThrowHelpers.cs index 9331ee9..69181f5 100644 --- a/src/CSF.Core/Helpers/ThrowHelpers.cs +++ b/src/CSF.Core/Helpers/ThrowHelpers.cs @@ -1,14 +1,21 @@ -using CSF.Exceptions; -using System.Collections; +using System.Collections; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace CSF.Helpers { - [EditorBrowsable(EditorBrowsableState.Advanced)] + /// + /// Represents a set of methods to throw exceptions for the invalid state of objects or operations. + /// + [EditorBrowsable(EditorBrowsableState.Never)] public static class ThrowHelpers { + /// + /// Throws for any use. + /// + /// The message to throw. + /// [DoesNotReturn] [EditorBrowsable(EditorBrowsableState.Never)] public static void InvalidOp([DisallowNull] string failureMessage) @@ -16,6 +23,12 @@ public static void InvalidOp([DisallowNull] string failureMessage) throw new InvalidOperationException(failureMessage); } + /// + /// Throws for value population. + /// + /// The object for which to throw + /// Do not set. + /// [DoesNotReturn] [EditorBrowsable(EditorBrowsableState.Never)] public static void InvalidArg(object value, [CallerArgumentExpression(nameof(value))] string arg = null) @@ -40,8 +53,8 @@ public static void InvalidArg(object value, [CallerArgumentExpression(nameof(val // if value is a string, this check ensures that more data is provided at throw, regarding the state of the string. if (value is string) { - throw new ArgumentMissingException( - message: "Provided string must carry at least a character that is not whitespace. It is not allowed to be null or empty.", + throw new ArgumentException( + message: "Provided string must carry at least one character that is not whitespace. It is not allowed to be null or empty.", paramName: arg); } @@ -51,6 +64,12 @@ public static void InvalidArg(object value, [CallerArgumentExpression(nameof(val paramName: arg); } + /// + /// Throws for duplicate values in a collection. + /// + /// The object for which to throw. + /// Do not set. + /// [DoesNotReturn] [EditorBrowsable(EditorBrowsableState.Never)] public static void NotDistinct(object value, [CallerArgumentExpression(nameof(value))] string arg = null) diff --git a/src/CSF.Hosting/Hosting/HostedCommandManager.cs b/src/CSF.Hosting/Hosting/HostedCommandManager.cs index 39a3a50..6173c1f 100644 --- a/src/CSF.Hosting/Hosting/HostedCommandManager.cs +++ b/src/CSF.Hosting/Hosting/HostedCommandManager.cs @@ -3,6 +3,8 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +[assembly: CLSCompliant(true)] + namespace CSF.Hosting { public class HostedCommandManager : CommandManager, IHostedService diff --git a/src/CSF.Tests.Hosting/Factory.cs b/src/CSF.Tests.Hosting/ActionFactory.cs similarity index 90% rename from src/CSF.Tests.Hosting/Factory.cs rename to src/CSF.Tests.Hosting/ActionFactory.cs index aea1496..0cd88e2 100644 --- a/src/CSF.Tests.Hosting/Factory.cs +++ b/src/CSF.Tests.Hosting/ActionFactory.cs @@ -5,12 +5,12 @@ namespace CSF.Tests.Hosting { - public class Factory : IActionFactory + public class ActionFactory : IActionFactory { private readonly ILoggerFactory _loggerFactory; private readonly StringParser _stringParser; - public Factory(ILoggerFactory loggerFactory) + public ActionFactory(ILoggerFactory loggerFactory) { _loggerFactory = loggerFactory; _stringParser = new StringParser(); diff --git a/src/CSF.Tests.Hosting/Program.cs b/src/CSF.Tests.Hosting/Program.cs index f4e5cbd..1f56a29 100644 --- a/src/CSF.Tests.Hosting/Program.cs +++ b/src/CSF.Tests.Hosting/Program.cs @@ -6,7 +6,7 @@ using System.Reflection; await Host.CreateDefaultBuilder(args) - .ConfigureCommands((context, configuration) => + .ConfigureCommands((context, configuration) => { configuration.AsyncApproach = AsyncApproach.Await; From 322ca28eaab01c7ed939c187d79f9add12348466 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Tue, 30 Jan 2024 19:55:27 +0100 Subject: [PATCH 18/40] Results, module XML --- src/CSF.Core/Core/CommandManager.cs | 2 +- src/CSF.Core/Core/Execution/ModuleBase.cs | 92 ++++++++++++++----- src/CSF.Core/Core/Results/ICommandResult.cs | 16 ++++ src/CSF.Core/Core/Results/Impl/CheckResult.cs | 5 + src/CSF.Core/Core/Results/Impl/MatchResult.cs | 15 ++- src/CSF.Core/Core/Results/Impl/ReadResult.cs | 10 +- src/CSF.Core/Core/Results/Impl/RunResult.cs | 15 ++- .../Core/Results/Impl/SearchResult.cs | 16 +++- src/CSF.Core/Core/Results/ResultResolver.cs | 13 ++- 9 files changed, 141 insertions(+), 43 deletions(-) diff --git a/src/CSF.Core/Core/CommandManager.cs b/src/CSF.Core/Core/CommandManager.cs index 9615b7a..faeb5a8 100644 --- a/src/CSF.Core/Core/CommandManager.cs +++ b/src/CSF.Core/Core/CommandManager.cs @@ -266,7 +266,7 @@ private async ValueTask RunAsync(ICommandContext context, MatchResult await module.AfterExecuteAsync(cancellationToken); - return module.ReturnTypeResolve(value); + return await module.ResolveInvocationResultAsync(value); } catch (Exception exception) { diff --git a/src/CSF.Core/Core/Execution/ModuleBase.cs b/src/CSF.Core/Core/Execution/ModuleBase.cs index 2b34f93..23d2ac8 100644 --- a/src/CSF.Core/Core/Execution/ModuleBase.cs +++ b/src/CSF.Core/Core/Execution/ModuleBase.cs @@ -2,11 +2,18 @@ namespace CSF.Core { + /// + /// Represents a that implements an implementation-friendly accessor to the . + /// + /// The implementation of known during command pipeline execution. public abstract class ModuleBase : ModuleBase where T : ICommandContext { private T _context; + /// + /// Gets the command context containing metadata and logging access for the command currently in scope. + /// public new T Context { get @@ -14,50 +21,87 @@ public abstract class ModuleBase : ModuleBase } } + /// + /// The base type needed to write commands with CSF. This type can be derived freely, in order to extend and implement command functionality. + /// Modules do not have state, they are instantiated and populated before a command runs and immediately disposed when it finishes. + /// + /// + /// All derived types must be known in to be discoverable and automatically registered during the creation of a . + /// public abstract class ModuleBase { + /// + /// Gets the command context containing metadata and logging access for the command currently in scope. + /// public ICommandContext Context { get; internal set; } + /// + /// Gets the services configured to start and run the command currently in scope. + /// public IServiceProvider Services { get; internal set; } + /// + /// Gets the reflection information about this command. + /// public CommandInfo Command { get; internal set; } - internal virtual RunResult ReturnTypeResolve(object value) + /// + /// Represents an overridable operation that runs before command invocation starts. + /// + /// + /// If this method throws an exception, the whole command invocation is halted. + /// + /// The provided at the root call for this command execution, which can be used to cancel running operations. + /// The awaitable result of this asynchronous operation. + public virtual ValueTask BeforeExecuteAsync(CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } + + /// + /// Represents an overridable operation that runs after command invocation ended. + /// + /// + /// If command invocation threw an exception, this method will not execute. + /// + /// The provided at the root call for this command execution, which can be used to cancel running operations. + /// The awaitable result of this asynchronous operation. + public virtual ValueTask AfterExecuteAsync(CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } + + /// + /// Represents an overridable operation that is responsible for resolving unknown invocation results. + /// + /// The invocation result of which no base handler exists. + /// if the unknown result is handled. if the unknown result is unhandled. + public virtual bool HandleUnknownInvocationResult(object value) + { + return false; + } + + internal virtual async Task ResolveInvocationResultAsync(object value) { switch (value) { case Task task: - return new(Command, task); + { + await task; + } + return new(Command); case null: - return new(Command, returnValue: null); + return new(Command); default: { - var result = HandleUnknownReturnType(value); - - if (!result.Success) + if (!HandleUnknownInvocationResult(value)) { - Context.LogWarning("{} returned unknown type. Consider overriding {} to resolve this message.", Command, nameof(HandleUnknownReturnType)); - return new(Command, returnValue: value); + Context.LogWarning("{} returned unknown type. Consider overriding {} to resolve this message.", Command, nameof(HandleUnknownInvocationResult)); } - return result; + return new(Command); } } } - - public virtual RunResult HandleUnknownReturnType(object value) - { - return new RunResult(Command, exception: null); - } - - public virtual ValueTask BeforeExecuteAsync(CancellationToken cancellationToken) - { - return ValueTask.CompletedTask; - } - - public virtual ValueTask AfterExecuteAsync(CancellationToken cancellationToken) - { - return ValueTask.CompletedTask; - } } } diff --git a/src/CSF.Core/Core/Results/ICommandResult.cs b/src/CSF.Core/Core/Results/ICommandResult.cs index 87f8742..d94b9e1 100644 --- a/src/CSF.Core/Core/Results/ICommandResult.cs +++ b/src/CSF.Core/Core/Results/ICommandResult.cs @@ -1,9 +1,25 @@ namespace CSF { + /// + /// Represents the result of any operation within the command execution pipeline. + /// + /// + /// This interface encompasses a number of results that each represent a different step in the execution pipeline. + /// To deduce which + /// public interface ICommandResult { + /// + /// Gets the exception that represents the reason and context of a failed operation. + /// + /// + /// Will be if returns . + /// public Exception Exception { get; } + /// + /// Gets if the result was succesful or not. + /// public bool Success { get; } } } diff --git a/src/CSF.Core/Core/Results/Impl/CheckResult.cs b/src/CSF.Core/Core/Results/Impl/CheckResult.cs index 09e5ac0..f67962f 100644 --- a/src/CSF.Core/Core/Results/Impl/CheckResult.cs +++ b/src/CSF.Core/Core/Results/Impl/CheckResult.cs @@ -1,9 +1,14 @@ namespace CSF { + /// + /// Represents the result of a check operation within the command execution pipeline. + /// public readonly struct CheckResult : ICommandResult { + /// public Exception Exception { get; } = null; + /// public bool Success { get; } = true; internal CheckResult(Exception exception) diff --git a/src/CSF.Core/Core/Results/Impl/MatchResult.cs b/src/CSF.Core/Core/Results/Impl/MatchResult.cs index 725f9dc..10fe82c 100644 --- a/src/CSF.Core/Core/Results/Impl/MatchResult.cs +++ b/src/CSF.Core/Core/Results/Impl/MatchResult.cs @@ -2,15 +2,23 @@ namespace CSF { + /// + /// Represents the result of a match operation within the command execution pipeline. + /// public readonly struct MatchResult : ICommandResult { + /// public Exception Exception { get; } = null; - public CommandInfo Command { get; } + /// + public bool Success { get; } - public object[] Reads { get; } + /// + /// Gets the command known during the matching operation. + /// + public CommandInfo Command { get; } - public bool Success { get; } + internal object[] Reads { get; } = null; internal MatchResult(CommandInfo command, object[] reads) { @@ -22,7 +30,6 @@ internal MatchResult(CommandInfo command, object[] reads) internal MatchResult(CommandInfo command, Exception exception) { Command = command; - Reads = null; Success = false; Exception = exception; diff --git a/src/CSF.Core/Core/Results/Impl/ReadResult.cs b/src/CSF.Core/Core/Results/Impl/ReadResult.cs index 159bfc9..f3634e8 100644 --- a/src/CSF.Core/Core/Results/Impl/ReadResult.cs +++ b/src/CSF.Core/Core/Results/Impl/ReadResult.cs @@ -1,13 +1,18 @@ namespace CSF { + /// + /// Represents the result of a read operation within the command execution pipeline. + /// public readonly struct ReadResult : ICommandResult { + /// public Exception Exception { get; } = null; - public object Value { get; } - + /// public bool Success { get; } + internal object Value { get; } = null; + internal ReadResult(object value) { Value = value; @@ -17,7 +22,6 @@ internal ReadResult(object value) internal ReadResult(Exception exception) { Exception = exception; - Value = null; Success = false; } } diff --git a/src/CSF.Core/Core/Results/Impl/RunResult.cs b/src/CSF.Core/Core/Results/Impl/RunResult.cs index 36f8edf..598ff90 100644 --- a/src/CSF.Core/Core/Results/Impl/RunResult.cs +++ b/src/CSF.Core/Core/Results/Impl/RunResult.cs @@ -2,16 +2,22 @@ namespace CSF { + /// + /// Represents the result of an invocation operation within the command execution pipeline. + /// public readonly struct RunResult : ICommandResult { + /// public Exception Exception { get; } = null; - public object ReturnType { get; } = null; + /// + public bool Success { get; } + /// + /// Gets the command responsible for the invocation. + /// public CommandInfo Command { get; } - public bool Success { get; } - internal RunResult(CommandInfo command, Exception exception) { Exception = exception; @@ -19,9 +25,8 @@ internal RunResult(CommandInfo command, Exception exception) Success = false; } - internal RunResult(CommandInfo command, object returnValue) + internal RunResult(CommandInfo command) { - ReturnType = returnValue; Command = command; Success = true; } diff --git a/src/CSF.Core/Core/Results/Impl/SearchResult.cs b/src/CSF.Core/Core/Results/Impl/SearchResult.cs index 710d3a4..4e4e4f9 100644 --- a/src/CSF.Core/Core/Results/Impl/SearchResult.cs +++ b/src/CSF.Core/Core/Results/Impl/SearchResult.cs @@ -2,14 +2,25 @@ namespace CSF { + /// + /// Represents the result of a search operation within the command execution pipeline. + /// public readonly struct SearchResult : ICommandResult { + /// public Exception Exception { get; } = null; - public CommandInfo Command { get; } - + /// public bool Success { get; } + /// + /// Gets the command that was found for this result. + /// + /// + /// Will be if returns . + /// + public CommandInfo Command { get; } = null; + internal int SearchHeight { get; } internal SearchResult(CommandInfo command, int srcHeight) @@ -22,7 +33,6 @@ internal SearchResult(CommandInfo command, int srcHeight) internal SearchResult(Exception exception) { Exception = exception; - Command = null; SearchHeight = 0; Success = false; } diff --git a/src/CSF.Core/Core/Results/ResultResolver.cs b/src/CSF.Core/Core/Results/ResultResolver.cs index e29ab1d..140bb49 100644 --- a/src/CSF.Core/Core/Results/ResultResolver.cs +++ b/src/CSF.Core/Core/Results/ResultResolver.cs @@ -1,12 +1,19 @@ namespace CSF.Core { - public sealed class ResultResolver(Func handler) + /// + /// Represents a container that implements an asynchronous functor to handle post-execution operations. + /// + /// A functor that serves as the handler for post-execution operations in this resolver. + public class ResultResolver(Func handler) { private static readonly Lazy _i = new(() => new ResultResolver(null)); + /// + /// Gets the handler responsible for post-execution operation handling. + /// public Func Handler { get; } = handler; - public Task TryHandleAsync(ICommandContext context, ICommandResult result, IServiceProvider services) + internal Task TryHandleAsync(ICommandContext context, ICommandResult result, IServiceProvider services) { if (Handler == null) { @@ -16,7 +23,7 @@ public Task TryHandleAsync(ICommandContext context, ICommandResult result, IServ return Handler(context, result, services); } - public static ResultResolver Default + internal static ResultResolver Default { get { From b2a7c42e42c39b52feac2d4a0a7d4121bb65f31e Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Wed, 31 Jan 2024 20:35:53 +0100 Subject: [PATCH 19/40] Context xml --- .../Core/Execution/ICommandContext.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/CSF.Core/Core/Execution/ICommandContext.cs b/src/CSF.Core/Core/Execution/ICommandContext.cs index bc20df2..658a251 100644 --- a/src/CSF.Core/Core/Execution/ICommandContext.cs +++ b/src/CSF.Core/Core/Execution/ICommandContext.cs @@ -2,18 +2,54 @@ namespace CSF.Core { + /// + /// Represents a container that contains metadata and logging access for a command attempted to be executed. + /// + /// + /// It is generally not adviced to implement this interface directly. Instead, consider implementing . + /// public interface ICommandContext { + /// + /// Creates and sends a trace log. + /// + /// Format string of the log message. + /// An object array that contains zero or more objects to format. public void LogTrace(string message, params object[] args); + /// + /// Creates and sends a debug log. + /// + /// Format string of the log message. + /// An object array that contains zero or more objects to format. public void LogDebug(string message, params object[] args); + /// + /// Creates and sends an information log. + /// + /// Format string of the log message. + /// An object array that contains zero or more objects to format. public void LogInformation(string message, params object[] args); + /// + /// Creates and sends a warning log. + /// + /// Format string of the log message. + /// An object array that contains zero or more objects to format. public void LogWarning(string message, params object[] args); + /// + /// Creates and sends an error log. + /// + /// Format string of the log message. + /// An object array that contains zero or more objects to format. public void LogError(string message, params object[] args); + /// + /// Creates and sends a critical log. + /// + /// Format string of the log message. + /// An object array that contains zero or more objects to format. public void LogCritical(string message, params object[] args); internal bool TryGetFallback([NotNullWhen(true)] out ICommandResult result); From 2aed732408fde0a4956261f355fdd79310a55ab8 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Fri, 2 Feb 2024 10:40:14 +0100 Subject: [PATCH 20/40] Attributes, context, asyncapproach XML --- .../Core/Attributes/CommandAttribute.cs | 14 ++++-- .../Core/Attributes/ComplexAttribute.cs | 2 +- .../Core/Attributes/DescriptionAttribute.cs | 6 +-- .../Core/Attributes/GroupAttribute.cs | 12 +++-- .../Attributes/PrimaryConstructorAttribute.cs | 2 +- .../Core/Attributes/PriorityAttribute.cs | 2 +- .../Core/Attributes/RemainderAttribute.cs | 2 +- .../Core/Configuration/AsyncApproach.cs | 50 ++++++++++++++++++- .../Core/Execution/ICommandContext.cs | 2 +- .../Core/Execution/Impl/CommandContext.cs | 30 +++++++++++ 10 files changed, 105 insertions(+), 17 deletions(-) diff --git a/src/CSF.Core/Core/Attributes/CommandAttribute.cs b/src/CSF.Core/Core/Attributes/CommandAttribute.cs index 0568b44..5a968e7 100644 --- a/src/CSF.Core/Core/Attributes/CommandAttribute.cs +++ b/src/CSF.Core/Core/Attributes/CommandAttribute.cs @@ -4,7 +4,7 @@ namespace CSF.Core { /// - /// An attribute that represents the required info to map a command. + /// An attribute that signifies a method as a command. /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] public sealed class CommandAttribute : Attribute @@ -15,17 +15,25 @@ public sealed class CommandAttribute : Attribute public string Name { get; } /// - /// The command aliases. + /// The command's aliases. /// public string[] Aliases { get; } + /// + /// Creates a new with provided name. + /// + /// The command name. public CommandAttribute([DisallowNull] string name) : this(name, []) { } - [CLSCompliant(false)] + /// + /// Creates a new with provided name and aliases. + /// + /// The command name. + /// The command's aliases. public CommandAttribute([DisallowNull] string name, params string[] aliases) { if (string.IsNullOrWhiteSpace(name)) diff --git a/src/CSF.Core/Core/Attributes/ComplexAttribute.cs b/src/CSF.Core/Core/Attributes/ComplexAttribute.cs index 9cc14e0..97e6ead 100644 --- a/src/CSF.Core/Core/Attributes/ComplexAttribute.cs +++ b/src/CSF.Core/Core/Attributes/ComplexAttribute.cs @@ -1,7 +1,7 @@ namespace CSF.Core { /// - /// Marks a parameter as complex, which will attempt to fetch the primary constructor values and use those as command parameters. + /// An attribute to mark a parameter as complex, which will attempt to fetch the primary constructor values and use those as command parameters. /// [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] public sealed class ComplexAttribute : Attribute diff --git a/src/CSF.Core/Core/Attributes/DescriptionAttribute.cs b/src/CSF.Core/Core/Attributes/DescriptionAttribute.cs index 99bd1e2..b728741 100644 --- a/src/CSF.Core/Core/Attributes/DescriptionAttribute.cs +++ b/src/CSF.Core/Core/Attributes/DescriptionAttribute.cs @@ -4,20 +4,20 @@ namespace CSF.Core { /// - /// Represents the description of a command. + /// An attribute to give a description to a command, argument or module. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Class, AllowMultiple = false)] public sealed class DescriptionAttribute : Attribute { /// - /// The description of this parameter, command or module. + /// The description of this command, argument or module. /// public string Description { get; } /// /// Sets up a new with provided value. /// - /// + /// The description for a command, argument or module. public DescriptionAttribute([DisallowNull] string description) { if (string.IsNullOrWhiteSpace(description)) diff --git a/src/CSF.Core/Core/Attributes/GroupAttribute.cs b/src/CSF.Core/Core/Attributes/GroupAttribute.cs index ddc39f7..407085d 100644 --- a/src/CSF.Core/Core/Attributes/GroupAttribute.cs +++ b/src/CSF.Core/Core/Attributes/GroupAttribute.cs @@ -4,8 +4,11 @@ namespace CSF.Core { /// - /// Represents a command group, functioning much like subcommands. + /// An attribute that signifies a module to be a group, allowing functionality much like subcommands. /// + /// + /// This attribute does not work on top-level modules. + /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public sealed class GroupAttribute : Attribute { @@ -22,7 +25,7 @@ public sealed class GroupAttribute : Attribute /// /// Creates a new with defined name. /// - /// + /// The group name. public GroupAttribute([DisallowNull] string name) : this(name, []) { @@ -32,9 +35,8 @@ public GroupAttribute([DisallowNull] string name) /// /// Creates a new with defined name. /// - /// - /// - [CLSCompliant(false)] + /// The group name. + /// The group's aliases. public GroupAttribute([DisallowNull] string name, params string[] aliases) { if (string.IsNullOrWhiteSpace(name)) diff --git a/src/CSF.Core/Core/Attributes/PrimaryConstructorAttribute.cs b/src/CSF.Core/Core/Attributes/PrimaryConstructorAttribute.cs index 3cdd919..f7eae78 100644 --- a/src/CSF.Core/Core/Attributes/PrimaryConstructorAttribute.cs +++ b/src/CSF.Core/Core/Attributes/PrimaryConstructorAttribute.cs @@ -1,7 +1,7 @@ namespace CSF.Core { /// - /// Represents an attribute that sets a specified constructor as the dependency injection constructor. + /// An attribute that sets a specified constructor as the dependency injection constructor. /// /// /// It is not intended to use this attribute on multiple constructors. If it is, it will pick the highest constructor specified with this attribute. diff --git a/src/CSF.Core/Core/Attributes/PriorityAttribute.cs b/src/CSF.Core/Core/Attributes/PriorityAttribute.cs index a6a55c5..fe6b343 100644 --- a/src/CSF.Core/Core/Attributes/PriorityAttribute.cs +++ b/src/CSF.Core/Core/Attributes/PriorityAttribute.cs @@ -3,7 +3,7 @@ namespace CSF.Core { /// - /// Represents an attribute that can prioritize one result over another when multiple matches were found. + /// An attribute that can prioritize one result over another when multiple matches were found. /// /// /// By default, a command has a priority of 0. Higher values take priority, meaning a command with a priority of 1 will execute first if other commands have a priority of 0. diff --git a/src/CSF.Core/Core/Attributes/RemainderAttribute.cs b/src/CSF.Core/Core/Attributes/RemainderAttribute.cs index b1e95ff..33420f4 100644 --- a/src/CSF.Core/Core/Attributes/RemainderAttribute.cs +++ b/src/CSF.Core/Core/Attributes/RemainderAttribute.cs @@ -1,7 +1,7 @@ namespace CSF.Core { /// - /// Defines that this parameter should be the remainder of the command phrase. + /// An attribute to define that a final parameter should use the remainder of the command query. /// /// /// This attribute can only be set on the last parameter of a command method. diff --git a/src/CSF.Core/Core/Configuration/AsyncApproach.cs b/src/CSF.Core/Core/Configuration/AsyncApproach.cs index 352c749..8c23ed5 100644 --- a/src/CSF.Core/Core/Configuration/AsyncApproach.cs +++ b/src/CSF.Core/Core/Configuration/AsyncApproach.cs @@ -1,11 +1,59 @@ -namespace CSF.Core +using CSF.TypeReaders; +using CSF.Preconditions; + +namespace CSF.Core { + /// + /// Defines a setting that tells the command execution pipeline to wait on command execution to finish or to slip thread. + /// + /// + /// The asynchronous execution approach drastically changes the expected behavior of executing a command: + /// + /// + /// is the default setting and tells the execution pipeline to finish executing before returning control to the caller. + /// This ensures that the execution will fully finish executing, whether it failed or not, before allowing another to be executed. + /// + /// + /// is a setting to be treated with care. + /// Instead of waiting for the full execution before returning control, the execution will return immediately after the entrypoint is called, slipping thread for the rest of execution. + /// When more than one input source is expected to be handled, this is generally the adviced method of execution. + /// + /// + /// public enum AsyncApproach { + /// + /// The default option. + /// Default = Await, + /// + /// Tells the command execution pipeline to finish execution before returning control to the caller. + /// +#pragma warning disable CA1069 // Enums values should not be duplicated Await = 0, +#pragma warning restore CA1069 // Enums values should not be duplicated + /// + /// Tells the command execution pipeline to immediately slip thread and return to the caller without context. + /// + /// + /// Changing to this setting, the following should be checked for thread-safety: + /// + /// + /// Services, specifically those created as singleton or scoped to anything but a single command. + /// + /// + /// Implementations of , and . + /// + /// + /// Generic collections and objects with shared access. + /// + /// + /// For ensuring thread safety in any of the above situations, it is important to know what this actually means. + ///
+ /// For more information, consider reading this article: + ///
Discard = 1, } } diff --git a/src/CSF.Core/Core/Execution/ICommandContext.cs b/src/CSF.Core/Core/Execution/ICommandContext.cs index 658a251..95e6788 100644 --- a/src/CSF.Core/Core/Execution/ICommandContext.cs +++ b/src/CSF.Core/Core/Execution/ICommandContext.cs @@ -3,7 +3,7 @@ namespace CSF.Core { /// - /// Represents a container that contains metadata and logging access for a command attempted to be executed. + /// A container that contains metadata and logging access for a command attempted to be executed. /// /// /// It is generally not adviced to implement this interface directly. Instead, consider implementing . diff --git a/src/CSF.Core/Core/Execution/Impl/CommandContext.cs b/src/CSF.Core/Core/Execution/Impl/CommandContext.cs index d7fbec2..efb0f87 100644 --- a/src/CSF.Core/Core/Execution/Impl/CommandContext.cs +++ b/src/CSF.Core/Core/Execution/Impl/CommandContext.cs @@ -1,35 +1,65 @@ namespace CSF.Core { + /// + /// Represents the base implementation of . + /// + /// + /// This class can be implemented to customize logging and command metadata. + /// public class CommandContext : ICommandContext { private readonly object _lock = new(); private ICommandResult _fallback; + /// + /// + /// This method can be overridden to provide an out-stream to log to. + /// public virtual void LogCritical(string message, params object[] args) { } + /// + /// + /// This method can be overridden to provide an out-stream to log to. + /// public virtual void LogDebug(string message, params object[] args) { } + /// + /// + /// This method can be overridden to provide an out-stream to log to. + /// public virtual void LogError(string message, params object[] args) { } + /// + /// + /// This method can be overridden to provide an out-stream to log to. + /// public virtual void LogInformation(string message, params object[] args) { } + /// + /// + /// This method can be overridden to provide an out-stream to log to. + /// public virtual void LogTrace(string message, params object[] args) { } + /// + /// + /// This method can be overridden to provide an out-stream to log to. + /// public virtual void LogWarning(string message, params object[] args) { From 5ccd519ba30181514ebca912279f593e01e2fda6 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Fri, 2 Feb 2024 10:43:33 +0100 Subject: [PATCH 21/40] Result, exception, collection, helpers XML --- src/CSF.Core/Core/CommandConfiguration.cs | 2 +- src/CSF.Core/Core/Results/ICommandResult.cs | 2 +- src/CSF.Core/Core/Results/Impl/CheckResult.cs | 2 +- src/CSF.Core/Core/Results/Impl/MatchResult.cs | 2 +- src/CSF.Core/Core/Results/Impl/ReadResult.cs | 2 +- src/CSF.Core/Core/Results/Impl/RunResult.cs | 2 +- src/CSF.Core/Core/Results/Impl/SearchResult.cs | 2 +- src/CSF.Core/Core/Results/ResultResolver.cs | 2 +- src/CSF.Core/Exceptions/CheckException.cs | 2 +- src/CSF.Core/Exceptions/ExecutionException.cs | 2 +- src/CSF.Core/Exceptions/MatchException.cs | 4 ++-- src/CSF.Core/Exceptions/ReadException.cs | 2 +- src/CSF.Core/Exceptions/RunException.cs | 2 +- src/CSF.Core/Exceptions/SearchException.cs | 2 +- src/CSF.Core/Helpers/CollectionHelpers.cs | 1 - src/CSF.Core/Helpers/ExecutionHelpers.cs | 1 - src/CSF.Core/Helpers/ReflectionHelpers.cs | 1 - src/CSF.Core/Helpers/ServiceHelpers.cs | 2 +- src/CSF.Core/Helpers/ThrowHelpers.cs | 2 +- 19 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/CSF.Core/Core/CommandConfiguration.cs b/src/CSF.Core/Core/CommandConfiguration.cs index b715ab3..916ed7a 100644 --- a/src/CSF.Core/Core/CommandConfiguration.cs +++ b/src/CSF.Core/Core/CommandConfiguration.cs @@ -6,7 +6,7 @@ namespace CSF.Core { /// - /// Represents the configuration to be implemented by a new . + /// The configuration to be implemented by a new . /// public class CommandConfiguration { diff --git a/src/CSF.Core/Core/Results/ICommandResult.cs b/src/CSF.Core/Core/Results/ICommandResult.cs index d94b9e1..8771aff 100644 --- a/src/CSF.Core/Core/Results/ICommandResult.cs +++ b/src/CSF.Core/Core/Results/ICommandResult.cs @@ -1,7 +1,7 @@ namespace CSF { /// - /// Represents the result of any operation within the command execution pipeline. + /// The result of any operation within the command execution pipeline. /// /// /// This interface encompasses a number of results that each represent a different step in the execution pipeline. diff --git a/src/CSF.Core/Core/Results/Impl/CheckResult.cs b/src/CSF.Core/Core/Results/Impl/CheckResult.cs index f67962f..c63fcb0 100644 --- a/src/CSF.Core/Core/Results/Impl/CheckResult.cs +++ b/src/CSF.Core/Core/Results/Impl/CheckResult.cs @@ -1,7 +1,7 @@ namespace CSF { /// - /// Represents the result of a check operation within the command execution pipeline. + /// The result of a check operation within the command execution pipeline. /// public readonly struct CheckResult : ICommandResult { diff --git a/src/CSF.Core/Core/Results/Impl/MatchResult.cs b/src/CSF.Core/Core/Results/Impl/MatchResult.cs index 10fe82c..1ab83f6 100644 --- a/src/CSF.Core/Core/Results/Impl/MatchResult.cs +++ b/src/CSF.Core/Core/Results/Impl/MatchResult.cs @@ -3,7 +3,7 @@ namespace CSF { /// - /// Represents the result of a match operation within the command execution pipeline. + /// The result of a match operation within the command execution pipeline. /// public readonly struct MatchResult : ICommandResult { diff --git a/src/CSF.Core/Core/Results/Impl/ReadResult.cs b/src/CSF.Core/Core/Results/Impl/ReadResult.cs index f3634e8..cbd43db 100644 --- a/src/CSF.Core/Core/Results/Impl/ReadResult.cs +++ b/src/CSF.Core/Core/Results/Impl/ReadResult.cs @@ -1,7 +1,7 @@ namespace CSF { /// - /// Represents the result of a read operation within the command execution pipeline. + /// The result of a read operation within the command execution pipeline. /// public readonly struct ReadResult : ICommandResult { diff --git a/src/CSF.Core/Core/Results/Impl/RunResult.cs b/src/CSF.Core/Core/Results/Impl/RunResult.cs index 598ff90..ee39809 100644 --- a/src/CSF.Core/Core/Results/Impl/RunResult.cs +++ b/src/CSF.Core/Core/Results/Impl/RunResult.cs @@ -3,7 +3,7 @@ namespace CSF { /// - /// Represents the result of an invocation operation within the command execution pipeline. + /// The result of an invocation operation within the command execution pipeline. /// public readonly struct RunResult : ICommandResult { diff --git a/src/CSF.Core/Core/Results/Impl/SearchResult.cs b/src/CSF.Core/Core/Results/Impl/SearchResult.cs index 4e4e4f9..cd35a43 100644 --- a/src/CSF.Core/Core/Results/Impl/SearchResult.cs +++ b/src/CSF.Core/Core/Results/Impl/SearchResult.cs @@ -3,7 +3,7 @@ namespace CSF { /// - /// Represents the result of a search operation within the command execution pipeline. + /// The result of a search operation within the command execution pipeline. /// public readonly struct SearchResult : ICommandResult { diff --git a/src/CSF.Core/Core/Results/ResultResolver.cs b/src/CSF.Core/Core/Results/ResultResolver.cs index 140bb49..1090ed2 100644 --- a/src/CSF.Core/Core/Results/ResultResolver.cs +++ b/src/CSF.Core/Core/Results/ResultResolver.cs @@ -1,7 +1,7 @@ namespace CSF.Core { /// - /// Represents a container that implements an asynchronous functor to handle post-execution operations. + /// A container that implements an asynchronous functor to handle post-execution operations. /// /// A functor that serves as the handler for post-execution operations in this resolver. public class ResultResolver(Func handler) diff --git a/src/CSF.Core/Exceptions/CheckException.cs b/src/CSF.Core/Exceptions/CheckException.cs index d8955e1..2a79968 100644 --- a/src/CSF.Core/Exceptions/CheckException.cs +++ b/src/CSF.Core/Exceptions/CheckException.cs @@ -1,7 +1,7 @@ namespace CSF.Exceptions { /// - /// Represents an that is thrown when a command failed precondition validation. + /// An that is thrown when a command failed precondition validation. /// /// The message that represents the reason of the exception being thrown. /// An exception thrown by an inner operation, if present. diff --git a/src/CSF.Core/Exceptions/ExecutionException.cs b/src/CSF.Core/Exceptions/ExecutionException.cs index 48281cc..293336b 100644 --- a/src/CSF.Core/Exceptions/ExecutionException.cs +++ b/src/CSF.Core/Exceptions/ExecutionException.cs @@ -1,7 +1,7 @@ namespace CSF.Exceptions { /// - /// Represents an exception thrown anywhere in the command execution pipeline. + /// An exception thrown anywhere in the command execution pipeline. /// public class ExecutionException : Exception { diff --git a/src/CSF.Core/Exceptions/MatchException.cs b/src/CSF.Core/Exceptions/MatchException.cs index 9950bf0..b93e280 100644 --- a/src/CSF.Core/Exceptions/MatchException.cs +++ b/src/CSF.Core/Exceptions/MatchException.cs @@ -1,13 +1,13 @@ namespace CSF.Exceptions { /// - /// Represents an that is thrown when precondition validation or argument conversion failed. + /// An that is thrown when precondition validation or argument conversion failed. /// /// The message that represents the reason of the exception being thrown. /// An exception thrown by an inner operation, if present. public class MatchException(string message, Exception innerException = null) : ExecutionException(message, innerException) { - private const string _exHeader = "Command failed to reach execution state. View inner exception for more details."; + } } diff --git a/src/CSF.Core/Exceptions/ReadException.cs b/src/CSF.Core/Exceptions/ReadException.cs index f53533e..54df94d 100644 --- a/src/CSF.Core/Exceptions/ReadException.cs +++ b/src/CSF.Core/Exceptions/ReadException.cs @@ -1,7 +1,7 @@ namespace CSF.Exceptions { /// - /// Represents an that is thrown when no matched command succeeded parsing its parameters. + /// An that is thrown when no matched command succeeded parsing its parameters. /// /// The message that represents the reason of the exception being thrown. /// An exception thrown by an inner operation, if present. diff --git a/src/CSF.Core/Exceptions/RunException.cs b/src/CSF.Core/Exceptions/RunException.cs index 7a0fea4..811d941 100644 --- a/src/CSF.Core/Exceptions/RunException.cs +++ b/src/CSF.Core/Exceptions/RunException.cs @@ -1,7 +1,7 @@ namespace CSF.Exceptions { /// - /// Represents an that is thrown when the command being executed failed to finish invocation. + /// An that is thrown when the command being executed failed to finish invocation. /// /// The message that represents the reason of the exception being thrown. /// An exception thrown by an inner operation, if present. diff --git a/src/CSF.Core/Exceptions/SearchException.cs b/src/CSF.Core/Exceptions/SearchException.cs index 7925e52..1052f95 100644 --- a/src/CSF.Core/Exceptions/SearchException.cs +++ b/src/CSF.Core/Exceptions/SearchException.cs @@ -1,7 +1,7 @@ namespace CSF.Exceptions { /// - /// Represents an that is thrown when no command could be found. + /// An that is thrown when no command could be found. /// /// The message that represents the reason of the exception being thrown. /// An exception thrown by an inner operation, if present. diff --git a/src/CSF.Core/Helpers/CollectionHelpers.cs b/src/CSF.Core/Helpers/CollectionHelpers.cs index a710403..5527935 100644 --- a/src/CSF.Core/Helpers/CollectionHelpers.cs +++ b/src/CSF.Core/Helpers/CollectionHelpers.cs @@ -1,5 +1,4 @@ using System.Collections; -using System.ComponentModel; namespace CSF.Helpers { diff --git a/src/CSF.Core/Helpers/ExecutionHelpers.cs b/src/CSF.Core/Helpers/ExecutionHelpers.cs index f332592..c36a617 100644 --- a/src/CSF.Core/Helpers/ExecutionHelpers.cs +++ b/src/CSF.Core/Helpers/ExecutionHelpers.cs @@ -1,6 +1,5 @@ using CSF.Core; using CSF.Reflection; -using System.ComponentModel; namespace CSF.Helpers { diff --git a/src/CSF.Core/Helpers/ReflectionHelpers.cs b/src/CSF.Core/Helpers/ReflectionHelpers.cs index 043d0c3..8501c49 100644 --- a/src/CSF.Core/Helpers/ReflectionHelpers.cs +++ b/src/CSF.Core/Helpers/ReflectionHelpers.cs @@ -2,7 +2,6 @@ using CSF.Preconditions; using CSF.Reflection; using CSF.TypeReaders; -using System.ComponentModel; using System.Reflection; namespace CSF.Helpers diff --git a/src/CSF.Core/Helpers/ServiceHelpers.cs b/src/CSF.Core/Helpers/ServiceHelpers.cs index 8a03c25..b139a03 100644 --- a/src/CSF.Core/Helpers/ServiceHelpers.cs +++ b/src/CSF.Core/Helpers/ServiceHelpers.cs @@ -7,7 +7,7 @@ namespace CSF.Helpers { /// - /// Represents a set of methods to populate and configure a . + /// A set of helper methods to populate and configure a . /// [EditorBrowsable(EditorBrowsableState.Advanced)] public static class ServiceHelpers diff --git a/src/CSF.Core/Helpers/ThrowHelpers.cs b/src/CSF.Core/Helpers/ThrowHelpers.cs index 69181f5..e72834b 100644 --- a/src/CSF.Core/Helpers/ThrowHelpers.cs +++ b/src/CSF.Core/Helpers/ThrowHelpers.cs @@ -6,7 +6,7 @@ namespace CSF.Helpers { /// - /// Represents a set of methods to throw exceptions for the invalid state of objects or operations. + /// A set of helper methods to throw exceptions for the invalid state of objects or operations. /// [EditorBrowsable(EditorBrowsableState.Never)] public static class ThrowHelpers From b0f515266eef0e44f00a44b7dd8e98916e603e2d Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Fri, 2 Feb 2024 11:12:29 +0100 Subject: [PATCH 22/40] Parser, precondition XML --- src/CSF.Core/Core/CommandManager.cs | 2 +- src/CSF.Core/Parsing/Impl/StringParser.cs | 22 +++++++++++ src/CSF.Core/Parsing/Parser.cs | 12 ++++++ .../Preconditions/PreconditionAttribute.cs | 37 ++++++++++++++++++- src/CSF.Core/TypeReaders/TypeReader.cs | 4 +- 5 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/CSF.Core/Core/CommandManager.cs b/src/CSF.Core/Core/CommandManager.cs index faeb5a8..6e0ce80 100644 --- a/src/CSF.Core/Core/CommandManager.cs +++ b/src/CSF.Core/Core/CommandManager.cs @@ -234,7 +234,7 @@ private async ValueTask CheckAsync(ICommandContext context, Command foreach (var precon in command.Preconditions) { - var result = await precon.EvaluateAsync(context, command, cancellationToken); + var result = await precon.EvaluateAsync(context, Services, command, cancellationToken); if (!result.Success) return result; diff --git a/src/CSF.Core/Parsing/Impl/StringParser.cs b/src/CSF.Core/Parsing/Impl/StringParser.cs index b5b4bbc..53eff37 100644 --- a/src/CSF.Core/Parsing/Impl/StringParser.cs +++ b/src/CSF.Core/Parsing/Impl/StringParser.cs @@ -2,11 +2,33 @@ namespace CSF.Parsing { + /// + /// The default implementation of , implementing as the raw value. + /// + /// + /// As edge cases are discovered in the parser logic, the parser guidelines may change, and command input might improve, or degrade based on different usecases. + /// public class StringParser : Parser { const char quote = '"'; const char whitespace = ' '; + /// + /// + /// This parser sets the following guidelines: + /// + /// + /// Whitespace announcements will wrap the previous argument and build a new one. + /// + /// + /// Quotations will wrap the previous argument and build a new one. + /// + /// + /// Quoted arguments will start when a start-quote is discovered, and consider all following whitespace as part of the previous argument. + /// This argument will only be wrapped when an end-quote is announced. + /// + /// + /// public override object[] Parse(string toParse) { var arr = Array.Empty(); diff --git a/src/CSF.Core/Parsing/Parser.cs b/src/CSF.Core/Parsing/Parser.cs index dbbed4f..bf9d74f 100644 --- a/src/CSF.Core/Parsing/Parser.cs +++ b/src/CSF.Core/Parsing/Parser.cs @@ -1,8 +1,20 @@ namespace CSF.Parsing { + /// + /// An abstract type that implements a very basic API for implementing command query parsing. + /// + /// + /// To benefit from the base implementation of this query parser, create a new and run . + /// + /// The input type this parser should convert into arguments. This type must implement . public abstract class Parser where T : IEquatable { + /// + /// Parses a raw object into object arguments based on guidelines defined by the implementation. + /// + /// The raw value to convert into arguments. + /// An array of objects converted by the parser. public abstract object[] Parse(T value); } } diff --git a/src/CSF.Core/Preconditions/PreconditionAttribute.cs b/src/CSF.Core/Preconditions/PreconditionAttribute.cs index df6e72e..8a56035 100644 --- a/src/CSF.Core/Preconditions/PreconditionAttribute.cs +++ b/src/CSF.Core/Preconditions/PreconditionAttribute.cs @@ -6,13 +6,37 @@ namespace CSF.Preconditions { + /// + /// An attribute that defines that a check should succeed before a command can be executed. + /// + /// + /// The method is responsible for doing this check. + /// Custom implementations of can be placed at module or command level, with each being ran in top-down order when a target is checked. + /// If multiple commands are found during matching, multiple sequences of preconditions will be ran to find a match that succeeds. + /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] public abstract class PreconditionAttribute : Attribute { - private static readonly string _exHeader = "Precondition result halted further command execution. View inner exception for more details."; + const string _exHeader = "Precondition result halted further command execution. View inner exception for more details."; - public abstract ValueTask EvaluateAsync(ICommandContext context, CommandInfo command, CancellationToken cancellationToken); + /// + /// Evaluates the known data about a command at the point of pre-execution, in order to determine if it can be executed or not. + /// + /// + /// Make use of or and to safely create the intended result. + /// + /// Context of the current execution. + /// The provider used to register modules and inject services. + /// Information about the command currently targetted. + /// The token to cancel the operation. + /// An awaitable that contains the result of the evaluation. + public abstract ValueTask EvaluateAsync(ICommandContext context, IServiceProvider services, CommandInfo command, CancellationToken cancellationToken); + /// + /// Creates a new representing a failed evaluation. + /// + /// The exception that caused the evaluation to fail. + /// A representing the failed evaluation. public static CheckResult Error([DisallowNull] Exception exception) { if (exception == null) @@ -25,6 +49,11 @@ public static CheckResult Error([DisallowNull] Exception exception) return new(new CheckException(_exHeader, exception)); } + /// + /// Creates a new representing a failed evaluation. + /// + /// The error that caused the evaluation to fail. + /// A representing the failed evaluation. public virtual CheckResult Error([DisallowNull] string error) { if (string.IsNullOrEmpty(error)) @@ -33,6 +62,10 @@ public virtual CheckResult Error([DisallowNull] string error) return new(new CheckException(error)); } + /// + /// Creates a new representing a succesful evaluation. + /// + /// A representing the succesful evaluation. public virtual CheckResult Success() { return new(); diff --git a/src/CSF.Core/TypeReaders/TypeReader.cs b/src/CSF.Core/TypeReaders/TypeReader.cs index 527759d..fce4541 100644 --- a/src/CSF.Core/TypeReaders/TypeReader.cs +++ b/src/CSF.Core/TypeReaders/TypeReader.cs @@ -9,13 +9,11 @@ namespace CSF.TypeReaders public abstract class TypeReader : TypeReader { public override Type Type { get; } = typeof(T); - - public override abstract ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value, CancellationToken cancellationToken); } public abstract class TypeReader { - private static readonly string _exHeader = "TypeReader failed to parse provided value as '{0}'. View inner exception for more details."; + const string _exHeader = "TypeReader failed to parse provided value as '{0}'. View inner exception for more details."; public abstract Type Type { get; } From 16fe641e7b21a00532d6309cf42db2542bb8c345 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Fri, 2 Feb 2024 11:31:39 +0100 Subject: [PATCH 23/40] TypeReader -> TypeConverter + new xml --- src/CSF.Core/Core/CommandConfiguration.cs | 38 ++--- src/CSF.Core/Core/CommandManager.cs | 12 +- .../Core/Configuration/AsyncApproach.cs | 2 +- .../Impl/{ReadResult.cs => ConvertResult.cs} | 8 +- .../{ReadException.cs => ConvertException.cs} | 4 +- src/CSF.Core/Helpers/ExecutionHelpers.cs | 14 +- src/CSF.Core/Helpers/ReflectionHelpers.cs | 8 +- src/CSF.Core/Reflection/IArgument.cs | 2 +- src/CSF.Core/Reflection/Impl/ArgumentInfo.cs | 4 +- src/CSF.Core/Reflection/Impl/CommandInfo.cs | 2 +- .../Reflection/Impl/ComplexArgumentInfo.cs | 4 +- src/CSF.Core/Reflection/Impl/ModuleInfo.cs | 2 +- ...BaseTypeReader.cs => BaseTypeConverter.cs} | 10 +- .../{ColorTypeReader.cs => ColorConverter.cs} | 6 +- .../TypeReaders/Impl/EnumTypeReader.cs | 4 +- .../TypeReaders/Impl/TimeSpanTypeReader.cs | 6 +- src/CSF.Core/TypeReaders/TypeConverter.cs | 148 ++++++++++++++++++ src/CSF.Core/TypeReaders/TypeReader.cs | 100 ------------ 18 files changed, 211 insertions(+), 163 deletions(-) rename src/CSF.Core/Core/Results/Impl/{ReadResult.cs => ConvertResult.cs} (63%) rename src/CSF.Core/Exceptions/{ReadException.cs => ConvertException.cs} (73%) rename src/CSF.Core/TypeReaders/Impl/{BaseTypeReader.cs => BaseTypeConverter.cs} (90%) rename src/CSF.Core/TypeReaders/Impl/{ColorTypeReader.cs => ColorConverter.cs} (89%) create mode 100644 src/CSF.Core/TypeReaders/TypeConverter.cs delete mode 100644 src/CSF.Core/TypeReaders/TypeReader.cs diff --git a/src/CSF.Core/Core/CommandConfiguration.cs b/src/CSF.Core/Core/CommandConfiguration.cs index 916ed7a..8854806 100644 --- a/src/CSF.Core/Core/CommandConfiguration.cs +++ b/src/CSF.Core/Core/CommandConfiguration.cs @@ -26,16 +26,16 @@ public Assembly[] Assemblies } } - private TypeReader[] _typeReaders = []; + private TypeConverter[] _typeReaders = []; /// - /// Gets a collection of 's that the will use to handle unknown argument types. + /// Gets a collection of 's that the will use to handle unknown argument types. /// /// - /// It is adviced not to create new implementations of without first confirming if the target type is not already supported. + /// It is adviced not to create new implementations of without first confirming if the target type is not already supported. /// All valuetypes and time/date types are already supported out of the box. /// - public TypeReader[] TypeReaders + public TypeConverter[] Converters { get { @@ -159,41 +159,41 @@ public CommandConfiguration TryAddAssembly(Assembly assembly) } /// - /// Replaces the existing values in with a new collection. + /// Replaces the existing values in with a new collection. /// /// /// To prevent duplicate value recognition, is called to remove duplicates from . /// - /// A collection of 's to parse unknown argument types. + /// A collection of 's to parse unknown argument types. /// The same for call chaining. - public CommandConfiguration WithTypeReaders(params TypeReader[] typeReaders) + public CommandConfiguration WithTypeReaders(params TypeConverter[] typeReaders) { if (typeReaders == null) { ThrowHelpers.InvalidArg(typeReaders); } - _typeReaders = typeReaders.Distinct(TypeReader.EqualityComparer.Default).ToArray(); + _typeReaders = typeReaders.Distinct(TypeConverter.EqualityComparer.Default).ToArray(); return this; } /// - /// Adds a to . + /// Adds a to . /// /// - /// This call will throw if already contains an implementation of the target type, validated by a custom equality comparer. + /// This call will throw if already contains an implementation of the target type, validated by a custom equality comparer. /// - /// A to parse unknown argument types. + /// A to parse unknown argument types. /// The same for call chaining. - public CommandConfiguration AddTypeReader(TypeReader typeReader) + public CommandConfiguration AddTypeReader(TypeConverter typeReader) { if (typeReader == null) { ThrowHelpers.InvalidArg(typeReader); } - if (_typeReaders.Contains(typeReader, TypeReader.EqualityComparer.Default)) + if (_typeReaders.Contains(typeReader, TypeConverter.EqualityComparer.Default)) { ThrowHelpers.NotDistinct(typeReader); } @@ -204,18 +204,18 @@ public CommandConfiguration AddTypeReader(TypeReader typeReader) } /// - /// Attempts to add a to . + /// Attempts to add a to . /// /// - /// Will not add to if it already contains an implementation of the target type, validated by a custom equality comparer. + /// Will not add to if it already contains an implementation of the target type, validated by a custom equality comparer. /// - /// A to parse unknown argument types. + /// A to parse unknown argument types. /// The same for call chaining. - public CommandConfiguration TryAddTypeReader(TypeReader typeReader) + public CommandConfiguration TryAddTypeReader(TypeConverter typeReader) { if (typeReader != null) { - if (!_typeReaders.Contains(typeReader, TypeReader.EqualityComparer.Default)) + if (!_typeReaders.Contains(typeReader, TypeConverter.EqualityComparer.Default)) { AddTr(typeReader); } @@ -249,7 +249,7 @@ private void AddAsm(Assembly assembly) _assemblies[oLen] = assembly; } - private void AddTr(TypeReader typeReader) + private void AddTr(TypeConverter typeReader) { var oLen = _typeReaders.Length; Array.Resize(ref _typeReaders, oLen); diff --git a/src/CSF.Core/Core/CommandManager.cs b/src/CSF.Core/Core/CommandManager.cs index 6e0ce80..7783bac 100644 --- a/src/CSF.Core/Core/CommandManager.cs +++ b/src/CSF.Core/Core/CommandManager.cs @@ -180,7 +180,7 @@ private async ValueTask MatchAsync(ICommandContext context, SearchR return new(search.Command, new MatchException("Command failed to reach execution state. View inner exception for more details.", check.Exception)); // read the command parameters in right order. - var readResult = await ReadAsync(context, search, args, cancellationToken); + var readResult = await ConvertAsync(context, search, args, cancellationToken); // exchange the reads for result, verifying successes in the process. var reads = new object[readResult.Length]; @@ -199,7 +199,7 @@ private async ValueTask MatchAsync(ICommandContext context, SearchR #endregion #region Reading - private async ValueTask ReadAsync(ICommandContext context, SearchResult search, object[] args, CancellationToken cancellationToken) + private async ValueTask ConvertAsync(ICommandContext context, SearchResult search, object[] args, CancellationToken cancellationToken) { context.LogDebug("Attempting argument conversion for {}", search.Command); @@ -212,15 +212,15 @@ private async ValueTask ReadAsync(ICommandContext context, SearchR // check if input equals command length. if (search.Command.MaxLength == length) - return await search.Command.Parameters.RecursiveReadAsync(context, args[length..], 0, cancellationToken); + return await search.Command.Parameters.RecursiveConvertAsync(context, Services, args[length..], 0, cancellationToken); // check if input is longer than command, but remainder to concatenate. if (search.Command.MaxLength <= length && search.Command.HasRemainder) - return await search.Command.Parameters.RecursiveReadAsync(context, args[length..], 0, cancellationToken); + return await search.Command.Parameters.RecursiveConvertAsync(context, Services, args[length..], 0, cancellationToken); // check if input is shorter than command, but optional parameters to replace. if (search.Command.MaxLength > length && search.Command.MinLength <= length) - return await search.Command.Parameters.RecursiveReadAsync(context, args[length..], 0, cancellationToken); + return await search.Command.Parameters.RecursiveConvertAsync(context, Services, args[length..], 0, cancellationToken); // input is too long or too short. return []; @@ -278,7 +278,7 @@ private async ValueTask RunAsync(ICommandContext context, MatchResult #region Building private IEnumerable BuildComponents(CommandConfiguration configuration) { - var typeReaders = TypeReader.CreateDefaultReaders().UnionBy(configuration.TypeReaders, x => x.Type).ToDictionary(x => x.Type, x => x); + var typeReaders = TypeConverter.CreateDefaultReaders().UnionBy(configuration.Converters, x => x.Type).ToDictionary(x => x.Type, x => x); var rootType = typeof(ModuleBase); foreach (var assembly in configuration.Assemblies) diff --git a/src/CSF.Core/Core/Configuration/AsyncApproach.cs b/src/CSF.Core/Core/Configuration/AsyncApproach.cs index 8c23ed5..eb3656d 100644 --- a/src/CSF.Core/Core/Configuration/AsyncApproach.cs +++ b/src/CSF.Core/Core/Configuration/AsyncApproach.cs @@ -44,7 +44,7 @@ public enum AsyncApproach /// Services, specifically those created as singleton or scoped to anything but a single command. /// /// - /// Implementations of , and . + /// Implementations of , and . /// /// /// Generic collections and objects with shared access. diff --git a/src/CSF.Core/Core/Results/Impl/ReadResult.cs b/src/CSF.Core/Core/Results/Impl/ConvertResult.cs similarity index 63% rename from src/CSF.Core/Core/Results/Impl/ReadResult.cs rename to src/CSF.Core/Core/Results/Impl/ConvertResult.cs index cbd43db..338fb73 100644 --- a/src/CSF.Core/Core/Results/Impl/ReadResult.cs +++ b/src/CSF.Core/Core/Results/Impl/ConvertResult.cs @@ -1,9 +1,9 @@ namespace CSF { /// - /// The result of a read operation within the command execution pipeline. + /// The result of a convert operation within the command execution pipeline. /// - public readonly struct ReadResult : ICommandResult + public readonly struct ConvertResult : ICommandResult { /// public Exception Exception { get; } = null; @@ -13,13 +13,13 @@ internal object Value { get; } = null; - internal ReadResult(object value) + internal ConvertResult(object value) { Value = value; Success = true; } - internal ReadResult(Exception exception) + internal ConvertResult(Exception exception) { Exception = exception; Success = false; diff --git a/src/CSF.Core/Exceptions/ReadException.cs b/src/CSF.Core/Exceptions/ConvertException.cs similarity index 73% rename from src/CSF.Core/Exceptions/ReadException.cs rename to src/CSF.Core/Exceptions/ConvertException.cs index 54df94d..5804a35 100644 --- a/src/CSF.Core/Exceptions/ReadException.cs +++ b/src/CSF.Core/Exceptions/ConvertException.cs @@ -1,11 +1,11 @@ namespace CSF.Exceptions { /// - /// An that is thrown when no matched command succeeded parsing its parameters. + /// An that is thrown when no matched command succeeded converting its arguments. /// /// The message that represents the reason of the exception being thrown. /// An exception thrown by an inner operation, if present. - public sealed class ReadException(string message, Exception innerException = null) + public sealed class ConvertException(string message, Exception innerException = null) : ExecutionException(message, innerException) { } diff --git a/src/CSF.Core/Helpers/ExecutionHelpers.cs b/src/CSF.Core/Helpers/ExecutionHelpers.cs index c36a617..52854c5 100644 --- a/src/CSF.Core/Helpers/ExecutionHelpers.cs +++ b/src/CSF.Core/Helpers/ExecutionHelpers.cs @@ -30,9 +30,9 @@ public static IEnumerable RecursiveSearch(this IEnumerable RecursiveReadAsync(this IArgument[] param, ICommandContext context, object[] args, int index, CancellationToken cancellationToken) + public static async Task RecursiveConvertAsync(this IArgument[] param, ICommandContext context, IServiceProvider services, object[] args, int index, CancellationToken cancellationToken) { - static async ValueTask ReadAsync(IArgument param, ICommandContext context, object arg, CancellationToken cancellationToken) + static async ValueTask ConvertAsync(IArgument param, ICommandContext context, IServiceProvider services, object arg, CancellationToken cancellationToken) { if (arg.GetType() == param.Type) return new(arg); @@ -40,10 +40,10 @@ static async ValueTask ReadAsync(IArgument param, ICommandContext co if (param.IsNullable && arg is null or "null" or "nothing") return new(arg); - return await param.TypeReader.ObjectEvaluateAsync(context, param, arg, cancellationToken); + return await param.TypeReader.ObjectEvaluateAsync(context, services, param, arg, cancellationToken); } - var results = new ReadResult[param.Length]; + var results = new ConvertResult[param.Length]; for (int i = 0; i < param.Length; i++) { @@ -55,7 +55,7 @@ static async ValueTask ReadAsync(IArgument param, ICommandContext co if (parameter.Type == typeof(string)) results[i] = new(input); else - results[i] = await ReadAsync(parameter, context, input, cancellationToken); + results[i] = await ConvertAsync(parameter, context, services, input, cancellationToken); break; } @@ -68,7 +68,7 @@ static async ValueTask ReadAsync(IArgument param, ICommandContext co if (parameter is ComplexArgumentInfo complex) { - var result = await complex.Parameters.RecursiveReadAsync(context, args, index, cancellationToken); + var result = await complex.Parameters.RecursiveConvertAsync(context, services, args, index, cancellationToken); index += result.Length; @@ -87,7 +87,7 @@ static async ValueTask ReadAsync(IArgument param, ICommandContext co continue; } - results[i] = await ReadAsync(parameter, context, args[index], cancellationToken); + results[i] = await ConvertAsync(parameter, context, services, args[index], cancellationToken); index++; } diff --git a/src/CSF.Core/Helpers/ReflectionHelpers.cs b/src/CSF.Core/Helpers/ReflectionHelpers.cs index 8501c49..8f1bb11 100644 --- a/src/CSF.Core/Helpers/ReflectionHelpers.cs +++ b/src/CSF.Core/Helpers/ReflectionHelpers.cs @@ -8,7 +8,7 @@ namespace CSF.Helpers { internal static class ReflectionHelpers { - public static IEnumerable GetModules(ModuleInfo module, IDictionary typeReaders) + public static IEnumerable GetModules(ModuleInfo module, IDictionary typeReaders) { foreach (var group in module.Type.GetNestedTypes()) { @@ -22,7 +22,7 @@ public static IEnumerable GetModules(ModuleInfo module, IDictionary< } } - public static IEnumerable GetCommands(ModuleInfo module, IDictionary typeReaders) + public static IEnumerable GetCommands(ModuleInfo module, IDictionary typeReaders) { foreach (var method in module.Type.GetMethods()) { @@ -46,7 +46,7 @@ public static IEnumerable GetCommands(ModuleInfo module, IDictionar } } - public static IConditional[] GetComponents(this ModuleInfo module, IDictionary typeReaders) + public static IConditional[] GetComponents(this ModuleInfo module, IDictionary typeReaders) { var commands = (IEnumerable)GetCommands(module, typeReaders) .OrderBy(x => x.Parameters.Length); @@ -58,7 +58,7 @@ public static IConditional[] GetComponents(this ModuleInfo module, IDictionary typeReaders) + public static IArgument[] GetParameters(this MethodBase method, IDictionary typeReaders) { var parameters = method.GetParameters(); diff --git a/src/CSF.Core/Reflection/IArgument.cs b/src/CSF.Core/Reflection/IArgument.cs index d264599..1ea7516 100644 --- a/src/CSF.Core/Reflection/IArgument.cs +++ b/src/CSF.Core/Reflection/IArgument.cs @@ -21,6 +21,6 @@ public interface IArgument : INameable public bool IsRemainder { get; } - public TypeReader TypeReader { get; } + public TypeConverter TypeReader { get; } } } diff --git a/src/CSF.Core/Reflection/Impl/ArgumentInfo.cs b/src/CSF.Core/Reflection/Impl/ArgumentInfo.cs index a802ca4..40b7f57 100644 --- a/src/CSF.Core/Reflection/Impl/ArgumentInfo.cs +++ b/src/CSF.Core/Reflection/Impl/ArgumentInfo.cs @@ -22,9 +22,9 @@ public sealed class ArgumentInfo : IArgument public Attribute[] Attributes { get; } - public TypeReader TypeReader { get; } + public TypeConverter TypeReader { get; } - internal ArgumentInfo(ParameterInfo parameterInfo, IDictionary typeReaders) + internal ArgumentInfo(ParameterInfo parameterInfo, IDictionary typeReaders) { var underlying = Nullable.GetUnderlyingType(parameterInfo.ParameterType); var attributes = parameterInfo.GetAttributes(false); diff --git a/src/CSF.Core/Reflection/Impl/CommandInfo.cs b/src/CSF.Core/Reflection/Impl/CommandInfo.cs index ee92d3c..a4b7194 100644 --- a/src/CSF.Core/Reflection/Impl/CommandInfo.cs +++ b/src/CSF.Core/Reflection/Impl/CommandInfo.cs @@ -35,7 +35,7 @@ public sealed class CommandInfo : IConditional, IArgumentBucket public MethodInfo Target { get; } - internal CommandInfo(ModuleInfo module, MethodInfo method, string[] aliases, IDictionary typeReaders) + internal CommandInfo(ModuleInfo module, MethodInfo method, string[] aliases, IDictionary typeReaders) { var attributes = method.GetAttributes(true); var preconditions = attributes.GetPreconditions(); diff --git a/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs b/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs index 2c2e3f6..35674cc 100644 --- a/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs +++ b/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs @@ -41,12 +41,12 @@ public class ComplexArgumentInfo : IArgument, IArgumentBucket public int MaxLength { get; } - public TypeReader TypeReader { get; } + public TypeConverter TypeReader { get; } public ConstructorInfo Constructor { get; } - internal ComplexArgumentInfo(ParameterInfo parameterInfo, IDictionary typeReaders) + internal ComplexArgumentInfo(ParameterInfo parameterInfo, IDictionary typeReaders) { var underlying = Nullable.GetUnderlyingType(parameterInfo.ParameterType); var attributes = parameterInfo.GetAttributes(false); diff --git a/src/CSF.Core/Reflection/Impl/ModuleInfo.cs b/src/CSF.Core/Reflection/Impl/ModuleInfo.cs index 723ac77..b093200 100644 --- a/src/CSF.Core/Reflection/Impl/ModuleInfo.cs +++ b/src/CSF.Core/Reflection/Impl/ModuleInfo.cs @@ -31,7 +31,7 @@ public sealed class ModuleInfo : IConditional public ModuleInfo Root { get; } - internal ModuleInfo(Type type, IDictionary typeReaders, ModuleInfo root = null, string expectedName = null, string[] aliases = null) + internal ModuleInfo(Type type, IDictionary typeReaders, ModuleInfo root = null, string expectedName = null, string[] aliases = null) { var attributes = type.GetAttributes(true); var preconditions = attributes.GetPreconditions(); diff --git a/src/CSF.Core/TypeReaders/Impl/BaseTypeReader.cs b/src/CSF.Core/TypeReaders/Impl/BaseTypeConverter.cs similarity index 90% rename from src/CSF.Core/TypeReaders/Impl/BaseTypeReader.cs rename to src/CSF.Core/TypeReaders/Impl/BaseTypeConverter.cs index 41a3c16..50bbc62 100644 --- a/src/CSF.Core/TypeReaders/Impl/BaseTypeReader.cs +++ b/src/CSF.Core/TypeReaders/Impl/BaseTypeConverter.cs @@ -3,13 +3,13 @@ namespace CSF.TypeReaders { - internal class BaseTypeReader : TypeReader + internal class BaseTypeReader : TypeConverter { private delegate bool Tpd(string str, out TValue value); private readonly static Lazy> _container = new(ValueGenerator); - public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value, CancellationToken cancellationToken) + public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value, CancellationToken cancellationToken) { var parser = _container.Value[Type] as Tpd; @@ -64,11 +64,11 @@ private static IReadOnlyDictionary ValueGenerator() } } - internal static class BaseTypeReader + internal static class BaseTypeConverter { - public static TypeReader[] CreateBaseReaders() + public static TypeConverter[] CreateBaseReaders() { - var callback = new TypeReader[] + var callback = new TypeConverter[] { // char new BaseTypeReader(), diff --git a/src/CSF.Core/TypeReaders/Impl/ColorTypeReader.cs b/src/CSF.Core/TypeReaders/Impl/ColorConverter.cs similarity index 89% rename from src/CSF.Core/TypeReaders/Impl/ColorTypeReader.cs rename to src/CSF.Core/TypeReaders/Impl/ColorConverter.cs index 3f5fe66..495e63c 100644 --- a/src/CSF.Core/TypeReaders/Impl/ColorTypeReader.cs +++ b/src/CSF.Core/TypeReaders/Impl/ColorConverter.cs @@ -7,12 +7,12 @@ namespace CSF.TypeReaders { - internal class ColorTypeReader : TypeReader + internal class ColorConverter : TypeConverter { private readonly Dictionary _colors; private readonly IReadOnlyDictionary _spacedColors; - public ColorTypeReader() + public ColorConverter() { var properties = typeof(Color).GetProperties(BindingFlags.Public | BindingFlags.Static); var colors = new Dictionary(properties.Length - 1, StringComparer.OrdinalIgnoreCase); @@ -47,7 +47,7 @@ public ColorTypeReader() _spacedColors = spacedNames; } - public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value, CancellationToken cancellationToken) + public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value, CancellationToken cancellationToken) { if (int.TryParse(value.Replace("#", "").Replace("0x", ""), NumberStyles.HexNumber, null, out var hexNumber)) return ValueTask.FromResult(Success(Color.FromArgb(hexNumber))); diff --git a/src/CSF.Core/TypeReaders/Impl/EnumTypeReader.cs b/src/CSF.Core/TypeReaders/Impl/EnumTypeReader.cs index 73688b1..02026a1 100644 --- a/src/CSF.Core/TypeReaders/Impl/EnumTypeReader.cs +++ b/src/CSF.Core/TypeReaders/Impl/EnumTypeReader.cs @@ -3,13 +3,13 @@ namespace CSF.TypeReaders { - internal class EnumTypeReader(Type targetEnumType) : TypeReader + internal class EnumTypeReader(Type targetEnumType) : TypeConverter { private static readonly Dictionary _readers = []; public override Type Type { get; } = targetEnumType; - public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value, CancellationToken cancellationToken) + public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value, CancellationToken cancellationToken) { if (Enum.TryParse(Type, value, true, out var result)) return ValueTask.FromResult(Success(result)); diff --git a/src/CSF.Core/TypeReaders/Impl/TimeSpanTypeReader.cs b/src/CSF.Core/TypeReaders/Impl/TimeSpanTypeReader.cs index 0435a31..7e4c242 100644 --- a/src/CSF.Core/TypeReaders/Impl/TimeSpanTypeReader.cs +++ b/src/CSF.Core/TypeReaders/Impl/TimeSpanTypeReader.cs @@ -4,12 +4,12 @@ namespace CSF.TypeReaders { - internal partial class TimeSpanTypeReader : TypeReader + internal partial class TimeSpanConverter : TypeConverter { private readonly IReadOnlyDictionary> _callback; private readonly Regex _regex = GenTSRegex(); - public TimeSpanTypeReader() + public TimeSpanConverter() { _callback = new Dictionary> { @@ -35,7 +35,7 @@ public TimeSpanTypeReader() }; } - public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value, CancellationToken cancellationToken) + public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value, CancellationToken cancellationToken) { if (!TimeSpan.TryParse(value, out TimeSpan span)) { diff --git a/src/CSF.Core/TypeReaders/TypeConverter.cs b/src/CSF.Core/TypeReaders/TypeConverter.cs new file mode 100644 index 0000000..84d622f --- /dev/null +++ b/src/CSF.Core/TypeReaders/TypeConverter.cs @@ -0,0 +1,148 @@ +using CSF.Core; +using CSF.Exceptions; +using CSF.Helpers; +using CSF.Reflection; +using System.Diagnostics.CodeAnalysis; + +namespace CSF.TypeReaders +{ + /// + /// The type this should convert into. + public abstract class TypeConverter : TypeConverter + { + /// + /// Gets the type that should be converted to. + /// + public override Type Type { get; } = typeof(T); + + /// + /// Creates a new representing a succesful evaluation. + /// + /// The value converted from a raw argument into the target type of this converter. + /// A representing the succesful evaluation. + public virtual ConvertResult Success(T value) + { + return base.Success(value); + } + } + + /// + /// An abstract type that can be implemented to create custom type conversion from a command query argument. + /// + /// + /// Registering custom 's is not an automated process. To register them for the to use, add them to . + /// + public abstract class TypeConverter + { + const string _exHeader = "TypeConverter failed to parse provided value as '{0}'. View inner exception for more details."; + + /// + /// Gets the type that should be converted to. This value determines what command arguments will use this converter. + /// + /// + /// It is important to ensure this converter actually returns the specified type in . If this is not the case, a critical exception will occur in runtime when the command is attempted to be executed. + /// + public abstract Type Type { get; } + + /// + /// Evaluates the known data about the argument to be converter into, as well as the raw value it should convert into a valid invocation parameter. + /// + /// Context of the current execution. + /// The provider used to register modules and inject services. + /// Information about the invocation argument this evaluation converts for. + /// The raw command query argument to convert. + /// The token to cancel the operation. + /// An awaitable that contains the result of the evaluation. + public abstract ValueTask EvaluateAsync(ICommandContext context, IServiceProvider services, IArgument argument, string raw, CancellationToken cancellationToken); + + internal ValueTask ObjectEvaluateAsync(ICommandContext context, IServiceProvider services, IArgument argument, object raw, CancellationToken cancellationToken) + { + if (raw is string str) + return EvaluateAsync(context, services, argument, str, cancellationToken); + + return EvaluateAsync(context, services, argument, raw.ToString(), cancellationToken); + } + + /// + /// Creates a new representing a failed evaluation. + /// + /// The exception that caused the evaluation to fail. + /// A representing the failed evaluation. + public virtual ConvertResult Error([DisallowNull] Exception exception) + { + if (exception == null) + ThrowHelpers.InvalidArg(exception); + + if (exception is ConvertException readEx) + { + return new(readEx); + } + return new(new ConvertException(string.Format(_exHeader, Type.Name), exception)); + } + + /// + /// Creates a new representing a failed evaluation. + /// + /// The error that caused the evaluation to fail. + /// A representing the failed evaluation. + public virtual ConvertResult Error([DisallowNull] string error) + { + if (string.IsNullOrEmpty(error)) + ThrowHelpers.InvalidArg(error); + + return new(new ConvertException(error)); + } + + /// + /// Creates a new representing a succesful evaluation. + /// + /// The value converted from a raw argument into the target type of this converter. + /// A representing the succesful evaluation. + public virtual ConvertResult Success(object value) + { + return new(value); + } + + internal static TypeConverter[] CreateDefaultReaders() + { + var range = BaseTypeConverter.CreateBaseReaders(); + + int length = range.Length; + Array.Resize(ref range, length + 2); + + range[length++] = new TimeSpanConverter(); + range[length++] = new ColorConverter(); + + return range; + } + + internal class EqualityComparer : IEqualityComparer + { + private static readonly Lazy _i = new(); + + public bool Equals(TypeConverter x, TypeConverter y) + { + if (x == y) + return true; + + if (x.Type == y.Type) + return true; + + return false; + } + + public int GetHashCode([DisallowNull] TypeConverter obj) + { + return obj.GetHashCode(); + } + + public static EqualityComparer Default + { + get + { + return _i.Value; + } + } + } + } +} diff --git a/src/CSF.Core/TypeReaders/TypeReader.cs b/src/CSF.Core/TypeReaders/TypeReader.cs deleted file mode 100644 index fce4541..0000000 --- a/src/CSF.Core/TypeReaders/TypeReader.cs +++ /dev/null @@ -1,100 +0,0 @@ -using CSF.Core; -using CSF.Exceptions; -using CSF.Helpers; -using CSF.Reflection; -using System.Diagnostics.CodeAnalysis; - -namespace CSF.TypeReaders -{ - public abstract class TypeReader : TypeReader - { - public override Type Type { get; } = typeof(T); - } - - public abstract class TypeReader - { - const string _exHeader = "TypeReader failed to parse provided value as '{0}'. View inner exception for more details."; - - public abstract Type Type { get; } - - public abstract ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value, CancellationToken cancellationToken); - - internal ValueTask ObjectEvaluateAsync(ICommandContext context, IArgument parameter, object value, CancellationToken cancellationToken) - { - if (value.GetType() == Type) - return ValueTask.FromResult(new ReadResult(value)); - - if (value is string str) - return EvaluateAsync(context, parameter, str, cancellationToken); - - return EvaluateAsync(context, parameter, value.ToString(), cancellationToken); - } - - public virtual ReadResult Error([DisallowNull] Exception exception) - { - if (exception == null) - ThrowHelpers.InvalidArg(exception); - - if (exception is ReadException readEx) - { - return new(readEx); - } - return new(new ReadException(string.Format(_exHeader, Type.Name), exception)); - } - - public virtual ReadResult Error([DisallowNull] string error) - { - if (string.IsNullOrEmpty(error)) - ThrowHelpers.InvalidArg(error); - - return new(new ReadException(error)); - } - - public virtual ReadResult Success(object value) - { - return new(value); - } - - internal static TypeReader[] CreateDefaultReaders() - { - var range = BaseTypeReader.CreateBaseReaders(); - - int length = range.Length; - Array.Resize(ref range, length + 2); - - range[length++] = new TimeSpanTypeReader(); - range[length++] = new ColorTypeReader(); - - return range; - } - - internal class EqualityComparer : IEqualityComparer - { - private static readonly Lazy _i = new(); - - public bool Equals(TypeReader x, TypeReader y) - { - if (x == y) - return true; - - if (x.Type == y.Type) - return true; - - return false; - } - - public int GetHashCode([DisallowNull] TypeReader obj) - { - return obj.GetHashCode(); - } - - public static EqualityComparer Default - { - get - { - return _i.Value; - } - } - } - } -} From ba011b465be90407334ee048c6d828f67ae5069c Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Fri, 2 Feb 2024 11:53:04 +0100 Subject: [PATCH 24/40] Finalize core XML --- src/CSF.Core/Core/CommandConfiguration.cs | 2 +- src/CSF.Core/Core/CommandManager.cs | 10 ++-- .../Core/Configuration/AsyncApproach.cs | 2 +- src/CSF.Core/Helpers/ExecutionHelpers.cs | 4 +- src/CSF.Core/Helpers/ReflectionHelpers.cs | 4 +- src/CSF.Core/Reflection/IArgument.cs | 41 +++++++++++---- src/CSF.Core/Reflection/IArgumentBucket.cs | 28 +++++++---- src/CSF.Core/Reflection/IConditional.cs | 16 ++++-- src/CSF.Core/Reflection/INameable.cs | 12 +++-- src/CSF.Core/Reflection/Impl/ArgumentInfo.cs | 19 +++++-- src/CSF.Core/Reflection/Impl/CommandInfo.cs | 37 +++++++++++--- .../Reflection/Impl/ComplexArgumentInfo.cs | 50 ++++++++++--------- src/CSF.Core/Reflection/Impl/ModuleInfo.cs | 33 ++++++++---- .../Impl/BaseTypeConverter.cs | 6 +-- .../Impl/ColorConverter.cs | 6 +-- .../Impl/EnumTypeReader.cs | 4 +- .../Impl/TimeSpanTypeReader.cs | 4 +- .../TypeConverter.cs | 2 +- 18 files changed, 186 insertions(+), 94 deletions(-) rename src/CSF.Core/{TypeReaders => TypeConverters}/Impl/BaseTypeConverter.cs (94%) rename src/CSF.Core/{TypeReaders => TypeConverters}/Impl/ColorConverter.cs (90%) rename src/CSF.Core/{TypeReaders => TypeConverters}/Impl/EnumTypeReader.cs (85%) rename src/CSF.Core/{TypeReaders => TypeConverters}/Impl/TimeSpanTypeReader.cs (94%) rename src/CSF.Core/{TypeReaders => TypeConverters}/TypeConverter.cs (99%) diff --git a/src/CSF.Core/Core/CommandConfiguration.cs b/src/CSF.Core/Core/CommandConfiguration.cs index 8854806..fcb8a10 100644 --- a/src/CSF.Core/Core/CommandConfiguration.cs +++ b/src/CSF.Core/Core/CommandConfiguration.cs @@ -1,5 +1,5 @@ using CSF.Helpers; -using CSF.TypeReaders; +using CSF.TypeConverters; using System.Diagnostics.CodeAnalysis; using System.Reflection; diff --git a/src/CSF.Core/Core/CommandManager.cs b/src/CSF.Core/Core/CommandManager.cs index 7783bac..903413d 100644 --- a/src/CSF.Core/Core/CommandManager.cs +++ b/src/CSF.Core/Core/CommandManager.cs @@ -1,7 +1,7 @@ using CSF.Exceptions; using CSF.Helpers; using CSF.Reflection; -using CSF.TypeReaders; +using CSF.TypeConverters; using Microsoft.Extensions.DependencyInjection; [assembly: CLSCompliant(true)] @@ -204,7 +204,7 @@ private async ValueTask ConvertAsync(ICommandContext context, S context.LogDebug("Attempting argument conversion for {}", search.Command); // skip if no parameters exist. - if (!search.Command.HasParameters) + if (!search.Command.HasArguments) return []; // determine height of search to discover command name. @@ -212,15 +212,15 @@ private async ValueTask ConvertAsync(ICommandContext context, S // check if input equals command length. if (search.Command.MaxLength == length) - return await search.Command.Parameters.RecursiveConvertAsync(context, Services, args[length..], 0, cancellationToken); + return await search.Command.Arguments.RecursiveConvertAsync(context, Services, args[length..], 0, cancellationToken); // check if input is longer than command, but remainder to concatenate. if (search.Command.MaxLength <= length && search.Command.HasRemainder) - return await search.Command.Parameters.RecursiveConvertAsync(context, Services, args[length..], 0, cancellationToken); + return await search.Command.Arguments.RecursiveConvertAsync(context, Services, args[length..], 0, cancellationToken); // check if input is shorter than command, but optional parameters to replace. if (search.Command.MaxLength > length && search.Command.MinLength <= length) - return await search.Command.Parameters.RecursiveConvertAsync(context, Services, args[length..], 0, cancellationToken); + return await search.Command.Arguments.RecursiveConvertAsync(context, Services, args[length..], 0, cancellationToken); // input is too long or too short. return []; diff --git a/src/CSF.Core/Core/Configuration/AsyncApproach.cs b/src/CSF.Core/Core/Configuration/AsyncApproach.cs index eb3656d..fa3522e 100644 --- a/src/CSF.Core/Core/Configuration/AsyncApproach.cs +++ b/src/CSF.Core/Core/Configuration/AsyncApproach.cs @@ -1,4 +1,4 @@ -using CSF.TypeReaders; +using CSF.TypeConverters; using CSF.Preconditions; namespace CSF.Core diff --git a/src/CSF.Core/Helpers/ExecutionHelpers.cs b/src/CSF.Core/Helpers/ExecutionHelpers.cs index 52854c5..bc4d801 100644 --- a/src/CSF.Core/Helpers/ExecutionHelpers.cs +++ b/src/CSF.Core/Helpers/ExecutionHelpers.cs @@ -40,7 +40,7 @@ static async ValueTask ConvertAsync(IArgument param, ICommandCont if (param.IsNullable && arg is null or "null" or "nothing") return new(arg); - return await param.TypeReader.ObjectEvaluateAsync(context, services, param, arg, cancellationToken); + return await param.Converter.ObjectEvaluateAsync(context, services, param, arg, cancellationToken); } var results = new ConvertResult[param.Length]; @@ -68,7 +68,7 @@ static async ValueTask ConvertAsync(IArgument param, ICommandCont if (parameter is ComplexArgumentInfo complex) { - var result = await complex.Parameters.RecursiveConvertAsync(context, services, args, index, cancellationToken); + var result = await complex.Arguments.RecursiveConvertAsync(context, services, args, index, cancellationToken); index += result.Length; diff --git a/src/CSF.Core/Helpers/ReflectionHelpers.cs b/src/CSF.Core/Helpers/ReflectionHelpers.cs index 8f1bb11..98e7113 100644 --- a/src/CSF.Core/Helpers/ReflectionHelpers.cs +++ b/src/CSF.Core/Helpers/ReflectionHelpers.cs @@ -1,7 +1,7 @@ using CSF.Core; using CSF.Preconditions; using CSF.Reflection; -using CSF.TypeReaders; +using CSF.TypeConverters; using System.Reflection; namespace CSF.Helpers @@ -49,7 +49,7 @@ public static IEnumerable GetCommands(ModuleInfo module, IDictionar public static IConditional[] GetComponents(this ModuleInfo module, IDictionary typeReaders) { var commands = (IEnumerable)GetCommands(module, typeReaders) - .OrderBy(x => x.Parameters.Length); + .OrderBy(x => x.Arguments.Length); var modules = (IEnumerable)GetModules(module, typeReaders) .OrderBy(x => x.Components.Length); diff --git a/src/CSF.Core/Reflection/IArgument.cs b/src/CSF.Core/Reflection/IArgument.cs index 1ea7516..b1e7a46 100644 --- a/src/CSF.Core/Reflection/IArgument.cs +++ b/src/CSF.Core/Reflection/IArgument.cs @@ -1,26 +1,49 @@ -using CSF.TypeReaders; +using CSF.TypeConverters; namespace CSF.Reflection { - + /// + /// Reveals information about an invocation argument of a command or any complex member. + /// public interface IArgument : INameable { - + /// + /// Gets the type of this argument. + /// + /// + /// The returned value is always underlying where available, ensuring converters do not attempt to convert a nullable type. + /// public Type Type { get; } - + /// + /// Gets the exposed type of this argument. + /// + /// + /// The returned value will differ from if is . + /// public Type ExposedType { get; } - + /// + /// Gets if this argument is nullable or not. + /// public bool IsNullable { get; } - + /// + /// Gets if this argument is optional or not. + /// public bool IsOptional { get; } - + /// + /// Gets if this argument is the query remainder or not. + /// public bool IsRemainder { get; } - - public TypeConverter TypeReader { get; } + /// + /// Gets the converter for this argument. + /// + /// + /// Will be if is or . + /// + public TypeConverter Converter { get; } } } diff --git a/src/CSF.Core/Reflection/IArgumentBucket.cs b/src/CSF.Core/Reflection/IArgumentBucket.cs index 0662874..84b575d 100644 --- a/src/CSF.Core/Reflection/IArgumentBucket.cs +++ b/src/CSF.Core/Reflection/IArgumentBucket.cs @@ -1,18 +1,28 @@ namespace CSF.Reflection { - + /// + /// Reveals information about a bucket that contains zero-or-more arguments to resolve. + /// public interface IArgumentBucket { - - public IArgument[] Parameters { get; } - - - public bool HasParameters { get; } - - + /// + /// Gets an array of arguments this bucket exposes. + /// + public IArgument[] Arguments { get; } + + /// + /// Gets if this bucket has zero or more arguments. + /// + public bool HasArguments { get; } + + /// + /// Gets the minimum length of this bucket's arguments. + /// public int MinLength { get; } - + /// + /// Gets the maximum length of this bucket's arguments. + /// public int MaxLength { get; } } } diff --git a/src/CSF.Core/Reflection/IConditional.cs b/src/CSF.Core/Reflection/IConditional.cs index 1c9ac1e..37c6d67 100644 --- a/src/CSF.Core/Reflection/IConditional.cs +++ b/src/CSF.Core/Reflection/IConditional.cs @@ -2,16 +2,24 @@ namespace CSF.Reflection { - + /// + /// Reveals information about a conditional component, needing validation in order to become part of execution. + /// public interface IConditional : INameable { - + /// + /// Gets an array of aliases for this component. + /// public string[] Aliases { get; } - + /// + /// Gets an array of 's defined atop this component. + /// public PreconditionAttribute[] Preconditions { get; } - + /// + /// Gets if this component has zero or more preconditions. + /// public bool HasPreconditions { get; } } } diff --git a/src/CSF.Core/Reflection/INameable.cs b/src/CSF.Core/Reflection/INameable.cs index 5d46b9a..1ae8d72 100644 --- a/src/CSF.Core/Reflection/INameable.cs +++ b/src/CSF.Core/Reflection/INameable.cs @@ -1,12 +1,18 @@ namespace CSF.Reflection { - + /// + /// Reveals a name and potential attributes of a component necessary for execution. + /// public interface INameable { - + /// + /// Gets the name of the component. + /// public string Name { get; } - + /// + /// Gets an array of attributes of this component. + /// public Attribute[] Attributes { get; } } } diff --git a/src/CSF.Core/Reflection/Impl/ArgumentInfo.cs b/src/CSF.Core/Reflection/Impl/ArgumentInfo.cs index 40b7f57..a6d137c 100644 --- a/src/CSF.Core/Reflection/Impl/ArgumentInfo.cs +++ b/src/CSF.Core/Reflection/Impl/ArgumentInfo.cs @@ -1,28 +1,36 @@ using CSF.Core; using CSF.Helpers; -using CSF.TypeReaders; +using CSF.TypeConverters; using System.Reflection; namespace CSF.Reflection { - + /// public sealed class ArgumentInfo : IArgument { + /// public string Name { get; } + /// public Type Type { get; } + /// public Type ExposedType { get; } + /// public bool IsNullable { get; } + /// public bool IsOptional { get; } + /// public bool IsRemainder { get; } + /// public Attribute[] Attributes { get; } - public TypeConverter TypeReader { get; } + /// + public TypeConverter Converter { get; } internal ArgumentInfo(ParameterInfo parameterInfo, IDictionary typeReaders) { @@ -51,16 +59,17 @@ internal ArgumentInfo(ParameterInfo parameterInfo, IDictionary public override string ToString() => $"{Type.Name} {Name}"; } diff --git a/src/CSF.Core/Reflection/Impl/CommandInfo.cs b/src/CSF.Core/Reflection/Impl/CommandInfo.cs index a4b7194..c75821d 100644 --- a/src/CSF.Core/Reflection/Impl/CommandInfo.cs +++ b/src/CSF.Core/Reflection/Impl/CommandInfo.cs @@ -1,38 +1,59 @@ using CSF.Core; using CSF.Helpers; using CSF.Preconditions; -using CSF.TypeReaders; +using CSF.TypeConverters; using System.Reflection; namespace CSF.Reflection { - + /// + /// Reveals information about a command. + /// public sealed class CommandInfo : IConditional, IArgumentBucket { + /// public string Name { get; } + /// public Attribute[] Attributes { get; } + /// public PreconditionAttribute[] Preconditions { get; } + /// public bool HasPreconditions { get; } - public IArgument[] Parameters { get; } + /// + public IArgument[] Arguments { get; } - public bool HasParameters { get; } + /// + public bool HasArguments { get; } + /// public bool HasRemainder { get; } + /// public int MinLength { get; } + /// public int MaxLength { get; } + /// public string[] Aliases { get; } + /// + /// Gets the priority of this command. + /// public byte Priority { get; } + /// + /// Gets the module in which the command is known. + /// public ModuleInfo Module { get; } + /// + /// Gets the invocation target of this command. + /// public MethodInfo Target { get; } internal CommandInfo(ModuleInfo module, MethodInfo method, string[] aliases, IDictionary typeReaders) @@ -65,8 +86,8 @@ internal CommandInfo(ModuleInfo module, MethodInfo method, string[] aliases, IDi Preconditions = preconditions; HasPreconditions = preconditions.Length > 0; - Parameters = parameters; - HasParameters = parameters.Length > 0; + Arguments = parameters; + HasArguments = parameters.Length > 0; HasRemainder = parameters.Any(x => x.IsRemainder); Name = aliases[0]; @@ -76,8 +97,8 @@ internal CommandInfo(ModuleInfo module, MethodInfo method, string[] aliases, IDi MaxLength = maxLength; } - + /// public override string ToString() - => $"{Module}.{Target.Name}['{Name}']({string.Join(", ", Parameters)})"; + => $"{Module}.{Target.Name}['{Name}']({string.Join(", ", Arguments)})"; } } diff --git a/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs b/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs index 35674cc..3c8f24c 100644 --- a/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs +++ b/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs @@ -1,49 +1,53 @@ using CSF.Helpers; -using CSF.TypeReaders; +using CSF.TypeConverters; using System.Reflection; namespace CSF.Reflection { - + /// + /// Reveals information about a type with a defined complex constructor. + /// public class ComplexArgumentInfo : IArgument, IArgumentBucket { - + /// public string Name { get; } - + /// public Type Type { get; } - + /// public Type ExposedType { get; } - + /// public bool IsNullable { get; } - + /// public bool IsOptional { get; } - + /// public bool IsRemainder { get; } - + /// public Attribute[] Attributes { get; } + /// + public IArgument[] Arguments { get; } - public IArgument[] Parameters { get; } - - - public bool HasParameters { get; } - + /// + public bool HasArguments { get; } + /// public int MinLength { get; } - + /// public int MaxLength { get; } + /// + public TypeConverter Converter { get; } - public TypeConverter TypeReader { get; } - - + /// + /// Gets the invocation target of this complex argument. + /// public ConstructorInfo Constructor { get; } internal ComplexArgumentInfo(ParameterInfo parameterInfo, IDictionary typeReaders) @@ -78,14 +82,14 @@ internal ComplexArgumentInfo(ParameterInfo parameterInfo, IDictionary 0; + Arguments = parameters; + HasArguments = parameters.Length > 0; Attributes = attributes; @@ -93,8 +97,8 @@ internal ComplexArgumentInfo(ParameterInfo parameterInfo, IDictionary public override string ToString() - => $"{Type.Name} ({string.Join(", ", Parameters)}) {Name}"; + => $"{Type.Name} ({string.Join(", ", Arguments)}) {Name}"; } } diff --git a/src/CSF.Core/Reflection/Impl/ModuleInfo.cs b/src/CSF.Core/Reflection/Impl/ModuleInfo.cs index b093200..f454dfe 100644 --- a/src/CSF.Core/Reflection/Impl/ModuleInfo.cs +++ b/src/CSF.Core/Reflection/Impl/ModuleInfo.cs @@ -1,34 +1,45 @@ using CSF.Helpers; using CSF.Preconditions; -using CSF.TypeReaders; +using CSF.TypeConverters; namespace CSF.Reflection { - + /// + /// Reveals information about a command module, hosting zero-or-more commands. + /// public sealed class ModuleInfo : IConditional { - + /// public string Name { get; } - + /// public string[] Aliases { get; } - + /// public Attribute[] Attributes { get; } - + /// public PreconditionAttribute[] Preconditions { get; } - + /// public bool HasPreconditions { get; } - + /// + /// Gets an array containing nested modules or commands inside this module. + /// public IConditional[] Components { get; } - + /// + /// Gets the type of this module. + /// public Type Type { get; } - + /// + /// Gets the root module. + /// + /// + /// Will be if this module is not nested. + /// public ModuleInfo Root { get; } internal ModuleInfo(Type type, IDictionary typeReaders, ModuleInfo root = null, string expectedName = null, string[] aliases = null) @@ -49,7 +60,7 @@ internal ModuleInfo(Type type, IDictionary typeReaders, Mod Aliases = aliases ?? [Name]; } - + /// public override string ToString() => $"{(Root != null ? $"{Root}." : "")}{(Type.Name != Name ? $"{Type.Name}['{Name}']" : $"{Name}")}"; } diff --git a/src/CSF.Core/TypeReaders/Impl/BaseTypeConverter.cs b/src/CSF.Core/TypeConverters/Impl/BaseTypeConverter.cs similarity index 94% rename from src/CSF.Core/TypeReaders/Impl/BaseTypeConverter.cs rename to src/CSF.Core/TypeConverters/Impl/BaseTypeConverter.cs index 50bbc62..0d6e87b 100644 --- a/src/CSF.Core/TypeReaders/Impl/BaseTypeConverter.cs +++ b/src/CSF.Core/TypeConverters/Impl/BaseTypeConverter.cs @@ -1,7 +1,7 @@ using CSF.Core; using CSF.Reflection; -namespace CSF.TypeReaders +namespace CSF.TypeConverters { internal class BaseTypeReader : TypeConverter { @@ -9,7 +9,7 @@ internal class BaseTypeReader : TypeConverter private readonly static Lazy> _container = new(ValueGenerator); - public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value, CancellationToken cancellationToken) + public override ValueTask EvaluateAsync(ICommandContext context, IServiceProvider services, IArgument parameter, string value, CancellationToken cancellationToken) { var parser = _container.Value[Type] as Tpd; @@ -19,7 +19,7 @@ public override ValueTask EvaluateAsync(ICommandContext context, return ValueTask.FromResult(Error($"The provided value does not match the expected type. Expected {typeof(T).Name}, got {value}. At: '{parameter.Name}'")); } - private static IReadOnlyDictionary ValueGenerator() + private static Dictionary ValueGenerator() { var callback = new Dictionary { diff --git a/src/CSF.Core/TypeReaders/Impl/ColorConverter.cs b/src/CSF.Core/TypeConverters/Impl/ColorConverter.cs similarity index 90% rename from src/CSF.Core/TypeReaders/Impl/ColorConverter.cs rename to src/CSF.Core/TypeConverters/Impl/ColorConverter.cs index 495e63c..2e29f1f 100644 --- a/src/CSF.Core/TypeReaders/Impl/ColorConverter.cs +++ b/src/CSF.Core/TypeConverters/Impl/ColorConverter.cs @@ -5,12 +5,12 @@ using System.Reflection; using System.Text; -namespace CSF.TypeReaders +namespace CSF.TypeConverters { internal class ColorConverter : TypeConverter { private readonly Dictionary _colors; - private readonly IReadOnlyDictionary _spacedColors; + private readonly Dictionary _spacedColors; public ColorConverter() { @@ -47,7 +47,7 @@ public ColorConverter() _spacedColors = spacedNames; } - public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value, CancellationToken cancellationToken) + public override ValueTask EvaluateAsync(ICommandContext context, IServiceProvider services, IArgument parameter, string value, CancellationToken cancellationToken) { if (int.TryParse(value.Replace("#", "").Replace("0x", ""), NumberStyles.HexNumber, null, out var hexNumber)) return ValueTask.FromResult(Success(Color.FromArgb(hexNumber))); diff --git a/src/CSF.Core/TypeReaders/Impl/EnumTypeReader.cs b/src/CSF.Core/TypeConverters/Impl/EnumTypeReader.cs similarity index 85% rename from src/CSF.Core/TypeReaders/Impl/EnumTypeReader.cs rename to src/CSF.Core/TypeConverters/Impl/EnumTypeReader.cs index 02026a1..9c7cb03 100644 --- a/src/CSF.Core/TypeReaders/Impl/EnumTypeReader.cs +++ b/src/CSF.Core/TypeConverters/Impl/EnumTypeReader.cs @@ -1,7 +1,7 @@ using CSF.Core; using CSF.Reflection; -namespace CSF.TypeReaders +namespace CSF.TypeConverters { internal class EnumTypeReader(Type targetEnumType) : TypeConverter { @@ -9,7 +9,7 @@ internal class EnumTypeReader(Type targetEnumType) : TypeConverter public override Type Type { get; } = targetEnumType; - public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value, CancellationToken cancellationToken) + public override ValueTask EvaluateAsync(ICommandContext context, IServiceProvider services, IArgument parameter, string value, CancellationToken cancellationToken) { if (Enum.TryParse(Type, value, true, out var result)) return ValueTask.FromResult(Success(result)); diff --git a/src/CSF.Core/TypeReaders/Impl/TimeSpanTypeReader.cs b/src/CSF.Core/TypeConverters/Impl/TimeSpanTypeReader.cs similarity index 94% rename from src/CSF.Core/TypeReaders/Impl/TimeSpanTypeReader.cs rename to src/CSF.Core/TypeConverters/Impl/TimeSpanTypeReader.cs index 7e4c242..1d6f9d1 100644 --- a/src/CSF.Core/TypeReaders/Impl/TimeSpanTypeReader.cs +++ b/src/CSF.Core/TypeConverters/Impl/TimeSpanTypeReader.cs @@ -2,7 +2,7 @@ using CSF.Reflection; using System.Text.RegularExpressions; -namespace CSF.TypeReaders +namespace CSF.TypeConverters { internal partial class TimeSpanConverter : TypeConverter { @@ -35,7 +35,7 @@ public TimeSpanConverter() }; } - public override ValueTask EvaluateAsync(ICommandContext context, IArgument parameter, string value, CancellationToken cancellationToken) + public override ValueTask EvaluateAsync(ICommandContext context, IServiceProvider services, IArgument parameter, string value, CancellationToken cancellationToken) { if (!TimeSpan.TryParse(value, out TimeSpan span)) { diff --git a/src/CSF.Core/TypeReaders/TypeConverter.cs b/src/CSF.Core/TypeConverters/TypeConverter.cs similarity index 99% rename from src/CSF.Core/TypeReaders/TypeConverter.cs rename to src/CSF.Core/TypeConverters/TypeConverter.cs index 84d622f..be5fb40 100644 --- a/src/CSF.Core/TypeReaders/TypeConverter.cs +++ b/src/CSF.Core/TypeConverters/TypeConverter.cs @@ -4,7 +4,7 @@ using CSF.Reflection; using System.Diagnostics.CodeAnalysis; -namespace CSF.TypeReaders +namespace CSF.TypeConverters { /// /// The type this should convert into. From bb9fc56e2f6cd80c095e70633ed3e9b5e76b2cdb Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Fri, 2 Feb 2024 12:18:17 +0100 Subject: [PATCH 25/40] Spelling :nerd: --- src/CSF.Core/Core/CommandManager.cs | 2 +- src/CSF.Core/Core/Results/ICommandResult.cs | 2 +- src/CSF.Core/Preconditions/PreconditionAttribute.cs | 4 ++-- src/CSF.Core/TypeConverters/TypeConverter.cs | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/CSF.Core/Core/CommandManager.cs b/src/CSF.Core/Core/CommandManager.cs index 903413d..72ff44c 100644 --- a/src/CSF.Core/Core/CommandManager.cs +++ b/src/CSF.Core/Core/CommandManager.cs @@ -142,7 +142,7 @@ private async Task ExecuteInternalAsync(ICommandContext context, object[] args, var match = await MatchAsync(context, search, args, cancellationToken); - // enter the invocation logic when a match is succesful. + // enter the invocation logic when a match is successful. if (match.Success) { var result = await RunAsync(context, match, cancellationToken); diff --git a/src/CSF.Core/Core/Results/ICommandResult.cs b/src/CSF.Core/Core/Results/ICommandResult.cs index 8771aff..9a9dddd 100644 --- a/src/CSF.Core/Core/Results/ICommandResult.cs +++ b/src/CSF.Core/Core/Results/ICommandResult.cs @@ -18,7 +18,7 @@ public interface ICommandResult public Exception Exception { get; } /// - /// Gets if the result was succesful or not. + /// Gets if the result was successful or not. /// public bool Success { get; } } diff --git a/src/CSF.Core/Preconditions/PreconditionAttribute.cs b/src/CSF.Core/Preconditions/PreconditionAttribute.cs index 8a56035..df190c9 100644 --- a/src/CSF.Core/Preconditions/PreconditionAttribute.cs +++ b/src/CSF.Core/Preconditions/PreconditionAttribute.cs @@ -63,9 +63,9 @@ public virtual CheckResult Error([DisallowNull] string error) } /// - /// Creates a new representing a succesful evaluation. + /// Creates a new representing a successful evaluation. /// - /// A representing the succesful evaluation. + /// A representing the successful evaluation. public virtual CheckResult Success() { return new(); diff --git a/src/CSF.Core/TypeConverters/TypeConverter.cs b/src/CSF.Core/TypeConverters/TypeConverter.cs index be5fb40..ee3bb20 100644 --- a/src/CSF.Core/TypeConverters/TypeConverter.cs +++ b/src/CSF.Core/TypeConverters/TypeConverter.cs @@ -16,10 +16,10 @@ public abstract class TypeConverter : TypeConverter public override Type Type { get; } = typeof(T); /// - /// Creates a new representing a succesful evaluation. + /// Creates a new representing a successful evaluation. /// /// The value converted from a raw argument into the target type of this converter. - /// A representing the succesful evaluation. + /// A representing the successful evaluation. public virtual ConvertResult Success(T value) { return base.Success(value); @@ -94,10 +94,10 @@ public virtual ConvertResult Error([DisallowNull] string error) } /// - /// Creates a new representing a succesful evaluation. + /// Creates a new representing a successful evaluation. /// /// The value converted from a raw argument into the target type of this converter. - /// A representing the succesful evaluation. + /// A representing the successful evaluation. public virtual ConvertResult Success(object value) { return new(value); From 35661ff51a6352b5da88a3578324ee04716456d5 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Fri, 2 Feb 2024 12:28:51 +0100 Subject: [PATCH 26/40] Clear unnecessary projects --- CSF.sln | 16 +---- .../CSF.Benchmarks.Parsing.csproj | 2 +- .../CSF.Benchmarks.Parsing}/Program.cs | 2 +- src/CSF.Core/Core/CommandManager.cs | 7 +-- .../Hosting/HostedCommandManager.cs | 5 -- src/CSF.Tests.Benchmarks/BenchmarkModule.cs | 49 --------------- .../CSF.Tests.Benchmarks.csproj | 20 ------ src/CSF.Tests.Benchmarks/Program.cs | 62 ------------------- src/CSF.Tests.TShock/CSF.Tests.TShock.csproj | 15 ----- src/CSF.Tests.TShock/Module.cs | 16 ----- src/CSF.Tests.TShock/Plugin.cs | 23 ------- 11 files changed, 4 insertions(+), 213 deletions(-) rename CSF.Benchmark.Parsing/CSF.Benchmark.Parsing.csproj => src/CSF.Benchmarks.Parsing/CSF.Benchmarks.Parsing.csproj (84%) rename {CSF.Benchmark.Parsing => src/CSF.Benchmarks.Parsing}/Program.cs (92%) delete mode 100644 src/CSF.Tests.Benchmarks/BenchmarkModule.cs delete mode 100644 src/CSF.Tests.Benchmarks/CSF.Tests.Benchmarks.csproj delete mode 100644 src/CSF.Tests.Benchmarks/Program.cs delete mode 100644 src/CSF.Tests.TShock/CSF.Tests.TShock.csproj delete mode 100644 src/CSF.Tests.TShock/Module.cs delete mode 100644 src/CSF.Tests.TShock/Plugin.cs diff --git a/CSF.sln b/CSF.sln index 56979a7..5937d57 100644 --- a/CSF.sln +++ b/CSF.sln @@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSF.Core", "src\CSF.Core\CS EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSF.Tests.Console", "src\CSF.Tests.Console\CSF.Tests.Console.csproj", "{A70E8C5D-0209-433B-9AB7-9C05C0DA04E3}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSF.Tests.TShock", "src\CSF.Tests.TShock\CSF.Tests.TShock.csproj", "{10058F39-52CB-44D6-AD8B-597F34B50B45}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSF.Samples.Console", "examples\CSF.Samples.Console\CSF.Samples.Console.csproj", "{19F7A07B-7349-4AA5-A9CC-58F1002DFED6}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{DA6771C2-541A-46E0-AF1A-B4256FF4CB5E}" @@ -21,11 +19,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSF.Tests.Hosting", "src\CS EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSF.Samples.Hosting", "examples\CSF.Samples.Hosting\CSF.Samples.Hosting.csproj", "{D99DCD40-8A42-42F5-8385-88EDF87057CC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSF.Tests.Benchmarks", "src\CSF.Tests.Benchmarks\CSF.Tests.Benchmarks.csproj", "{0A3A4351-DD9A-4E25-9CA7-8CFFA5A3EE7B}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Benchmarks", "Benchmarks", "{7EB2ED0C-26A8-4F51-81CD-A0AB13B739C9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSF.Benchmark.Parsing", "CSF.Benchmark.Parsing\CSF.Benchmark.Parsing.csproj", "{6ABD1982-481C-4006-B830-997837E2B24D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSF.Benchmarks.Parsing", "src\CSF.Benchmarks.Parsing\CSF.Benchmarks.Parsing.csproj", "{6ABD1982-481C-4006-B830-997837E2B24D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -41,10 +37,6 @@ Global {A70E8C5D-0209-433B-9AB7-9C05C0DA04E3}.Debug|Any CPU.Build.0 = Debug|Any CPU {A70E8C5D-0209-433B-9AB7-9C05C0DA04E3}.Release|Any CPU.ActiveCfg = Release|Any CPU {A70E8C5D-0209-433B-9AB7-9C05C0DA04E3}.Release|Any CPU.Build.0 = Release|Any CPU - {10058F39-52CB-44D6-AD8B-597F34B50B45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {10058F39-52CB-44D6-AD8B-597F34B50B45}.Debug|Any CPU.Build.0 = Debug|Any CPU - {10058F39-52CB-44D6-AD8B-597F34B50B45}.Release|Any CPU.ActiveCfg = Release|Any CPU - {10058F39-52CB-44D6-AD8B-597F34B50B45}.Release|Any CPU.Build.0 = Release|Any CPU {19F7A07B-7349-4AA5-A9CC-58F1002DFED6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {19F7A07B-7349-4AA5-A9CC-58F1002DFED6}.Debug|Any CPU.Build.0 = Debug|Any CPU {19F7A07B-7349-4AA5-A9CC-58F1002DFED6}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -61,10 +53,6 @@ Global {D99DCD40-8A42-42F5-8385-88EDF87057CC}.Debug|Any CPU.Build.0 = Debug|Any CPU {D99DCD40-8A42-42F5-8385-88EDF87057CC}.Release|Any CPU.ActiveCfg = Release|Any CPU {D99DCD40-8A42-42F5-8385-88EDF87057CC}.Release|Any CPU.Build.0 = Release|Any CPU - {0A3A4351-DD9A-4E25-9CA7-8CFFA5A3EE7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0A3A4351-DD9A-4E25-9CA7-8CFFA5A3EE7B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0A3A4351-DD9A-4E25-9CA7-8CFFA5A3EE7B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0A3A4351-DD9A-4E25-9CA7-8CFFA5A3EE7B}.Release|Any CPU.Build.0 = Release|Any CPU {6ABD1982-481C-4006-B830-997837E2B24D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6ABD1982-481C-4006-B830-997837E2B24D}.Debug|Any CPU.Build.0 = Debug|Any CPU {6ABD1982-481C-4006-B830-997837E2B24D}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -75,11 +63,9 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {A70E8C5D-0209-433B-9AB7-9C05C0DA04E3} = {DA6771C2-541A-46E0-AF1A-B4256FF4CB5E} - {10058F39-52CB-44D6-AD8B-597F34B50B45} = {DA6771C2-541A-46E0-AF1A-B4256FF4CB5E} {19F7A07B-7349-4AA5-A9CC-58F1002DFED6} = {01CCF11A-2D95-44C9-81EA-EE7D6A36FE7A} {DC2807A7-F45F-422F-A9CB-75E1CC0A9978} = {DA6771C2-541A-46E0-AF1A-B4256FF4CB5E} {D99DCD40-8A42-42F5-8385-88EDF87057CC} = {01CCF11A-2D95-44C9-81EA-EE7D6A36FE7A} - {0A3A4351-DD9A-4E25-9CA7-8CFFA5A3EE7B} = {DA6771C2-541A-46E0-AF1A-B4256FF4CB5E} {6ABD1982-481C-4006-B830-997837E2B24D} = {7EB2ED0C-26A8-4F51-81CD-A0AB13B739C9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/CSF.Benchmark.Parsing/CSF.Benchmark.Parsing.csproj b/src/CSF.Benchmarks.Parsing/CSF.Benchmarks.Parsing.csproj similarity index 84% rename from CSF.Benchmark.Parsing/CSF.Benchmark.Parsing.csproj rename to src/CSF.Benchmarks.Parsing/CSF.Benchmarks.Parsing.csproj index 5508c60..a1b0794 100644 --- a/CSF.Benchmark.Parsing/CSF.Benchmark.Parsing.csproj +++ b/src/CSF.Benchmarks.Parsing/CSF.Benchmarks.Parsing.csproj @@ -12,7 +12,7 @@ - + diff --git a/CSF.Benchmark.Parsing/Program.cs b/src/CSF.Benchmarks.Parsing/Program.cs similarity index 92% rename from CSF.Benchmark.Parsing/Program.cs rename to src/CSF.Benchmarks.Parsing/Program.cs index ed3ebae..608f77d 100644 --- a/CSF.Benchmark.Parsing/Program.cs +++ b/src/CSF.Benchmarks.Parsing/Program.cs @@ -9,7 +9,7 @@ public class Program private static readonly StringParser _parser = new(); [Params("command", "a larger command with context", "a massive command \"with quotes\" and several additional 1 22 333 4444 5555")] - public string Text { get; set; } + public string? Text { get; set; } static void Main() => BenchmarkRunner.Run(); diff --git a/src/CSF.Core/Core/CommandManager.cs b/src/CSF.Core/Core/CommandManager.cs index 72ff44c..975ced9 100644 --- a/src/CSF.Core/Core/CommandManager.cs +++ b/src/CSF.Core/Core/CommandManager.cs @@ -14,7 +14,7 @@ namespace CSF.Core /// /// This API is completely CLS compliant where it is supported, always implementing an overload that is CLS compliant, where it otherwise would not be. /// - public class CommandManager : IDisposable + public class CommandManager { private readonly object _searchLock = new(); private readonly ResultResolver _resultHandle; @@ -313,10 +313,5 @@ public static ServiceProvider Default } } } - - public void Dispose() - { - - } } } diff --git a/src/CSF.Hosting/Hosting/HostedCommandManager.cs b/src/CSF.Hosting/Hosting/HostedCommandManager.cs index 6173c1f..eaa12e0 100644 --- a/src/CSF.Hosting/Hosting/HostedCommandManager.cs +++ b/src/CSF.Hosting/Hosting/HostedCommandManager.cs @@ -65,10 +65,5 @@ public Task StopAsync(CancellationToken cancellationToken) { throw new NotImplementedException(); } - - public void Dispose() - { - - } } } diff --git a/src/CSF.Tests.Benchmarks/BenchmarkModule.cs b/src/CSF.Tests.Benchmarks/BenchmarkModule.cs deleted file mode 100644 index e50b7b2..0000000 --- a/src/CSF.Tests.Benchmarks/BenchmarkModule.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace CSF.Tests.Benchmarks -{ - [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "")] - [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "")] - public class BenchmarkModule : ModuleBase - { - [Command("command")] - public Task MyCommand() - { - return Task.CompletedTask; - } - - [Command("command")] - public Task MyCommand(int i) - { - return Task.CompletedTask; - } - - [Command("command-op")] - public Task MyOpCommand(int i = 0) - { - return Task.CompletedTask; - } - - [Group("subcommand")] - public class InnerModule : ModuleBase - { - [Command("command")] - public Task MyCommand() - { - return Task.CompletedTask; - } - - [Command("command")] - public Task MyCommand(int i) - { - return Task.CompletedTask; - } - - [Command("command-op")] - public Task MyOpCommand(int i = 0) - { - return Task.CompletedTask; - } - } - } -} diff --git a/src/CSF.Tests.Benchmarks/CSF.Tests.Benchmarks.csproj b/src/CSF.Tests.Benchmarks/CSF.Tests.Benchmarks.csproj deleted file mode 100644 index 3c2ed5e..0000000 --- a/src/CSF.Tests.Benchmarks/CSF.Tests.Benchmarks.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - Exe - net8.0 - enable - enable - true - - - - - - - - - - - - diff --git a/src/CSF.Tests.Benchmarks/Program.cs b/src/CSF.Tests.Benchmarks/Program.cs deleted file mode 100644 index 48000bd..0000000 --- a/src/CSF.Tests.Benchmarks/Program.cs +++ /dev/null @@ -1,62 +0,0 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Running; -using Microsoft.Extensions.DependencyInjection; -using System.Diagnostics.CodeAnalysis; - -namespace CSF.Tests.Benchmarks -{ - [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "")] - - [MemoryDiagnoser] - public class Program - { - private readonly static IServiceProvider _services; - private readonly static CommandManager _manager; - - static Program() - { - _services = new ServiceCollection() - .WithCommandManager(x => - { - x.RegistrationAssemblies = new[] { typeof(Program).Assembly }; - }) - .BuildServiceProvider(); - - _manager = _services.GetRequiredService(); - - _noparam = new("command"); - _param = new("command 1"); - _paramnoprov = new("command-op"); - _noparamnested = new("subcommand command"); - _paramnested = new("subcommand command 1"); - } - - private static void Main() - => BenchmarkRunner.Run(); - - private readonly static CommandContext _noparam; - [Benchmark] - public void Parameterless() - => _manager.ExecuteAsync(_noparam); - - private readonly static CommandContext _param; - [Benchmark] - public void Parametered() - => _manager.ExecuteAsync(_param); - - private readonly static CommandContext _paramnoprov; - [Benchmark] - public void ParameteredUnprovided() - => _manager.ExecuteAsync(_paramnoprov); - - private readonly static CommandContext _noparamnested; - [Benchmark] - public void NestedParameterless() - => _manager.ExecuteAsync(_noparamnested); - - private readonly static CommandContext _paramnested; - [Benchmark] - public void NestedParametered() - => _manager.ExecuteAsync(_paramnested); - } -} \ No newline at end of file diff --git a/src/CSF.Tests.TShock/CSF.Tests.TShock.csproj b/src/CSF.Tests.TShock/CSF.Tests.TShock.csproj deleted file mode 100644 index 1e2bfe8..0000000 --- a/src/CSF.Tests.TShock/CSF.Tests.TShock.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net6.0 - enable - enable - - MSB3270 - - - - - - - diff --git a/src/CSF.Tests.TShock/Module.cs b/src/CSF.Tests.TShock/Module.cs deleted file mode 100644 index 6f4cfd4..0000000 --- a/src/CSF.Tests.TShock/Module.cs +++ /dev/null @@ -1,16 +0,0 @@ -using CSF.TShock; -using TShockAPI; - -namespace CSF.Tests.TShock -{ - [RequirePermission("plugin")] - public class Module : TSModuleBase - { - [RequirePermission("test")] - [Command("test")] - public void Test(TSPlayer player, TSPlayer _) - { - Respond(player.Name); - } - } -} diff --git a/src/CSF.Tests.TShock/Plugin.cs b/src/CSF.Tests.TShock/Plugin.cs deleted file mode 100644 index f8fecce..0000000 --- a/src/CSF.Tests.TShock/Plugin.cs +++ /dev/null @@ -1,23 +0,0 @@ -using CSF.TShock; -using Terraria; -using TerrariaApi.Server; - -namespace CSF.Tests.TShock -{ - [ApiVersion(2, 1)] - public class Plugin : TerrariaPlugin - { - private readonly TSCommandFramework _framework; - - public Plugin(Main game) - : base(game) - { - _framework = new(new()); - } - - public override async void Initialize() - { - await _framework.BuildModulesAsync(typeof(Plugin).Assembly); - } - } -} \ No newline at end of file From 1c75bb3efb3ffcc7153e6b2f6523ce325c0579a5 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Fri, 2 Feb 2024 12:30:09 +0100 Subject: [PATCH 27/40] Remove TShock sample --- .../CSF.Samples.TShock5.csproj | 15 -------- examples/CSF.Samples.TShock5/Commands.cs | 35 ------------------- examples/CSF.Samples.TShock5/Plugin.cs | 28 --------------- 3 files changed, 78 deletions(-) delete mode 100644 examples/CSF.Samples.TShock5/CSF.Samples.TShock5.csproj delete mode 100644 examples/CSF.Samples.TShock5/Commands.cs delete mode 100644 examples/CSF.Samples.TShock5/Plugin.cs diff --git a/examples/CSF.Samples.TShock5/CSF.Samples.TShock5.csproj b/examples/CSF.Samples.TShock5/CSF.Samples.TShock5.csproj deleted file mode 100644 index 8a52fa6..0000000 --- a/examples/CSF.Samples.TShock5/CSF.Samples.TShock5.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net6.0 - enable - enable - - MSB3270 - - - - - - - diff --git a/examples/CSF.Samples.TShock5/Commands.cs b/examples/CSF.Samples.TShock5/Commands.cs deleted file mode 100644 index 0b0b3c0..0000000 --- a/examples/CSF.Samples.TShock5/Commands.cs +++ /dev/null @@ -1,35 +0,0 @@ -using CSF.TShock; -using TShockAPI; - -namespace CSF.Samples.TShock -{ - // This node sets a top level permission, i.e. 'csf....' - [RequirePermission("csf")] - public sealed class Commands : TSModuleBase - { - // Defines a command with the name 'who', adding a description that the registrator will use as TShock's command HELPTEXT. - [Command("who")] - [Description("Gets all players on the server.")] - // This node will turn into 'csf.who' - [RequirePermission("who")] - public void Who(bool getIndex = false) // optional parameter getIndex, if someone would execute /who true it will resolve successfully. - { - var players = Context.Server.Players; - - var stringified = players.Select(x => $"{x.Name}{(getIndex ? $" (i: {x.Index})" : "")}"); - - Info($"All online players ({players.Count()}/{Context.Server.Settings.MaxSlots}): \n{stringified}"); - } - - [Command("playerinfo", "pi")] - //[Aliases("pi")] // -- /\ - [Description("Displays information about a player.")] - [RequirePermission("playerinfo")] - public IResult PlayerInfo(TSPlayer? player = null) - { - player ??= Context.Player; - - return Info("Player account: {}", player.Account); - } - } -} diff --git a/examples/CSF.Samples.TShock5/Plugin.cs b/examples/CSF.Samples.TShock5/Plugin.cs deleted file mode 100644 index 8a2365f..0000000 --- a/examples/CSF.Samples.TShock5/Plugin.cs +++ /dev/null @@ -1,28 +0,0 @@ -using CSF.TShock; -using Terraria; -using TerrariaApi.Server; - -namespace CSF.Samples.TShock5 -{ - [ApiVersion(2, 1)] - public sealed class Plugin : TerrariaPlugin - { - private readonly TSCommandFramework _fx; - - public Plugin(Main game) - : base(game) - { - // Define the command standardization framework made for TShock. - _fx = new(new CommandConfiguration() - { - DoAsynchronousExecution = false - }); - } - - public override async void Initialize() - { - // Build the modules available in the current assembly. - await _fx.BuildModulesAsync(typeof(Plugin).Assembly); - } - } -} \ No newline at end of file From c597b4c236450aad46f77d17c04a7b4bcc1e48a1 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Fri, 2 Feb 2024 12:30:28 +0100 Subject: [PATCH 28/40] Remove spectre, tshock roots --- src/CSF.Spectre/CSF.Spectre.csproj | 47 ------- src/CSF.Spectre/SpectreCommandContext.cs | 22 ---- src/CSF.Spectre/SpectreModuleBase.cs | 49 ------- src/CSF.TShock/CSF.TShock.csproj | 56 -------- .../Attributes/ReplaceExistingAttribute.cs | 22 ---- .../Attributes/RequirePermissionAttribute.cs | 23 ---- .../Commands/TypeReaders/PlayerReader.cs | 22 ---- .../Commands/TypeReaders/TSPlayerReader.cs | 21 --- src/CSF.TShock/ITSCommandContext.cs | 36 ----- src/CSF.TShock/Server/ServerInfo.cs | 43 ------ src/CSF.TShock/TSCommandContext.cs | 54 -------- src/CSF.TShock/TSCommandFramework.cs | 100 -------------- src/CSF.TShock/TSModuleBase.cs | 124 ------------------ 13 files changed, 619 deletions(-) delete mode 100644 src/CSF.Spectre/CSF.Spectre.csproj delete mode 100644 src/CSF.Spectre/SpectreCommandContext.cs delete mode 100644 src/CSF.Spectre/SpectreModuleBase.cs delete mode 100644 src/CSF.TShock/CSF.TShock.csproj delete mode 100644 src/CSF.TShock/Commands/Attributes/ReplaceExistingAttribute.cs delete mode 100644 src/CSF.TShock/Commands/Attributes/RequirePermissionAttribute.cs delete mode 100644 src/CSF.TShock/Commands/TypeReaders/PlayerReader.cs delete mode 100644 src/CSF.TShock/Commands/TypeReaders/TSPlayerReader.cs delete mode 100644 src/CSF.TShock/ITSCommandContext.cs delete mode 100644 src/CSF.TShock/Server/ServerInfo.cs delete mode 100644 src/CSF.TShock/TSCommandContext.cs delete mode 100644 src/CSF.TShock/TSCommandFramework.cs delete mode 100644 src/CSF.TShock/TSModuleBase.cs diff --git a/src/CSF.Spectre/CSF.Spectre.csproj b/src/CSF.Spectre/CSF.Spectre.csproj deleted file mode 100644 index bd70a35..0000000 --- a/src/CSF.Spectre/CSF.Spectre.csproj +++ /dev/null @@ -1,47 +0,0 @@ - - - - net6.0 - - 2.1 - 2.1 - 2.1.1.0 - 2.1 - - Armano den Boef and CSF contributors. - - https://github.com/Rozen4334/CSF.NET - https://github.com/Rozen4334/CSF.NET - git - - Version 2.1; Made for net6+. - Text, Console, Commands, Framework - - Command Standardization Framework Extension; Spectre.Console - An extension for CSF that adds support for Spectre.Console. - - en-US - - true - CSF.NET.Spectre - - MIT - true - Armano den Boef and CSF contributors. - - csf_nogap.png - - - - - True - \ - - - - - - - - - diff --git a/src/CSF.Spectre/SpectreCommandContext.cs b/src/CSF.Spectre/SpectreCommandContext.cs deleted file mode 100644 index c168b8b..0000000 --- a/src/CSF.Spectre/SpectreCommandContext.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Spectre.Console; -using System; - -namespace CSF.Spectre -{ - /// - public class SpectreCommandContext : CommandContext - { - /// - /// The underlying console for this handler. - /// - [CLSCompliant(false)] - public IAnsiConsole Console { get; } - - /// - public SpectreCommandContext(string rawInput, Parser parser = null) - : base(rawInput, parser) - { - Console = AnsiConsole.Console; - } - } -} diff --git a/src/CSF.Spectre/SpectreModuleBase.cs b/src/CSF.Spectre/SpectreModuleBase.cs deleted file mode 100644 index 3ea4ac1..0000000 --- a/src/CSF.Spectre/SpectreModuleBase.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Spectre.Console; -using System; -[assembly: CLSCompliant(true)] - -namespace CSF.Spectre -{ - /// - public class SpectreModuleBase : ModuleBase - where T : ICommandContext - { - /// - public override void Respond(string message) - { - AnsiConsole.MarkupLineInterpolated($"{message}"); - } - - /// - /// Returns the input value of the requested question. - /// - /// The question to ask. - /// The string with selected value within. - public string Ask(string question) - { - return AnsiConsole.Ask(question); - } - - /// - /// Returns the input value of the requested prompt. - /// - /// The prompt to request a value for. - /// The string with selected value within. - [CLSCompliant(false)] - public string Prompt(TextPrompt prompt) - { - return AnsiConsole.Prompt(prompt); - } - - /// - /// Returns the value of a selected item. - /// - /// The selection to pick from. - /// The string with selected value within. - [CLSCompliant(false)] - public string Select(SelectionPrompt selection) - { - return AnsiConsole.Prompt(selection); - } - } -} diff --git a/src/CSF.TShock/CSF.TShock.csproj b/src/CSF.TShock/CSF.TShock.csproj deleted file mode 100644 index 7c61050..0000000 --- a/src/CSF.TShock/CSF.TShock.csproj +++ /dev/null @@ -1,56 +0,0 @@ - - - - net6.0 - enable - - MSB3270 - - 1.4 - 1.4 - 1.4.0.0 - 1.4 - - Armano den Boef and CSF contributors. - - https://github.com/Rozen4334/CSF.NET - https://github.com/Rozen4334/CSF.NET - git - - Version 1.4; Made for net6+. - Text, Console, Commands, Framework, TShock - - Command Standardization Framework Extension; TShock for Terraria - An extension for CSF that adds support for TShock for Terraria. - - en-US - - true - CSF.NET.TShock - - MIT - true - Armano den Boef and CSF contributors. - - csf_nogap.png - - README.md - - - - - True - \ - - - True - \ - - - - - - - - - diff --git a/src/CSF.TShock/Commands/Attributes/ReplaceExistingAttribute.cs b/src/CSF.TShock/Commands/Attributes/ReplaceExistingAttribute.cs deleted file mode 100644 index ff69555..0000000 --- a/src/CSF.TShock/Commands/Attributes/ReplaceExistingAttribute.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace CSF.TShock -{ - /// - /// Represents an attribute that marks if another command with matching name should be replaced on registration. - /// - public sealed class ReplaceExistingAttribute : Attribute - { - /// - /// Defines if a command should be replaced on registration. - /// - public bool ShouldReplace { get; } - - /// - /// Defines a new that should replace an existing registered command. - /// - /// - public ReplaceExistingAttribute(bool replace = true) - { - ShouldReplace = replace; - } - } -} diff --git a/src/CSF.TShock/Commands/Attributes/RequirePermissionAttribute.cs b/src/CSF.TShock/Commands/Attributes/RequirePermissionAttribute.cs deleted file mode 100644 index ae7f024..0000000 --- a/src/CSF.TShock/Commands/Attributes/RequirePermissionAttribute.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace CSF.TShock -{ - /// - /// Represents an attribute that defines the required permission node at the nesting level of the current step. - /// - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)] - public class RequirePermissionAttribute : Attribute - { - /// - /// Defines a permission node for this command. Permission nodes will be joined by '.' if they're specified at nested level. - /// - public string PermissionNode { get; } - - /// - /// Defines a new from provided permission node. - /// - /// The permission node for this command. - public RequirePermissionAttribute(string permissionNode) - { - PermissionNode = permissionNode; - } - } -} diff --git a/src/CSF.TShock/Commands/TypeReaders/PlayerReader.cs b/src/CSF.TShock/Commands/TypeReaders/PlayerReader.cs deleted file mode 100644 index fbbdb74..0000000 --- a/src/CSF.TShock/Commands/TypeReaders/PlayerReader.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Terraria; -using TShockAPI; - -namespace CSF.TShock -{ - public sealed class PlayerReader : TypeReader - { - public override Task ReadAsync(IContext context, Parameter info, object value, IServiceProvider provider) - { - var players = TSPlayer.FindByNameOrID(value.ToString()); - - if (!players.Any()) - return Task.FromResult(TypeReaderResult.FromError("No player found.")); - - else if (players.Count > 1) - return Task.FromResult(TypeReaderResult.FromError($"Multiple players found: \n{string.Join(", ", players.Select(x => x.Name))}")); - - else - return Task.FromResult(TypeReaderResult.FromSuccess(players[0].TPlayer)); - } - } -} diff --git a/src/CSF.TShock/Commands/TypeReaders/TSPlayerReader.cs b/src/CSF.TShock/Commands/TypeReaders/TSPlayerReader.cs deleted file mode 100644 index 71020a7..0000000 --- a/src/CSF.TShock/Commands/TypeReaders/TSPlayerReader.cs +++ /dev/null @@ -1,21 +0,0 @@ -using TShockAPI; - -namespace CSF.TShock -{ - public sealed class TSPlayerReader : TypeReader - { - public override Task ReadAsync(IContext context, Parameter info, object value, IServiceProvider provider) - { - var players = TSPlayer.FindByNameOrID(value.ToString()); - - if (!players.Any()) - return Task.FromResult(TypeReaderResult.FromError("No player found.")); - - else if (players.Count > 1) - return Task.FromResult(TypeReaderResult.FromError($"Multiple players found: \n{string.Join(", ", players.Select(x => x.Name))}")); - - else - return Task.FromResult(TypeReaderResult.FromSuccess(players[0])); - } - } -} diff --git a/src/CSF.TShock/ITSCommandContext.cs b/src/CSF.TShock/ITSCommandContext.cs deleted file mode 100644 index 030ba28..0000000 --- a/src/CSF.TShock/ITSCommandContext.cs +++ /dev/null @@ -1,36 +0,0 @@ -using TShockAPI; - -namespace CSF.TShock -{ - /// - /// Represents the context for TShock commands. - /// - /// - public interface ITSCommandContext : IContext - { - /// - /// Determines if the command is silently executed or not. - /// - bool IsSilent { get; } - - /// - /// The player invoking this command. - /// - TSPlayer Player { get; } - - /// - /// The server this command was invoked on. - /// - ServerInfo Server { get; } - - /// - /// The TShock arguments provided for this command. - /// - CommandArgs CommandArguments { get; } - - /// - /// The prefix for the command. - /// - IPrefix Prefix { get; } - } -} diff --git a/src/CSF.TShock/Server/ServerInfo.cs b/src/CSF.TShock/Server/ServerInfo.cs deleted file mode 100644 index 1c84e30..0000000 --- a/src/CSF.TShock/Server/ServerInfo.cs +++ /dev/null @@ -1,43 +0,0 @@ -using TShockAPI; - -namespace CSF.TShock -{ - /// - /// Information about the server that this command was invoked on. - /// - public class ServerInfo - { - /// - /// All players online at the moment of command execution. - /// - /// - /// This value represents all players that are not , and a . - /// - public IEnumerable Players { get; } - - /// - /// The amount of players on the server at the moment of command execution. - /// - public int PlayerCount { get; } - - /// - /// The player acting as the server itself. - /// - public TSPlayer Console { get; } - - /// - /// The settings defined on the server itself. - /// - public TShockAPI.Configuration.TShockSettings Settings { get; } - - internal ServerInfo() - { - Players = TShockAPI.TShock.Players.Where(x => x is not null && x.Active && x.RealPlayer); - PlayerCount = Players.Count(); - - Console = TSPlayer.Server; - - Settings = TShockAPI.TShock.Config.Settings; - } - } -} diff --git a/src/CSF.TShock/TSCommandContext.cs b/src/CSF.TShock/TSCommandContext.cs deleted file mode 100644 index 9fc0e4a..0000000 --- a/src/CSF.TShock/TSCommandContext.cs +++ /dev/null @@ -1,54 +0,0 @@ -using TShockAPI; - -namespace CSF.TShock -{ - /// - /// A command context providing TShock command info. - /// - public class TSCommandContext : ITSCommandContext - { - /// - public string Name { get; set; } - - /// - public IReadOnlyList Parameters { get; set; } - - /// - /// Represents the raw command input. - /// - public string RawInput { get; set; } - - /// - public bool IsSilent { get; } - - /// - public TSPlayer Player { get; } - - /// - public ServerInfo Server { get; } - - /// - public CommandArgs CommandArguments { get; } - - /// - public IPrefix Prefix { get; } - - /// - /// Creates a new CommandContext from the provided - /// - /// - /// - public TSCommandContext(CommandArgs args, string rawInput, IPrefix prefix) - { - Prefix = prefix; - - CommandArguments = args; - RawInput = rawInput; - Parameters = args.Parameters; - Player = args.Player; - IsSilent = args.Silent; - - Name = rawInput.Split(' ')[0]; - } - } -} diff --git a/src/CSF.TShock/TSCommandFramework.cs b/src/CSF.TShock/TSCommandFramework.cs deleted file mode 100644 index d7d8436..0000000 --- a/src/CSF.TShock/TSCommandFramework.cs +++ /dev/null @@ -1,100 +0,0 @@ -using TShockAPI; - -namespace CSF.TShock -{ - /// - /// Represents a intended to be functional for TShock plugins. - /// - public class TSCommandFramework : CommandFramework - { - /// - /// Creates a new for processing modules inside the framework. - /// - /// - public TSCommandFramework(CommandConfiguration config) - : base(config) - { - config.InvokeOnlyNameRegistrations = true; - - base.CommandRegistered += CommandRegistered; - - config.TypeReaders - .Include(new TSPlayerReader()) - .Include(new PlayerReader()); - - config.Prefixes - .Include(new StringPrefix("/")) - .Include(new StringPrefix(".")); - } - - private new Task CommandRegistered(IConditionalComponent arg) - { - var permissions = new List(); - bool shouldReplace = false; - string description = ""; - foreach (var attribute in arg.Attributes) - { - if (attribute is RequirePermissionAttribute permAttribute) - permissions.Add(permAttribute.PermissionNode); - - if (attribute is ReplaceExistingAttribute replaceAttribute) - shouldReplace = replaceAttribute.ShouldReplace; - - if (attribute is DescriptionAttribute descriptionAttribute) - description = descriptionAttribute.Description; - } - - if (shouldReplace) - { - TShockAPI.Commands.ChatCommands.RemoveAll(x => x.Names.Any(o => arg.Aliases.Any(n => o == n))); - - // A hacky resolver for replacing TShock commands. Consider making this optional. - var range = Commands.TShockCommands.Where(x => x.Names.Any(o => arg.Aliases.Any(n => o == n))).ToList(); - - if (TShockAPI.Commands.TShockCommands.Count != range.Count) - TShockAPI.Commands.TShockCommands = new((IList)range); - } - - TShockAPI.Commands.ChatCommands.Add(new TShockAPI.Command(string.Join(".", (IEnumerable)permissions), async (x) => await ExecuteCommandAsync(x), arg.Aliases) - { - HelpText = description - }); - - return Task.CompletedTask; - } - - public virtual Task CreateContextAsync(CommandArgs args, string rawInput) - { - if (!TryParsePrefix(ref rawInput, out var prefix)) - prefix = EmptyPrefix.Create(); - - return Task.FromResult(new TSCommandContext(args, rawInput, prefix)); - } - - public virtual async Task ExecuteCommandAsync(CommandArgs args) - { - var context = await CreateContextAsync(args, args.Message); - - var result = await ExecuteCommandAsync(context); - - if (!result.IsSuccess) - args.Player.SendErrorMessage(result.ErrorMessage); - } - - protected override SearchResult CommandNotFoundResult(T context) - { - return SearchResult.FromError($"A command with name: '{context.Name}' was not found."); - } - - protected override SearchResult NoApplicableOverloadResult(T context) - { - return SearchResult.FromError($"No best override was found for command with name: '{context.Name}'."); - } - - protected override ExecuteResult UnhandledExceptionResult(T context, Command command, Exception ex) - { - TShockAPI.TShock.Log.ConsoleError(ex.ToString()); - return ExecuteResult.FromError($"An unhandled exception has occurred. Please check logs for more details"); - } - } -} diff --git a/src/CSF.TShock/TSModuleBase.cs b/src/CSF.TShock/TSModuleBase.cs deleted file mode 100644 index ce89b41..0000000 --- a/src/CSF.TShock/TSModuleBase.cs +++ /dev/null @@ -1,124 +0,0 @@ -using Microsoft.Xna.Framework; - -namespace CSF.TShock -{ - /// - public class TSModuleBase : ModuleBase - where T : ITSCommandContext - { - /// - public override ExecuteResult Error(string message, params object[] values) - { - Context.Player.SendErrorMessage(message, values); - return ExecuteResult.FromSuccess(); - } - - /// - public override Task ErrorAsync(string message, params object[] values) - { - return Task.FromResult(Error(message, values)); - } - - /// - /// Responds with a multi-match error. - /// - /// The found matches. - public ExecuteResult Error(IEnumerable matches) - { - Context.Player.SendMultipleMatchError(matches); - return ExecuteResult.FromSuccess(); - } - - /// - /// Responds with a multi-match error. - /// - /// The found matches. - /// An asynchronous with no return type. - public Task ErrorAsync(IEnumerable matches) - { - return Task.FromResult(Error(matches)); - } - - /// - public override ExecuteResult Info(string message, params object[] values) - { - Context.Player.SendInfoMessage(message, values); - return ExecuteResult.FromSuccess(); - } - - /// - public override Task InfoAsync(string message, params object[] values) - { - return Task.FromResult(Info(message, values)); - } - - /// - public override ExecuteResult Success(string message, params object[] values) - { - Context.Player.SendSuccessMessage(message, values); - return ExecuteResult.FromSuccess(); - } - - /// - public override Task SuccessAsync(string message, params object[] values) - { - return Task.FromResult(Success(message, values)); - } - - /// - public override ExecuteResult Respond(string message, params object[] values) - { - return Respond(string.Format(message, values), Color.LightGray); - } - - /// - public override Task RespondAsync(string message, params object[] values) - { - return Task.FromResult(Respond(string.Format(message, values), Color.LightGray)); - } - - /// - /// Responds with the provided color. - /// - /// The message to send. - /// The color to send this message in. - public ExecuteResult Respond(string message, Color color) - { - Context.Player.SendMessage(message, color); - return ExecuteResult.FromSuccess(); - } - - /// - /// Responds with the provided color. - /// - /// The message to send. - /// The color to send this message in. - /// An asynchronous with no return type. - public Task RespondAsync(string message, Color color) - { - return Task.FromResult(Respond(message, color)); - } - - /// - /// Announce a message to the entire server with provided color. - /// - /// The message to send. - /// The color to send this message in. - public ExecuteResult Announce(string message, Color color) - { - TShockAPI.TShock.Utils.Broadcast(message, color); - return ExecuteResult.FromSuccess(); - } - - /// - /// Announce a message to the entire server with provided color. - /// - /// The message to send. - /// The color to send this message in. - /// An asynchronous with no return type. - public Task AnnounceAsync(string message, Color color) - { - return Task.FromResult(Announce(message, color)); - } - } -} From eb2be1e90feab72e6039c9954b9c27e42c62440f Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Fri, 2 Feb 2024 12:32:38 +0100 Subject: [PATCH 29/40] Wiki push --- wiki/Home.md | 70 +++++ wiki/_Footer.md | 10 + "wiki/\342\232\241-Quick-Guide.md" | 303 +++++++++++++++++++++ "wiki/\342\234\205-Preconditions.md" | 101 +++++++ "wiki/\360\237\223\226-Typereaders.md" | 55 ++++ "wiki/\360\237\223\235-Command-Context.md" | 1 + 6 files changed, 540 insertions(+) create mode 100644 wiki/Home.md create mode 100644 wiki/_Footer.md create mode 100644 "wiki/\342\232\241-Quick-Guide.md" create mode 100644 "wiki/\342\234\205-Preconditions.md" create mode 100644 "wiki/\360\237\223\226-Typereaders.md" create mode 100644 "wiki/\360\237\223\235-Command-Context.md" diff --git a/wiki/Home.md b/wiki/Home.md new file mode 100644 index 0000000..8f72b50 --- /dev/null +++ b/wiki/Home.md @@ -0,0 +1,70 @@ +## Welcome to the CSF.NET wiki! + +CSF's main focus is for its users to enjoy writing code with the library and master it with relative ease. +For this claim to have any value, a great deal is done to help a developer write intuitive and boilerplate-less code, alongside keeping documentation simple and complete. + +- Get started with CSF quickly by checking out the **[[Quick Guide|⚡-Quick-Guide]]**. + +### 💉 Additional Packages + +CSF is not without its own dependencies, but it tries its best to keep all dependencies within a trusted atmosphere, using packages only when they outweigh self-written implementations. So far, it only depends on packages published by official channels of the .NET ecosystem. + +#### Dependency Injection + +Having grown into a vital part of building effective and modern applications, Dependency Injection (DI) is no less important to be carried along in the equally modern CSF. +It integrates this feature deeply into its architecture and depends on it to function from the ground up. + +For applications to function with `CSF.Core` or `CSF.Spectre`, it is necessary to install DI functionality through Microsoft's publicized package(s): + +```xml + +``` +> This and other required packages can also be installed through the Package Manager, .NET CLI or from within your IDE. + +#### Hosting + +Carrying out further support within the .NET ecosystem, CSF also introduces a hosting package for deploying apps with the .NET generic host. + +For applications to function with `CSF.Hosting`, it is necessary to install the hosting package that it also implements, also publicized by Microsoft itself: + +```xml + +``` + +> The hosting extensions package publicized by Microsoft implements the packages necessary for the core component of CSF, and does not expect to have its dependencies implemented alongside it. + +*For each of these packages, the minimum version is determined by CSF itself, usually being the latest or equal to the target framework upon which it was released. It is suggested to choose the latest version at time of installing.* + +### ⚒️ Customizability + +While already fully functional out of the box, this command framework does not shy away from covering extensive applications with more specific needs, which in turn need more than the base features to function according to its developer's expectations. Within CSF, the `CommandManager` can be inherited, opening up a range of protected virtual methods that are applicable during pipeline execution. These can be overridden to modify and customize the pipeline to accompany various needs. + +Alongside this, `CommandContext`'s, `TypeReader`'s, `PreconditionAttribute`'s and `Parser`'s can all be inherited and custom ones created for environmental specifics, custom type interpretation and more. + +### ⚡ Performance + +While CSF's primary focus is to be the most versatile and easy to use command framework, it also aims to carry high performance throughout the entire execution flow, while keeping source code readable and comprehensible for library contributors and users alike. +Benchmarks are ran for every version of CSF, with a near complete guarantee that it will not reach higher values upon newly released versions. + +> The only cases where optimization will be cast aside is when the developer experience outweighs the need for further optimization. +> This can be by reason of introducing certain features in the execution pipeline, or by newly introduced steps within said pipeline. In any other cases, be it minor updates or revisions, there will be no change in execution performance. + +Benchmarks of the most recent stable release (`CSF 2.0`) show the following results: + +| Method | Mean | Error | StdDev | Gen0 | Allocated | +|---------------------- |---------:|---------:|--------:|-------:|----------:| +| Parameterless | 701.7 ns | 8.99 ns | 8.41 ns | 0.1745 | 1.07 KB | +| Parametered | 884.9 ns | 10.56 ns | 9.36 ns | 0.2060 | 1.27 KB | +| ParameteredUnprovided | 898.5 ns | 10.40 ns | 9.22 ns | 0.2069 | 1.27 KB | +| NestedParameterless | 693.6 ns | 7.41 ns | 6.94 ns | 0.1745 | 1.07 KB | +| NestedParametered | 895.2 ns | 5.68 ns | 5.03 ns | 0.2060 | 1.27 KB | + +*These benchmarks represent the command execution pipeline from the moment when a parsed `CommandContext` is provided to the framework to be executed, to post execution handling.* + +#### Legend: +- Mean : Arithmetic mean of all measurements. +- Error : Half of 99.9% confidence interval. +- StdDev : Standard deviation of all measurements. +- Gen0 : GC Generation 0 collects per 1000 operations. +- Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B). +- 1 ns : 1 Nanosecond (0.000000001 sec). diff --git a/wiki/_Footer.md b/wiki/_Footer.md new file mode 100644 index 0000000..da7f7ef --- /dev/null +++ b/wiki/_Footer.md @@ -0,0 +1,10 @@ +# 🔍 Recommended Content + +### [[Learn About Typereaders|📖-Typereaders]] +Learn to use typereaders, which are responsible for parsing a parameter input into your desired value. + +### [[Using Preconditions|✅-Preconditions]] +Use preconditions to ensure that the command is allowed to be executed. + +### [[Customizing the Command Context|📝-Command-Context]] +Customize your Command Context with your own added information related to the command or the executing target. \ No newline at end of file diff --git "a/wiki/\342\232\241-Quick-Guide.md" "b/wiki/\342\232\241-Quick-Guide.md" new file mode 100644 index 0000000..50482f5 --- /dev/null +++ "b/wiki/\342\232\241-Quick-Guide.md" @@ -0,0 +1,303 @@ +This guide introduces you to CSF, covering the basics for developers ranging from beginner to veteran. + +- [Before Coding](#-setting-up-the-basics) +- [Defining Commands](#-defining-commands) +- [Running your Commands](#-running-your-commands) +- [Putting it Together](#-summary) + +## 📩 Before Coding + +Before we can start writing code, there are a few things to set up. This includes getting a fresh project to start, and installing CSF. + +### Creating a New Project + +Before being able to write code, we need to give ourselves a place to do it. +With your favourite code editor, we will create a new empty project. +This project can be anything, but in our case, a **Console Application** will be the focus. +If you are using `dotnet-cli`, you can create such a project by executing `dotnet new console`. +Keep in mind that the project has to target `.net6` or higher. + +> In a visual editor such as Rider or Visual studio, you can create it by simply opening the application, navigating to 'Create New Project' and selecting the Console Application template. + +### Installing the Package + +To be able to reference CSF, we will need to head over to your package manager to install the package. +This can be in the PM console, in your `.csproj` file or through the code editor. +We can grab the latest version from [NuGet](https://www.nuget.org/packages/CSF.NET). +If you prefer to use the visual tools, simply look up `csf.net` and install the first package available. + +## 📝 Defining Commands + +### Creating a Module + +Let's create a new file in your favourite code editor. +The ideal way to do this is by calling it `XModule.cs` where `X` is its name or category. + +After creating the file, we should see the following: + +```cs +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace XProject +{ + internal class XModule + { + } +} +``` + +Next up, we will want to make the class public, and turn it into a module. +To do that, we need to include CSF into the usings, and change the signature of the class to inherit `ModuleBase`. +With both of those steps out of the way, it should now look like this: + +```cs +using CSF; + +namespace XProject +{ + public class XModule : ModuleBase + { + } +} +``` + +> Starting in .NET6, [implicit usings](https://learn.microsoft.com/en-gb/dotnet/core/project-sdk/overview#implicit-using-directives) are part of the core features. +> Because we will assume that this is a clean, newly created project on .NET 6 or higher, we can remove the usings without an effect on the functionality. +> If you do see errors from removing the extra usings, please ignore this advice. + +### Creating Commands + +With your basic module complete, we are now ready to start writing commands. +To begin, we will write 2 commands: + +#### Command 1: Hello World + +Within the class declaration of the file, we will write a new method. +The method can return `Task` or `void`, but for the sake of simplicity, we will use `void`. + +```cs +using CSF; + +namespace XProject +{ + public class XModule : ModuleBase + { + public void HelloWorld() + { + } + } +} +``` + +Next up, we will want to decorate the method with an attribute. The `[Command]` attribute accepts a name, and registers the command by it, so we can use that name to run it from our input source. We will call the command `helloworld`. Let's add the attribute and see how that looks: + +```cs +using CSF; + +namespace XProject +{ + public class XModule : ModuleBase + { + [Command("helloworld")] + public void HelloWorld() + { + } + } +} +``` + +Now the `HelloWorld` method will run when we run the `helloworld` command, but there is still one thing missing. The method is empty! You can add anything here, but in our case, a simple reply will do: + +```cs +using CSF; + +namespace XProject +{ + public class XModule : ModuleBase + { + [Command("helloworld")] + public void HelloWorld() + { + Respond("Hello world!"); + } + } +} +``` + +> To actually start testing and running the command, skip to [# + +#### Command 2: Reply + +Our next command will be quite simple as well. Just like before, we can create a new method, mark it with an attribute and give it a name. Except this time, it will accept input, and respond with it. Let's create this functionality: + +```cs +using CSF; + +namespace XProject +{ + public class XModule : ModuleBase + { + [Command("helloworld")] + public void HelloWorld() + { + Respond("Hello world!"); + } + + [Command("reply")] + public void Reply([Remainder] string message) + { + Respond(message); + } + } +} +``` + +As you can see here, the reply method accepts a parameter called `message`, and replies to the command with its value. Normally, this would automatically accept a string parameter, usually a single word: `word` or multiple words connected by quotations: `"a few words"`. + +In our case however, the command is marked with `[Remainder]`. This attribute has a special property, it ignores all parameters provided after it. Instead, it connects all of them together and makes it one long string (or object). This way you don't need to add quotation marks, making the command more intuitive to use. + +## 🏗️ Running your Commands + +CSF is quite simple in that it does a very good job in doing things *for* you. Because of this, setting up your commands is not actually directly visible. But, there is one very important step, and for it, we need to look at a more advanced concept in programming. Dependency Injection! + +### Setting up Dependency Injection + +You don't actually need to understand how dependency injection works to enjoy its benefits. +For newcomers, it is worth it to do some research, but this guide does not depend on it. +CSF does however, expect a component to be installed to work with it in the background. For installing it, please refer to [here](https://github.com/csmir/CSF.NET/wiki#-additional-packages). + +> Dependency Injection is well documented by Microsoft and the .NET foundation [here](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection). + +To set up the framework with it, we will go back to the `Program.cs` file. +It may look different for everyone, but for simplicity sake, we will only focus on [top-level statements](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/top-level-statements). + +Let's start with the basics, and define a new `ServiceCollection`. +This collection works like a list, and things can be added to it. +CSF already has the needed methods to configure everything at once, so all we have to do is call them: + +```cs +using CSF; +using Microsoft.Extensions.DependencyInjection; + +var collection = new ServiceCollection(); + +collection.WithCommandManager(); + +var services = collection.BuildServiceProvider(); +``` + +The `AddCommandManager` method configures everything we need, and leaves very little for us to do. +With that out of the way, we can build the collection into a provider. +To do this, we call the `BuildServiceProvider` method. + +Now that everything in the background has been built, the command manager has also been made accessible to us. +While staying in `Program.cs`, we can grab the manager from the new `services` variable by calling `GetRequiredService`. + +```cs +var framework = services.GetRequiredService(); +``` + +Hovering over the `CommandManager` declaration, you can read some things about it. It is the root type, or class, that makes CSF work as a whole. +You can greatly customize it and make a lot of changes to your personal taste. +Though, we only need very little from it. + +### Listening to Input + +The most important part of running a command is defining *what* command you want to run. +In our case, we want to test the `helloworld` command, so we can define that as our input. +You can replace the way to get your input with anything you like, ranging from `Console.ReadLine` to listening to a discord message. + +```cs +var input = "helloworld"; + +var context = new CommandContext(input); +``` + +With this input, we can create the most important component to running a command: The context. +Execution accepts `ICommandContext` and will use that to parse and run the command. +For regular string commands, `CommandContext` is predefined by CSF for you to use. + +### Running the Command + +With the context defined, it is time to run the command. +By calling `ExecuteAsync`, CSF will read your command input and try to find the most appropriate command to run. +In our case, it is an exact match to our `helloworld` command, so it will succeed the search and run it. + +```cs +var result = await framework.ExecuteAsync(context); + +if (result.Failed()) + throw result.Exception; +``` + +The final step is to handle the result of the command. +If the command failed in any way, the `CommandResult` returned by the execution will cover where it went wrong and contain the exception that occurred. +Here, it is simplest to see if it failed, and then throw the exception if it did. + +## 📑 Summary + +Let's review what we achieved in this guide. +First of all, we created an empty project and installed the package. +After that, we defined our module that implements 2 commands. +One accepting a value, and one being a simple trigger. + +To run the command, we moved back to the `Program.cs` file and implemented the code to set up the commands. +Finally, we set up the requirements to run it. + +Let's take a look at the code we wrote to summarize our work. + +### Module and Commands + +```cs +#XModule.cs +using CSF; + +namespace XProject +{ + public class XModule : ModuleBase + { + [Command("helloworld")] + public void HelloWorld() + { + Respond("Hello world!"); + } + + [Command("reply")] + public void Reply([Remainder] string message) + { + Respond(message); + } + } +} +``` + +### Setup and Execution + +```cs +#Program.cs +using CSF; +using Microsoft.Extensions.DependencyInjection; + +var collection = new ServiceCollection() + .AddCommandManager(); + +var services = collection.BuildServiceProvider(); + +var framework = services.GetRequiredService(); + +var input = "helloworld"; + +var context = new CommandContext(input); + +var result = await framework.ExecuteAsync(context); + +if (result.Failed()) + throw result.Exception; +``` + +If these files match yours, you're good to go. +To run your command, simply run the application and see the result. \ No newline at end of file diff --git "a/wiki/\342\234\205-Preconditions.md" "b/wiki/\342\234\205-Preconditions.md" new file mode 100644 index 0000000..58cf628 --- /dev/null +++ "b/wiki/\342\234\205-Preconditions.md" @@ -0,0 +1,101 @@ +Preconditions are checks that ensure that the command in scope is allowed to be executed. +Let's work on an example precondition to understand how they work. + +- [Creating your Precondition](#-creating-your-precondition) +- [Using your Precondition](#-using-your-precondition) + +## 🏗️ Creating your Precondition + +All preconditions inherit `PreconditionAttribute`, which in turn inherits `Attribute`. To start creating your own precondition, you have to inherit `PreconditionAttribute` on a class: + +```cs +using CSF; + +namespace XProject +{ + public class RequireOperatingSystemAttribute : PreconditionAttribute + { + public override Result Evaluate(ICommandContext context, Command command, IServiceProvider provider) + { + } + } +} +``` + +With this class defined, and the method that will operate the evaluation being implemented, we can now write our code which defines the succession and failure conditions. + +First of all, the restriction must be compared against. To define this, we will implement a constructor parameter, automatically resolved as an attribute parameter by the IDE or code editor: + +```cs +using CSF; + +namespace XProject +{ + public class RequireOperatingSystemAttribute : PreconditionAttribute + { + + public PlatformID Platform { get; } + + public RequireOperatingSystemAttribute(PlatformID platform) + { + Platform = platform; + } + + public override Result Evaluate(ICommandContext context, Command command, IServiceProvider provider) + { + } + } +} +``` + +After this has been defined, the `Platform` property can be used to evaluate the current operating system against. Our focus goes to the `Evaluate` method, which will run this check. + +```cs + public override Result Evaluate(ICommandContext context, Command command, IServiceProvider provider) + { + if (Environment.OSVersion.Platform == Platform) + return Success(); + + return Failure("The platform this command was executed does not support this operation."); + } +``` + +That's it. With this done, we can look towards the application of our created precondition. + +## 📝 Using your Precondition + +After you have written your precondition, it is time to use it. Let's define a command that depends on the operating system to run. + +```cs +[Command("copy")] +public void Copy([Remainder] string toCopy) +{ + Process clipboardExecutable = new() + { + StartInfo = new ProcessStartInfo + { + RedirectStandardInput = true, + FileName = "clip", + } + }; + clipboardExecutable.Start(); + + clipboardExecutable.StandardInput.Write(toCopy); + clipboardExecutable.StandardInput.Close(); + + Respond("Succesfully copied the content to your clipboard."); +} +``` + +This method will use the windows clip executable to copy a string onto your clipboard. +This method does not work on macOS or Linux, so we have to make sure the command is executed on windows. + +To do this, all we have to do is add `[RequireOperatingSystem(PlatformID.Win32NT)]` above the method, like so: + +```cs +[Command("copy")] +[RequireOperatingSystem(PlatformID.Win32NT)] +public void Copy([Remainder] string toCopy) +``` + +The precondition is now defined on this command, and will be called when this command is triggered. \ No newline at end of file diff --git "a/wiki/\360\237\223\226-Typereaders.md" "b/wiki/\360\237\223\226-Typereaders.md" new file mode 100644 index 0000000..0c5e27a --- /dev/null +++ "b/wiki/\360\237\223\226-Typereaders.md" @@ -0,0 +1,55 @@ +Typereaders read provided argument input in string format and try to convert them into the types as defined in the command signature. +Let's work on an example Typereader to learn how they work. + +- [Creating your Typereader](#-creating-your-typereader) +- [Using your Typereader](#-using-your-typereader) + +## 🏗️ Creating your Typereader + +All Typereaders inherit `TypeReader` or `TypeReader`. To start creating your Typereader, you have to inherit one of the two on a class. + +> For the simplicity of this documentation, only the generic type is introduced here. + +```cs +using CSF; + +namespace XProject +{ + public class GuidTypeReader : TypeReader + { + public override Result Evaluate(ICommandContext context, IParameterComponent parameter, IServiceProvider services, string value) + { + } + } +} +``` + +With this class defined and the method that will operate the evaluation being implemented, we can now write our code which defines the succession and failure conditions. In case of success, we also need to pass in the parsed object that the typereader expects to see returned. + +```cs + public override Result Evaluate(ICommandContext context, IParameterComponent parameter, IServiceProvider services, string value) + { + if (Guid.TryParse(value, out Guid guid)) + return Success(guid); + + return Failure("Failed to parse the input string as a GUID."); + } +``` + +That's it. With this done, we can look towards the application of our created Typereader. + +## 📝 Using your Typereader + +After you have written your Typereader, it is time to use it. Let's define a command that receives a `Guid` as one of its parameters. + +```cs +[Command("guid")] +public void Guid(Guid guid) +{ + Respond("Here is your guid: " + guid.ToString()); +} +``` + +This method will accept a Guid as parameter, and then responds with it. This can be handled any other way. Because of how CSF is written, if the Typereader is defined in the same assembly as registered commands, it will also be automatically registered. + +> If your Typereader by special chance is not automatically registered, or you want to register X amount of the same type of typereader, it can be added manually through `IServiceCollection.WithCommandManager(x => x.TypeReaders = [])` \ No newline at end of file diff --git "a/wiki/\360\237\223\235-Command-Context.md" "b/wiki/\360\237\223\235-Command-Context.md" new file mode 100644 index 0000000..63e8645 --- /dev/null +++ "b/wiki/\360\237\223\235-Command-Context.md" @@ -0,0 +1 @@ +Customizing the Command Context \ No newline at end of file From caa2b8453c8a2000a8b49c3e94b6fa09bc53056d Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Fri, 2 Feb 2024 12:35:51 +0100 Subject: [PATCH 30/40] Move /wiki to /docs --- {wiki => docs}/Home.md | 0 {wiki => docs}/_Footer.md | 0 .../\342\232\241-Quick-Guide.md" | 0 .../\342\234\205-Preconditions.md" | 0 .../\360\237\223\226-Typereaders.md" | 0 .../\360\237\223\235-Command-Context.md" | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename {wiki => docs}/Home.md (100%) rename {wiki => docs}/_Footer.md (100%) rename "wiki/\342\232\241-Quick-Guide.md" => "docs/\342\232\241-Quick-Guide.md" (100%) rename "wiki/\342\234\205-Preconditions.md" => "docs/\342\234\205-Preconditions.md" (100%) rename "wiki/\360\237\223\226-Typereaders.md" => "docs/\360\237\223\226-Typereaders.md" (100%) rename "wiki/\360\237\223\235-Command-Context.md" => "docs/\360\237\223\235-Command-Context.md" (100%) diff --git a/wiki/Home.md b/docs/Home.md similarity index 100% rename from wiki/Home.md rename to docs/Home.md diff --git a/wiki/_Footer.md b/docs/_Footer.md similarity index 100% rename from wiki/_Footer.md rename to docs/_Footer.md diff --git "a/wiki/\342\232\241-Quick-Guide.md" "b/docs/\342\232\241-Quick-Guide.md" similarity index 100% rename from "wiki/\342\232\241-Quick-Guide.md" rename to "docs/\342\232\241-Quick-Guide.md" diff --git "a/wiki/\342\234\205-Preconditions.md" "b/docs/\342\234\205-Preconditions.md" similarity index 100% rename from "wiki/\342\234\205-Preconditions.md" rename to "docs/\342\234\205-Preconditions.md" diff --git "a/wiki/\360\237\223\226-Typereaders.md" "b/docs/\360\237\223\226-Typereaders.md" similarity index 100% rename from "wiki/\360\237\223\226-Typereaders.md" rename to "docs/\360\237\223\226-Typereaders.md" diff --git "a/wiki/\360\237\223\235-Command-Context.md" "b/docs/\360\237\223\235-Command-Context.md" similarity index 100% rename from "wiki/\360\237\223\235-Command-Context.md" rename to "docs/\360\237\223\235-Command-Context.md" From 29691e16e4dde367c33bd1dfb7ee6349cbd52b1f Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Fri, 2 Feb 2024 12:37:40 +0100 Subject: [PATCH 31/40] Simplify naming in tree --- .../Command-Context.md | 0 "docs/\342\234\205-Preconditions.md" => docs/Preconditions.md | 0 "docs/\342\232\241-Quick-Guide.md" => docs/Quick-Guide.md | 0 "docs/\360\237\223\226-Typereaders.md" => docs/Typereaders.md | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename "docs/\360\237\223\235-Command-Context.md" => docs/Command-Context.md (100%) rename "docs/\342\234\205-Preconditions.md" => docs/Preconditions.md (100%) rename "docs/\342\232\241-Quick-Guide.md" => docs/Quick-Guide.md (100%) rename "docs/\360\237\223\226-Typereaders.md" => docs/Typereaders.md (100%) diff --git "a/docs/\360\237\223\235-Command-Context.md" b/docs/Command-Context.md similarity index 100% rename from "docs/\360\237\223\235-Command-Context.md" rename to docs/Command-Context.md diff --git "a/docs/\342\234\205-Preconditions.md" b/docs/Preconditions.md similarity index 100% rename from "docs/\342\234\205-Preconditions.md" rename to docs/Preconditions.md diff --git "a/docs/\342\232\241-Quick-Guide.md" b/docs/Quick-Guide.md similarity index 100% rename from "docs/\342\232\241-Quick-Guide.md" rename to docs/Quick-Guide.md diff --git "a/docs/\360\237\223\226-Typereaders.md" b/docs/Typereaders.md similarity index 100% rename from "docs/\360\237\223\226-Typereaders.md" rename to docs/Typereaders.md From 9f2208b7bce05d29169cb6feb2d98f844d9ecd0c Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Fri, 2 Feb 2024 13:24:39 +0100 Subject: [PATCH 32/40] Niche bug fixes --- src/CSF.Core/Core/CommandManager.cs | 48 +++++++++-------- src/CSF.Core/Core/Results/Impl/CheckResult.cs | 5 ++ src/CSF.Core/Helpers/ExecutionHelpers.cs | 7 +-- src/CSF.Core/Reflection/Impl/CommandInfo.cs | 2 +- .../Reflection/Impl/ComplexArgumentInfo.cs | 2 +- src/CSF.Tests.Console/Complex/ComplexType.cs | 2 +- .../Complex/ComplexerType.cs | 2 +- src/CSF.Tests.Console/LoggingContext.cs | 54 +++++++++++++++++++ src/CSF.Tests.Console/Modules/AsyncModule.cs | 2 +- src/CSF.Tests.Console/Modules/Module.cs | 1 - src/CSF.Tests.Console/Program.cs | 7 +-- 11 files changed, 97 insertions(+), 35 deletions(-) create mode 100644 src/CSF.Tests.Console/LoggingContext.cs diff --git a/src/CSF.Core/Core/CommandManager.cs b/src/CSF.Core/Core/CommandManager.cs index 975ced9..4d18c6f 100644 --- a/src/CSF.Core/Core/CommandManager.cs +++ b/src/CSF.Core/Core/CommandManager.cs @@ -60,7 +60,7 @@ public CommandManager(IServiceProvider services, CommandConfiguration configurat Configuration = configuration; _resultHandle = services.GetService() - ?? ResultResolver.Default; + ?? Configuration.ResultResolver; } /// @@ -159,6 +159,7 @@ private async Task ExecuteInternalAsync(ICommandContext context, object[] args, if (c is 0) { await _resultHandle.TryHandleAsync(context, new SearchResult(new SearchException("No commands were found with the provided input.")), Services); + return; } // if there is a fallback present, we send matchfailure. @@ -190,7 +191,7 @@ private async ValueTask MatchAsync(ICommandContext context, SearchR if (!readResult[i].Success) return new(search.Command, readResult[i].Exception); - reads[i] = readResult[i]; + reads[i] = readResult[i].Value; } // return successful match if execution reaches here. @@ -198,10 +199,27 @@ private async ValueTask MatchAsync(ICommandContext context, SearchR } #endregion + #region Checking + private async ValueTask CheckAsync(ICommandContext context, CommandInfo command, CancellationToken cancellationToken) + { + context.LogDebug("Attempting validations for {0}", command); + + foreach (var precon in command.Preconditions) + { + var result = await precon.EvaluateAsync(context, Services, command, cancellationToken); + + if (!result.Success) + return result; + } + + return new(true); + } + #endregion + #region Reading private async ValueTask ConvertAsync(ICommandContext context, SearchResult search, object[] args, CancellationToken cancellationToken) { - context.LogDebug("Attempting argument conversion for {}", search.Command); + context.LogDebug("Attempting argument conversion for {0}", search.Command); // skip if no parameters exist. if (!search.Command.HasArguments) @@ -212,43 +230,27 @@ private async ValueTask ConvertAsync(ICommandContext context, S // check if input equals command length. if (search.Command.MaxLength == length) - return await search.Command.Arguments.RecursiveConvertAsync(context, Services, args[length..], 0, cancellationToken); + return await search.Command.Arguments.RecursiveConvertAsync(context, Services, args[^length..], 0, cancellationToken); // check if input is longer than command, but remainder to concatenate. if (search.Command.MaxLength <= length && search.Command.HasRemainder) - return await search.Command.Arguments.RecursiveConvertAsync(context, Services, args[length..], 0, cancellationToken); + return await search.Command.Arguments.RecursiveConvertAsync(context, Services, args[^length..], 0, cancellationToken); // check if input is shorter than command, but optional parameters to replace. if (search.Command.MaxLength > length && search.Command.MinLength <= length) - return await search.Command.Arguments.RecursiveConvertAsync(context, Services, args[length..], 0, cancellationToken); + return await search.Command.Arguments.RecursiveConvertAsync(context, Services, args[^length..], 0, cancellationToken); // input is too long or too short. return []; } #endregion - #region Checking - private async ValueTask CheckAsync(ICommandContext context, CommandInfo command, CancellationToken cancellationToken) - { - context.LogDebug("Attempting validations for {}", command); - - foreach (var precon in command.Preconditions) - { - var result = await precon.EvaluateAsync(context, Services, command, cancellationToken); - - if (!result.Success) - return result; - } - return new(); - } - #endregion - #region Running private async ValueTask RunAsync(ICommandContext context, MatchResult match, CancellationToken cancellationToken) { try { - context.LogInformation("Executing {} with {} resolved arguments.", match.Command, match.Reads.Length); + context.LogInformation("Executing {0} with {1} resolved arguments.", match.Command, match.Reads.Length); var targetInstance = Services.GetService(match.Command.Module.Type); diff --git a/src/CSF.Core/Core/Results/Impl/CheckResult.cs b/src/CSF.Core/Core/Results/Impl/CheckResult.cs index c63fcb0..c24c089 100644 --- a/src/CSF.Core/Core/Results/Impl/CheckResult.cs +++ b/src/CSF.Core/Core/Results/Impl/CheckResult.cs @@ -16,5 +16,10 @@ internal CheckResult(Exception exception) Exception = exception; Success = false; } + + internal CheckResult(bool success) + { + Success = success; + } } } diff --git a/src/CSF.Core/Helpers/ExecutionHelpers.cs b/src/CSF.Core/Helpers/ExecutionHelpers.cs index bc4d801..9d890b9 100644 --- a/src/CSF.Core/Helpers/ExecutionHelpers.cs +++ b/src/CSF.Core/Helpers/ExecutionHelpers.cs @@ -1,5 +1,6 @@ using CSF.Core; using CSF.Reflection; +using System.Reflection.Metadata; namespace CSF.Helpers { @@ -22,7 +23,7 @@ public static IEnumerable RecursiveSearch(this IEnumerable RecursiveConvertAsync(this IArgument[] { static async ValueTask ConvertAsync(IArgument param, ICommandContext context, IServiceProvider services, object arg, CancellationToken cancellationToken) { - if (arg.GetType() == param.Type) + if (param.IsNullable && arg is null or "null" or "nothing") return new(arg); - if (param.IsNullable && arg is null or "null" or "nothing") + if (param.Type == typeof(string) || param.Type == typeof(object)) return new(arg); return await param.Converter.ObjectEvaluateAsync(context, services, param, arg, cancellationToken); diff --git a/src/CSF.Core/Reflection/Impl/CommandInfo.cs b/src/CSF.Core/Reflection/Impl/CommandInfo.cs index c75821d..5514a20 100644 --- a/src/CSF.Core/Reflection/Impl/CommandInfo.cs +++ b/src/CSF.Core/Reflection/Impl/CommandInfo.cs @@ -66,7 +66,7 @@ internal CommandInfo(ModuleInfo module, MethodInfo method, string[] aliases, IDi if (parameters.Any(x => x.Attributes.Contains(false))) { - if (parameters[^1].IsRemainder) + if (parameters.Length > 1 && parameters[^1].IsRemainder) { ThrowHelpers.InvalidOp($"{nameof(RemainderAttribute)} can only exist on the last parameter of a method."); } diff --git a/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs b/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs index 3c8f24c..2a0e930 100644 --- a/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs +++ b/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs @@ -74,7 +74,7 @@ internal ComplexArgumentInfo(ParameterInfo parameterInfo, IDictionary 0) + if (parameters.Length == 0) { ThrowHelpers.InvalidOp("Complex types are expected to have at least 1 constructor parameter."); } diff --git a/src/CSF.Tests.Console/Complex/ComplexType.cs b/src/CSF.Tests.Console/Complex/ComplexType.cs index 42486f4..87576d7 100644 --- a/src/CSF.Tests.Console/Complex/ComplexType.cs +++ b/src/CSF.Tests.Console/Complex/ComplexType.cs @@ -1,6 +1,6 @@ using CSF.Core; -namespace CSF.Tests.Complex +namespace CSF.Tests { public class ComplexType { diff --git a/src/CSF.Tests.Console/Complex/ComplexerType.cs b/src/CSF.Tests.Console/Complex/ComplexerType.cs index fd88feb..12ecb6b 100644 --- a/src/CSF.Tests.Console/Complex/ComplexerType.cs +++ b/src/CSF.Tests.Console/Complex/ComplexerType.cs @@ -1,6 +1,6 @@ using CSF.Core; -namespace CSF.Tests.Complex +namespace CSF.Tests { public class ComplexerType { diff --git a/src/CSF.Tests.Console/LoggingContext.cs b/src/CSF.Tests.Console/LoggingContext.cs new file mode 100644 index 0000000..45e4847 --- /dev/null +++ b/src/CSF.Tests.Console/LoggingContext.cs @@ -0,0 +1,54 @@ +using CSF.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSF.Tests +{ + internal class LoggingContext : CommandContext + { + public override void LogCritical(string message, params object[] args) + { + Console.ForegroundColor = ConsoleColor.DarkRed; + Console.WriteLine("[Critical] " + string.Format(message, args)); + Console.ResetColor(); + } + + public override void LogError(string message, params object[] args) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("[Error] " + string.Format(message, args)); + Console.ResetColor(); + } + + public override void LogWarning(string message, params object[] args) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("[Warn] " + string.Format(message, args)); + Console.ResetColor(); + } + + public override void LogInformation(string message, params object[] args) + { + Console.ForegroundColor = ConsoleColor.Blue; + Console.WriteLine("[Info] " + string.Format(message, args)); + Console.ResetColor(); + } + + public override void LogDebug(string message, params object[] args) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine("[Debug] " + string.Format(message, args)); + Console.ResetColor(); + } + + public override void LogTrace(string message, params object[] args) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine("[Trace] " + string.Format(message, args)); + Console.ResetColor(); + } + } +} diff --git a/src/CSF.Tests.Console/Modules/AsyncModule.cs b/src/CSF.Tests.Console/Modules/AsyncModule.cs index 6ecc9de..f83d013 100644 --- a/src/CSF.Tests.Console/Modules/AsyncModule.cs +++ b/src/CSF.Tests.Console/Modules/AsyncModule.cs @@ -1,6 +1,6 @@ using CSF.Core; -namespace CSF.Tests.Console.Modules +namespace CSF.Tests { public sealed class AsyncModule : ModuleBase { diff --git a/src/CSF.Tests.Console/Modules/Module.cs b/src/CSF.Tests.Console/Modules/Module.cs index 9d45bdb..8a3000d 100644 --- a/src/CSF.Tests.Console/Modules/Module.cs +++ b/src/CSF.Tests.Console/Modules/Module.cs @@ -1,5 +1,4 @@ using CSF.Core; -using CSF.Tests.Complex; namespace CSF.Tests { diff --git a/src/CSF.Tests.Console/Program.cs b/src/CSF.Tests.Console/Program.cs index 2275fa5..4468db3 100644 --- a/src/CSF.Tests.Console/Program.cs +++ b/src/CSF.Tests.Console/Program.cs @@ -1,6 +1,7 @@ using CSF.Core; using CSF.Helpers; using CSF.Parsing; +using CSF.Tests; using Microsoft.Extensions.DependencyInjection; using System.Reflection; @@ -12,11 +13,11 @@ { if (result.Success) { - + await Task.CompletedTask; } else { - + Console.WriteLine(result.Exception); } }); configuration.WithAssemblies(Assembly.GetEntryAssembly()); @@ -32,5 +33,5 @@ { var input = parser.Parse(Console.ReadLine()!); - await framework.TryExecuteAsync(null, input); + await framework.TryExecuteAsync(new LoggingContext(), input); } \ No newline at end of file From 3e72e63c0399f5ca86b92bab18a0fe3a9efcc7d6 Mon Sep 17 00:00:00 2001 From: Armano <68127614+csmir@users.noreply.github.com> Date: Sat, 3 Feb 2024 12:41:57 +0100 Subject: [PATCH 33/40] Update README.md --- README.md | 132 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 120 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index bc28f21..98ae460 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,123 @@ -

- - logo - -

+![csfbanner_lighttrans_outline](https://github.com/csmir/CSF.NET/assets/68127614/7255e535-41b6-431c-87f2-2d9aa18ef6f9) -

- buildstatus - download - help -

+# 🏗️ CSF.NET - Command Standardization Framework for .NET -# ⚒️ CSF.NET - The Command Standardization Framework for .NET +CSF is an attribute based framework that makes creating and processing **text based commands** easy for any platform. It implements a modular, easy to implement pipeline for registering and executing commands, as well as a wide range of customization options to make development on different platforms as easy as possible. -Welcome to the new era of commands! CSF aims to reduce your boilerplate while keeping customizability high. It is intuitive, developer friendly and scalable, all with performance in mind. For more about writing code with this framework, visit the [documentation](https://github.com/csmir/CSF.NET/wiki)! +## 📍 Features + +### 🗨️ Type Conversion: + +`ValueType`, `Enum` and nullable variant types are automatically parsed by the library and populate commands as below: + +```cs +... +[Command("test")] +public void Test(int param1, DateTime param2) +{ + Console.WriteLine("{0}, {1}", param1, param2); +} +... +``` +> This will automatically parse `int` by using the default `int.TryParse` implementation, and will do the same for the `DateTime`. + +Outside of this, implementing and adding your own `TypeConverter`'s is also supported to handle commands with your own type signature. Nullability is automatically resolved by the library. + +> See feature [documentation](https://github.com/csmir/CSF.NET/wiki/Type-Conversion) for more. + +### 🛑 Preconditions: + +Implementing `PreconditionAttribute` creates a new evaluation to add in the set of attributes defined above command definitions. +When a command is attempted to be executed, it will walk through every precondition present and abort execution if any of them fail. + +```cs +... +[MyPrecondition(PlatformID.Unix)] +[Command("copy")] +public async Task Handle() +{ + +} +... +``` + +> See feature [documentation](https://github.com/csmir/CSF.NET/wiki/Preconditions) for more. + +### 💡 Customization: + +`CommandContext` and `ModuleBase` can each be implemented in your own ways, them serving as extensible carriers for command-level data. You can add a number of application-unique properties that are populated at creation. + +```cs +... +class MyContext(User user) : CommandContext +{ + public User CommandUser { get; } = user; +} +... +``` + +> See feature [documentation](https://github.com/csmir/CSF.NET/wiki/Modules-And-Contexts) for more. + +### 💉 Dependency Injection: + +You can provide an `IServiceProvider` at execution to inject modules with dependencies, in accordance to the conventions `Microsoft.Extensions.DependencyInjection` follows. The `IServiceProvider` has a number of extensions that are suggested to be used when writing your codebase with CSF. These extensions serve you and the program, reducing boilerplate in the application setup. + +```cs +... +var services = new ServiceCollection() + .ConfigureCommands(configuration => + { + configuration.WithAssemblies(Assembly.GetEntryAssembly()); + }); +... +``` + +> See feature [documentation](https://github.com/csmir/CSF.NET/wiki/Dependency-Injection) for more. + +### 📖 Informative Results: + +CSF.NET will return results for running commands through a `ResultResolver`. This resolver has a default implementation that can be configured through the `CommandConfiguration` + +```cs +... +var configuration = new CommandConfiguration(); + +configuration.ConfigureResultAction(async (context, result, services) => +{ + if (result.Success) + { + await Task.CompletedTask; + } + else + { + Console.WriteLine(result.Exception); + } +}); +... +``` + + +> See feature [documentation](https://github.com/csmir/CSF.NET/wiki/Handling-Results) for more. + +### 📄 Reflection: + +The framework saves cached command data in its own reflection types. +These types, such as `CommandInfo` `ArgumentInfo` and `ModuleInfo` store informative data about a command, its root module and any submembers. + +The reflection data is accessible in various ways, most commonly in scope during type conversion & precondition evaluation. + +## 🤖 Samples + +Samples are available to learn how to implement CSF in your own programs. + +- [CSF.Samples.Console](https://github.com/Rozen4334/CSF.NET/tree/master/examples/CSF.Samples.Console) + - Shows how to implement CSF on a basic console application. +- [CSF.Samples.Hosting](https://github.com/Rozen4334/CSF.NET/tree/master/examples/CSF.Samples.Console) + - Shows how to implement CSF on a hosted console application. + +## 📰 Extensions + +CSF introduces a number of extensions for external libraries. + +- [CSF.Hosting](https://github.com/Rozen4334/CSF.NET/tree/master/src/CSF.Hosting) + - A package that wraps around [Microsoft.Extensions.Hosting](https://learn.microsoft.com/en-us/dotnet/core/extensions/generic-host?tabs=appbuilder). From 41f50250fd7cb9bff0defdf11f72930caee16830 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Sat, 3 Feb 2024 12:47:19 +0100 Subject: [PATCH 34/40] XML finalization --- src/CSF.Core/Core/CommandManager.cs | 25 +------ src/CSF.Core/Core/Results/ResultResolver.cs | 14 +++- src/CSF.Core/Helpers/ReflectionHelpers.cs | 19 +++++ src/CSF.Core/Helpers/ServiceHelpers.cs | 2 +- src/CSF.Core/TypeConverters/TypeConverter.cs | 2 +- .../Execution/Impl/HostedCommandContext.cs | 18 ++++- .../Core/Execution/Impl/HostedModuleBase.cs | 21 ++++++ src/CSF.Hosting/Helpers/HostHelpers.cs | 50 ++++++++++++++ src/CSF.Hosting/Helpers/ServiceHelpers.cs | 39 ----------- .../Hosting/Execution/IActionFactory.cs | 22 ------ .../Execution/Impl/HostedModuleBase.cs | 15 ---- .../Hosting/HostedCommandManager.cs | 69 ------------------- src/CSF.Tests.Hosting/ActionFactory.cs | 48 ------------- .../CSF.Tests.Hosting.csproj | 2 +- src/CSF.Tests.Hosting/CommandHandler.cs | 53 ++++++++++++++ src/CSF.Tests.Hosting/Module.cs | 21 ------ src/CSF.Tests.Hosting/Modules/Module.cs | 18 +++++ src/CSF.Tests.Hosting/Program.cs | 15 ++-- 18 files changed, 203 insertions(+), 250 deletions(-) rename src/CSF.Hosting/{Hosting => Core}/Execution/Impl/HostedCommandContext.cs (67%) create mode 100644 src/CSF.Hosting/Core/Execution/Impl/HostedModuleBase.cs create mode 100644 src/CSF.Hosting/Helpers/HostHelpers.cs delete mode 100644 src/CSF.Hosting/Helpers/ServiceHelpers.cs delete mode 100644 src/CSF.Hosting/Hosting/Execution/IActionFactory.cs delete mode 100644 src/CSF.Hosting/Hosting/Execution/Impl/HostedModuleBase.cs delete mode 100644 src/CSF.Hosting/Hosting/HostedCommandManager.cs delete mode 100644 src/CSF.Tests.Hosting/ActionFactory.cs create mode 100644 src/CSF.Tests.Hosting/CommandHandler.cs delete mode 100644 src/CSF.Tests.Hosting/Module.cs create mode 100644 src/CSF.Tests.Hosting/Modules/Module.cs diff --git a/src/CSF.Core/Core/CommandManager.cs b/src/CSF.Core/Core/CommandManager.cs index 4d18c6f..cbc6758 100644 --- a/src/CSF.Core/Core/CommandManager.cs +++ b/src/CSF.Core/Core/CommandManager.cs @@ -12,7 +12,7 @@ namespace CSF.Core /// The root type serving as a basis for all operations and functionality as provided by the Command Standardization Framework. ///
/// - /// This API is completely CLS compliant where it is supported, always implementing an overload that is CLS compliant, where it otherwise would not be. + /// To learn more about use of this type and other features of CSF, check out the README on GitHub: /// public class CommandManager { @@ -50,7 +50,7 @@ public CommandManager(IServiceProvider services, CommandConfiguration configurat ThrowHelpers.InvalidArg(nameof(configuration.Assemblies)); } - Commands = BuildComponents(configuration) + Commands = ReflectionHelpers.BuildComponents(configuration) .SelectMany(x => x.Components) .ToHashSet(); @@ -277,27 +277,6 @@ private async ValueTask RunAsync(ICommandContext context, MatchResult } #endregion - #region Building - private IEnumerable BuildComponents(CommandConfiguration configuration) - { - var typeReaders = TypeConverter.CreateDefaultReaders().UnionBy(configuration.Converters, x => x.Type).ToDictionary(x => x.Type, x => x); - - var rootType = typeof(ModuleBase); - foreach (var assembly in configuration.Assemblies) - { - foreach (var type in assembly.GetTypes()) - { - if (rootType.IsAssignableFrom(type) - && !type.IsAbstract - && !type.ContainsGenericParameters) - { - yield return new ModuleInfo(type, typeReaders); - } - } - } - } - #endregion - internal class ServiceProvider : IServiceProvider { private static readonly Lazy _i = new(); diff --git a/src/CSF.Core/Core/Results/ResultResolver.cs b/src/CSF.Core/Core/Results/ResultResolver.cs index 1090ed2..16ee99f 100644 --- a/src/CSF.Core/Core/Results/ResultResolver.cs +++ b/src/CSF.Core/Core/Results/ResultResolver.cs @@ -1,4 +1,6 @@ -namespace CSF.Core +using System.ComponentModel; + +namespace CSF.Core { /// /// A container that implements an asynchronous functor to handle post-execution operations. @@ -13,7 +15,15 @@ public class ResultResolver(Func public Func Handler { get; } = handler; - internal Task TryHandleAsync(ICommandContext context, ICommandResult result, IServiceProvider services) + /// + /// Validates the state of the and attempts to execute the delegate. + /// + /// Context of the current execution. + /// The result of the command, being successful or containing failure information. + /// The provider used to register modules and inject services. + /// An awaitable that waits for the delegate to finish. + [EditorBrowsable(EditorBrowsableState.Never)] + public Task TryHandleAsync(ICommandContext context, ICommandResult result, IServiceProvider services) { if (Handler == null) { diff --git a/src/CSF.Core/Helpers/ReflectionHelpers.cs b/src/CSF.Core/Helpers/ReflectionHelpers.cs index 98e7113..2026bb6 100644 --- a/src/CSF.Core/Helpers/ReflectionHelpers.cs +++ b/src/CSF.Core/Helpers/ReflectionHelpers.cs @@ -8,6 +8,25 @@ namespace CSF.Helpers { internal static class ReflectionHelpers { + public static IEnumerable BuildComponents(CommandConfiguration configuration) + { + var typeReaders = TypeConverter.CreateDefaultReaders().UnionBy(configuration.Converters, x => x.Type).ToDictionary(x => x.Type, x => x); + + var rootType = typeof(ModuleBase); + foreach (var assembly in configuration.Assemblies) + { + foreach (var type in assembly.GetTypes()) + { + if (rootType.IsAssignableFrom(type) + && !type.IsAbstract + && !type.ContainsGenericParameters) + { + yield return new ModuleInfo(type, typeReaders); + } + } + } + } + public static IEnumerable GetModules(ModuleInfo module, IDictionary typeReaders) { foreach (var group in module.Type.GetNestedTypes()) diff --git a/src/CSF.Core/Helpers/ServiceHelpers.cs b/src/CSF.Core/Helpers/ServiceHelpers.cs index b139a03..06516b6 100644 --- a/src/CSF.Core/Helpers/ServiceHelpers.cs +++ b/src/CSF.Core/Helpers/ServiceHelpers.cs @@ -7,7 +7,7 @@ namespace CSF.Helpers { /// - /// A set of helper methods to populate and configure a . + /// A set of helper methods to populate and configure an . /// [EditorBrowsable(EditorBrowsableState.Advanced)] public static class ServiceHelpers diff --git a/src/CSF.Core/TypeConverters/TypeConverter.cs b/src/CSF.Core/TypeConverters/TypeConverter.cs index ee3bb20..e4225bb 100644 --- a/src/CSF.Core/TypeConverters/TypeConverter.cs +++ b/src/CSF.Core/TypeConverters/TypeConverter.cs @@ -45,7 +45,7 @@ public abstract class TypeConverter public abstract Type Type { get; } /// - /// Evaluates the known data about the argument to be converter into, as well as the raw value it should convert into a valid invocation parameter. + /// Evaluates the known data about the argument to be converted into, as well as the raw value it should convert into a valid invocation parameter. /// /// Context of the current execution. /// The provider used to register modules and inject services. diff --git a/src/CSF.Hosting/Hosting/Execution/Impl/HostedCommandContext.cs b/src/CSF.Hosting/Core/Execution/Impl/HostedCommandContext.cs similarity index 67% rename from src/CSF.Hosting/Hosting/Execution/Impl/HostedCommandContext.cs rename to src/CSF.Hosting/Core/Execution/Impl/HostedCommandContext.cs index 90ccc58..b5bab3c 100644 --- a/src/CSF.Hosting/Hosting/Execution/Impl/HostedCommandContext.cs +++ b/src/CSF.Hosting/Core/Execution/Impl/HostedCommandContext.cs @@ -1,38 +1,50 @@ -using CSF.Core; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; -namespace CSF.Hosting +namespace CSF.Core { #pragma warning disable CA2254 // Template should be a static expression + /// + /// An implementation of that is configured to use an to resolve log messages. + /// + /// The logger to use as a resolver for log messages. public class HostedCommandContext(ILogger logger) : CommandContext { + /// + /// Gets the logger used to resolve log messages. + /// public ILogger Logger { get; } = logger; + /// public override void LogTrace(string message, params object[] args) { Logger.Log(logLevel: LogLevel.Trace, message: message, args: args); } + /// public override void LogDebug(string message, params object[] args) { Logger.Log(LogLevel.Debug, message, args); } + /// public override void LogInformation(string message, params object[] args) { Logger.Log(LogLevel.Information, message, args); } + /// public override void LogWarning(string message, params object[] args) { Logger.Log(LogLevel.Warning, message, args); } + /// public override void LogError(string message, params object[] args) { Logger.Log(LogLevel.Error, message, args); } + /// public override void LogCritical(string message, params object[] args) { Logger.Log(LogLevel.Critical, message, args); diff --git a/src/CSF.Hosting/Core/Execution/Impl/HostedModuleBase.cs b/src/CSF.Hosting/Core/Execution/Impl/HostedModuleBase.cs new file mode 100644 index 0000000..2a52c99 --- /dev/null +++ b/src/CSF.Hosting/Core/Execution/Impl/HostedModuleBase.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Logging; + +namespace CSF.Core +{ + /// + /// + /// This implementation introduces support for logging at module-level. + /// + public class HostedModuleBase : ModuleBase + where T : HostedCommandContext + { + /// + /// Gets the logger with which log messages are sent. + /// + public ILogger Logger + { + get + => Context.Logger; + } + } +} diff --git a/src/CSF.Hosting/Helpers/HostHelpers.cs b/src/CSF.Hosting/Helpers/HostHelpers.cs new file mode 100644 index 0000000..cbdfc86 --- /dev/null +++ b/src/CSF.Hosting/Helpers/HostHelpers.cs @@ -0,0 +1,50 @@ +using CSF.Core; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + +namespace CSF.Helpers +{ + /// + /// A set of helper methods to populate and configure an . + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static class HostHelpers + { + /// + /// Configures the to support use of a . + /// + /// + /// A delegate to configure the with the current 's building context. + /// The same for call chaining. + public static IHostBuilder ConfigureCommands(this IHostBuilder builder, [DisallowNull] Action configureDelegate) + { + return builder.ConfigureCommands(configureDelegate); + } + + /// + /// Configures the to support use of as an implementation of . + /// + /// The implementation of a to configure for use. + /// + /// A delegate to configure the with the current 's building context. + /// The same for call chaining. + public static IHostBuilder ConfigureCommands(this IHostBuilder builder, [DisallowNull] Action configureDelegate) + where TManager : CommandManager + { + builder.ConfigureServices((context, services) => + { + var config = new CommandConfiguration(); + configureDelegate(context, config); + + services.AddModules(config); + + services.TryAddSingleton(config); + services.TryAddSingleton(); + }); + + return builder; + } + } +} diff --git a/src/CSF.Hosting/Helpers/ServiceHelpers.cs b/src/CSF.Hosting/Helpers/ServiceHelpers.cs deleted file mode 100644 index bfc386d..0000000 --- a/src/CSF.Hosting/Helpers/ServiceHelpers.cs +++ /dev/null @@ -1,39 +0,0 @@ -using CSF.Core; -using CSF.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Hosting; -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; - -namespace CSF.Helpers -{ - [EditorBrowsable(EditorBrowsableState.Advanced)] - public static class ServiceHelpers - { - public static IHostBuilder ConfigureCommands(this IHostBuilder builder, [DisallowNull] Action configureDelegate) - where TFactory : class, IActionFactory - { - return builder.ConfigureCommands(configureDelegate); - } - - public static IHostBuilder ConfigureCommands(this IHostBuilder builder, [DisallowNull] Action configureDelegate) - where TManager : HostedCommandManager where TFactory : class, IActionFactory - { - builder.ConfigureServices((context, services) => - { - var config = new CommandConfiguration(); - configureDelegate(context, config); - - services.AddModules(config); - - services.TryAddSingleton(config); - services.TryAddSingleton(); - - services.AddScoped(); - }); - - return builder; - } - } -} diff --git a/src/CSF.Hosting/Hosting/Execution/IActionFactory.cs b/src/CSF.Hosting/Hosting/Execution/IActionFactory.cs deleted file mode 100644 index 7d56b7d..0000000 --- a/src/CSF.Hosting/Hosting/Execution/IActionFactory.cs +++ /dev/null @@ -1,22 +0,0 @@ -using CSF.Core; - -namespace CSF.Hosting -{ - public interface IActionFactory : IDisposable - { - public ValueTask CreateContextAsync(CancellationToken cancellationToken); - - public ValueTask CreateArgsAsync(CancellationToken cancellationToken); - } - - public interface IActionFactory : IActionFactory - where T : ICommandContext - { - public new ValueTask CreateContextAsync(CancellationToken cancellationToken); - - async ValueTask IActionFactory.CreateContextAsync(CancellationToken cancellationToken) - { - return await CreateContextAsync(cancellationToken).ConfigureAwait(false); - } - } -} diff --git a/src/CSF.Hosting/Hosting/Execution/Impl/HostedModuleBase.cs b/src/CSF.Hosting/Hosting/Execution/Impl/HostedModuleBase.cs deleted file mode 100644 index ca26279..0000000 --- a/src/CSF.Hosting/Hosting/Execution/Impl/HostedModuleBase.cs +++ /dev/null @@ -1,15 +0,0 @@ -using CSF.Core; -using Microsoft.Extensions.Logging; - -namespace CSF.Hosting -{ - public class HostedModuleBase : ModuleBase - where T : HostedCommandContext - { - public ILogger Logger - { - get - => Context.Logger; - } - } -} diff --git a/src/CSF.Hosting/Hosting/HostedCommandManager.cs b/src/CSF.Hosting/Hosting/HostedCommandManager.cs deleted file mode 100644 index eaa12e0..0000000 --- a/src/CSF.Hosting/Hosting/HostedCommandManager.cs +++ /dev/null @@ -1,69 +0,0 @@ -using CSF.Core; -using CSF.Helpers; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -[assembly: CLSCompliant(true)] - -namespace CSF.Hosting -{ - public class HostedCommandManager : CommandManager, IHostedService - { - public ILogger Logger { get; } - - public IActionFactory ActionFactory { get; } - - public HostedCommandManager(ILogger logger, IActionFactory factory, IServiceProvider services, CommandConfiguration configuration) - : base(services, configuration) - { - ActionFactory = factory; - Logger = logger; - } - - public async Task ExecuteAsync(object[] args, CancellationToken cancellationToken) - { - var context = await ActionFactory.CreateContextAsync(cancellationToken).ConfigureAwait(false); - - await TryExecuteAsync(context, args, cancellationToken); - } - - public Task StartAsync(CancellationToken cancellationToken) - { - _ = RunAsync(cancellationToken); - - return Task.CompletedTask; - } - - internal async Task RunAsync(CancellationToken cancellationToken) - { - try - { - while (cancellationToken.IsCancellationRequested) - { - var args = await ActionFactory.CreateArgsAsync(cancellationToken).ConfigureAwait(false); - - if (args == null) - { - ThrowHelpers.InvalidArg(args); - } - - if (args.Length == 0) - { - ThrowHelpers.InvalidArg(args); - } - - await ExecuteAsync(args, cancellationToken).ConfigureAwait(false); - } - } - catch - { - // WIP - } - } - - public Task StopAsync(CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/CSF.Tests.Hosting/ActionFactory.cs b/src/CSF.Tests.Hosting/ActionFactory.cs deleted file mode 100644 index 0cd88e2..0000000 --- a/src/CSF.Tests.Hosting/ActionFactory.cs +++ /dev/null @@ -1,48 +0,0 @@ -using CSF.Helpers; -using CSF.Hosting; -using CSF.Parsing; -using Microsoft.Extensions.Logging; - -namespace CSF.Tests.Hosting -{ - public class ActionFactory : IActionFactory - { - private readonly ILoggerFactory _loggerFactory; - private readonly StringParser _stringParser; - - public ActionFactory(ILoggerFactory loggerFactory) - { - _loggerFactory = loggerFactory; - _stringParser = new StringParser(); - } - - public ValueTask CreateArgsAsync(CancellationToken cancellationToken) - { - var input = Console.ReadLine(); - - var args = _stringParser.Parse(input); - - if (args.Length == 0) - { - ThrowHelpers.InvalidArg(args); - } - - return ValueTask.FromResult(args); - } - - public ValueTask CreateContextAsync(CancellationToken cancellationToken) - { - var guid = Guid.NewGuid(); - var logger = _loggerFactory.CreateLogger($"CSF.Command.Pipeline[{Guid.NewGuid()}]"); - - logger.LogTrace("Generating context with ID {}", guid); - - return ValueTask.FromResult(new HostedCommandContext(logger)); - } - - public void Dispose() - { - - } - } -} diff --git a/src/CSF.Tests.Hosting/CSF.Tests.Hosting.csproj b/src/CSF.Tests.Hosting/CSF.Tests.Hosting.csproj index c686f43..42230c8 100644 --- a/src/CSF.Tests.Hosting/CSF.Tests.Hosting.csproj +++ b/src/CSF.Tests.Hosting/CSF.Tests.Hosting.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/src/CSF.Tests.Hosting/CommandHandler.cs b/src/CSF.Tests.Hosting/CommandHandler.cs new file mode 100644 index 0000000..3e29f2e --- /dev/null +++ b/src/CSF.Tests.Hosting/CommandHandler.cs @@ -0,0 +1,53 @@ +using CSF.Core; +using CSF.Helpers; +using CSF.Parsing; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace CSF.Tests +{ + public sealed class CommandHandler(CommandManager manager, ILoggerFactory logger) : IHostedService + { + private readonly ILoggerFactory _factory = logger; + private readonly CommandManager _manager = manager; + + private readonly StringParser _parser = new(); + private readonly CancellationTokenSource _cts = new(); + + public Task StartAsync(CancellationToken cancellationToken) + { + _ = RunAsync(_cts.Token); + return Task.CompletedTask; + } + + private async Task RunAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + var input = Console.ReadLine(); + + var args = _parser.Parse(input); + + if (args.Length == 0) + { + ThrowHelpers.InvalidArg(args); + } + + var guid = Guid.NewGuid(); + var logger = _factory.CreateLogger($"CSF.Command.Pipeline[{Guid.NewGuid()}]"); + + logger.LogTrace("Generating context with ID {}", guid); + + var context = new HostedCommandContext(logger); + + await _manager.TryExecuteAsync(context, args, cancellationToken).ConfigureAwait(false); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _cts.Cancel(); + return Task.CompletedTask; + } + } +} diff --git a/src/CSF.Tests.Hosting/Module.cs b/src/CSF.Tests.Hosting/Module.cs deleted file mode 100644 index df0a874..0000000 --- a/src/CSF.Tests.Hosting/Module.cs +++ /dev/null @@ -1,21 +0,0 @@ -using CSF.Core; -using CSF.Hosting; - -namespace CSF.Tests.Hosting -{ - public sealed class Module : HostedModuleBase - { -#pragma warning disable IDE0052 // Remove unread private members - private readonly IServiceProvider _provider; -#pragma warning restore IDE0052 // Remove unread private members - - public Module(IServiceProvider provider) - { - _provider = provider; - } - - [Command("help")] - public void Help() - => Console.WriteLine("Helped"); - } -} diff --git a/src/CSF.Tests.Hosting/Modules/Module.cs b/src/CSF.Tests.Hosting/Modules/Module.cs new file mode 100644 index 0000000..ba4eb6f --- /dev/null +++ b/src/CSF.Tests.Hosting/Modules/Module.cs @@ -0,0 +1,18 @@ +using CSF.Core; +using Microsoft.Extensions.DependencyInjection; + +namespace CSF.Tests +{ + public sealed class Module(IServiceProvider provider) : HostedModuleBase + { + private readonly IServiceProvider _provider = provider; + + [Command("stop")] + public void Stop() + { + var handler = _provider.GetRequiredService(); + + handler.StopAsync(default); + } + } +} diff --git a/src/CSF.Tests.Hosting/Program.cs b/src/CSF.Tests.Hosting/Program.cs index 1f56a29..da819a0 100644 --- a/src/CSF.Tests.Hosting/Program.cs +++ b/src/CSF.Tests.Hosting/Program.cs @@ -1,28 +1,33 @@ using CSF.Core; using CSF.Helpers; -using CSF.Tests.Hosting; +using CSF.Tests; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Reflection; await Host.CreateDefaultBuilder(args) - .ConfigureCommands((context, configuration) => + .ConfigureCommands((context, configuration) => { configuration.AsyncApproach = AsyncApproach.Await; configuration.TryAddAssembly(Assembly.GetEntryAssembly()); - configuration.ConfigureResultAction(async (context, result, services) => + configuration.ConfigureResultAction((context, result, services) => { if (result.Success) { - + return Task.CompletedTask; } else { - + return Task.CompletedTask; } }); }) + .ConfigureServices((context, services) => + { + services.AddHostedService(); + }) .ConfigureLogging(x => { x.AddSimpleConsole(); From 59b3a3c3bcefb643614fe2145031329bfe889a4b Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Sat, 3 Feb 2024 14:23:17 +0100 Subject: [PATCH 35/40] Docs 1/x --- README.md | 93 +++++++++++++++++++++--------------- docs/Home.md | 66 +------------------------- docs/Quick-Guide.md | 112 ++++++++++++++++++++++++++++++-------------- 3 files changed, 133 insertions(+), 138 deletions(-) diff --git a/README.md b/README.md index 98ae460..5614170 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,18 @@ ![csfbanner_lighttrans_outline](https://github.com/csmir/CSF.NET/assets/68127614/7255e535-41b6-431c-87f2-2d9aa18ef6f9) -# 🏗️ CSF.NET - Command Standardization Framework for .NET +# CSF.NET - Command Standardization Framework for .NET CSF is an attribute based framework that makes creating and processing **text based commands** easy for any platform. It implements a modular, easy to implement pipeline for registering and executing commands, as well as a wide range of customization options to make development on different platforms as easy as possible. -## 📍 Features +- [Features](#features) +- [Additional Packages](#additional-packages) +- [Samples](#samples) -### 🗨️ Type Conversion: +## Features -`ValueType`, `Enum` and nullable variant types are automatically parsed by the library and populate commands as below: +#### Type Conversion: + +For raw input, automated conversion to fit command signature is supported by `TypeConverter`'s. `ValueType`, `Enum` and nullable variant types are automatically parsed by the framework and populate commands as below: ```cs ... @@ -19,22 +23,22 @@ public void Test(int param1, DateTime param2) } ... ``` -> This will automatically parse `int` by using the default `int.TryParse` implementation, and will do the same for the `DateTime`. +- This will automatically parse `int` by using the default `int.TryParse` implementation, and will do the same for the `DateTime`. -Outside of this, implementing and adding your own `TypeConverter`'s is also supported to handle commands with your own type signature. Nullability is automatically resolved by the library. +Outside of this, implementing and adding your own `TypeConverter`'s is also supported to handle command signatures with normally unsupported types. > See feature [documentation](https://github.com/csmir/CSF.NET/wiki/Type-Conversion) for more. -### 🛑 Preconditions: +#### Preconditions: Implementing `PreconditionAttribute` creates a new evaluation to add in the set of attributes defined above command definitions. When a command is attempted to be executed, it will walk through every precondition present and abort execution if any of them fail. ```cs ... -[MyPrecondition(PlatformID.Unix)] -[Command("copy")] -public async Task Handle() +[CustomPrecondition] +[Command("test")] +public async Task Test() { } @@ -43,22 +47,7 @@ public async Task Handle() > See feature [documentation](https://github.com/csmir/CSF.NET/wiki/Preconditions) for more. -### 💡 Customization: - -`CommandContext` and `ModuleBase` can each be implemented in your own ways, them serving as extensible carriers for command-level data. You can add a number of application-unique properties that are populated at creation. - -```cs -... -class MyContext(User user) : CommandContext -{ - public User CommandUser { get; } = user; -} -... -``` - -> See feature [documentation](https://github.com/csmir/CSF.NET/wiki/Modules-And-Contexts) for more. - -### 💉 Dependency Injection: +#### Dependency Injection: You can provide an `IServiceProvider` at execution to inject modules with dependencies, in accordance to the conventions `Microsoft.Extensions.DependencyInjection` follows. The `IServiceProvider` has a number of extensions that are suggested to be used when writing your codebase with CSF. These extensions serve you and the program, reducing boilerplate in the application setup. @@ -74,14 +63,12 @@ var services = new ServiceCollection() > See feature [documentation](https://github.com/csmir/CSF.NET/wiki/Dependency-Injection) for more. -### 📖 Informative Results: +#### Informative Results: CSF.NET will return results for running commands through a `ResultResolver`. This resolver has a default implementation that can be configured through the `CommandConfiguration` ```cs ... -var configuration = new CommandConfiguration(); - configuration.ConfigureResultAction(async (context, result, services) => { if (result.Success) @@ -99,14 +86,50 @@ configuration.ConfigureResultAction(async (context, result, services) => > See feature [documentation](https://github.com/csmir/CSF.NET/wiki/Handling-Results) for more. -### 📄 Reflection: +#### Customization: + +While already fully functional out of the box, the framework does not shy away from covering extensive applications with more specific needs, which in turn need more than the base features to function according to its developer's expectations. + +Types such as `CommandContext`, `ModuleBase`, `TypeConverter`, `PreconditionAttribute` and `Parser` can all be inherited and custom ones created for environmental specifics, custom type conversion and more. + +#### Reflection: The framework saves cached command data in its own reflection types. -These types, such as `CommandInfo` `ArgumentInfo` and `ModuleInfo` store informative data about a command, its root module and any submembers. +These types, such as `CommandInfo`, `ArgumentInfo` and `ModuleInfo` store informative data about a command, its root module and any submembers. The reflection data is accessible in various ways, most commonly in scope during type conversion & precondition evaluation. -## 🤖 Samples +## Additional Packages + +CSF is not without its own dependencies, but it tries its best to keep all dependencies within a trusted atmosphere, using packages only when they outweigh self-written implementations. So far, it only depends on packages published by official channels of the .NET ecosystem. + +#### Dependency Injection + +Having grown into a vital part of building effective and modern applications, Dependency Injection (DI) is no less important to be carried along in the equally modern CSF. +It integrates this feature deeply into its architecture and depends on it to function from the ground up. + +For applications to function with `CSF.Core`, it is necessary to install DI functionality through Microsoft's publicized package(s): + +```xml + +``` +> This and other required packages can also be installed through the Package Manager, .NET CLI or from within your IDE. + +#### Hosting + +Carrying out further support within the .NET ecosystem, CSF also introduces a hosting package for deploying apps with the .NET generic host. + +For applications to function with `CSF.Hosting`, it is necessary to install the hosting package that it also implements, also publicized by Microsoft itself: + +```xml + +``` + +> The hosting extensions package publicized by Microsoft implements the packages necessary for the core component of CSF, and does not expect to have its dependencies implemented alongside it. + +*For each of these packages, the minimum version is determined by CSF itself, usually being the latest or equal to the target framework upon which it was released. It is suggested to choose the latest version at time of installation.* + +## Samples Samples are available to learn how to implement CSF in your own programs. @@ -115,9 +138,3 @@ Samples are available to learn how to implement CSF in your own programs. - [CSF.Samples.Hosting](https://github.com/Rozen4334/CSF.NET/tree/master/examples/CSF.Samples.Console) - Shows how to implement CSF on a hosted console application. -## 📰 Extensions - -CSF introduces a number of extensions for external libraries. - -- [CSF.Hosting](https://github.com/Rozen4334/CSF.NET/tree/master/src/CSF.Hosting) - - A package that wraps around [Microsoft.Extensions.Hosting](https://learn.microsoft.com/en-us/dotnet/core/extensions/generic-host?tabs=appbuilder). diff --git a/docs/Home.md b/docs/Home.md index 8f72b50..ef64ddc 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -3,68 +3,4 @@ CSF's main focus is for its users to enjoy writing code with the library and master it with relative ease. For this claim to have any value, a great deal is done to help a developer write intuitive and boilerplate-less code, alongside keeping documentation simple and complete. -- Get started with CSF quickly by checking out the **[[Quick Guide|⚡-Quick-Guide]]**. - -### 💉 Additional Packages - -CSF is not without its own dependencies, but it tries its best to keep all dependencies within a trusted atmosphere, using packages only when they outweigh self-written implementations. So far, it only depends on packages published by official channels of the .NET ecosystem. - -#### Dependency Injection - -Having grown into a vital part of building effective and modern applications, Dependency Injection (DI) is no less important to be carried along in the equally modern CSF. -It integrates this feature deeply into its architecture and depends on it to function from the ground up. - -For applications to function with `CSF.Core` or `CSF.Spectre`, it is necessary to install DI functionality through Microsoft's publicized package(s): - -```xml - -``` -> This and other required packages can also be installed through the Package Manager, .NET CLI or from within your IDE. - -#### Hosting - -Carrying out further support within the .NET ecosystem, CSF also introduces a hosting package for deploying apps with the .NET generic host. - -For applications to function with `CSF.Hosting`, it is necessary to install the hosting package that it also implements, also publicized by Microsoft itself: - -```xml - -``` - -> The hosting extensions package publicized by Microsoft implements the packages necessary for the core component of CSF, and does not expect to have its dependencies implemented alongside it. - -*For each of these packages, the minimum version is determined by CSF itself, usually being the latest or equal to the target framework upon which it was released. It is suggested to choose the latest version at time of installing.* - -### ⚒️ Customizability - -While already fully functional out of the box, this command framework does not shy away from covering extensive applications with more specific needs, which in turn need more than the base features to function according to its developer's expectations. Within CSF, the `CommandManager` can be inherited, opening up a range of protected virtual methods that are applicable during pipeline execution. These can be overridden to modify and customize the pipeline to accompany various needs. - -Alongside this, `CommandContext`'s, `TypeReader`'s, `PreconditionAttribute`'s and `Parser`'s can all be inherited and custom ones created for environmental specifics, custom type interpretation and more. - -### ⚡ Performance - -While CSF's primary focus is to be the most versatile and easy to use command framework, it also aims to carry high performance throughout the entire execution flow, while keeping source code readable and comprehensible for library contributors and users alike. -Benchmarks are ran for every version of CSF, with a near complete guarantee that it will not reach higher values upon newly released versions. - -> The only cases where optimization will be cast aside is when the developer experience outweighs the need for further optimization. -> This can be by reason of introducing certain features in the execution pipeline, or by newly introduced steps within said pipeline. In any other cases, be it minor updates or revisions, there will be no change in execution performance. - -Benchmarks of the most recent stable release (`CSF 2.0`) show the following results: - -| Method | Mean | Error | StdDev | Gen0 | Allocated | -|---------------------- |---------:|---------:|--------:|-------:|----------:| -| Parameterless | 701.7 ns | 8.99 ns | 8.41 ns | 0.1745 | 1.07 KB | -| Parametered | 884.9 ns | 10.56 ns | 9.36 ns | 0.2060 | 1.27 KB | -| ParameteredUnprovided | 898.5 ns | 10.40 ns | 9.22 ns | 0.2069 | 1.27 KB | -| NestedParameterless | 693.6 ns | 7.41 ns | 6.94 ns | 0.1745 | 1.07 KB | -| NestedParametered | 895.2 ns | 5.68 ns | 5.03 ns | 0.2060 | 1.27 KB | - -*These benchmarks represent the command execution pipeline from the moment when a parsed `CommandContext` is provided to the framework to be executed, to post execution handling.* - -#### Legend: -- Mean : Arithmetic mean of all measurements. -- Error : Half of 99.9% confidence interval. -- StdDev : Standard deviation of all measurements. -- Gen0 : GC Generation 0 collects per 1000 operations. -- Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B). -- 1 ns : 1 Nanosecond (0.000000001 sec). +- Get started with CSF quickly by checking out the **[[Quick Guide|Quick-Guide]]**. diff --git a/docs/Quick-Guide.md b/docs/Quick-Guide.md index 50482f5..8727c07 100644 --- a/docs/Quick-Guide.md +++ b/docs/Quick-Guide.md @@ -1,11 +1,11 @@ This guide introduces you to CSF, covering the basics for developers ranging from beginner to veteran. -- [Before Coding](#-setting-up-the-basics) -- [Defining Commands](#-defining-commands) -- [Running your Commands](#-running-your-commands) -- [Putting it Together](#-summary) +- [Before Coding](#before-coding) +- [Defining Commands](#defining-commands) +- [Running your Commands](#running-your-commands) +- [Putting it Together](#summary) -## 📩 Before Coding +## Before Coding Before we can start writing code, there are a few things to set up. This includes getting a fresh project to start, and installing CSF. @@ -15,7 +15,7 @@ Before being able to write code, we need to give ourselves a place to do it. With your favourite code editor, we will create a new empty project. This project can be anything, but in our case, a **Console Application** will be the focus. If you are using `dotnet-cli`, you can create such a project by executing `dotnet new console`. -Keep in mind that the project has to target `.net6` or higher. +Keep in mind that the project has to target `.net8` or higher. > In a visual editor such as Rider or Visual studio, you can create it by simply opening the application, navigating to 'Create New Project' and selecting the Console Application template. @@ -26,7 +26,7 @@ This can be in the PM console, in your `.csproj` file or through the code editor We can grab the latest version from [NuGet](https://www.nuget.org/packages/CSF.NET). If you prefer to use the visual tools, simply look up `csf.net` and install the first package available. -## 📝 Defining Commands +## Defining Commands ### Creating a Module @@ -122,13 +122,13 @@ namespace XProject [Command("helloworld")] public void HelloWorld() { - Respond("Hello world!"); + Console.WriteLine("Hello world!"); } } } ``` -> To actually start testing and running the command, skip to [# +> To actually start testing and running the command, skip to [here](#-running-your-commands). #### Command 2: Reply @@ -160,7 +160,7 @@ As you can see here, the reply method accepts a parameter called `message`, and In our case however, the command is marked with `[Remainder]`. This attribute has a special property, it ignores all parameters provided after it. Instead, it connects all of them together and makes it one long string (or object). This way you don't need to add quotation marks, making the command more intuitive to use. -## 🏗️ Running your Commands +## Running your Commands CSF is quite simple in that it does a very good job in doing things *for* you. Because of this, setting up your commands is not actually directly visible. But, there is one very important step, and for it, we need to look at a more advanced concept in programming. Dependency Injection! @@ -177,44 +177,75 @@ It may look different for everyone, but for simplicity sake, we will only focus Let's start with the basics, and define a new `ServiceCollection`. This collection works like a list, and things can be added to it. -CSF already has the needed methods to configure everything at once, so all we have to do is call them: +CSF already has the needed methods to configure everything, so all we have to do is call and use them: ```cs using CSF; using Microsoft.Extensions.DependencyInjection; -var collection = new ServiceCollection(); +var collection = new ServiceCollection() + .ConfigureCommands(configuration => + { + + }); +``` -collection.WithCommandManager(); +The `ConfigureCommands` method exposes a configuration delegate for us to use. +In order to handle the result of commands, we will make sure results are handled the way we want them to: -var services = collection.BuildServiceProvider(); +```cs + ... + configuration.ConfigureResultAction((context, result, services) => + { + if (!result.Success) + { + Console.WriteLine(result.Exception); + } + + return Task.CompletedTask; + }); + ... ``` -The `AddCommandManager` method configures everything we need, and leaves very little for us to do. With that out of the way, we can build the collection into a provider. To do this, we call the `BuildServiceProvider` method. +```cs +... +var services = collection.BuildServiceProvider(); +... +``` + Now that everything in the background has been built, the command manager has also been made accessible to us. While staying in `Program.cs`, we can grab the manager from the new `services` variable by calling `GetRequiredService`. ```cs +... var framework = services.GetRequiredService(); +... ``` Hovering over the `CommandManager` declaration, you can read some things about it. It is the root type, or class, that makes CSF work as a whole. You can greatly customize it and make a lot of changes to your personal taste. -Though, we only need very little from it. +Though, we only need very little from it. ### Listening to Input The most important part of running a command is defining *what* command you want to run. -In our case, we want to test the `helloworld` command, so we can define that as our input. -You can replace the way to get your input with anything you like, ranging from `Console.ReadLine` to listening to a discord message. +In our case, we want to test commands with console input, so we will use `Console.ReadLine` to receive inputs. +You can replace the way to get your input with anything you like, ranging from reading a game chat to listening for a discord message. ```cs -var input = "helloworld"; +... +var parser = new StringParser(); + +while (true) +{ + var input = parser.Parse(Console.ReadLine()); -var context = new CommandContext(input); + var context = new CommandContext(); +} +... ``` With this input, we can create the most important component to running a command: The context. @@ -223,22 +254,21 @@ For regular string commands, `CommandContext` is predefined by CSF for you to us ### Running the Command -With the context defined, it is time to run the command. -By calling `ExecuteAsync`, CSF will read your command input and try to find the most appropriate command to run. +With the context defined, it is time t`o run the command. +By calling `TryExecute` in the `while` loop, CSF will read your command input and try to find the most appropriate command to run. In our case, it is an exact match to our `helloworld` command, so it will succeed the search and run it. ```cs -var result = await framework.ExecuteAsync(context); - -if (result.Failed()) - throw result.Exception; + ... + framework.TryExecute(context, input); + ... ``` The final step is to handle the result of the command. -If the command failed in any way, the `CommandResult` returned by the execution will cover where it went wrong and contain the exception that occurred. +If the command failed in any way, the `ICommandResult` returned by the execution will cover where it went wrong and contain the exception that occurred. Here, it is simplest to see if it failed, and then throw the exception if it did. -## 📑 Summary +## Summary Let's review what we achieved in this guide. First of all, we created an empty project and installed the package. @@ -283,20 +313,32 @@ using CSF; using Microsoft.Extensions.DependencyInjection; var collection = new ServiceCollection() - .AddCommandManager(); + .ConfigureCommands(configuration => + { + configuration.ConfigureResultAction((context, result, services) => + { + if (!result.Success) + { + Console.WriteLine(result.Exception); + } + + return Task.CompletedTask; + }); + }); var services = collection.BuildServiceProvider(); var framework = services.GetRequiredService(); +var parser = new StringParser(); -var input = "helloworld"; - -var context = new CommandContext(input); +while (true) +{ + var input = parser.Parse(Console.ReadLine()); -var result = await framework.ExecuteAsync(context); + var context = new CommandContext(); -if (result.Failed()) - throw result.Exception; + framework.TryExecute(context, input); +} ``` If these files match yours, you're good to go. From e3cfda014c71d20513fdf4212ad404c8812bea29 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Sun, 4 Feb 2024 13:27:30 +0100 Subject: [PATCH 36/40] Huge work on docs, sample continuity --- README.md | 30 ++++-- docs/Command-Context.md | 1 - docs/Customization.md | 0 docs/Home.md | 20 +++- docs/Preconditions.md | 47 ++++---- docs/Quick-Guide.md | 63 +++++------ docs/Type-Conversion.md | 100 ++++++++++++++++++ docs/Typereaders.md | 55 ---------- docs/_Footer.md | 10 -- .../CSF.Samples.Console/Modules/CopyModule.cs | 6 +- .../CSF.Samples.Console/Modules/XModule.cs | 17 +-- .../RequireOperationSystemAttribute.cs | 23 ++-- examples/CSF.Samples.Console/Program.cs | 35 ++++-- .../TypeConverters/ReflectionTypeConverter.cs | 35 ++++++ .../TypeReaders/GuidTypeReader.cs | 14 --- .../Core/Attributes/CommandAttribute.cs | 4 +- .../Core/Attributes/DescriptionAttribute.cs | 2 +- .../Core/Attributes/GroupAttribute.cs | 4 +- src/CSF.Core/Core/CommandConfiguration.cs | 20 ++-- src/CSF.Core/Core/CommandManager.cs | 2 +- .../Core/Execution/Impl/CommandContext.cs | 2 +- src/CSF.Core/Helpers/ThrowHelpers.cs | 4 +- .../Preconditions/PreconditionAttribute.cs | 4 +- src/CSF.Core/Reflection/Impl/CommandInfo.cs | 2 +- .../Reflection/Impl/ComplexArgumentInfo.cs | 2 +- src/CSF.Core/TypeConverters/TypeConverter.cs | 24 +++-- src/CSF.Tests.Hosting/CommandHandler.cs | 2 +- 27 files changed, 315 insertions(+), 213 deletions(-) delete mode 100644 docs/Command-Context.md create mode 100644 docs/Customization.md create mode 100644 docs/Type-Conversion.md delete mode 100644 docs/Typereaders.md delete mode 100644 docs/_Footer.md create mode 100644 examples/CSF.Samples.Console/TypeConverters/ReflectionTypeConverter.cs delete mode 100644 examples/CSF.Samples.Console/TypeReaders/GuidTypeReader.cs diff --git a/README.md b/README.md index 5614170..1664413 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ CSF is an attribute based framework that makes creating and processing **text ba - [Features](#features) - [Additional Packages](#additional-packages) -- [Samples](#samples) +- [Getting Started](#getting-started) ## Features -#### Type Conversion: +#### Type Conversion For raw input, automated conversion to fit command signature is supported by `TypeConverter`'s. `ValueType`, `Enum` and nullable variant types are automatically parsed by the framework and populate commands as below: @@ -29,7 +29,7 @@ Outside of this, implementing and adding your own `TypeConverter`'s is also supp > See feature [documentation](https://github.com/csmir/CSF.NET/wiki/Type-Conversion) for more. -#### Preconditions: +#### Preconditions Implementing `PreconditionAttribute` creates a new evaluation to add in the set of attributes defined above command definitions. When a command is attempted to be executed, it will walk through every precondition present and abort execution if any of them fail. @@ -47,7 +47,7 @@ public async Task Test() > See feature [documentation](https://github.com/csmir/CSF.NET/wiki/Preconditions) for more. -#### Dependency Injection: +#### Dependency Injection You can provide an `IServiceProvider` at execution to inject modules with dependencies, in accordance to the conventions `Microsoft.Extensions.DependencyInjection` follows. The `IServiceProvider` has a number of extensions that are suggested to be used when writing your codebase with CSF. These extensions serve you and the program, reducing boilerplate in the application setup. @@ -63,7 +63,7 @@ var services = new ServiceCollection() > See feature [documentation](https://github.com/csmir/CSF.NET/wiki/Dependency-Injection) for more. -#### Informative Results: +#### Informative Results CSF.NET will return results for running commands through a `ResultResolver`. This resolver has a default implementation that can be configured through the `CommandConfiguration` @@ -86,13 +86,13 @@ configuration.ConfigureResultAction(async (context, result, services) => > See feature [documentation](https://github.com/csmir/CSF.NET/wiki/Handling-Results) for more. -#### Customization: +#### Customization While already fully functional out of the box, the framework does not shy away from covering extensive applications with more specific needs, which in turn need more than the base features to function according to its developer's expectations. Types such as `CommandContext`, `ModuleBase`, `TypeConverter`, `PreconditionAttribute` and `Parser` can all be inherited and custom ones created for environmental specifics, custom type conversion and more. -#### Reflection: +#### Reflection The framework saves cached command data in its own reflection types. These types, such as `CommandInfo`, `ArgumentInfo` and `ModuleInfo` store informative data about a command, its root module and any submembers. @@ -129,12 +129,20 @@ For applications to function with `CSF.Hosting`, it is necessary to install the *For each of these packages, the minimum version is determined by CSF itself, usually being the latest or equal to the target framework upon which it was released. It is suggested to choose the latest version at time of installation.* -## Samples +## Getting Started + +There are various resources available in order to get started with CSF. Below, you can find samples and directions to the quick guide. + +#### Quick Guide + +You can find the quick guide [here](https://github.com/csmir/CSF.NET/wiki/Quick-Guide). +This guide introduces you to the basics of defining modules, commands, and how to run them. + +#### Samples Samples are available to learn how to implement CSF in your own programs. -- [CSF.Samples.Console](https://github.com/Rozen4334/CSF.NET/tree/master/examples/CSF.Samples.Console) +- [CSF.Samples.Console](https://github.com/csmir/CSF.NET/tree/master/examples/CSF.Samples.Console) - Shows how to implement CSF on a basic console application. -- [CSF.Samples.Hosting](https://github.com/Rozen4334/CSF.NET/tree/master/examples/CSF.Samples.Console) +- [CSF.Samples.Hosting](https://github.com/csmir/CSF.NET/tree/master/examples/CSF.Samples.Console) - Shows how to implement CSF on a hosted console application. - diff --git a/docs/Command-Context.md b/docs/Command-Context.md deleted file mode 100644 index 63e8645..0000000 --- a/docs/Command-Context.md +++ /dev/null @@ -1 +0,0 @@ -Customizing the Command Context \ No newline at end of file diff --git a/docs/Customization.md b/docs/Customization.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/Home.md b/docs/Home.md index ef64ddc..2e02fbf 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -1,6 +1,20 @@ -## Welcome to the CSF.NET wiki! - CSF's main focus is for its users to enjoy writing code with the library and master it with relative ease. For this claim to have any value, a great deal is done to help a developer write intuitive and boilerplate-less code, alongside keeping documentation simple and complete. -- Get started with CSF quickly by checking out the **[[Quick Guide|Quick-Guide]]**. +## Available Content: + +#### [[Quick Guide|Quick-Guide]] + +A quick start to CSF and various practices. + +#### [[Using Preconditions|Preconditions]] + +Define your own pre-execution checks to ensure commands will only run when they are allowed to run. + +#### [[Type Conversion|Type-Conversion]] + +Create custom conversion for changing raw input into types. + +#### [[Customization|Customization]] + +Customize your modules and contexts to scale up your applications. \ No newline at end of file diff --git a/docs/Preconditions.md b/docs/Preconditions.md index 58cf628..b274b85 100644 --- a/docs/Preconditions.md +++ b/docs/Preconditions.md @@ -1,22 +1,24 @@ Preconditions are checks that ensure that the command in scope is allowed to be executed. Let's work on an example precondition to understand how they work. -- [Creating your Precondition](#-creating-your-precondition) -- [Using your Precondition](#-using-your-precondition) +- [Creating your Precondition](#creating-your-precondition) +- [Using your Precondition](#using-your-precondition) -## 🏗️ Creating your Precondition +## Creating your Precondition All preconditions inherit `PreconditionAttribute`, which in turn inherits `Attribute`. To start creating your own precondition, you have to inherit `PreconditionAttribute` on a class: ```cs -using CSF; +using CSF.Core; +using CSF.Reflection; namespace XProject { public class RequireOperatingSystemAttribute : PreconditionAttribute { - public override Result Evaluate(ICommandContext context, Command command, IServiceProvider provider) + public override ValueTask EvaluateAsync(ICommandContext context, IServiceProvider services, CommandInfo command, CancellationToken cancellationToken) { + } } } @@ -27,42 +29,40 @@ With this class defined, and the method that will operate the evaluation being i First of all, the restriction must be compared against. To define this, we will implement a constructor parameter, automatically resolved as an attribute parameter by the IDE or code editor: ```cs -using CSF; +using CSF.Core; +using CSF.Reflection; namespace XProject { - public class RequireOperatingSystemAttribute : PreconditionAttribute + public class RequireOperatingSystemAttribute(PlatformID platform) : PreconditionAttribute { + public PlatformID Platform { get; } = platform; - public PlatformID Platform { get; } - - public RequireOperatingSystemAttribute(PlatformID platform) + public override ValueTask EvaluateAsync(ICommandContext context, IServiceProvider services, CommandInfo command, CancellationToken cancellationToken) { - Platform = platform; - } - public override Result Evaluate(ICommandContext context, Command command, IServiceProvider provider) - { } } } ``` -After this has been defined, the `Platform` property can be used to evaluate the current operating system against. Our focus goes to the `Evaluate` method, which will run this check. +After this has been defined, the `Platform` property can be used to evaluate the current operating system against. Our focus goes to the `EvaluateAsync` method, which will run this check. ```cs - public override Result Evaluate(ICommandContext context, Command command, IServiceProvider provider) +... + public override ValueTask EvaluateAsync(ICommandContext context, IServiceProvider services, CommandInfo command, CancellationToken cancellationToken) { if (Environment.OSVersion.Platform == Platform) - return Success(); + return ValueTask.FromResult(Success()); - return Failure("The platform this command was executed does not support this operation."); + return ValueTask.FromResult(Error("The platform does not support this operation.")); } +... ``` That's it. With this done, we can look towards the application of our created precondition. -## 📝 Using your Precondition +## Using your Precondition After you have written your precondition, it is time to use it. Let's define a command that depends on the operating system to run. @@ -83,12 +83,12 @@ public void Copy([Remainder] string toCopy) clipboardExecutable.StandardInput.Write(toCopy); clipboardExecutable.StandardInput.Close(); - Respond("Succesfully copied the content to your clipboard."); + Console.Writeline("Succesfully copied the content to your clipboard."); } ``` -This method will use the windows clip executable to copy a string onto your clipboard. -This method does not work on macOS or Linux, so we have to make sure the command is executed on windows. +This method will use the Windows clip executable to copy a string onto your clipboard. +Though, this clipboard approach does not work on MacOS or Linux, so we have to make sure the command is executed on windows. To do this, all we have to do is add `[RequireOperatingSystem(PlatformID.Win32NT)]` above the method, like so: @@ -96,6 +96,7 @@ To do this, all we have to do is add `[RequireOperatingSystem(PlatformID.Win32NT [Command("copy")] [RequireOperatingSystem(PlatformID.Win32NT)] public void Copy([Remainder] string toCopy) +... ``` -The precondition is now defined on this command, and will be called when this command is triggered. \ No newline at end of file +The precondition is now defined on this command, and will be called when this command is triggered. If the platform you run it on is indeed not Windows, it will fail. \ No newline at end of file diff --git a/docs/Quick-Guide.md b/docs/Quick-Guide.md index 8727c07..0b03ddc 100644 --- a/docs/Quick-Guide.md +++ b/docs/Quick-Guide.md @@ -42,9 +42,9 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace XProject +namespace CSF.Samples { - internal class XModule + internal class ExampleModule { } } @@ -55,11 +55,11 @@ To do that, we need to include CSF into the usings, and change the signature of With both of those steps out of the way, it should now look like this: ```cs -using CSF; +using CSF.Core; -namespace XProject +namespace CSF.Samples { - public class XModule : ModuleBase + public class ExampleModule : ModuleBase { } } @@ -80,11 +80,11 @@ Within the class declaration of the file, we will write a new method. The method can return `Task` or `void`, but for the sake of simplicity, we will use `void`. ```cs -using CSF; +using CSF.Core; -namespace XProject +namespace CSF.Samples { - public class XModule : ModuleBase + public class ExampleModule : ModuleBase { public void HelloWorld() { @@ -96,11 +96,11 @@ namespace XProject Next up, we will want to decorate the method with an attribute. The `[Command]` attribute accepts a name, and registers the command by it, so we can use that name to run it from our input source. We will call the command `helloworld`. Let's add the attribute and see how that looks: ```cs -using CSF; +using CSF.Core; -namespace XProject +namespace CSF.Samples { - public class XModule : ModuleBase + public class ExampleModule : ModuleBase { [Command("helloworld")] public void HelloWorld() @@ -113,11 +113,11 @@ namespace XProject Now the `HelloWorld` method will run when we run the `helloworld` command, but there is still one thing missing. The method is empty! You can add anything here, but in our case, a simple reply will do: ```cs -using CSF; +using CSF.Core; -namespace XProject +namespace CSF.Samples { - public class XModule : ModuleBase + public class ExampleModule : ModuleBase { [Command("helloworld")] public void HelloWorld() @@ -135,11 +135,11 @@ namespace XProject Our next command will be quite simple as well. Just like before, we can create a new method, mark it with an attribute and give it a name. Except this time, it will accept input, and respond with it. Let's create this functionality: ```cs -using CSF; +using CSF.Core; -namespace XProject +namespace CSF.Samples { - public class XModule : ModuleBase + public class ExampleModule : ModuleBase { [Command("helloworld")] public void HelloWorld() @@ -180,7 +180,11 @@ This collection works like a list, and things can be added to it. CSF already has the needed methods to configure everything, so all we have to do is call and use them: ```cs -using CSF; +using CSF.Core; +using CSF.Helpers; +using CSF.Parsing; +using CSF.Samples; + using Microsoft.Extensions.DependencyInjection; var collection = new ServiceCollection() @@ -190,8 +194,10 @@ var collection = new ServiceCollection() }); ``` -The `ConfigureCommands` method exposes a configuration delegate for us to use. -In order to handle the result of commands, we will make sure results are handled the way we want them to: +The `ConfigureCommands` method exposes a configuration delegate for us to use. With this delegate, we are free to configure some of the essential options to receive and bring in information about registration and execution of commands. + +If the command failed in any way, an `ICommandResult` brought forward by the execution will cover where it went wrong and contain the exception that occurred. +Here, it is simplest to see if it failed, and then throw the exception if it did. For this to be handled the way we expect it to, we need to configure it: ```cs ... @@ -227,7 +233,7 @@ var framework = services.GetRequiredService(); Hovering over the `CommandManager` declaration, you can read some things about it. It is the root type, or class, that makes CSF work as a whole. You can greatly customize it and make a lot of changes to your personal taste. -Though, we only need very little from it. +Though, we only need very little from it for this guide. ### Listening to Input @@ -250,13 +256,13 @@ while (true) With this input, we can create the most important component to running a command: The context. Execution accepts `ICommandContext` and will use that to parse and run the command. -For regular string commands, `CommandContext` is predefined by CSF for you to use. +For regular console commands, `CommandContext` is predefined by CSF for you to use. ### Running the Command -With the context defined, it is time t`o run the command. +With the context defined, it is time to run the command. By calling `TryExecute` in the `while` loop, CSF will read your command input and try to find the most appropriate command to run. -In our case, it is an exact match to our `helloworld` command, so it will succeed the search and run it. +In our case, it is an exact match to our `helloworld` command, so querying this after starting the program will succeed the search and run it. ```cs ... @@ -264,10 +270,6 @@ In our case, it is an exact match to our `helloworld` command, so it will succee ... ``` -The final step is to handle the result of the command. -If the command failed in any way, the `ICommandResult` returned by the execution will cover where it went wrong and contain the exception that occurred. -Here, it is simplest to see if it failed, and then throw the exception if it did. - ## Summary Let's review what we achieved in this guide. @@ -309,7 +311,9 @@ namespace XProject ```cs #Program.cs -using CSF; +using CSF.Core; +using CSF.Helpers; +using CSF.Parsing; using Microsoft.Extensions.DependencyInjection; var collection = new ServiceCollection() @@ -321,7 +325,6 @@ var collection = new ServiceCollection() { Console.WriteLine(result.Exception); } - return Task.CompletedTask; }); }); diff --git a/docs/Type-Conversion.md b/docs/Type-Conversion.md new file mode 100644 index 0000000..868e01f --- /dev/null +++ b/docs/Type-Conversion.md @@ -0,0 +1,100 @@ +A `TypeConverter` reads provided argument input in string format and try to convert them into the types as defined in the command signature. +Let's work on an example to learn how they work. + +- [Creating your TypeConverter](#creating-your-typeconverter) +- [Using your TypeConverter](#using-your-typeconverter) + +## Creating your TypeConverter + +All TypeConverter inherit `TypeConverter` or `TypeConverter`. To start creating your TypeConverter, you have to inherit one of the two on a class. + +> For the simplicity of this documentation, only the generic type is introduced here. + +```cs +using CSF.Core; +using CSF.Reflection; +using CSF.TypeConverters; + +namespace CSF.Samples +{ + public class ReflectionTypeConverter : TypeConverter + { + public override async ValueTask EvaluateAsync(ICommandContext context, IServiceProvider services, IArgument argument, string raw, CancellationToken cancellationToken) + { + } + } +} +``` + +With this class defined and the method that will operate the evaluation being implemented, we can now write our code which defines the succession and failure conditions. In case of success, we also need to pass in the parsed object that the typereader expects to see returned. + +```cs + ... + public override async ValueTask EvaluateAsync(ICommandContext context, IServiceProvider services, IArgument argument, string raw, CancellationToken cancellationToken) + { + try + { + var typeSrc = Type.GetType( + typeName: raw, + throwOnError: true, + ignoreCase: true); + + return ValueTask.FromResult(Success(typeSrc)); + } + catch (Exception ex) + { + return ValueTask.FromResult(Error(ex)); + } + } + ... +``` + +With the logic defined, we can also add options in the converter, for example by customizing how `ignoreCase` is configured in the `Type` search: + +```cs +... + public class ReflectionTypeConverter(bool caseIgnore) : TypeConverter + { + private readonly bool _caseIgnore = caseIgnore; + + ... + } +... +``` + +```cs + + ... + var typeSrc = Type.GetType( + typeName: raw, + throwOnError: true, + ignoreCase: _caseIgnore); + ... +``` + +## Using your TypeConverter + +After you have written your TypeConverter, it is time to use it. Let's define a command that receives a `Type` as one of its parameters. + +```cs + ... + [Command("type-info", "typeinfo", "type")] + public void TypeInfo(Type type) + { + Console.WriteLine($"Information about: {type.Name}"); + + Console.WriteLine($"Fullname: {type.FullName}"); + Console.WriteLine($"Assembly: {type.Assembly.FullName}"); + } + ... +``` + +If you start the program now, it will throw an error on startup. `System.Type` has no known `TypeConverter`. In order to resolve this error, we must return to the `Program.cs` file of your application, and define the `TypeConverter` through configuration: + +```cs + ... + configuration.AddTypeReader(new ReflectionTypeConverter(caseIgnore: true)); + ... +``` + +Restarting the program now, you can try out your new command and see the results for yourself. \ No newline at end of file diff --git a/docs/Typereaders.md b/docs/Typereaders.md deleted file mode 100644 index 0c5e27a..0000000 --- a/docs/Typereaders.md +++ /dev/null @@ -1,55 +0,0 @@ -Typereaders read provided argument input in string format and try to convert them into the types as defined in the command signature. -Let's work on an example Typereader to learn how they work. - -- [Creating your Typereader](#-creating-your-typereader) -- [Using your Typereader](#-using-your-typereader) - -## 🏗️ Creating your Typereader - -All Typereaders inherit `TypeReader` or `TypeReader`. To start creating your Typereader, you have to inherit one of the two on a class. - -> For the simplicity of this documentation, only the generic type is introduced here. - -```cs -using CSF; - -namespace XProject -{ - public class GuidTypeReader : TypeReader - { - public override Result Evaluate(ICommandContext context, IParameterComponent parameter, IServiceProvider services, string value) - { - } - } -} -``` - -With this class defined and the method that will operate the evaluation being implemented, we can now write our code which defines the succession and failure conditions. In case of success, we also need to pass in the parsed object that the typereader expects to see returned. - -```cs - public override Result Evaluate(ICommandContext context, IParameterComponent parameter, IServiceProvider services, string value) - { - if (Guid.TryParse(value, out Guid guid)) - return Success(guid); - - return Failure("Failed to parse the input string as a GUID."); - } -``` - -That's it. With this done, we can look towards the application of our created Typereader. - -## 📝 Using your Typereader - -After you have written your Typereader, it is time to use it. Let's define a command that receives a `Guid` as one of its parameters. - -```cs -[Command("guid")] -public void Guid(Guid guid) -{ - Respond("Here is your guid: " + guid.ToString()); -} -``` - -This method will accept a Guid as parameter, and then responds with it. This can be handled any other way. Because of how CSF is written, if the Typereader is defined in the same assembly as registered commands, it will also be automatically registered. - -> If your Typereader by special chance is not automatically registered, or you want to register X amount of the same type of typereader, it can be added manually through `IServiceCollection.WithCommandManager(x => x.TypeReaders = [])` \ No newline at end of file diff --git a/docs/_Footer.md b/docs/_Footer.md deleted file mode 100644 index da7f7ef..0000000 --- a/docs/_Footer.md +++ /dev/null @@ -1,10 +0,0 @@ -# 🔍 Recommended Content - -### [[Learn About Typereaders|📖-Typereaders]] -Learn to use typereaders, which are responsible for parsing a parameter input into your desired value. - -### [[Using Preconditions|✅-Preconditions]] -Use preconditions to ensure that the command is allowed to be executed. - -### [[Customizing the Command Context|📝-Command-Context]] -Customize your Command Context with your own added information related to the command or the executing target. \ No newline at end of file diff --git a/examples/CSF.Samples.Console/Modules/CopyModule.cs b/examples/CSF.Samples.Console/Modules/CopyModule.cs index 48d3e32..9da874a 100644 --- a/examples/CSF.Samples.Console/Modules/CopyModule.cs +++ b/examples/CSF.Samples.Console/Modules/CopyModule.cs @@ -1,7 +1,7 @@ -using CSF; +using CSF.Core; using System.Diagnostics; -namespace XProject +namespace CSF.Samples { public class CopyModule : ModuleBase { @@ -21,7 +21,7 @@ public void Copy([Remainder] string toCopy) clipboardExecutable.StandardInput.Write(toCopy); clipboardExecutable.StandardInput.Close(); - Respond("Succesfully copied the content to your clipboard."); + Console.WriteLine("Succesfully copied the content to your clipboard."); } } } diff --git a/examples/CSF.Samples.Console/Modules/XModule.cs b/examples/CSF.Samples.Console/Modules/XModule.cs index 8ffcbe4..25c9a56 100644 --- a/examples/CSF.Samples.Console/Modules/XModule.cs +++ b/examples/CSF.Samples.Console/Modules/XModule.cs @@ -1,25 +1,28 @@ -using CSF; +using CSF.Core; -namespace XProject +namespace CSF.Samples { public class XModule : ModuleBase { [Command("helloworld")] public void HelloWorld() { - Respond("Hello world!"); + Console.WriteLine("Hello world!"); } [Command("reply")] public void Reply([Remainder] string message) { - Respond(message); + Console.WriteLine(message); } - [Command("guid")] - public void Guid(Guid guid) + [Command("type-info", "typeinfo", "type")] + public void TypeInfo(Type type) { - Respond("Here is your guid: " + guid.ToString()); + Console.WriteLine($"Information about: {type.Name}"); + + Console.WriteLine($"Fullname: {type.FullName}"); + Console.WriteLine($"Assembly: {type.Assembly.FullName}"); } } } \ No newline at end of file diff --git a/examples/CSF.Samples.Console/Preconditions/RequireOperationSystemAttribute.cs b/examples/CSF.Samples.Console/Preconditions/RequireOperationSystemAttribute.cs index eabaa42..695c494 100644 --- a/examples/CSF.Samples.Console/Preconditions/RequireOperationSystemAttribute.cs +++ b/examples/CSF.Samples.Console/Preconditions/RequireOperationSystemAttribute.cs @@ -1,22 +1,21 @@ -using CSF; +// Documentation of this file can be found at: https://github.com/csmir/CSF.NET/wiki/Preconditions. -namespace XProject +using CSF.Core; +using CSF.Preconditions; +using CSF.Reflection; + +namespace CSF.Samples { - public class RequireOperatingSystemAttribute : PreconditionAttribute + public class RequireOperatingSystemAttribute(PlatformID platform) : PreconditionAttribute { - public PlatformID Platform { get; } - - public RequireOperatingSystemAttribute(PlatformID platform) - { - Platform = platform; - } + public PlatformID Platform { get; } = platform; - public override Result EvaluateAsync(ICommandContext context, Command command, IServiceProvider provider) + public override ValueTask EvaluateAsync(ICommandContext context, IServiceProvider services, CommandInfo command, CancellationToken cancellationToken) { if (Environment.OSVersion.Platform == Platform) - return Success(); + return ValueTask.FromResult(Success()); - return Failure("The platform this command was executed does not support this operation."); + return ValueTask.FromResult(Error("The platform does not support this operation.")); } } } diff --git a/examples/CSF.Samples.Console/Program.cs b/examples/CSF.Samples.Console/Program.cs index f09e172..50bee4e 100644 --- a/examples/CSF.Samples.Console/Program.cs +++ b/examples/CSF.Samples.Console/Program.cs @@ -1,23 +1,38 @@ -using CSF; +// This sample implements the same structure as https://github.com/csmir/CSF.NET/wiki/Quick-Guide. +// It also implements examples for Precondition and TypeConverter documentation. + +using CSF.Core; +using CSF.Helpers; +using CSF.Parsing; +using CSF.Samples; + using Microsoft.Extensions.DependencyInjection; var collection = new ServiceCollection() - .WithCommandManager(); + .ConfigureCommands(configuration => + { + configuration.ConfigureResultAction((context, result, services) => + { + if (!result.Success) + { + Console.WriteLine(result.Exception); + } + + return Task.CompletedTask; + }); + configuration.AddTypeReader(new ReflectionTypeConverter(caseIgnore: true)); + }); var services = collection.BuildServiceProvider(); var framework = services.GetRequiredService(); +var parser = new StringParser(); while (true) { - var input = Console.ReadLine()!; - - var context = new CommandContext(input); - - var result = framework.ExecuteAsync(context); + var input = parser.Parse(Console.ReadLine()); - if (result.Failed(out var failure)) - Console.WriteLine(failure.Exception); + var context = new CommandContext(); - await result; + framework.TryExecute(context, input); } \ No newline at end of file diff --git a/examples/CSF.Samples.Console/TypeConverters/ReflectionTypeConverter.cs b/examples/CSF.Samples.Console/TypeConverters/ReflectionTypeConverter.cs new file mode 100644 index 0000000..db114ca --- /dev/null +++ b/examples/CSF.Samples.Console/TypeConverters/ReflectionTypeConverter.cs @@ -0,0 +1,35 @@ +// Documentation of this file can be found at https://github.com/csmir/CSF.NET/wiki/Type-Conversion. + +using CSF.Core; +using CSF.Reflection; +using CSF.TypeConverters; + +namespace CSF.Samples +{ + public class ReflectionTypeConverter(bool caseIgnore) : TypeConverter + { + private readonly bool _caseIgnore = caseIgnore; + + public override ValueTask EvaluateAsync(ICommandContext context, IServiceProvider services, IArgument argument, string raw, CancellationToken cancellationToken) + { + try + { + var typeSrc = Type.GetType( + typeName: raw, + throwOnError: true, + ignoreCase: _caseIgnore); + + if (typeSrc == null) + { + return ValueTask.FromResult(Error($"A type with name '{raw}' was not found.")); + } + + return ValueTask.FromResult(Success(typeSrc)); + } + catch (Exception ex) + { + return ValueTask.FromResult(Error(ex)); + } + } + } +} diff --git a/examples/CSF.Samples.Console/TypeReaders/GuidTypeReader.cs b/examples/CSF.Samples.Console/TypeReaders/GuidTypeReader.cs deleted file mode 100644 index 702875d..0000000 --- a/examples/CSF.Samples.Console/TypeReaders/GuidTypeReader.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace CSF.Console -{ - // This type reader will try to convert the provided string to a GUID before passing it through to the command. - public sealed class GuidTypeReader : TypeReader - { - public override Result Evaluate(ICommandContext context, IParameterComponent parameter, IServiceProvider services, string value) - { - if (Guid.TryParse(value, out Guid guid)) - return Success(guid); - - return Failure("Failed to parse the input string as a GUID."); - } - } -} diff --git a/src/CSF.Core/Core/Attributes/CommandAttribute.cs b/src/CSF.Core/Core/Attributes/CommandAttribute.cs index 5a968e7..483ec60 100644 --- a/src/CSF.Core/Core/Attributes/CommandAttribute.cs +++ b/src/CSF.Core/Core/Attributes/CommandAttribute.cs @@ -37,13 +37,13 @@ public CommandAttribute([DisallowNull] string name) public CommandAttribute([DisallowNull] string name, params string[] aliases) { if (string.IsNullOrWhiteSpace(name)) - ThrowHelpers.InvalidArg(name); + ThrowHelpers.ThrowInvalidArgument(name); var arr = new string[aliases.Length + 1]; for (int i = 0; i < aliases.Length; i++) { if (string.IsNullOrWhiteSpace(aliases[i])) - ThrowHelpers.InvalidArg(aliases); + ThrowHelpers.ThrowInvalidArgument(aliases); if (arr.Contains(aliases[i])) ThrowHelpers.NotDistinct(aliases); diff --git a/src/CSF.Core/Core/Attributes/DescriptionAttribute.cs b/src/CSF.Core/Core/Attributes/DescriptionAttribute.cs index b728741..d751519 100644 --- a/src/CSF.Core/Core/Attributes/DescriptionAttribute.cs +++ b/src/CSF.Core/Core/Attributes/DescriptionAttribute.cs @@ -21,7 +21,7 @@ public sealed class DescriptionAttribute : Attribute public DescriptionAttribute([DisallowNull] string description) { if (string.IsNullOrWhiteSpace(description)) - ThrowHelpers.InvalidArg(description); + ThrowHelpers.ThrowInvalidArgument(description); Description = description; } diff --git a/src/CSF.Core/Core/Attributes/GroupAttribute.cs b/src/CSF.Core/Core/Attributes/GroupAttribute.cs index 407085d..f2bb3b5 100644 --- a/src/CSF.Core/Core/Attributes/GroupAttribute.cs +++ b/src/CSF.Core/Core/Attributes/GroupAttribute.cs @@ -40,13 +40,13 @@ public GroupAttribute([DisallowNull] string name) public GroupAttribute([DisallowNull] string name, params string[] aliases) { if (string.IsNullOrWhiteSpace(name)) - ThrowHelpers.InvalidArg(name); + ThrowHelpers.ThrowInvalidArgument(name); var arr = new string[aliases.Length + 1]; for (int i = 0; i < aliases.Length; i++) { if (string.IsNullOrWhiteSpace(aliases[i])) - ThrowHelpers.InvalidArg(aliases); + ThrowHelpers.ThrowInvalidArgument(aliases); if (arr.Contains(aliases[i])) ThrowHelpers.NotDistinct(aliases); diff --git a/src/CSF.Core/Core/CommandConfiguration.cs b/src/CSF.Core/Core/CommandConfiguration.cs index fcb8a10..6bd61a1 100644 --- a/src/CSF.Core/Core/CommandConfiguration.cs +++ b/src/CSF.Core/Core/CommandConfiguration.cs @@ -10,7 +10,7 @@ namespace CSF.Core /// public class CommandConfiguration { - private Assembly[] _assemblies = [Assembly.GetExecutingAssembly()]; + private Assembly[] _assemblies = [Assembly.GetEntryAssembly()]; /// /// Gets a collection of assemblies that the will use to register commands. @@ -58,7 +58,7 @@ public ResultResolver ResultResolver { if (value == null) { - ThrowHelpers.InvalidArg(value); + ThrowHelpers.ThrowInvalidArgument(value); } _resultResolver = value; @@ -85,7 +85,7 @@ public AsyncApproach AsyncApproach { if (value is not AsyncApproach.Await or AsyncApproach.Discard) { - ThrowHelpers.InvalidOp("AsyncApproach does not support values that exceed the provided options, ranging between 0 and 1."); + ThrowHelpers.ThrowInvalidOperation("AsyncApproach does not support values that exceed the provided options, ranging between 0 and 1."); } _asyncApproach = value; @@ -104,7 +104,7 @@ public CommandConfiguration WithAssemblies(params Assembly[] assemblies) { if (assemblies == null) { - ThrowHelpers.InvalidArg(assemblies); + ThrowHelpers.ThrowInvalidArgument(assemblies); } _assemblies = assemblies.Distinct().ToArray(); @@ -124,7 +124,7 @@ public CommandConfiguration AddAssembly(Assembly assembly) { if (assembly == null) { - ThrowHelpers.InvalidArg(assembly); + ThrowHelpers.ThrowInvalidArgument(assembly); } if (_assemblies.Contains(assembly)) @@ -170,7 +170,7 @@ public CommandConfiguration WithTypeReaders(params TypeConverter[] typeReaders) { if (typeReaders == null) { - ThrowHelpers.InvalidArg(typeReaders); + ThrowHelpers.ThrowInvalidArgument(typeReaders); } _typeReaders = typeReaders.Distinct(TypeConverter.EqualityComparer.Default).ToArray(); @@ -190,7 +190,7 @@ public CommandConfiguration AddTypeReader(TypeConverter typeReader) { if (typeReader == null) { - ThrowHelpers.InvalidArg(typeReader); + ThrowHelpers.ThrowInvalidArgument(typeReader); } if (_typeReaders.Contains(typeReader, TypeConverter.EqualityComparer.Default)) @@ -233,7 +233,7 @@ public CommandConfiguration ConfigureResultAction([DisallowNull] Func [DoesNotReturn] [EditorBrowsable(EditorBrowsableState.Never)] - public static void InvalidOp([DisallowNull] string failureMessage) + public static void ThrowInvalidOperation([DisallowNull] string failureMessage) { throw new InvalidOperationException(failureMessage); } @@ -31,7 +31,7 @@ public static void InvalidOp([DisallowNull] string failureMessage) /// [DoesNotReturn] [EditorBrowsable(EditorBrowsableState.Never)] - public static void InvalidArg(object value, [CallerArgumentExpression(nameof(value))] string arg = null) + public static void ThrowInvalidArgument(object value, [CallerArgumentExpression(nameof(value))] string arg = null) { // should resolve overload when a collection is null or empty. if (value is ICollection or IEnumerable) diff --git a/src/CSF.Core/Preconditions/PreconditionAttribute.cs b/src/CSF.Core/Preconditions/PreconditionAttribute.cs index df190c9..481e817 100644 --- a/src/CSF.Core/Preconditions/PreconditionAttribute.cs +++ b/src/CSF.Core/Preconditions/PreconditionAttribute.cs @@ -40,7 +40,7 @@ public abstract class PreconditionAttribute : Attribute public static CheckResult Error([DisallowNull] Exception exception) { if (exception == null) - ThrowHelpers.InvalidArg(exception); + ThrowHelpers.ThrowInvalidArgument(exception); if (exception is CheckException checkEx) { @@ -57,7 +57,7 @@ public static CheckResult Error([DisallowNull] Exception exception) public virtual CheckResult Error([DisallowNull] string error) { if (string.IsNullOrEmpty(error)) - ThrowHelpers.InvalidArg(error); + ThrowHelpers.ThrowInvalidArgument(error); return new(new CheckException(error)); } diff --git a/src/CSF.Core/Reflection/Impl/CommandInfo.cs b/src/CSF.Core/Reflection/Impl/CommandInfo.cs index 5514a20..1474bc8 100644 --- a/src/CSF.Core/Reflection/Impl/CommandInfo.cs +++ b/src/CSF.Core/Reflection/Impl/CommandInfo.cs @@ -68,7 +68,7 @@ internal CommandInfo(ModuleInfo module, MethodInfo method, string[] aliases, IDi { if (parameters.Length > 1 && parameters[^1].IsRemainder) { - ThrowHelpers.InvalidOp($"{nameof(RemainderAttribute)} can only exist on the last parameter of a method."); + ThrowHelpers.ThrowInvalidOperation($"{nameof(RemainderAttribute)} can only exist on the last parameter of a method."); } } diff --git a/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs b/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs index 2a0e930..0dece02 100644 --- a/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs +++ b/src/CSF.Core/Reflection/Impl/ComplexArgumentInfo.cs @@ -76,7 +76,7 @@ internal ComplexArgumentInfo(ParameterInfo parameterInfo, IDictionary ObjectEvaluateAsync(ICommandContext context, I public virtual ConvertResult Error([DisallowNull] Exception exception) { if (exception == null) - ThrowHelpers.InvalidArg(exception); + { + ThrowHelpers.ThrowInvalidArgument(exception); + } - if (exception is ConvertException readEx) + if (exception is ConvertException convertEx) { - return new(readEx); + return new(convertEx); } return new(new ConvertException(string.Format(_exHeader, Type.Name), exception)); } @@ -88,7 +90,9 @@ public virtual ConvertResult Error([DisallowNull] Exception exception) public virtual ConvertResult Error([DisallowNull] string error) { if (string.IsNullOrEmpty(error)) - ThrowHelpers.InvalidArg(error); + { + ThrowHelpers.ThrowInvalidArgument(error); + } return new(new ConvertException(error)); } @@ -105,15 +109,15 @@ public virtual ConvertResult Success(object value) internal static TypeConverter[] CreateDefaultReaders() { - var range = BaseTypeConverter.CreateBaseReaders(); + var arr = BaseTypeConverter.CreateBaseReaders(); - int length = range.Length; - Array.Resize(ref range, length + 2); + int i = arr.Length; + Array.Resize(ref arr, i + 2); - range[length++] = new TimeSpanConverter(); - range[length++] = new ColorConverter(); + arr[i++] = new TimeSpanConverter(); + arr[i++] = new ColorConverter(); - return range; + return arr; } internal class EqualityComparer : IEqualityComparer diff --git a/src/CSF.Tests.Hosting/CommandHandler.cs b/src/CSF.Tests.Hosting/CommandHandler.cs index 3e29f2e..d2e42c5 100644 --- a/src/CSF.Tests.Hosting/CommandHandler.cs +++ b/src/CSF.Tests.Hosting/CommandHandler.cs @@ -30,7 +30,7 @@ private async Task RunAsync(CancellationToken cancellationToken) if (args.Length == 0) { - ThrowHelpers.InvalidArg(args); + ThrowHelpers.ThrowInvalidArgument(args); } var guid = Guid.NewGuid(); From d5ab5c00adf99435e20f841f06d4529526163eaa Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Sun, 4 Feb 2024 21:38:26 +0100 Subject: [PATCH 37/40] Further readme change, new files --- README.md | 2 +- docs/Dependency-Injection.md | 0 docs/Results.md | 0 .../Modules/{XModule.cs => ExampleModule.cs} | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 docs/Dependency-Injection.md create mode 100644 docs/Results.md rename examples/CSF.Samples.Console/Modules/{XModule.cs => ExampleModule.cs} (93%) diff --git a/README.md b/README.md index 1664413..b2d3900 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ configuration.ConfigureResultAction(async (context, result, services) => ``` -> See feature [documentation](https://github.com/csmir/CSF.NET/wiki/Handling-Results) for more. +> See feature [documentation](https://github.com/csmir/CSF.NET/wiki/Results) for more. #### Customization diff --git a/docs/Dependency-Injection.md b/docs/Dependency-Injection.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/Results.md b/docs/Results.md new file mode 100644 index 0000000..e69de29 diff --git a/examples/CSF.Samples.Console/Modules/XModule.cs b/examples/CSF.Samples.Console/Modules/ExampleModule.cs similarity index 93% rename from examples/CSF.Samples.Console/Modules/XModule.cs rename to examples/CSF.Samples.Console/Modules/ExampleModule.cs index 25c9a56..9504b6c 100644 --- a/examples/CSF.Samples.Console/Modules/XModule.cs +++ b/examples/CSF.Samples.Console/Modules/ExampleModule.cs @@ -2,7 +2,7 @@ namespace CSF.Samples { - public class XModule : ModuleBase + public class ExampleModule : ModuleBase { [Command("helloworld")] public void HelloWorld() From fbcb8d20343f6f7c72e3e829074e18660b822a12 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Mon, 5 Feb 2024 15:15:01 +0100 Subject: [PATCH 38/40] Auto-scope service layer logic --- README.md | 27 +++++++------- docs/Dependency-Injection.md | 8 +++++ docs/Home.md | 2 +- src/CSF.Core/Core/CommandManager.cs | 36 ++++++++++--------- .../Core/Configuration/AsyncApproach.cs | 2 +- src/CSF.Core/Helpers/ExecutionHelpers.cs | 13 +++---- src/CSF.Core/Helpers/ServiceHelpers.cs | 17 +++++++++ 7 files changed, 66 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index b2d3900..318e016 100644 --- a/README.md +++ b/README.md @@ -68,22 +68,21 @@ var services = new ServiceCollection() CSF.NET will return results for running commands through a `ResultResolver`. This resolver has a default implementation that can be configured through the `CommandConfiguration` ```cs -... -configuration.ConfigureResultAction(async (context, result, services) => -{ - if (result.Success) - { - await Task.CompletedTask; - } - else - { - Console.WriteLine(result.Exception); - } -}); -... + ... + configuration.ConfigureResultAction(async (context, result, services) => + { + if (result.Success) + { + await Task.CompletedTask; + } + else + { + Console.WriteLine(result.Exception); + } + }); + ... ``` - > See feature [documentation](https://github.com/csmir/CSF.NET/wiki/Results) for more. #### Customization diff --git a/docs/Dependency-Injection.md b/docs/Dependency-Injection.md index e69de29..c985277 100644 --- a/docs/Dependency-Injection.md +++ b/docs/Dependency-Injection.md @@ -0,0 +1,8 @@ +Dependency Injection is vital to command invocation, managing all types that implement `ModuleBase` and injecting registered services. +To understand how this works at its core, it is **important** to read any of the following resources about the terminology and how it's designed in .NET: + +> Dependency Injection will be referred to as 'DI' from here forward. + +- [DI in .NET](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) +- [DI Usages in apps](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-usage) +- [DI Guidelines](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-guidelines) \ No newline at end of file diff --git a/docs/Home.md b/docs/Home.md index 2e02fbf..0db2af8 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -5,7 +5,7 @@ For this claim to have any value, a great deal is done to help a developer write #### [[Quick Guide|Quick-Guide]] -A quick start to CSF and various practices. +A quick start to CSF and introduction to basic practices. #### [[Using Preconditions|Preconditions]] diff --git a/src/CSF.Core/Core/CommandManager.cs b/src/CSF.Core/Core/CommandManager.cs index 7077e5d..e81fa5b 100644 --- a/src/CSF.Core/Core/CommandManager.cs +++ b/src/CSF.Core/Core/CommandManager.cs @@ -134,20 +134,22 @@ private async Task ExecuteInternalAsync(ICommandContext context, object[] args, { var searches = Search(args); + var scope = Services.CreateScope(Configuration); + var c = 0; foreach (var search in searches.OrderByDescending(x => x.Command.Priority)) { c++; - var match = await MatchAsync(context, search, args, cancellationToken); + var match = await MatchAsync(context, scope, search, args, cancellationToken); // enter the invocation logic when a match is successful. if (match.Success) { - var result = await RunAsync(context, match, cancellationToken); + var result = await RunAsync(context, scope, match, cancellationToken); - await _resultHandle.TryHandleAsync(context, result, Services); + await _resultHandle.TryHandleAsync(context, result, scope.ServiceProvider); return; } @@ -165,23 +167,23 @@ private async Task ExecuteInternalAsync(ICommandContext context, object[] args, // if there is a fallback present, we send matchfailure. if (context.TryGetFallback(out var fallback)) { - await _resultHandle.TryHandleAsync(context, fallback, Services); + await _resultHandle.TryHandleAsync(context, fallback, scope.ServiceProvider); } } #endregion #region Matching - private async ValueTask MatchAsync(ICommandContext context, SearchResult search, object[] args, CancellationToken cancellationToken) + private async ValueTask MatchAsync(ICommandContext context, IServiceScope scope, SearchResult search, object[] args, CancellationToken cancellationToken) { // check command preconditions. - var check = await CheckAsync(context, search.Command, cancellationToken); + var check = await CheckAsync(context, scope, search.Command, cancellationToken); // verify check success, if not, return the failure. if (!check.Success) return new(search.Command, new MatchException("Command failed to reach execution state. View inner exception for more details.", check.Exception)); // read the command parameters in right order. - var readResult = await ConvertAsync(context, search, args, cancellationToken); + var readResult = await ConvertAsync(context, scope, search, args, cancellationToken); // exchange the reads for result, verifying successes in the process. var reads = new object[readResult.Length]; @@ -200,13 +202,13 @@ private async ValueTask MatchAsync(ICommandContext context, SearchR #endregion #region Checking - private async ValueTask CheckAsync(ICommandContext context, CommandInfo command, CancellationToken cancellationToken) + private async ValueTask CheckAsync(ICommandContext context, IServiceScope scope, CommandInfo command, CancellationToken cancellationToken) { context.LogDebug("Attempting validations for {0}", command); foreach (var precon in command.Preconditions) { - var result = await precon.EvaluateAsync(context, Services, command, cancellationToken); + var result = await precon.EvaluateAsync(context, scope.ServiceProvider, command, cancellationToken); if (!result.Success) return result; @@ -217,7 +219,7 @@ private async ValueTask CheckAsync(ICommandContext context, Command #endregion #region Reading - private async ValueTask ConvertAsync(ICommandContext context, SearchResult search, object[] args, CancellationToken cancellationToken) + private async ValueTask ConvertAsync(ICommandContext context, IServiceScope scope, SearchResult search, object[] args, CancellationToken cancellationToken) { context.LogDebug("Attempting argument conversion for {0}", search.Command); @@ -230,15 +232,15 @@ private async ValueTask ConvertAsync(ICommandContext context, S // check if input equals command length. if (search.Command.MaxLength == length) - return await search.Command.Arguments.RecursiveConvertAsync(context, Services, args[^length..], 0, cancellationToken); + return await search.Command.Arguments.RecursiveConvertAsync(context, scope, args[^length..], 0, cancellationToken); // check if input is longer than command, but remainder to concatenate. if (search.Command.MaxLength <= length && search.Command.HasRemainder) - return await search.Command.Arguments.RecursiveConvertAsync(context, Services, args[^length..], 0, cancellationToken); + return await search.Command.Arguments.RecursiveConvertAsync(context, scope, args[^length..], 0, cancellationToken); // check if input is shorter than command, but optional parameters to replace. if (search.Command.MaxLength > length && search.Command.MinLength <= length) - return await search.Command.Arguments.RecursiveConvertAsync(context, Services, args[^length..], 0, cancellationToken); + return await search.Command.Arguments.RecursiveConvertAsync(context, scope, args[^length..], 0, cancellationToken); // input is too long or too short. return []; @@ -246,7 +248,7 @@ private async ValueTask ConvertAsync(ICommandContext context, S #endregion #region Running - private async ValueTask RunAsync(ICommandContext context, MatchResult match, CancellationToken cancellationToken) + private async ValueTask RunAsync(ICommandContext context, IServiceScope scope, MatchResult match, CancellationToken cancellationToken) { try { @@ -256,11 +258,11 @@ private async ValueTask RunAsync(ICommandContext context, MatchResult var module = targetInstance != null ? targetInstance as ModuleBase - : ActivatorUtilities.CreateInstance(Services, match.Command.Module.Type) as ModuleBase; + : ActivatorUtilities.CreateInstance(scope.ServiceProvider, match.Command.Module.Type) as ModuleBase; module.Context = context; module.Command = match.Command; - module.Services = Services; + module.Services = scope.ServiceProvider; await module.BeforeExecuteAsync(cancellationToken); @@ -286,7 +288,7 @@ public object GetService(Type serviceType) return null; } - public static ServiceProvider Default + public static IServiceProvider Default { get { diff --git a/src/CSF.Core/Core/Configuration/AsyncApproach.cs b/src/CSF.Core/Core/Configuration/AsyncApproach.cs index fa3522e..df90d6b 100644 --- a/src/CSF.Core/Core/Configuration/AsyncApproach.cs +++ b/src/CSF.Core/Core/Configuration/AsyncApproach.cs @@ -10,7 +10,7 @@ namespace CSF.Core /// The asynchronous execution approach drastically changes the expected behavior of executing a command: /// /// - /// is the default setting and tells the execution pipeline to finish executing before returning control to the caller. + /// is the default setting and tells the pipeline to finish executing before returning control to the caller. /// This ensures that the execution will fully finish executing, whether it failed or not, before allowing another to be executed. /// /// diff --git a/src/CSF.Core/Helpers/ExecutionHelpers.cs b/src/CSF.Core/Helpers/ExecutionHelpers.cs index 9d890b9..579ce02 100644 --- a/src/CSF.Core/Helpers/ExecutionHelpers.cs +++ b/src/CSF.Core/Helpers/ExecutionHelpers.cs @@ -1,5 +1,6 @@ using CSF.Core; using CSF.Reflection; +using Microsoft.Extensions.DependencyInjection; using System.Reflection.Metadata; namespace CSF.Helpers @@ -31,9 +32,9 @@ public static IEnumerable RecursiveSearch(this IEnumerable RecursiveConvertAsync(this IArgument[] param, ICommandContext context, IServiceProvider services, object[] args, int index, CancellationToken cancellationToken) + public static async Task RecursiveConvertAsync(this IArgument[] param, ICommandContext context, IServiceScope scope, object[] args, int index, CancellationToken cancellationToken) { - static async ValueTask ConvertAsync(IArgument param, ICommandContext context, IServiceProvider services, object arg, CancellationToken cancellationToken) + static async ValueTask ConvertAsync(IArgument param, ICommandContext context, IServiceScope scope, object arg, CancellationToken cancellationToken) { if (param.IsNullable && arg is null or "null" or "nothing") return new(arg); @@ -41,7 +42,7 @@ static async ValueTask ConvertAsync(IArgument param, ICommandCont if (param.Type == typeof(string) || param.Type == typeof(object)) return new(arg); - return await param.Converter.ObjectEvaluateAsync(context, services, param, arg, cancellationToken); + return await param.Converter.ObjectEvaluateAsync(context, scope.ServiceProvider, param, arg, cancellationToken); } var results = new ConvertResult[param.Length]; @@ -56,7 +57,7 @@ static async ValueTask ConvertAsync(IArgument param, ICommandCont if (parameter.Type == typeof(string)) results[i] = new(input); else - results[i] = await ConvertAsync(parameter, context, services, input, cancellationToken); + results[i] = await ConvertAsync(parameter, context, scope, input, cancellationToken); break; } @@ -69,7 +70,7 @@ static async ValueTask ConvertAsync(IArgument param, ICommandCont if (parameter is ComplexArgumentInfo complex) { - var result = await complex.Arguments.RecursiveConvertAsync(context, services, args, index, cancellationToken); + var result = await complex.Arguments.RecursiveConvertAsync(context, scope, args, index, cancellationToken); index += result.Length; @@ -88,7 +89,7 @@ static async ValueTask ConvertAsync(IArgument param, ICommandCont continue; } - results[i] = await ConvertAsync(parameter, context, services, args[index], cancellationToken); + results[i] = await ConvertAsync(parameter, context, scope, args[index], cancellationToken); index++; } diff --git a/src/CSF.Core/Helpers/ServiceHelpers.cs b/src/CSF.Core/Helpers/ServiceHelpers.cs index 06516b6..fc43712 100644 --- a/src/CSF.Core/Helpers/ServiceHelpers.cs +++ b/src/CSF.Core/Helpers/ServiceHelpers.cs @@ -81,5 +81,22 @@ public static IServiceCollection AddModules(this IServiceCollection collection, return collection; } + + /// + /// Generates a by resolving the configured within . + /// + /// + /// The configuration for generating 's + /// A matching the configured logic, being async when is set. + [EditorBrowsable(EditorBrowsableState.Never)] + public static IServiceScope CreateScope(this IServiceProvider provider, CommandConfiguration configuration) + { + if (configuration.AsyncApproach == AsyncApproach.Await) + { + return provider.CreateScope(); + } + + return provider.CreateAsyncScope(); + } } } From 8d490997e1e79a0b5bae7beb85748c0a0f8a6cdb Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Mon, 5 Feb 2024 15:18:40 +0100 Subject: [PATCH 39/40] EOP(?) disposer logic --- src/CSF.Core/Core/CommandManager.cs | 4 ++-- src/CSF.Core/Core/Results/ResultResolver.cs | 20 ++++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/CSF.Core/Core/CommandManager.cs b/src/CSF.Core/Core/CommandManager.cs index e81fa5b..6e80d94 100644 --- a/src/CSF.Core/Core/CommandManager.cs +++ b/src/CSF.Core/Core/CommandManager.cs @@ -149,7 +149,7 @@ private async Task ExecuteInternalAsync(ICommandContext context, object[] args, { var result = await RunAsync(context, scope, match, cancellationToken); - await _resultHandle.TryHandleAsync(context, result, scope.ServiceProvider); + await _resultHandle.TryHandleAsync(context, result, scope); return; } @@ -167,7 +167,7 @@ private async Task ExecuteInternalAsync(ICommandContext context, object[] args, // if there is a fallback present, we send matchfailure. if (context.TryGetFallback(out var fallback)) { - await _resultHandle.TryHandleAsync(context, fallback, scope.ServiceProvider); + await _resultHandle.TryHandleAsync(context, fallback, scope); } } #endregion diff --git a/src/CSF.Core/Core/Results/ResultResolver.cs b/src/CSF.Core/Core/Results/ResultResolver.cs index 16ee99f..3f8c00c 100644 --- a/src/CSF.Core/Core/Results/ResultResolver.cs +++ b/src/CSF.Core/Core/Results/ResultResolver.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using Microsoft.Extensions.DependencyInjection; +using System.ComponentModel; namespace CSF.Core { @@ -20,17 +21,24 @@ public class ResultResolver(Func /// Context of the current execution. /// The result of the command, being successful or containing failure information. - /// The provider used to register modules and inject services. + /// The provider used to register modules and inject services. /// An awaitable that waits for the delegate to finish. [EditorBrowsable(EditorBrowsableState.Never)] - public Task TryHandleAsync(ICommandContext context, ICommandResult result, IServiceProvider services) + public async Task TryHandleAsync(ICommandContext context, ICommandResult result, IServiceScope scope) { - if (Handler == null) + if (Handler != null) { - return Task.CompletedTask; + await Handler(context, result, scope.ServiceProvider); } - return Handler(context, result, services); + if (scope is AsyncServiceScope asyncScope) + { + await asyncScope.DisposeAsync(); + } + else + { + scope.Dispose(); + } } internal static ResultResolver Default From 23267b84b4f34527270c30cbf872821d63ec917f Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:55:15 +0100 Subject: [PATCH 40/40] Document configuration, dependency injection --- README.md | 13 ++- docs/Configuration.md | 74 ++++++++++++ docs/Dependency-Injection.md | 108 +++++++++++++++++- docs/Home.md | 22 +++- docs/Type-Conversion.md | 2 +- src/CSF.Core/Core/CommandConfiguration.cs | 20 ++++ src/CSF.Core/Core/CommandManager.cs | 30 ++--- .../Core/Configuration/AsyncApproach.cs | 2 +- .../Core/Configuration/ScopeApproach.cs | 36 ++++++ src/CSF.Core/Core/Execution/ModuleBase.cs | 50 +++++++- src/CSF.Core/Helpers/ExecutionHelpers.cs | 12 +- src/CSF.Core/Helpers/ServiceHelpers.cs | 19 ++- 12 files changed, 348 insertions(+), 40 deletions(-) create mode 100644 docs/Configuration.md create mode 100644 src/CSF.Core/Core/Configuration/ScopeApproach.cs diff --git a/README.md b/README.md index 318e016..a6b9154 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ # CSF.NET - Command Standardization Framework for .NET -CSF is an attribute based framework that makes creating and processing **text based commands** easy for any platform. It implements a modular, easy to implement pipeline for registering and executing commands, as well as a wide range of customization options to make development on different platforms as easy as possible. +CSF is an attribute based framework that makes creating and processing **text based commands** easy for any platform. +It implements a modular, easy to implement pipeline for registering and executing commands, as well as a wide range of customization options to make development on different platforms as easy as possible. - [Features](#features) - [Additional Packages](#additional-packages) @@ -12,7 +13,8 @@ CSF is an attribute based framework that makes creating and processing **text ba #### Type Conversion -For raw input, automated conversion to fit command signature is supported by `TypeConverter`'s. `ValueType`, `Enum` and nullable variant types are automatically parsed by the framework and populate commands as below: +For raw input, automated conversion to fit command signature is supported by `TypeConverter`'s. +`ValueType`, `Enum` and nullable variant types are automatically parsed by the framework and populate commands as below: ```cs ... @@ -49,7 +51,9 @@ public async Task Test() #### Dependency Injection -You can provide an `IServiceProvider` at execution to inject modules with dependencies, in accordance to the conventions `Microsoft.Extensions.DependencyInjection` follows. The `IServiceProvider` has a number of extensions that are suggested to be used when writing your codebase with CSF. These extensions serve you and the program, reducing boilerplate in the application setup. +You can provide an `IServiceProvider` at execution to inject modules with dependencies, in accordance to the conventions `Microsoft.Extensions.DependencyInjection` follows. +The `IServiceProvider` has a number of extensions that are suggested to be used when writing your codebase with CSF. +These extensions serve you and the program, reducing boilerplate in the application setup. ```cs ... @@ -65,7 +69,8 @@ var services = new ServiceCollection() #### Informative Results -CSF.NET will return results for running commands through a `ResultResolver`. This resolver has a default implementation that can be configured through the `CommandConfiguration` +CSF.NET will return results for running commands through a `ResultResolver`. +This resolver has a default implementation that can be configured through the `CommandConfiguration` ```cs ... diff --git a/docs/Configuration.md b/docs/Configuration.md new file mode 100644 index 0000000..026a594 --- /dev/null +++ b/docs/Configuration.md @@ -0,0 +1,74 @@ +The configuration of command registration and execution can be overwhelming if you are unfamiliar with the various options exposed. This chapter introduces the various options and elaborates the functionality covered within. + +## The Command Configuration + +`CommandConfiguration` serves as the base class for handling configuration options for the `CommandManager` and how it runs the execution pipeline. +It exposes a fair amount of options, each useful in different situations. + +### Assemblies + +Known assemblies are a core component to how CSF functions, iterating through each type defined per-assembly to register commands and writing them to the `IReadOnlySet` exposed in `CommandManager`. This is the collection used to find, match and execute commands. + +By default, `Assemblies` is populated by `Assembly.GetEntryAssembly`, which serves as the entry-point executable to run the framework with. + +The `CommandConfiguration` exposes an array that is accessible by the following methods: + +#### `TryAddAssembly` + +This method will attempt to add an `Assembly` to `Assemblies`, returning if the assembly was already added. + +#### `AddAssembly` + +This method will attempt to add an `Assembly` to `Assemblies`, throwing an exception if the assembly was already added. + +#### `WithAssemblies` + +This method will replace all current assemblies with `params Assembly[]` as passed into the method. Duplicates are automatically removed through calling `IEnumerable.Union`. + +### Type Converters (Converters) + +In many cases, there is need for custom `TypeConverter` implementations to convert types that CSF does not already convert for you. These all need to be registered here, in the same way as `Assemblies`. The overloads for adding new type converters is the same as the prior, with no exceptions. + +### Result Resolver (ResultResolver) + +Results can be handled in an elaborate many ways, as documented [[here|Results]]. This configuration option allows you to set a custom resolver or redefine the base implementation with `ConfigureResultAction`. + +### Async Approach (AsyncApproach) + +The `AsyncApproach` option defines how commands are ran. There are two options aside from `Default`, which is set to `Await`. + +#### Await + +This is the default setting and tells the pipeline to finish executing before returning control to the caller. +This ensures that the execution will fully finish executing, whether it failed or not, before allowing another to be executed. + +#### Discard + +Instead of waiting for the full execution before returning control, the execution will return immediately after the entrypoint is called, slipping thread for the rest of execution. +When more than one input source is expected to be handled, this is generally the advised method of execution. + +Changing to this setting, the following should be checked for thread-safety: + +- Services, specifically those created as singleton or scoped to anything but a single command. +- Implementations of `TypeConverter`, `TypeConverter{T}` and `PreconditionAttribute`. +- Generic collections and objects with shared access. + +> To ensure thread safety in any of the above situations, it is important to know what this actually means. +> For more information, consider reading [this article](https://learn.microsoft.com/en-us/dotnet/standard/threading/managed-threading-best-practices). + +### Scope Approach (ScopeApproach) + +#### OnlyAsync + +This option forces the `IServiceProvider` to use `CreateAsyncScope` to generate new scopes. This structure wraps around `IServiceScope` to carry the ability to handle `IAsyncDisposable` types. This is usually unnecessary, unless if `AsyncApproach` is set to `Discard` but no `IAsyncDisposable` services exist. + +#### OnlySync + +Here, the option forces the `IServiceProvider` to use `CreateScope`. If no settings are changed in the `CommandConfiguration`, this is the default approach. It is unusual for smaller applications to have scopes that implement `IAsyncDisposable` pattern. Though, if this is the case, this setting must be changed. + +#### ByAsyncApproach + +Here, `AsyncApproach` determines the value (one of the above). + +- When set to `Discard`, it will follow the logic as set in `OnlyAsync`. +- When set to `Await`, it will follow `OnlySync` logic. \ No newline at end of file diff --git a/docs/Dependency-Injection.md b/docs/Dependency-Injection.md index c985277..be72475 100644 --- a/docs/Dependency-Injection.md +++ b/docs/Dependency-Injection.md @@ -1,8 +1,112 @@ Dependency Injection is vital to command invocation, managing all types that implement `ModuleBase` and injecting registered services. -To understand how this works at its core, it is **important** to read any of the following resources about the terminology and how it's designed in .NET: +If you do not understand how this works at its core, it is **important** to read the following resources about the terminology and how it's designed in .NET: > Dependency Injection will be referred to as 'DI' from here forward. - [DI in .NET](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) - [DI Usages in apps](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-usage) -- [DI Guidelines](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-guidelines) \ No newline at end of file +- [DI Guidelines](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-guidelines) + +## Setting up Injection + +`ModuleBase` has native support for service injection. +Without the need to set anything up, you can freely pass around services through the primary constructors of any implementations of this base type: + +```cs +public class MyModule(MyService service) : ModuleBase +{ + readonly MyService _service = service; +} +``` + +With access to this functionality, there are a lot of things that can be achieved, simplified and/or expanded upon. Some of this, we will explore in this guide. + +### Accessing the CommandManager + +The `CommandManager` hosts a lot of information about the execution of commands as a whole. Sometimes, it is needed to access that information in a command. +Normally, the manager is not passed along into the module, but we can inject it to achieve the same goal. + +```cs +public class MyModule(CommandManager manager) : ModuleBase +{ + readonly CommandManager _manager = manager; + readonly StringParser _parser = new StringParser(); + + [Command("search")] + public void Search([Remainder]string commandQuery) + { + var args = _parser.Parse(commandQuery); + var searches = _manager.Search(args); + + foreach (var search in searches) + { + Console.WriteLine(search.Command); + } + } +} +``` + +This command allows a user to search for commands defined by the `CommandManager`, returning zero-or-more results based on the input query. +The manager reveals more than only this, each of it's properties being accessible in this way. + +### Accessing other Modules + +All types that implement `ModuleBase` are command modules, and usually isolated to being only used to execute a command, before being disposed. +Though sometimes, a command will need different restrictions to achieve the same goal, calling upon a function in a different module to resolve the rest of execution. + +```cs +public class MyModule(MyNestedModule nested) : ModuleBase +{ + readonly MyNestedModule _nested = nested; + + [Group("do")] + public class MyNestedModule : ModuleBase + { + [Command("something")] + public void Something(bool oneStep = false) + { + if (oneStep) + Console.WriteLine("I did something in one step."); + else + Console.WriteLine("I did something in more than one step."); + } + } + + [Command("something")] + public void Something() + { + _nested.Something(true); + } +} +``` + +While this example may be niche, it shows off some of the DI features introduced by CSF.NET during command execution. + +> It is important to keep in mind that `MyNestedModule` is not prepared for it's own command, it's `Context`, `Services` and `Command` property will be null. + +## Command Scopes + +CSF is designed to treat a single command input as a *request*. Naturally, viewing it as a request brings with it the necessity to handle them as scopes. +For this, the pipeline creates a new `IServiceScope` at each call to `TryExecuteX`. When it does so, it takes a specific setting into consideration: + +### Scope Approach + +`ScopeApproach` serves as a setting to the `CommandConfiguration` that defines how scopes are created. The difference lies in how the disposing pattern is treated. This option is more elaborated in a fitting chapter of the documentation. + +> Learn more about `ScopeApproach` and it's effects [[here|Configuration#ScopeApproach]] + +### Module Disposing + +Modules are part of the scope created for them, so they naturally also implement the disposing pattern. `IDisposable` and `IAsyncDisposable` are made accessible for developers to override in their own modules, allowing to choose to implement either of these patterns manually where necessary. + +```cs +public class MyModule : ModuleBase +{ + public override void Dispose(bool disposing) + { + + } +} +``` + +> This pattern preliminarily implements a defined disposer pattern, where `disposing` represents whether or not to free managed resources, instead of only unmanaged ones. \ No newline at end of file diff --git a/docs/Home.md b/docs/Home.md index 0db2af8..5b225cd 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -3,18 +3,30 @@ For this claim to have any value, a great deal is done to help a developer write ## Available Content: -#### [[Quick Guide|Quick-Guide]] +### ⚡ [[Quick Guide|Quick-Guide]] A quick start to CSF and introduction to basic practices. -#### [[Using Preconditions|Preconditions]] +### ⚙️ [[Configuration|Configuration]] + +More about how to configure CSF, from module registration to command execution. + +### 🛑 [[Using Preconditions|Preconditions]] Define your own pre-execution checks to ensure commands will only run when they are allowed to run. -#### [[Type Conversion|Type-Conversion]] +### 📖 [[Type Conversion|Type-Conversion]] Create custom conversion for changing raw input into types. -#### [[Customization|Customization]] +### 🔍 [[Dealing with Post-Execution]] + +All about handling command results and treating custom return types. + +### 🔗 [[Exploring Customization|Customization]] + +Customize your modules and contexts to scale up your applications, or simply to reduce code repetition. + +### 💉 [[Mastering Dependency Injection||Dependency-Injection]] -Customize your modules and contexts to scale up your applications. \ No newline at end of file +Learning all there is to know about dependency injection in .NET and how it benefits CSF. \ No newline at end of file diff --git a/docs/Type-Conversion.md b/docs/Type-Conversion.md index 868e01f..712dd5a 100644 --- a/docs/Type-Conversion.md +++ b/docs/Type-Conversion.md @@ -26,7 +26,7 @@ namespace CSF.Samples } ``` -With this class defined and the method that will operate the evaluation being implemented, we can now write our code which defines the succession and failure conditions. In case of success, we also need to pass in the parsed object that the typereader expects to see returned. +With this class defined and the method that will operate the evaluation being implemented, we can now write our code which defines the succession and failure conditions. In case of success, we also need to pass in the parsed object that the TypeConverter expects to see returned. ```cs ... diff --git a/src/CSF.Core/Core/CommandConfiguration.cs b/src/CSF.Core/Core/CommandConfiguration.cs index 6bd61a1..04a6199 100644 --- a/src/CSF.Core/Core/CommandConfiguration.cs +++ b/src/CSF.Core/Core/CommandConfiguration.cs @@ -92,6 +92,26 @@ public AsyncApproach AsyncApproach } } + private ScopeApproach _scopeApproach = ScopeApproach.Default; + + /// + /// + /// + public ScopeApproach ScopeApproach + { + get + { + return _scopeApproach; + } + set + { + if (value is not ScopeApproach.ByAsyncApproach or ScopeApproach.OnlySync or ScopeApproach.OnlyAsync) + { + ThrowHelpers.ThrowInvalidOperation("ScopeApproach does not support values that exceed the provided optiosn, ranging between 0 and 2."); + } + } + } + /// /// Replaces the existing values in with a new collection. /// diff --git a/src/CSF.Core/Core/CommandManager.cs b/src/CSF.Core/Core/CommandManager.cs index 6e80d94..9e94b03 100644 --- a/src/CSF.Core/Core/CommandManager.cs +++ b/src/CSF.Core/Core/CommandManager.cs @@ -142,12 +142,12 @@ private async Task ExecuteInternalAsync(ICommandContext context, object[] args, { c++; - var match = await MatchAsync(context, scope, search, args, cancellationToken); + var match = await MatchAsync(context, scope.ServiceProvider, search, args, cancellationToken); // enter the invocation logic when a match is successful. if (match.Success) { - var result = await RunAsync(context, scope, match, cancellationToken); + var result = await RunAsync(context, scope.ServiceProvider, match, cancellationToken); await _resultHandle.TryHandleAsync(context, result, scope); @@ -160,7 +160,7 @@ private async Task ExecuteInternalAsync(ICommandContext context, object[] args, // if no searches were found, we send searchfailure. if (c is 0) { - await _resultHandle.TryHandleAsync(context, new SearchResult(new SearchException("No commands were found with the provided input.")), Services); + await _resultHandle.TryHandleAsync(context, new SearchResult(new SearchException("No commands were found with the provided input.")), scope); return; } @@ -173,17 +173,17 @@ private async Task ExecuteInternalAsync(ICommandContext context, object[] args, #endregion #region Matching - private async ValueTask MatchAsync(ICommandContext context, IServiceScope scope, SearchResult search, object[] args, CancellationToken cancellationToken) + private async ValueTask MatchAsync(ICommandContext context, IServiceProvider services, SearchResult search, object[] args, CancellationToken cancellationToken) { // check command preconditions. - var check = await CheckAsync(context, scope, search.Command, cancellationToken); + var check = await CheckAsync(context, services, search.Command, cancellationToken); // verify check success, if not, return the failure. if (!check.Success) return new(search.Command, new MatchException("Command failed to reach execution state. View inner exception for more details.", check.Exception)); // read the command parameters in right order. - var readResult = await ConvertAsync(context, scope, search, args, cancellationToken); + var readResult = await ConvertAsync(context, services, search, args, cancellationToken); // exchange the reads for result, verifying successes in the process. var reads = new object[readResult.Length]; @@ -202,13 +202,13 @@ private async ValueTask MatchAsync(ICommandContext context, IServic #endregion #region Checking - private async ValueTask CheckAsync(ICommandContext context, IServiceScope scope, CommandInfo command, CancellationToken cancellationToken) + private async ValueTask CheckAsync(ICommandContext context, IServiceProvider services, CommandInfo command, CancellationToken cancellationToken) { context.LogDebug("Attempting validations for {0}", command); foreach (var precon in command.Preconditions) { - var result = await precon.EvaluateAsync(context, scope.ServiceProvider, command, cancellationToken); + var result = await precon.EvaluateAsync(context, services, command, cancellationToken); if (!result.Success) return result; @@ -219,7 +219,7 @@ private async ValueTask CheckAsync(ICommandContext context, IServic #endregion #region Reading - private async ValueTask ConvertAsync(ICommandContext context, IServiceScope scope, SearchResult search, object[] args, CancellationToken cancellationToken) + private async ValueTask ConvertAsync(ICommandContext context, IServiceProvider services, SearchResult search, object[] args, CancellationToken cancellationToken) { context.LogDebug("Attempting argument conversion for {0}", search.Command); @@ -232,15 +232,15 @@ private async ValueTask ConvertAsync(ICommandContext context, I // check if input equals command length. if (search.Command.MaxLength == length) - return await search.Command.Arguments.RecursiveConvertAsync(context, scope, args[^length..], 0, cancellationToken); + return await search.Command.Arguments.RecursiveConvertAsync(context, services, args[^length..], 0, cancellationToken); // check if input is longer than command, but remainder to concatenate. if (search.Command.MaxLength <= length && search.Command.HasRemainder) - return await search.Command.Arguments.RecursiveConvertAsync(context, scope, args[^length..], 0, cancellationToken); + return await search.Command.Arguments.RecursiveConvertAsync(context, services, args[^length..], 0, cancellationToken); // check if input is shorter than command, but optional parameters to replace. if (search.Command.MaxLength > length && search.Command.MinLength <= length) - return await search.Command.Arguments.RecursiveConvertAsync(context, scope, args[^length..], 0, cancellationToken); + return await search.Command.Arguments.RecursiveConvertAsync(context, services, args[^length..], 0, cancellationToken); // input is too long or too short. return []; @@ -248,7 +248,7 @@ private async ValueTask ConvertAsync(ICommandContext context, I #endregion #region Running - private async ValueTask RunAsync(ICommandContext context, IServiceScope scope, MatchResult match, CancellationToken cancellationToken) + private async ValueTask RunAsync(ICommandContext context, IServiceProvider services, MatchResult match, CancellationToken cancellationToken) { try { @@ -258,11 +258,11 @@ private async ValueTask RunAsync(ICommandContext context, IServiceSco var module = targetInstance != null ? targetInstance as ModuleBase - : ActivatorUtilities.CreateInstance(scope.ServiceProvider, match.Command.Module.Type) as ModuleBase; + : ActivatorUtilities.CreateInstance(services, match.Command.Module.Type) as ModuleBase; module.Context = context; module.Command = match.Command; - module.Services = scope.ServiceProvider; + module.Services = services; await module.BeforeExecuteAsync(cancellationToken); diff --git a/src/CSF.Core/Core/Configuration/AsyncApproach.cs b/src/CSF.Core/Core/Configuration/AsyncApproach.cs index df90d6b..5020bb1 100644 --- a/src/CSF.Core/Core/Configuration/AsyncApproach.cs +++ b/src/CSF.Core/Core/Configuration/AsyncApproach.cs @@ -16,7 +16,7 @@ namespace CSF.Core /// /// is a setting to be treated with care. /// Instead of waiting for the full execution before returning control, the execution will return immediately after the entrypoint is called, slipping thread for the rest of execution. - /// When more than one input source is expected to be handled, this is generally the adviced method of execution. + /// When more than one input source is expected to be handled, this is generally the advised method of execution. /// /// /// diff --git a/src/CSF.Core/Core/Configuration/ScopeApproach.cs b/src/CSF.Core/Core/Configuration/ScopeApproach.cs new file mode 100644 index 0000000..815f1de --- /dev/null +++ b/src/CSF.Core/Core/Configuration/ScopeApproach.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSF.Core +{ + /// + /// + /// + public enum ScopeApproach + { + /// + /// + /// + Default = ByAsyncApproach, + + /// + /// + /// +#pragma warning disable CA1069 // Enums values should not be duplicated + ByAsyncApproach = 0, +#pragma warning restore CA1069 // Enums values should not be duplicated + + /// + /// + /// + OnlySync = 1, + + /// + /// + /// + OnlyAsync = 2, + } +} diff --git a/src/CSF.Core/Core/Execution/ModuleBase.cs b/src/CSF.Core/Core/Execution/ModuleBase.cs index 23d2ac8..078f352 100644 --- a/src/CSF.Core/Core/Execution/ModuleBase.cs +++ b/src/CSF.Core/Core/Execution/ModuleBase.cs @@ -28,8 +28,10 @@ public abstract class ModuleBase : ModuleBase /// /// All derived types must be known in to be discoverable and automatically registered during the creation of a . /// - public abstract class ModuleBase + public abstract class ModuleBase : IDisposable, IAsyncDisposable { + private bool disposedValue; + /// /// Gets the command context containing metadata and logging access for the command currently in scope. /// @@ -81,7 +83,7 @@ public virtual bool HandleUnknownInvocationResult(object value) return false; } - internal virtual async Task ResolveInvocationResultAsync(object value) + internal async Task ResolveInvocationResultAsync(object value) { switch (value) { @@ -103,5 +105,49 @@ internal virtual async Task ResolveInvocationResultAsync(object value } } } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// Defines whether to dispose managed objects or not. + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// Defines whether to dispose managed objects or not. + /// A holding the state of disposing. + protected virtual ValueTask DisposeAsync(bool disposing) + { + Dispose(disposing: disposing); + return ValueTask.CompletedTask; + } + + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + public async ValueTask DisposeAsync() + { + await DisposeAsync(disposing: true); + GC.SuppressFinalize(this); + } } } diff --git a/src/CSF.Core/Helpers/ExecutionHelpers.cs b/src/CSF.Core/Helpers/ExecutionHelpers.cs index 579ce02..2414a21 100644 --- a/src/CSF.Core/Helpers/ExecutionHelpers.cs +++ b/src/CSF.Core/Helpers/ExecutionHelpers.cs @@ -32,9 +32,9 @@ public static IEnumerable RecursiveSearch(this IEnumerable RecursiveConvertAsync(this IArgument[] param, ICommandContext context, IServiceScope scope, object[] args, int index, CancellationToken cancellationToken) + public static async Task RecursiveConvertAsync(this IArgument[] param, ICommandContext context, IServiceProvider services, object[] args, int index, CancellationToken cancellationToken) { - static async ValueTask ConvertAsync(IArgument param, ICommandContext context, IServiceScope scope, object arg, CancellationToken cancellationToken) + static async ValueTask ConvertAsync(IArgument param, ICommandContext context, IServiceProvider services, object arg, CancellationToken cancellationToken) { if (param.IsNullable && arg is null or "null" or "nothing") return new(arg); @@ -42,7 +42,7 @@ static async ValueTask ConvertAsync(IArgument param, ICommandCont if (param.Type == typeof(string) || param.Type == typeof(object)) return new(arg); - return await param.Converter.ObjectEvaluateAsync(context, scope.ServiceProvider, param, arg, cancellationToken); + return await param.Converter.ObjectEvaluateAsync(context, services, param, arg, cancellationToken); } var results = new ConvertResult[param.Length]; @@ -57,7 +57,7 @@ static async ValueTask ConvertAsync(IArgument param, ICommandCont if (parameter.Type == typeof(string)) results[i] = new(input); else - results[i] = await ConvertAsync(parameter, context, scope, input, cancellationToken); + results[i] = await ConvertAsync(parameter, context, services, input, cancellationToken); break; } @@ -70,7 +70,7 @@ static async ValueTask ConvertAsync(IArgument param, ICommandCont if (parameter is ComplexArgumentInfo complex) { - var result = await complex.Arguments.RecursiveConvertAsync(context, scope, args, index, cancellationToken); + var result = await complex.Arguments.RecursiveConvertAsync(context, services, args, index, cancellationToken); index += result.Length; @@ -89,7 +89,7 @@ static async ValueTask ConvertAsync(IArgument param, ICommandCont continue; } - results[i] = await ConvertAsync(parameter, context, scope, args[index], cancellationToken); + results[i] = await ConvertAsync(parameter, context, services, args[index], cancellationToken); index++; } diff --git a/src/CSF.Core/Helpers/ServiceHelpers.cs b/src/CSF.Core/Helpers/ServiceHelpers.cs index fc43712..cd227c4 100644 --- a/src/CSF.Core/Helpers/ServiceHelpers.cs +++ b/src/CSF.Core/Helpers/ServiceHelpers.cs @@ -91,12 +91,23 @@ public static IServiceCollection AddModules(this IServiceCollection collection, [EditorBrowsable(EditorBrowsableState.Never)] public static IServiceScope CreateScope(this IServiceProvider provider, CommandConfiguration configuration) { - if (configuration.AsyncApproach == AsyncApproach.Await) + switch (configuration.ScopeApproach) { - return provider.CreateScope(); + case ScopeApproach.ByAsyncApproach when configuration.AsyncApproach is AsyncApproach.Discard: + case ScopeApproach.OnlyAsync: + { + return provider.CreateAsyncScope(); + } + case ScopeApproach.ByAsyncApproach when configuration.AsyncApproach is AsyncApproach.Await: + case ScopeApproach.OnlySync: + { + return provider.CreateScope(); + } + default: + { + throw new NotImplementedException(); + } } - - return provider.CreateAsyncScope(); } } }