Pretty diffs for case classes.
The library is published for Scala 2.12 and 2.13.
- goals of the project
- teaser
- colors
- integrations
- ignoring
- customization
- similar projects
- commercial support
- human-readable case class diffs
- support for popular testing frameworks
- OOTB collections support
- OOTB non-case class support
- smaller compilation overhead compared to shapless based solutions (thanks to magnolia <3)
- programmer friendly and type safe api for partial ignore
Add the following dependency:
"com.softwaremill.diffx" %% "diffx-core" % "0.3.29-SNAPSHOT"
sealed trait Parent
case class Bar(s: String, i: Int) extends Parent
case class Foo(bar: Bar, b: List[Int], parent: Option[Parent]) extends Parent
val right: Foo = Foo(
Bar("asdf", 5),
List(123, 1234),
Some(Bar("asdf", 5))
)
// right: Foo = Foo(Bar("asdf", 5), List(123, 1234), Some(Bar("asdf", 5)))
val left: Foo = Foo(
Bar("asdf", 66),
List(1234),
Some(right)
)
// left: Foo = Foo(
// Bar("asdf", 66),
// List(1234),
// Some(Foo(Bar("asdf", 5), List(123, 1234), Some(Bar("asdf", 5))))
// )
import com.softwaremill.diffx._
compare(left, right)
// res0: DiffResult = DiffResultObject(
// "Foo",
// ListMap(
// "bar" -> DiffResultObject(
// "Bar",
// ListMap("s" -> Identical("asdf"), "i" -> DiffResultValue(66, 5))
// ),
// "b" -> DiffResultObject(
// "List",
// ListMap("0" -> DiffResultValue(1234, 123), "1" -> DiffResultMissing(1234))
// ),
// "parent" -> DiffResultValue("repl.Session.App.Foo", "repl.Session.App.Bar")
// )
// )
Will result in:
When running tests through sbt, default diffx's colors work well on both dark and light backgrounds. Unfortunately Intellij Idea forces the default color to red when displaying test's error. This means that it is impossible to print something with the standard default color (either white or black depending on the color scheme).
To have better colors, external information about the desired theme is required.
Specify environment variable DIFFX_COLOR_THEME
and set it to either light
or dark
.
I had to specify it in /etc/environment
rather than home profile for Intellij Idea to picked it up.
If anyone has an idea how this could be improved, I am open for suggestions.
To use with scalatest, add the following dependency:
"com.softwaremill.diffx" %% "diffx-scalatest" % "0.3.29-SNAPSHOT" % Test
Then, extend the com.softwaremill.diffx.scalatest.DiffMatcher
trait or import com.softwaremill.diffx.scalatest.DiffMatcher._
.
After that you will be able to use syntax such as:
import org.scalatest.matchers.should.Matchers._
import com.softwaremill.diffx.scalatest.DiffMatcher._
left should matchTo(right)
Giving you nice error messages:
To use with specs2, add the following dependency:
"com.softwaremill.diffx" %% "diffx-specs2" % "0.3.29-SNAPSHOT" % Test
Then, extend the com.softwaremill.diffx.specs2.DiffMatcher
trait or import com.softwaremill.diffx.specs2.DiffMatcher._
.
After that you will be able to use syntax such as:
import org.specs2.matcher.MustMatchers.{left => _, right => _, _}
import com.softwaremill.diffx.specs2.DiffMatcher._
left must matchTo(right)
To use with utest, add following dependency:
"com.softwaremill.diffx" %% "diffx-utest" % "0.3.29-SNAPSHOT" % Test
Then, mixin DiffxAssertions
trait or add import com.softwaremill.diffx.utest.DiffxAssertions._
to your test code.
To assert using diffx use assertEquals
as follows:
import com.softwaremill.diffx.utest.DiffxAssertions._
assertEqual(left, right)
Fields can be excluded from comparision by calling the ignore
method on the Diff
instance.
Since Diff
instances are immutable, the ignore
method creates a copy of the instance with modified logic.
You can use this instance explicitly.
If you still would like to use it implicitly, you first need to summon the instance of the Diff
typeclass using
the Derived
typeclass wrapper: Derived[Diff[Person]]
. Thanks to that trick, later you will be able to put your modified
instance of the Diff
typeclass into the implicit scope. The whole process looks as follows:
case class Person(name:String, age:Int)
implicit val modifiedDiff: Diff[Person] = Derived[Diff[Person]].ignore[Person,String](_.name)
If you'd like to implement custom matching logic for the given type, create an implicit Diff
instance for that
type, and make sure it's in scope when any Diff
instances depending on that type are created.
If there is already a typeclass for a particular type, but you would like to use another one, you wil have to override existing instance. Because of the "exporting" mechanism the top level typeclass is Derived[Diff]
rather then Diff
and that's the typeclass you need to override.
To understand it better, consider following example with NonEmptyList
from cats.
NonEmptyList
is implemented as case class so diffx will create a Derived[Diff[NonEmptyList]]
typeclass instance using magnolia derivation.
Obviously that's not what we usually want. In most of the cases we would like NonEmptyList
to be compared as a list.
Diffx already has an instance of a typeclass for a list. One more thing to do is to use that typeclass by converting NonEmptyList
to list which can be done using contramap
method.
The final code looks as follows:
import cats.data.NonEmptyList
implicit def nelDiff[T: Diff]: Derived[Diff[NonEmptyList[T]]] =
Derived(Diff[List[T]].contramap[NonEmptyList[T]](_.toList))
And here's an example customizing the Diff
instance for a child class of a sealed trait
sealed trait ABParent
case class A(id: String, name: String) extends ABParent
case class B(id: String, name: String) extends ABParent
implicit val diffA: Derived[Diff[A]] = Derived(Diff.gen[A].value.ignore[A, String](_.id))
val a1: ABParent = A("1", "X")
// a1: ABParent = A("1", "X")
val a2: ABParent = A("2", "X")
// a2: ABParent = A("2", "X")
compare(a1, a2)
// res5: DiffResult = Identical(A("1", "X"))
You may need to add -Wmacros:after
Scala compiler option to make sure to check for unused implicits
after macro expansion.
If you get warnings from Magnolia which looks like magnolia: using fallback derivation for TYPE
,
you can use the Silencer compiler plugin to silent the warning
with the compiler option "-P:silencer:globalFilters=^magnolia: using fallback derivation.*$"
- com.softwaremill.common.tagging
"com.softwaremill.diffx" %% "diffx-tagging" % "0.3.29-SNAPSHOT"
com.softwaremill.diffx.tagging.DiffTaggingSupport
- eu.timepit.refined
"com.softwaremill.diffx" %% "diffx-refined" % "0.3.29-SNAPSHOT"
com.softwaremill.diffx.refined.RefinedSupport
- org.typelevel.cats
"com.softwaremill.diffx" %% "diffx-cats" % "0.3.29-SNAPSHOT"
com.softwaremill.diffx.cats.DiffCatsInstances
There is a number of similar projects from which diffx draws inspiration.
Below is a list of some of them, which I am aware of, with their main differences:
- xotai/diff - based on shapeless, seems not to be activly developed anymore
- ratatool-diffy - the main purpose is to compare large data sets stored on gs or hdfs
We offer commercial support for diffx and related technologies, as well as development services. Contact us to learn more about our offer!
Copyright (C) 2019 SoftwareMill https://softwaremill.com.