Parameter preconditions and typereader overridingtags/1.0-rc
@@ -0,0 +1,22 @@ | |||
using System; | |||
using System.Reflection; | |||
namespace Discord.Commands | |||
{ | |||
[AttributeUsage(AttributeTargets.Parameter)] | |||
public class OverrideTypeReaderAttribute : Attribute | |||
{ | |||
private static readonly TypeInfo _typeReaderTypeInfo = typeof(TypeReader).GetTypeInfo(); | |||
public Type TypeReader { get; } | |||
public OverrideTypeReaderAttribute(Type overridenTypeReader) | |||
{ | |||
if (!_typeReaderTypeInfo.IsAssignableFrom(overridenTypeReader.GetTypeInfo())) | |||
throw new ArgumentException($"{nameof(overridenTypeReader)} must inherit from {nameof(TypeReader)}"); | |||
TypeReader = overridenTypeReader; | |||
} | |||
} | |||
} |
@@ -0,0 +1,11 @@ | |||
using System; | |||
using System.Threading.Tasks; | |||
namespace Discord.Commands | |||
{ | |||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)] | |||
public abstract class ParameterPreconditionAttribute : Attribute | |||
{ | |||
public abstract Task<PreconditionResult> CheckPermissions(CommandContext context, ParameterInfo parameter, object value, IDependencyMap map); | |||
} | |||
} |
@@ -182,6 +182,10 @@ namespace Discord.Commands | |||
// TODO: C#7 type switch | |||
if (attribute is SummaryAttribute) | |||
builder.Summary = (attribute as SummaryAttribute).Text; | |||
else if (attribute is OverrideTypeReaderAttribute) | |||
builder.TypeReader = GetTypeReader(service, paramType, (attribute as OverrideTypeReaderAttribute).TypeReader); | |||
else if (attribute is ParameterPreconditionAttribute) | |||
builder.AddPrecondition(attribute as ParameterPreconditionAttribute); | |||
else if (attribute is ParamArrayAttribute) | |||
{ | |||
builder.IsMultiple = true; | |||
@@ -196,23 +200,42 @@ namespace Discord.Commands | |||
} | |||
} | |||
var reader = service.GetTypeReader(paramType); | |||
if (reader == null) | |||
if (builder.TypeReader == null) | |||
{ | |||
var paramTypeInfo = paramType.GetTypeInfo(); | |||
if (paramTypeInfo.IsEnum) | |||
{ | |||
reader = EnumTypeReader.GetReader(paramType); | |||
service.AddTypeReader(paramType, reader); | |||
} | |||
else | |||
var reader = service.GetDefaultTypeReader(paramType); | |||
if (reader == null) | |||
{ | |||
throw new InvalidOperationException($"{paramType.FullName} is not supported as a command parameter, are you missing a TypeReader?"); | |||
var paramTypeInfo = paramType.GetTypeInfo(); | |||
if (paramTypeInfo.IsEnum) | |||
{ | |||
reader = EnumTypeReader.GetReader(paramType); | |||
service.AddTypeReader(paramType, reader); | |||
} | |||
else | |||
{ | |||
throw new InvalidOperationException($"{paramType.FullName} is not supported as a command parameter, are you missing a TypeReader?"); | |||
} | |||
} | |||
builder.ParameterType = paramType; | |||
builder.TypeReader = reader; | |||
} | |||
} | |||
private static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType) | |||
{ | |||
var readers = service.GetTypeReaders(paramType); | |||
TypeReader reader = null; | |||
if (readers != null) | |||
if (readers.TryGetValue(typeReaderType, out reader)) | |||
return reader; | |||
//could not find any registered type reader: try to create one | |||
reader = ReflectionUtils.CreateObject<TypeReader>(typeReaderType.GetTypeInfo(), service, DependencyMap.Empty); | |||
service.AddTypeReader(paramType, reader); | |||
builder.ParameterType = paramType; | |||
builder.TypeReader = reader; | |||
return reader; | |||
} | |||
private static bool IsValidModuleDefinition(TypeInfo typeInfo) | |||
@@ -1,10 +1,15 @@ | |||
using System; | |||
using System.Linq; | |||
using System.Reflection; | |||
using System.Collections.Generic; | |||
namespace Discord.Commands.Builders | |||
{ | |||
public class ParameterBuilder | |||
{ | |||
private readonly List<ParameterPreconditionAttribute> _preconditions; | |||
public CommandBuilder Command { get; } | |||
public string Name { get; internal set; } | |||
public Type ParameterType { get; internal set; } | |||
@@ -16,16 +21,20 @@ namespace Discord.Commands.Builders | |||
public object DefaultValue { get; set; } | |||
public string Summary { get; set; } | |||
public IReadOnlyList<ParameterPreconditionAttribute> Preconditions => _preconditions; | |||
//Automatic | |||
internal ParameterBuilder(CommandBuilder command) | |||
{ | |||
_preconditions = new List<ParameterPreconditionAttribute>(); | |||
Command = command; | |||
} | |||
//User-defined | |||
internal ParameterBuilder(CommandBuilder command, string name, Type type) | |||
: this(command) | |||
{ | |||
Preconditions.NotNull(name, nameof(name)); | |||
Discord.Preconditions.NotNull(name, nameof(name)); | |||
Name = name; | |||
SetType(type); | |||
@@ -33,7 +42,11 @@ namespace Discord.Commands.Builders | |||
internal void SetType(Type type) | |||
{ | |||
TypeReader = Command.Module.Service.GetTypeReader(type); | |||
var readers = Command.Module.Service.GetTypeReaders(type); | |||
if (readers == null) | |||
throw new InvalidOperationException($"{type} does not have a TypeReader registered for it"); | |||
TypeReader = readers.FirstOrDefault().Value; | |||
if (type.GetTypeInfo().IsValueType) | |||
DefaultValue = Activator.CreateInstance(type); | |||
@@ -49,7 +62,7 @@ namespace Discord.Commands.Builders | |||
} | |||
public ParameterBuilder WithDefault(object defaultValue) | |||
{ | |||
DefaultValue = defaultValue; | |||
DefaultValue = defaultValue; | |||
return this; | |||
} | |||
public ParameterBuilder WithIsOptional(bool isOptional) | |||
@@ -68,6 +81,12 @@ namespace Discord.Commands.Builders | |||
return this; | |||
} | |||
public ParameterBuilder AddPrecondition(ParameterPreconditionAttribute precondition) | |||
{ | |||
_preconditions.Add(precondition); | |||
return this; | |||
} | |||
internal ParameterInfo Build(CommandInfo info) | |||
{ | |||
if (TypeReader == null) | |||
@@ -15,7 +15,8 @@ namespace Discord.Commands | |||
{ | |||
private readonly SemaphoreSlim _moduleLock; | |||
private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs; | |||
private readonly ConcurrentDictionary<Type, TypeReader> _typeReaders; | |||
private readonly ConcurrentDictionary<Type, ConcurrentDictionary<Type, TypeReader>> _typeReaders; | |||
private readonly ConcurrentDictionary<Type, TypeReader> _defaultTypeReaders; | |||
private readonly ConcurrentBag<ModuleInfo> _moduleDefs; | |||
private readonly CommandMap _map; | |||
@@ -24,6 +25,7 @@ namespace Discord.Commands | |||
public IEnumerable<ModuleInfo> Modules => _moduleDefs.Select(x => x); | |||
public IEnumerable<CommandInfo> Commands => _moduleDefs.SelectMany(x => x.Commands); | |||
public ILookup<Type, TypeReader> TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new {y.Key, y.Value})).ToLookup(x => x.Key, x => x.Value); | |||
public CommandService() : this(new CommandServiceConfig()) { } | |||
public CommandService(CommandServiceConfig config) | |||
@@ -32,7 +34,9 @@ namespace Discord.Commands | |||
_typedModuleDefs = new ConcurrentDictionary<Type, ModuleInfo>(); | |||
_moduleDefs = new ConcurrentBag<ModuleInfo>(); | |||
_map = new CommandMap(); | |||
_typeReaders = new ConcurrentDictionary<Type, TypeReader> | |||
_typeReaders = new ConcurrentDictionary<Type, ConcurrentDictionary<Type, TypeReader>>(); | |||
_defaultTypeReaders = new ConcurrentDictionary<Type, TypeReader> | |||
{ | |||
[typeof(bool)] = new SimpleTypeReader<bool>(), | |||
[typeof(char)] = new SimpleTypeReader<char>(), | |||
@@ -50,7 +54,7 @@ namespace Discord.Commands | |||
[typeof(decimal)] = new SimpleTypeReader<decimal>(), | |||
[typeof(DateTime)] = new SimpleTypeReader<DateTime>(), | |||
[typeof(DateTimeOffset)] = new SimpleTypeReader<DateTimeOffset>(), | |||
[typeof(TimeSpan)] = new SimpleTypeReader<TimeSpan>(), | |||
[typeof(IMessage)] = new MessageTypeReader<IMessage>(), | |||
[typeof(IUserMessage)] = new MessageTypeReader<IUserMessage>(), | |||
[typeof(IChannel)] = new ChannelTypeReader<IChannel>(), | |||
@@ -196,16 +200,25 @@ namespace Discord.Commands | |||
//Type Readers | |||
public void AddTypeReader<T>(TypeReader reader) | |||
{ | |||
_typeReaders[typeof(T)] = reader; | |||
var readers = _typeReaders.GetOrAdd(typeof(T), x => new ConcurrentDictionary<Type, TypeReader>()); | |||
readers[reader.GetType()] = reader; | |||
} | |||
public void AddTypeReader(Type type, TypeReader reader) | |||
{ | |||
_typeReaders[type] = reader; | |||
var readers = _typeReaders.GetOrAdd(type, x=> new ConcurrentDictionary<Type, TypeReader>()); | |||
readers[reader.GetType()] = reader; | |||
} | |||
internal IDictionary<Type, TypeReader> GetTypeReaders(Type type) | |||
{ | |||
ConcurrentDictionary<Type, TypeReader> definedTypeReaders; | |||
if (_typeReaders.TryGetValue(type, out definedTypeReaders)) | |||
return definedTypeReaders; | |||
return null; | |||
} | |||
internal TypeReader GetTypeReader(Type type) | |||
internal TypeReader GetDefaultTypeReader(Type type) | |||
{ | |||
TypeReader reader; | |||
if (_typeReaders.TryGetValue(type, out reader)) | |||
if (_defaultTypeReaders.TryGetValue(type, out reader)) | |||
return reader; | |||
return null; | |||
} | |||
@@ -137,7 +137,15 @@ namespace Discord.Commands | |||
try | |||
{ | |||
var args = GenerateArgs(argList, paramList); | |||
object[] args = GenerateArgs(argList, paramList); | |||
foreach (var parameter in Parameters) | |||
{ | |||
var result = await parameter.CheckPreconditionsAsync(context, args, map).ConfigureAwait(false); | |||
if (!result.IsSuccess) | |||
return ExecuteResult.FromError(result); | |||
} | |||
switch (RunMode) | |||
{ | |||
case RunMode.Sync: //Always sync | |||
@@ -1,5 +1,7 @@ | |||
using System; | |||
using System.Linq; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.Threading.Tasks; | |||
using Discord.Commands.Builders; | |||
@@ -10,6 +12,17 @@ namespace Discord.Commands | |||
{ | |||
private readonly TypeReader _reader; | |||
public CommandInfo Command { get; } | |||
public string Name { get; } | |||
public string Summary { get; } | |||
public bool IsOptional { get; } | |||
public bool IsRemainder { get; } | |||
public bool IsMultiple { get; } | |||
public Type Type { get; } | |||
public object DefaultValue { get; } | |||
public IReadOnlyList<ParameterPreconditionAttribute> Preconditions { get; } | |||
internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandService service) | |||
{ | |||
Command = command; | |||
@@ -23,17 +36,30 @@ namespace Discord.Commands | |||
Type = builder.ParameterType; | |||
DefaultValue = builder.DefaultValue; | |||
Preconditions = builder.Preconditions.ToImmutableArray(); | |||
_reader = builder.TypeReader; | |||
} | |||
public CommandInfo Command { get; } | |||
public string Name { get; } | |||
public string Summary { get; } | |||
public bool IsOptional { get; } | |||
public bool IsRemainder { get; } | |||
public bool IsMultiple { get; } | |||
public Type Type { get; } | |||
public object DefaultValue { get; } | |||
public async Task<PreconditionResult> CheckPreconditionsAsync(CommandContext context, object[] args, IDependencyMap map = null) | |||
{ | |||
if (map == null) | |||
map = DependencyMap.Empty; | |||
int position = 0; | |||
for(position = 0; position < Command.Parameters.Count; position++) | |||
if (Command.Parameters[position] == this) | |||
break; | |||
foreach (var precondition in Preconditions) | |||
{ | |||
var result = await precondition.CheckPermissions(context, this, args[position], map).ConfigureAwait(false); | |||
if (!result.IsSuccess) | |||
return result; | |||
} | |||
return PreconditionResult.FromSuccess(); | |||
} | |||
public async Task<TypeReaderResult> Parse(CommandContext context, string input) | |||
{ | |||