Skip to content

Commit

Permalink
Add RegEx support to JsonMatcher (#1091)
Browse files Browse the repository at this point in the history
* json matcher regex

* better test

* regression
  • Loading branch information
StefH committed Apr 6, 2024
1 parent ef9baf3 commit 54fe082
Show file tree
Hide file tree
Showing 7 changed files with 349 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public class MatcherModel
/// </summary>
public string? MatchOperator { get; set; }

#region JsonPartialMatcher and JsonPartialWildcardMatcher
#region JsonMatcher, JsonPartialMatcher and JsonPartialWildcardMatcher
/// <summary>
/// Support Regex.
/// </summary>
Expand Down
23 changes: 9 additions & 14 deletions src/WireMock.Net/Matchers/AbstractJsonPartialMatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,15 @@ namespace WireMock.Matchers;
/// </summary>
public abstract class AbstractJsonPartialMatcher : JsonMatcher
{
/// <summary>
/// Support Regex
/// </summary>
public bool Regex { get; }

/// <summary>
/// Initializes a new instance of the <see cref="AbstractJsonPartialMatcher"/> class.
/// </summary>
/// <param name="value">The string value to check for equality.</param>
/// <param name="ignoreCase">Ignore the case from the PropertyName and PropertyValue (string only).</param>
/// <param name="regex">Support Regex.</param>
protected AbstractJsonPartialMatcher(string value, bool ignoreCase = false, bool regex = false) : base(value, ignoreCase)
protected AbstractJsonPartialMatcher(string value, bool ignoreCase = false, bool regex = false) :
base(value, ignoreCase, regex)
{
Regex = regex;
}

/// <summary>
Expand All @@ -32,9 +27,9 @@ protected AbstractJsonPartialMatcher(string value, bool ignoreCase = false, bool
/// <param name="value">The object value to check for equality.</param>
/// <param name="ignoreCase">Ignore the case from the PropertyName and PropertyValue (string only).</param>
/// <param name="regex">Support Regex.</param>
protected AbstractJsonPartialMatcher(object value, bool ignoreCase = false, bool regex = false) : base(value, ignoreCase)
protected AbstractJsonPartialMatcher(object value, bool ignoreCase = false, bool regex = false) :
base(value, ignoreCase, regex)
{
Regex = regex;
}

/// <summary>
Expand All @@ -44,15 +39,15 @@ protected AbstractJsonPartialMatcher(object value, bool ignoreCase = false, bool
/// <param name="value">The value to check for equality.</param>
/// <param name="ignoreCase">Ignore the case from the PropertyName and PropertyValue (string only).</param>
/// <param name="regex">Support Regex.</param>
protected AbstractJsonPartialMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false, bool regex = false) : base(matchBehaviour, value, ignoreCase)
protected AbstractJsonPartialMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false, bool regex = false) :
base(matchBehaviour, value, ignoreCase, regex)
{
Regex = regex;
}

/// <inheritdoc />
protected override bool IsMatch(JToken? value, JToken? input)
protected override bool IsMatch(JToken value, JToken? input)
{
if (value == null || value == input)
if (value == input)
{
return true;
}
Expand All @@ -72,7 +67,7 @@ protected override bool IsMatch(JToken? value, JToken? input)
((value.Type == JTokenType.Guid && input.Type == JTokenType.String) ||
(value.Type == JTokenType.String && input.Type == JTokenType.Guid)))
{
return IsMatch(value.ToString(), input.ToString());
return IsMatch(value.ToString().ToUpperInvariant(), input.ToString().ToUpperInvariant());
}

if (input == null || value.Type != input.Type)
Expand Down
91 changes: 86 additions & 5 deletions src/WireMock.Net/Matchers/JsonMatcher.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
using Stef.Validation;
using WireMock.Util;
using JsonUtils = WireMock.Util.JsonUtils;

namespace WireMock.Matchers;

Expand All @@ -23,6 +25,11 @@ public class JsonMatcher : IJsonMatcher
/// <inheritdoc cref="IIgnoreCaseMatcher.IgnoreCase"/>
public bool IgnoreCase { get; }

/// <summary>
/// Support Regex
/// </summary>
public bool Regex { get; }

private readonly JToken _valueAsJToken;
private readonly Func<JToken, JToken> _jTokenConverter;

Expand All @@ -31,7 +38,8 @@ public class JsonMatcher : IJsonMatcher
/// </summary>
/// <param name="value">The string value to check for equality.</param>
/// <param name="ignoreCase">Ignore the case from the PropertyName and PropertyValue (string only).</param>
public JsonMatcher(string value, bool ignoreCase = false) : this(MatchBehaviour.AcceptOnMatch, value, ignoreCase)
/// <param name="regex">Support Regex.</param>
public JsonMatcher(string value, bool ignoreCase = false, bool regex = false) : this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, regex)
{
}

Expand All @@ -40,7 +48,8 @@ public JsonMatcher(string value, bool ignoreCase = false) : this(MatchBehaviour.
/// </summary>
/// <param name="value">The object value to check for equality.</param>
/// <param name="ignoreCase">Ignore the case from the PropertyName and PropertyValue (string only).</param>
public JsonMatcher(object value, bool ignoreCase = false) : this(MatchBehaviour.AcceptOnMatch, value, ignoreCase)
/// <param name="regex">Support Regex.</param>
public JsonMatcher(object value, bool ignoreCase = false, bool regex = false) : this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, regex)
{
}

Expand All @@ -50,12 +59,14 @@ public JsonMatcher(object value, bool ignoreCase = false) : this(MatchBehaviour.
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="value">The value to check for equality.</param>
/// <param name="ignoreCase">Ignore the case from the PropertyName and PropertyValue (string only).</param>
public JsonMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false)
/// <param name="regex">Support Regex.</param>
public JsonMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false, bool regex = false)
{
Guard.NotNull(value);

MatchBehaviour = matchBehaviour;
IgnoreCase = ignoreCase;
Regex = regex;

Value = value;
_valueAsJToken = JsonUtils.ConvertValueToJToken(value);
Expand Down Expand Up @@ -93,9 +104,79 @@ public MatchResult IsMatch(object? input)
/// <param name="value">Matcher value</param>
/// <param name="input">Input value</param>
/// <returns></returns>
protected virtual bool IsMatch(JToken value, JToken input)
protected virtual bool IsMatch(JToken value, JToken? input)
{
return JToken.DeepEquals(value, input);
// If equal, return true.
if (input == value)
{
return true;
}

// If input, return false.
if (input == null)
{
return false;
}

// If using Regex and the value is a string, use the MatchRegex method.
if (Regex && value.Type == JTokenType.String)
{
var valueAsString = value.ToString();

var (valid, result) = RegexUtils.MatchRegex(valueAsString, input.ToString());
if (valid)
{
return result;
}
}

// If the value is a Guid and the input is a string, or vice versa, convert them to strings and compare the string values.
if ((value.Type == JTokenType.Guid && input.Type == JTokenType.String) || (value.Type == JTokenType.String && input.Type == JTokenType.Guid))
{
return JToken.DeepEquals(value.ToString().ToUpperInvariant(), input.ToString().ToUpperInvariant());
}

switch (value.Type)
{
// If the value is an object, compare all properties.
case JTokenType.Object:
var valueProperties = value.ToObject<Dictionary<string, JToken>>() ?? new Dictionary<string, JToken>();
var inputProperties = input.ToObject<Dictionary<string, JToken>>() ?? new Dictionary<string, JToken>();

// If the number of properties is different, return false.
if (valueProperties.Count != inputProperties.Count)
{
return false;
}

// Compare all properties. The input must match all properties of the value.
foreach (var pair in valueProperties)
{
if (!IsMatch(pair.Value, inputProperties[pair.Key]))
{
return false;
}
}

return true;

// If the value is an array, compare all elements.
case JTokenType.Array:
var valueArray = value.ToObject<JToken[]>() ?? EmptyArray<JToken>.Value;
var inputArray = input.ToObject<JToken[]>() ?? EmptyArray<JToken>.Value;

// If the number of elements is different, return false.
if (valueArray.Length != inputArray.Length)
{
return false;
}

return !valueArray.Where((valueToken, index) => !IsMatch(valueToken, inputArray[index])).Any();

default:
// Use JToken.DeepEquals() for all other types.
return JToken.DeepEquals(value, input);
}
}

private static string? ToUpper(string? input)
Expand Down
10 changes: 3 additions & 7 deletions src/WireMock.Net/Serialization/MatcherMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public MatcherMapper(WireMockServerSettings settings)

case nameof(JsonMatcher):
var valueForJsonMatcher = matcherModel.Pattern ?? matcherModel.Patterns;
return new JsonMatcher(matchBehaviour, valueForJsonMatcher!, ignoreCase);
return new JsonMatcher(matchBehaviour, valueForJsonMatcher!, ignoreCase, useRegex);

case nameof(JsonPartialMatcher):
var valueForJsonPartialMatcher = matcherModel.Pattern ?? matcherModel.Patterns;
Expand Down Expand Up @@ -152,12 +152,8 @@ public MatcherMapper(WireMockServerSettings settings)

switch (matcher)
{
case JsonPartialMatcher jsonPartialMatcher:
model.Regex = jsonPartialMatcher.Regex;
break;

case JsonPartialWildcardMatcher jsonPartialWildcardMatcher:
model.Regex = jsonPartialWildcardMatcher.Regex;
case JsonMatcher jsonMatcher:
model.Regex = jsonMatcher.Regex;
break;

case XPathMatcher xpathMatcher:
Expand Down
Loading

0 comments on commit 54fe082

Please sign in to comment.