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

Support for partial JSON matching #539

Merged
merged 10 commits into from
Nov 17, 2020
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
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;
gleb-osokin marked this conversation as resolved.
Show resolved Hide resolved
}

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