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 | // TODO: C#7 type switch | ||||
| if (attribute is SummaryAttribute) | if (attribute is SummaryAttribute) | ||||
| builder.Summary = (attribute as SummaryAttribute).Text; | 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) | else if (attribute is ParamArrayAttribute) | ||||
| { | { | ||||
| builder.IsMultiple = true; | 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) | private static bool IsValidModuleDefinition(TypeInfo typeInfo) | ||||
| @@ -1,10 +1,15 @@ | |||||
| using System; | using System; | ||||
| using System.Linq; | |||||
| using System.Reflection; | using System.Reflection; | ||||
| using System.Collections.Generic; | |||||
| namespace Discord.Commands.Builders | namespace Discord.Commands.Builders | ||||
| { | { | ||||
| public class ParameterBuilder | public class ParameterBuilder | ||||
| { | { | ||||
| private readonly List<ParameterPreconditionAttribute> _preconditions; | |||||
| public CommandBuilder Command { get; } | public CommandBuilder Command { get; } | ||||
| public string Name { get; internal set; } | public string Name { get; internal set; } | ||||
| public Type ParameterType { get; internal set; } | public Type ParameterType { get; internal set; } | ||||
| @@ -16,16 +21,20 @@ namespace Discord.Commands.Builders | |||||
| public object DefaultValue { get; set; } | public object DefaultValue { get; set; } | ||||
| public string Summary { get; set; } | public string Summary { get; set; } | ||||
| public IReadOnlyList<ParameterPreconditionAttribute> Preconditions => _preconditions; | |||||
| //Automatic | //Automatic | ||||
| internal ParameterBuilder(CommandBuilder command) | internal ParameterBuilder(CommandBuilder command) | ||||
| { | { | ||||
| _preconditions = new List<ParameterPreconditionAttribute>(); | |||||
| Command = command; | Command = command; | ||||
| } | } | ||||
| //User-defined | //User-defined | ||||
| internal ParameterBuilder(CommandBuilder command, string name, Type type) | internal ParameterBuilder(CommandBuilder command, string name, Type type) | ||||
| : this(command) | : this(command) | ||||
| { | { | ||||
| Preconditions.NotNull(name, nameof(name)); | |||||
| Discord.Preconditions.NotNull(name, nameof(name)); | |||||
| Name = name; | Name = name; | ||||
| SetType(type); | SetType(type); | ||||
| @@ -33,7 +42,11 @@ namespace Discord.Commands.Builders | |||||
| internal void SetType(Type type) | 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) | if (type.GetTypeInfo().IsValueType) | ||||
| DefaultValue = Activator.CreateInstance(type); | DefaultValue = Activator.CreateInstance(type); | ||||
| @@ -49,7 +62,7 @@ namespace Discord.Commands.Builders | |||||
| } | } | ||||
| public ParameterBuilder WithDefault(object defaultValue) | public ParameterBuilder WithDefault(object defaultValue) | ||||
| { | { | ||||
| DefaultValue = defaultValue; | |||||
| DefaultValue = defaultValue; | |||||
| return this; | return this; | ||||
| } | } | ||||
| public ParameterBuilder WithIsOptional(bool isOptional) | public ParameterBuilder WithIsOptional(bool isOptional) | ||||
| @@ -68,6 +81,12 @@ namespace Discord.Commands.Builders | |||||
| return this; | return this; | ||||
| } | } | ||||
| public ParameterBuilder AddPrecondition(ParameterPreconditionAttribute precondition) | |||||
| { | |||||
| _preconditions.Add(precondition); | |||||
| return this; | |||||
| } | |||||
| internal ParameterInfo Build(CommandInfo info) | internal ParameterInfo Build(CommandInfo info) | ||||
| { | { | ||||
| if (TypeReader == null) | if (TypeReader == null) | ||||
| @@ -15,7 +15,8 @@ namespace Discord.Commands | |||||
| { | { | ||||
| private readonly SemaphoreSlim _moduleLock; | private readonly SemaphoreSlim _moduleLock; | ||||
| private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs; | 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 ConcurrentBag<ModuleInfo> _moduleDefs; | ||||
| private readonly CommandMap _map; | private readonly CommandMap _map; | ||||
| @@ -24,6 +25,7 @@ namespace Discord.Commands | |||||
| public IEnumerable<ModuleInfo> Modules => _moduleDefs.Select(x => x); | public IEnumerable<ModuleInfo> Modules => _moduleDefs.Select(x => x); | ||||
| public IEnumerable<CommandInfo> Commands => _moduleDefs.SelectMany(x => x.Commands); | 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() : this(new CommandServiceConfig()) { } | ||||
| public CommandService(CommandServiceConfig config) | public CommandService(CommandServiceConfig config) | ||||
| @@ -32,7 +34,9 @@ namespace Discord.Commands | |||||
| _typedModuleDefs = new ConcurrentDictionary<Type, ModuleInfo>(); | _typedModuleDefs = new ConcurrentDictionary<Type, ModuleInfo>(); | ||||
| _moduleDefs = new ConcurrentBag<ModuleInfo>(); | _moduleDefs = new ConcurrentBag<ModuleInfo>(); | ||||
| _map = new CommandMap(); | _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(bool)] = new SimpleTypeReader<bool>(), | ||||
| [typeof(char)] = new SimpleTypeReader<char>(), | [typeof(char)] = new SimpleTypeReader<char>(), | ||||
| @@ -50,7 +54,7 @@ namespace Discord.Commands | |||||
| [typeof(decimal)] = new SimpleTypeReader<decimal>(), | [typeof(decimal)] = new SimpleTypeReader<decimal>(), | ||||
| [typeof(DateTime)] = new SimpleTypeReader<DateTime>(), | [typeof(DateTime)] = new SimpleTypeReader<DateTime>(), | ||||
| [typeof(DateTimeOffset)] = new SimpleTypeReader<DateTimeOffset>(), | [typeof(DateTimeOffset)] = new SimpleTypeReader<DateTimeOffset>(), | ||||
| [typeof(TimeSpan)] = new SimpleTypeReader<TimeSpan>(), | |||||
| [typeof(IMessage)] = new MessageTypeReader<IMessage>(), | [typeof(IMessage)] = new MessageTypeReader<IMessage>(), | ||||
| [typeof(IUserMessage)] = new MessageTypeReader<IUserMessage>(), | [typeof(IUserMessage)] = new MessageTypeReader<IUserMessage>(), | ||||
| [typeof(IChannel)] = new ChannelTypeReader<IChannel>(), | [typeof(IChannel)] = new ChannelTypeReader<IChannel>(), | ||||
| @@ -196,16 +200,25 @@ namespace Discord.Commands | |||||
| //Type Readers | //Type Readers | ||||
| public void AddTypeReader<T>(TypeReader reader) | 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) | 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; | TypeReader reader; | ||||
| if (_typeReaders.TryGetValue(type, out reader)) | |||||
| if (_defaultTypeReaders.TryGetValue(type, out reader)) | |||||
| return reader; | return reader; | ||||
| return null; | return null; | ||||
| } | } | ||||
| @@ -137,7 +137,15 @@ namespace Discord.Commands | |||||
| try | 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) | switch (RunMode) | ||||
| { | { | ||||
| case RunMode.Sync: //Always sync | case RunMode.Sync: //Always sync | ||||
| @@ -1,5 +1,7 @@ | |||||
| using System; | using System; | ||||
| using System.Linq; | using System.Linq; | ||||
| using System.Collections.Generic; | |||||
| using System.Collections.Immutable; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Discord.Commands.Builders; | using Discord.Commands.Builders; | ||||
| @@ -10,6 +12,17 @@ namespace Discord.Commands | |||||
| { | { | ||||
| private readonly TypeReader _reader; | 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) | internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandService service) | ||||
| { | { | ||||
| Command = command; | Command = command; | ||||
| @@ -23,17 +36,30 @@ namespace Discord.Commands | |||||
| Type = builder.ParameterType; | Type = builder.ParameterType; | ||||
| DefaultValue = builder.DefaultValue; | DefaultValue = builder.DefaultValue; | ||||
| Preconditions = builder.Preconditions.ToImmutableArray(); | |||||
| _reader = builder.TypeReader; | _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) | public async Task<TypeReaderResult> Parse(CommandContext context, string input) | ||||
| { | { | ||||