This project provides the Difference ADT, which models a structural diff of
scala values. It is built (and depends) on
cats and
shapeless.
Differences are computed via the Diff[A] type class that provides a function
of type (A, A) => Option[Difference].
The goal of this library is to provide convenient, customizable and extensible generic
derivation of Diff instances, as well as a decent textual representation
of Difference values.
This is intended primarily as an aid for automated testing, and was motivated by spending way too much time playing "Spot the difference" with large ADTs in test failure logs, but there may be other use cases.
case class Address( street: String, city: String )
case class Person( name: String, age: Int, address: Address )
import fr.thomasdufour.autodiff.Diff
import fr.thomasdufour.autodiff.Pretty
import fr.thomasdufour.autodiff.derived
implicit val personDiff: Diff[Person] = {
import derived.auto._
derived.semi.diff
}
val difference = personDiff(
Person( "Jean Martin", 29, Address( "2 rue Pasteur", "Lille" ) ),
Person( "Jean Martin", 55, Address( "2 rue Pasteur", "Lyon" ) )
)
println( Pretty.colorized2.show( difference ) )yields:
Only Scala 2.12 and 2.13 are supported, and 2.13 support starts in version 0.4.0.
auto-diff |
Scala | Cats |
|---|---|---|
| 0.3.0 | 2.12 | 1.6.1 |
| 0.4.0 | 2.12, 2.13 | 2.0.0 |
Add some or all of the following to your build.sbt:
libraryDependencies ++= Seq(
"fr.thomasdufour" %% "auto-diff-core" % "x.y.z",
"fr.thomasdufour" %% "auto-diff-generic" % "x.y.z",
"fr.thomasdufour" %% "auto-diff-enumeratum" % "x.y.z",
"fr.thomasdufour" %% "auto-diff-scalatest" % "x.y.z" % "test"
)For the current version, check the maven central badge at the top of this readme.
Diff instances are available implicitly (from the companion object) for:
- primitive types
StringandUUIDjava.timetypes- Scala tuples
- Some collections and ADTs, including:
OptionEitherList,Vector, finiteStreamandLazyList,[Sorted]Set,[Sorted]Mapand a few more specialized variantsIterableas a fallback
cats.datatypes includingValidated,ChainandNonEmpty{Chain|List|Vector|Set}- enumeratum
Enums when using theautodiff-enumeratummodule.
import fr.thomasdufour.autodiff.Diff
import fr.thomasdufour.autodiff.Pretty
def printDiff[A: Diff]( x: A, y: A ): Unit =
println( Pretty.colorized2.showDiff( x, y ) )
printDiff( 1, 2 )
printDiff( "abc", "def" )
printDiff( Some( "abc" ), None )
printDiff( Left( "error" ), Right( 42 ) )
printDiff[Either[String, Int]]( Right( 66 ), Right( 42 ) )
printDiff( 1 :: 2 :: Nil, 1 :: 3 :: 4 :: Nil )
printDiff( Map( "a" -> 1, "b" -> 2 ), Map( "b" -> 3, "a" -> 1 ) )Generic derivation is based on shapeless.LabelledGeneric, and is available for:
- case classes and objects
- sealed trait hierarchies
Like in Typelevel kittens, there are three modes of generic derivation: semi-automatic, full-automatic, and cached full-automatic.
As these modes are so directly copied from kittens, the best explanation of how they work would be found in the kittens README
The recommended mode is semi-automatic, with auto-derivation locally in scope and assigning the
resulting instances to (implicit) vals.
Example:
case class Item( description: String )
case class Bag( items: List[Item] )
import fr.thomasdufour.autodiff.Diff
import fr.thomasdufour.autodiff.Pretty
import fr.thomasdufour.autodiff.derived
implicit val bagDiff: Diff[Bag] = {
import derived.auto._
derived.semi.diff
}
println(
Pretty.colorized2.showDiff(
Bag( Item( "a wombat" ) :: Item( "coffee" ) :: Item( "a green fountain pen" ) :: Nil ),
Bag( Item( "4 paperclips" ) :: Item( "coffee" ) :: Nil )
) )Generic derivation should work with recursive types, including mutually recursive types and parametric recursive types. See the tests for some examples
Deriving Diff instances for mutually recursive types and declaring them implicit in the same scope can go quite badly.
And by badly I mean NullPointerException- or StackOverflowError-badly.
A suggestion on how to fix this issue:
import fr.thomasdufour.autodiff.Diff
import fr.thomasdufour.autodiff.derived.auto
import fr.thomasdufour.autodiff.derived.semi
case class Outer( inners: Vector[Inner] )
case class Inner( outers: Vector[Outer] )
// BAD!! DON'T DO THIS
object implicits1 {
lazy implicit val outerDiff: Diff[Outer] = semi.diff[Outer]
lazy implicit val innerDiff: Diff[Inner] = semi.diff[Inner]
}
// DO THIS INSTEAD
trait implicits2 {
protected def mkOuterDiff: Diff[Outer] = {
import auto._
semi.diff[Outer]
}
protected def mkInnerDiff: Diff[Inner] = {
import auto._
semi.diff[Inner]
}
}
object implicits2 extends implicits2 {
implicit val outerDiff: Diff[Outer] = mkOuterDiff
implicit val innerDiff: Diff[Inner] = mkInnerDiff
}A Diff instance can be customized by having an implicit Diff "override" in scope for a field,
for example we might consider that the order of the items in the bag is irrelevant (but without
the ability to modify Bag to have a Set[Item], say):
case class Item( description: String )
case class Bag( items: List[Item] )
import fr.thomasdufour.autodiff.Diff
import fr.thomasdufour.autodiff.Pretty
import fr.thomasdufour.autodiff.derived
implicit val bagDiff: Diff[Bag] = {
import derived.auto._
implicit val diffItems: Diff[List[Item]] = Diff.inAnyOrder
derived.semi.diff
}
println(
Pretty.colorized2.showDiff(
Bag( Item( "a wombat" ) :: Nil ),
Bag( Item( "4 paperclips" ) :: Item( "a wombat" ) :: Nil )
)
)There are a number of ways to manually create Diff instances.
The most flexible (and possibly complex) would be to implement the trait directly. Other than that, we can create a diff from:
- An equality function (and a show function):
Diff.explicitEqShow[A]( eqv: ( A, A ) => Boolean, show: A => String ): Diff[A] - Implicit
cats.Eqandcats.Showinstances:Diff.implicitEqShow[A]( implicit E: Eq[A], S: Show[A] ): Diff[A] - The default equality and toString:
Diff.defaultEqShow[A]: Diff[A]
There are also functions Diff.forProductN (1 ≤ N ≤ 22) that allow manually deconstructing
types to (diffable) components.
Finally, there are Diff.ignore that never reports its arguments as different and Diff.inAnyOrder
that compares collections as if they were unordered bags.
An optional, implicit Hint[A] can be used for some Diff instances related to unordered collection diffing, such as
Diff.inAnyOrder or Diff[Set[A]], to indicate whether elements can be equal. This can help align mismatched elements
and provide a better Difference.
(TODO: longer explanation & examples)
Support Scala 2.13 (and cross-build for 2.12) - this is actually #1.OK, starting with 0.4.0-RC1.- Scala.js support, perhaps
- Further API exploration for the "front-end", including test framework integration.
- Improve test coverage, especially text rendering of differences.
- Integrate
cats.Showrather than havingDiffalso carry a show method- Maybe try to expand on that to have the show function drill "just deep enough" into data.
xdotai/diff for inspiration.
circe for some implementation techniques in early versions.
kittens for generic derivation guidelines.



