From 6d46347ebcf4cfdb1ecc74cdb43d4d83b2219593 Mon Sep 17 00:00:00 2001 From: FiniteReality Date: Wed, 16 Nov 2016 20:40:28 +0000 Subject: [PATCH] Finish implementation of command builders --- .../Builders/CommandBuilder.cs | 4 +- .../Builders/ModuleBuilder.cs | 10 +- .../Builders/ParameterBuilder.cs | 1 + src/Discord.Net.Commands/CommandService.cs | 50 ++-- src/Discord.Net.Commands/Info/CommandInfo.cs | 17 +- src/Discord.Net.Commands/Info/ModuleInfo.cs | 13 +- .../Utilities/ModuleClassBuilder.cs | 213 ++++++++++++++++++ .../{ => Utilities}/ReflectionUtils.cs | 0 8 files changed, 255 insertions(+), 53 deletions(-) create mode 100644 src/Discord.Net.Commands/Utilities/ModuleClassBuilder.cs rename src/Discord.Net.Commands/{ => Utilities}/ReflectionUtils.cs (100%) diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs index 2ea01e3bb..cd699ae61 100644 --- a/src/Discord.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -22,7 +22,7 @@ namespace Discord.Commands.Builders public string Name { get; set; } public string Summary { get; set; } public string Remarks { get; set; } - public Func Callback { get; set; } + public Func Callback { get; set; } public ModuleBuilder Module { get; } public List Preconditions => preconditions; @@ -47,7 +47,7 @@ namespace Discord.Commands.Builders return this; } - public CommandBuilder SetCallback(Func callback) + public CommandBuilder SetCallback(Func callback) { Callback = callback; return this; diff --git a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs index dd30b0130..79f818f38 100644 --- a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -12,16 +12,16 @@ namespace Discord.Commands.Builders private List aliases; public ModuleBuilder() - : this(null) - { } - - internal ModuleBuilder(ModuleBuilder parent) { commands = new List(); submodules = new List(); preconditions = new List(); aliases = new List(); - + } + + internal ModuleBuilder(ModuleBuilder parent) + : this() + { ParentModule = parent; } diff --git a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs index 8a0a17ab0..7de26b72f 100644 --- a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs @@ -39,6 +39,7 @@ namespace Discord.Commands.Builders public ParameterBuilder SetDefault(T defaultValue) { + Optional = true; DefaultValue = defaultValue; ParameterType = typeof(T); diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index ef0dba7e7..08cf75ac8 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -11,10 +11,8 @@ namespace Discord.Commands { public class CommandService { - private static readonly TypeInfo _moduleTypeInfo = typeof(ModuleBase).GetTypeInfo(); - private readonly SemaphoreSlim _moduleLock; - private readonly ConcurrentDictionary _moduleDefs; + private readonly ConcurrentDictionary _moduleDefs; private readonly ConcurrentDictionary _typeReaders; private readonly CommandMap _map; @@ -71,16 +69,18 @@ namespace Discord.Commands try { var typeInfo = typeof(T).GetTypeInfo(); - if (!_moduleTypeInfo.IsAssignableFrom(typeInfo)) - throw new ArgumentException($"Modules must inherit ModuleBase."); - - if (typeInfo.IsAbstract) - throw new InvalidOperationException("Modules must not be abstract."); if (_moduleDefs.ContainsKey(typeof(T))) throw new ArgumentException($"This module has already been added."); - return AddModuleInternal(typeInfo); + var module = ModuleClassBuilder.Build(this, typeInfo).First(); + + _moduleDefs[typeof(T)] = module; + + foreach (var cmd in module.Commands) + _map.AddCommand(cmd); + + return module; } finally { @@ -93,43 +93,25 @@ namespace Discord.Commands await _moduleLock.WaitAsync().ConfigureAwait(false); try { - foreach (var type in assembly.ExportedTypes) - { - if (!_moduleDefs.ContainsKey(type)) - { - var typeInfo = type.GetTypeInfo(); - if (_moduleTypeInfo.IsAssignableFrom(typeInfo)) - { - var dontAutoLoad = typeInfo.GetCustomAttribute(); - if (dontAutoLoad == null && !typeInfo.IsAbstract) - moduleDefs.Add(AddModuleInternal(typeInfo)); - } - } - } - return moduleDefs.ToImmutable(); + var types = ModuleClassBuilder.Search(assembly); + return ModuleClassBuilder.Build(types, this).ToImmutableArray(); } finally { _moduleLock.Release(); } } - private ModuleInfo AddModuleInternal(TypeInfo typeInfo) - { - var moduleDef = new ModuleInfo(typeInfo, this); - _moduleDefs[typeInfo.AsType()] = moduleDef; - - foreach (var cmd in moduleDef.Commands) - _map.AddCommand(cmd); - - return moduleDef; - } public async Task RemoveModule(ModuleInfo module) { await _moduleLock.WaitAsync().ConfigureAwait(false); try { - return RemoveModuleInternal(module.Source.BaseType); + var type = _moduleDefs.FirstOrDefault(x => x.Value == module); + if (default(KeyValuePair).Key == type.Key) + throw new KeyNotFoundException($"Could not find the key for the module {module?.Name ?? module?.Aliases?.FirstOrDefault()}"); + + return RemoveModuleInternal(type.Key); } finally { diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index fc1e0421b..31caaf3a8 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -15,7 +15,7 @@ namespace Discord.Commands private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); private static readonly ConcurrentDictionary, object>> _arrayConverters = new ConcurrentDictionary, object>>(); - private readonly Func _action; + private readonly Func _action; public ModuleInfo Module { get; } public string Name { get; } @@ -89,7 +89,7 @@ namespace Discord.Commands return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false); } - public Task Execute(CommandContext context, ParseResult parseResult) + public Task Execute(CommandContext context, ParseResult parseResult, IDependencyMap map) { if (!parseResult.IsSuccess) return Task.FromResult(ExecuteResult.FromError(parseResult)); @@ -110,23 +110,26 @@ namespace Discord.Commands paramList[i] = parseResult.ParamValues[i].Values.First().Value; } - return Execute(context, argList, paramList); + return Execute(context, argList, paramList, map); } - public async Task Execute(CommandContext context, IEnumerable argList, IEnumerable paramList) + public async Task Execute(CommandContext context, IEnumerable argList, IEnumerable paramList, IDependencyMap map) { + if (map == null) + map = DependencyMap.Empty; + try { var args = GenerateArgs(argList, paramList); switch (RunMode) { case RunMode.Sync: //Always sync - await _action(context, args).ConfigureAwait(false); + await _action(context, args, map).ConfigureAwait(false); break; case RunMode.Mixed: //Sync until first await statement - var t1 = _action(context, args); + var t1 = _action(context, args, map); break; case RunMode.Async: //Always async - var t2 = Task.Run(() => _action(context, args)); + var t2 = Task.Run(() => _action(context, args, map)); break; } return ExecuteResult.FromSuccess(); diff --git a/src/Discord.Net.Commands/Info/ModuleInfo.cs b/src/Discord.Net.Commands/Info/ModuleInfo.cs index 33a6574c4..6ec0d657b 100644 --- a/src/Discord.Net.Commands/Info/ModuleInfo.cs +++ b/src/Discord.Net.Commands/Info/ModuleInfo.cs @@ -31,14 +31,15 @@ namespace Discord.Commands Preconditions = BuildPreconditions(builder).ToImmutableArray(); } - private static List BuildAliases(ModuleBuilder builder) + private static IEnumerable BuildAliases(ModuleBuilder builder) { IEnumerable result = null; Stack builderStack = new Stack(); + builderStack.Push(builder); - ModuleBuilder parent = builder; - while (parent.ParentModule != null) + ModuleBuilder parent = builder.ParentModule; + while (parent != null) { builderStack.Push(parent); parent = parent.ParentModule; @@ -49,11 +50,13 @@ namespace Discord.Commands ModuleBuilder level = builderStack.Pop(); // get the topmost builder if (result == null) result = level.Aliases.ToList(); // create a shallow copy so we don't overwrite the builder unexpectedly - else + else if (result.Count() > level.Aliases.Count) result = result.Permutate(level.Aliases, (first, second) => first + " " + second); + else + result = level.Aliases.Permutate(result, (second, first) => first + " " + second); } - return result.ToList(); + return result; } private static List BuildPreconditions(ModuleBuilder builder) diff --git a/src/Discord.Net.Commands/Utilities/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Utilities/ModuleClassBuilder.cs new file mode 100644 index 000000000..30ee1e5ba --- /dev/null +++ b/src/Discord.Net.Commands/Utilities/ModuleClassBuilder.cs @@ -0,0 +1,213 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; + +using Discord.Commands.Builders; + +namespace Discord.Commands +{ + internal static class ModuleClassBuilder + { + private static readonly TypeInfo _moduleTypeInfo = typeof(ModuleBase).GetTypeInfo(); + + public static IEnumerable Search(Assembly assembly) + { + foreach (var type in assembly.ExportedTypes) + { + var typeInfo = type.GetTypeInfo(); + if (IsValidModuleDefinition(typeInfo) && + !typeInfo.IsDefined(typeof(DontAutoLoadAttribute))) + { + yield return typeInfo; + } + } + } + + public static IEnumerable Build(CommandService service, params TypeInfo[] validTypes) => Build(validTypes, service); + public static IEnumerable Build(IEnumerable validTypes, CommandService service) + { + if (!validTypes.Any()) + throw new InvalidOperationException("Could not find any valid modules from the given selection"); + + var topLevelGroups = validTypes.Where(x => x.DeclaringType == null); + var subGroups = validTypes.Intersect(topLevelGroups); + + var builtTypes = new List(); + + var result = new List(); + + foreach (var typeInfo in topLevelGroups) + { + // this shouldn't be the case; may be safe to remove? + if (builtTypes.Contains(typeInfo)) + continue; + + builtTypes.Add(typeInfo); + + var module = new ModuleBuilder(); + + BuildModule(module, typeInfo, service); + BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); + + result.Add(module.Build(service)); + } + + return result; + } + + private static void BuildSubTypes(ModuleBuilder builder, IEnumerable subTypes, List builtTypes, CommandService service) + { + foreach (var typeInfo in subTypes) + { + if (builtTypes.Contains(typeInfo)) + continue; + + builtTypes.Add(typeInfo); + + builder.AddSubmodule((module) => { + BuildModule(module, typeInfo, service); + BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); + }); + } + } + + private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, CommandService service) + { + var attributes = typeInfo.GetCustomAttributes(); + + 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) + builder.Summary = (attribute as SummaryAttribute).Text; + else if (attribute is RemarksAttribute) + builder.Remarks = (attribute as RemarksAttribute).Text; + else if (attribute is AliasAttribute) + builder.AddAliases((attribute as AliasAttribute).Aliases); + else if (attribute is GroupAttribute) + builder.AddAliases((attribute as GroupAttribute).Prefix); + else if (attribute is PreconditionAttribute) + builder.AddPrecondition(attribute as PreconditionAttribute); + } + + var validCommands = typeInfo.DeclaredMethods.Where(x => IsValidCommandDefinition(x)); + + foreach (var method in validCommands) + { + builder.AddCommand((command) => { + BuildCommand(command, typeInfo, method, service); + }); + } + } + + private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service) + { + var attributes = method.GetCustomAttributes(); + + 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) + builder.Summary = (attribute as SummaryAttribute).Text; + else if (attribute is RemarksAttribute) + builder.Remarks = (attribute as RemarksAttribute).Text; + else if (attribute is AliasAttribute) + builder.AddAliases((attribute as AliasAttribute).Aliases); + else if (attribute is GroupAttribute) + builder.AddAliases((attribute as GroupAttribute).Prefix); + else if (attribute is PreconditionAttribute) + builder.AddPrecondition(attribute as PreconditionAttribute); + } + + var parameters = method.GetParameters(); + int pos = 0, count = parameters.Length; + foreach (var paramInfo in parameters) + { + builder.AddParameter((parameter) => { + BuildParameter(parameter, paramInfo, pos++, count, service); + }); + } + + var createInstance = ReflectionUtils.CreateBuilder(typeInfo, service); + + builder.Callback = (ctx, args, map) => { + var instance = createInstance(map); + instance.Context = ctx; + try + { + return method.Invoke(instance, args) as Task ?? Task.CompletedTask; + } + finally{ + (instance as IDisposable)?.Dispose(); + } + }; + } + + private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service) + { + var attributes = paramInfo.GetCustomAttributes(); + var paramType = paramInfo.ParameterType; + + 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) + builder.Summary = (attribute as SummaryAttribute).Text; + else if (attribute is ParamArrayAttribute) + { + builder.Multiple = true; + paramType = paramType.GetElementType(); + } + else if (attribute is RemainderAttribute) + { + if (position != count-1) + throw new InvalidOperationException("Remainder parameters must be the last parameter in a command."); + + builder.Remainder = true; + } + } + + var reader = service.GetTypeReader(paramType); + if (reader == null) + { + 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.TypeReader = reader; + } + + private static bool IsValidModuleDefinition(TypeInfo typeInfo) + { + return _moduleTypeInfo.IsAssignableFrom(typeInfo) && + !typeInfo.IsAbstract; + } + + private static bool IsValidCommandDefinition(MethodInfo methodInfo) + { + return methodInfo.IsDefined(typeof(CommandAttribute)) && + methodInfo.ReturnType != typeof(Task) && + !methodInfo.IsStatic && + !methodInfo.IsGenericMethod; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Commands/ReflectionUtils.cs b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs similarity index 100% rename from src/Discord.Net.Commands/ReflectionUtils.cs rename to src/Discord.Net.Commands/Utilities/ReflectionUtils.cs