| @@ -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); | var readers = service.GetTypeReaders(paramType); | ||||
| TypeReader reader = null; | TypeReader reader = null; | ||||
| @@ -56,11 +56,27 @@ namespace Discord.Commands.Builders | |||||
| private TypeReader GetReader(Type type) | 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) | if (readers != null) | ||||
| return readers.FirstOrDefault().Value; | return readers.FirstOrDefault().Value; | ||||
| else | else | ||||
| return Command.Module.Service.GetDefaultTypeReader(type); | |||||
| return commands.GetDefaultTypeReader(type); | |||||
| } | } | ||||
| public ParameterBuilder WithSummary(string summary) | public ParameterBuilder WithSummary(string summary) | ||||
| @@ -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 | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,8 +1,9 @@ | |||||
| <Project Sdk="Microsoft.NET.Sdk"> | |||||
| <Project Sdk="Microsoft.NET.Sdk"> | |||||
| <PropertyGroup> | <PropertyGroup> | ||||
| <OutputType>Exe</OutputType> | <OutputType>Exe</OutputType> | ||||
| <RootNamespace>Discord</RootNamespace> | <RootNamespace>Discord</RootNamespace> | ||||
| <TargetFramework>netcoreapp1.1</TargetFramework> | <TargetFramework>netcoreapp1.1</TargetFramework> | ||||
| <DebugType>portable</DebugType> | |||||
| <PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81</PackageTargetFallback> | <PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81</PackageTargetFallback> | ||||
| </PropertyGroup> | </PropertyGroup> | ||||
| <ItemGroup> | <ItemGroup> | ||||
| @@ -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); | |||||
| } | |||||
| } | |||||