Skip to content

Commit

Permalink
feat: custom prop names via prop values (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
maranmaran authored Jul 14, 2023
1 parent 29dd0e4 commit 1cf3b9f
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 10 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,37 @@ Output
]
```

By turning on flag `FromPropertyValue` differ will attempt to retireve value via reflection treating given name as PATH.

```cs
class Car([property:DiffPropertyName("Company", true)]string Model, string Company);

Car car1 = new Car("Supra", "Toyota");
Car car2 = new Car("Corolla", "Toyota");

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

Output

```
[
{
"fullPath": "model",
"fieldPath": "",
"fieldName": "model",
"leftValue": "Toyota",
"rightValue": "Corolla",
"customFullPath": "Toyota", <-- taken from Company prop
"customFieldPath": "",
"customFieldName": "Toyota" <-- taken from Company prop
}
]
```

This can also be nested path like: `Company.Name`
Iterables are not supported. Default return value is `Name` value of the `DiffPropertyName` attribute

## DiffCollectionId attribute

Switches default index-based diffing to key-value diff.
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.1.1</Version>
<Version>2.1.2</Version>
<Authors>Marko Urh</Authors>
<Company>Perun</Company>
<Copyright>Copyright (c) 2023 Marko Urh and other authors.</Copyright>
Expand Down
49 changes: 49 additions & 0 deletions source/Perun.Differ.Tests/DifferTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,19 @@ public void IgnoreParentKeepChild_KeepsChildIgnoresOther()
Assert.True(diffs.Aggregate(true, (acc, cur) => acc &= cur.FieldName.EndsWith("keepMe")));
}

[Fact]
public void CustomNameDefined_Reflection_RegisteredInDiff()
{
var faker = new AutoFaker<CustomNameReflectionModel>();
var left = faker.UseSeed(1).Generate();
var right = faker.UseSeed(2).Generate();

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

Assert.Equal(left.B, diff.CustomFieldName);
Assert.Equal("a", diff.FieldName);
}

[Fact]
public void CustomNameDefined_RegisteredInDiff()
{
Expand Down Expand Up @@ -708,5 +721,41 @@ public void NestedCustomNameDefined_RegisteredInDiff()
Assert.Equal("b", diff.FieldPath);
Assert.Equal("a", diff.FieldName);
}

[Fact]
public void NestedCustomNameDefined_Reflection_RegisteredInDiff()
{
var faker = new AutoFaker<CustomNameNestedReflectionModel>();
var left = faker.UseSeed(1).Generate();
var right = faker.UseSeed(2).Generate();

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

Assert.Equal("MyCustomName.ACustomName", diff.CustomFullPath);
Assert.Equal("MyCustomName", diff.CustomFieldPath);
Assert.Equal("ACustomName", diff.CustomFieldName);

Assert.Equal("b.a", diff.FullPath);
Assert.Equal("b", diff.FieldPath);
Assert.Equal("a", diff.FieldName);
}

[Fact]
public void DeepNestedCustomNameDefined_Reflection_RegisteredInDiff()
{
var faker = new AutoFaker<CustomNameDeepNestedReflectionModel>();
var left = faker.UseSeed(1).Generate();
var right = faker.UseSeed(2).Generate();

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

Assert.Equal("MyCustomName.MyCustomName.ACustomName", diff.CustomFullPath);
Assert.Equal("MyCustomName.MyCustomName", diff.CustomFieldPath);
Assert.Equal("ACustomName", diff.CustomFieldName);

Assert.Equal("b.b.a", diff.FullPath);
Assert.Equal("b.b", diff.FieldPath);
Assert.Equal("a", diff.FieldName);
}
}
}
22 changes: 22 additions & 0 deletions source/Perun.Differ.Tests/TestTypes/CustomNameModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,33 @@ internal class CustomNameModel
{
[DiffPropertyName("ACustomName")]
public string A { get; set; }

public string B => "MyCustomName";
}

internal class CustomNameReflectionModel
{
[DiffPropertyName("B", fromPropertyValue: true)]
public string A { get; set; }

public string B => "MyCustomName";
}

internal class CustomNameNestedModel
{
[DiffPropertyName("BCustomName")]
public CustomNameModel B { get; set; }
}

internal class CustomNameNestedReflectionModel
{
[DiffPropertyName("B.B", fromPropertyValue: true)]
public CustomNameModel B { get; set; }
}

internal class CustomNameDeepNestedReflectionModel
{
[DiffPropertyName("B.B.B", fromPropertyValue: true)]
public CustomNameNestedReflectionModel B { get; set; }
}
}
27 changes: 23 additions & 4 deletions source/Perun.Differ/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,30 @@ public DiffCollectionId(string name)
[AttributeUsage(AttributeTargets.Property)]
public sealed class DiffPropertyName : Attribute
{
public string Name { get; }

public DiffPropertyName(string name)
public string Name { get; }

/// <summary>
/// Attempts to retrieve value from property via reflection
/// </summary>
/// <example>
/// Name: B.B.B
/// Takes value from B.B.B nested path
/// </example>
public bool FromPropertyValue { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="DiffPropertyName"/> class.
/// </summary>
/// <param name="name">
/// Custom name
/// </param>
/// <param name="fromPropertyValue">
/// Tries to fetch value via reflection, treating Name as path
/// </param>
public DiffPropertyName(string name, bool fromPropertyValue = false)
{
Name = name;
Name = name;
FromPropertyValue = fromPropertyValue;
}
}

Expand Down
42 changes: 40 additions & 2 deletions source/Perun.Differ/DifferDotNet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace Differ.DotNet
{
[PublicAPI]
[PublicAPI]
public sealed class DifferDotNet
{
/// <summary>
Expand Down Expand Up @@ -80,7 +80,7 @@ DiffActions actions
foreach (var prop in type.GetProperties())
{
var name = prop.Name.ToCamelCase();
var customName = (prop.GetCustomAttribute<DiffPropertyName>()?.Name ?? name);
var customName = GetCustomPropertyName<T>(type, rightObj, leftObj, prop) ?? name;

var fullPath = path + name;
var customFullPath = customPath + customName;
Expand Down Expand Up @@ -121,6 +121,44 @@ DiffActions actions

return true;
}

private static string GetCustomPropertyName<T>(Type type, T left, T right, PropertyInfo prop)
{
var attr = prop?.GetCustomAttribute<DiffPropertyName>();
if (attr is null)
{
return null;
}

var segments = attr.Name.Split('.');
var nestedProp = prop;
var nestedType = type;
object nestedLeft = left;
object nestedRight = right;

for (var i = 0; i < segments.Length; i++)
{
nestedProp = nestedType?.GetProperty(segments[i]);
if (nestedProp is null)
{
return attr.Name;
}

nestedType = nestedProp.PropertyType;
if (nestedType.IsIterable())
{
return attr.Name;
}

if (i < segments.Length - 1)
{
nestedLeft = nestedProp.GetValue(nestedLeft);
nestedRight = nestedProp.GetValue(nestedRight);
}
}

return (nestedProp.GetValue(nestedRight) ?? nestedProp.GetValue(nestedLeft))?.ToString();
}

private static bool HandleIterable<T>(
string path,
Expand Down
6 changes: 3 additions & 3 deletions source/Perun.Differ/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ public static object[] ToArray(this IEnumerator enumerator)

internal static bool IsIterable(this Type type)
{
if (type.IsArray || type == typeof(IEnumerable) || type.GetInterfaces().Contains(typeof(IEnumerable)))
if (type == typeof(string))
{
return true;
return false;
}

return false;
return type.IsArray || type == typeof(IEnumerable) || type.GetInterfaces().Contains(typeof(IEnumerable));
}

internal static Type GetIterableType(this Type type)
Expand Down

0 comments on commit 1cf3b9f

Please sign in to comment.