Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RegEx support to JsonMatcher #1091

Merged
merged 4 commits into from
Apr 6, 2024
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
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
Loading