Browse Source

Complete command builders implementation

In theory this should just work, more testing is needed though
tags/1.0-rc
FiniteReality 8 years ago
parent
commit
de645548a9
7 changed files with 176 additions and 44 deletions
  1. +37
    -0
      src/Discord.Net.Commands/Builders/CommandBuilder.cs
  2. +9
    -0
      src/Discord.Net.Commands/Builders/ModuleBuilder.cs
  3. +10
    -0
      src/Discord.Net.Commands/Builders/ParameterBuilder.cs
  4. +72
    -28
      src/Discord.Net.Commands/CommandService.cs
  5. +4
    -0
      src/Discord.Net.Commands/Info/CommandInfo.cs
  6. +16
    -2
      src/Discord.Net.Commands/Info/ModuleInfo.cs
  7. +28
    -14
      src/Discord.Net.Commands/Utilities/ModuleClassBuilder.cs

+ 37
- 0
src/Discord.Net.Commands/Builders/CommandBuilder.cs View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Collections.Generic; using System.Collections.Generic;


@@ -22,6 +23,8 @@ namespace Discord.Commands.Builders
public string Name { get; set; } public string Name { get; set; }
public string Summary { get; set; } public string Summary { get; set; }
public string Remarks { 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 Func<CommandContext, object[], IDependencyMap, Task> Callback { get; set; }
public ModuleBuilder Module { get; } public ModuleBuilder Module { get; }


@@ -47,6 +50,18 @@ namespace Discord.Commands.Builders
return this; 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) public CommandBuilder SetCallback(Func<CommandContext, object[], IDependencyMap, Task> callback)
{ {
Callback = callback; Callback = callback;
@@ -75,6 +90,28 @@ namespace Discord.Commands.Builders


internal CommandInfo Build(ModuleInfo info, CommandService service) 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); return new CommandInfo(this, info, service);
} }
} }

+ 9
- 0
src/Discord.Net.Commands/Builders/ModuleBuilder.cs View File

@@ -83,6 +83,15 @@ namespace Discord.Commands.Builders


public ModuleInfo Build(CommandService service) 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); return new ModuleInfo(this, service);
} }
} }


+ 10
- 0
src/Discord.Net.Commands/Builders/ParameterBuilder.cs View File

@@ -81,9 +81,19 @@ namespace Discord.Commands.Builders


internal ParameterInfo Build(CommandInfo info, CommandService service) 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) if (TypeReader == null)
TypeReader = service.GetTypeReader(ParameterType); 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); return new ParameterInfo(this, info, service);
} }
} }

+ 72
- 28
src/Discord.Net.Commands/CommandService.cs View File

@@ -7,22 +7,26 @@ using System.Reflection;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;


using Discord.Commands.Builders;

namespace Discord.Commands namespace Discord.Commands
{ {
public class CommandService public class CommandService
{ {
private readonly SemaphoreSlim _moduleLock; private readonly SemaphoreSlim _moduleLock;
private readonly ConcurrentDictionary<Type, ModuleInfo> _moduleDefs;
private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs;
private readonly ConcurrentDictionary<Type, TypeReader> _typeReaders; private readonly ConcurrentDictionary<Type, TypeReader> _typeReaders;
private readonly ConcurrentBag<ModuleInfo> _moduleDefs;
private readonly CommandMap _map; 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() public CommandService()
{ {
_moduleLock = new SemaphoreSlim(1, 1); _moduleLock = new SemaphoreSlim(1, 1);
_moduleDefs = new ConcurrentDictionary<Type, ModuleInfo>();
_typedModuleDefs = new ConcurrentDictionary<Type, ModuleInfo>();
_moduleDefs = new ConcurrentBag<ModuleInfo>();
_map = new CommandMap(); _map = new CommandMap();
_typeReaders = new ConcurrentDictionary<Type, TypeReader> _typeReaders = new ConcurrentDictionary<Type, TypeReader>
{ {
@@ -63,6 +67,22 @@ namespace Discord.Commands
} }


//Modules //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>() public async Task<ModuleInfo> AddModule<T>()
{ {
await _moduleLock.WaitAsync().ConfigureAwait(false); await _moduleLock.WaitAsync().ConfigureAwait(false);
@@ -70,17 +90,17 @@ namespace Discord.Commands
{ {
var typeInfo = typeof(T).GetTypeInfo(); var typeInfo = typeof(T).GetTypeInfo();


if (_moduleDefs.ContainsKey(typeof(T)))
if (_typedModuleDefs.ContainsKey(typeof(T)))
throw new ArgumentException($"This module has already been added."); 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 finally
{ {
@@ -89,29 +109,44 @@ namespace Discord.Commands
} }
public async Task<IEnumerable<ModuleInfo>> AddModules(Assembly assembly) public async Task<IEnumerable<ModuleInfo>> AddModules(Assembly assembly)
{ {
var moduleDefs = ImmutableArray.CreateBuilder<ModuleInfo>();
await _moduleLock.WaitAsync().ConfigureAwait(false); await _moduleLock.WaitAsync().ConfigureAwait(false);
try 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 finally
{ {
_moduleLock.Release(); _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) public async Task<bool> RemoveModule(ModuleInfo module)
{ {
await _moduleLock.WaitAsync().ConfigureAwait(false); await _moduleLock.WaitAsync().ConfigureAwait(false);
try 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 finally
{ {
@@ -123,24 +158,33 @@ namespace Discord.Commands
await _moduleLock.WaitAsync().ConfigureAwait(false); await _moduleLock.WaitAsync().ConfigureAwait(false);
try try
{ {
return RemoveModuleInternal(typeof(T));
ModuleInfo module;
_typedModuleDefs.TryGetValue(typeof(T), out module);
if (module == default(ModuleInfo))
return false;
return RemoveModuleInternal(module);
} }
finally finally
{ {
_moduleLock.Release(); _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 //Type Readers


+ 4
- 0
src/Discord.Net.Commands/Info/CommandInfo.cs View File

@@ -37,10 +37,14 @@ namespace Discord.Commands
Summary = builder.Summary; Summary = builder.Summary;
Remarks = builder.Remarks; Remarks = builder.Remarks;


RunMode = builder.RunMode;
Priority = builder.Priority;

Aliases = module.Aliases.Permutate(builder.Aliases, (first, second) => first + " " + second).ToImmutableArray(); Aliases = module.Aliases.Permutate(builder.Aliases, (first, second) => first + " " + second).ToImmutableArray();
Preconditions = builder.Preconditions.ToImmutableArray(); Preconditions = builder.Preconditions.ToImmutableArray();


Parameters = builder.Parameters.Select(x => x.Build(this, service)).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; _action = builder.Callback;
} }


+ 16
- 2
src/Discord.Net.Commands/Info/ModuleInfo.cs View File

@@ -17,6 +17,7 @@ namespace Discord.Commands
public IReadOnlyList<string> Aliases { get; } public IReadOnlyList<string> Aliases { get; }
public IEnumerable<CommandInfo> Commands { get; } public IEnumerable<CommandInfo> Commands { get; }
public IReadOnlyList<PreconditionAttribute> Preconditions { get; } public IReadOnlyList<PreconditionAttribute> Preconditions { get; }
public IReadOnlyList<ModuleInfo> Submodules { get; }


internal ModuleInfo(ModuleBuilder builder, CommandService service) internal ModuleInfo(ModuleBuilder builder, CommandService service)
{ {
@@ -29,6 +30,8 @@ namespace Discord.Commands
Aliases = BuildAliases(builder).ToImmutableArray(); Aliases = BuildAliases(builder).ToImmutableArray();
Commands = builder.Commands.Select(x => x.Build(this, service)); Commands = builder.Commands.Select(x => x.Build(this, service));
Preconditions = BuildPreconditions(builder).ToImmutableArray(); Preconditions = BuildPreconditions(builder).ToImmutableArray();

Submodules = BuildSubmodules(builder, service).ToImmutableArray();
} }


private static IEnumerable<string> BuildAliases(ModuleBuilder builder) private static IEnumerable<string> BuildAliases(ModuleBuilder builder)
@@ -59,13 +62,24 @@ namespace Discord.Commands
return result; 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) private static List<PreconditionAttribute> BuildPreconditions(ModuleBuilder builder)
{ {
var result = new List<PreconditionAttribute>(); var result = new List<PreconditionAttribute>();



ModuleBuilder parent = builder; ModuleBuilder parent = builder;
while (parent.ParentModule != null)
while (parent != null)
{ {
result.AddRange(parent.Preconditions); result.AddRange(parent.Preconditions);
parent = parent.ParentModule; parent = parent.ParentModule;


+ 28
- 14
src/Discord.Net.Commands/Utilities/ModuleClassBuilder.cs View File

@@ -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()) if (!validTypes.Any())
throw new InvalidOperationException("Could not find any valid modules from the given selection"); 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 builtTypes = new List<TypeInfo>();


var result = new List<ModuleInfo>();
var result = new Dictionary<Type, ModuleInfo>();


foreach (var typeInfo in topLevelGroups) foreach (var typeInfo in topLevelGroups)
{ {
// this shouldn't be the case; may be safe to remove? // this shouldn't be the case; may be safe to remove?
if (builtTypes.Contains(typeInfo))
if (result.ContainsKey(typeInfo.AsType()))
continue; continue;


builtTypes.Add(typeInfo);

var module = new ModuleBuilder(); var module = new ModuleBuilder();


BuildModule(module, typeInfo, service); BuildModule(module, typeInfo, service);
BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service);


result.Add(module.Build(service));
result[typeInfo.AsType()] = module.Build(service);
} }


return result; return result;
@@ -61,15 +59,18 @@ namespace Discord.Commands
{ {
foreach (var typeInfo in subTypes) foreach (var typeInfo in subTypes)
{ {
if (!IsValidModuleDefinition(typeInfo))
continue;
if (builtTypes.Contains(typeInfo)) if (builtTypes.Contains(typeInfo))
continue; continue;

builtTypes.Add(typeInfo);
builder.AddSubmodule((module) => { builder.AddSubmodule((module) => {
BuildModule(module, typeInfo, service); BuildModule(module, typeInfo, service);
BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service);
}); });

builtTypes.Add(typeInfo);
} }
} }


@@ -89,7 +90,10 @@ namespace Discord.Commands
else if (attribute is AliasAttribute) else if (attribute is AliasAttribute)
builder.AddAliases((attribute as AliasAttribute).Aliases); builder.AddAliases((attribute as AliasAttribute).Aliases);
else if (attribute is GroupAttribute) else if (attribute is GroupAttribute)
{
builder.Name = builder.Name ?? (attribute as GroupAttribute).Prefix;
builder.AddAliases((attribute as GroupAttribute).Prefix); builder.AddAliases((attribute as GroupAttribute).Prefix);
}
else if (attribute is PreconditionAttribute) else if (attribute is PreconditionAttribute)
builder.AddPrecondition(attribute as PreconditionAttribute); builder.AddPrecondition(attribute as PreconditionAttribute);
} }
@@ -111,8 +115,17 @@ namespace Discord.Commands
foreach (var attribute in attributes) foreach (var attribute in attributes)
{ {
// TODO: C#7 type switch // 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; builder.Name = (attribute as NameAttribute).Text;
else if (attribute is PriorityAttribute)
builder.Priority = (attribute as PriorityAttribute).Priority;
else if (attribute is SummaryAttribute) else if (attribute is SummaryAttribute)
builder.Summary = (attribute as SummaryAttribute).Text; builder.Summary = (attribute as SummaryAttribute).Text;
else if (attribute is RemarksAttribute) else if (attribute is RemarksAttribute)
@@ -154,15 +167,15 @@ namespace Discord.Commands
var attributes = paramInfo.GetCustomAttributes(); var attributes = paramInfo.GetCustomAttributes();
var paramType = paramInfo.ParameterType; var paramType = paramInfo.ParameterType;


builder.Name = paramInfo.Name;

builder.Optional = paramInfo.IsOptional; builder.Optional = paramInfo.IsOptional;
builder.DefaultValue = paramInfo.HasDefaultValue ? paramInfo.DefaultValue : null; builder.DefaultValue = paramInfo.HasDefaultValue ? paramInfo.DefaultValue : null;


foreach (var attribute in attributes) foreach (var attribute in attributes)
{ {
// TODO: C#7 type switch // 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; builder.Summary = (attribute as SummaryAttribute).Text;
else if (attribute is ParamArrayAttribute) else if (attribute is ParamArrayAttribute)
{ {
@@ -193,6 +206,7 @@ namespace Discord.Commands
} }
} }


builder.ParameterType = paramType;
builder.TypeReader = reader; builder.TypeReader = reader;
} }


@@ -205,7 +219,7 @@ namespace Discord.Commands
private static bool IsValidCommandDefinition(MethodInfo methodInfo) private static bool IsValidCommandDefinition(MethodInfo methodInfo)
{ {
return methodInfo.IsDefined(typeof(CommandAttribute)) && return methodInfo.IsDefined(typeof(CommandAttribute)) &&
methodInfo.ReturnType != typeof(Task) &&
methodInfo.ReturnType == typeof(Task) &&
!methodInfo.IsStatic && !methodInfo.IsStatic &&
!methodInfo.IsGenericMethod; !methodInfo.IsGenericMethod;
} }


Loading…
Cancel
Save