Skip to content

Commit 5054499

Browse files
committed
add WithoutStrictOrdering for IJsonAssertionOptions<T>
1 parent 0606d9a commit 5054499

5 files changed

Lines changed: 113 additions & 2 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System.Linq;
2+
using Newtonsoft.Json.Linq;
3+
4+
namespace FluentAssertions.Json.Common
5+
{
6+
internal static class JTokenExtensions
7+
{
8+
public static JToken Normalize(this JToken token)
9+
{
10+
return token switch
11+
{
12+
JObject obj => new JObject(obj.Properties().OrderBy(p => p.Name).Select(p => new JProperty(p.Name, Normalize(p.Value)))),
13+
JArray array => new JArray(array.Select(Normalize).OrderBy(x => x.ToString(Newtonsoft.Json.Formatting.None))),
14+
_ => token
15+
};
16+
}
17+
}
18+
}

Src/FluentAssertions.Json/IJsonAssertionOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,10 @@ public interface IJsonAssertionOptions<T>
1616
/// The assertion to execute when the predicate is met.
1717
/// </param>
1818
IJsonAssertionRestriction<T, TProperty> Using<TProperty>(Action<IAssertionContext<TProperty>> action);
19+
20+
/// <summary>
21+
/// Configures the JSON assertion to ignore the order of elements in arrays or collections during comparison, allowing for equivalency checks regardless of element sequence.
22+
/// </summary>
23+
IJsonAssertionOptions<T> WithoutStrictOrdering();
1924
}
2025
}

Src/FluentAssertions.Json/JTokenDifferentiator.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using FluentAssertions.Execution;
5+
using FluentAssertions.Json.Common;
56
using Newtonsoft.Json.Linq;
67

78
namespace FluentAssertions.Json
@@ -11,12 +12,14 @@ internal class JTokenDifferentiator
1112
private readonly bool ignoreExtraProperties;
1213

1314
private readonly Func<IJsonAssertionOptions<object>, IJsonAssertionOptions<object>> config;
15+
private readonly JsonAssertionOptions<object> options;
1416

1517
public JTokenDifferentiator(bool ignoreExtraProperties,
1618
Func<IJsonAssertionOptions<object>, IJsonAssertionOptions<object>> config)
1719
{
1820
this.ignoreExtraProperties = ignoreExtraProperties;
1921
this.config = config;
22+
this.options = (JsonAssertionOptions<object>)config(new JsonAssertionOptions<object>());
2023
}
2124

2225
public Difference FindFirstDifference(JToken actual, JToken expected)
@@ -38,6 +41,12 @@ public Difference FindFirstDifference(JToken actual, JToken expected)
3841
return new Difference(DifferenceKind.ExpectedIsNull, path);
3942
}
4043

44+
if (options.IsStrictOrdering is false)
45+
{
46+
actual = actual.Normalize();
47+
expected = expected.Normalize();
48+
}
49+
4150
return FindFirstDifference(actual, expected, path);
4251
}
4352

Src/FluentAssertions.Json/JsonAssertionOptions.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,27 @@
44
namespace FluentAssertions.Json
55
{
66
/// <summary>
7-
/// Represents the run-time type-specific behavior of a JSON structural equivalency assertion. It is the equivalent of <see cref="FluentAssertions.Equivalency.EquivalencyAssertionOptions{T}"/>
7+
/// Represents the run-time type-specific behavior of a JSON structural equivalency assertion. It is the equivalent of <see cref="FluentAssertions.Equivalency.EquivalencyOptions{T}"/>
88
/// </summary>
99
public sealed class JsonAssertionOptions<T> : EquivalencyOptions<T>, IJsonAssertionOptions<T>
1010
{
11+
internal JsonAssertionOptions() { }
12+
1113
public JsonAssertionOptions(EquivalencyOptions<T> equivalencyAssertionOptions) : base(equivalencyAssertionOptions)
1214
{
13-
1415
}
16+
17+
public bool IsStrictOrdering { get; private set; } = true;
18+
1519
public new IJsonAssertionRestriction<T, TProperty> Using<TProperty>(Action<IAssertionContext<TProperty>> action)
1620
{
1721
return new JsonAssertionRestriction<T, TProperty>(base.Using(action));
1822
}
23+
24+
public new IJsonAssertionOptions<T> WithoutStrictOrdering()
25+
{
26+
IsStrictOrdering = false;
27+
return this;
28+
}
1929
}
2030
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Newtonsoft.Json.Linq;
4+
using Xunit;
5+
using Xunit.Sdk;
6+
7+
namespace FluentAssertions.Json.Specs
8+
{
9+
public class WithoutStrictOrderingSpecs
10+
{
11+
[Theory]
12+
[MemberData(nameof(Should_HandleJToken_WhenNeedToIgnoreOrdering_SampleData))]
13+
public void Should_HandleJToken_WhenNeedToIgnoreOrdering(string json1, string json2)
14+
{
15+
// Arrange
16+
var j1 = JToken.Parse(json1);
17+
var j2 = JToken.Parse(json2);
18+
19+
// Act
20+
j1.Should().BeEquivalentTo(j2, opt => opt.WithoutStrictOrdering());
21+
22+
// Assert
23+
}
24+
25+
[Theory]
26+
[MemberData(nameof(Should_DoNotHandleJToken_WhenNoNeedToIgnoreOrdering_SampleData))]
27+
public void Should_DoNotHandleJToken_WhenNoNeedToIgnoreOrdering(string json1, string json2)
28+
{
29+
// Arrange
30+
var j1 = JToken.Parse(json1);
31+
var j2 = JToken.Parse(json2);
32+
33+
// Act
34+
var action = new Func<AndConstraint<JTokenAssertions>>(() => j1.Should().BeEquivalentTo(j2));
35+
36+
// Assert
37+
action.Should().Throw<XunitException>();
38+
}
39+
40+
public static IEnumerable<object[]> Should_DoNotHandleJToken_WhenNoNeedToIgnoreOrdering_SampleData()
41+
{
42+
yield return new object[] { @"{""ids"":[1,2,3]}", @"{""ids"":[3,2,1]}" };
43+
yield return new object[] { @"{""names"":[""a"",""b""]}", @"{""names"":[""b"",""a""]}" };
44+
yield return new object[]
45+
{
46+
@"{""vals"":[{""type"":1,""name"":""a""},{""name"":""b"",""type"":2}]}",
47+
@"{""vals"":[{""type"":2,""name"":""b""},{""name"":""a"",""type"":1}]}"
48+
};
49+
}
50+
51+
public static IEnumerable<object[]> Should_HandleJToken_WhenNeedToIgnoreOrdering_SampleData()
52+
{
53+
yield return new object[] { @"{""ids"":[1,2,3]}", @"{""ids"":[3,2,1]}" };
54+
yield return new object[] { @"{""ids"":[1,2,3]}", @"{""ids"":[1,2,3]}" };
55+
yield return new object[] { @"{""type"":2,""name"":""b""}", @"{""name"":""b"",""type"":2}" };
56+
yield return new object[] { @"{""names"":[""a"",""b""]}", @"{""names"":[""b"",""a""]}" };
57+
yield return new object[]
58+
{
59+
@"{""vals"":[{""type"":1,""name"":""a""},{""name"":""b"",""type"":2}]}",
60+
@"{""vals"":[{""type"":2,""name"":""b""},{""name"":""a"",""type"":1}]}"
61+
};
62+
yield return new object[]
63+
{
64+
@"{""vals"":[{""type"":1,""name"":""a""},{""name"":""b"",""type"":2}]}",
65+
@"{""vals"":[{""name"":""a"",""type"":1},{""type"":2,""name"":""b""}]}"
66+
};
67+
}
68+
}
69+
}

0 commit comments

Comments
 (0)