Skip to content

Commit

Permalink
Prototype dynamic response headers
Browse files Browse the repository at this point in the history
Prototype implementation to support justeattakeaway#473.
  • Loading branch information
martincostello committed Aug 24, 2022
1 parent b48a57c commit fb9440d
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 30 deletions.
198 changes: 168 additions & 30 deletions src/HttpClientInterception/HttpRequestInterceptionBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Just Eat, 2017. All rights reserved.
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.

using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Net;

namespace JustEat.HttpClientInterception;
Expand Down Expand Up @@ -426,10 +428,7 @@ public HttpRequestInterceptionBuilder WithContentHeader(string name, IEnumerable
throw new ArgumentNullException(nameof(values));
}

if (_contentHeaders is null)
{
_contentHeaders = new Dictionary<string, ICollection<string>>(StringComparer.OrdinalIgnoreCase);
}
_contentHeaders ??= new Dictionary<string, ICollection<string>>(StringComparer.OrdinalIgnoreCase);

if (!_contentHeaders.TryGetValue(name, out var current))
{
Expand Down Expand Up @@ -551,10 +550,7 @@ public HttpRequestInterceptionBuilder WithResponseHeader(string name, IEnumerabl
throw new ArgumentNullException(nameof(values));
}

if (_responseHeaders is null)
{
_responseHeaders = new Dictionary<string, ICollection<string>>(StringComparer.OrdinalIgnoreCase);
}
_responseHeaders ??= new Dictionary<string, ICollection<string>>(StringComparer.OrdinalIgnoreCase);

if (!_responseHeaders.TryGetValue(name, out ICollection<string>? current))
{
Expand Down Expand Up @@ -617,6 +613,29 @@ public HttpRequestInterceptionBuilder WithResponseHeaders(IDictionary<string, IC
return this;
}

/// <summary>
/// Sets a delegate to a method that generates any custom HTTP response headers to use.
/// </summary>
/// <param name="headerFactory">Any delegate that creates any custom HTTP response headers to use.</param>
/// <returns>
/// The current <see cref="HttpRequestInterceptionBuilder"/>.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="headerFactory"/> is <see langword="null"/>.
/// </exception>
public HttpRequestInterceptionBuilder WithResponseHeaders(
Func<IEnumerable<KeyValuePair<string, ICollection<string>>>> headerFactory)
{
if (headerFactory is null)
{
throw new ArgumentNullException(nameof(headerFactory));
}

_responseHeaders = new DynamicDictionary(headerFactory);
IncrementRevision();
return this;
}

/// <summary>
/// Sets media type for the response body content.
/// </summary>
Expand Down Expand Up @@ -869,10 +888,7 @@ public HttpRequestInterceptionBuilder ForRequestHeader(string name, IEnumerable<
throw new ArgumentNullException(nameof(values));
}

if (_requestHeaders is null)
{
_requestHeaders = new Dictionary<string, ICollection<string>>(StringComparer.OrdinalIgnoreCase);
}
_requestHeaders ??= new Dictionary<string, ICollection<string>>(StringComparer.OrdinalIgnoreCase);

if (!_requestHeaders.TryGetValue(name, out ICollection<string>? current))
{
Expand Down Expand Up @@ -983,40 +999,61 @@ internal HttpInterceptionResponse Build()
Version = _version,
};

if (_requestHeaders?.Count > 0)
if (_requestHeaders is not null)
{
var headers = new Dictionary<string, IEnumerable<string>>(_requestHeaders.Count);

foreach (var pair in _requestHeaders)
if (_requestHeaders is DynamicDictionary factory)
{
headers[pair.Key] = pair.Value;
response.RequestHeaders = factory;
}
else if (_requestHeaders.Count > 0)
{
var headers = new Dictionary<string, IEnumerable<string>>(_requestHeaders.Count);

response.RequestHeaders = headers;
foreach (var pair in _requestHeaders)
{
headers[pair.Key] = pair.Value;
}

response.RequestHeaders = headers;
}
}

if (_responseHeaders?.Count > 0)
if (_responseHeaders is not null)
{
var headers = new Dictionary<string, IEnumerable<string>>(_responseHeaders.Count);

foreach (var pair in _responseHeaders)
if (_responseHeaders is DynamicDictionary factory)
{
headers[pair.Key] = pair.Value;
response.ResponseHeaders = factory;
}
else if (_responseHeaders.Count > 0)
{
var headers = new Dictionary<string, IEnumerable<string>>(_responseHeaders.Count);

response.ResponseHeaders = headers;
foreach (var pair in _responseHeaders)
{
headers[pair.Key] = pair.Value;
}

response.ResponseHeaders = headers;
}
}

if (_contentHeaders?.Count > 0)
if (_contentHeaders is not null)
{
var headers = new Dictionary<string, IEnumerable<string>>(_contentHeaders.Count);

foreach (var pair in _contentHeaders)
if (_contentHeaders is DynamicDictionary factory)
{
headers[pair.Key] = pair.Value;
response.ContentHeaders = factory;
}
else if (_contentHeaders.Count > 0)
{
var headers = new Dictionary<string, IEnumerable<string>>(_contentHeaders.Count);

response.ContentHeaders = headers;
foreach (var pair in _contentHeaders)
{
headers[pair.Key] = pair.Value;
}

response.ContentHeaders = headers;
}
}

return response;
Expand All @@ -1036,4 +1073,105 @@ private void IncrementRevision()
_revision++;
}
}

private sealed class DynamicDictionary :
IDictionary<string, ICollection<string>>,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>
{
private readonly Func<IEnumerable<KeyValuePair<string, ICollection<string>>>> _generator;

internal DynamicDictionary(Func<IEnumerable<KeyValuePair<string, ICollection<string>>>> generator)
{
_generator = generator;
}

[ExcludeFromCodeCoverage]
public ICollection<string> Keys => throw new NotSupportedException();

[ExcludeFromCodeCoverage]
public ICollection<ICollection<string>> Values => throw new NotSupportedException();

[ExcludeFromCodeCoverage]
public int Count => throw new NotSupportedException();

[ExcludeFromCodeCoverage]
public bool IsReadOnly => true;

[ExcludeFromCodeCoverage]
public ICollection<string> this[string key]
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}

public IEnumerator<KeyValuePair<string, ICollection<string>>> GetEnumerator()
=> _generator().GetEnumerator();

IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();

IEnumerator<KeyValuePair<string, IEnumerable<string>>> IEnumerable<KeyValuePair<string, IEnumerable<string>>>.GetEnumerator()
=> new EnumeratorAdapter(GetEnumerator());

[ExcludeFromCodeCoverage]
public void Add(string key, ICollection<string> value)
=> throw new NotSupportedException();

[ExcludeFromCodeCoverage]
public void Add(KeyValuePair<string, ICollection<string>> item)
=> throw new NotSupportedException();

[ExcludeFromCodeCoverage]
public void Clear()
=> throw new NotSupportedException();

[ExcludeFromCodeCoverage]
public bool Contains(KeyValuePair<string, ICollection<string>> item)
=> throw new NotSupportedException();

[ExcludeFromCodeCoverage]
public bool ContainsKey(string key)
=> throw new NotSupportedException();

[ExcludeFromCodeCoverage]
public void CopyTo(KeyValuePair<string, ICollection<string>>[] array, int arrayIndex)
=> throw new NotSupportedException();

[ExcludeFromCodeCoverage]
public bool Remove(string key)
=> throw new NotSupportedException();

[ExcludeFromCodeCoverage]
public bool Remove(KeyValuePair<string, ICollection<string>> item)
=> throw new NotSupportedException();

[ExcludeFromCodeCoverage]
public bool TryGetValue(string key, out ICollection<string> value)
=> throw new NotSupportedException();

private sealed class EnumeratorAdapter : IEnumerator<KeyValuePair<string, IEnumerable<string>>>
{
private readonly IEnumerator<KeyValuePair<string, ICollection<string>>> _enumerator;

internal EnumeratorAdapter(IEnumerator<KeyValuePair<string, ICollection<string>>> enumerator)
{
_enumerator = enumerator;
}

public KeyValuePair<string, IEnumerable<string>> Current
=> new(_enumerator.Current.Key, _enumerator.Current.Value);

object IEnumerator.Current
=> _enumerator.Current;

public void Dispose()
=> _enumerator.Dispose();

public bool MoveNext()
=> _enumerator.MoveNext();

public void Reset()
=> _enumerator.Reset();
}
}
}
1 change: 1 addition & 0 deletions src/HttpClientInterception/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.WithResponseHeaders(System.Func<System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, System.Collections.Generic.ICollection<string!>!>>!>! headerFactory) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder!
41 changes: 41 additions & 0 deletions tests/HttpClientInterception.Tests/Examples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -755,4 +755,45 @@ static bool IsHttpGetForJustEatGitHubOrg(HttpRequestMessage request)
// Verify that the expected number of attempts were made
count.ShouldBe(retryCount);
}

[Fact]
public static async Task Dynamic_Headers()
{
// Arrange
int counter = 0;

var builder = new HttpRequestInterceptionBuilder()
.ForHost("service.local")
.ForPath("resource")
.WithJsonContent(new object())
.WithResponseHeaders(() =>
{
return new Dictionary<string, ICollection<string>>()
{
["x-count"] = new[] { (++counter).ToString(CultureInfo.InvariantCulture) },
};
});

var options = new HttpClientInterceptorOptions()
.Register(builder);

using var client = options.CreateHttpClient();
using var body = new StringContent(@"{ ""FirstName"": ""John"" }");

// Act
using var response1 = await client.GetAsync("http://service.local/resource");

// Assert
response1.Headers.TryGetValues("x-count", out var values).ShouldBeTrue();
values.ShouldNotBeNull();
values.ShouldBe(new[] { "1" });

// Act
using var response2 = await client.GetAsync("http://service.local/resource");

// Assert
response2.Headers.TryGetValues("x-count", out values).ShouldBeTrue();
values.ShouldNotBeNull();
values.ShouldBe(new[] { "2" });
}
}

0 comments on commit fb9440d

Please sign in to comment.