Skip to content

Commit

Permalink
Protype for additional custom matching
Browse files Browse the repository at this point in the history
Prototype of updating the matching behaviour for justeattakeaway#249.
  • Loading branch information
martincostello committed Sep 18, 2020
1 parent 610dd16 commit 5abf6a2
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 20 deletions.
29 changes: 28 additions & 1 deletion src/HttpClientInterception/HttpClientInterceptorOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,9 @@ public virtual HttpClient CreateHttpClient(HttpMessageHandler? innerHandler = nu
/// </returns>
private static string BuildKey(HttpInterceptionResponse interceptor)
{
if (interceptor.UserMatcher != null || interceptor.ContentMatcher != null)
if (interceptor.UserMatcher != null ||
interceptor.ContentMatcher != null ||
interceptor.AdditionalMatchers?.Count > 0)
{
// Use the internal matcher's hash code as UserMatcher (a delegate)
// will always return the hash code. See https://stackoverflow.com/q/6624151/1064169
Expand Down Expand Up @@ -522,6 +524,31 @@ private void ConfigureMatcherAndRegister(HttpInterceptionResponse registration)
matcher = new RegistrationMatcher(registration, _comparer);
}

if (registration.AdditionalMatchers?.Count > 0)
{
var predicates = new List<HttpRequestPredicate>(registration.AdditionalMatchers.Count + 1)
{
matcher.IsMatchAsync,
};

predicates.AddRange(registration.AdditionalMatchers);

async Task<bool> MatchAll(HttpRequestMessage request)
{
foreach (var predicate in predicates)
{
if (!await predicate(request).ConfigureAwait(false))
{
return false;
}
}

return true;
}

matcher = new DelegatingMatcher(MatchAll);
}

registration.InternalMatcher = matcher;

string key = BuildKey(registration);
Expand Down
5 changes: 4 additions & 1 deletion src/HttpClientInterception/HttpInterceptionResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using JustEat.HttpClientInterception.Matching;

namespace JustEat.HttpClientInterception
{
internal sealed class HttpInterceptionResponse
{
internal ICollection<HttpRequestPredicate>? AdditionalMatchers { get; set; }

internal Func<HttpContent, Task<bool>>? ContentMatcher { get; set; }

internal Func<HttpRequestMessage, Task<bool>>? UserMatcher { get; set; }
internal HttpRequestPredicate? UserMatcher { get; set; }

internal Matching.RequestMatcher? InternalMatcher { get; set; }

Expand Down
63 changes: 60 additions & 3 deletions src/HttpClientInterception/HttpRequestInterceptionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public class HttpRequestInterceptionBuilder

private static readonly Func<Task<byte[]>> EmptyContentFactory = () => EmptyContent;

private ICollection<HttpRequestPredicate>? _additionalMatchers;

private Func<Task<byte[]>>? _contentFactory;

private Func<HttpContent, Task<bool>>? _contentMatcher;
Expand All @@ -38,7 +40,7 @@ public class HttpRequestInterceptionBuilder

private Func<HttpRequestMessage, CancellationToken, Task<bool>>? _onIntercepted;

private Func<HttpRequestMessage, Task<bool>>? _requestMatcher;
private HttpRequestPredicate? _requestMatcher;

private string? _reasonPhrase;

Expand Down Expand Up @@ -80,7 +82,7 @@ public HttpRequestInterceptionBuilder()
/// </remarks>
public HttpRequestInterceptionBuilder For(Predicate<HttpRequestMessage> predicate)
{
_requestMatcher = predicate == null ? null : new Func<HttpRequestMessage, Task<bool>>((message) => Task.FromResult(predicate(message)));
_requestMatcher = predicate == null ? null : new HttpRequestPredicate((message) => Task.FromResult(predicate(message)));
return this;
}

Expand All @@ -99,7 +101,7 @@ public HttpRequestInterceptionBuilder For(Predicate<HttpRequestMessage> predicat
/// </remarks>
public HttpRequestInterceptionBuilder For(Func<HttpRequestMessage, Task<bool>> predicate)
{
_requestMatcher = predicate;
_requestMatcher = new HttpRequestPredicate(predicate);
return this;
}

Expand Down Expand Up @@ -900,10 +902,65 @@ public HttpRequestInterceptionBuilder ForContent(Func<HttpContent, Task<bool>>?
return this;
}

/// <summary>
/// Configures the builder to additionally match any request that meets the criteria defined
/// by the specified asynchronous predicate if the request matches the default critiera.
/// </summary>
/// <param name="predicate">
/// A delegate to a method which returns <see langword="true"/> if the
/// request is considered a match; otherwise <see langword="false"/>.
/// </param>
/// <returns>
/// The current <see cref="HttpRequestInterceptionBuilder"/>.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="predicate"/> is <see langword="null"/>.
/// </exception>
public HttpRequestInterceptionBuilder AndFor(Predicate<HttpRequestMessage> predicate)
{
if (predicate is null)
{
throw new ArgumentNullException(nameof(predicate));
}

// TODO Need a way to clear/reset the additional matchers
_additionalMatchers ??= new List<HttpRequestPredicate>();
_additionalMatchers.Add(new HttpRequestPredicate((message) => Task.FromResult(predicate(message))));
return this;
}

/// <summary>
/// Configures the builder to additionally match any request that meets the criteria defined
/// by the specified asynchronous predicate if the request matches the default critiera.
/// </summary>
/// <param name="predicate">
/// A delegate to an asynchronous method which returns <see langword="true"/> if
/// the request is considered a match; otherwise <see langword="false"/>.
/// </param>
/// <returns>
/// The current <see cref="HttpRequestInterceptionBuilder"/>.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="predicate"/> is <see langword="null"/>.
/// </exception>
public HttpRequestInterceptionBuilder AndFor(Func<HttpRequestMessage, Task<bool>> predicate)
{
if (predicate is null)
{
throw new ArgumentNullException(nameof(predicate));
}

// TODO Need a way to clear/reset the additional matchers
_additionalMatchers ??= new List<HttpRequestPredicate>();
_additionalMatchers.Add(new HttpRequestPredicate(predicate));
return this;
}

internal HttpInterceptionResponse Build()
{
var response = new HttpInterceptionResponse()
{
AdditionalMatchers = _additionalMatchers,
ContentFactory = _contentFactory ?? EmptyContentFactory,
ContentMatcher = _contentMatcher,
ContentStream = _contentStream,
Expand Down
10 changes: 10 additions & 0 deletions src/HttpClientInterception/HttpRequestPredicate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// 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.Net.Http;
using System.Threading.Tasks;

namespace JustEat.HttpClientInterception
{
internal delegate Task<bool> HttpRequestPredicate(HttpRequestMessage request);
}
5 changes: 2 additions & 3 deletions src/HttpClientInterception/Matching/DelegatingMatcher.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// 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;
using System.Net.Http;
using System.Threading.Tasks;

Expand All @@ -16,13 +15,13 @@ internal sealed class DelegatingMatcher : RequestMatcher
/// <summary>
/// The user-provided predicate to use to test for a match. This field is read-only.
/// </summary>
private readonly Func<HttpRequestMessage, Task<bool>> _predicate;
private readonly HttpRequestPredicate _predicate;

/// <summary>
/// Initializes a new instance of the <see cref="DelegatingMatcher"/> class.
/// </summary>
/// <param name="predicate">The user-provided delegate to use for matching.</param>
internal DelegatingMatcher(Func<HttpRequestMessage, Task<bool>> predicate)
internal DelegatingMatcher(HttpRequestPredicate predicate)
{
_predicate = predicate;
}
Expand Down
2 changes: 2 additions & 0 deletions src/HttpClientInterception/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.AndFor(System.Func<System.Net.Http.HttpRequestMessage!, System.Threading.Tasks.Task<bool>!>! predicate) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder!
JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.AndFor(System.Predicate<System.Net.Http.HttpRequestMessage!>! predicate) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder!
18 changes: 6 additions & 12 deletions tests/HttpClientInterception.Tests/Examples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -679,18 +679,12 @@ public static async Task Simulate_Http_Rate_Limiting()
// Keep track of how many HTTP requests to GitHub have been made
int count = 0;

static bool IsHttpGetForJustEatGitHubOrg(HttpRequestMessage request)
{
return
request.Method.Equals(HttpMethod.Get) &&
request.RequestUri == new Uri("https://api.github.com/orgs/justeat");
}

// Register an HTTP 429 error with a specified priority. The For() delegate
// Register an HTTP 429 error with a specified priority. The AndFor() delegate
// is used to match invocation counts that are not divisible by three so that
// the first, second, fourth, fifth, seventh etc. request returns an error.
var builder1 = new HttpRequestInterceptionBuilder()
.For((request) => IsHttpGetForJustEatGitHubOrg(request) && ++count % 3 != 0)
new HttpRequestInterceptionBuilder()
.ForUrl("https://api.github.com/orgs/justeat")
.AndFor((_) => ++count % 3 != 0)
.HavingPriority(0)
.Responds()
.WithStatus(HttpStatusCode.TooManyRequests)
Expand All @@ -700,8 +694,8 @@ static bool IsHttpGetForJustEatGitHubOrg(HttpRequestMessage request)
// Register another request for an HTTP 200 with no priority that will match
// if the higher-priority for the HTTP 429 response does not match the request.
// In practice this will match for the third, sixth, ninth etc. request.
var builder2 = new HttpRequestInterceptionBuilder()
.For(IsHttpGetForJustEatGitHubOrg)
new HttpRequestInterceptionBuilder()
.ForUrl("https://api.github.com/orgs/justeat")
.Responds()
.WithStatus(HttpStatusCode.OK)
.WithSystemTextJsonContent(new { id = 1516790, login = "justeat", url = "https://api.github.com/orgs/justeat" })
Expand Down

0 comments on commit 5abf6a2

Please sign in to comment.