Browse Source

Merge bb2eb645b2 into 3395700720

pull/1487/merge
Paulo GitHub 3 years ago
parent
commit
d97057cc6c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 159 additions and 45 deletions
  1. +9
    -10
      src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
  2. +2
    -3
      src/Discord.Net.Commands/Builders/ParameterBuilder.cs
  3. +89
    -30
      src/Discord.Net.Commands/CommandService.cs
  4. +2
    -2
      src/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs
  5. +1
    -0
      src/Discord.Net.Commands/Readers/TypeReader.cs
  6. +55
    -0
      src/Discord.Net.Examples/Commands/CommandService.Examples.cs
  7. +1
    -0
      src/Discord.Net.Examples/Discord.Net.Examples.csproj

+ 9
- 10
src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs View File

@@ -276,24 +276,23 @@ namespace Discord.Commands


if (builder.TypeReader == null) if (builder.TypeReader == null)
{ {
builder.TypeReader = service.GetDefaultTypeReader(paramType)
?? service.GetTypeReaders(paramType)?.FirstOrDefault().Value;
builder.TypeReader = service.GetTypeReaders(paramType, false)?.FirstOrDefault().Value
?? service.GetDefaultTypeReader(paramType);
} }
} }


internal static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services) internal static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services)
{ {
var readers = service.GetTypeReaders(paramType);
TypeReader reader = null;
var readers = service.GetTypeReaders(paramType, true);
if (readers != null) if (readers != null)
{
if (readers.TryGetValue(typeReaderType, out reader))
return reader;
}
foreach (var kvp in readers)
if (kvp.Key == typeReaderType)
return kvp.Value;


//We dont have a cached type reader, create one //We dont have a cached type reader, create one
reader = ReflectionUtils.CreateObject<TypeReader>(typeReaderType.GetTypeInfo(), service, services);
service.AddTypeReader(paramType, reader, false);
TypeReader reader = ReflectionUtils.CreateObject<TypeReader>(typeReaderType.GetTypeInfo(), service, services);
reader.IsOverride = true;
service.AddTypeReader(paramType, reader);


return reader; return reader;
} }


+ 2
- 3
src/Discord.Net.Commands/Builders/ParameterBuilder.cs View File

@@ -60,7 +60,7 @@ namespace Discord.Commands.Builders
if (type.GetTypeInfo().GetCustomAttribute<NamedArgumentTypeAttribute>() != null) if (type.GetTypeInfo().GetCustomAttribute<NamedArgumentTypeAttribute>() != null)
{ {
IsRemainder = true; IsRemainder = true;
var reader = commands.GetTypeReaders(type)?.FirstOrDefault().Value;
var reader = commands.GetTypeReaders(type, false)?.FirstOrDefault().Value;
if (reader == null) if (reader == null)
{ {
Type readerType; Type readerType;
@@ -80,8 +80,7 @@ namespace Discord.Commands.Builders
return reader; return reader;
} }



var readers = commands.GetTypeReaders(type);
var readers = commands.GetTypeReaders(type, false);
if (readers != null) if (readers != null)
return readers.FirstOrDefault().Value; return readers.FirstOrDefault().Value;
else else


+ 89
- 30
src/Discord.Net.Commands/CommandService.cs View File

@@ -48,6 +48,7 @@ namespace Discord.Commands
private readonly SemaphoreSlim _moduleLock; private readonly SemaphoreSlim _moduleLock;
private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs; private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs;
private readonly ConcurrentDictionary<Type, ConcurrentDictionary<Type, TypeReader>> _typeReaders; private readonly ConcurrentDictionary<Type, ConcurrentDictionary<Type, TypeReader>> _typeReaders;
private readonly ConcurrentDictionary<Type, ConcurrentQueue<Type>> _userEntityTypeReaders;
private readonly ConcurrentDictionary<Type, TypeReader> _defaultTypeReaders; private readonly ConcurrentDictionary<Type, TypeReader> _defaultTypeReaders;
private readonly ImmutableList<(Type EntityType, Type TypeReaderType)> _entityTypeReaders; private readonly ImmutableList<(Type EntityType, Type TypeReaderType)> _entityTypeReaders;
private readonly HashSet<ModuleInfo> _moduleDefs; private readonly HashSet<ModuleInfo> _moduleDefs;
@@ -77,6 +78,15 @@ namespace Discord.Commands
/// </summary> /// </summary>
public ILookup<Type, TypeReader> TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new { y.Key, y.Value })).ToLookup(x => x.Key, x => x.Value); public ILookup<Type, TypeReader> TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new { y.Key, y.Value })).ToLookup(x => x.Key, x => x.Value);


/// <summary>
/// Represents all entity type reader <see cref="Type" />s loaded within <see cref="CommandService"/>.
/// </summary>
/// <returns>
/// A <see cref="ILookup{TKey, TElement}"/>; the key is the object type to be read by the <see cref="TypeReader"/>,
/// while the element is the type of the <see cref="TypeReader"/> generic definition.
/// </returns>
public ILookup<Type, Type> EntityTypeReaders => _userEntityTypeReaders.SelectMany(x => x.Value.Select(y => new { x.Key, TypeReaderType = y })).ToLookup(x => x.Key, y => y.TypeReaderType);

/// <summary> /// <summary>
/// Initializes a new <see cref="CommandService"/> class. /// Initializes a new <see cref="CommandService"/> class.
/// </summary> /// </summary>
@@ -109,6 +119,7 @@ namespace Discord.Commands
_moduleDefs = new HashSet<ModuleInfo>(); _moduleDefs = new HashSet<ModuleInfo>();
_map = new CommandMap(this); _map = new CommandMap(this);
_typeReaders = new ConcurrentDictionary<Type, ConcurrentDictionary<Type, TypeReader>>(); _typeReaders = new ConcurrentDictionary<Type, ConcurrentDictionary<Type, TypeReader>>();
_userEntityTypeReaders = new ConcurrentDictionary<Type, ConcurrentQueue<Type>>();


_defaultTypeReaders = new ConcurrentDictionary<Type, TypeReader>(); _defaultTypeReaders = new ConcurrentDictionary<Type, TypeReader>();
foreach (var type in PrimitiveParsers.SupportedTypes) foreach (var type in PrimitiveParsers.SupportedTypes)
@@ -329,8 +340,6 @@ namespace Discord.Commands
/// type. /// type.
/// If <typeparamref name="T" /> is a <see cref="ValueType" />, a nullable <see cref="TypeReader" /> will /// If <typeparamref name="T" /> is a <see cref="ValueType" />, a nullable <see cref="TypeReader" /> will
/// also be added. /// also be added.
/// If a default <see cref="TypeReader" /> exists for <typeparamref name="T" />, a warning will be logged
/// and the default <see cref="TypeReader" /> will be replaced.
/// </summary> /// </summary>
/// <typeparam name="T">The object type to be read by the <see cref="TypeReader"/>.</typeparam> /// <typeparam name="T">The object type to be read by the <see cref="TypeReader"/>.</typeparam>
/// <param name="reader">An instance of the <see cref="TypeReader" /> to be added.</param> /// <param name="reader">An instance of the <see cref="TypeReader" /> to be added.</param>
@@ -341,17 +350,49 @@ namespace Discord.Commands
/// type. /// type.
/// If <paramref name="type" /> is a <see cref="ValueType" />, a nullable <see cref="TypeReader" /> for the /// If <paramref name="type" /> is a <see cref="ValueType" />, a nullable <see cref="TypeReader" /> for the
/// value type will also be added. /// value type will also be added.
/// If a default <see cref="TypeReader" /> exists for <paramref name="type" />, a warning will be logged and
/// the default <see cref="TypeReader" /> will be replaced.
/// </summary> /// </summary>
/// <param name="type">A <see cref="Type" /> instance for the type to be read.</param> /// <param name="type">A <see cref="Type" /> instance for the type to be read.</param>
/// <param name="reader">An instance of the <see cref="TypeReader" /> to be added.</param> /// <param name="reader">An instance of the <see cref="TypeReader" /> to be added.</param>
public void AddTypeReader(Type type, TypeReader reader) public void AddTypeReader(Type type, TypeReader reader)
{ {
if (_defaultTypeReaders.ContainsKey(type))
_ = _cmdLogger.WarningAsync($"The default TypeReader for {type.FullName} was replaced by {reader.GetType().FullName}." +
"To suppress this message, use AddTypeReader<T>(reader, true).");
AddTypeReader(type, reader, true);
var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary<Type, TypeReader>());
readers[reader.GetType()] = reader;

if (type.GetTypeInfo().IsValueType)
AddNullableTypeReader(type, reader);
}
/// <summary>
/// Adds a custom entity <see cref="TypeReader" /> to this <see cref="CommandService" /> for the supplied
/// object type.
/// </summary>
/// <example>
/// <para>The following example adds a custom entity reader to this <see cref="CommandService"/>.</para>
/// <code language="cs" region="AddEntityTypeReader"
/// source="..\Discord.Net.Examples\Commands\CommandService.Examples.cs" />
/// </example>
/// <typeparam name="T">The object type to be read by the <see cref="TypeReader"/>.</typeparam>
/// <param name="typeReaderGenericType">A generic type definition (with one open argument) of the <see cref="TypeReader" />.</param>
public void AddEntityTypeReader<T>(Type typeReaderGenericType)
=> AddEntityTypeReader(typeof(T), typeReaderGenericType);
/// <summary>
/// Adds a custom entity <see cref="TypeReader" /> to this <see cref="CommandService" /> for the supplied
/// object type.
/// </summary>
/// <param name="type">A <see cref="Type" /> instance for the type to be read.</param>
/// <param name="typeReaderGenericType">A generic type definition (with one open argument) of the <see cref="TypeReader" />.</param>
public void AddEntityTypeReader(Type type, Type typeReaderGenericType)
{
if (!typeReaderGenericType.IsGenericTypeDefinition)
throw new ArgumentException("TypeReader type must be a generic type definition.", nameof(typeReaderGenericType));
Type[] genericArgs = typeReaderGenericType.GetGenericArguments();
if (genericArgs.Length != 1)
throw new ArgumentException("TypeReader type must accept one and only one open generic argument.", nameof(typeReaderGenericType));
if (!genericArgs[0].IsGenericParameter)
throw new ArgumentException("TypeReader type must accept one and only one open generic argument.", nameof(typeReaderGenericType));
if (!genericArgs[0].GenericParameterAttributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint))
throw new ArgumentException("TypeReader generic argument must have a reference type constraint.", nameof(typeReaderGenericType));
var readers = _userEntityTypeReaders.GetOrAdd(type, x => new ConcurrentQueue<Type>());
readers.Enqueue(typeReaderGenericType);
} }
/// <summary> /// <summary>
/// Adds a custom <see cref="TypeReader" /> to this <see cref="CommandService" /> for the supplied object /// Adds a custom <see cref="TypeReader" /> to this <see cref="CommandService" /> for the supplied object
@@ -359,14 +400,20 @@ namespace Discord.Commands
/// If <typeparamref name="T" /> is a <see cref="ValueType" />, a nullable <see cref="TypeReader" /> will /// If <typeparamref name="T" /> is a <see cref="ValueType" />, a nullable <see cref="TypeReader" /> will
/// also be added. /// also be added.
/// </summary> /// </summary>
/// <example>
/// <para>The following example adds a custom entity reader to this <see cref="CommandService"/>.</para>
/// <code language="cs" region="AddEntityTypeReader2"
/// source="..\Discord.Net.Examples\Commands\CommandService.Examples.cs" />
/// </example>
/// <typeparam name="T">The object type to be read by the <see cref="TypeReader"/>.</typeparam> /// <typeparam name="T">The object type to be read by the <see cref="TypeReader"/>.</typeparam>
/// <param name="reader">An instance of the <see cref="TypeReader" /> to be added.</param> /// <param name="reader">An instance of the <see cref="TypeReader" /> to be added.</param>
/// <param name="replaceDefault"> /// <param name="replaceDefault">
/// Defines whether the <see cref="TypeReader"/> should replace the default one for /// Defines whether the <see cref="TypeReader"/> should replace the default one for
/// <see cref="Type" /> if it exists. /// <see cref="Type" /> if it exists.
/// </param> /// </param>
[Obsolete("This method is deprecated. Use the method without the replaceDefault argument.")]
public void AddTypeReader<T>(TypeReader reader, bool replaceDefault) public void AddTypeReader<T>(TypeReader reader, bool replaceDefault)
=> AddTypeReader(typeof(T), reader, replaceDefault);
=> AddTypeReader(typeof(T), reader);
/// <summary> /// <summary>
/// Adds a custom <see cref="TypeReader" /> to this <see cref="CommandService" /> for the supplied object /// Adds a custom <see cref="TypeReader" /> to this <see cref="CommandService" /> for the supplied object
/// type. /// type.
@@ -379,27 +426,10 @@ namespace Discord.Commands
/// Defines whether the <see cref="TypeReader"/> should replace the default one for <see cref="Type" /> if /// Defines whether the <see cref="TypeReader"/> should replace the default one for <see cref="Type" /> if
/// it exists. /// it exists.
/// </param> /// </param>
[Obsolete("This method is deprecated. Use the method without the replaceDefault argument.")]
public void AddTypeReader(Type type, TypeReader reader, bool replaceDefault) public void AddTypeReader(Type type, TypeReader reader, bool replaceDefault)
{
if (replaceDefault && HasDefaultTypeReader(type))
{
_defaultTypeReaders.AddOrUpdate(type, reader, (k, v) => reader);
if (type.GetTypeInfo().IsValueType)
{
var nullableType = typeof(Nullable<>).MakeGenericType(type);
var nullableReader = NullableTypeReader.Create(type, reader);
_defaultTypeReaders.AddOrUpdate(nullableType, nullableReader, (k, v) => nullableReader);
}
}
else
{
var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary<Type, TypeReader>());
readers[reader.GetType()] = reader;
=> AddTypeReader(type, reader);


if (type.GetTypeInfo().IsValueType)
AddNullableTypeReader(type, reader);
}
}
internal bool HasDefaultTypeReader(Type type) internal bool HasDefaultTypeReader(Type type)
{ {
if (_defaultTypeReaders.ContainsKey(type)) if (_defaultTypeReaders.ContainsKey(type))
@@ -416,10 +446,39 @@ namespace Discord.Commands
var nullableReader = NullableTypeReader.Create(valueType, valueTypeReader); var nullableReader = NullableTypeReader.Create(valueType, valueTypeReader);
readers[nullableReader.GetType()] = nullableReader; readers[nullableReader.GetType()] = nullableReader;
} }
internal IDictionary<Type, TypeReader> GetTypeReaders(Type type)
internal IEnumerable<KeyValuePair<Type, TypeReader>> GetTypeReaders(Type type, bool includeOverride)
{ {
if (_typeReaders.TryGetValue(type, out var definedTypeReaders)) if (_typeReaders.TryGetValue(type, out var definedTypeReaders))
return definedTypeReaders;
return includeOverride ? definedTypeReaders : definedTypeReaders.Where(x => !x.Value.IsOverride);

var assignableEntityReaders = _userEntityTypeReaders.Where(x => x.Key.IsAssignableFrom(type));

int assignableTo = -1;
KeyValuePair<Type, ConcurrentQueue<Type>>? entityReaders = null;
foreach (var entityReader in assignableEntityReaders)
{
int assignables = assignableEntityReaders.Sum(x => !x.Equals(entityReader) && x.Key.IsAssignableFrom(entityReader.Key) ? 1 : 0);
if (assignableTo == -1)
{
// First time
assignableTo = assignables;
entityReaders = entityReader;
}
// Try to get the most specific type reader, i.e. IMessageChannel is assignable to IChannel, but not the inverse
else if (assignables > assignableTo)
{
assignableTo = assignables;
entityReaders = entityReader;
}
}

if (entityReaders != null)
{
var entityTypeReaderType = entityReaders.Value.Value.First();
TypeReader reader = Activator.CreateInstance(entityTypeReaderType.MakeGenericType(type)) as TypeReader;
AddTypeReader(type, reader);
return GetTypeReaders(type, false);
}
return null; return null;
} }
internal TypeReader GetDefaultTypeReader(Type type) internal TypeReader GetDefaultTypeReader(Type type)


+ 2
- 2
src/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs View File

@@ -136,8 +136,8 @@ namespace Discord.Commands
var overridden = prop.GetCustomAttribute<OverrideTypeReaderAttribute>(); var overridden = prop.GetCustomAttribute<OverrideTypeReaderAttribute>();
var reader = (overridden != null) var reader = (overridden != null)
? ModuleClassBuilder.GetTypeReader(_commands, elemType, overridden.TypeReader, services) ? ModuleClassBuilder.GetTypeReader(_commands, elemType, overridden.TypeReader, services)
: (_commands.GetDefaultTypeReader(elemType)
?? _commands.GetTypeReaders(elemType).FirstOrDefault().Value);
: (_commands.GetTypeReaders(elemType, false)?.FirstOrDefault().Value
?? _commands.GetDefaultTypeReader(elemType));


if (reader != null) if (reader != null)
{ {


+ 1
- 0
src/Discord.Net.Commands/Readers/TypeReader.cs View File

@@ -8,6 +8,7 @@ namespace Discord.Commands
/// </summary> /// </summary>
public abstract class TypeReader public abstract class TypeReader
{ {
internal bool IsOverride { get; set; } = false;
/// <summary> /// <summary>
/// Attempts to parse the <paramref name="input"/> into the desired type. /// Attempts to parse the <paramref name="input"/> into the desired type.
/// </summary> /// </summary>


+ 55
- 0
src/Discord.Net.Examples/Commands/CommandService.Examples.cs View File

@@ -0,0 +1,55 @@
using Discord.Commands;
using JetBrains.Annotations;
using System;
using System.Threading.Tasks;

namespace Discord.Net.Examples.Commands
{
[PublicAPI]
internal class CommandServiceExamples
{
#region AddEntityTypeReader

public void AddCustomUserEntityReader(CommandService commandService)
{
commandService.AddEntityTypeReader<IUser>(typeof(MyUserTypeReader<>));
}

public class MyUserTypeReader<T> : TypeReader
where T : class, IUser
{
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
{
if (ulong.TryParse(input, out var id))
return ((await context.Client.GetUserAsync(id)) is T user)
? TypeReaderResult.FromSuccess(user)
: TypeReaderResult.FromError(CommandError.ObjectNotFound, "User not found.");
return TypeReaderResult.FromError(CommandError.ParseFailed, "Couldn't parse input to ulong.");
}
}

#endregion

#region AddEntityTypeReader2

public void AddCustomChannelEntityReader(CommandService commandService)
{
commandService.AddEntityTypeReader<IUser>(typeof(MyUserTypeReader<>));
}

public class MyChannelTypeReader<T> : TypeReader
where T : class, IChannel
{
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
{
if (ulong.TryParse(input, out var id))
return ((await context.Client.GetChannelAsync(id)) is T channel)
? TypeReaderResult.FromSuccess(channel)
: TypeReaderResult.FromError(CommandError.ObjectNotFound, "Channel not found.");
return TypeReaderResult.FromError(CommandError.ParseFailed, "Couldn't parse input to ulong.");
}
}

#endregion
}
}

+ 1
- 0
src/Discord.Net.Examples/Discord.Net.Examples.csproj View File

@@ -13,6 +13,7 @@
</ItemGroup> </ItemGroup>


<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Discord.Net.Commands\Discord.Net.Commands.csproj" />
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> <ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />
<ProjectReference Include="..\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" /> <ProjectReference Include="..\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" />
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3" /> <PackageReference Include="JetBrains.Annotations" Version="2019.1.3" />


Loading…
Cancel
Save