Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/WireMock.Net.Abstractions/Models/IBodyData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public interface IBodyData
/// <summary>
/// The body as Form UrlEncoded dictionary.
/// </summary>
IDictionary<string, string>? BodyAsFormUrlEncoded { get; set; }
IDictionary<string, WireMockList<string>>? BodyAsFormUrlEncoded { get; set; }

/// <summary>
/// The detected body type (detection based on body content).
Expand Down
94 changes: 93 additions & 1 deletion src/WireMock.Net.Abstractions/Types/WireMockList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,98 @@ public WireMockList(IEnumerable<T> collection) : base(collection)
/// <param name="values">The values to set.</param>
public static implicit operator WireMockList<T>(T[] values) => new(values);

/// <summary>
/// Operator for equality comparison from WireMockList to T
/// </summary>
public static bool operator ==(WireMockList<T>? left, T? right)
{
if (ReferenceEquals(left, right))
{
return true;
}

if (left?.Count == 1 && Equals(left[0], right))
{
return true;
}

return false;
}

/// <summary>
/// Operator for equality comparison from T to WireMockList
/// </summary>
public static bool operator ==(T? left, WireMockList<T>? right)
{
if (ReferenceEquals(left, right))
{
return true;
}

if (right?.Count == 1 && Equals(left, right[0]))
{
return true;
}

return false;
}

/// <summary>
/// Operator for inequality comparison from WireMockList to T
/// </summary>
public static bool operator !=(WireMockList<T>? left, T? right) => !(left == right);

/// <summary>
/// Operator for inequality comparison from T to WireMockList
/// </summary>
public static bool operator !=(T? left, WireMockList<T>? right) => !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current instance.
/// Two <see cref="WireMockList{T}"/> instances are equal if they contain the same elements in the same order.
/// </summary>
public override bool Equals(object? obj)
{
if (ReferenceEquals(this, obj))
{
return true;
}

if (obj is WireMockList<T> other)
{
if (Count != other.Count)
{
return false;
}

for (var i = 0; i < Count; i++)
{
if (!Equals(this[i], other[i]))
{
return false;
}
}

return true;
}

return false;
}

/// <summary>
/// Returns a hash code for this instance based on its elements.
/// </summary>
public override int GetHashCode()
{
var hashCode = 17;
foreach (var item in this)
{
hashCode = hashCode * 31 + (item?.GetHashCode() ?? 0);
}

return hashCode;
}

/// <summary>
/// Returns a <see cref="string" /> that represents this instance.
/// </summary>
Expand All @@ -61,7 +153,7 @@ public override string ToString()
{
return strValue;
}

return this[0]?.ToString() ?? string.Empty;

default:
Expand Down
61 changes: 38 additions & 23 deletions src/WireMock.Net.Minimal/Matchers/FormUrlEncodedMatcher.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// Copyright © WireMock.Net

using System.Linq;
using AnyOfTypes;
using Stef.Validation;
using WireMock.Extensions;
using WireMock.Models;
using WireMock.Types;
using WireMock.Util;

namespace WireMock.Matchers;
Expand All @@ -21,7 +21,7 @@ public class FormUrlEncodedMatcher : IStringMatcher, IIgnoreCaseMatcher
/// <inheritdoc />
public MatchBehaviour MatchBehaviour { get; }

private readonly List<(WildcardMatcher Key, WildcardMatcher? Value)> _pairs = [];
private readonly List<(WildcardMatcher Key, WildcardMatcher[]? Values)> KeyValueMatchers = [];

/// <summary>
/// Initializes a new instance of the <see cref="FormUrlEncodedMatcher"/> class.
Expand Down Expand Up @@ -91,9 +91,10 @@ public FormUrlEncodedMatcher(
{
foreach (var nameValue in nameValueCollection)
{
var keyMatcher = new WildcardMatcher(MatchBehaviour.AcceptOnMatch, [nameValue.Key], ignoreCase, MatchOperator);
var valueMatcher = new WildcardMatcher(MatchBehaviour.AcceptOnMatch, [nameValue.Value], ignoreCase, MatchOperator);
_pairs.Add((keyMatcher, valueMatcher));
var keyMatcher = new WildcardMatcher(MatchBehaviour.AcceptOnMatch, nameValue.Key, ignoreCase);
Comment thread
StefH marked this conversation as resolved.
var valueMatchers = nameValue.Value.Select(value => new WildcardMatcher(MatchBehaviour.AcceptOnMatch, value, ignoreCase)).ToArray();

KeyValueMatchers.Add((keyMatcher, valueMatchers));
}
}
}
Expand All @@ -119,34 +120,48 @@ public MatchResult IsMatch(string? input)
return MatchResult.From(Name, MatchBehaviourHelper.Convert(MatchBehaviour, score));
}

private bool[] GetMatches(IDictionary<string, string> inputNameValueCollection)
private List<double> GetMatches(IDictionary<string, WireMockList<string>> inputNameValueCollection)
{
var matches = new List<bool>();
if (_pairs.Count > inputNameValueCollection.Count)
var inputPairs = inputNameValueCollection.ToArray();
var rowCount = inputPairs.Length;
var columnCount = KeyValueMatchers.Count;

if (rowCount == 0 && columnCount == 0)
{
matches.AddRange(Enumerable.Repeat(false, _pairs.Count - inputNameValueCollection.Count));
return [];
}

foreach (var inputKeyValuePair in inputNameValueCollection)
var matrix = new double[rowCount][];

for (var row = 0; row < rowCount; row++)
{
var match = false;
foreach (var pair in _pairs)
matrix[row] = new double[columnCount];

var inputKeyValuePair = inputPairs[row];
var inputKey = inputKeyValuePair.Key;
var inputValues = inputKeyValuePair.Value;

for (var column = 0; column < columnCount; column++)
{
var keyMatchResult = pair.Key.IsMatch(inputKeyValuePair.Key).IsPerfect();
if (keyMatchResult)
{
match = pair.Value?.IsMatch(inputKeyValuePair.Value).IsPerfect() ?? false;
if (match)
{
break;
}
}
var (keyMatcher, valuesMatchers) = KeyValueMatchers[column];

var keyScore = keyMatcher.IsMatch(inputKey).Score;
var valueScore = valuesMatchers != null ? MatchScores.ToScore(inputValues, valuesMatchers) : MatchScores.Perfect;

matrix[row][column] = Math.Min(keyScore, valueScore);
}
}

var rowScores = rowCount == 0 ? [] : matrix.Select(row => row.Length == 0 ? MatchScores.Mismatch : row.Max()).ToList();

matches.Add(match);
var columnScores = new List<double>();
for (var column = 0; column < columnCount; column++)
{
columnScores.Add(rowCount == 0 ? MatchScores.Mismatch : matrix.Max(row => row[column]));
}

return matches.ToArray();
rowScores.AddRange(columnScores);
return rowScores;
}

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using Stef.Validation;
using WireMock.Matchers.Helpers;
using WireMock.Types;
using WireMock.Util;

namespace WireMock.Matchers.Request;
Expand Down Expand Up @@ -34,7 +35,7 @@ public class RequestMessageBodyMatcher : IRequestMatcher
/// <summary>
/// The body data function for FormUrlEncoded
/// </summary>
public Func<IDictionary<string, string>?, bool>? MatchOnBodyAsFormUrlEncodedFunc { get; }
public Func<IDictionary<string, WireMockList<string>>?, bool>? MatchOnBodyAsFormUrlEncodedFunc { get; }

/// <summary>
/// The matchers.
Expand Down Expand Up @@ -116,7 +117,7 @@ public RequestMessageBodyMatcher(Func<IBodyData?, bool> func)
/// Initializes a new instance of the <see cref="RequestMessageBodyMatcher"/> class.
/// </summary>
/// <param name="func">The function.</param>
public RequestMessageBodyMatcher(Func<IDictionary<string, string>?, bool> func)
public RequestMessageBodyMatcher(Func<IDictionary<string, WireMockList<string>>?, bool> func)
{
MatchOnBodyAsFormUrlEncodedFunc = Guard.NotNull(func);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,36 +115,6 @@ private double GetMatchScore(IRequestMessage requestMessage)
}

// Return the score based on Matchers and valuesPresentInRequestMessage
return CalculateScore(Matchers, valuesPresentInRequestMessage);
}

private static double CalculateScore(IReadOnlyList<IStringMatcher> matchers, WireMockList<string> valuesPresentInRequestMessage)
{
var total = new List<double>();

// If the total patterns in all matchers > values in message, use the matcher as base
if (matchers.Sum(m => m.GetPatterns().Length) > valuesPresentInRequestMessage.Count)
{
foreach (var matcher in matchers)
{
double score = 0d;
foreach (string valuePresentInRequestMessage in valuesPresentInRequestMessage)
{
score += matcher.IsMatch(valuePresentInRequestMessage).Score / matcher.GetPatterns().Length;
}

total.Add(score);
}
}
else
{
foreach (string valuePresentInRequestMessage in valuesPresentInRequestMessage)
{
var score = matchers.Max(m => m.IsMatch(valuePresentInRequestMessage).Score);
total.Add(score);
}
}

return total.Any() ? MatchScores.ToScore(total, MatchOperator.Average) : 0;
return MatchScores.ToScore(valuesPresentInRequestMessage, Matchers.ToArray());
}
}
3 changes: 2 additions & 1 deletion src/WireMock.Net.Minimal/RequestBuilders/Request.WithBody.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Stef.Validation;
using WireMock.Matchers;
using WireMock.Matchers.Request;
using WireMock.Types;
using WireMock.Util;

namespace WireMock.RequestBuilders;
Expand Down Expand Up @@ -84,7 +85,7 @@ public IRequestBuilder WithBody(Func<IBodyData?, bool> func)
}

/// <inheritdoc />
public IRequestBuilder WithBody(Func<IDictionary<string, string>?, bool> func)
public IRequestBuilder WithBody(Func<IDictionary<string, WireMockList<string>>?, bool> func)
{
_requestMatchers.Add(new RequestMessageBodyMatcher(Guard.NotNull(func)));
return this;
Expand Down
2 changes: 0 additions & 2 deletions src/WireMock.Net.Shared/Extensions/AnyOfExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
// Copyright © WireMock.Net

using System.Collections.Generic;
using System.Linq;
using AnyOfTypes;
using WireMock.Models;

Expand Down
34 changes: 28 additions & 6 deletions src/WireMock.Net.Shared/Matchers/MatchScores.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright © WireMock.Net

using System;
using System.Collections.Generic;
using System.Linq;
using WireMock.Types;

namespace WireMock.Matchers;

Expand Down Expand Up @@ -57,7 +55,7 @@ public static double ToScore(bool value)
/// <param name="values">The values.</param>
/// <param name="matchOperator">The <see cref="MatchOperator"/>.</param>
/// <returns>average score</returns>
public static double ToScore(IReadOnlyCollection<bool> values, MatchOperator matchOperator)
public static double ToScore(IReadOnlyList<bool> values, MatchOperator matchOperator)
{
return ToScore(values.Select(ToScore).ToArray(), matchOperator);
}
Expand All @@ -68,9 +66,9 @@ public static double ToScore(IReadOnlyCollection<bool> values, MatchOperator mat
/// <param name="values">The values.</param>
/// <param name="matchOperator"></param>
/// <returns>average score</returns>
public static double ToScore(IReadOnlyCollection<double> values, MatchOperator matchOperator)
public static double ToScore(IReadOnlyList<double> values, MatchOperator matchOperator)
{
if (!values.Any())
if (values.Count == 0)
{
return Mismatch;
}
Expand All @@ -82,4 +80,28 @@ public static double ToScore(IReadOnlyCollection<double> values, MatchOperator m
_ => values.Average()
};
}

internal static double ToScore(WireMockList<string> values, IStringMatcher[] matchers, MatchOperator matchOperator = MatchOperator.And)
{
// Create a matrix of scores where each row corresponds to a value and each column corresponds to a matcher.
var matrix = values
.Select(value => matchers
Comment thread
StefH marked this conversation as resolved.
.Select(matcher => matcher.IsMatch(value).Score).ToArray()
)
.ToArray();

if (matrix.Length == 0 || matchers.Length == 0)
{
return Mismatch;
}

// For each value, how well was it matched by its best matcher?
var rowScore = matchOperator == MatchOperator.And ? matrix.Average(row => row.Max()) : matrix.Max(row => row.Max());
Comment thread
StefH marked this conversation as resolved.

// For each matcher, how well was it satisfied by its best value?
var columnRange = Enumerable.Range(0, matchers.Length);
var columnScore = matchOperator == MatchOperator.And ? columnRange.Average(column => matrix.Max(row => row[column])) : columnRange.Max(column => matrix.Max(row => row[column]));

return matchOperator == MatchOperator.And ? Math.Min(rowScore, columnScore) : Math.Max(rowScore, columnScore);
}
}
2 changes: 1 addition & 1 deletion src/WireMock.Net.Shared/Models/BodyData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class BodyData : IBodyData
public string? BodyAsString { get; set; }

/// <inheritdoc />
public IDictionary<string, string>? BodyAsFormUrlEncoded { get; set; }
public IDictionary<string, WireMockList<string>>? BodyAsFormUrlEncoded { get; set; }

/// <inheritdoc />
public object? BodyAsJson { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
// Copyright © WireMock.Net

using System;
using System.Collections.Generic;
using WireMock.Matchers;
using WireMock.Types;
using WireMock.Util;

namespace WireMock.RequestBuilders;
Expand Down Expand Up @@ -100,5 +99,5 @@ public interface IBodyRequestBuilder : IMultiPartRequestBuilder
/// </summary>
/// <param name="func">The form-urlencoded values.</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBody(Func<IDictionary<string, string>?, bool> func);
IRequestBuilder WithBody(Func<IDictionary<string, WireMockList<string>>?, bool> func);
Comment thread
StefH marked this conversation as resolved.
}
Loading
Loading