Skip to content

Commit fd4710a

Browse files
committed
Add CustomDiffer functions
1 parent 30f0e27 commit fd4710a

File tree

4 files changed

+216
-26
lines changed

4 files changed

+216
-26
lines changed

src/Diffract/CustomDiffer.fs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
namespace DEdge.Diffract
2+
3+
open System
4+
open DEdge.Diffract
5+
6+
[<Sealed; AbstractClass>]
7+
type CustomDiffer<'T> =
8+
9+
/// <summary>
10+
/// Create a custom differ for a specific type.
11+
/// </summary>
12+
/// <param name="buildDiffFunction">Builds the diff function for this type.</param>
13+
static member Build(buildDiffFunction: Func<IDifferFactory, Func<'T, 'T, Diff option>>) =
14+
{ new ICustomDiffer with
15+
member this.GetCustomDiffer<'U>(differFactory, shape) =
16+
if shape.Type = typeof<'T> then
17+
let diffFunction = buildDiffFunction.Invoke(differFactory)
18+
{ new IDiffer<'T> with
19+
member _.Diff(x1, x2) = diffFunction.Invoke(x1, x2) }
20+
|> unbox<IDiffer<'U>>
21+
|> Some
22+
else
23+
None }
24+
25+
/// <summary>
26+
/// Create a custom differ for a specific type.
27+
/// </summary>
28+
/// <param name="buildDiffFunction">Builds the diff function for this type.</param>
29+
static member Build(buildDiffFunction: IDifferFactory -> 'T -> 'T -> Diff option) =
30+
{ new ICustomDiffer with
31+
member this.GetCustomDiffer<'U>(differFactory, shape) =
32+
if shape.Type = typeof<'T> then
33+
let diffFunction = buildDiffFunction differFactory
34+
{ new IDiffer<'T> with
35+
member _.Diff(x1, x2) = diffFunction x1 x2 }
36+
|> unbox<IDiffer<'U>>
37+
|> Some
38+
else
39+
None }
40+
41+
/// <summary>
42+
/// Create a custom differ for a specific type.
43+
/// </summary>
44+
/// <param name="diffFunction">The diff function for this type.</param>
45+
static member Build(diffFunction: Func<'T, 'T, Diff option>) =
46+
CustomDiffer.Build(fun _ -> diffFunction)
47+
48+
/// <summary>
49+
/// Create a custom differ for a specific type by mapping it to a diffable type.
50+
/// </summary>
51+
/// <param name="mapFunction">The mapping function.</param>
52+
/// <typeparam name="T">The type for which a custom differ is being created.</typeparam>
53+
/// <typeparam name="U">The type used to actually perform the diff.</typeparam>
54+
static member Map<'U>(mapFunction: Func<'T, 'U>) =
55+
CustomDiffer<'T>.Build(fun differFactory ->
56+
let differ = differFactory.GetDiffer<'U>()
57+
fun x1 x2 -> differ.Diff(mapFunction.Invoke(x1), mapFunction.Invoke(x2)))
58+
59+
[<Sealed; AbstractClass>]
60+
type CustomDiffer =
61+
62+
/// <summary>
63+
/// Create a custom differ for a specific type.
64+
/// </summary>
65+
/// <param name="buildDiffFunction">Builds the diff function for this type.</param>
66+
static member Build(buildDiffFunction: Func<IDifferFactory, Func<'T, 'T, Diff option>>) =
67+
CustomDiffer<'T>.Build(buildDiffFunction)
68+
69+
/// <summary>
70+
/// Create a custom differ for a specific type.
71+
/// </summary>
72+
/// <param name="buildDiffFunction">Builds the diff function for this type.</param>
73+
static member Build(buildDiffFunction: IDifferFactory -> 'T -> 'T -> Diff option) =
74+
CustomDiffer<'T>.Build(buildDiffFunction)
75+
76+
/// <summary>
77+
/// Create a custom differ for a specific type.
78+
/// </summary>
79+
/// <param name="diffFunction">The diff function for this type.</param>
80+
static member Build(diffFunction: Func<'T, 'T, Diff option>) =
81+
CustomDiffer<'T>.Build(diffFunction)
82+
83+
/// <summary>
84+
/// Create a custom differ for a leaf type using default comparison and a custom display format.
85+
/// </summary>
86+
/// <param name="format">The display format.</param>
87+
static member Leaf<'T when 'T : equality> (format: Func<'T, string>) =
88+
CustomDiffer.Build<'T>(fun x y ->
89+
if x = y then
90+
None
91+
else
92+
Diff.Value(format.Invoke(x), format.Invoke(y))
93+
|> Some)
94+
95+
/// <summary>
96+
/// Combine multiple custom differs.
97+
/// </summary>
98+
/// <param name="differs">The custom differs.</param>
99+
static member Combine (differs: seq<ICustomDiffer>) =
100+
CombinedCustomDiffer(differs) :> ICustomDiffer
101+
102+
/// <summary>
103+
/// Combine multiple custom differs.
104+
/// </summary>
105+
/// <param name="differs">The custom differs.</param>
106+
static member Combine ([<ParamArray>] differs: ICustomDiffer[]) =
107+
CustomDiffer.Combine(differs :> seq<_>)

src/Diffract/Diffract.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
</PropertyGroup>
1515
<ItemGroup>
1616
<Compile Include="Types.fs" />
17+
<Compile Include="CustomDiffer.fs" />
1718
<Compile Include="ReadOnlyDictionaryShape.fs" />
1819
<Compile Include="DictionaryShape.fs" />
1920
<Compile Include="Differ.fs" />

tests/Diffract.CSharp.Tests/CustomDiffers.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,36 @@ public void CustomDiffer()
2626
Assert.Equal(expectedDiff, actualDiff);
2727
}
2828

29+
[Fact]
30+
public void CustomDifferWithCombinators()
31+
{
32+
var expectedDiff = "D Expect = \"a\"\n Actual = \"b\"\n";
33+
var expected = new Container(new CustomDiffable("a"));
34+
var actual = new Container(new CustomDiffable("b"));
35+
var actualDiff = MyDifferWithCombinators.Get<Container>().ToString(expected, actual);
36+
Assert.Equal(expectedDiff, actualDiff);
37+
}
38+
39+
[Fact]
40+
public void CustomDifferWithMap()
41+
{
42+
var expectedDiff = "D Expect = \"a\"\n Actual = \"b\"\n";
43+
var expected = new Container(new CustomDiffable("a"));
44+
var actual = new Container(new CustomDiffable("b"));
45+
var actualDiff = MyDifferWithMap.Get<Container>().ToString(expected, actual);
46+
Assert.Equal(expectedDiff, actualDiff);
47+
}
48+
49+
[Fact]
50+
public void CustomDifferWithMapToAnonymousObject()
51+
{
52+
var expectedDiff = "D.v Expect = \"a\"\n Actual = \"b\"\n";
53+
var expected = new Container(new CustomDiffable("a"));
54+
var actual = new Container(new CustomDiffable("b"));
55+
var actualDiff = MyDifferWithMapToAnonymousObject.Get<Container>().ToString(expected, actual);
56+
Assert.Equal(expectedDiff, actualDiff);
57+
}
58+
2959
public record CustomDiffable(string X);
3060

3161
public record Container(CustomDiffable D);
@@ -57,5 +87,48 @@ public FSharpOption<IDiffer<T>> GetCustomDiffer<T>(IDifferFactory differFactory,
5787
: null;
5888
}
5989
}
90+
91+
public static class MyDifferWithCombinators
92+
{
93+
public static IDiffer<T> Get<T>() => Singleton<T>.Instance;
94+
95+
private static class Singleton<T>
96+
{
97+
public static readonly IDiffer<T> Instance = CustomDiffer.GetDiffer<T>();
98+
}
99+
100+
private static readonly ICustomDiffer CustomDiffer =
101+
CustomDiffer<CustomDiffable>.Build(factory =>
102+
{
103+
var stringDiffer = factory.GetDiffer<string>();
104+
return (x1, x2) => stringDiffer.Diff(x1.X, x2.X);
105+
});
106+
}
107+
108+
public static class MyDifferWithMap
109+
{
110+
public static IDiffer<T> Get<T>() => Singleton<T>.Instance;
111+
112+
private static class Singleton<T>
113+
{
114+
public static readonly IDiffer<T> Instance = CustomDiffer.GetDiffer<T>();
115+
}
116+
117+
private static readonly ICustomDiffer CustomDiffer =
118+
CustomDiffer<CustomDiffable>.Map(x => x.X);
119+
}
120+
121+
public static class MyDifferWithMapToAnonymousObject
122+
{
123+
public static IDiffer<T> Get<T>() => Singleton<T>.Instance;
124+
125+
private static class Singleton<T>
126+
{
127+
public static readonly IDiffer<T> Instance = CustomDiffer.GetDiffer<T>();
128+
}
129+
130+
private static readonly ICustomDiffer CustomDiffer =
131+
CustomDiffer<CustomDiffable>.Map(x => new { v = x.X });
132+
}
60133
}
61134
}

tests/Diffract.Tests/Tests.fs

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ module MyDiffModule =
8888

8989
let differ<'T> = CustomDiffer().GetDiffer<'T>()
9090

91+
module MyDiffWithCombinators =
92+
93+
let customDiffer = CustomDiffer<CustomDiffable>.Build(fun factory ->
94+
let stringDiffer = factory.GetDiffer<string>()
95+
fun x1 x2 -> stringDiffer.Diff(x1.x, x2.x))
96+
97+
let differ<'T> = customDiffer.GetDiffer<'T>()
98+
9199
type MyDiffer(differFactory: IDifferFactory) =
92100
let stringDiffer = differFactory.GetDiffer<string>()
93101

@@ -105,61 +113,62 @@ type MyCustomDiffer() =
105113
type MyDiffType<'T>() =
106114
static member val Differ = MyCustomDiffer().GetDiffer<'T>()
107115

116+
type MyDiffTypeWithCombinators<'T>() =
117+
static let customDiffer = CustomDiffer<CustomDiffable>.Build(fun factory ->
118+
let stringDiffer = factory.GetDiffer<string>()
119+
fun x1 x2 -> stringDiffer.Diff(x1.x, x2.x))
120+
121+
static member val Differ = customDiffer.GetDiffer<'T>()
122+
108123
[<Fact>]
109124
let ``Custom differ`` () =
110125
Assert.Equal("x Expect = \"a\"\n Actual = \"b\"\n",
111126
Differ.ToString({ x = "a" }, { x = "b" }))
112127
Assert.Equal("Expect = \"a\"\nActual = \"b\"\n",
113128
Differ.ToString({ x = "a" }, { x = "b" }, MyDiffModule.differ))
129+
Assert.Equal("Expect = \"a\"\nActual = \"b\"\n",
130+
Differ.ToString({ x = "a" }, { x = "b" }, MyDiffWithCombinators.differ))
114131
Assert.Equal("Expect = \"a\"\nActual = \"b\"\n",
115132
Differ.ToString({ x = "a" }, { x = "b" }, MyDiffType.Differ))
133+
Assert.Equal("Expect = \"a\"\nActual = \"b\"\n",
134+
Differ.ToString({ x = "a" }, { x = "b" }, MyDiffTypeWithCombinators.Differ))
116135

117136
module ``Custom differ with custom diff output`` =
118137

119-
type MyCustomDiffer() =
120-
interface ICustomDiffer with
121-
member this.GetCustomDiffer<'T>(_, shape) =
122-
if shape.Type = typeof<CustomDiffable> then
123-
{ new IDiffer<CustomDiffable> with
124-
member _.Diff(x1, x2) =
125-
if x1.x = x2.x then
126-
None
127-
else
128-
Diff.MakeCustom(fun writer param indent path recur ->
129-
if param.ensureFirstLineIsAligned then writer.WriteLine()
130-
let indentLike str = String.replicate (String.length str) " "
131-
let dpath = if path = "" then "" else path + " "
132-
writer.WriteLine($"{indent}{dpath}{param.x1Name} __is__ {x1.x}")
133-
writer.WriteLine($"{indent}{indentLike dpath}{param.x2Name} __is__ {x2.x}"))
134-
|> Some }
135-
|> unbox<IDiffer<'T>>
136-
|> Some
137-
else
138-
None
138+
let myCustomDiffer = CustomDiffer<CustomDiffable>.Build(fun x1 x2 ->
139+
if x1.x = x2.x then
140+
None
141+
else
142+
Diff.MakeCustom(fun writer param indent path recur ->
143+
if param.ensureFirstLineIsAligned then writer.WriteLine()
144+
let indentLike str = String.replicate (String.length str) " "
145+
let dpath = if path = "" then "" else path + " "
146+
writer.WriteLine($"{indent}{dpath}{param.x1Name} __is__ {x1.x}")
147+
writer.WriteLine($"{indent}{indentLike dpath}{param.x2Name} __is__ {x2.x}"))
148+
|> Some)
139149

140-
type MyDiffType<'T>() =
141-
static member val Differ = MyCustomDiffer().GetDiffer<'T>()
150+
let differ<'T> = myCustomDiffer.GetDiffer<'T>()
142151

143152
[<Fact>]
144153
let ``Assert with immediate value adds newline`` () =
145154
let ex = Assert.Throws<AssertionFailedException>(fun () ->
146-
Differ.Assert({ x = "a" }, { x = "b" }, MyDiffType.Differ))
155+
Differ.Assert({ x = "a" }, { x = "b" }, differ))
147156
Assert.Equal("\nExpect __is__ a\nActual __is__ b\n", ex.Message)
148157

149158
[<Fact>]
150159
let ``Assert with nested value doesn't add newline`` () =
151160
let ex = Assert.Throws<AssertionFailedException>(fun () ->
152-
Differ.Assert({| i = { x = "a" } |}, {| i = { x = "b" } |}, MyDiffType.Differ))
161+
Differ.Assert({| i = { x = "a" } |}, {| i = { x = "b" } |}, differ))
153162
Assert.Equal("i Expect __is__ a\n Actual __is__ b\n", ex.Message)
154163

155164
[<Fact>]
156165
let ``ToString with immediate value doesn't add newline`` () =
157-
let diff = Differ.ToString({ x = "a" }, { x = "b" }, MyDiffType.Differ)
166+
let diff = Differ.ToString({ x = "a" }, { x = "b" }, differ)
158167
Assert.Equal("Expect __is__ a\nActual __is__ b\n", diff)
159168

160169
[<Fact>]
161170
let ``ToString with nested value doesn't add newline`` () =
162-
let diff = Differ.ToString({| i = { x = "a" } |}, {| i = { x = "b" } |}, MyDiffType.Differ)
171+
let diff = Differ.ToString({| i = { x = "a" } |}, {| i = { x = "b" } |}, differ)
163172
Assert.Equal("i Expect __is__ a\n Actual __is__ b\n", diff)
164173

165174
type Rec = { xRec: Rec option }

0 commit comments

Comments
 (0)