This commit also adds a TypeReaders property to CommandService, so it is possible to see all of the registered TypeReaders. This makes it possible for users to implement their own parsing instead of using the built-in parsing.
262 lines
10 KiB
C#
262 lines
10 KiB
C#
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 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");
|
|
|
|
var topLevelGroups = validTypes.Where(x => x.DeclaringType == null);
|
|
var subGroups = validTypes.Intersect(topLevelGroups);
|
|
|
|
var builtTypes = new List<TypeInfo>();
|
|
|
|
var result = new Dictionary<Type, ModuleInfo>();
|
|
|
|
foreach (var typeInfo in topLevelGroups)
|
|
{
|
|
// TODO: This shouldn't be the case; may be safe to remove?
|
|
if (result.ContainsKey(typeInfo.AsType()))
|
|
continue;
|
|
|
|
var module = new ModuleBuilder(service, null);
|
|
|
|
BuildModule(module, typeInfo, service);
|
|
BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service);
|
|
|
|
result[typeInfo.AsType()] = 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 (!IsValidModuleDefinition(typeInfo))
|
|
continue;
|
|
|
|
if (builtTypes.Contains(typeInfo))
|
|
continue;
|
|
|
|
builder.AddModule((module) => {
|
|
BuildModule(module, typeInfo, service);
|
|
BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service);
|
|
});
|
|
|
|
builtTypes.Add(typeInfo);
|
|
}
|
|
}
|
|
|
|
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.AddAlias((attribute as AliasAttribute).Aliases);
|
|
else if (attribute is GroupAttribute)
|
|
{
|
|
var groupAttr = attribute as GroupAttribute;
|
|
builder.Name = builder.Name ?? groupAttr.Prefix;
|
|
builder.AddAlias(groupAttr.Prefix);
|
|
}
|
|
else if (attribute is PreconditionAttribute)
|
|
builder.AddPrecondition(attribute as PreconditionAttribute);
|
|
}
|
|
|
|
if (builder.Name == null)
|
|
builder.Name = typeInfo.Name;
|
|
|
|
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 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)
|
|
builder.Remarks = (attribute as RemarksAttribute).Text;
|
|
else if (attribute is AliasAttribute)
|
|
builder.AddAliases((attribute as AliasAttribute).Aliases);
|
|
else if (attribute is PreconditionAttribute)
|
|
builder.AddPrecondition(attribute as PreconditionAttribute);
|
|
}
|
|
|
|
if (builder.Name == null)
|
|
builder.Name = method.Name;
|
|
|
|
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.Name = paramInfo.Name;
|
|
|
|
builder.IsOptional = paramInfo.IsOptional;
|
|
builder.DefaultValue = paramInfo.HasDefaultValue ? paramInfo.DefaultValue : null;
|
|
|
|
foreach (var attribute in attributes)
|
|
{
|
|
// 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;
|
|
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.IsRemainder = true;
|
|
}
|
|
}
|
|
|
|
if (builder.TypeReader == null)
|
|
{
|
|
var readers = service.GetTypeReaders(paramType);
|
|
var reader = readers?.FirstOrDefault();
|
|
|
|
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.ParameterType = paramType;
|
|
builder.TypeReader = reader;
|
|
}
|
|
}
|
|
|
|
private static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType)
|
|
{
|
|
var readers = service.GetTypeReaders(paramType);
|
|
if (readers != null)
|
|
{
|
|
var reader = readers.FirstOrDefault(x => x.GetType() == typeReaderType);
|
|
if (reader != default(TypeReader))
|
|
{
|
|
return reader;
|
|
}
|
|
}
|
|
|
|
//could not find any registered type reader: try to create one
|
|
var typeReader = ReflectionUtils.CreateObject<TypeReader>(typeReaderType.GetTypeInfo(), service, DependencyMap.Empty);
|
|
service.AddTypeReader(paramType, typeReader);
|
|
|
|
return typeReader;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
} |