Skip to content

Commit

Permalink
feat: Optional keep + Collection ID matching strategy (#7)
Browse files Browse the repository at this point in the history
* feat: KeepInDiff optional property
* feat: DiffCollectionId for key-based diff strategy of collections
  • Loading branch information
maranmaran authored Jun 29, 2023
1 parent f221686 commit b35d58d
Show file tree
Hide file tree
Showing 8 changed files with 252 additions and 14 deletions.
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ Output
]
```

Keep in diff has optional property `IgnoreIfNoSiblingOrChildDiff` which will actually ignore keep attribute if there's no sibling or child diff present rendering it unusable or not desired as it's extra information without some other context.

## IgnoreInDiff attribute

Ignores root and child values
Expand Down Expand Up @@ -129,6 +131,47 @@ Output
]
```

## DiffCollectionId attribute

Switches default index-based diffing to key-value diff.
Nested types and values in the collection return their keys which is used to detect changes.

Say we have 3 items in an array and we remove first item:

With index-based matching

| left | right |
| ------- | ------- |
| 1, car | 2, bike |
| 2, bike | 3, road |
| 3, road | null |

Diff will be:
car -> bike
road -> null

Because due to removal, items moved in array and indexes changed.

With key-based matching by defining DiffCollectionId of underlying object:

| left | right |
| ------- | ------- |
| 1, car | null |
| 2, bike | 2, bike |
| 3, road | 3, road |

Diff will be:
car -> null

```cs
class Car([property:DiffPropertyName("Make")]string Model);

Car car1 = new Car("Toyota");
Car car2 = new Car("Ford");

IEnumerable<Difference> carDiff = DifferDotNet.Diff(car1, car2);
```

# Full demo

Combine and enjoy:
Expand Down
2 changes: 1 addition & 1 deletion source/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<Version>2.0.4-optional-keep</Version>
<Version>2.1.0</Version>
<Authors>Marko Urh</Authors>
<Company>Perun</Company>
<Copyright>Copyright (c) 2023 Marko Urh and other authors.</Copyright>
Expand Down
66 changes: 65 additions & 1 deletion source/Perun.Differ.Tests/DifferTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,45 @@ public void SimpleIterable_Diffs()
);
}

[Fact]
public void ComplexIterable_ListIdDefined_DeleteFirstItem_DeleteDiffRatherThanUpdateDiff()
{
var faker = new AutoFaker<ComplexIterableWithId>();

var left = faker.Generate();
var right = new ComplexIterableWithId
{
Iterable = left.Iterable.Select(x => new ComplexWithId(x.Id, x.Value)).ToList()
};

right.Iterable.RemoveAt(0);

var diff = DifferDotNet.Diff(left, right).Single();

Assert.Null(diff.RightValue);
}

[Fact]
public void ComplexNestedIterable_ListIdDefined_DeleteFirstItem_DeleteDiffRatherThanUpdateDiff()
{
var faker = new AutoFaker<ComplexNestedIterableWithId>();

var left = faker.Generate();
var right = new ComplexNestedIterableWithId
{
Iterable = left.Iterable.Select(x => x.Select(y => new ComplexWithId(y.Id, y.Value)).ToList()).ToList()
};

right.Iterable.First().RemoveAt(0);
right.Iterable.First().Add(new ComplexWithId("Added", "New Value"));

var diff = DifferDotNet.Diff(left, right).ToArray();

Assert.Equivalent(2, diff.Length);
Assert.Null(diff[0].RightValue);
Assert.Null(diff[1].LeftValue);
}

[Fact]
public void ComplexIterable_Diffs()
{
Expand Down Expand Up @@ -483,13 +522,38 @@ public void OptionalKeepDiff_Complex_ChildChange_DoesNotIgnore()
var left = faker.UseSeed(1).Generate();
var right = faker.UseSeed(2).Generate();

right.NoDiff = left.NoDiff;
right.Sibling = left.Sibling;

var diff = DifferDotNet.Diff(left, right);

Assert.NotNull(diff.SingleOrDefault());
}

[Fact]
public void OptionalKeepDiff_IterableSimple_ChildChange_DoesNotIgnore()
{
var fakerComplex = new AutoFaker<ComplexOptionalKeepModel>();
var complexSet = fakerComplex.Generate(11);

var left = new IterableComplexOptionalKeepModel { Iterable = complexSet.Select(x => new ComplexOptionalKeepModel(x)).ToList() };
var right = new IterableComplexOptionalKeepModel { Iterable = complexSet.Select(x => new ComplexOptionalKeepModel(x)).ToList() };

// Update sibling
right.Iterable[1] = new ComplexOptionalKeepModel
{
KeepMeOnlyIfSiblingIsDiffed = right.Iterable[1].KeepMeOnlyIfSiblingIsDiffed,
Sibling = "Changed Sibling - keeps KeepMeOnlyIfSiblingIsDiffed"
};

// This is path x.1.string
// Optional keep must not return path.10.string
// Since we use trie and match "startWith" 1 != 10

var diff = DifferDotNet.Diff(left, right);

Assert.Equal(2, diff.Count());
}

[Fact]
public void KeepDiff_Simple_Keeps()
{
Expand Down
26 changes: 26 additions & 0 deletions source/Perun.Differ.Tests/TestTypes/IterableModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,30 @@ public class NestedComplexIterableTypes
public ICollection<ICollection<ComplexType>> CollectionGeneric { get; set; }
public IDictionary<ComplexType, IDictionary<ComplexType, ComplexType>> DictionaryGeneric { get; set; }
}

public class ComplexIterableWithId
{
[DiffCollectionId("Id")]
public List<ComplexWithId> Iterable { get; set; }
}

public class ComplexNestedIterableWithId
{
[DiffCollectionId("Id")]
public List<List<ComplexWithId>> Iterable { get; set; }
}

public record ComplexWithId
{
public ComplexWithId(string id, string value)
{
Id = id;
Value = value;
}

[IgnoreInDiff]
public string Id { get; set; }

public string Value { get; set; }
};
}
23 changes: 19 additions & 4 deletions source/Perun.Differ.Tests/TestTypes/KeepAttributeModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,32 @@ public class ComplexKeepModel

public class SimpleOptionalKeepModel
{
[KeepInDiff(IgnoreIfNoOtherDiff = true)]
[KeepInDiff(IgnoreIfNoSiblingOrChildDiff = true)]
public string NoDiffKeepMe { get; set; }

public string NoDiff { get; set; }
}

public class ComplexOptionalKeepModel
{
[KeepInDiff(IgnoreIfNoOtherDiff = true)]
public ComplexType NoDiffKeepMe { get; set; }
public ComplexOptionalKeepModel()
{
}

public ComplexType NoDiff { get; set; }
public ComplexOptionalKeepModel(ComplexOptionalKeepModel a)
{
KeepMeOnlyIfSiblingIsDiffed = a.KeepMeOnlyIfSiblingIsDiffed;
Sibling = a.Sibling;
}

[KeepInDiff(IgnoreIfNoSiblingOrChildDiff = true)]
public string KeepMeOnlyIfSiblingIsDiffed { get; set; }

public string Sibling { get; set; }
}

public class IterableComplexOptionalKeepModel
{
public List<ComplexOptionalKeepModel> Iterable { get; set; }
}
}
19 changes: 17 additions & 2 deletions source/Perun.Differ/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public sealed class KeepInDiffAttribute : Attribute
/// If <c>true</c> attribute is ignored (not kept), if no sibling or child diffs exist.
/// If <c>false</c> values are always kept.
/// </value>
public bool IgnoreIfNoOtherDiff { get; set; }
public bool IgnoreIfNoSiblingOrChildDiff { get; set; }
}

/// <summary>
Expand All @@ -30,6 +30,21 @@ public sealed class IgnoreInDiffAttribute : Attribute
{
}

/// <summary>
/// Switches array diffing from index based to key-value based
/// </summary>
[PublicAPI]
[AttributeUsage(AttributeTargets.Property)]
public sealed class DiffCollectionId : Attribute
{
public string Name { get; }

public DiffCollectionId(string name)
{
Name = name;
}
}

/// <summary>
/// Sets custom property name in diff.
/// <see cref="Difference.CustomFieldName"/>
Expand All @@ -52,7 +67,7 @@ internal enum DiffActions
Default = 0,
Keep = 1,
Ignore = 2,
KeepOptional = 4,
KeepOptional = 4
}

internal sealed class DiffCollection
Expand Down
Loading

0 comments on commit b35d58d

Please sign in to comment.