diff --git a/src/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs b/src/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs
new file mode 100644
index 000000000..a43286110
--- /dev/null
+++ b/src/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Discord.Commands
+{
+ ///
+ /// Instructs the command system to treat command paramters of this type
+ /// as a collection of named arguments matching to its properties.
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+ public sealed class NamedArgumentTypeAttribute : Attribute { }
+}
diff --git a/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs
index 85f5df10e..a44dcb6e4 100644
--- a/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs
+++ b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs
@@ -1,5 +1,4 @@
using System;
-
using System.Reflection;
namespace Discord.Commands
@@ -27,8 +26,8 @@ namespace Discord.Commands
/// => ReplyAsync(time);
///
///
- [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
- public class OverrideTypeReaderAttribute : Attribute
+ [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ public sealed class OverrideTypeReaderAttribute : Attribute
{
private static readonly TypeInfo TypeReaderTypeInfo = typeof(TypeReader).GetTypeInfo();
diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
index 3b71c87b0..aec8dcbe3 100644
--- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
+++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
@@ -280,7 +280,7 @@ namespace Discord.Commands
}
}
- private static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services)
+ internal static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services)
{
var readers = service.GetTypeReaders(paramType);
TypeReader reader = null;
diff --git a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs
index 8a59c247c..4ad5bfac0 100644
--- a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs
+++ b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs
@@ -56,11 +56,36 @@ namespace Discord.Commands.Builders
private TypeReader GetReader(Type type)
{
- var readers = Command.Module.Service.GetTypeReaders(type);
+ var commands = Command.Module.Service;
+ if (type.GetTypeInfo().GetCustomAttribute() != null)
+ {
+ IsRemainder = true;
+ var reader = commands.GetTypeReaders(type)?.FirstOrDefault().Value;
+ if (reader == null)
+ {
+ Type readerType;
+ try
+ {
+ readerType = typeof(NamedArgumentTypeReader<>).MakeGenericType(new[] { type });
+ }
+ catch (ArgumentException ex)
+ {
+ throw new InvalidOperationException($"Parameter type '{type.Name}' for command '{Command.Name}' must be a class with a public parameterless constructor to use as a NamedArgumentType.", ex);
+ }
+
+ reader = (TypeReader)Activator.CreateInstance(readerType, new[] { commands });
+ commands.AddTypeReader(type, reader);
+ }
+
+ return reader;
+ }
+
+
+ var readers = commands.GetTypeReaders(type);
if (readers != null)
return readers.FirstOrDefault().Value;
else
- return Command.Module.Service.GetDefaultTypeReader(type);
+ return commands.GetDefaultTypeReader(type);
}
public ParameterBuilder WithSummary(string summary)
diff --git a/src/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs b/src/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs
new file mode 100644
index 000000000..01559293f
--- /dev/null
+++ b/src/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs
@@ -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 : TypeReader
+ where T : class, new()
+ {
+ private static readonly IReadOnlyDictionary _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 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