diff --git a/SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs b/SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs index 78369142f..17ff4403f 100644 --- a/SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs +++ b/SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs @@ -366,21 +366,29 @@ public void Dispose() /// public override bool TryInvokeMember( InvokeMemberBinder binder, object[] args, out object result ) { - if ( binder.CallInfo.ArgumentNames.Count != args.Length ) + IDictionary methodArgs; + + if ( args.Length == 1 && binder.CallInfo.ArgumentNames.Count == 0 && args[ 0 ] is IDictionary explicitArgs ) + { + methodArgs = explicitArgs; + } + else if ( binder.CallInfo.ArgumentNames.Count != args.Length ) { - throw new InvalidOperationException( "Argument mismatch in API call. All parameters must be passed as named arguments." ); + throw new InvalidOperationException( "Argument mismatch in API call. All parameters must be passed as named arguments, or as a single un-named dictionary argument." ); + } + else + { + methodArgs = Enumerable.Range( 0, args.Length ) + .ToDictionary( + x => binder.CallInfo.ArgumentNames[ x ], + x => args[ x ] ); } var apiArgs = new Dictionary(); - var requestMethod = HttpMethod.Get; - // convert named arguments into key value pairs - for ( int x = 0 ; x < args.Length ; x++ ) + foreach ( var ( argName, argValue ) in methodArgs ) { - string argName = binder.CallInfo.ArgumentNames[ x ]; - object argValue = args[ x ]; - // method is a reserved param for selecting the http request method if ( argName.Equals( "method", StringComparison.OrdinalIgnoreCase ) ) { @@ -391,11 +399,11 @@ public override bool TryInvokeMember( InvokeMemberBinder binder, object[] args, else if ( argValue is IEnumerable && !( argValue is string ) ) { int index = 0; - IEnumerable enumerable = argValue as IEnumerable; + var enumerable = argValue as IEnumerable; foreach ( object value in enumerable ) { - apiArgs.Add( String.Format( "{0}[{1}]", argName, index++ ), value ); + apiArgs.Add( string.Format( "{0}[{1}]", argName, index++ ), value ); } continue; diff --git a/SteamKit2/SteamKit2/Util/KeyValuePairExtensions.cs b/SteamKit2/SteamKit2/Util/KeyValuePairExtensions.cs new file mode 100644 index 000000000..4227cadc7 --- /dev/null +++ b/SteamKit2/SteamKit2/Util/KeyValuePairExtensions.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace SteamKit2 +{ + static class KeyValuePairExtensions + { + public static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) + { + key = kvp.Key; + value = kvp.Value; + } + } +} diff --git a/SteamKit2/Tests/Tests.csproj b/SteamKit2/Tests/Tests.csproj index abf74ab28..67aa7a3aa 100644 --- a/SteamKit2/Tests/Tests.csproj +++ b/SteamKit2/Tests/Tests.csproj @@ -7,6 +7,7 @@ + diff --git a/SteamKit2/Tests/WebAPIFacts.cs b/SteamKit2/Tests/WebAPIFacts.cs index 7c7e86d03..ee040ded4 100644 --- a/SteamKit2/Tests/WebAPIFacts.cs +++ b/SteamKit2/Tests/WebAPIFacts.cs @@ -1,8 +1,8 @@ using System; -using System.IO; +using System.Collections.Generic; using System.Net; using System.Net.Http; -using System.Net.Sockets; +using System.Threading; using System.Threading.Tasks; using SteamKit2; using Xunit; @@ -44,56 +44,59 @@ public void SteamConfigWebAPIInterface() [Fact] public async Task ThrowsWebAPIRequestExceptionIfRequestUnsuccessful() { - var listener = new TcpListener(new IPEndPoint(IPAddress.Loopback, 28123)); - listener.Start(); - try - { - AcceptAndAutoReplyNextSocket(listener); + var configuration = SteamConfiguration.Create( c => c.WithHttpClientFactory( () => new HttpClient( new ServiceUnavailableHttpMessageHandler() ) ) ); + dynamic iface = configuration.GetAsyncWebAPIInterface( "IFooService" ); - var baseUri = "http://localhost:28123"; - dynamic iface = WebAPI.GetAsyncInterface(new Uri(baseUri), "IFooService"); + await Assert.ThrowsAsync(() => (Task)iface.PerformFooOperation()); + } - await Assert.ThrowsAsync(() => (Task)iface.PerformFooOperation()); - } - finally + [Fact] + public async Task UsesSingleParameterArgumentsDictionary() + { + var capturingHandler = new CaturingHttpMessageHandler(); + var configuration = SteamConfiguration.Create( c => c.WithHttpClientFactory( () => new HttpClient( capturingHandler ) ) ); + + dynamic iface = configuration.GetAsyncWebAPIInterface( "IFooService" ); + + var args = new Dictionary { - listener.Stop(); - } + [ "f" ] = "foo", + [ "b" ] = "bar", + [ "method" ] = "PUT" + }; + + var response = await iface.PerformFooOperation2( args ); + + var request = capturingHandler.MostRecentRequest; + Assert.NotNull( request ); + Assert.Equal( "/IFooService/PerformFooOperation/v2", request.RequestUri.AbsolutePath ); + Assert.Equal( HttpMethod.Put, request.Method ); + + var formData = await request.Content.ReadAsFormDataAsync(); + Assert.Equal( 3, formData.Count ); + Assert.Equal( "foo", formData[ "f" ] ); + Assert.Equal( "bar", formData[ "b" ] ); + Assert.Equal( "vdf", formData[ "format" ] ); } - // Primitive HTTP listener function that always returns HTTP 503. - static void AcceptAndAutoReplyNextSocket(TcpListener listener) + sealed class ServiceUnavailableHttpMessageHandler : HttpMessageHandler { - void OnSocketAccepted(IAsyncResult result) - { - try - { - using (var socket = listener.EndAcceptSocket(result)) - using (var stream = new NetworkStream(socket)) - using (var reader = new StreamReader(stream)) - using (var writer = new StreamWriter(stream)) - { - string line; - do - { - line = reader.ReadLine(); - } - while (!string.IsNullOrEmpty(line)); - - writer.WriteLine("HTTP/1.1 503 Service Unavailable"); - writer.WriteLine("X-Response-Source: Unit Test"); - writer.WriteLine(); - } - } - catch - { - } - } + protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken ) + => Task.FromResult( new HttpResponseMessage( HttpStatusCode.ServiceUnavailable ) ); + } + + sealed class CaturingHttpMessageHandler : HttpMessageHandler + { + public HttpRequestMessage MostRecentRequest { get; private set; } - var ar = listener.BeginAcceptSocket(OnSocketAccepted, null); - if (ar.CompletedSynchronously) + protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken ) { - OnSocketAccepted(ar); + MostRecentRequest = request; + + return Task.FromResult( new HttpResponseMessage( HttpStatusCode.OK ) + { + Content = new ByteArrayContent( Array.Empty() ) + }); } } }