* Add grouping of preconditions to allow for flexible precondition logic. * Fix checking Module Preconditions twice (and none of the command's own) * Fix command preconditions group 0 looping over every other precondition anyway #whoopsies * Use custom message when a non-zero Precondition Group fails. * Fix doc comment rendering. * Refactor loops into local function * Considering a new result type * Switch to IReadOnlyCollection<T> and fix compiler errors * Revert PreconditionResult -> IResult in return types - Change PreconditionResult to a class that PreconditionGroupResult inherits. * Feedback on property name. * Change grouping type int -> string * Explicitly use an ordinal StringComparer * Full stops on error messages * Remove some sillyness. * Remove unneeded using.tags/1.0
| @@ -6,6 +6,13 @@ namespace Discord.Commands | |||||
| [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] | ||||
| public abstract class PreconditionAttribute : Attribute | public abstract class PreconditionAttribute : Attribute | ||||
| { | { | ||||
| /// <summary> | |||||
| /// Specify a group that this precondition belongs to. Preconditions of the same group require only one | |||||
| /// of the preconditions to pass in order to be successful (A || B). Specifying <see cref="Group"/> = <see cref="null"/> | |||||
| /// or not at all will require *all* preconditions to pass, just like normal (A && B). | |||||
| /// </summary> | |||||
| public string Group { get; set; } = null; | |||||
| public abstract Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services); | public abstract Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services); | ||||
| } | } | ||||
| } | } | ||||
| @@ -18,7 +18,7 @@ namespace Discord.Commands | |||||
| public Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) | public Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) | ||||
| => Command.CheckPreconditionsAsync(context, services); | => Command.CheckPreconditionsAsync(context, services); | ||||
| public Task<ParseResult> ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult? preconditionResult = null) | |||||
| public Task<ParseResult> ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult preconditionResult = null) | |||||
| => Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult); | => Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult); | ||||
| public Task<ExecuteResult> ExecuteAsync(ICommandContext context, IEnumerable<object> argList, IEnumerable<object> paramList, IServiceProvider services) | public Task<ExecuteResult> ExecuteAsync(ICommandContext context, IEnumerable<object> argList, IEnumerable<object> paramList, IServiceProvider services) | ||||
| => Command.ExecuteAsync(context, argList, paramList, services); | => Command.ExecuteAsync(context, argList, paramList, services); | ||||
| @@ -68,29 +68,49 @@ namespace Discord.Commands | |||||
| { | { | ||||
| services = services ?? EmptyServiceProvider.Instance; | services = services ?? EmptyServiceProvider.Instance; | ||||
| foreach (PreconditionAttribute precondition in Module.Preconditions) | |||||
| async Task<PreconditionGroupResult> CheckGroups(IEnumerable<PreconditionAttribute> preconditions, string type) | |||||
| { | { | ||||
| var result = await precondition.CheckPermissions(context, this, services).ConfigureAwait(false); | |||||
| if (!result.IsSuccess) | |||||
| return result; | |||||
| foreach (IGrouping<string, PreconditionAttribute> preconditionGroup in preconditions.GroupBy(p => p.Group, StringComparer.Ordinal)) | |||||
| { | |||||
| if (preconditionGroup.Key == null) | |||||
| { | |||||
| foreach (PreconditionAttribute precondition in preconditionGroup) | |||||
| { | |||||
| var result = await precondition.CheckPermissions(context, this, services).ConfigureAwait(false); | |||||
| if (!result.IsSuccess) | |||||
| return PreconditionGroupResult.FromError($"{type} default precondition group failed.", new[] { result }); | |||||
| } | |||||
| } | |||||
| else | |||||
| { | |||||
| var results = new List<PreconditionResult>(); | |||||
| foreach (PreconditionAttribute precondition in preconditionGroup) | |||||
| results.Add(await precondition.CheckPermissions(context, this, services).ConfigureAwait(false)); | |||||
| if (!results.Any(p => p.IsSuccess)) | |||||
| return PreconditionGroupResult.FromError($"{type} precondition group {preconditionGroup.Key} failed.", results); | |||||
| } | |||||
| } | |||||
| return PreconditionGroupResult.FromSuccess(); | |||||
| } | } | ||||
| foreach (PreconditionAttribute precondition in Preconditions) | |||||
| { | |||||
| var result = await precondition.CheckPermissions(context, this, services).ConfigureAwait(false); | |||||
| if (!result.IsSuccess) | |||||
| return result; | |||||
| } | |||||
| var moduleResult = await CheckGroups(Module.Preconditions, "Module"); | |||||
| if (!moduleResult.IsSuccess) | |||||
| return moduleResult; | |||||
| var commandResult = await CheckGroups(Preconditions, "Command"); | |||||
| if (!commandResult.IsSuccess) | |||||
| return commandResult; | |||||
| return PreconditionResult.FromSuccess(); | return PreconditionResult.FromSuccess(); | ||||
| } | } | ||||
| public async Task<ParseResult> ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, PreconditionResult? preconditionResult = null) | |||||
| public async Task<ParseResult> ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, PreconditionResult preconditionResult = null) | |||||
| { | { | ||||
| if (!searchResult.IsSuccess) | if (!searchResult.IsSuccess) | ||||
| return ParseResult.FromError(searchResult); | return ParseResult.FromError(searchResult); | ||||
| if (preconditionResult != null && !preconditionResult.Value.IsSuccess) | |||||
| return ParseResult.FromError(preconditionResult.Value); | |||||
| if (preconditionResult != null && !preconditionResult.IsSuccess) | |||||
| return ParseResult.FromError(preconditionResult); | |||||
| string input = searchResult.Text.Substring(startIndex); | string input = searchResult.Text.Substring(startIndex); | ||||
| return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false); | return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false); | ||||
| @@ -0,0 +1,27 @@ | |||||
| using System.Collections.Generic; | |||||
| using System.Diagnostics; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||||
| public class PreconditionGroupResult : PreconditionResult | |||||
| { | |||||
| public IReadOnlyCollection<PreconditionResult> PreconditionResults { get; } | |||||
| protected PreconditionGroupResult(CommandError? error, string errorReason, ICollection<PreconditionResult> preconditions) | |||||
| : base(error, errorReason) | |||||
| { | |||||
| PreconditionResults = (preconditions ?? new List<PreconditionResult>(0)).ToReadOnlyCollection(); | |||||
| } | |||||
| public static new PreconditionGroupResult FromSuccess() | |||||
| => new PreconditionGroupResult(null, null, null); | |||||
| public static PreconditionGroupResult FromError(string reason, ICollection<PreconditionResult> preconditions) | |||||
| => new PreconditionGroupResult(CommandError.UnmetPrecondition, reason, preconditions); | |||||
| public static new PreconditionGroupResult FromError(IResult result) //needed? | |||||
| => new PreconditionGroupResult(result.Error, result.ErrorReason, null); | |||||
| public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | |||||
| private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | |||||
| } | |||||
| } | |||||
| @@ -3,14 +3,14 @@ | |||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| { | { | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public struct PreconditionResult : IResult | |||||
| public class PreconditionResult : IResult | |||||
| { | { | ||||
| public CommandError? Error { get; } | public CommandError? Error { get; } | ||||
| public string ErrorReason { get; } | public string ErrorReason { get; } | ||||
| public bool IsSuccess => !Error.HasValue; | public bool IsSuccess => !Error.HasValue; | ||||
| private PreconditionResult(CommandError? error, string errorReason) | |||||
| protected PreconditionResult(CommandError? error, string errorReason) | |||||
| { | { | ||||
| Error = error; | Error = error; | ||||
| ErrorReason = errorReason; | ErrorReason = errorReason; | ||||