Skip to content

Commit

Permalink
Merge pull request #675 from SteamRE/674-webapi-args
Browse files Browse the repository at this point in the history
Add support to WebAPI for a consumer-supplied dictionary of arguments
  • Loading branch information
yaakov-h authored May 14, 2019
2 parents ada2c52 + 2b209cb commit 4a24c3d
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 54 deletions.
28 changes: 18 additions & 10 deletions SteamKit2/SteamKit2/Steam/WebAPI/WebAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -366,21 +366,29 @@ public void Dispose()
/// </exception>
public override bool TryInvokeMember( InvokeMemberBinder binder, object[] args, out object result )
{
if ( binder.CallInfo.ArgumentNames.Count != args.Length )
IDictionary<string, object> methodArgs;

if ( args.Length == 1 && binder.CallInfo.ArgumentNames.Count == 0 && args[ 0 ] is IDictionary<string, object> 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<string, object>();

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 ) )
{
Expand All @@ -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;
Expand Down
13 changes: 13 additions & 0 deletions SteamKit2/SteamKit2/Util/KeyValuePairExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Collections.Generic;

namespace SteamKit2
{
static class KeyValuePairExtensions
{
public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> kvp, out TKey key, out TValue value)
{
key = kvp.Key;
value = kvp.Value;
}
}
}
1 change: 1 addition & 0 deletions SteamKit2/Tests/Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
<PackageReference Include="xunit" Version="2.2.0" />
<PackageReference Include="xunit.analyzers" Version="0.10.0" />
Expand Down
91 changes: 47 additions & 44 deletions SteamKit2/Tests/WebAPIFacts.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<WebAPIRequestException>(() => (Task)iface.PerformFooOperation());
}

await Assert.ThrowsAsync<WebAPIRequestException>(() => (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<string, object>
{
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<HttpResponseMessage> 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<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken )
{
OnSocketAccepted(ar);
MostRecentRequest = request;

return Task.FromResult( new HttpResponseMessage( HttpStatusCode.OK )
{
Content = new ByteArrayContent( Array.Empty<byte>() )
});
}
}
}
Expand Down

0 comments on commit 4a24c3d

Please sign in to comment.