Finish implementation of command builders

This commit is contained in:
FiniteReality
2016-11-16 20:40:28 +00:00
parent ab95ced41d
commit 6d46347ebc
8 changed files with 255 additions and 53 deletions

View File

@@ -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<TypeInfo> 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<ModuleInfo> Build(CommandService service, params TypeInfo[] validTypes) => Build(validTypes, service);
public static IEnumerable<ModuleInfo> Build(IEnumerable<TypeInfo> 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<TypeInfo>();
var result = new List<ModuleInfo>();
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<TypeInfo> subTypes, List<TypeInfo> 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<ModuleBase>(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;
}
}
}

View File

@@ -0,0 +1,54 @@
using System;
using System.Linq;
using System.Reflection;
namespace Discord.Commands
{
internal class ReflectionUtils
{
internal static T CreateObject<T>(TypeInfo typeInfo, CommandService service, IDependencyMap map = null)
=> CreateBuilder<T>(typeInfo, service)(map);
internal static Func<IDependencyMap, T> CreateBuilder<T>(TypeInfo typeInfo, CommandService service)
{
var constructors = typeInfo.DeclaredConstructors.Where(x => !x.IsStatic).ToArray();
if (constructors.Length == 0)
throw new InvalidOperationException($"No constructor found for \"{typeInfo.FullName}\"");
else if (constructors.Length > 1)
throw new InvalidOperationException($"Multiple constructors found for \"{typeInfo.FullName}\"");
var constructor = constructors[0];
System.Reflection.ParameterInfo[] parameters = constructor.GetParameters();
return (map) =>
{
object[] args = new object[parameters.Length];
for (int i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
object arg;
if (map == null || !map.TryGet(parameter.ParameterType, out arg))
{
if (parameter.ParameterType == typeof(CommandService))
arg = service;
else if (parameter.ParameterType == typeof(IDependencyMap))
arg = map;
else
throw new InvalidOperationException($"Failed to create \"{typeInfo.FullName}\", dependency \"{parameter.ParameterType.Name}\" was not found.");
}
args[i] = arg;
}
try
{
return (T)constructor.Invoke(args);
}
catch (Exception ex)
{
throw new Exception($"Failed to create \"{typeInfo.FullName}\"", ex);
}
};
}
}
}