Skip to content

Commit

Permalink
Merge pull request #74 from jonnii/refactor-safe-stream
Browse files Browse the repository at this point in the history
Safe stream and proper disposal
  • Loading branch information
jonnii authored Jan 2, 2018
2 parents ab0a3c7 + 4976b27 commit 3ae7460
Show file tree
Hide file tree
Showing 12 changed files with 119 additions and 104 deletions.
11 changes: 6 additions & 5 deletions src/SpeakEasy.IntegrationTests/BasicAsyncHttpMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ public BasicAsyncHttpMethods(ApiFixture fixture)
[Fact]
public async void ShouldGetAsync()
{
var response = await client.Get("products/1");

Assert.Contains(":1337/api/products/1", response.State.RequestUrl.ToString());
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
using (var response = await client.Get("products/1"))
{
Assert.Contains(":1337/api/products/1", response.State.RequestUrl.ToString());
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}

[Fact]
Expand Down Expand Up @@ -248,7 +249,7 @@ public async void ShouldCallbackWithState()
var message = string.Empty;

await client.Post("locations")
.On(HttpStatusCode.BadRequest, status => { message = status.StatusDescription; });
.On(HttpStatusCode.BadRequest, status => { message = status.ReasonPhrase; });

Assert.Equal("titles cannot start with 'bad'", message);
}
Expand Down
13 changes: 5 additions & 8 deletions src/SpeakEasy.Specifications/Fixtures/HttpResponses.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System;
using System.IO;
using System.IO;
using System.Net;
using System.Net.Http;

namespace SpeakEasy.Specifications.Fixtures
{
Expand All @@ -16,13 +16,10 @@ public static HttpResponse Create(ISerializer serializer, Stream bodyStream, Htt
return new HttpResponse(
serializer,
bodyStream,
new HttpResponseState(code,
"status description",
new Uri("http://example.com/companies"),
new HttpResponseState(
new HttpResponseMessage { StatusCode = code },
cookies,
"contentType",
"server",
null), null);
"contentType"), null);
}
}
}
25 changes: 9 additions & 16 deletions src/SpeakEasy.Specifications/HttpResponseHandlerSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,28 @@
namespace SpeakEasy.Specifications
{
[Subject(typeof(HttpResponseHandler))]
class HttpResponseHandlerSpecs : WithSubject<HttpResponseHandler>
class HttpResponseHandlerSpecs : WithFakes
{
class when_unwrapping_as
{
static ISerializer deserializer;

Establish context = () =>
{
deserializer = An<ISerializer>();
static HttpResponseHandler handler;

The<IHttpResponseWithBody>().WhenToldTo(r => r.Deserializer).Return(deserializer);
};
Establish context = () =>
handler = new HttpResponseHandler(The<IHttpResponse>(), The<ISerializer>(), new SingleUseStream(new MemoryStream(Encoding.UTF8.GetBytes("abcd"))));

class when_unwrapping_as
{
Because of = () =>
Subject.As<Company>();
handler.As<Company>();

It should_deserialize_with_deserializer = () =>
deserializer.WasToldTo(d => d.Deserialize<Company>(Param.IsAny<Stream>()));
The<ISerializer>().WasToldTo(d => d.Deserialize<Company>(Param.IsAny<Stream>()));
}

class when_getting_as_byte_array
{
static byte[] bytes;

Establish context = () =>
The<IHttpResponseWithBody>().WhenToldTo(r => r.Body).Return(new MemoryStream(Encoding.UTF8.GetBytes("abcd")));

Because of = () =>
bytes = Subject.AsByteArray().Await();
bytes = handler.AsByteArray().Await();

It should_get_bytes = () =>
bytes.Length.ShouldBeGreaterThan(0);
Expand Down
2 changes: 0 additions & 2 deletions src/SpeakEasy.Specifications/RequestRunnerSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ namespace SpeakEasy.Specifications
[Subject(typeof(RequestRunner))]
class RequestRunnerSpecs : WithSubject<RequestRunner>
{
static HttpRequestMessage webRequest;

static IHttpRequest request;

Establish context = () =>
Expand Down
27 changes: 17 additions & 10 deletions src/SpeakEasy/HttpResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,21 @@

namespace SpeakEasy
{
public class HttpResponse : IHttpResponseWithBody
public class HttpResponse : IHttpResponse
{
private readonly ISerializer deserializer;

private readonly SingleUseStream body;

public HttpResponse(
ISerializer deserializer,
Stream body,
IHttpResponseState state,
HttpContentHeaders headers)
{
Deserializer = deserializer;
Body = body;
this.deserializer = deserializer;
this.body = new SingleUseStream(body);

State = state;
Headers = headers;
}
Expand All @@ -23,10 +28,6 @@ public HttpResponse(

public IHttpResponseState State { get; }

public Stream Body { get; }

public ISerializer Deserializer { get; }

public string ContentType => State.ContentType;

public HttpStatusCode StatusCode => State.StatusCode;
Expand All @@ -53,7 +54,7 @@ public IHttpResponse On<T>(HttpStatusCode code, Action<T> action)
return this;
}

var deserialied = Deserializer.Deserialize<T>(Body);
var deserialied = deserializer.Deserialize<T>(body.GetAndConsumeStream());
action(deserialied);

return this;
Expand Down Expand Up @@ -86,7 +87,7 @@ public IHttpResponseHandler On(HttpStatusCode code)
OnIncorrectStatusCode(code);
}

return new HttpResponseHandler(this);
return new HttpResponseHandler(this, deserializer, body);
}

public IHttpResponseHandler On(int code)
Expand All @@ -101,7 +102,7 @@ public IHttpResponseHandler OnOk()
OnIncorrectStatusCode(HttpStatusCode.OK);
}

return new HttpResponseHandler(this);
return new HttpResponseHandler(this, deserializer, body);
}

private void OnIncorrectStatusCode(HttpStatusCode expected)
Expand Down Expand Up @@ -133,5 +134,11 @@ public bool IsOk()
{
return Is(HttpStatusCode.OK);
}

public void Dispose()
{
body.Dispose();
State.Dispose();
}
}
}
26 changes: 13 additions & 13 deletions src/SpeakEasy/HttpResponseHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,29 @@ namespace SpeakEasy
{
internal class HttpResponseHandler : IHttpResponseHandler
{
private readonly IHttpResponseWithBody response;
private readonly IHttpResponse response;

public HttpResponseHandler(IHttpResponseWithBody response)
private readonly ISerializer serializer;

private readonly SingleUseStream body;

public HttpResponseHandler(IHttpResponse response, ISerializer serializer, SingleUseStream body)
{
this.response = response;
this.serializer = serializer;
this.body = body;
}

public IHttpResponse Response => response;

public object As(Type type)
{
var deserializer = response.Deserializer;

return deserializer.Deserialize(response.Body, type);
return serializer.Deserialize(body.GetAndConsumeStream(), type);
}

public T As<T>()
{
var deserializer = response.Deserializer;

return deserializer.Deserialize<T>(response.Body);
return serializer.Deserialize<T>(body.GetAndConsumeStream());
}

public T As<T>(Func<IHttpResponseHandler, T> constructor)
Expand All @@ -41,18 +43,16 @@ public Task<byte[]> AsByteArray()

public async Task<byte[]> AsByteArray(int bufferSize)
{
var body = response.Body;

using (var copy = new MemoryStream())
{
await body.CopyToAsync(copy, bufferSize).ConfigureAwait(false);
await body.GetAndConsumeStream().CopyToAsync(copy, bufferSize).ConfigureAwait(false);
return copy.ToArray();
}
}

public async Task<string> AsString()
{
using (var reader = new StreamReader(response.Body))
using (var reader = new StreamReader(body.GetAndConsumeStream()))
{
return await reader.ReadToEndAsync().ConfigureAwait(false);
}
Expand All @@ -66,7 +66,7 @@ public IFile AsFile()
contentDisposition.Name,
contentDisposition.FileName,
response.ContentType,
response.Body);
body.GetAndConsumeStream());
}
}
}
35 changes: 16 additions & 19 deletions src/SpeakEasy/HttpResponseState.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http;

namespace SpeakEasy
{
Expand All @@ -9,40 +9,37 @@ namespace SpeakEasy
/// </summary>
public class HttpResponseState : IHttpResponseState
{
private readonly HttpContentHeaders headers;
private readonly HttpResponseMessage httpResponseMessage;

public HttpResponseState(
HttpStatusCode statusCode,
string statusDescription,
Uri requestUrl,
HttpResponseMessage httpResponseMessage,
Cookie[] cookies,
string contentType,
string server,
HttpContentHeaders headers)
string contentType)
{
this.headers = headers;
StatusCode = statusCode;
StatusDescription = statusDescription;
RequestUrl = requestUrl;
this.httpResponseMessage = httpResponseMessage;
Cookies = cookies;
ContentType = contentType;
Server = server;
}

public string Server { get; }
public string Server => httpResponseMessage.Headers.Server.ToString();

public string ContentEncoding => headers.ContentEncoding.ToString();
public string ContentEncoding => httpResponseMessage.Content.Headers.ContentEncoding.ToString();

public DateTime LastModified => headers.LastModified.GetValueOrDefault(DateTime.UtcNow).Date;
public DateTime LastModified => httpResponseMessage.Content.Headers.LastModified.GetValueOrDefault(DateTime.UtcNow).Date;

public HttpStatusCode StatusCode { get; }
public HttpStatusCode StatusCode => httpResponseMessage.StatusCode;

public string StatusDescription { get; }
public string ReasonPhrase => httpResponseMessage.ReasonPhrase;

public Uri RequestUrl { get; }
public Uri RequestUrl => httpResponseMessage.RequestMessage.RequestUri;

public Cookie[] Cookies { get; }

public string ContentType { get; }

public void Dispose()
{
httpResponseMessage.Dispose();
}
}
}
2 changes: 1 addition & 1 deletion src/SpeakEasy/IHttpResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace SpeakEasy
/// A chainable http response which gives you access to all the data available
/// on a response to an http service
/// </summary>
public interface IHttpResponse
public interface IHttpResponse : IDisposable
{
HttpContentHeaders Headers { get; }

Expand Down
12 changes: 5 additions & 7 deletions src/SpeakEasy/IHttpResponseState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,18 @@ namespace SpeakEasy
/// <summary>
/// An IHttpResponseState contains all the response state from an http endpoint.
/// </summary>
public interface IHttpResponseState
public interface IHttpResponseState : IDisposable
{
Uri RequestUrl { get; }

string Server { get; }

string ContentType { get; }

string ContentEncoding { get; }
HttpStatusCode StatusCode { get; }

string StatusDescription { get; }
string ReasonPhrase { get; }

DateTime LastModified { get; }
string ContentEncoding { get; }

HttpStatusCode StatusCode { get; }
DateTime LastModified { get; }
}
}
11 changes: 0 additions & 11 deletions src/SpeakEasy/IHttpResponseWithBody.cs

This file was deleted.

22 changes: 10 additions & 12 deletions src/SpeakEasy/Middleware/RequestMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,14 @@ public async Task<IHttpResponse> Invoke(IHttpRequest request, CancellationToken
await serializedBody.WriteTo(httpRequest, cancellationToken).ConfigureAwait(false);

var httpResponse = await client.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
var responseStream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);

return CreateHttpResponse(
httpRequest,
httpResponse,
responseStream);
{
var responseStream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);

return CreateHttpResponse(
httpRequest,
httpResponse,
responseStream);
}
}
}

Expand Down Expand Up @@ -106,13 +108,9 @@ public IHttpResponse CreateHttpResponse(HttpRequestMessage httpRequest, HttpResp
: cookieCollection.Cast<Cookie>().ToArray();

var state = new HttpResponseState(
httpResponse.StatusCode,
httpResponse.ReasonPhrase,
httpResponse.RequestMessage.RequestUri,
httpResponse,
cookies,
contentType,
httpResponse.Headers.Server.ToString(),
httpResponse.Content.Headers);
contentType);

return new HttpResponse(
deserializer,
Expand Down
Loading

0 comments on commit 3ae7460

Please sign in to comment.