Skip to content

Commit

Permalink
Support for partial JSON matching (#539)
Browse files Browse the repository at this point in the history
* support Json partial match with JsonPartialMatcher

* fix erroneous filenames

* add newline

* newlines fix

* add JsonPartialMatcher to mapper

* curly braces for ifs

* fix JToken type comparison

* more test cases

* rename AreEqual -> IsMatch + more test cases

* separate tests for JPath matcher values

Co-authored-by: Gleb Osokin <[email protected]>
  • Loading branch information
gleb-osokin and Gleb Osokin authored Nov 17, 2020
1 parent 2d95167 commit 548fc2c
Show file tree
Hide file tree
Showing 5 changed files with 578 additions and 17 deletions.
37 changes: 21 additions & 16 deletions src/WireMock.Net/Matchers/JsonMatcher.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections;
using System;
using System.Collections;
using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json;
Expand All @@ -17,7 +18,7 @@ public class JsonMatcher : IValueMatcher, IIgnoreCaseMatcher
public object Value { get; }

/// <inheritdoc cref="IMatcher.Name"/>
public string Name => "JsonMatcher";
public virtual string Name => "JsonMatcher";

/// <inheritdoc cref="IMatcher.MatchBehaviour"/>
public MatchBehaviour MatchBehaviour { get; }
Expand All @@ -29,6 +30,7 @@ public class JsonMatcher : IValueMatcher, IIgnoreCaseMatcher
public bool ThrowException { get; }

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

/// <summary>
/// Initializes a new instance of the <see cref="JsonMatcher"/> class.
Expand Down Expand Up @@ -67,6 +69,9 @@ public JsonMatcher(MatchBehaviour matchBehaviour, [NotNull] object value, bool i

Value = value;
_valueAsJToken = ConvertValueToJToken(value);
_jTokenConverter = ignoreCase
? (Func<JToken, JToken>)Rename
: jToken => jToken;
}

/// <inheritdoc cref="IObjectMatcher.IsMatch"/>
Expand All @@ -81,7 +86,9 @@ public double IsMatch(object input)
{
var inputAsJToken = ConvertValueToJToken(input);

match = DeepEquals(_valueAsJToken, inputAsJToken);
match = IsMatch(
_jTokenConverter(_valueAsJToken),
_jTokenConverter(inputAsJToken));
}
catch (JsonException)
{
Expand All @@ -95,6 +102,17 @@ public double IsMatch(object input)
return MatchBehaviourHelper.Convert(MatchBehaviour, MatchScores.ToScore(match));
}

/// <summary>
/// Compares the input against the matcher value
/// </summary>
/// <param name="value">Matcher value</param>
/// <param name="input">Input value</param>
/// <returns></returns>
protected virtual bool IsMatch(JToken value, JToken input)
{
return JToken.DeepEquals(value, input);
}

private static JToken ConvertValueToJToken(object value)
{
// Check if JToken, string, IEnumerable or object
Expand All @@ -114,19 +132,6 @@ private static JToken ConvertValueToJToken(object value)
}
}

private bool DeepEquals(JToken value, JToken input)
{
if (!IgnoreCase)
{
return JToken.DeepEquals(value, input);
}

JToken renamedValue = Rename(value);
JToken renamedInput = Rename(input);

return JToken.DeepEquals(renamedValue, renamedInput);
}

private static string ToUpper(string input)
{
return input?.ToUpperInvariant();
Expand Down
87 changes: 87 additions & 0 deletions src/WireMock.Net/Matchers/JsonPartialMatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json.Linq;

namespace WireMock.Matchers
{
/// <summary>
/// JsonPartialMatcher
/// </summary>
public class JsonPartialMatcher : JsonMatcher
{
/// <inheritdoc cref="IMatcher.Name"/>
public override string Name => "JsonPartialMatcher";

/// <summary>
/// Initializes a new instance of the <see cref="JsonPartialMatcher"/> 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="throwException">Throw an exception when the internal matching fails because of invalid input.</param>
public JsonPartialMatcher([NotNull] string value, bool ignoreCase = false, bool throwException = false)
: base(value, ignoreCase, throwException)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="JsonPartialMatcher"/> class.
/// </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>
/// <param name="throwException">Throw an exception when the internal matching fails because of invalid input.</param>
public JsonPartialMatcher([NotNull] object value, bool ignoreCase = false, bool throwException = false)
: base(value, ignoreCase, throwException)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="JsonPartialMatcher"/> class.
/// </summary>
/// <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>
/// <param name="throwException">Throw an exception when the internal matching fails because of invalid input.</param>
public JsonPartialMatcher(MatchBehaviour matchBehaviour, [NotNull] object value, bool ignoreCase = false, bool throwException = false)
: base(matchBehaviour, value, ignoreCase, throwException)
{
}

/// <inheritdoc />
protected override bool IsMatch(JToken value, JToken input)
{
if (value == null || value == input)
{
return true;
}

if (input == null || value.Type != input.Type)
{
return false;
}

switch (value.Type)
{
case JTokenType.Object:
var nestedValues = value.ToObject<Dictionary<string, JToken>>();
return nestedValues?.Any() != true ||
nestedValues.All(pair => IsMatch(pair.Value, input.SelectToken(pair.Key)));

case JTokenType.Array:
var valuesArray = value.ToObject<JToken[]>();
var tokenArray = input.ToObject<JToken[]>();

if (valuesArray?.Any() != true)
{
return true;
}

return tokenArray?.Any() == true &&
valuesArray.All(subFilter => tokenArray.Any(subToken => IsMatch(subFilter, subToken)));

default:
return value.ToString() == input.ToString();
}
}
}
}
4 changes: 4 additions & 0 deletions src/WireMock.Net/Serialization/MatcherMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ public IMatcher Map([CanBeNull] MatcherModel matcher)
object value = matcher.Pattern ?? matcher.Patterns;
return new JsonMatcher(matchBehaviour, value, ignoreCase, throwExceptionWhenMatcherFails);

case "JsonPartialMatcher":
object matcherValue = matcher.Pattern ?? matcher.Patterns;
return new JsonPartialMatcher(matchBehaviour, matcherValue, ignoreCase, throwExceptionWhenMatcherFails);

case "JsonPathMatcher":
return new JsonPathMatcher(matchBehaviour, throwExceptionWhenMatcherFails, stringPatterns);

Expand Down
Loading

0 comments on commit 548fc2c

Please sign in to comment.