Skip to content

Commit

Permalink
refactor: add new Pattern type that replaces IPatternMatcher<> making…
Browse files Browse the repository at this point in the history
… the API a lot simpler.
  • Loading branch information
skwasjer committed Sep 8, 2024
1 parent 7c5024b commit 5f72dca
Show file tree
Hide file tree
Showing 16 changed files with 314 additions and 155 deletions.
8 changes: 8 additions & 0 deletions src/Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="IsExternalInit" Version="1.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="RequiredMemberAttribute" Version="1.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<!--https://docs.microsoft.com/en-us/visualstudio/msbuild/customize-your-build?view=vs-2019#use-case-multi-level-merging -->
Expand Down
56 changes: 44 additions & 12 deletions src/MockHttp/Extensions/RequestMatchingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using System.Text;
using MockHttp.Http;
using MockHttp.Matchers;
using MockHttp.Matchers.Patterns;
using MockHttp.Patterns;
using static MockHttp.Http.UriExtensions;

namespace MockHttp;
Expand Down Expand Up @@ -41,18 +41,13 @@ private static bool ContainsWildcard(this string value)
public static RequestMatching RequestUri(this RequestMatching builder, string requestUri, bool allowWildcards = true)
#pragma warning restore CA1054
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}

if (requestUri is null)
{
throw new ArgumentNullException(nameof(requestUri));
}

return allowWildcards && requestUri.ContainsWildcard()
? builder.With(new UriMatcher(new UriStringPatternMatcher(uri => uri.ToString(), new WildcardPatternMatcher(requestUri)), requestUri))
? builder.RequestUri(WildcardPattern.Create(requestUri))
: builder.RequestUri(new Uri(requestUri, DotNetRelativeOrAbsolute));
}

Expand All @@ -64,17 +59,54 @@ public static RequestMatching RequestUri(this RequestMatching builder, string re
/// <returns>The request matching builder instance.</returns>
public static RequestMatching RequestUri(this RequestMatching builder, Uri requestUri)
{
if (builder is null)
if (requestUri is null)
{
throw new ArgumentNullException(nameof(builder));
throw new ArgumentNullException(nameof(requestUri));
}

if (requestUri is null)
return builder.RequestUri(GetAbsoluteOrRelativePattern(requestUri));

static Pattern GetAbsoluteOrRelativePattern(Uri pattern)
{
throw new ArgumentNullException(nameof(requestUri));
Uri patternRooted = pattern.EnsureIsRooted();
return new Pattern
{
Value = pattern.ToString(),
IsMatch = input =>
{
var uri = new Uri(input, DotNetRelativeOrAbsolute);
return IsAbsoluteUriMatch(patternRooted, uri) || IsRelativeUriMatch(patternRooted, uri);
}
};

static bool IsAbsoluteUriMatch(Uri pattern, Uri uri)
{
return pattern.IsAbsoluteUri && uri.Equals(pattern);
}

static bool IsRelativeUriMatch(Uri pattern, Uri uri)
{
return !pattern.IsAbsoluteUri
&& uri.IsBaseOf(pattern)
&& uri.ToString().EndsWith(pattern.ToString(), StringComparison.Ordinal);
}
}
}

/// <summary>
/// Matches a request by specified <paramref name="pattern" />.
/// </summary>
/// <param name="builder">The request matching builder instance.</param>
/// <param name="pattern">The pattern that must match the request URI.</param>
/// <returns>The request matching builder instance.</returns>
private static RequestMatching RequestUri(this RequestMatching builder, Pattern pattern)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}

return builder.With(new UriMatcher(new RelativeOrAbsoluteUriPatternMatcher(requestUri), requestUri.ToString()));
return builder.With(new UriMatcher(pattern));
}

/// <summary>
Expand Down
12 changes: 6 additions & 6 deletions src/MockHttp/Http/HttpHeaderEqualityComparer.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System.ComponentModel;
using MockHttp.Matchers.Patterns;
using MockHttp.Patterns;

namespace MockHttp.Http;

Expand All @@ -22,7 +22,7 @@ internal enum HttpHeaderMatchType
internal sealed class HttpHeaderEqualityComparer : IEqualityComparer<KeyValuePair<string, IEnumerable<string>>>
{
private readonly HttpHeaderMatchType? _matchType;
private readonly IPatternMatcher<string>? _valuePatternMatcher;
private readonly Pattern? _valuePatternMatcher;

public HttpHeaderEqualityComparer(HttpHeaderMatchType matchType)
{
Expand All @@ -34,9 +34,9 @@ public HttpHeaderEqualityComparer(HttpHeaderMatchType matchType)
_matchType = matchType;
}

public HttpHeaderEqualityComparer(IPatternMatcher<string> valuePatternMatcher)
public HttpHeaderEqualityComparer(Pattern valuePatternMatcher)
{
_valuePatternMatcher = valuePatternMatcher ?? throw new ArgumentNullException(nameof(valuePatternMatcher));
_valuePatternMatcher = valuePatternMatcher;
}

public bool Equals(KeyValuePair<string, IEnumerable<string>> x, KeyValuePair<string, IEnumerable<string>> y)
Expand All @@ -62,8 +62,8 @@ public bool Equals(KeyValuePair<string, IEnumerable<string>> x, KeyValuePair<str
.All(xValue =>
{
string[] headerValues = HttpHeadersCollection.ParseHttpHeaderValue(yValue).ToArray();
return _valuePatternMatcher is null && headerValues.Contains(xValue)
|| (_valuePatternMatcher is not null && headerValues.Any(_valuePatternMatcher.IsMatch));
return !_valuePatternMatcher.HasValue && headerValues.Contains(xValue)
|| (_valuePatternMatcher.HasValue && headerValues.Any(_valuePatternMatcher.Value.IsMatch));

Check warning on line 66 in src/MockHttp/Http/HttpHeaderEqualityComparer.cs

View workflow job for this annotation

GitHub Actions / analysis

Collection-specific "Exists" method should be used instead of the "Any" extension. (https://rules.sonarsource.com/csharp/RSPEC-6605)

Check warning on line 66 in src/MockHttp/Http/HttpHeaderEqualityComparer.cs

View workflow job for this annotation

GitHub Actions / analysis

Collection-specific "Exists" method should be used instead of the "Any" extension. (https://rules.sonarsource.com/csharp/RSPEC-6605)

Check warning on line 66 in src/MockHttp/Http/HttpHeaderEqualityComparer.cs

View workflow job for this annotation

GitHub Actions / analysis

Collection-specific "Exists" method should be used instead of the "Any" extension. (https://rules.sonarsource.com/csharp/RSPEC-6605)
})
))
{
Expand Down
9 changes: 4 additions & 5 deletions src/MockHttp/Matchers/HttpHeadersMatcher.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Net.Http.Headers;
using MockHttp.Http;
using MockHttp.Matchers.Patterns;
using MockHttp.Patterns;
using MockHttp.Responses;

namespace MockHttp.Matchers;
Expand Down Expand Up @@ -41,10 +41,9 @@ public HttpHeadersMatcher(string name, string value, bool allowWildcards = false
throw new ArgumentNullException(nameof(name));
}

WildcardPatternMatcher? patternMatcher = allowWildcards ? new WildcardPatternMatcher(value) : null;
_equalityComparer = patternMatcher is null
? new HttpHeaderEqualityComparer(HttpHeaderMatchType.HeaderNameAndPartialValues)
: new HttpHeaderEqualityComparer(patternMatcher);
_equalityComparer = allowWildcards
? new HttpHeaderEqualityComparer(WildcardPattern.Create(value))
: new HttpHeaderEqualityComparer(HttpHeaderMatchType.HeaderNameAndPartialValues);

Value.Add(name, value);
}
Expand Down
11 changes: 0 additions & 11 deletions src/MockHttp/Matchers/Patterns/IPatternMatcher.cs

This file was deleted.

37 changes: 0 additions & 37 deletions src/MockHttp/Matchers/Patterns/RegexPatternMatcher.cs

This file was deleted.

This file was deleted.

19 changes: 0 additions & 19 deletions src/MockHttp/Matchers/Patterns/UriStringPatternMatcher.cs

This file was deleted.

29 changes: 19 additions & 10 deletions src/MockHttp/Matchers/UriMatcher.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Runtime.CompilerServices;
using MockHttp.Matchers.Patterns;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using MockHttp.Patterns;
using MockHttp.Responses;

namespace MockHttp.Matchers;
Expand All @@ -9,26 +10,28 @@ namespace MockHttp.Matchers;
/// </summary>
internal class UriMatcher : HttpRequestMatcher
{
private readonly IPatternMatcher<Uri> _patternMatcher;
private readonly string _name;
private readonly Pattern _pattern;
private readonly string _patternDescription;
private readonly Func<Uri, string>? _selectorFn;

/// <summary>
/// Initializes a new instance of the <see cref="UriMatcher" /> class.
/// </summary>
/// <param name="patternMatcher">A matcher implementation that validates the URI.</param>
/// <param name="patternDescription">A description of the pattern.</param>
/// <param name="pattern">The pattern to match.</param>
/// <param name="selector">An expression to extract the part of the URI to match against. If <see langword="null" />, uses the complete URI.</param>
/// <param name="name">The name of this matcher.</param>
/// <exception cref="ArgumentNullException">Thrown when a required argument is <see langword="null" />.</exception>
internal UriMatcher
(
IPatternMatcher<Uri> patternMatcher,
string patternDescription,
Pattern pattern,
Expression<Func<Uri, string>>? selector = null,
[CallerMemberName] string? name = null
)
{
_patternMatcher = patternMatcher ?? throw new ArgumentNullException(nameof(patternMatcher));
_patternDescription = patternDescription ?? throw new ArgumentNullException(nameof(patternDescription));
_pattern = pattern;
_selectorFn = selector?.Compile();
_patternDescription = pattern.Value;

name ??= GetType().Name;
if (name.EndsWith("Matcher", StringComparison.Ordinal))
Expand All @@ -48,7 +51,13 @@ public override bool IsMatch(MockHttpRequestContext requestContext)
}

Uri? uri = requestContext.Request.RequestUri;
return uri is not null && _patternMatcher.IsMatch(uri);
if (uri is null)
{
return false;
}

string value = _selectorFn?.Invoke(uri) ?? uri.ToString();
return _pattern.IsMatch(value);
}

/// <inheritdoc />
Expand Down
7 changes: 7 additions & 0 deletions src/MockHttp/Patterns/IPattern.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace MockHttp.Patterns;

internal interface IPattern
{
string Value { get; }
Func<string, bool> IsMatch { get; }
}
22 changes: 22 additions & 0 deletions src/MockHttp/Patterns/Pattern.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace MockHttp.Patterns;

internal readonly record struct Pattern : IPattern
{
public required string Value { get; internal init; }
public required Func<string, bool> IsMatch { get; internal init; }

/// <inheritdoc />
public override string ToString()
{
return Value;
}

public static Pattern MatchExactly(string value)
{
return new Pattern
{
Value = value,
IsMatch = input => StringComparer.Ordinal.Equals(value, input)
};
}
}
Loading

0 comments on commit 5f72dca

Please sign in to comment.