Skip to content

Commit f55c872

Browse files
authored
Implemented Include Field Match Option. (#167)
1 parent 9966944 commit f55c872

17 files changed

+557
-4
lines changed

src/Snapshooter/Core/MatchOperators/ExcludeMatchOperator.cs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,29 @@
22

33
namespace Snapshooter.Core;
44

5+
/// <summary>
6+
/// This field match operator can exclude some specific
7+
/// fields from a snapshot. The fields will completely be
8+
/// removed from the snapshot. These excluded fields will
9+
/// not appear in the snapshot then.
10+
/// </summary>
511
public class ExcludeMatchOperator : FieldMatchOperator
612
{
713
private readonly string _fieldsPath;
814

15+
/// <summary>
16+
/// Creates a new instance of the <see cref="ExcludeMatchOperator"/>
17+
/// </summary>
18+
/// <param name="fieldsPath">The path of the field to exclude.</param>
919
public ExcludeMatchOperator(string fieldsPath)
1020
{
1121
_fieldsPath = fieldsPath;
1222
}
1323

24+
/// <inheritdoc/>
1425
public override bool HasFormatAction() => true;
1526

27+
/// <inheritdoc/>
1628
public override void FormatFields(JToken snapshotData)
1729
{
1830
FieldOption fieldOption = new FieldOption(snapshotData);
@@ -23,21 +35,24 @@ public override void FormatFields(JToken snapshotData)
2335
}
2436
}
2537

38+
/// <inheritdoc/>
2639
public override FieldOption ExecuteMatch(
2740
JToken snapshotData,
2841
JToken expectedSnapshotData)
2942
{
3043
return new FieldOption(snapshotData);
3144
}
3245

33-
private JToken FormatField(JToken field)
46+
private static void FormatField(JToken field)
3447
{
35-
if (field.Parent is { } parent)
48+
if (field.Parent is JArray array)
49+
{
50+
array.Remove(field);
51+
}
52+
else if (field.Parent is { } parent)
3653
{
3754
parent.Remove();
3855
}
39-
40-
return field;
4156
}
4257
}
4358

src/Snapshooter/Core/MatchOperators/FieldMatchOperator.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,31 @@
44

55
namespace Snapshooter.Core;
66

7+
/// <summary>
8+
/// Base class of the match operators.
9+
/// </summary>
710
public abstract class FieldMatchOperator
811
{
12+
/// <summary>
13+
/// Defines if the field match operator will format
14+
/// the snapshot.
15+
/// </summary>
16+
/// <returns>True if the snapshot will be formatted, otherwise False</returns>
917
public abstract bool HasFormatAction();
1018

19+
/// <summary>
20+
/// Formats the specified fields of the snapshot.
21+
/// </summary>
22+
/// <param name="snapshotData">The entire snapshot.</param>
1123
public abstract void FormatFields(JToken snapshotData);
1224

25+
/// <summary>
26+
/// Compares the specified match fields of the
27+
/// current snapshot with the original snapshot.
28+
/// </summary>
29+
/// <param name="snapshotData">The current snapshot.</param>
30+
/// <param name="expectedSnapshotData">The original snapshot.</param>
31+
/// <returns></returns>
1332
public abstract FieldOption ExecuteMatch(
1433
JToken snapshotData, JToken expectedSnapshotData);
1534

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using Newtonsoft.Json.Linq;
4+
5+
namespace Snapshooter.Core;
6+
7+
/// <summary>
8+
/// This field match operator can include some specific
9+
/// fields of a snapshot. Only the specified fields will
10+
/// be included within the snapshot. All other fields will
11+
/// be removed/excluded from the snapshot.
12+
/// </summary>
13+
public class IncludeMatchOperator : FieldMatchOperator
14+
{
15+
private readonly List<string> _fieldsPaths;
16+
17+
/// <summary>
18+
/// Creates a new instance of the <see cref="IncludeMatchOperator"/>
19+
/// </summary>
20+
public IncludeMatchOperator()
21+
{
22+
_fieldsPaths = new List<string>();
23+
}
24+
25+
/// <inheritdoc/>
26+
public override bool HasFormatAction() => true;
27+
28+
/// <summary>
29+
/// Adds a path of a field to include.
30+
/// </summary>
31+
/// <param name="fieldPath">The path of a field to include.</param>
32+
public void AddFieldPath(string fieldPath)
33+
{
34+
if(!_fieldsPaths.Contains(fieldPath))
35+
{
36+
_fieldsPaths.Add(fieldPath);
37+
}
38+
}
39+
40+
/// <inheritdoc/>
41+
public override void FormatFields(JToken snapshotData)
42+
{
43+
FieldOption fieldOption = new FieldOption(snapshotData);
44+
45+
List<JToken> includedFields = new List<JToken>();
46+
47+
foreach(string fieldPath in _fieldsPaths)
48+
{
49+
List<JToken> fieldsToInclude = fieldOption
50+
.FindFieldTokens(fieldPath)
51+
.ToList();
52+
53+
includedFields.AddRange(fieldsToInclude);
54+
}
55+
56+
FilterIncludedFieldsOnly(snapshotData, includedFields);
57+
}
58+
59+
/// <inheritdoc/>
60+
public override FieldOption ExecuteMatch(
61+
JToken snapshotData,
62+
JToken expectedSnapshotData)
63+
{
64+
return new FieldOption(snapshotData);
65+
}
66+
67+
private static void FilterIncludedFieldsOnly(
68+
JToken snapshotField,
69+
List<JToken> includedFields)
70+
{
71+
foreach (JToken child in snapshotField.Children().ToList())
72+
{
73+
if (child.HasValues)
74+
{
75+
if (!includedFields.Any(field =>
76+
child.Path.StartsWith(field.Path) ||
77+
field.Path.StartsWith(child.Path)))
78+
{
79+
RemoveField(child);
80+
81+
continue;
82+
}
83+
84+
FilterIncludedFieldsOnly(child, includedFields);
85+
}
86+
}
87+
}
88+
89+
private static void RemoveField(JToken child)
90+
{
91+
if (child is JProperty property)
92+
{
93+
property.Remove();
94+
}
95+
else if (child.Parent is JArray array)
96+
{
97+
array.Remove(child);
98+
}
99+
else if (child is JValue value)
100+
{
101+
value.Parent?.Remove();
102+
}
103+
}
104+
}
105+

src/Snapshooter/MatchOptions.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
34
using Snapshooter.Core;
45
using Snapshooter.Exceptions;
56

@@ -474,6 +475,30 @@ public MatchOptions ExcludeField(string fieldPath)
474475
return this;
475476
}
476477

478+
/// <summary>
479+
/// The <see cref="IncludeField(string)"/> option includes only the field(s) in a snapshot,
480+
/// which have been defined by the fieldPath.
481+
/// The field(s) to include is given by the json path <paramref name="fieldPath"/>.
482+
/// Only these fields will appear in the snapshot.
483+
/// </summary>
484+
/// <param name="fieldPath">The json path to the field(s) to include.</param>
485+
public MatchOptions IncludeField(string fieldPath)
486+
{
487+
FieldMatchOperator fieldMatchOperator =
488+
_matchOperators.SingleOrDefault(op => op is IncludeMatchOperator);
489+
490+
if (fieldMatchOperator == null)
491+
{
492+
fieldMatchOperator = new IncludeMatchOperator();
493+
_matchOperators.Add(fieldMatchOperator);
494+
}
495+
496+
var includeMatchOperator = (IncludeMatchOperator)fieldMatchOperator;
497+
498+
includeMatchOperator.AddFieldPath(fieldPath);
499+
500+
return this;
501+
}
477502

478503
private MatchOptions AddIgnoreMatchOperator<T>(string fieldsPath)
479504
{

test/Snapshooter.Xunit.Tests/MatchOptions/ExcludeField/ExcludeFieldTests.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.IO;
3+
using System.Net;
34
using FluentAssertions;
45
using Snapshooter.Tests.Data;
56
using Snapshooter.Xunit.Tests.Helpers;
@@ -159,4 +160,39 @@ public void ExcludeField_ExcludeAllFieldsModified_Mismatch()
159160
act.Should().Throw<EqualException>()
160161
.Which.Message.Should().Contain("CountryCode");
161162
}
163+
164+
[Fact]
165+
public void ExcludeField_DuplicateExcludeFieldsSnapshot_SuccessfullyCompared()
166+
{
167+
// arrange
168+
TestPerson testPerson = TestDataBuilder
169+
.TestPersonMarkWalton()
170+
.Build();
171+
172+
// act & assert
173+
Snapshot.Match(testPerson, options => options
174+
.ExcludeField("Firstname")
175+
.ExcludeField("DateOfBirth")
176+
.ExcludeField("Size")
177+
.ExcludeField("Address.Country.CountryCode")
178+
.ExcludeField("Children")
179+
.ExcludeField("Relatives[*].Relatives")
180+
.ExcludeField("Relatives[*].Address")
181+
.ExcludeField("Relatives[*].Address")
182+
.ExcludeField("**.Size")
183+
.ExcludeField("**.CountryCode"));
184+
}
185+
186+
[Fact]
187+
public void ExcludeField_ExcludeArrayFieldSnapshot_SuccessfullyCompared()
188+
{
189+
// arrange
190+
TestPerson testPerson = TestDataBuilder
191+
.TestPersonMarkWalton()
192+
.Build();
193+
194+
// act & assert
195+
Snapshot.Match(testPerson, options => options
196+
.ExcludeField("Children[0]"));
197+
}
162198
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"Id": "c78c698f-9ee5-4b4b-9a0e-ef729b1f8ec8",
3+
"Lastname": "Walton",
4+
"CreationDate": "2018-06-06T00:00:00",
5+
"Age": 30,
6+
"Address": {
7+
"Street": "Rohrstrasse",
8+
"StreetNumber": 12,
9+
"Plz": 8304,
10+
"City": "Wallislellen",
11+
"Country": {
12+
"Name": "Switzerland"
13+
}
14+
},
15+
"Relatives": [
16+
{
17+
"Id": "fcf04ca6-d8f2-4214-a3ff-d0ded5bad4de",
18+
"Firstname": "Sandra",
19+
"Lastname": "Schneider",
20+
"CreationDate": "2019-04-01T00:00:00",
21+
"DateOfBirth": "1996-02-14T00:00:00",
22+
"Age": null,
23+
"Children": []
24+
}
25+
]
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"Id": "c78c698f-9ee5-4b4b-9a0e-ef729b1f8ec8",
3+
"Firstname": "Mark",
4+
"Lastname": "Walton",
5+
"CreationDate": "2018-06-06T00:00:00",
6+
"DateOfBirth": "2000-06-25T00:00:00",
7+
"Age": 30,
8+
"Size": 182.5214,
9+
"Address": {
10+
"Street": "Rohrstrasse",
11+
"StreetNumber": 12,
12+
"Plz": 8304,
13+
"City": "Wallislellen",
14+
"Country": {
15+
"Name": "Switzerland",
16+
"CountryCode": "CH"
17+
}
18+
},
19+
"Children": [
20+
{
21+
"Name": null,
22+
"DateOfBirth": "2015-02-12T00:00:00"
23+
},
24+
{
25+
"Name": "Hanna",
26+
"DateOfBirth": "2012-03-20T00:00:00"
27+
}
28+
],
29+
"Relatives": [
30+
{
31+
"Id": "fcf04ca6-d8f2-4214-a3ff-d0ded5bad4de",
32+
"Firstname": "Sandra",
33+
"Lastname": "Schneider",
34+
"CreationDate": "2019-04-01T00:00:00",
35+
"DateOfBirth": "1996-02-14T00:00:00",
36+
"Age": null,
37+
"Size": 165.23,
38+
"Address": {
39+
"Street": "Bahnhofstrasse",
40+
"StreetNumber": 450,
41+
"Plz": 8000,
42+
"City": "Zurich",
43+
"Country": {
44+
"Name": "Switzerland",
45+
"CountryCode": "CH"
46+
}
47+
},
48+
"Children": [],
49+
"Relatives": null
50+
}
51+
]
52+
}

0 commit comments

Comments
 (0)