Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature: Add a way to invoke a command specifying optional values by …
…name (#1123) * Add NamedArgumentTypeAttribute * Add NamedArgumentTypeReader * Fix superflous empty line. * Fix logic for quoted arguments * Throw an exception with a tailored message. * Add a catch to wrap parsing/input errors * Trim potential excess whitespace * Fix an off-by-one * Support to read an IEnumerable property * Add a doc * Add assertion for the collection test
- Loading branch information
Showing
7 changed files
with
369 additions
and
9 deletions.
There are no files selected for viewing
11 changes: 11 additions & 0 deletions
11
src/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
using System; | ||
|
||
namespace Discord.Commands | ||
{ | ||
/// <summary> | ||
/// Instructs the command system to treat command paramters of this type | ||
/// as a collection of named arguments matching to its properties. | ||
/// </summary> | ||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] | ||
public sealed class NamedArgumentTypeAttribute : Attribute { } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
191 changes: 191 additions & 0 deletions
191
src/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
using System; | ||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Linq; | ||
using System.Reflection; | ||
using System.Threading.Tasks; | ||
|
||
namespace Discord.Commands | ||
{ | ||
internal sealed class NamedArgumentTypeReader<T> : TypeReader | ||
where T : class, new() | ||
{ | ||
private static readonly IReadOnlyDictionary<string, PropertyInfo> _tProps = typeof(T).GetTypeInfo().DeclaredProperties | ||
.Where(p => p.SetMethod != null && p.SetMethod.IsPublic && !p.SetMethod.IsStatic) | ||
.ToImmutableDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase); | ||
|
||
private readonly CommandService _commands; | ||
|
||
public NamedArgumentTypeReader(CommandService commands) | ||
{ | ||
_commands = commands; | ||
} | ||
|
||
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services) | ||
{ | ||
var result = new T(); | ||
var state = ReadState.LookingForParameter; | ||
int beginRead = 0, currentRead = 0; | ||
|
||
while (state != ReadState.End) | ||
{ | ||
try | ||
{ | ||
var prop = Read(out var arg); | ||
var propVal = await ReadArgumentAsync(prop, arg).ConfigureAwait(false); | ||
if (propVal != null) | ||
prop.SetMethod.Invoke(result, new[] { propVal }); | ||
else | ||
return TypeReaderResult.FromError(CommandError.ParseFailed, $"Could not parse the argument for the parameter '{prop.Name}' as type '{prop.PropertyType}'."); | ||
} | ||
catch (Exception ex) | ||
{ | ||
//TODO: use the Exception overload after a rebase on latest | ||
return TypeReaderResult.FromError(CommandError.Exception, ex.Message); | ||
} | ||
} | ||
|
||
return TypeReaderResult.FromSuccess(result); | ||
|
||
PropertyInfo Read(out string arg) | ||
{ | ||
string currentParam = null; | ||
char match = '\0'; | ||
|
||
for (; currentRead < input.Length; currentRead++) | ||
{ | ||
var currentChar = input[currentRead]; | ||
switch (state) | ||
{ | ||
case ReadState.LookingForParameter: | ||
if (Char.IsWhiteSpace(currentChar)) | ||
continue; | ||
else | ||
{ | ||
beginRead = currentRead; | ||
state = ReadState.InParameter; | ||
} | ||
break; | ||
case ReadState.InParameter: | ||
if (currentChar != ':') | ||
continue; | ||
else | ||
{ | ||
currentParam = input.Substring(beginRead, currentRead - beginRead); | ||
state = ReadState.LookingForArgument; | ||
} | ||
break; | ||
case ReadState.LookingForArgument: | ||
if (Char.IsWhiteSpace(currentChar)) | ||
continue; | ||
else | ||
{ | ||
beginRead = currentRead; | ||
state = (QuotationAliasUtils.GetDefaultAliasMap.TryGetValue(currentChar, out match)) | ||
? ReadState.InQuotedArgument | ||
: ReadState.InArgument; | ||
} | ||
break; | ||
case ReadState.InArgument: | ||
if (!Char.IsWhiteSpace(currentChar)) | ||
continue; | ||
else | ||
return GetPropAndValue(out arg); | ||
case ReadState.InQuotedArgument: | ||
if (currentChar != match) | ||
continue; | ||
else | ||
return GetPropAndValue(out arg); | ||
} | ||
} | ||
|
||
if (currentParam == null) | ||
throw new InvalidOperationException("No parameter name was read."); | ||
|
||
return GetPropAndValue(out arg); | ||
|
||
PropertyInfo GetPropAndValue(out string argv) | ||
{ | ||
bool quoted = state == ReadState.InQuotedArgument; | ||
state = (currentRead == (quoted ? input.Length - 1 : input.Length)) | ||
? ReadState.End | ||
: ReadState.LookingForParameter; | ||
|
||
if (quoted) | ||
{ | ||
argv = input.Substring(beginRead + 1, currentRead - beginRead - 1).Trim(); | ||
currentRead++; | ||
} | ||
else | ||
argv = input.Substring(beginRead, currentRead - beginRead); | ||
|
||
return _tProps[currentParam]; | ||
} | ||
} | ||
|
||
async Task<object> ReadArgumentAsync(PropertyInfo prop, string arg) | ||
{ | ||
var elemType = prop.PropertyType; | ||
bool isCollection = false; | ||
if (elemType.GetTypeInfo().IsGenericType && elemType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) | ||
{ | ||
elemType = prop.PropertyType.GenericTypeArguments[0]; | ||
isCollection = true; | ||
} | ||
|
||
var overridden = prop.GetCustomAttribute<OverrideTypeReaderAttribute>(); | ||
var reader = (overridden != null) | ||
? ModuleClassBuilder.GetTypeReader(_commands, elemType, overridden.TypeReader, services) | ||
: (_commands.GetDefaultTypeReader(elemType) | ||
?? _commands.GetTypeReaders(elemType).FirstOrDefault().Value); | ||
|
||
if (reader != null) | ||
{ | ||
if (isCollection) | ||
{ | ||
var method = _readMultipleMethod.MakeGenericMethod(elemType); | ||
var task = (Task<IEnumerable>)method.Invoke(null, new object[] { reader, context, arg.Split(','), services }); | ||
return await task.ConfigureAwait(false); | ||
} | ||
else | ||
return await ReadSingle(reader, context, arg, services).ConfigureAwait(false); | ||
} | ||
return null; | ||
} | ||
} | ||
|
||
private static async Task<object> ReadSingle(TypeReader reader, ICommandContext context, string arg, IServiceProvider services) | ||
{ | ||
var readResult = await reader.ReadAsync(context, arg, services).ConfigureAwait(false); | ||
return (readResult.IsSuccess) | ||
? readResult.BestMatch | ||
: null; | ||
} | ||
private static async Task<IEnumerable> ReadMultiple<TObj>(TypeReader reader, ICommandContext context, IEnumerable<string> args, IServiceProvider services) | ||
{ | ||
var objs = new List<TObj>(); | ||
foreach (var arg in args) | ||
{ | ||
var read = await ReadSingle(reader, context, arg.Trim(), services).ConfigureAwait(false); | ||
if (read != null) | ||
objs.Add((TObj)read); | ||
} | ||
return objs.ToImmutableArray(); | ||
} | ||
private static readonly MethodInfo _readMultipleMethod = typeof(NamedArgumentTypeReader<T>) | ||
.GetTypeInfo() | ||
.DeclaredMethods | ||
.Single(m => m.IsPrivate && m.IsStatic && m.Name == nameof(ReadMultiple)); | ||
|
||
private enum ReadState | ||
{ | ||
LookingForParameter, | ||
InParameter, | ||
LookingForArgument, | ||
InArgument, | ||
InQuotedArgument, | ||
End | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.