Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
18 changes: 18 additions & 0 deletions Src/FluentAssertions.Json/Common/JTokenExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Linq;
using Newtonsoft.Json.Linq;

namespace FluentAssertions.Json.Common
{
internal static class JTokenExtensions
{
public static JToken Normalize(this JToken token)
{
return token switch
{
JObject obj => new JObject(obj.Properties().OrderBy(p => p.Name).Select(p => new JProperty(p.Name, Normalize(p.Value)))),
JArray array => new JArray(array.Select(Normalize).OrderBy(x => x.ToString(Newtonsoft.Json.Formatting.None))),
_ => token
Comment on lines +21 to +23
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ (nitpick) I would prefer to have this spelled out instead of a switch statement for readability

};
}
}
}
5 changes: 5 additions & 0 deletions Src/FluentAssertions.Json/IJsonAssertionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,10 @@ public interface IJsonAssertionOptions<T>
/// The assertion to execute when the predicate is met.
/// </param>
IJsonAssertionRestriction<T, TProperty> Using<TProperty>(Action<IAssertionContext<TProperty>> action);

/// <summary>
/// Configures the JSON assertion to ignore the order of elements in arrays or collections during comparison, allowing for equivalency checks regardless of element sequence.
/// </summary>
IJsonAssertionOptions<T> WithoutStrictOrdering();
}
}
9 changes: 9 additions & 0 deletions Src/FluentAssertions.Json/JTokenDifferentiator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using FluentAssertions.Execution;
using FluentAssertions.Json.Common;
using Newtonsoft.Json.Linq;

namespace FluentAssertions.Json
Expand All @@ -11,12 +12,14 @@ internal class JTokenDifferentiator
private readonly bool ignoreExtraProperties;

private readonly Func<IJsonAssertionOptions<object>, IJsonAssertionOptions<object>> config;
private readonly JsonAssertionOptions<object> options;

public JTokenDifferentiator(bool ignoreExtraProperties,
Func<IJsonAssertionOptions<object>, IJsonAssertionOptions<object>> config)
{
this.ignoreExtraProperties = ignoreExtraProperties;
this.config = config;
this.options = (JsonAssertionOptions<object>)config(new JsonAssertionOptions<object>());
}

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

if (options.IsStrictOrdering is false)
{
actual = actual.Normalize();
expected = expected.Normalize();
}

return FindFirstDifference(actual, expected, path);
}

Expand Down
14 changes: 12 additions & 2 deletions Src/FluentAssertions.Json/JsonAssertionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,27 @@
namespace FluentAssertions.Json
{
/// <summary>
/// Represents the run-time type-specific behavior of a JSON structural equivalency assertion. It is the equivalent of <see cref="FluentAssertions.Equivalency.EquivalencyAssertionOptions{T}"/>
/// Represents the run-time type-specific behavior of a JSON structural equivalency assertion. It is the equivalent of <see cref="FluentAssertions.Equivalency.EquivalencyOptions{T}"/>
/// </summary>
public sealed class JsonAssertionOptions<T> : EquivalencyOptions<T>, IJsonAssertionOptions<T>
{
internal JsonAssertionOptions() { }

public JsonAssertionOptions(EquivalencyOptions<T> equivalencyAssertionOptions) : base(equivalencyAssertionOptions)
{

}

internal bool IsStrictOrdering { get; private set; } = true;

public new IJsonAssertionRestriction<T, TProperty> Using<TProperty>(Action<IAssertionContext<TProperty>> action)
{
return new JsonAssertionRestriction<T, TProperty>(base.Using(action));
}

public new IJsonAssertionOptions<T> WithoutStrictOrdering()
{
IsStrictOrdering = false;
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace FluentAssertions.Json
public interface IJsonAssertionOptions<T>
{
FluentAssertions.Json.IJsonAssertionRestriction<T, TProperty> Using<TProperty>(System.Action<FluentAssertions.Equivalency.IAssertionContext<TProperty>> action);
FluentAssertions.Json.IJsonAssertionOptions<T> WithoutStrictOrdering();
}
public interface IJsonAssertionRestriction<T, TMember>
{
Expand Down Expand Up @@ -50,6 +51,7 @@ namespace FluentAssertions.Json
{
public JsonAssertionOptions(FluentAssertions.Equivalency.EquivalencyOptions<T> equivalencyAssertionOptions) { }
public FluentAssertions.Json.IJsonAssertionRestriction<T, TProperty> Using<TProperty>(System.Action<FluentAssertions.Equivalency.IAssertionContext<TProperty>> action) { }
public FluentAssertions.Json.IJsonAssertionOptions<T> WithoutStrictOrdering() { }
}
public sealed class JsonAssertionRestriction<T, TProperty> : FluentAssertions.Json.IJsonAssertionRestriction<T, TProperty>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace FluentAssertions.Json
public interface IJsonAssertionOptions<T>
{
FluentAssertions.Json.IJsonAssertionRestriction<T, TProperty> Using<TProperty>(System.Action<FluentAssertions.Equivalency.IAssertionContext<TProperty>> action);
FluentAssertions.Json.IJsonAssertionOptions<T> WithoutStrictOrdering();
}
public interface IJsonAssertionRestriction<T, TMember>
{
Expand Down Expand Up @@ -50,6 +51,7 @@ namespace FluentAssertions.Json
{
public JsonAssertionOptions(FluentAssertions.Equivalency.EquivalencyOptions<T> equivalencyAssertionOptions) { }
public FluentAssertions.Json.IJsonAssertionRestriction<T, TProperty> Using<TProperty>(System.Action<FluentAssertions.Equivalency.IAssertionContext<TProperty>> action) { }
public FluentAssertions.Json.IJsonAssertionOptions<T> WithoutStrictOrdering() { }
}
public sealed class JsonAssertionRestriction<T, TProperty> : FluentAssertions.Json.IJsonAssertionRestriction<T, TProperty>
{
Expand Down
69 changes: 69 additions & 0 deletions Tests/FluentAssertions.Json.Specs/WithoutStrictOrderingSpecs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using Xunit;
using Xunit.Sdk;

namespace FluentAssertions.Json.Specs
{
public class WithoutStrictOrderingSpecs
{
[Theory]
[MemberData(nameof(Should_HandleJToken_WhenNeedToIgnoreOrdering_SampleData))]
public void Should_HandleJToken_WhenNeedToIgnoreOrdering(string json1, string json2)
{
// Arrange
var j1 = JToken.Parse(json1);
var j2 = JToken.Parse(json2);

// Act
j1.Should().BeEquivalentTo(j2, opt => opt.WithoutStrictOrdering());

// Assert
}

[Theory]
[MemberData(nameof(Should_DoNotHandleJToken_WhenNoNeedToIgnoreOrdering_SampleData))]
public void Should_DoNotHandleJToken_WhenNoNeedToIgnoreOrdering(string json1, string json2)
{
// Arrange
var j1 = JToken.Parse(json1);
var j2 = JToken.Parse(json2);

// Act
var action = new Func<AndConstraint<JTokenAssertions>>(() => j1.Should().BeEquivalentTo(j2));

// Assert
action.Should().Throw<XunitException>();
}

public static IEnumerable<object[]> Should_DoNotHandleJToken_WhenNoNeedToIgnoreOrdering_SampleData()
{
yield return new object[] { @"{""ids"":[1,2,3]}", @"{""ids"":[3,2,1]}" };
yield return new object[] { @"{""names"":[""a"",""b""]}", @"{""names"":[""b"",""a""]}" };
yield return new object[]
{
@"{""vals"":[{""type"":1,""name"":""a""},{""name"":""b"",""type"":2}]}",
@"{""vals"":[{""type"":2,""name"":""b""},{""name"":""a"",""type"":1}]}"
};
}

public static IEnumerable<object[]> Should_HandleJToken_WhenNeedToIgnoreOrdering_SampleData()
{
yield return new object[] { @"{""ids"":[1,2,3]}", @"{""ids"":[3,2,1]}" };
yield return new object[] { @"{""ids"":[1,2,3]}", @"{""ids"":[1,2,3]}" };
yield return new object[] { @"{""type"":2,""name"":""b""}", @"{""name"":""b"",""type"":2}" };
yield return new object[] { @"{""names"":[""a"",""b""]}", @"{""names"":[""b"",""a""]}" };
yield return new object[]
{
@"{""vals"":[{""type"":1,""name"":""a""},{""name"":""b"",""type"":2}]}",
@"{""vals"":[{""type"":2,""name"":""b""},{""name"":""a"",""type"":1}]}"
};
yield return new object[]
{
@"{""vals"":[{""type"":1,""name"":""a""},{""name"":""b"",""type"":2}]}",
@"{""vals"":[{""name"":""a"",""type"":1},{""type"":2,""name"":""b""}]}"
};
}
}
}
Loading