Skip to content

Commit

Permalink
Add CustomDiffer functions
Browse files Browse the repository at this point in the history
  • Loading branch information
Tarmil committed Sep 29, 2023
1 parent 30f0e27 commit 4263985
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 26 deletions.
107 changes: 107 additions & 0 deletions src/Diffract/CustomDiffer.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
namespace DEdge.Diffract

open System
open DEdge.Diffract

[<Sealed; AbstractClass>]
type CustomDiffer<'T> =

/// <summary>
/// Create a custom differ for a specific type.
/// </summary>
/// <param name="buildDiffFunction">Builds the diff function for this type.</param>
static member Build(buildDiffFunction: Func<IDifferFactory, Func<'T, 'T, Diff option>>) =
{ new ICustomDiffer with
member this.GetCustomDiffer<'U>(differFactory, shape) =
if shape.Type = typeof<'T> then
let diffFunction = buildDiffFunction.Invoke(differFactory)
{ new IDiffer<'T> with
member _.Diff(x1, x2) = diffFunction.Invoke(x1, x2) }
|> unbox<IDiffer<'U>>
|> Some
else
None }

/// <summary>
/// Create a custom differ for a specific type.
/// </summary>
/// <param name="buildDiffFunction">Builds the diff function for this type.</param>
static member Build(buildDiffFunction: IDifferFactory -> 'T -> 'T -> Diff option) =
{ new ICustomDiffer with
member this.GetCustomDiffer<'U>(differFactory, shape) =
if shape.Type = typeof<'T> then
let diffFunction = buildDiffFunction differFactory
{ new IDiffer<'T> with
member _.Diff(x1, x2) = diffFunction x1 x2 }
|> unbox<IDiffer<'U>>
|> Some
else
None }

/// <summary>
/// Create a custom differ for a specific type.
/// </summary>
/// <param name="diffFunction">The diff function for this type.</param>
static member Build(diffFunction: Func<'T, 'T, Diff option>) =
CustomDiffer.Build(fun _ -> diffFunction)

/// <summary>
/// Create a custom differ for a specific type by mapping it to a diffable type.
/// </summary>
/// <param name="mapFunction">The mapping function.</param>
/// <typeparam name="T">The type for which a custom differ is being created.</typeparam>
/// <typeparam name="U">The type used to actually perform the diff.</typeparam>
static member Map<'U>(mapFunction: Func<'T, 'U>) =
CustomDiffer<'T>.Build(fun differFactory ->
let differ = differFactory.GetDiffer<'U>()
fun x1 x2 -> differ.Diff(mapFunction.Invoke(x1), mapFunction.Invoke(x2)))

[<Sealed; AbstractClass>]
type CustomDiffer =

/// <summary>
/// Create a custom differ for a specific type.
/// </summary>
/// <param name="buildDiffFunction">Builds the diff function for this type.</param>
static member Build(buildDiffFunction: Func<IDifferFactory, Func<'T, 'T, Diff option>>) =
CustomDiffer<'T>.Build(buildDiffFunction)

/// <summary>
/// Create a custom differ for a specific type.
/// </summary>
/// <param name="buildDiffFunction">Builds the diff function for this type.</param>
static member Build(buildDiffFunction: IDifferFactory -> 'T -> 'T -> Diff option) =
CustomDiffer<'T>.Build(buildDiffFunction)

/// <summary>
/// Create a custom differ for a specific type.
/// </summary>
/// <param name="diffFunction">The diff function for this type.</param>
static member Build(diffFunction: Func<'T, 'T, Diff option>) =
CustomDiffer<'T>.Build(diffFunction)

/// <summary>
/// Create a custom differ for a leaf type using default comparison and a custom display format.
/// </summary>
/// <param name="format">The display format.</param>
static member Leaf<'T when 'T : equality> (format: Func<'T, string>) =
CustomDiffer.Build<'T>(fun x y ->
if x = y then
None
else
Diff.Value(format.Invoke(x), format.Invoke(y))
|> Some)

/// <summary>
/// Combine multiple custom differs.
/// </summary>
/// <param name="differs">The custom differs.</param>
static member Combine (differs: seq<ICustomDiffer>) =
CombinedCustomDiffer(differs) :> ICustomDiffer

/// <summary>
/// Combine multiple custom differs.
/// </summary>
/// <param name="differs">The custom differs.</param>
static member Combine ([<ParamArray>] differs: ICustomDiffer[]) =
CustomDiffer.Combine(differs :> seq<_>)
1 change: 1 addition & 0 deletions src/Diffract/Diffract.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="Types.fs" />
<Compile Include="CustomDiffer.fs" />
<Compile Include="ReadOnlyDictionaryShape.fs" />
<Compile Include="DictionaryShape.fs" />
<Compile Include="Differ.fs" />
Expand Down
73 changes: 73 additions & 0 deletions tests/Diffract.CSharp.Tests/CustomDiffers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,36 @@ public void CustomDiffer()
Assert.Equal(expectedDiff, actualDiff);
}

[Fact]
public void CustomDifferWithCombinators()
{
var expectedDiff = "D Expect = \"a\"\n Actual = \"b\"\n";
var expected = new Container(new CustomDiffable("a"));
var actual = new Container(new CustomDiffable("b"));
var actualDiff = MyDifferWithCombinators.Get<Container>().ToString(expected, actual);
Assert.Equal(expectedDiff, actualDiff);
}

[Fact]
public void CustomDifferWithMap()
{
var expectedDiff = "D Expect = \"a\"\n Actual = \"b\"\n";
var expected = new Container(new CustomDiffable("a"));
var actual = new Container(new CustomDiffable("b"));
var actualDiff = MyDifferWithMap.Get<Container>().ToString(expected, actual);
Assert.Equal(expectedDiff, actualDiff);
}

[Fact]
public void CustomDifferWithMapToAnonymousObject()
{
var expectedDiff = "D.v Expect = \"a\"\n Actual = \"b\"\n";
var expected = new Container(new CustomDiffable("a"));
var actual = new Container(new CustomDiffable("b"));
var actualDiff = MyDifferWithMapToAnonymousObject.Get<Container>().ToString(expected, actual);
Assert.Equal(expectedDiff, actualDiff);
}

public record CustomDiffable(string X);

public record Container(CustomDiffable D);
Expand Down Expand Up @@ -57,5 +87,48 @@ public FSharpOption<IDiffer<T>> GetCustomDiffer<T>(IDifferFactory differFactory,
: null;
}
}

public static class MyDifferWithCombinators
{
public static IDiffer<T> Get<T>() => Singleton<T>.Instance;

private static class Singleton<T>
{
public static readonly IDiffer<T> Instance = CustomDiffer.GetDiffer<T>();
}

private static readonly ICustomDiffer CustomDiffer =
CustomDiffer<CustomDiffable>.Build(factory =>
{
var stringDiffer = factory.GetDiffer<string>();
return (x1, x2) => stringDiffer.Diff(x1.X, x2.X);
});
}

public static class MyDifferWithMap
{
public static IDiffer<T> Get<T>() => Singleton<T>.Instance;

private static class Singleton<T>
{
public static readonly IDiffer<T> Instance = CustomDiffer.GetDiffer<T>();
}

private static readonly ICustomDiffer CustomDiffer =
CustomDiffer<CustomDiffable>.Map(x => x.X);
}

public static class MyDifferWithMapToAnonymousObject
{
public static IDiffer<T> Get<T>() => Singleton<T>.Instance;

private static class Singleton<T>
{
public static readonly IDiffer<T> Instance = CustomDiffer.GetDiffer<T>();
}

private static readonly ICustomDiffer CustomDiffer =
CustomDiffer<CustomDiffable>.Map(x => new { v = x.X });
}
}
}
61 changes: 35 additions & 26 deletions tests/Diffract.Tests/Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ module MyDiffModule =

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

module MyDiffWithCombinators =

let customDiffer = CustomDiffer<CustomDiffable>.Build(fun factory ->
let stringDiffer = factory.GetDiffer<string>()
fun x1 x2 -> stringDiffer.Diff(x1.x, x2.x))

let differ<'T> = customDiffer.GetDiffer<'T>()

type MyDiffer(differFactory: IDifferFactory) =
let stringDiffer = differFactory.GetDiffer<string>()

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

type MyDiffTypeWithCombinators<'T>() =
static let customDiffer = CustomDiffer<CustomDiffable>.Build(fun factory ->
let stringDiffer = factory.GetDiffer<string>()
fun x1 x2 -> stringDiffer.Diff(x1.x, x2.x))

static member val Differ = customDiffer.GetDiffer<'T>()

[<Fact>]
let ``Custom differ`` () =
Assert.Equal("x Expect = \"a\"\n Actual = \"b\"\n",
Differ.ToString({ x = "a" }, { x = "b" }))
Assert.Equal("Expect = \"a\"\nActual = \"b\"\n",
Differ.ToString({ x = "a" }, { x = "b" }, MyDiffModule.differ))
Assert.Equal("Expect = \"a\"\nActual = \"b\"\n",
Differ.ToString({ x = "a" }, { x = "b" }, MyDiffWithCombinators.differ))
Assert.Equal("Expect = \"a\"\nActual = \"b\"\n",
Differ.ToString({ x = "a" }, { x = "b" }, MyDiffType.Differ))
Assert.Equal("Expect = \"a\"\nActual = \"b\"\n",
Differ.ToString({ x = "a" }, { x = "b" }, MyDiffTypeWithCombinators.Differ))

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

type MyCustomDiffer() =
interface ICustomDiffer with
member this.GetCustomDiffer<'T>(_, shape) =
if shape.Type = typeof<CustomDiffable> then
{ new IDiffer<CustomDiffable> with
member _.Diff(x1, x2) =
if x1.x = x2.x then
None
else
Diff.MakeCustom(fun writer param indent path recur ->
if param.ensureFirstLineIsAligned then writer.WriteLine()
let indentLike str = String.replicate (String.length str) " "
let dpath = if path = "" then "" else path + " "
writer.WriteLine($"{indent}{dpath}{param.x1Name} __is__ {x1.x}")
writer.WriteLine($"{indent}{indentLike dpath}{param.x2Name} __is__ {x2.x}"))
|> Some }
|> unbox<IDiffer<'T>>
|> Some
else
None
let myCustomDiffer = CustomDiffer<CustomDiffable>.Build(fun x1 x2 ->
if x1.x = x2.x then
None
else
Diff.MakeCustom(fun writer param indent path recur ->
if param.ensureFirstLineIsAligned then writer.WriteLine()
let indentLike str = String.replicate (String.length str) " "
let dpath = if path = "" then "" else path + " "
writer.WriteLine($"{indent}{dpath}{param.x1Name} __is__ {x1.x}")
writer.WriteLine($"{indent}{indentLike dpath}{param.x2Name} __is__ {x2.x}"))
|> Some)

type MyDiffType<'T>() =
static member val Differ = MyCustomDiffer().GetDiffer<'T>()
let differ<'T> = myCustomDiffer.GetDiffer<'T>()

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

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

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

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

type Rec = { xRec: Rec option }
Expand Down

0 comments on commit 4263985

Please sign in to comment.