// Copyright © WireMock.Net using AnyOfTypes; using Stef.Validation; using WireMock.Extensions; using WireMock.Models; using WireMock.Types; using WireMock.Util; namespace WireMock.Matchers; /// /// FormUrl Encoded fields Matcher /// /// /// public class FormUrlEncodedMatcher : IStringMatcher, IIgnoreCaseMatcher { private readonly AnyOf[] _patterns; /// public MatchBehaviour MatchBehaviour { get; } private readonly List<(WildcardMatcher Key, WildcardMatcher[]? Values)> KeyValueMatchers = []; /// /// Initializes a new instance of the class. /// /// The pattern. /// Ignore the case from the pattern. /// The to use. (default = "Or") public FormUrlEncodedMatcher( AnyOf pattern, bool ignoreCase = false, MatchOperator matchOperator = MatchOperator.Or) : this(MatchBehaviour.AcceptOnMatch, [pattern], ignoreCase, matchOperator) { } /// /// Initializes a new instance of the class. /// /// The match behaviour. /// The pattern. /// Ignore the case from the pattern. /// The to use. (default = "Or") public FormUrlEncodedMatcher( MatchBehaviour matchBehaviour, AnyOf pattern, bool ignoreCase = false, MatchOperator matchOperator = MatchOperator.Or) : this(matchBehaviour, [pattern], ignoreCase, matchOperator) { } /// /// Initializes a new instance of the class. /// /// The patterns. /// Ignore the case from the pattern. /// The to use. (default = "Or") public FormUrlEncodedMatcher( AnyOf[] patterns, bool ignoreCase = false, MatchOperator matchOperator = MatchOperator.Or) : this(MatchBehaviour.AcceptOnMatch, patterns, ignoreCase, matchOperator) { } /// /// Initializes a new instance of the class. /// /// The match behaviour. /// The patterns. /// Ignore the case from the pattern. /// The to use. (default = "Or") public FormUrlEncodedMatcher( MatchBehaviour matchBehaviour, AnyOf[] patterns, bool ignoreCase = false, MatchOperator matchOperator = MatchOperator.Or) { _patterns = Guard.NotNull(patterns); IgnoreCase = ignoreCase; MatchBehaviour = matchBehaviour; MatchOperator = matchOperator; foreach (var pattern in _patterns) { if (QueryStringParser.TryParse(pattern, IgnoreCase, out var nameValueCollection)) { foreach (var nameValue in nameValueCollection) { var keyMatcher = new WildcardMatcher(MatchBehaviour.AcceptOnMatch, nameValue.Key, ignoreCase); var valueMatchers = nameValue.Value.Select(value => new WildcardMatcher(MatchBehaviour.AcceptOnMatch, value, ignoreCase)).ToArray(); KeyValueMatchers.Add((keyMatcher, valueMatchers)); } } } } /// public MatchResult IsMatch(string? input) { // Input is null or empty and if no patterns defined, return Perfect match. if (string.IsNullOrEmpty(input) && _patterns.Length == 0) { return MatchResult.From(Name, MatchScores.Perfect); } if (!QueryStringParser.TryParse(input, IgnoreCase, out var inputNameValueCollection)) { return MatchResult.From(Name, MatchScores.Mismatch); } var matches = GetMatches(inputNameValueCollection); var score = MatchScores.ToScore(matches, MatchOperator); return MatchResult.From(Name, score); } private List GetMatches(IDictionary> inputNameValueCollection) { var inputPairs = inputNameValueCollection.ToArray(); var rowCount = inputPairs.Length; var columnCount = KeyValueMatchers.Count; if (rowCount == 0 && columnCount == 0) { return []; } var matrix = new double[rowCount][]; for (var row = 0; row < rowCount; row++) { 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 (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(); var columnScores = new List(); for (var column = 0; column < columnCount; column++) { columnScores.Add(rowCount == 0 ? MatchScores.Mismatch : matrix.Max(row => row[column])); } rowScores.AddRange(columnScores); return rowScores; } /// public virtual AnyOf[] GetPatterns() { return _patterns; } /// public virtual string Name => nameof(FormUrlEncodedMatcher); /// public bool IgnoreCase { get; } /// public MatchOperator MatchOperator { get; } /// public string GetCSharpCodeArguments() { return $"new {Name}" + $"(" + $"{MatchBehaviour.GetFullyQualifiedEnumValue()}, " + $"{MappingConverterUtils.ToCSharpCodeArguments(_patterns)}, " + $"{CSharpFormatter.ToCSharpBooleanLiteral(IgnoreCase)}, " + $"{MatchOperator.GetFullyQualifiedEnumValue()}" + $")"; } }