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

Add For/Exclude to allow exclusion of members inside a collection #1782

Merged
merged 25 commits into from Apr 20, 2022
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
67b9c7f
Added EquivalencyAssertionOptionsBuilder to allow excluding members n…
whymatter Jan 14, 2022
a10e079
Merge branch 'master' of https://github.com/whymatter/fluentassertions
whymatter Feb 17, 2022
96842c8
Call copy constructor
whymatter Jan 16, 2022
b36dab7
Implemented ThenExcluding
whymatter Jan 20, 2022
066380c
Run AcceptApiChanges, added test
whymatter Jan 20, 2022
b8d80d8
PR feedback
whymatter Feb 3, 2022
884969f
Added documentation and release notes for ThenExcluding
whymatter Feb 3, 2022
c93fc0f
Fixed wrong pull request numer in release notes
whymatter Feb 3, 2022
2f8f97f
AcceptApiChanges.ps1
whymatter Feb 3, 2022
e2dc5d7
PR feedback, refere to ThenExcluding instead of NestedExclusionOption…
whymatter Feb 5, 2022
6fffb89
Add * just in segments, not in dottedPath
whymatter Feb 17, 2022
cf01143
Switch EqualsAnyIndexQualifier conditions
whymatter Feb 21, 2022
f0be438
Use is instead of == for null check
whymatter Feb 21, 2022
3c05a68
Fix typo
whymatter Feb 21, 2022
3222aed
Fix typo
whymatter Feb 21, 2022
0774341
Cleanup .csproj
whymatter Feb 21, 2022
e65bd76
Merge branch 'fluentassertions:develop' into issue-1771
whymatter Mar 19, 2022
981d36f
Rename to For, Exclude
whymatter Mar 19, 2022
f8f0e8e
Update docs/_pages/objectgraphs.md
whymatter Apr 7, 2022
bff06f5
Merge branch 'develop' into issue-1771
whymatter Apr 7, 2022
2255439
Rename Combine methods to Join/Append, fix that For can be used to ex…
whymatter Apr 7, 2022
7b99ffc
Code review fixes, calling For without Exclude does not compile
whymatter Apr 11, 2022
8086925
ApiApproval
whymatter Apr 13, 2022
88d1909
Merge branch 'fluentassertions:develop' into issue-1771
whymatter Apr 19, 2022
903e494
releases.md, remove null check
whymatter Apr 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 18 additions & 6 deletions Src/FluentAssertions/Common/MemberPath.cs
Expand Up @@ -17,6 +17,8 @@ internal class MemberPath

private string[] segments;

private static readonly MemberPathSegmentEqualityComparer MemberPathSegmentEqualityComparer = new();

public MemberPath(IMember member, string parentPath)
: this(member.ReflectedType, member.DeclaringType, parentPath.Combine(member.Name))
{
Expand Down Expand Up @@ -53,7 +55,7 @@ public bool IsSameAs(MemberPath candidate)
{
string[] candidateSegments = candidate.Segments;

return candidateSegments.SequenceEqual(Segments);
return candidateSegments.SequenceEqual(Segments, MemberPathSegmentEqualityComparer);
}

return false;
Expand All @@ -64,15 +66,22 @@ private bool IsParentOf(MemberPath candidate)
string[] candidateSegments = candidate.Segments;

return candidateSegments.Length > Segments.Length &&
candidateSegments.Take(Segments.Length).SequenceEqual(Segments);
candidateSegments.Take(Segments.Length).SequenceEqual(Segments, MemberPathSegmentEqualityComparer);
}

private bool IsChildOf(MemberPath candidate)
{
string[] candidateSegments = candidate.Segments;

return candidateSegments.Length < Segments.Length
&& candidateSegments.SequenceEqual(Segments.Take(candidateSegments.Length));
&& candidateSegments.SequenceEqual(Segments.Take(candidateSegments.Length),
MemberPathSegmentEqualityComparer);
}

public MemberPath Join(MemberPath nextPath, string separator)
dennisdoomen marked this conversation as resolved.
Show resolved Hide resolved
{
var extendedDottedPath = dottedPath.Combine(nextPath.dottedPath, separator);
return new MemberPath(declaringType, nextPath.reflectedType, extendedDottedPath);
}

/// <summary>
Expand All @@ -86,7 +95,7 @@ public bool IsEquivalentTo(string path)
public bool HasSameParentAs(MemberPath path)
{
return Segments.Length == path.Segments.Length
&& GetParentSegments().SequenceEqual(path.GetParentSegments());
&& GetParentSegments().SequenceEqual(path.GetParentSegments(), MemberPathSegmentEqualityComparer);
}

private IEnumerable<string> GetParentSegments() => Segments.Take(Segments.Length - 1);
Expand All @@ -96,6 +105,11 @@ public bool HasSameParentAs(MemberPath path)
/// </summary>
public bool GetContainsSpecificCollectionIndex() => dottedPath.ContainsSpecificCollectionIndex();

private string[] Segments =>
segments ??= dottedPath
.Replace("[]", "[*]", StringComparison.Ordinal)
.Split(new[] { '.', '[', ']' }, StringSplitOptions.RemoveEmptyEntries);

/// <summary>
/// Returns a copy of the current object as if it represented an un-indexed item in a collection.
/// </summary>
Expand All @@ -104,8 +118,6 @@ public MemberPath WithCollectionAsRoot()
return new MemberPath(reflectedType, declaringType, "[]." + dottedPath);
}

private string[] Segments => segments ??= dottedPath.Split(new[] { '.', '[', ']' }, StringSplitOptions.RemoveEmptyEntries);

/// <summary>
/// Returns the name of the member the current path points to without its parent path.
/// </summary>
Expand Down
54 changes: 54 additions & 0 deletions Src/FluentAssertions/Common/MemberPathSegmentEqualityComparer.cs
@@ -0,0 +1,54 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace FluentAssertions.Common
{
/// <summary>
/// Compares two segments of a <see cref="MemberPath"/>.
/// Sets the <see cref="AnyIndexQualifier"/> equal with any numeric index qualifier.
/// All other comparisons are default string equality.
/// </summary>
internal class MemberPathSegmentEqualityComparer : IEqualityComparer<string>
dennisdoomen marked this conversation as resolved.
Show resolved Hide resolved
jnyrup marked this conversation as resolved.
Show resolved Hide resolved
{
private const string AnyIndexQualifier = "*";
private static readonly Regex IndexQualifierRegex = new(@"^\d+$");

/// <summary>
/// Compares two segments of a <see cref="MemberPath"/>.
/// </summary>
/// <param name="x">Left part of the comparison.</param>
/// <param name="y">Right part of the comparison.</param>
/// <returns>True if segments are equal, false if not.</returns>
public bool Equals(string x, string y)
{
if (x is null || y is null)
{
return x == y;
}
jnyrup marked this conversation as resolved.
Show resolved Hide resolved

if (x == AnyIndexQualifier)
{
return EqualsAnyIndexQualifier(y);
}

if (y == AnyIndexQualifier)
{
return EqualsAnyIndexQualifier(x);
}

return x == y;
}

private static bool EqualsAnyIndexQualifier(string segment)
dennisdoomen marked this conversation as resolved.
Show resolved Hide resolved
=> segment == AnyIndexQualifier || IndexQualifierRegex.IsMatch(segment);

public int GetHashCode(string obj)
{
#if NETCOREAPP2_1_OR_GREATER
return obj.GetHashCode(System.StringComparison.Ordinal);
#else
return obj.GetHashCode();
#endif
}
}
}
11 changes: 10 additions & 1 deletion Src/FluentAssertions/Equivalency/EquivalencyAssertionOptions.cs
Expand Up @@ -2,7 +2,6 @@

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using FluentAssertions.Common;
using FluentAssertions.Equivalency.Execution;
Expand Down Expand Up @@ -40,6 +39,16 @@ public EquivalencyAssertionOptions<TExpectation> Excluding(Expression<Func<TExpe
return this;
}

/// <summary>
/// Selects a collection to define exclusions at.
/// Allows to navigate deeper by using <see cref="For{TNext}"/>.
/// </summary>
public NestedExclusionOptionBuilder<TExpectation, TNext> For<TNext>(Expression<Func<TExpectation, IEnumerable<TNext>>> expression)
{
var selectionRule = new ExcludeMemberByPathSelectionRule(expression.GetMemberPath());
return new NestedExclusionOptionBuilder<TExpectation, TNext>(this, selectionRule);
}

/// <summary>
/// Includes the specified member in the equality check.
/// </summary>
Expand Down
46 changes: 46 additions & 0 deletions Src/FluentAssertions/Equivalency/NestedExclusionOptionBuilder.cs
@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using FluentAssertions.Common;
using FluentAssertions.Equivalency.Selection;

namespace FluentAssertions.Equivalency
{
public class NestedExclusionOptionBuilder<TExpectation, TCurrent> : EquivalencyAssertionOptions<TExpectation>
jnyrup marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// The selected path starting at the first <see cref="EquivalencyAssertionOptions{TExpectation}.For{TNext}"/>.
/// </summary>
private readonly ExcludeMemberByPathSelectionRule currentPathSelectionRule;

internal NestedExclusionOptionBuilder(EquivalencyAssertionOptions<TExpectation> equivalencyAssertionOptions,
ExcludeMemberByPathSelectionRule currentPathSelectionRule)
: base(equivalencyAssertionOptions)
{
this.currentPathSelectionRule = currentPathSelectionRule;
}

/// <summary>
/// Selects a nested property to exclude. This ends the <see cref="For{TNext}"/> chain.
/// </summary>
public EquivalencyAssertionOptions<TExpectation> Exclude(Expression<Func<TCurrent, object>> expression)
{
var nextPath = expression.GetMemberPath();
currentPathSelectionRule.AppendPath(nextPath);
AddSelectionRule(currentPathSelectionRule);
return this;
}

/// <summary>
/// Adds the selected collection to the <see cref="For{TNext}"/> chain.
/// </summary>
public NestedExclusionOptionBuilder<TExpectation, TNext> For<TNext>(
Expression<Func<TCurrent, IEnumerable<TNext>>> expression)
{
var nextPath = expression.GetMemberPath();
currentPathSelectionRule.AppendPath(nextPath);
return new NestedExclusionOptionBuilder<TExpectation, TNext>(this, currentPathSelectionRule);
}
}
}
Expand Up @@ -8,7 +8,7 @@ namespace FluentAssertions.Equivalency.Selection
/// </summary>
internal class ExcludeMemberByPathSelectionRule : SelectMemberByPathSelectionRule
{
private readonly MemberPath memberToExclude;
private MemberPath memberToExclude;

public ExcludeMemberByPathSelectionRule(MemberPath pathToExclude)
: base(pathToExclude.ToString())
Expand All @@ -23,6 +23,12 @@ public ExcludeMemberByPathSelectionRule(MemberPath pathToExclude)
memberToExclude.IsSameAs(new MemberPath(member, parentPath)));
}

public void AppendPath(MemberPath nextPath)
{
memberToExclude = memberToExclude.Join(nextPath, "[]");
SetSelectedPath(memberToExclude.ToString());
}

public override string ToString()
{
return "Exclude member " + memberToExclude;
Expand Down
Expand Up @@ -7,7 +7,7 @@ namespace FluentAssertions.Equivalency.Selection
{
internal abstract class SelectMemberByPathSelectionRule : IMemberSelectionRule
{
private readonly string selectedPath;
private string selectedPath;

protected SelectMemberByPathSelectionRule(string selectedPath)
{
Expand All @@ -16,6 +16,11 @@ protected SelectMemberByPathSelectionRule(string selectedPath)

public virtual bool IncludesMembers => false;

protected void SetSelectedPath(string path)
{
this.selectedPath = path;
}

public IEnumerable<IMember> SelectMembers(INode currentNode, IEnumerable<IMember> selectedMembers,
MemberSelectionContext context)
{
Expand Down
Expand Up @@ -757,6 +757,7 @@ namespace FluentAssertions.Equivalency
public EquivalencyAssertionOptions(FluentAssertions.Equivalency.IEquivalencyAssertionOptions defaults) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<System.Collections.Generic.IEnumerable<TExpectation>> AsCollection() { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Excluding(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expression) { }
public FluentAssertions.Equivalency.NestedExclusionOptionBuilder<TExpectation, TNext> For<TNext>(System.Linq.Expressions.Expression<System.Func<TExpectation, System.Collections.Generic.IEnumerable<TNext>>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Including(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping(string expectationMemberPath, string subjectMemberPath) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping<TSubject>(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expectationMemberPath, System.Linq.Expressions.Expression<System.Func<TSubject, object>> subjectMemberPath) { }
Expand Down Expand Up @@ -921,6 +922,11 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
}
public class NestedExclusionOptionBuilder<TExpectation, TCurrent> : FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>
{
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Exclude(System.Linq.Expressions.Expression<System.Func<TCurrent, object>> expression) { }
public FluentAssertions.Equivalency.NestedExclusionOptionBuilder<TExpectation, TNext> For<TNext>(System.Linq.Expressions.Expression<System.Func<TCurrent, System.Collections.Generic.IEnumerable<TNext>>> expression) { }
}
public class Node : FluentAssertions.Equivalency.INode
{
public Node() { }
Expand Down
Expand Up @@ -769,6 +769,7 @@ namespace FluentAssertions.Equivalency
public EquivalencyAssertionOptions(FluentAssertions.Equivalency.IEquivalencyAssertionOptions defaults) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<System.Collections.Generic.IEnumerable<TExpectation>> AsCollection() { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Excluding(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expression) { }
public FluentAssertions.Equivalency.NestedExclusionOptionBuilder<TExpectation, TNext> For<TNext>(System.Linq.Expressions.Expression<System.Func<TExpectation, System.Collections.Generic.IEnumerable<TNext>>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Including(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping(string expectationMemberPath, string subjectMemberPath) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping<TSubject>(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expectationMemberPath, System.Linq.Expressions.Expression<System.Func<TSubject, object>> subjectMemberPath) { }
Expand Down Expand Up @@ -933,6 +934,11 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
}
public class NestedExclusionOptionBuilder<TExpectation, TCurrent> : FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>
{
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Exclude(System.Linq.Expressions.Expression<System.Func<TCurrent, object>> expression) { }
public FluentAssertions.Equivalency.NestedExclusionOptionBuilder<TExpectation, TNext> For<TNext>(System.Linq.Expressions.Expression<System.Func<TCurrent, System.Collections.Generic.IEnumerable<TNext>>> expression) { }
}
public class Node : FluentAssertions.Equivalency.INode
{
public Node() { }
Expand Down
Expand Up @@ -757,6 +757,7 @@ namespace FluentAssertions.Equivalency
public EquivalencyAssertionOptions(FluentAssertions.Equivalency.IEquivalencyAssertionOptions defaults) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<System.Collections.Generic.IEnumerable<TExpectation>> AsCollection() { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Excluding(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expression) { }
public FluentAssertions.Equivalency.NestedExclusionOptionBuilder<TExpectation, TNext> For<TNext>(System.Linq.Expressions.Expression<System.Func<TExpectation, System.Collections.Generic.IEnumerable<TNext>>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Including(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping(string expectationMemberPath, string subjectMemberPath) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping<TSubject>(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expectationMemberPath, System.Linq.Expressions.Expression<System.Func<TSubject, object>> subjectMemberPath) { }
Expand Down Expand Up @@ -921,6 +922,11 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
}
public class NestedExclusionOptionBuilder<TExpectation, TCurrent> : FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>
{
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Exclude(System.Linq.Expressions.Expression<System.Func<TCurrent, object>> expression) { }
public FluentAssertions.Equivalency.NestedExclusionOptionBuilder<TExpectation, TNext> For<TNext>(System.Linq.Expressions.Expression<System.Func<TCurrent, System.Collections.Generic.IEnumerable<TNext>>> expression) { }
}
public class Node : FluentAssertions.Equivalency.INode
{
public Node() { }
Expand Down
Expand Up @@ -757,6 +757,7 @@ namespace FluentAssertions.Equivalency
public EquivalencyAssertionOptions(FluentAssertions.Equivalency.IEquivalencyAssertionOptions defaults) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<System.Collections.Generic.IEnumerable<TExpectation>> AsCollection() { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Excluding(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expression) { }
public FluentAssertions.Equivalency.NestedExclusionOptionBuilder<TExpectation, TNext> For<TNext>(System.Linq.Expressions.Expression<System.Func<TExpectation, System.Collections.Generic.IEnumerable<TNext>>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Including(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping(string expectationMemberPath, string subjectMemberPath) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping<TSubject>(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expectationMemberPath, System.Linq.Expressions.Expression<System.Func<TSubject, object>> subjectMemberPath) { }
Expand Down Expand Up @@ -921,6 +922,11 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
}
public class NestedExclusionOptionBuilder<TExpectation, TCurrent> : FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>
{
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Exclude(System.Linq.Expressions.Expression<System.Func<TCurrent, object>> expression) { }
public FluentAssertions.Equivalency.NestedExclusionOptionBuilder<TExpectation, TNext> For<TNext>(System.Linq.Expressions.Expression<System.Func<TCurrent, System.Collections.Generic.IEnumerable<TNext>>> expression) { }
}
public class Node : FluentAssertions.Equivalency.INode
{
public Node() { }
Expand Down