Update JsonMatcher to support IgnoreArrayOrder

This commit is contained in:
Stef Heyenrath
2026-05-29 20:42:52 +02:00
parent 36b0a93a6c
commit 5a1605751b
4 changed files with 272 additions and 11 deletions
@@ -32,6 +32,11 @@ public class JsonMatcher : IJsonMatcher
/// </summary> /// </summary>
public bool Regex { get; } public bool Regex { get; }
/// <summary>
/// Ignore array order when comparing
/// </summary>
public bool IgnoreArrayOrder { get; }
private readonly JToken _valueAsJToken; private readonly JToken _valueAsJToken;
/// <summary> /// <summary>
@@ -40,7 +45,8 @@ public class JsonMatcher : IJsonMatcher
/// <param name="value">The string value to check for equality.</param> /// <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="ignoreCase">Ignore the case from the PropertyName and PropertyValue (string only).</param>
/// <param name="regex">Support Regex.</param> /// <param name="regex">Support Regex.</param>
public JsonMatcher(string value, bool ignoreCase = false, bool regex = false) : this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, regex) /// <param name="ignoreArrayOrder">Ignore array element order when comparing.</param>
public JsonMatcher(string value, bool ignoreCase = false, bool regex = false, bool ignoreArrayOrder = false) : this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, regex, ignoreArrayOrder)
{ {
} }
@@ -50,7 +56,8 @@ public class JsonMatcher : IJsonMatcher
/// <param name="value">The object value to check for equality.</param> /// <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="ignoreCase">Ignore the case from the PropertyName and PropertyValue (string only).</param>
/// <param name="regex">Support Regex.</param> /// <param name="regex">Support Regex.</param>
public JsonMatcher(object value, bool ignoreCase = false, bool regex = false) : this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, regex) /// <param name="ignoreArrayOrder">Ignore array element order when comparing.</param>
public JsonMatcher(object value, bool ignoreCase = false, bool regex = false, bool ignoreArrayOrder = false) : this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, regex, ignoreArrayOrder)
{ {
} }
@@ -61,13 +68,15 @@ public class JsonMatcher : IJsonMatcher
/// <param name="value">The value to check for equality.</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="ignoreCase">Ignore the case from the PropertyName and PropertyValue (string only).</param>
/// <param name="regex">Support Regex.</param> /// <param name="regex">Support Regex.</param>
public JsonMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false, bool regex = false) /// <param name="ignoreArrayOrder">Ignore array element order when comparing.</param>
public JsonMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false, bool regex = false, bool ignoreArrayOrder = false)
{ {
Guard.NotNull(value); Guard.NotNull(value);
MatchBehaviour = matchBehaviour; MatchBehaviour = matchBehaviour;
IgnoreCase = ignoreCase; IgnoreCase = ignoreCase;
Regex = regex; Regex = regex;
IgnoreArrayOrder = ignoreArrayOrder;
Value = value; Value = value;
_valueAsJToken = ConvertValueToJToken(value); _valueAsJToken = ConvertValueToJToken(value);
@@ -106,7 +115,8 @@ public class JsonMatcher : IJsonMatcher
$"{MatchBehaviour.GetFullyQualifiedEnumValue()}, " + $"{MatchBehaviour.GetFullyQualifiedEnumValue()}, " +
$"{CSharpFormatter.ConvertToAnonymousObjectDefinition(Value, 3)}, " + $"{CSharpFormatter.ConvertToAnonymousObjectDefinition(Value, 3)}, " +
$"{CSharpFormatter.ToCSharpBooleanLiteral(IgnoreCase)}, " + $"{CSharpFormatter.ToCSharpBooleanLiteral(IgnoreCase)}, " +
$"{CSharpFormatter.ToCSharpBooleanLiteral(Regex)}" + $"{CSharpFormatter.ToCSharpBooleanLiteral(Regex)}, " +
$"{CSharpFormatter.ToCSharpBooleanLiteral(IgnoreArrayOrder)}" +
$")"; $")";
} }
@@ -183,6 +193,13 @@ public class JsonMatcher : IJsonMatcher
return false; return false;
} }
if (IgnoreArrayOrder)
{
// Sort both arrays by their string representation and compare
valueArray = valueArray.OrderBy(t => t.ToString()).ToArray();
inputArray = inputArray.OrderBy(t => t.ToString()).ToArray();
}
return !valueArray.Where((valueToken, index) => !IsMatch(valueToken, inputArray[index])).Any(); return !valueArray.Where((valueToken, index) => !IsMatch(valueToken, inputArray[index])).Any();
default: default:
@@ -35,6 +35,11 @@ public class SystemTextJsonMatcher : IJsonMatcher
/// </summary> /// </summary>
public bool Regex { get; } public bool Regex { get; }
/// <summary>
/// Ignore array order when comparing
/// </summary>
public bool IgnoreArrayOrder { get; }
private readonly JsonElement _valueAsJsonElement; private readonly JsonElement _valueAsJsonElement;
/// <summary> /// <summary>
@@ -43,8 +48,9 @@ public class SystemTextJsonMatcher : IJsonMatcher
/// <param name="value">The string value to check for equality.</param> /// <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="ignoreCase">Ignore the case from the PropertyName and PropertyValue (string only).</param>
/// <param name="regex">Support Regex.</param> /// <param name="regex">Support Regex.</param>
public SystemTextJsonMatcher(string value, bool ignoreCase = false, bool regex = false) /// <param name="ignoreArrayOrder">Ignore array element order when comparing.</param>
: this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, regex) public SystemTextJsonMatcher(string value, bool ignoreCase = false, bool regex = false, bool ignoreArrayOrder = false)
: this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, regex, ignoreArrayOrder)
{ {
} }
@@ -54,8 +60,9 @@ public class SystemTextJsonMatcher : IJsonMatcher
/// <param name="value">The object value to check for equality.</param> /// <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="ignoreCase">Ignore the case from the PropertyName and PropertyValue (string only).</param>
/// <param name="regex">Support Regex.</param> /// <param name="regex">Support Regex.</param>
public SystemTextJsonMatcher(object value, bool ignoreCase = false, bool regex = false) /// <param name="ignoreArrayOrder">Ignore array element order when comparing.</param>
: this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, regex) public SystemTextJsonMatcher(object value, bool ignoreCase = false, bool regex = false, bool ignoreArrayOrder = false)
: this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, regex, ignoreArrayOrder)
{ {
} }
@@ -66,13 +73,15 @@ public class SystemTextJsonMatcher : IJsonMatcher
/// <param name="value">The value to check for equality.</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="ignoreCase">Ignore the case from the PropertyName and PropertyValue (string only).</param>
/// <param name="regex">Support Regex.</param> /// <param name="regex">Support Regex.</param>
public SystemTextJsonMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false, bool regex = false) /// <param name="ignoreArrayOrder">Ignore array element order when comparing.</param>
public SystemTextJsonMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false, bool regex = false, bool ignoreArrayOrder = false)
{ {
Guard.NotNull(value); Guard.NotNull(value);
MatchBehaviour = matchBehaviour; MatchBehaviour = matchBehaviour;
IgnoreCase = ignoreCase; IgnoreCase = ignoreCase;
Regex = regex; Regex = regex;
IgnoreArrayOrder = ignoreArrayOrder;
Value = value; Value = value;
_valueAsJsonElement = ConvertToJsonElement(value); _valueAsJsonElement = ConvertToJsonElement(value);
@@ -111,7 +120,8 @@ public class SystemTextJsonMatcher : IJsonMatcher
$"{MatchBehaviour.GetFullyQualifiedEnumValue()}, " + $"{MatchBehaviour.GetFullyQualifiedEnumValue()}, " +
$"{CSharpFormatter.ConvertToAnonymousObjectDefinition(Value, 3)}, " + $"{CSharpFormatter.ConvertToAnonymousObjectDefinition(Value, 3)}, " +
$"{CSharpFormatter.ToCSharpBooleanLiteral(IgnoreCase)}, " + $"{CSharpFormatter.ToCSharpBooleanLiteral(IgnoreCase)}, " +
$"{CSharpFormatter.ToCSharpBooleanLiteral(Regex)}" + $"{CSharpFormatter.ToCSharpBooleanLiteral(Regex)}, " +
$"{CSharpFormatter.ToCSharpBooleanLiteral(IgnoreArrayOrder)}" +
$")"; $")";
} }
@@ -202,6 +212,13 @@ public class SystemTextJsonMatcher : IJsonMatcher
return false; return false;
} }
if (IgnoreArrayOrder)
{
// Sort both arrays by their string representation and compare
valueArray = valueArray.OrderBy(e => e.GetRawText()).ToArray();
inputArray = inputArray.OrderBy(e => e.GetRawText()).ToArray();
}
return !valueArray.Where((valueToken, index) => !IsMatch(valueToken, inputArray[index])).Any(); return !valueArray.Where((valueToken, index) => !IsMatch(valueToken, inputArray[index])).Any();
} }
@@ -523,4 +523,134 @@ public class JsonMatcherTests
// Assert // Assert
Assert.Equal(1.0, score); Assert.Equal(1.0, score);
} }
[Fact]
public void JsonMatcher_IsMatch_Array_WithIgnoreArrayOrderTrue_DifferentOrder_ShouldMatch()
{
// Assign
var matcher = new JsonMatcher(new[] { "a", "b", "c" }, ignoreArrayOrder: true);
// Act
var jArray = new JArray
{
"c",
"a",
"b"
};
var score = matcher.IsMatch(jArray).Score;
// Assert
Assert.Equal(1.0, score);
}
[Fact]
public void JsonMatcher_IsMatch_Array_WithIgnoreArrayOrderFalse_DifferentOrder_ShouldNotMatch()
{
// Assign
var matcher = new JsonMatcher(new[] { "a", "b", "c" }, ignoreArrayOrder: false);
// Act
var jArray = new JArray
{
"c",
"a",
"b"
};
var score = matcher.IsMatch(jArray).Score;
// Assert
Assert.Equal(MatchScores.Mismatch, score);
}
[Fact]
public void JsonMatcher_IsMatch_Array_WithIgnoreArrayOrderTrue_SameOrder_ShouldMatch()
{
// Assign
var matcher = new JsonMatcher(new[] { "a", "b", "c" }, ignoreArrayOrder: true);
// Act
var jArray = new JArray
{
"a",
"b",
"c"
};
var score = matcher.IsMatch(jArray).Score;
// Assert
Assert.Equal(1.0, score);
}
[Fact]
public void JsonMatcher_IsMatch_Array_WithIgnoreArrayOrderTrue_DifferentLength_ShouldNotMatch()
{
// Assign
var matcher = new JsonMatcher(new[] { "a", "b", "c" }, ignoreArrayOrder: true);
// Act
var jArray = new JArray
{
"a",
"b"
};
var score = matcher.IsMatch(jArray).Score;
// Assert
Assert.Equal(MatchScores.Mismatch, score);
}
[Fact]
public void JsonMatcher_IsMatch_ObjectWithArray_WithIgnoreArrayOrderTrue_DifferentOrder_ShouldMatch()
{
// Assign
var matcher = new JsonMatcher(new { Items = new[] { "x", "y", "z" } }, ignoreArrayOrder: true);
// Act
var jObject = new JObject
{
{ "Items", new JArray("z", "x", "y") }
};
var score = matcher.IsMatch(jObject).Score;
// Assert
Assert.Equal(1.0, score);
}
[Fact]
public void JsonMatcher_IsMatch_ArrayAsString_WithIgnoreArrayOrderTrue_DifferentOrder_ShouldMatch()
{
// Assign
var matcher = new JsonMatcher("[ \"a\", \"b\", \"c\" ]", ignoreArrayOrder: true);
// Act
var jArray = new JArray
{
"c",
"b",
"a"
};
var score = matcher.IsMatch(jArray).Score;
// Assert
Assert.Equal(1.0, score);
}
[Fact]
public void JsonMatcher_IsMatch_ArrayOfNumbers_WithIgnoreArrayOrderTrue_DifferentOrder_ShouldMatch()
{
// Assign
var matcher = new JsonMatcher(new[] { 1, 2, 3 }, ignoreArrayOrder: true);
// Act
var jArray = new JArray
{
3,
1,
2
};
var score = matcher.IsMatch(jArray).Score;
// Assert
Assert.Equal(1.0, score);
}
} }
@@ -367,4 +367,101 @@ public class SystemTextJsonMatcherTests
// Assert // Assert
Assert.Equal(1.0, match); Assert.Equal(1.0, match);
} }
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_Array_WithIgnoreArrayOrderTrue_DifferentOrder_ShouldMatch()
{
// Assign
var matcher = new SystemTextJsonMatcher(new[] { "a", "b", "c" }, ignoreArrayOrder: true);
// Act
var jsonElement = JsonDocument.Parse("[ \"c\", \"a\", \"b\" ]").RootElement;
var score = matcher.IsMatch(jsonElement).Score;
// Assert
Assert.Equal(1.0, score);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_Array_WithIgnoreArrayOrderFalse_DifferentOrder_ShouldNotMatch()
{
// Assign
var matcher = new SystemTextJsonMatcher(new[] { "a", "b", "c" }, ignoreArrayOrder: false);
// Act
var jsonElement = JsonDocument.Parse("[ \"c\", \"a\", \"b\" ]").RootElement;
var score = matcher.IsMatch(jsonElement).Score;
// Assert
Assert.Equal(MatchScores.Mismatch, score);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_Array_WithIgnoreArrayOrderTrue_SameOrder_ShouldMatch()
{
// Assign
var matcher = new SystemTextJsonMatcher(new[] { "a", "b", "c" }, ignoreArrayOrder: true);
// Act
var jsonElement = JsonDocument.Parse("[ \"a\", \"b\", \"c\" ]").RootElement;
var score = matcher.IsMatch(jsonElement).Score;
// Assert
Assert.Equal(1.0, score);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_Array_WithIgnoreArrayOrderTrue_DifferentLength_ShouldNotMatch()
{
// Assign
var matcher = new SystemTextJsonMatcher(new[] { "a", "b", "c" }, ignoreArrayOrder: true);
// Act
var jsonElement = JsonDocument.Parse("[ \"a\", \"b\" ]").RootElement;
var score = matcher.IsMatch(jsonElement).Score;
// Assert
Assert.Equal(MatchScores.Mismatch, score);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_ObjectWithArray_WithIgnoreArrayOrderTrue_DifferentOrder_ShouldMatch()
{
// Assign
var matcher = new SystemTextJsonMatcher(new { Items = new[] { "x", "y", "z" } }, ignoreArrayOrder: true);
// Act
var match = matcher.IsMatch("{ \"Items\" : [ \"z\", \"x\", \"y\" ] }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_ArrayAsString_WithIgnoreArrayOrderTrue_DifferentOrder_ShouldMatch()
{
// Assign
var matcher = new SystemTextJsonMatcher("[ \"a\", \"b\", \"c\" ]", ignoreArrayOrder: true);
// Act
var jsonElement = JsonDocument.Parse("[ \"c\", \"b\", \"a\" ]").RootElement;
var score = matcher.IsMatch(jsonElement).Score;
// Assert
Assert.Equal(1.0, score);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_ArrayOfNumbers_WithIgnoreArrayOrderTrue_DifferentOrder_ShouldMatch()
{
// Assign
var matcher = new SystemTextJsonMatcher(new[] { 1, 2, 3 }, ignoreArrayOrder: true);
// Act
var jsonElement = JsonDocument.Parse("[ 3, 1, 2 ]").RootElement;
var score = matcher.IsMatch(jsonElement).Score;
// Assert
Assert.Equal(1.0, score);
}
}