diff --git a/src/WireMock.Net/Matchers/JsonMatcher.cs b/src/WireMock.Net/Matchers/JsonMatcher.cs
index 85cfe6c0..e37d7fbc 100644
--- a/src/WireMock.Net/Matchers/JsonMatcher.cs
+++ b/src/WireMock.Net/Matchers/JsonMatcher.cs
@@ -1,4 +1,5 @@
-using System.Collections;
+using System;
+using System.Collections;
using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json;
@@ -17,7 +18,7 @@ namespace WireMock.Matchers
public object Value { get; }
///
- public string Name => "JsonMatcher";
+ public virtual string Name => "JsonMatcher";
///
public MatchBehaviour MatchBehaviour { get; }
@@ -29,6 +30,7 @@ namespace WireMock.Matchers
public bool ThrowException { get; }
private readonly JToken _valueAsJToken;
+ private readonly Func _jTokenConverter;
///
/// Initializes a new instance of the class.
@@ -67,6 +69,9 @@ namespace WireMock.Matchers
Value = value;
_valueAsJToken = ConvertValueToJToken(value);
+ _jTokenConverter = ignoreCase
+ ? (Func)Rename
+ : jToken => jToken;
}
///
@@ -81,7 +86,9 @@ namespace WireMock.Matchers
{
var inputAsJToken = ConvertValueToJToken(input);
- match = DeepEquals(_valueAsJToken, inputAsJToken);
+ match = IsMatch(
+ _jTokenConverter(_valueAsJToken),
+ _jTokenConverter(inputAsJToken));
}
catch (JsonException)
{
@@ -95,6 +102,17 @@ namespace WireMock.Matchers
return MatchBehaviourHelper.Convert(MatchBehaviour, MatchScores.ToScore(match));
}
+ ///
+ /// Compares the input against the matcher value
+ ///
+ /// Matcher value
+ /// Input value
+ ///
+ 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
@@ -114,19 +132,6 @@ namespace WireMock.Matchers
}
}
- 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();
diff --git a/src/WireMock.Net/Matchers/JsonPartialMatcher.cs b/src/WireMock.Net/Matchers/JsonPartialMatcher.cs
new file mode 100644
index 00000000..54b04cb5
--- /dev/null
+++ b/src/WireMock.Net/Matchers/JsonPartialMatcher.cs
@@ -0,0 +1,87 @@
+using System.Collections.Generic;
+using System.Linq;
+using JetBrains.Annotations;
+using Newtonsoft.Json.Linq;
+
+namespace WireMock.Matchers
+{
+ ///
+ /// JsonPartialMatcher
+ ///
+ public class JsonPartialMatcher : JsonMatcher
+ {
+ ///
+ public override string Name => "JsonPartialMatcher";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The string value to check for equality.
+ /// Ignore the case from the PropertyName and PropertyValue (string only).
+ /// Throw an exception when the internal matching fails because of invalid input.
+ public JsonPartialMatcher([NotNull] string value, bool ignoreCase = false, bool throwException = false)
+ : base(value, ignoreCase, throwException)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The object value to check for equality.
+ /// Ignore the case from the PropertyName and PropertyValue (string only).
+ /// Throw an exception when the internal matching fails because of invalid input.
+ public JsonPartialMatcher([NotNull] object value, bool ignoreCase = false, bool throwException = false)
+ : base(value, ignoreCase, throwException)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The match behaviour.
+ /// The value to check for equality.
+ /// Ignore the case from the PropertyName and PropertyValue (string only).
+ /// Throw an exception when the internal matching fails because of invalid input.
+ public JsonPartialMatcher(MatchBehaviour matchBehaviour, [NotNull] object value, bool ignoreCase = false, bool throwException = false)
+ : base(matchBehaviour, value, ignoreCase, throwException)
+ {
+ }
+
+ ///
+ 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>();
+ return nestedValues?.Any() != true ||
+ nestedValues.All(pair => IsMatch(pair.Value, input.SelectToken(pair.Key)));
+
+ case JTokenType.Array:
+ var valuesArray = value.ToObject();
+ var tokenArray = input.ToObject();
+
+ 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();
+ }
+ }
+ }
+}
diff --git a/src/WireMock.Net/Serialization/MatcherMapper.cs b/src/WireMock.Net/Serialization/MatcherMapper.cs
index 9a0c9bcb..3021401c 100644
--- a/src/WireMock.Net/Serialization/MatcherMapper.cs
+++ b/src/WireMock.Net/Serialization/MatcherMapper.cs
@@ -67,6 +67,10 @@ namespace WireMock.Serialization
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);
diff --git a/test/WireMock.Net.Tests/Matchers/JsonPartialMatcherTests.cs b/test/WireMock.Net.Tests/Matchers/JsonPartialMatcherTests.cs
new file mode 100644
index 00000000..0e274035
--- /dev/null
+++ b/test/WireMock.Net.Tests/Matchers/JsonPartialMatcherTests.cs
@@ -0,0 +1,385 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using FluentAssertions;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using NFluent;
+using WireMock.Matchers;
+using Xunit;
+
+namespace WireMock.Net.Tests.Matchers
+{
+ public class JsonPartialMatcherTests
+ {
+ [Fact]
+ public void JsonPartialMatcher_GetName()
+ {
+ // Assign
+ var matcher = new JsonPartialMatcher("{}");
+
+ // Act
+ string name = matcher.Name;
+
+ // Assert
+ Check.That(name).Equals("JsonPartialMatcher");
+ }
+
+ [Fact]
+ public void JsonPartialMatcher_GetValue()
+ {
+ // Assign
+ var matcher = new JsonPartialMatcher("{}");
+
+ // Act
+ object value = matcher.Value;
+
+ // Assert
+ Check.That(value).Equals("{}");
+ }
+
+ [Fact]
+ public void JsonPartialMatcher_WithInvalidStringValue_Should_ThrowException()
+ {
+ // Act
+ Action action = () => new JsonPartialMatcher(MatchBehaviour.AcceptOnMatch, "{ \"Id\"");
+
+ // Assert
+ action.Should().Throw();
+ }
+
+ [Fact]
+ public void JsonPartialMatcher_WithInvalidObjectValue_Should_ThrowException()
+ {
+ // Act
+ Action action = () => new JsonPartialMatcher(MatchBehaviour.AcceptOnMatch, new MemoryStream());
+
+ // Assert
+ action.Should().Throw();
+ }
+
+ [Fact]
+ public void JsonPartialMatcher_IsMatch_WithInvalidValue_And_ThrowExceptionIsFalse_Should_ReturnMismatch()
+ {
+ // Assign
+ var matcher = new JsonPartialMatcher("");
+
+ // Act
+ double match = matcher.IsMatch(new MemoryStream());
+
+ // Assert
+ Check.That(match).IsEqualTo(0);
+ }
+
+ [Fact]
+ public void JsonPartialMatcher_IsMatch_WithInvalidValue_And_ThrowExceptionIsTrue_Should_ReturnMismatch()
+ {
+ // Assign
+ var matcher = new JsonPartialMatcher("", false, true);
+
+ // Act
+ Action action = () => matcher.IsMatch(new MemoryStream());
+
+ // Assert
+ action.Should().Throw();
+ }
+
+ [Fact]
+ public void JsonPartialMatcher_IsMatch_ByteArray()
+ {
+ // Assign
+ var bytes = new byte[0];
+ var matcher = new JsonPartialMatcher("");
+
+ // Act
+ double match = matcher.IsMatch(bytes);
+
+ // Assert
+ Check.That(match).IsEqualTo(0);
+ }
+
+ [Fact]
+ public void JsonPartialMatcher_IsMatch_NullString()
+ {
+ // Assign
+ string s = null;
+ var matcher = new JsonPartialMatcher("");
+
+ // Act
+ double match = matcher.IsMatch(s);
+
+ // Assert
+ Check.That(match).IsEqualTo(0);
+ }
+
+ [Fact]
+ public void JsonPartialMatcher_IsMatch_NullObject()
+ {
+ // Assign
+ object o = null;
+ var matcher = new JsonPartialMatcher("");
+
+ // Act
+ double match = matcher.IsMatch(o);
+
+ // Assert
+ Check.That(match).IsEqualTo(0);
+ }
+
+ [Fact]
+ public void JsonPartialMatcher_IsMatch_JArray()
+ {
+ // Assign
+ var matcher = new JsonPartialMatcher(new[] { "x", "y" });
+
+ // Act
+ var jArray = new JArray
+ {
+ "x",
+ "y"
+ };
+ double match = matcher.IsMatch(jArray);
+
+ // Assert
+ Assert.Equal(1.0, match);
+ }
+
+ [Fact]
+ public void JsonPartialMatcher_IsMatch_JObject()
+ {
+ // Assign
+ var matcher = new JsonPartialMatcher(new { Id = 1, Name = "Test" });
+
+ // Act
+ var jobject = new JObject
+ {
+ { "Id", new JValue(1) },
+ { "Name", new JValue("Test") }
+ };
+ double match = matcher.IsMatch(jobject);
+
+ // Assert
+ Assert.Equal(1.0, match);
+ }
+
+ [Fact]
+ public void JsonPartialMatcher_IsMatch_WithIgnoreCaseTrue_JObject()
+ {
+ // Assign
+ var matcher = new JsonPartialMatcher(new { id = 1, Name = "test" }, true);
+
+ // Act
+ var jobject = new JObject
+ {
+ { "Id", new JValue(1) },
+ { "NaMe", new JValue("Test") }
+ };
+ double match = matcher.IsMatch(jobject);
+
+ // Assert
+ Assert.Equal(1.0, match);
+ }
+
+ [Fact]
+ public void JsonPartialMatcher_IsMatch_JObjectParsed()
+ {
+ // Assign
+ var matcher = new JsonPartialMatcher(new { Id = 1, Name = "Test" });
+
+ // Act
+ var jobject = JObject.Parse("{ \"Id\" : 1, \"Name\" : \"Test\" }");
+ double match = matcher.IsMatch(jobject);
+
+ // Assert
+ Assert.Equal(1.0, match);
+ }
+
+ [Fact]
+ public void JsonPartialMatcher_IsMatch_WithIgnoreCaseTrue_JObjectParsed()
+ {
+ // Assign
+ var matcher = new JsonPartialMatcher(new { Id = 1, Name = "TESt" }, true);
+
+ // Act
+ var jobject = JObject.Parse("{ \"Id\" : 1, \"Name\" : \"Test\" }");
+ double match = matcher.IsMatch(jobject);
+
+ // Assert
+ Assert.Equal(1.0, match);
+ }
+
+ [Fact]
+ public void JsonPartialMatcher_IsMatch_JArrayAsString()
+ {
+ // Assign
+ var matcher = new JsonPartialMatcher("[ \"x\", \"y\" ]");
+
+ // Act
+ var jArray = new JArray
+ {
+ "x",
+ "y"
+ };
+ double match = matcher.IsMatch(jArray);
+
+ // Assert
+ Assert.Equal(1.0, match);
+ }
+
+ [Fact]
+ public void JsonPartialMatcher_IsMatch_JObjectAsString()
+ {
+ // Assign
+ var matcher = new JsonPartialMatcher("{ \"Id\" : 1, \"Name\" : \"Test\" }");
+
+ // Act
+ var jobject = new JObject
+ {
+ { "Id", new JValue(1) },
+ { "Name", new JValue("Test") }
+ };
+ double match = matcher.IsMatch(jobject);
+
+ // Assert
+ Assert.Equal(1.0, match);
+ }
+
+ [Fact]
+ public void JsonPartialMatcher_IsMatch_WithIgnoreCaseTrue_JObjectAsString()
+ {
+ // Assign
+ var matcher = new JsonPartialMatcher("{ \"Id\" : 1, \"Name\" : \"test\" }", true);
+
+ // Act
+ var jobject = new JObject
+ {
+ { "Id", new JValue(1) },
+ { "Name", new JValue("Test") }
+ };
+ double match = matcher.IsMatch(jobject);
+
+ // Assert
+ Assert.Equal(1.0, match);
+ }
+
+ [Fact]
+ public void JsonPartialMatcher_IsMatch_JObjectAsString_RejectOnMatch()
+ {
+ // Assign
+ var matcher = new JsonPartialMatcher(MatchBehaviour.RejectOnMatch, "{ \"Id\" : 1, \"Name\" : \"Test\" }");
+
+ // Act
+ var jobject = new JObject
+ {
+ { "Id", new JValue(1) },
+ { "Name", new JValue("Test") }
+ };
+ double match = matcher.IsMatch(jobject);
+
+ // Assert
+ Assert.Equal(0.0, match);
+ }
+
+ [Fact]
+ public void JsonPartialMatcher_IsMatch_JObjectWithDateTimeOffsetAsString()
+ {
+ // Assign
+ var matcher = new JsonPartialMatcher("{ \"preferredAt\" : \"2019-11-21T10:32:53.2210009+00:00\" }");
+
+ // Act
+ var jobject = new JObject
+ {
+ { "preferredAt", new JValue("2019-11-21T10:32:53.2210009+00:00") }
+ };
+ double match = matcher.IsMatch(jobject);
+
+ // Assert
+ Assert.Equal(1.0, match);
+ }
+
+ [Theory]
+ [InlineData("{\"test\":\"abc\"}", "{\"test\":\"abc\",\"other\":\"xyz\"}")]
+ [InlineData("\"test\"", "\"test\"")]
+ [InlineData("123", "123")]
+ [InlineData("[\"test\"]", "[\"test\"]")]
+ [InlineData("[\"test\"]", "[\"test\", \"other\"]")]
+ [InlineData("[123]", "[123]")]
+ [InlineData("[123]", "[123, 456]")]
+ [InlineData("{ \"test\":\"value\" }", "{\"test\":\"value\",\"other\":123}")]
+ [InlineData("{ \"test\":\"value\" }", "{\"test\":\"value\"}")]
+ [InlineData("{\"test\":{\"nested\":\"value\"}}", "{\"test\":{\"nested\":\"value\"}}")]
+ public void JsonPartialMatcher_IsMatch_StringInputValidMatch(string value, string input)
+ {
+ // Assign
+ var matcher = new JsonPartialMatcher(value);
+
+ // Act
+ double match = matcher.IsMatch(input);
+
+ // Assert
+ Assert.Equal(1.0, match);
+ }
+
+ [Theory]
+ [InlineData("\"test\"", null)]
+ [InlineData("\"test1\"", "\"test2\"")]
+ [InlineData("123", "1234")]
+ [InlineData("[\"test\"]", "[\"test1\"]")]
+ [InlineData("[\"test\"]", "[\"test1\", \"test2\"]")]
+ [InlineData("[123]", "[1234]")]
+ [InlineData("{}", "\"test\"")]
+ [InlineData("{ \"test\":\"value\" }", "{\"test\":\"value2\"}")]
+ [InlineData("{ \"test.nested\":\"value\" }", "{\"test\":{\"nested\":\"value1\"}}")]
+ [InlineData("{\"test\":{\"test1\":\"value\"}}", "{\"test\":{\"test1\":\"value1\"}}")]
+ [InlineData("[{ \"test.nested\":\"value\" }]", "[{\"test\":{\"nested\":\"value1\"}}]")]
+ public void JsonPartialMatcher_IsMatch_StringInputWithInvalidMatch(string value, string input)
+ {
+ // Assign
+ var matcher = new JsonPartialMatcher(value);
+
+ // Act
+ double match = matcher.IsMatch(input);
+
+ // Assert
+ Assert.Equal(0.0, match);
+ }
+
+ [Theory]
+ [InlineData("{ \"test.nested\":123 }", "{\"test\":{\"nested\":123}}")]
+ [InlineData("{ \"test.nested\":[123, 456] }", "{\"test\":{\"nested\":[123, 456]}}")]
+ [InlineData("{ \"test.nested\":\"value\" }", "{\"test\":{\"nested\":\"value\"}}")]
+ [InlineData("{ \"['name.with.dot']\":\"value\" }", "{\"name.with.dot\":\"value\"}")]
+ [InlineData("[{ \"test.nested\":\"value\" }]", "[{\"test\":{\"nested\":\"value\"}}]")]
+ [InlineData("[{ \"['name.with.dot']\":\"value\" }]", "[{\"name.with.dot\":\"value\"}]")]
+ public void JsonPartialMatcher_IsMatch_ValueAsJPathValidMatch(string value, string input)
+ {
+ // Assign
+ var matcher = new JsonPartialMatcher(value);
+
+ // Act
+ double match = matcher.IsMatch(input);
+
+ // Assert
+ Assert.Equal(1.0, match);
+ }
+
+ [Theory]
+ [InlineData("{ \"test.nested\":123 }", "{\"test\":{\"nested\":456}}")]
+ [InlineData("{ \"test.nested\":[123, 456] }", "{\"test\":{\"nested\":[1, 2]}}")]
+ [InlineData("{ \"test.nested\":\"value\" }", "{\"test\":{\"nested\":\"value1\"}}")]
+ [InlineData("{ \"['name.with.dot']\":\"value\" }", "{\"name.with.dot\":\"value1\"}")]
+ [InlineData("[{ \"test.nested\":\"value\" }]", "[{\"test\":{\"nested\":\"value1\"}}]")]
+ [InlineData("[{ \"['name.with.dot']\":\"value\" }]", "[{\"name.with.dot\":\"value1\"}]")]
+ public void JsonPartialMatcher_IsMatch_ValueAsJPathInvalidMatch(string value, string input)
+ {
+ // Assign
+ var matcher = new JsonPartialMatcher(value);
+
+ // Act
+ double match = matcher.IsMatch(input);
+
+ // Assert
+ Assert.Equal(0.0, match);
+ }
+ }
+}
diff --git a/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs b/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs
index 14dd0554..d1d9f049 100644
--- a/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs
+++ b/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs
@@ -221,5 +221,85 @@ namespace WireMock.Net.Tests.Serialization
matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch);
matcher.Value.Should().BeEquivalentTo(patterns);
}
+
+ [Fact]
+ public void MatcherMapper_Map_MatcherModel_JsonPartialMatcher_Pattern_As_String()
+ {
+ // Assign
+ var pattern = "{ \"AccountIds\": [ 1, 2, 3 ] }";
+ var model = new MatcherModel
+ {
+ Name = "JsonPartialMatcher",
+ Pattern = pattern
+ };
+
+ // Act
+ var matcher = (JsonPartialMatcher)_sut.Map(model);
+
+ // Assert
+ matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch);
+ matcher.Value.Should().BeEquivalentTo(pattern);
+ }
+
+ [Fact]
+ public void MatcherMapper_Map_MatcherModel_JsonPartialMatcher_Patterns_As_String()
+ {
+ // Assign
+ var pattern1 = "{ \"AccountIds\": [ 1, 2, 3 ] }";
+ var pattern2 = "{ \"X\": \"x\" }";
+ var patterns = new[] { pattern1, pattern2 };
+ var model = new MatcherModel
+ {
+ Name = "JsonPartialMatcher",
+ Pattern = patterns
+ };
+
+ // Act
+ var matcher = (JsonPartialMatcher)_sut.Map(model);
+
+ // Assert
+ matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch);
+ matcher.Value.Should().BeEquivalentTo(patterns);
+ }
+
+ [Fact]
+ public void MatcherMapper_Map_MatcherModel_JsonPartialMatcher_Pattern_As_Object()
+ {
+ // Assign
+ var pattern = new { AccountIds = new[] { 1, 2, 3 } };
+ var model = new MatcherModel
+ {
+ Name = "JsonPartialMatcher",
+ Pattern = pattern
+ };
+
+ // Act
+ var matcher = (JsonPartialMatcher)_sut.Map(model);
+
+ // Assert
+ matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch);
+ matcher.Value.Should().BeEquivalentTo(pattern);
+ }
+
+ [Fact]
+ public void MatcherMapper_Map_MatcherModel_JsonPartialMatcher_Patterns_As_Object()
+ {
+ // Assign
+ object pattern1 = new { AccountIds = new[] { 1, 2, 3 } };
+ object pattern2 = new { X = "x" };
+ var patterns = new[] { pattern1, pattern2 };
+ var model = new MatcherModel
+ {
+ Name = "JsonPartialMatcher",
+ Patterns = patterns
+ };
+
+ // Act
+ var matcher = (JsonMatcher)_sut.Map(model);
+
+ // Assert
+ matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch);
+ matcher.Value.Should().BeEquivalentTo(patterns);
+ }
}
-}
\ No newline at end of file
+}