From 9db700e238918f2dbebe84884f32b93786b34f9b Mon Sep 17 00:00:00 2001 From: Yaakov Date: Sun, 5 May 2019 14:22:39 +1000 Subject: [PATCH 1/5] Add WebAPI support for a consumer-supplied dictionary of arguments. Fixes #674. --- SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs | 29 +++++++++++--- SteamKit2/Tests/Tests.csproj | 1 + SteamKit2/Tests/WebAPIFacts.cs | 46 ++++++++++++++++++++++ 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs b/SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs index 78369142f..c6a1966b6 100644 --- a/SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs +++ b/SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs @@ -366,20 +366,37 @@ 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 = new Dictionary(); - var apiArgs = new Dictionary(); + for ( var x = 0; x < args.Length; x++ ) + { + string argName = binder.CallInfo.ArgumentNames[ x ]; + object argValue = args[ x ]; + methodArgs.Add( argName, argValue ); + } + } + + 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 kvp in methodArgs ) { - string argName = binder.CallInfo.ArgumentNames[ x ]; - object argValue = args[ x ]; + string argName = kvp.Key; + object argValue = kvp.Value; // method is a reserved param for selecting the http request method if ( argName.Equals( "method", StringComparison.OrdinalIgnoreCase ) ) 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..09cd5f767 100644 --- a/SteamKit2/Tests/WebAPIFacts.cs +++ b/SteamKit2/Tests/WebAPIFacts.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; using System.Net.Sockets; +using System.Threading; using System.Threading.Tasks; using SteamKit2; using Xunit; @@ -61,6 +63,35 @@ public async Task ThrowsWebAPIRequestExceptionIfRequestUnsuccessful() } } + [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 + { + [ "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) { @@ -96,5 +127,20 @@ void OnSocketAccepted(IAsyncResult result) OnSocketAccepted(ar); } } + + sealed class CaturingHttpMessageHandler : HttpMessageHandler + { + public HttpRequestMessage MostRecentRequest { get; private set; } + + protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken ) + { + MostRecentRequest = request; + + return Task.FromResult( new HttpResponseMessage( HttpStatusCode.OK ) + { + Content = new ByteArrayContent( Array.Empty() ) + }); + } + } } } From 89d10a151ce806b80ef294a20f5b034f64c5070b Mon Sep 17 00:00:00 2001 From: Yaakov Date: Sun, 5 May 2019 14:25:32 +1000 Subject: [PATCH 2/5] Simplify existing WebAPI test to remove custom HTTP listener --- SteamKit2/Tests/WebAPIFacts.cs | 55 ++++------------------------------ 1 file changed, 6 insertions(+), 49 deletions(-) diff --git a/SteamKit2/Tests/WebAPIFacts.cs b/SteamKit2/Tests/WebAPIFacts.cs index 09cd5f767..ee040ded4 100644 --- a/SteamKit2/Tests/WebAPIFacts.cs +++ b/SteamKit2/Tests/WebAPIFacts.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.IO; using System.Net; using System.Net.Http; -using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using SteamKit2; @@ -46,21 +44,10 @@ public void SteamConfigWebAPIInterface() [Fact] public async Task ThrowsWebAPIRequestExceptionIfRequestUnsuccessful() { - var listener = new TcpListener(new IPEndPoint(IPAddress.Loopback, 28123)); - listener.Start(); - try - { - AcceptAndAutoReplyNextSocket(listener); - - var baseUri = "http://localhost:28123"; - dynamic iface = WebAPI.GetAsyncInterface(new Uri(baseUri), "IFooService"); + var configuration = SteamConfiguration.Create( c => c.WithHttpClientFactory( () => new HttpClient( new ServiceUnavailableHttpMessageHandler() ) ) ); + dynamic iface = configuration.GetAsyncWebAPIInterface( "IFooService" ); - await Assert.ThrowsAsync(() => (Task)iface.PerformFooOperation()); - } - finally - { - listener.Stop(); - } + await Assert.ThrowsAsync(() => (Task)iface.PerformFooOperation()); } [Fact] @@ -92,40 +79,10 @@ public async Task UsesSingleParameterArgumentsDictionary() 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 - { - } - } - - var ar = listener.BeginAcceptSocket(OnSocketAccepted, null); - if (ar.CompletedSynchronously) - { - OnSocketAccepted(ar); - } + protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken ) + => Task.FromResult( new HttpResponseMessage( HttpStatusCode.ServiceUnavailable ) ); } sealed class CaturingHttpMessageHandler : HttpMessageHandler From 3c23d033542578344962be175fcf1c309485fa2e Mon Sep 17 00:00:00 2001 From: Yaakov Date: Sun, 5 May 2019 14:27:36 +1000 Subject: [PATCH 3/5] fix comment --- SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs b/SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs index c6a1966b6..ee616c0b8 100644 --- a/SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs +++ b/SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs @@ -380,6 +380,7 @@ public override bool TryInvokeMember( InvokeMemberBinder binder, object[] args, { methodArgs = new Dictionary(); + // convert named arguments into key value pairs for ( var x = 0; x < args.Length; x++ ) { string argName = binder.CallInfo.ArgumentNames[ x ]; @@ -392,7 +393,6 @@ public override bool TryInvokeMember( InvokeMemberBinder binder, object[] args, var apiArgs = new Dictionary(); var requestMethod = HttpMethod.Get; - // convert named arguments into key value pairs foreach ( var kvp in methodArgs ) { string argName = kvp.Key; From b339055e2e3fa20137eebe25aeb7d951428f76c2 Mon Sep 17 00:00:00 2001 From: Yaakov Date: Sun, 5 May 2019 14:29:12 +1000 Subject: [PATCH 4/5] Simplify --- SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs b/SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs index ee616c0b8..b7a2fe21f 100644 --- a/SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs +++ b/SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs @@ -378,16 +378,10 @@ public override bool TryInvokeMember( InvokeMemberBinder binder, object[] args, } else { - methodArgs = new Dictionary(); - - // convert named arguments into key value pairs - for ( var x = 0; x < args.Length; x++ ) - { - string argName = binder.CallInfo.ArgumentNames[ x ]; - object argValue = args[ x ]; - - methodArgs.Add( argName, argValue ); - } + methodArgs = Enumerable.Range( 0, args.Length ) + .ToDictionary( + x => binder.CallInfo.ArgumentNames[ x ], + x => args[ x ] ); } var apiArgs = new Dictionary(); @@ -395,8 +389,7 @@ public override bool TryInvokeMember( InvokeMemberBinder binder, object[] args, foreach ( var kvp in methodArgs ) { - string argName = kvp.Key; - object argValue = kvp.Value; + var ( argName, argValue) = ( kvp.Key, kvp.Value ); // method is a reserved param for selecting the http request method if ( argName.Equals( "method", StringComparison.OrdinalIgnoreCase ) ) @@ -408,11 +401,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; From 2b209cb9f771b479a941fad16648171803d23787 Mon Sep 17 00:00:00 2001 From: Yaakov Date: Sun, 5 May 2019 14:31:59 +1000 Subject: [PATCH 5/5] Simplify --- SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs | 4 +--- SteamKit2/SteamKit2/Util/KeyValuePairExtensions.cs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 SteamKit2/SteamKit2/Util/KeyValuePairExtensions.cs diff --git a/SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs b/SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs index b7a2fe21f..17ff4403f 100644 --- a/SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs +++ b/SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs @@ -387,10 +387,8 @@ public override bool TryInvokeMember( InvokeMemberBinder binder, object[] args, var apiArgs = new Dictionary(); var requestMethod = HttpMethod.Get; - foreach ( var kvp in methodArgs ) + foreach ( var ( argName, argValue ) in methodArgs ) { - var ( argName, argValue) = ( kvp.Key, kvp.Value ); - // method is a reserved param for selecting the http request method if ( argName.Equals( "method", StringComparison.OrdinalIgnoreCase ) ) { 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; + } + } +}