In theory this should just work, more testing is needed thoughtags/1.0-rc
| @@ -1,4 +1,5 @@ | |||
| using System; | |||
| using System.Linq; | |||
| using System.Threading.Tasks; | |||
| using System.Collections.Generic; | |||
| @@ -22,6 +23,8 @@ namespace Discord.Commands.Builders | |||
| public string Name { get; set; } | |||
| public string Summary { get; set; } | |||
| public string Remarks { get; set; } | |||
| public RunMode RunMode { get; set; } | |||
| public int Priority { get; set; } | |||
| public Func<CommandContext, object[], IDependencyMap, Task> Callback { get; set; } | |||
| public ModuleBuilder Module { get; } | |||
| @@ -47,6 +50,18 @@ namespace Discord.Commands.Builders | |||
| return this; | |||
| } | |||
| public CommandBuilder SetRunMode(RunMode runMode) | |||
| { | |||
| RunMode = runMode; | |||
| return this; | |||
| } | |||
| public CommandBuilder SetPriority(int priority) | |||
| { | |||
| Priority = priority; | |||
| return this; | |||
| } | |||
| public CommandBuilder SetCallback(Func<CommandContext, object[], IDependencyMap, Task> callback) | |||
| { | |||
| Callback = callback; | |||
| @@ -75,6 +90,28 @@ namespace Discord.Commands.Builders | |||
| internal CommandInfo Build(ModuleInfo info, CommandService service) | |||
| { | |||
| if (aliases.Count == 0) | |||
| throw new InvalidOperationException("Commands require at least one alias to be registered"); | |||
| if (Callback == null) | |||
| throw new InvalidOperationException("Commands require a callback to be built"); | |||
| if (Name == null) | |||
| Name = aliases[0]; | |||
| if (parameters.Count > 0) | |||
| { | |||
| var lastParam = parameters[parameters.Count - 1]; | |||
| var firstMultipleParam = parameters.FirstOrDefault(x => x.Multiple); | |||
| if ((firstMultipleParam != null) && (firstMultipleParam != lastParam)) | |||
| throw new InvalidOperationException("Only the last parameter in a command may have the Multiple flag."); | |||
| var firstRemainderParam = parameters.FirstOrDefault(x => x.Remainder); | |||
| if ((firstRemainderParam != null) && (firstRemainderParam != lastParam)) | |||
| throw new InvalidOperationException("Only the last parameter in a command may have the Remainder flag."); | |||
| } | |||
| return new CommandInfo(this, info, service); | |||
| } | |||
| } | |||
| @@ -83,6 +83,15 @@ namespace Discord.Commands.Builders | |||
| public ModuleInfo Build(CommandService service) | |||
| { | |||
| if (aliases.Count == 0) | |||
| throw new InvalidOperationException("Modules require at least one alias to be registered"); | |||
| if (commands.Count == 0 && submodules.Count == 0) | |||
| throw new InvalidOperationException("Tried to build empty module"); | |||
| if (Name == null) | |||
| Name = aliases[0]; | |||
| return new ModuleInfo(this, service); | |||
| } | |||
| } | |||
| @@ -81,9 +81,19 @@ namespace Discord.Commands.Builders | |||
| internal ParameterInfo Build(CommandInfo info, CommandService service) | |||
| { | |||
| // TODO: should we throw when we don't have a name? | |||
| if (Name == null) | |||
| Name = "[unknown parameter]"; | |||
| if (ParameterType == null) | |||
| throw new InvalidOperationException($"Could not build parameter {Name} from command {info.Name} - An invalid parameter type was given"); | |||
| if (TypeReader == null) | |||
| TypeReader = service.GetTypeReader(ParameterType); | |||
| if (TypeReader == null) | |||
| throw new InvalidOperationException($"Could not build parameter {Name} from command {info.Name} - A valid TypeReader could not be found"); | |||
| return new ParameterInfo(this, info, service); | |||
| } | |||
| } | |||
| @@ -7,22 +7,26 @@ using System.Reflection; | |||
| using System.Threading; | |||
| using System.Threading.Tasks; | |||
| using Discord.Commands.Builders; | |||
| namespace Discord.Commands | |||
| { | |||
| public class CommandService | |||
| { | |||
| private readonly SemaphoreSlim _moduleLock; | |||
| private readonly ConcurrentDictionary<Type, ModuleInfo> _moduleDefs; | |||
| private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs; | |||
| private readonly ConcurrentDictionary<Type, TypeReader> _typeReaders; | |||
| private readonly ConcurrentBag<ModuleInfo> _moduleDefs; | |||
| private readonly CommandMap _map; | |||
| public IEnumerable<ModuleInfo> Modules => _moduleDefs.Select(x => x.Value); | |||
| public IEnumerable<CommandInfo> Commands => _moduleDefs.SelectMany(x => x.Value.Commands); | |||
| public IEnumerable<ModuleInfo> Modules => _typedModuleDefs.Select(x => x.Value); | |||
| public IEnumerable<CommandInfo> Commands => _typedModuleDefs.SelectMany(x => x.Value.Commands); | |||
| public CommandService() | |||
| { | |||
| _moduleLock = new SemaphoreSlim(1, 1); | |||
| _moduleDefs = new ConcurrentDictionary<Type, ModuleInfo>(); | |||
| _typedModuleDefs = new ConcurrentDictionary<Type, ModuleInfo>(); | |||
| _moduleDefs = new ConcurrentBag<ModuleInfo>(); | |||
| _map = new CommandMap(); | |||
| _typeReaders = new ConcurrentDictionary<Type, TypeReader> | |||
| { | |||
| @@ -63,6 +67,22 @@ namespace Discord.Commands | |||
| } | |||
| //Modules | |||
| public async Task<ModuleInfo> BuildModule(Action<ModuleBuilder> buildFunc) | |||
| { | |||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| var builder = new ModuleBuilder(); | |||
| buildFunc(builder); | |||
| var module = builder.Build(this); | |||
| return LoadModuleInternal(module); | |||
| } | |||
| finally | |||
| { | |||
| _moduleLock.Release(); | |||
| } | |||
| } | |||
| public async Task<ModuleInfo> AddModule<T>() | |||
| { | |||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | |||
| @@ -70,17 +90,17 @@ namespace Discord.Commands | |||
| { | |||
| var typeInfo = typeof(T).GetTypeInfo(); | |||
| if (_moduleDefs.ContainsKey(typeof(T))) | |||
| if (_typedModuleDefs.ContainsKey(typeof(T))) | |||
| throw new ArgumentException($"This module has already been added."); | |||
| var module = ModuleClassBuilder.Build(this, typeInfo).First(); | |||
| var module = ModuleClassBuilder.Build(this, typeInfo).FirstOrDefault(); | |||
| _moduleDefs[typeof(T)] = module; | |||
| if (module.Value == default(ModuleInfo)) | |||
| throw new InvalidOperationException($"Could not build the module {typeof(T).FullName}, did you pass an invalid type?"); | |||
| foreach (var cmd in module.Commands) | |||
| _map.AddCommand(cmd); | |||
| return module; | |||
| _typedModuleDefs[module.Key] = module.Value; | |||
| return LoadModuleInternal(module.Value); | |||
| } | |||
| finally | |||
| { | |||
| @@ -89,29 +109,44 @@ namespace Discord.Commands | |||
| } | |||
| public async Task<IEnumerable<ModuleInfo>> AddModules(Assembly assembly) | |||
| { | |||
| var moduleDefs = ImmutableArray.CreateBuilder<ModuleInfo>(); | |||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| var types = ModuleClassBuilder.Search(assembly); | |||
| return ModuleClassBuilder.Build(types, this).ToImmutableArray(); | |||
| var types = ModuleClassBuilder.Search(assembly).ToArray(); | |||
| var moduleDefs = ModuleClassBuilder.Build(types, this); | |||
| foreach (var info in moduleDefs) | |||
| { | |||
| _typedModuleDefs[info.Key] = info.Value; | |||
| LoadModuleInternal(info.Value); | |||
| } | |||
| return moduleDefs.Select(x => x.Value).ToImmutableArray(); | |||
| } | |||
| finally | |||
| { | |||
| _moduleLock.Release(); | |||
| } | |||
| } | |||
| private ModuleInfo LoadModuleInternal(ModuleInfo module) | |||
| { | |||
| _moduleDefs.Add(module); | |||
| foreach (var command in module.Commands) | |||
| _map.AddCommand(command); | |||
| foreach (var submodule in module.Submodules) | |||
| LoadModuleInternal(submodule); | |||
| return module; | |||
| } | |||
| public async Task<bool> RemoveModule(ModuleInfo module) | |||
| { | |||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| var type = _moduleDefs.FirstOrDefault(x => x.Value == module); | |||
| if (default(KeyValuePair<Type, ModuleInfo>).Key == type.Key) | |||
| throw new KeyNotFoundException($"Could not find the key for the module {module?.Name ?? module?.Aliases?.FirstOrDefault()}"); | |||
| return RemoveModuleInternal(type.Key); | |||
| return RemoveModuleInternal(module); | |||
| } | |||
| finally | |||
| { | |||
| @@ -123,24 +158,33 @@ namespace Discord.Commands | |||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| return RemoveModuleInternal(typeof(T)); | |||
| ModuleInfo module; | |||
| _typedModuleDefs.TryGetValue(typeof(T), out module); | |||
| if (module == default(ModuleInfo)) | |||
| return false; | |||
| return RemoveModuleInternal(module); | |||
| } | |||
| finally | |||
| { | |||
| _moduleLock.Release(); | |||
| } | |||
| } | |||
| private bool RemoveModuleInternal(Type type) | |||
| private bool RemoveModuleInternal(ModuleInfo module) | |||
| { | |||
| ModuleInfo unloadedModule; | |||
| if (_moduleDefs.TryRemove(type, out unloadedModule)) | |||
| var defsRemove = module; | |||
| if (!_moduleDefs.TryTake(out defsRemove)) | |||
| return false; | |||
| foreach (var cmd in module.Commands) | |||
| _map.RemoveCommand(cmd); | |||
| foreach (var submodule in module.Submodules) | |||
| { | |||
| foreach (var cmd in unloadedModule.Commands) | |||
| _map.RemoveCommand(cmd); | |||
| return true; | |||
| RemoveModuleInternal(submodule); | |||
| } | |||
| else | |||
| return false; | |||
| return true; | |||
| } | |||
| //Type Readers | |||
| @@ -37,10 +37,14 @@ namespace Discord.Commands | |||
| Summary = builder.Summary; | |||
| Remarks = builder.Remarks; | |||
| RunMode = builder.RunMode; | |||
| Priority = builder.Priority; | |||
| Aliases = module.Aliases.Permutate(builder.Aliases, (first, second) => first + " " + second).ToImmutableArray(); | |||
| Preconditions = builder.Preconditions.ToImmutableArray(); | |||
| Parameters = builder.Parameters.Select(x => x.Build(this, service)).ToImmutableArray(); | |||
| HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].Multiple : false; | |||
| _action = builder.Callback; | |||
| } | |||
| @@ -17,6 +17,7 @@ namespace Discord.Commands | |||
| public IReadOnlyList<string> Aliases { get; } | |||
| public IEnumerable<CommandInfo> Commands { get; } | |||
| public IReadOnlyList<PreconditionAttribute> Preconditions { get; } | |||
| public IReadOnlyList<ModuleInfo> Submodules { get; } | |||
| internal ModuleInfo(ModuleBuilder builder, CommandService service) | |||
| { | |||
| @@ -29,6 +30,8 @@ namespace Discord.Commands | |||
| Aliases = BuildAliases(builder).ToImmutableArray(); | |||
| Commands = builder.Commands.Select(x => x.Build(this, service)); | |||
| Preconditions = BuildPreconditions(builder).ToImmutableArray(); | |||
| Submodules = BuildSubmodules(builder, service).ToImmutableArray(); | |||
| } | |||
| private static IEnumerable<string> BuildAliases(ModuleBuilder builder) | |||
| @@ -59,13 +62,24 @@ namespace Discord.Commands | |||
| return result; | |||
| } | |||
| private static List<ModuleInfo> BuildSubmodules(ModuleBuilder parent, CommandService service) | |||
| { | |||
| var result = new List<ModuleInfo>(); | |||
| foreach (var submodule in parent.Modules) | |||
| { | |||
| result.Add(submodule.Build(service)); | |||
| } | |||
| return result; | |||
| } | |||
| private static List<PreconditionAttribute> BuildPreconditions(ModuleBuilder builder) | |||
| { | |||
| var result = new List<PreconditionAttribute>(); | |||
| ModuleBuilder parent = builder; | |||
| while (parent.ParentModule != null) | |||
| while (parent != null) | |||
| { | |||
| result.AddRange(parent.Preconditions); | |||
| parent = parent.ParentModule; | |||
| @@ -25,8 +25,8 @@ namespace Discord.Commands | |||
| } | |||
| } | |||
| public static IEnumerable<ModuleInfo> Build(CommandService service, params TypeInfo[] validTypes) => Build(validTypes, service); | |||
| public static IEnumerable<ModuleInfo> Build(IEnumerable<TypeInfo> validTypes, CommandService service) | |||
| public static Dictionary<Type, ModuleInfo> Build(CommandService service, params TypeInfo[] validTypes) => Build(validTypes, service); | |||
| public static Dictionary<Type, ModuleInfo> Build(IEnumerable<TypeInfo> validTypes, CommandService service) | |||
| { | |||
| if (!validTypes.Any()) | |||
| throw new InvalidOperationException("Could not find any valid modules from the given selection"); | |||
| @@ -36,22 +36,20 @@ namespace Discord.Commands | |||
| var builtTypes = new List<TypeInfo>(); | |||
| var result = new List<ModuleInfo>(); | |||
| var result = new Dictionary<Type, ModuleInfo>(); | |||
| foreach (var typeInfo in topLevelGroups) | |||
| { | |||
| // this shouldn't be the case; may be safe to remove? | |||
| if (builtTypes.Contains(typeInfo)) | |||
| if (result.ContainsKey(typeInfo.AsType())) | |||
| continue; | |||
| builtTypes.Add(typeInfo); | |||
| var module = new ModuleBuilder(); | |||
| BuildModule(module, typeInfo, service); | |||
| BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); | |||
| result.Add(module.Build(service)); | |||
| result[typeInfo.AsType()] = module.Build(service); | |||
| } | |||
| return result; | |||
| @@ -61,15 +59,18 @@ namespace Discord.Commands | |||
| { | |||
| foreach (var typeInfo in subTypes) | |||
| { | |||
| if (!IsValidModuleDefinition(typeInfo)) | |||
| continue; | |||
| if (builtTypes.Contains(typeInfo)) | |||
| continue; | |||
| builtTypes.Add(typeInfo); | |||
| builder.AddSubmodule((module) => { | |||
| BuildModule(module, typeInfo, service); | |||
| BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); | |||
| }); | |||
| builtTypes.Add(typeInfo); | |||
| } | |||
| } | |||
| @@ -89,7 +90,10 @@ namespace Discord.Commands | |||
| else if (attribute is AliasAttribute) | |||
| builder.AddAliases((attribute as AliasAttribute).Aliases); | |||
| else if (attribute is GroupAttribute) | |||
| { | |||
| builder.Name = builder.Name ?? (attribute as GroupAttribute).Prefix; | |||
| builder.AddAliases((attribute as GroupAttribute).Prefix); | |||
| } | |||
| else if (attribute is PreconditionAttribute) | |||
| builder.AddPrecondition(attribute as PreconditionAttribute); | |||
| } | |||
| @@ -111,8 +115,17 @@ namespace Discord.Commands | |||
| foreach (var attribute in attributes) | |||
| { | |||
| // TODO: C#7 type switch | |||
| if (attribute is NameAttribute) | |||
| if (attribute is CommandAttribute) | |||
| { | |||
| var cmdAttr = attribute as CommandAttribute; | |||
| builder.AddAliases(cmdAttr.Text); | |||
| builder.RunMode = cmdAttr.RunMode; | |||
| builder.Name = builder.Name ?? cmdAttr.Text; | |||
| } | |||
| else if (attribute is NameAttribute) | |||
| builder.Name = (attribute as NameAttribute).Text; | |||
| else if (attribute is PriorityAttribute) | |||
| builder.Priority = (attribute as PriorityAttribute).Priority; | |||
| else if (attribute is SummaryAttribute) | |||
| builder.Summary = (attribute as SummaryAttribute).Text; | |||
| else if (attribute is RemarksAttribute) | |||
| @@ -154,15 +167,15 @@ namespace Discord.Commands | |||
| var attributes = paramInfo.GetCustomAttributes(); | |||
| var paramType = paramInfo.ParameterType; | |||
| builder.Name = paramInfo.Name; | |||
| builder.Optional = paramInfo.IsOptional; | |||
| builder.DefaultValue = paramInfo.HasDefaultValue ? paramInfo.DefaultValue : null; | |||
| foreach (var attribute in attributes) | |||
| { | |||
| // TODO: C#7 type switch | |||
| if (attribute is NameAttribute) | |||
| builder.Name = (attribute as NameAttribute).Text; | |||
| else if (attribute is SummaryAttribute) | |||
| if (attribute is SummaryAttribute) | |||
| builder.Summary = (attribute as SummaryAttribute).Text; | |||
| else if (attribute is ParamArrayAttribute) | |||
| { | |||
| @@ -193,6 +206,7 @@ namespace Discord.Commands | |||
| } | |||
| } | |||
| builder.ParameterType = paramType; | |||
| builder.TypeReader = reader; | |||
| } | |||
| @@ -205,7 +219,7 @@ namespace Discord.Commands | |||
| private static bool IsValidCommandDefinition(MethodInfo methodInfo) | |||
| { | |||
| return methodInfo.IsDefined(typeof(CommandAttribute)) && | |||
| methodInfo.ReturnType != typeof(Task) && | |||
| methodInfo.ReturnType == typeof(Task) && | |||
| !methodInfo.IsStatic && | |||
| !methodInfo.IsGenericMethod; | |||
| } | |||