Browse Source

Add NamedArgumentTypeReader

pull/1123/head
Joe4evr 7 years ago
parent
commit
ffd6b92671
5 changed files with 217 additions and 4 deletions
  1. +1
    -1
      src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
  2. +18
    -2
      src/Discord.Net.Commands/Builders/ParameterBuilder.cs
  3. +139
    -0
      src/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs
  4. +2
    -1
      test/Discord.Net.Tests/Discord.Net.Tests.csproj
  5. +57
    -0
      test/Discord.Net.Tests/Tests.TypeReaders.cs

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

@@ -280,7 +280,7 @@ namespace Discord.Commands
}
}

private 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;


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

@@ -56,11 +56,27 @@ namespace Discord.Commands.Builders

private TypeReader GetReader(Type type)
{
var readers = Command.Module.Service.GetTypeReaders(type);
var commands = Command.Module.Service;
if (type.GetTypeInfo().GetCustomAttribute<NamedArgumentTypeAttribute>() != null)
{
IsRemainder = true;
var reader = commands.GetTypeReaders(type)?.FirstOrDefault().Value;
if (reader == null)
{
var readerType = typeof(NamedArgumentTypeReader<>).MakeGenericType(new[] { type });
reader = (TypeReader)Activator.CreateInstance(readerType, new[] { commands });
commands.AddTypeReader(type, reader);
}

return reader;
}


var readers = commands.GetTypeReaders(type);
if (readers != null)
return readers.FirstOrDefault().Value;
else
return Command.Module.Service.GetDefaultTypeReader(type);
return commands.GetDefaultTypeReader(type);
}

public ParameterBuilder WithSummary(string summary)


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

@@ -0,0 +1,139 @@

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;

namespace Discord.Commands
{
internal sealed class NamedArgumentTypeReader<T> : TypeReader
where T : class, new()
{
private static readonly TypeInfo _tInfo = typeof(T).GetTypeInfo();
private static readonly IReadOnlyDictionary<string, PropertyInfo> _tProps = _tInfo.DeclaredProperties
.Where(p => p.SetMethod != null && p.SetMethod.IsPublic && !p.SetMethod.IsStatic)
.ToImmutableDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase);

private readonly CommandService _commands;

public NamedArgumentTypeReader(CommandService commands)
{
_commands = commands;
}

public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
{
var result = new T();
var state = ReadState.LookingForParameter;
int beginRead = 0, currentRead = 0;

while (state != ReadState.End)
{
var prop = Read(out var arg);
var propVal = await ReadArgumentAsync(prop, arg).ConfigureAwait(false);
if (propVal != null)
prop.SetMethod.Invoke(result, new[] { propVal });
else
return TypeReaderResult.FromError(CommandError.ParseFailed, $"Could not parse the argument for the parameter '{prop.Name}' as type '{prop.PropertyType}'.");
}

return TypeReaderResult.FromSuccess(result);

PropertyInfo Read(out string arg)
{
string currentParam = null;
char match = '\0';

for (; currentRead < input.Length; currentRead++)
{
var currentChar = input[currentRead];
switch (state)
{
case ReadState.LookingForParameter:
if (Char.IsWhiteSpace(currentChar))
continue;
else
{
beginRead = currentRead;
state = ReadState.InParameter;
}
break;
case ReadState.InParameter:
if (currentChar != ':')
continue;
else
{
currentParam = input.Substring(beginRead, currentRead - beginRead);
state = ReadState.LookingForArgument;
}
break;
case ReadState.LookingForArgument:
if (Char.IsWhiteSpace(currentChar))
continue;
else
{
beginRead = currentRead;
state = (QuotationAliasUtils.GetDefaultAliasMap.TryGetValue(currentChar, out match))
? ReadState.InQuotedArgument
: ReadState.InArgument;
}
break;
case ReadState.InArgument:
if (!Char.IsWhiteSpace(currentChar))
continue;
else
return GetPropAndValue(out arg);
case ReadState.InQuotedArgument:
if (currentChar != match)
continue;
else
return GetPropAndValue(out arg);
}
}

return GetPropAndValue(out arg);

PropertyInfo GetPropAndValue(out string argv)
{
state = (currentRead == input.Length)
? ReadState.End
: ReadState.LookingForParameter;

var prop = _tProps[currentParam];
argv = input.Substring(beginRead, currentRead - beginRead);
return prop;
}
}

async Task<object> ReadArgumentAsync(PropertyInfo prop, string arg)
{
var overridden = prop.GetCustomAttribute<OverrideTypeReaderAttribute>();
var reader = (overridden != null)
? ModuleClassBuilder.GetTypeReader(_commands, prop.PropertyType, overridden.TypeReader, services)
: (_commands.GetDefaultTypeReader(prop.PropertyType)
?? _commands.GetTypeReaders(prop.PropertyType).FirstOrDefault().Value);

if (reader != null)
{
var readResult = await reader.ReadAsync(context, arg, services).ConfigureAwait(false);
return (readResult.IsSuccess)
? readResult.BestMatch
: null;
}
return null;
}
}

private enum ReadState
{
LookingForParameter,
InParameter,
LookingForArgument,
InArgument,
InQuotedArgument,
End
}
}
}

+ 2
- 1
test/Discord.Net.Tests/Discord.Net.Tests.csproj View File

@@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<RootNamespace>Discord</RootNamespace>
<TargetFramework>netcoreapp1.1</TargetFramework>
<DebugType>portable</DebugType>
<PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81</PackageTargetFallback>
</PropertyGroup>
<ItemGroup>


+ 57
- 0
test/Discord.Net.Tests/Tests.TypeReaders.cs View File

@@ -0,0 +1,57 @@
using System;
using System.Threading.Tasks;
using Discord.Commands;
using Xunit;

namespace Discord
{
public sealed class TypeReaderTests
{
[Fact]
public async Task TestNamedArgumentReader()
{
var commands = new CommandService();
var module = await commands.AddModuleAsync<TestModule>(null);

Assert.NotNull(module);
Assert.NotEmpty(module.Commands);

var cmd = module.Commands[0];
Assert.NotNull(cmd);
Assert.NotEmpty(cmd.Parameters);

var param = cmd.Parameters[0];
Assert.NotNull(param);
Assert.True(param.IsRemainder);

var result = await param.ParseAsync(null, "foo: 42 bar: hello");
Assert.True(result.IsSuccess);

var m = result.BestMatch as ArgumentType;
Assert.NotNull(m);
Assert.Equal(expected: 42, actual: m.Foo);
Assert.Equal(expected: "hello", actual: m.Bar);
}
}

[NamedArgumentType]
public sealed class ArgumentType
{
public int Foo { get; set; }

[OverrideTypeReader(typeof(CustomTypeReader))]
public string Bar { get; set; }
}

public sealed class CustomTypeReader : TypeReader
{
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
=> Task.FromResult(TypeReaderResult.FromSuccess(input));
}

public sealed class TestModule : ModuleBase
{
[Command("test")]
public Task TestCommand(ArgumentType arg) => Task.Delay(0);
}
}

Loading…
Cancel
Save