diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 00000000..583decfd --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,7 @@ +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" \ No newline at end of file diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 00000000..73432a56 --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,44 @@ +name: CI + +on: + push: + branches: [ "**" ] + tags: [ v* ] + pull_request: + branches: [ "**" ] + +jobs: + build: + runs-on: ubuntu-20.04 + env: + JAVA_OPTS: -Xmx4G + steps: + - uses: actions/checkout@v2 + - uses: coursier/cache-action@v6.1 + - uses: olafurpg/setup-scala@v12 + with: + java-version: adopt@1.11 + - name: Compile docs + run: sbt compileDocs + - name: Run tests with sbt + run: sbt test + + publish: + name: Publish release + if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) + needs: [build] + runs-on: ubuntu-20.04 + env: + JAVA_OPTS: -Xmx4G + steps: + - uses: actions/checkout@v2.3.4 + with: + fetch-depth: 0 + - uses: coursier/cache-action@v6.1 + - uses: olafurpg/setup-scala@v12 + - run: sbt ci-release + env: + PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + PGP_SECRET: ${{ secrets.PGP_SECRET }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} diff --git a/.mergify.yml b/.mergify.yml index 54e87096..2e542012 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -6,7 +6,7 @@ pull_request_rules: - name: automatic merge for scala-steward pull requests affecting build.sbt conditions: - author=scala-steward - - status-success=continuous-integration/travis-ci/pr + - check-success=build - "#files=1" - files=build.sbt actions: @@ -15,7 +15,7 @@ pull_request_rules: - name: automatic merge for scala-steward pull requests affecting project plugins.sbt conditions: - author=scala-steward - - status-success=continuous-integration/travis-ci/pr + - check-success=build - "#files=1" - files=project/plugins.sbt actions: @@ -24,7 +24,7 @@ pull_request_rules: - name: semi-automatic merge for scala-steward pull requests conditions: - author=scala-steward - - status-success=continuous-integration/travis-ci/pr + - check-success=build - "#approved-reviews-by>=1" actions: merge: @@ -32,7 +32,7 @@ pull_request_rules: - name: automatic merge for scala-steward pull requests affecting project build.properties conditions: - author=scala-steward - - status-success=continuous-integration/travis-ci/pr + - check-success=build - "#files=1" - files=project/build.properties actions: @@ -41,7 +41,7 @@ pull_request_rules: - name: automatic merge for scala-steward pull requests affecting .scalafmt.conf conditions: - author=scala-steward - - status-success=continuous-integration/travis-ci/pr + - check-success=build - "#files=1" - files=.scalafmt.conf actions: diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..4d0de76a --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,11 @@ +# Required +version: 2 + +sphinx: + configuration: generated-docs/out/conf.py + +# Optionally set the version of Python and requirements required to build your docs +python: + version: 3.7 + install: + - requirements: generated-docs/out/requirements.pip \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 720e8605..00000000 --- a/.travis.yml +++ /dev/null @@ -1,30 +0,0 @@ -language: scala -scala: - - 2.12.8 - - 2.13.1 -before_install: - - bash scripts/decrypt_files_if_not_pr.sh -before_cache: - - du -h -d 1 $HOME/.ivy2/ - - du -h -d 2 $HOME/.sbt/ - - du -h -d 4 $HOME/.coursier/ - - find $HOME/.sbt -name "*.lock" -type f -delete - - find $HOME/.ivy2/cache -name "ivydata-*.properties" -type f -delete - - find $HOME/.coursier/cache -name "*.lock" -type f -delete -cache: - directories: - - "$HOME/.sbt/1.0" - - "$HOME/.sbt/boot/scala*" - - "$HOME/.sbt/cache" - - "$HOME/.sbt/launchers" - - "$HOME/.ivy2/cache" - - "$HOME/.coursier" -script: - - sbt ++$TRAVIS_SCALA_VERSION test -deploy: - - provider: script - script: sbt publishRelease - skip_cleanup: true - on: - all_branches: true - condition: $TRAVIS_SCALA_VERSION = "2.12.8" && $TRAVIS_TAG =~ ^v[0-9]+\.[0-9]+(\.[0-9]+)? diff --git a/README.md b/README.md index 03803204..27c4ef94 100644 --- a/README.md +++ b/README.md @@ -1,247 +1,23 @@ ![diffx](https://github.com/softwaremill/diffx/raw/master/banner.png) -[![Build Status](https://travis-ci.org/softwaremill/diffx.svg?branch=master)](https://travis-ci.org/softwaremill/diffx) +[![Build Status](https://img.shields.io/github/workflow/status/softwaremill/diffx/CI/master)](https://github.com/softwaremill/diffx/actions) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.softwaremill.diffx/diffx-core_2.13/badge.svg)](https://search.maven.org/search?q=g:com.softwaremill.diffx) [![Gitter](https://badges.gitter.im/softwaremill/diffx.svg)](https://gitter.im/softwaremill/diffx?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Mergify Status](https://img.shields.io/endpoint.svg?url=https://gh.mergify.io/badges/softwaremill/diffx&style=flat)](https://mergify.io) [![Scala Steward badge](https://img.shields.io/badge/Scala_Steward-helping-brightgreen.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAMAAAARSr4IAAAAVFBMVEUAAACHjojlOy5NWlrKzcYRKjGFjIbp293YycuLa3pYY2LSqql4f3pCUFTgSjNodYRmcXUsPD/NTTbjRS+2jomhgnzNc223cGvZS0HaSD0XLjbaSjElhIr+AAAAAXRSTlMAQObYZgAAAHlJREFUCNdNyosOwyAIhWHAQS1Vt7a77/3fcxxdmv0xwmckutAR1nkm4ggbyEcg/wWmlGLDAA3oL50xi6fk5ffZ3E2E3QfZDCcCN2YtbEWZt+Drc6u6rlqv7Uk0LdKqqr5rk2UCRXOk0vmQKGfc94nOJyQjouF9H/wCc9gECEYfONoAAAAASUVORK5CYII=)](https://scala-steward.org) -Pretty diffs for case classes. +Pretty diffs for case classes. -The library is published for Scala 2.12 and 2.13. +## Documentation -## Table of contents -- [goals of the project](#goals-of-the-project) -- [teaser](#teaser) -- [colors](#colors) -- integrations - - [scalatest](#scalatest-integration) - - [specs2](#specs2-integration) - - [utest](#utest-integration) - - [other](#other-3rd-party-libraries-support) -- [ignoring](#ignoring) -- [customization](#customization) -- [similar projects](#similar-projects) -- [commercial support](#commercial-support) +diffx documentation is available at [diffx-scala.readthedocs.io](https://diffx-scala.readthedocs.io). -## Goals of the project +## Modifying documentation +The documentation is typechecked using `mdoc`. The sources for the documentation exist in `docs-sources`. Don't modify the generated documentation in `generated-docs`, as these files will get overwritten! -- 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 +When generating documentation, it's best to set the version to the current one, so that the generated doc files don't include modifications with the current snapshot version. -## Teaser -Add the following dependency: - -```scala -"com.softwaremill.diffx" %% "diffx-core" % "0.3.30" -``` - -```scala -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.MdocSession.App.Foo", "repl.MdocSession.App.Bar") -// ) -// ) -``` - -Will result in: - -![example](https://github.com/softwaremill/diff-x/blob/master/example.png?raw=true) - - -## Colors - -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. - -## Scalatest integration - -To use with scalatest, add the following dependency: - -```scala -"com.softwaremill.diffx" %% "diffx-scalatest" % "0.3.30" % 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: - -```scala -import org.scalatest.matchers.should.Matchers._ -import com.softwaremill.diffx.scalatest.DiffMatcher._ - -left should matchTo(right) -``` - -Giving you nice error messages: - -## Specs2 integration - -To use with specs2, add the following dependency: - -```scala -"com.softwaremill.diffx" %% "diffx-specs2" % "0.3.30" % 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: - -```scala -import org.specs2.matcher.MustMatchers.{left => _, right => _, _} -import com.softwaremill.diffx.specs2.DiffMatcher._ - -left must matchTo(right) -``` - -## Utest integration - -To use with utest, add following dependency: - -```scala -"com.softwaremill.diffx" %% "diffx-utest" % "0.3.30" % 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: - -```scala -import com.softwaremill.diffx.utest.DiffxAssertions._ -assertEqual(left, right) -``` - -## Ignoring - -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: - -```scala -case class Person(name:String, age:Int) -implicit val modifiedDiff: Diff[Person] = Derived[Diff[Person]].ignore[Person,String](_.name) -``` - -## Customization - -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: - -```scala -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 - -```scala -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)) -``` -```scala -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](https://github.com/ghik/silencer) compiler plugin to silent the warning -with the compiler option `"-P:silencer:globalFilters=^magnolia: using fallback derivation.*$"` - -## Other 3rd party libraries support - -- [com.softwaremill.common.tagging](https://github.com/softwaremill/scala-common) - ```scala - "com.softwaremill.diffx" %% "diffx-tagging" % "0.3.30" - ``` - `com.softwaremill.diffx.tagging.DiffTaggingSupport` -- [eu.timepit.refined](https://github.com/fthomas/refined) - ```scala - "com.softwaremill.diffx" %% "diffx-refined" % "0.3.30" - ``` - `com.softwaremill.diffx.refined.RefinedSupport` -- [org.typelevel.cats](https://github.com/typelevel/cats) - ```scala - "com.softwaremill.diffx" %% "diffx-cats" % "0.3.30" - ``` - `com.softwaremill.diffx.cats.DiffCatsInstances` - -## Similar projects - -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](https://github.com/xdotai/diff) - based on shapeless, seems not to be activly developed anymore -- [ratatool-diffy](https://github.com/spotify/ratatool/tree/master/ratatool-diffy) - the main purpose is to compare large data sets stored on gs or hdfs - -## Commercial Support - -We offer commercial support for diffx and related technologies, as well as development services. [Contact us](https://softwaremill.com) to learn more about our offer! +That is, in sbt run: `set version := "0.5.0"`, before running `mdoc` in `docs`. ## Copyright diff --git a/build.sbt b/build.sbt index 26bae4d6..b5671b72 100644 --- a/build.sbt +++ b/build.sbt @@ -1,187 +1,234 @@ -import com.softwaremill.PublishTravis.publishTravisSettings -import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject} +import com.softwaremill.UpdateVersionInDocs +import sbt.Def +import sbt.Reference.display -val v2_12 = "2.12.8" -val v2_13 = "2.13.1" +val scala212 = "2.12.14" +val scala213 = "2.13.6" -val scalatestVersion = "3.2.3" -val specs2Version = "4.10.5" -val smlTaggingVersion = "2.2.1" +val scalaIdeaVersion = scala212 // the version for which to import sources into intellij -lazy val commonSettings = commonSmlBuildSettings ++ ossPublishSettings ++ acyclicSettings ++ Seq( +val scalatestVersion = "3.2.9" +val specs2Version = "4.12.1" +val smlTaggingVersion = "2.3.0" + +lazy val commonSettings: Seq[Def.Setting[_]] = commonSmlBuildSettings ++ ossPublishSettings ++ Seq( organization := "com.softwaremill.diffx", - scalaVersion := v2_12, - scalafmtOnCompile := true, - crossScalaVersions := Seq(v2_12, v2_13), - libraryDependencies ++= Seq(compilerPlugin("com.softwaremill.neme" %% "neme-plugin" % "0.0.5")), scmInfo := Some(ScmInfo(url("https://github.com/softwaremill/diffx"), "git@github.com:softwaremill/diffx.git")), - // sbt-release - releaseCrossBuild := true + ideSkipProject := (scalaVersion.value != scalaIdeaVersion) || thisProjectRef.value.project.contains("JS"), + updateDocs := Def.taskDyn { + val files1 = UpdateVersionInDocs(sLog.value, organization.value, version.value) + Def.task { + (docs.jvm(scala213) / mdoc).toTask("").value + files1 ++ Seq(file("generated-docs/out")) + } + }.value ) -lazy val core = crossProject(JVMPlatform, JSPlatform) - .crossType(CrossType.Pure) - .in(file("core")) - .settings(commonSettings: _*) +val compileDocs: TaskKey[Unit] = taskKey[Unit]("Compiles docs module throwing away its output") +compileDocs := { + (docs.jvm(scala213) / mdoc).toTask(" --out target/diffx-docs").value +} + +val versionSpecificScalaSources = { + Compile / unmanagedSourceDirectories := { + val current = (Compile / unmanagedSourceDirectories).value + val sv = (Compile / scalaVersion).value + val baseDirectory = (Compile / scalaSource).value + val suffixes = CrossVersion.partialVersion(sv) match { + case Some((2, 13)) => List("2", "2.13+") + case Some((2, _)) => List("2", "2.13-") + case Some((3, _)) => List("3") + case _ => Nil + } + val versionSpecificSources = suffixes.map(s => new File(baseDirectory.getAbsolutePath + "-" + s)) + versionSpecificSources ++ current + } +} + +lazy val core = (projectMatrix in file("core")) + .settings(commonSettings) .settings( name := "diffx-core", libraryDependencies ++= Seq( - "com.propensive" %% "magnolia" % "0.17.0", + "com.propensive" %%% "magnolia" % "0.17.0", "org.scala-lang" % "scala-reflect" % scalaVersion.value, - "org.scalatest" %% "scalatest-flatspec" % scalatestVersion % Test, - "org.scalatest" %% "scalatest-freespec" % scalatestVersion % Test, - "org.scalatest" %% "scalatest-shouldmatchers" % scalatestVersion % Test + "org.scalatest" %%% "scalatest-flatspec" % scalatestVersion % Test, + "org.scalatest" %%% "scalatest-freespec" % scalatestVersion % Test, + "org.scalatest" %%% "scalatest-shouldmatchers" % scalatestVersion % Test, + "io.github.cquiroz" %%% "scala-java-time" % "2.3.0" % Test ), - unmanagedSourceDirectories in Compile += { - // sourceDirectory returns a platform-scoped directory, e.g. /.jvm - val sourceDir = (baseDirectory in Compile).value / ".." / "src" / "main" - CrossVersion.partialVersion(scalaVersion.value) match { - case Some((2, n)) if n >= 13 => sourceDir / "scala-2.13+" - case _ => sourceDir / "scala-2.13-" - } - } + versionSpecificScalaSources + ) + .jvmPlatform( + scalaVersions = List(scala212, scala213) + ) + .jsPlatform( + scalaVersions = List(scala212, scala213) ) -lazy val coreJVM = core.jvm -lazy val coreJS = core.js - -lazy val scalatest = crossProject(JVMPlatform, JSPlatform) - .in(file("scalatest")) - .settings(commonSettings: _*) +lazy val scalatest = (projectMatrix in file("scalatest")) + .settings(commonSettings) .settings( name := "diffx-scalatest", libraryDependencies ++= Seq( - "org.scalatest" %% "scalatest-matchers-core" % scalatestVersion, - "org.scalatest" %% "scalatest-flatspec" % scalatestVersion % Test, - "org.scalatest" %% "scalatest-shouldmatchers" % scalatestVersion % Test + "org.scalatest" %%% "scalatest-matchers-core" % scalatestVersion, + "org.scalatest" %%% "scalatest-flatspec" % scalatestVersion % Test, + "org.scalatest" %%% "scalatest-shouldmatchers" % scalatestVersion % Test ) ) .dependsOn(core) + .jvmPlatform( + scalaVersions = List(scala212, scala213) + ) + .jsPlatform( + scalaVersions = List(scala212, scala213) + ) -lazy val scalatestJVM = scalatest.jvm -lazy val scalatestJS = scalatest.js - -lazy val specs2 = crossProject(JVMPlatform, JSPlatform) - .in(file("specs2")) - .settings(commonSettings: _*) +lazy val specs2 = (projectMatrix in file("specs2")) + .settings(commonSettings) .settings( name := "diffx-specs2", libraryDependencies ++= Seq( - "org.specs2" %% "specs2-core" % specs2Version + "org.specs2" %%% "specs2-core" % specs2Version ) ) .dependsOn(core) + .jvmPlatform( + scalaVersions = List(scala212, scala213) + ) + .jsPlatform( + scalaVersions = List(scala212, scala213) + ) -lazy val specs2JVM = specs2.jvm -lazy val specs2JS = specs2.js - -lazy val utest = crossProject(JVMPlatform, JSPlatform) - .in(file("utest")) - .settings(commonSettings: _*) +lazy val utest = (projectMatrix in file("utest")) + .settings(commonSettings) .settings( - name := "diffx-utests", + name := "diffx-utest", libraryDependencies ++= Seq( - "com.lihaoyi" %% "utest" % "0.7.5" + "com.lihaoyi" %%% "utest" % "0.7.10" ), testFrameworks += new TestFramework("utest.runner.Framework") ) .dependsOn(core) + .jvmPlatform( + scalaVersions = List(scala212, scala213) + ) + .jsPlatform( + scalaVersions = List(scala212, scala213) + ) -lazy val utestJVM = utest.jvm -lazy val utestJS = utest.js +lazy val munit = (projectMatrix in file("munit")) + .settings(commonSettings) + .settings( + name := "diffx-munit", + libraryDependencies ++= Seq( + "org.scalameta" %%% "munit" % "0.7.26" + ), + testFrameworks += new TestFramework("munit.Framework") + ) + .dependsOn(core) + .jvmPlatform( + scalaVersions = List(scala212, scala213) + ) + .jsPlatform( + scalaVersions = List(scala212, scala213), + settings = commonSettings ++ Seq(scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) }) + ) -lazy val tagging = crossProject(JVMPlatform, JSPlatform) - .in(file("tagging")) - .settings(commonSettings: _*) +lazy val tagging = (projectMatrix in file("tagging")) + .settings(commonSettings) .settings( name := "diffx-tagging", libraryDependencies ++= Seq( - "com.softwaremill.common" %% "tagging" % smlTaggingVersion, - "org.scalatest" %% "scalatest-flatspec" % scalatestVersion % Test, - "org.scalatest" %% "scalatest-shouldmatchers" % scalatestVersion % Test + "com.softwaremill.common" %%% "tagging" % smlTaggingVersion, + "org.scalatest" %%% "scalatest-flatspec" % scalatestVersion % Test, + "org.scalatest" %%% "scalatest-shouldmatchers" % scalatestVersion % Test ) ) .dependsOn(core) + .jvmPlatform( + scalaVersions = List(scala212, scala213) + ) + .jsPlatform( + scalaVersions = List(scala212, scala213) + ) -lazy val taggingJVM = tagging.jvm -lazy val taggingJS = tagging.js - -lazy val cats = crossProject(JVMPlatform, JSPlatform) - .in(file("cats")) - .settings(commonSettings: _*) +lazy val cats = (projectMatrix in file("cats")) + .settings(commonSettings) .settings( name := "diffx-cats", libraryDependencies ++= Seq( - "org.typelevel" %% "cats-core" % "2.2.0", - "org.scalatest" %% "scalatest-freespec" % scalatestVersion % Test, - "org.scalatest" %% "scalatest-shouldmatchers" % scalatestVersion % Test + "org.typelevel" %%% "cats-core" % "2.6.1", + "org.scalatest" %%% "scalatest-freespec" % scalatestVersion % Test, + "org.scalatest" %%% "scalatest-shouldmatchers" % scalatestVersion % Test ) ) .dependsOn(core) + .jvmPlatform( + scalaVersions = List(scala212, scala213) + ) + .jsPlatform( + scalaVersions = List(scala212, scala213) + ) -lazy val catsJVM = cats.jvm -lazy val catsJS = cats.js - -lazy val refined = crossProject(JVMPlatform, JSPlatform) - .in(file("refined")) - .settings(commonSettings: _*) +lazy val refined = (projectMatrix in file("refined")) + .settings(commonSettings) .settings( name := "diffx-refined", libraryDependencies ++= Seq( - "eu.timepit" %% "refined" % "0.9.18", - "org.scalatest" %% "scalatest-flatspec" % scalatestVersion % Test, - "org.scalatest" %% "scalatest-shouldmatchers" % scalatestVersion % Test + "eu.timepit" %%% "refined" % "0.9.26", + "org.scalatest" %%% "scalatest-flatspec" % scalatestVersion % Test, + "org.scalatest" %%% "scalatest-shouldmatchers" % scalatestVersion % Test ) ) .dependsOn(core) + .jvmPlatform( + scalaVersions = List(scala212, scala213) + ) + .jsPlatform( + scalaVersions = List(scala212, scala213) + ) +// -lazy val refinedJVM = refined.jvm -lazy val refinedJS = refined.js - -lazy val docs = project - .in(file("generated-docs")) // important: it must not be docs/ +lazy val docs = (projectMatrix in file("generated-docs")) // important: it must not be docs/ + .enablePlugins(MdocPlugin) .settings(commonSettings) .settings( publishArtifact := false, name := "docs", libraryDependencies ++= Seq( - "org.typelevel" %% "cats-core" % "2.2.0", + "org.typelevel" %% "cats-core" % "2.6.1", "org.scalatest" %% "scalatest-shouldmatchers" % scalatestVersion - ) - ) - .dependsOn(coreJVM, scalatestJVM, specs2JVM, utestJVM, refinedJVM, taggingJVM) - .enablePlugins(MdocPlugin) - .settings( + ), mdocIn := file("docs-sources"), moduleName := "diffx-docs", mdocVariables := Map( "VERSION" -> version.value ), - mdocOut := file(".") + mdocOut := file("generated-docs/out") ) + .dependsOn(core, scalatest, specs2, utest, refined, tagging, cats, munit) + .jvmPlatform(scalaVersions = List(scala213)) + +val testJVM = taskKey[Unit]("Test JVM projects") +val testJS = taskKey[Unit]("Test JS projects") + +val allAggregates = + core.projectRefs ++ scalatest.projectRefs ++ + specs2.projectRefs ++ utest.projectRefs ++ cats.projectRefs ++ + refined.projectRefs ++ tagging.projectRefs ++ docs.projectRefs ++ munit.projectRefs + +def filterProject(p: String => Boolean) = + ScopeFilter(inProjects(allAggregates.filter(pr => p(display(pr.project))): _*)) lazy val rootProject = project .in(file(".")) - .settings(commonSettings: _*) - .settings(publishArtifact := false, name := "diffx") - .settings(publishTravisSettings) - .settings(beforeCommitSteps := { - Seq(releaseStepInputTask(docs / mdoc), Release.stageChanges("README.md")) - }) - .aggregate( - coreJVM, - coreJS, - scalatestJVM, - scalatestJS, - specs2JVM, - specs2JS, - utestJVM, - utestJS, - refinedJVM, - refinedJS, - taggingJVM, - taggingJS, - catsJVM, - catsJS, - docs + .settings(commonSettings) + .settings( + publishArtifact := false, + name := "diffx", + scalaVersion := scalaIdeaVersion, + testJVM := (Test / test).all(filterProject(p => !p.contains("JS") && !p.contains("Native"))).value, + testJS := (Test / test).all(filterProject(_.contains("JS"))).value ) + .aggregate(allAggregates: _*) diff --git a/cats/shared/src/main/scala/com/softwaremill/diffx/cats/DiffCatsInstances.scala b/cats/shared/src/main/scala/com/softwaremill/diffx/cats/DiffCatsInstances.scala deleted file mode 100644 index 6db53a02..00000000 --- a/cats/shared/src/main/scala/com/softwaremill/diffx/cats/DiffCatsInstances.scala +++ /dev/null @@ -1,19 +0,0 @@ -package com.softwaremill.diffx.cats - -import cats.data.{NonEmptyChain, NonEmptyList, NonEmptySet, NonEmptyVector} -import com.softwaremill.diffx.{Derived, Diff} -import com.softwaremill.diffx.Diff._ - -trait DiffCatsInstances { - implicit def diffNel[T: Diff]: Derived[Diff[NonEmptyList[T]]] = - Derived(Diff[List[T]].contramap[NonEmptyList[T]](_.toList)) - - implicit def diffNec[T: Diff]: Derived[Diff[NonEmptyChain[T]]] = - Derived(Diff[List[T]].contramap[NonEmptyChain[T]](_.toChain.toList)) - - implicit def diffNes[T: Diff]: Derived[Diff[NonEmptySet[T]]] = - Derived(Diff[Set[T]].contramap[NonEmptySet[T]](_.toSortedSet)) - - implicit def diffNev[T: Diff]: Derived[Diff[NonEmptyVector[T]]] = - Derived(Diff[List[T]].contramap[NonEmptyVector[T]](_.toVector.toList)) -} diff --git a/cats/src/main/scala/com/softwaremill/diffx/cats/DiffCatsInstances.scala b/cats/src/main/scala/com/softwaremill/diffx/cats/DiffCatsInstances.scala new file mode 100644 index 00000000..ca77a945 --- /dev/null +++ b/cats/src/main/scala/com/softwaremill/diffx/cats/DiffCatsInstances.scala @@ -0,0 +1,19 @@ +package com.softwaremill.diffx.cats + +import cats.data.{NonEmptyChain, NonEmptyList, NonEmptySet, NonEmptyVector} +import com.softwaremill.diffx.Diff +import com.softwaremill.diffx.Diff._ + +trait DiffCatsInstances { + implicit def diffNel[T: Diff]: Diff[NonEmptyList[T]] = + Diff[List[T]].contramap[NonEmptyList[T]](_.toList) + + implicit def diffNec[T: Diff]: Diff[NonEmptyChain[T]] = + Diff[List[T]].contramap[NonEmptyChain[T]](_.toChain.toList) + + implicit def diffNes[T: Diff]: Diff[NonEmptySet[T]] = + Diff[Set[T]].contramap[NonEmptySet[T]](_.toSortedSet) + + implicit def diffNev[T: Diff]: Diff[NonEmptyVector[T]] = + Diff[List[T]].contramap[NonEmptyVector[T]](_.toVector.toList) +} diff --git a/cats/shared/src/main/scala/com/softwaremill/diffx/cats/package.scala b/cats/src/main/scala/com/softwaremill/diffx/cats/package.scala similarity index 100% rename from cats/shared/src/main/scala/com/softwaremill/diffx/cats/package.scala rename to cats/src/main/scala/com/softwaremill/diffx/cats/package.scala diff --git a/cats/shared/src/test/scala/com/softwaremill/diffx/cats/DiffxCatsTest.scala b/cats/src/test/scala/com/softwaremill/diffx/cats/DiffxCatsTest.scala similarity index 100% rename from cats/shared/src/test/scala/com/softwaremill/diffx/cats/DiffxCatsTest.scala rename to cats/src/test/scala/com/softwaremill/diffx/cats/DiffxCatsTest.scala diff --git a/core/src/main/boilerplate/com/softwaremill/diffx/TupleInstances.scala.template b/core/src/main/boilerplate/com/softwaremill/diffx/TupleInstances.scala.template new file mode 100644 index 00000000..91a5ae65 --- /dev/null +++ b/core/src/main/boilerplate/com/softwaremill/diffx/TupleInstances.scala.template @@ -0,0 +1,21 @@ +package com.softwaremill.diffx + +trait TupleInstances { + + [2..#implicit def dTuple1[[#T1#]](implicit [#d1: Diff[T1]#]): Diff[Tuple1[[#T1#]]] = new Diff[Tuple1[[#T1#]]] { + override def apply( + left: ([#T1#]), + right: ([#T1#]), + context: DiffContext + ): DiffResult = { + val results = List([#"_1" -> d1.apply(left._1, right._1)#]).toMap + if (results.values.forall(_.isIdentical)) { + Identical(left) + } else { + DiffResultObject("Tuple1", results) + } + } + } + # + ] +} \ No newline at end of file diff --git a/core/src/main/scala-2.13+/com.softwaremill.diffx/package.scala b/core/src/main/scala-2.13+/com.softwaremill.diffx/package.scala index e1d662b3..0058ba33 100644 --- a/core/src/main/scala-2.13+/com.softwaremill.diffx/package.scala +++ b/core/src/main/scala-2.13+/com.softwaremill.diffx/package.scala @@ -1,5 +1,4 @@ package com.softwaremill -import acyclic.skipped import scala.annotation.compileTimeOnly import scala.collection.Factory diff --git a/core/src/main/scala-2.13-/com.softwaremill.diffx/package.scala b/core/src/main/scala-2.13-/com.softwaremill.diffx/package.scala index b4081154..1d3cd45f 100644 --- a/core/src/main/scala-2.13-/com.softwaremill.diffx/package.scala +++ b/core/src/main/scala-2.13-/com.softwaremill.diffx/package.scala @@ -1,5 +1,4 @@ package com.softwaremill -import acyclic.skipped import scala.annotation.compileTimeOnly import scala.collection.TraversableLike diff --git a/core/src/main/scala/com/softwaremill/diffx/Diff.scala b/core/src/main/scala/com/softwaremill/diffx/Diff.scala index b0d1d393..71636a74 100644 --- a/core/src/main/scala/com/softwaremill/diffx/Diff.scala +++ b/core/src/main/scala/com/softwaremill/diffx/Diff.scala @@ -1,39 +1,92 @@ package com.softwaremill.diffx -import acyclic.skipped +import com.softwaremill.diffx.ObjectMatcher.{IterableEntry, MapEntry} +import com.softwaremill.diffx.generic.{DiffMagnoliaDerivation, MagnoliaDerivedMacro} +import com.softwaremill.diffx.instances._ trait Diff[-T] { outer => - def apply(left: T, right: T): DiffResult = apply(left, right, Nil) - def apply(left: T, right: T, toIgnore: List[FieldPath]): DiffResult + def apply(left: T, right: T): DiffResult = apply(left, right, DiffContext.Empty) + def apply(left: T, right: T, context: DiffContext): DiffResult def contramap[R](f: R => T): Diff[R] = - (left: R, right: R, toIgnore: List[FieldPath]) => { - outer(f(left), f(right), toIgnore) + (left: R, right: R, context: DiffContext) => { + outer(f(left), f(right), context) } - def ignore[S <: T, U](path: S => U): Diff[S] = macro IgnoreMacro.ignoreMacro[S, U] + def modifyUnsafe(path: String*)(diff: Diff[_]): Diff[T] = + new Diff[T] { + override def apply(left: T, right: T, context: DiffContext): DiffResult = + outer.apply(left, right, context.merge(DiffContext(Tree.fromList(path.toList, diff), List.empty, Tree.empty))) + } - def ignoreUnsafe(fields: String*): Diff[T] = + def modifyMatcherUnsafe(path: String*)(matcher: ObjectMatcher[_]): Diff[T] = new Diff[T] { - override def apply(left: T, right: T, toIgnore: List[FieldPath]): DiffResult = - outer.apply(left, right, toIgnore ++ List(fields.toList)) + override def apply(left: T, right: T, context: DiffContext): DiffResult = + outer.apply( + left, + right, + context.merge(DiffContext(Tree.empty, List.empty, Tree.fromList(path.toList, matcher))) + ) } } -object Diff extends DiffInstances { +object Diff extends MiddlePriorityDiff with TupleInstances { def apply[T: Diff]: Diff[T] = implicitly[Diff[T]] - def identical[T]: Diff[T] = (left: T, _: T, _: List[FieldPath]) => Identical(left) + def ignored[T]: Diff[T] = (_: T, _: T, _: DiffContext) => DiffResult.Ignored def compare[T: Diff](left: T, right: T): DiffResult = apply[T].apply(left, right) /** Create a Diff instance using [[Object#equals]] */ - def useEquals[T]: Derived[Diff[T]] = Derived(Diff.fallback[T]) + def useEquals[T]: Diff[T] = Diff.fallback[T] + + def approximate[T: Numeric](epsilon: T): Diff[T] = + new ApproximateDiffForNumeric[T](epsilon) + + def derived[T]: Derived[Diff[T]] = macro MagnoliaDerivedMacro.derivedGen[T] + + implicit val diffForString: Diff[String] = new DiffForString + implicit val diffForRange: Diff[Range] = Diff.useEquals[Range] + implicit val diffForChar: Diff[Char] = Diff.useEquals[Char] + implicit val diffForBoolean: Diff[Boolean] = Diff.useEquals[Boolean] + implicit def diffForNumeric[T: Numeric]: Diff[T] = new DiffForNumeric[T] + implicit def diffForMap[K, V, C[KK, VV] <: scala.collection.Map[KK, VV]](implicit + dv: Diff[V], + dk: Diff[K], + matcher: ObjectMatcher[MapEntry[K, V]] + ): Diff[C[K, V]] = new DiffForMap[K, V, C](matcher, dk, dv) + implicit def diffForOptional[T](implicit ddt: Diff[T]): Diff[Option[T]] = new DiffForOption[T](ddt) + implicit def diffForSet[T, C[W] <: scala.collection.Set[W]](implicit + dt: Diff[T], + matcher: ObjectMatcher[T] + ): Diff[C[T]] = new DiffForSet[T, C](dt, matcher) + implicit def diffForEither[L, R](implicit ld: Diff[L], rd: Diff[R]): Diff[Either[L, R]] = + new DiffForEither[L, R](ld, rd) +} + +trait MiddlePriorityDiff extends DiffMagnoliaDerivation with LowPriorityDiff { + + implicit def diffForIterable[T, C[W] <: Iterable[W]](implicit + dt: Diff[T], + matcher: ObjectMatcher[IterableEntry[T]] + ): Diff[C[T]] = new DiffForIterable[T, C](dt, matcher) +} + +trait LowPriorityDiff { // Implicit instance of Diff[T] created from implicit Derived[Diff[T]] - implicit def anyDiff[T](implicit dd: Derived[Diff[T]]): Diff[T] = dd.value + implicit def derivedDiff[T](implicit dd: Derived[Diff[T]]): Diff[T] = dd.value + + implicit class RichDerivedDiff[T](val dd: Derived[Diff[T]]) { + def contramap[R](f: R => T): Derived[Diff[R]] = Derived(dd.value.contramap(f)) - // Implicit conversion - implicit def unwrapDerivedDiff[T](dd: Derived[Diff[T]]): Diff[T] = dd.value + def modify[U](path: T => U): DerivedDiffLens[T, U] = macro ModifyMacro.derivedModifyMacro[T, U] + def ignore[U](path: T => U): Derived[Diff[T]] = macro ModifyMacro.derivedIgnoreMacro[T, U] + } + + implicit class RichDiff[T](val d: Diff[T]) { + def modify[U](path: T => U): DiffLens[T, U] = macro ModifyMacro.modifyMacro[T, U] + def ignore[U](path: T => U): Diff[T] = macro ModifyMacro.ignoreMacro[T, U] + } } case class Derived[T](value: T) @@ -41,3 +94,32 @@ case class Derived[T](value: T) object Derived { def apply[T: Derived]: Derived[T] = implicitly[Derived[T]] } + +case class DiffLens[T, U](outer: Diff[T], path: List[String]) { + def setTo(d: Diff[U]): Diff[T] = { + outer.modifyUnsafe(path: _*)(d) + } + def ignore(): Diff[T] = outer.modifyUnsafe(path: _*)(Diff.ignored) + + def withMapMatcher[K, V](m: ObjectMatcher[(K, V)])(implicit ev1: U <:< scala.collection.Map[K, V]): Diff[T] = + outer.modifyMatcherUnsafe(path: _*)(m) + def withSetMatcher[V](m: ObjectMatcher[V])(implicit ev2: U <:< scala.collection.Set[V]): Diff[T] = + outer.modifyMatcherUnsafe(path: _*)(m) + def withListMatcher[V](m: ObjectMatcher[(Int, V)])(implicit ev3: U <:< Iterable[V]): Diff[T] = + outer.modifyMatcherUnsafe(path: _*)(m) +} +case class DerivedDiffLens[T, U](outer: Diff[T], path: List[String]) { + def setTo(d: Diff[U]): Derived[Diff[T]] = { + Derived(outer.modifyUnsafe(path: _*)(d)) + } + def ignore(): Derived[Diff[T]] = Derived(outer.modifyUnsafe(path: _*)(Diff.ignored)) + + def withMapMatcher[K, V](m: ObjectMatcher[MapEntry[K, V]])(implicit + ev1: U <:< scala.collection.Map[K, V] + ): Derived[Diff[T]] = + Derived(outer.modifyMatcherUnsafe(path: _*)(m)) + def withSetMatcher[V](m: ObjectMatcher[V])(implicit ev2: U <:< scala.collection.Set[V]): Derived[Diff[T]] = + Derived(outer.modifyMatcherUnsafe(path: _*)(m)) + def withListMatcher[V](m: ObjectMatcher[IterableEntry[V]])(implicit ev3: U <:< Iterable[V]): Derived[Diff[T]] = + Derived(outer.modifyMatcherUnsafe(path: _*)(m)) +} diff --git a/core/src/main/scala/com/softwaremill/diffx/DiffContext.scala b/core/src/main/scala/com/softwaremill/diffx/DiffContext.scala new file mode 100644 index 00000000..5fcd64bf --- /dev/null +++ b/core/src/main/scala/com/softwaremill/diffx/DiffContext.scala @@ -0,0 +1,87 @@ +package com.softwaremill.diffx + +case class DiffContext(overrides: Tree[Diff[_]], path: FieldPath, matcherOverrides: Tree[ObjectMatcher[_]]) { + def merge(other: DiffContext): DiffContext = { + DiffContext(overrides.merge(other.overrides), List.empty, matcherOverrides.merge(other.matcherOverrides)) + } + + def getOverride(label: String): Option[Diff[_]] = { + treeOverride(label, overrides) + } + + def getMatcherOverride[T]: Option[ObjectMatcher[T]] = { + matcherOverrides match { + case Tree.Leaf(v) => Some(v.asInstanceOf[ObjectMatcher[T]]) + case Tree.Node(_) => None + } + } + + private def treeOverride[T](label: String, tree: Tree[T]) = { + tree match { + case Tree.Leaf(_) => throw new IllegalStateException(s"Expected node, got leaf at $path") + case Tree.Node(tries) => getOverrideFromNode(label, tries) + } + } + + private def getOverrideFromNode[T](label: String, tries: Map[String, Tree[T]]) = { + tries.get(label) match { + case Some(Tree.Leaf(v)) => Some(v) + case _ => None + } + } + + def getNextStep(label: String): DiffContext = { + val currentPath = path :+ label + (getNextOverride(label, overrides), getNextOverride(label, matcherOverrides)) match { + case (Some(d), Some(m)) => DiffContext(d, currentPath, m) + case (None, Some(m)) => DiffContext(Tree.empty, currentPath, m) + case (Some(d), None) => DiffContext(d, currentPath, Tree.empty) + case (None, None) => DiffContext(Tree.empty, currentPath, Tree.empty) + } + } + + private def getNextOverride[T](label: String, tree: Tree[T]) = { + tree match { + case Tree.Leaf(_) => None + case Tree.Node(tries) => tries.get(label) + } + } +} + +object DiffContext { + val Empty: DiffContext = DiffContext(Tree.empty, List.empty, Tree.empty) + def atPath(path: FieldPath, diff: Diff[_]): DiffContext = Empty.copy(overrides = Tree.fromList(path, diff)) + def atPath(path: FieldPath, matcher: ObjectMatcher[_]): DiffContext = + Empty.copy(matcherOverrides = Tree.fromList(path, matcher)) +} + +sealed trait Tree[T] { + def merge(tree: Tree[T]): Tree[T] +} +object Tree { + def empty[T]: Node[T] = Tree.Node[T](Map.empty) + + case class Leaf[T](v: T) extends Tree[T] { + override def merge(tree: Tree[T]): Tree[T] = tree + } + case class Node[T](tries: Map[String, Tree[T]]) extends Tree[T] { + override def merge(tree: Tree[T]): Tree[T] = { + tree match { + case Leaf(v) => Leaf(v) + case Node(otherTries) => + val keys = tries.keySet ++ otherTries.keySet + Node(keys.map { k => + k -> ((tries.get(k), otherTries.get(k)) match { + case (Some(t1), Some(t2)) => t1.merge(t2) + case (Some(t1), None) => t1 + case (None, Some(t2)) => t2 + case (None, None) => throw new IllegalStateException("cannot happen") + }) + }.toMap) + } + } + } + def fromList[T](path: FieldPath, obj: T): Tree[T] = { + path.reverse.foldLeft(Leaf(obj): Tree[T])((acc, item) => Node(Map(item -> acc))) + } +} diff --git a/core/src/main/scala/com/softwaremill/diffx/DiffForMap.scala b/core/src/main/scala/com/softwaremill/diffx/DiffForMap.scala deleted file mode 100644 index c7ab6396..00000000 --- a/core/src/main/scala/com/softwaremill/diffx/DiffForMap.scala +++ /dev/null @@ -1,62 +0,0 @@ -package com.softwaremill.diffx -import acyclic.skipped - -import com.softwaremill.diffx.Matching._ - -private[diffx] class DiffForMap[K, V, C[KK, VV] <: scala.collection.Map[KK, VV]]( - matcher: ObjectMatcher[K], - diffKey: Diff[K], - diffValue: Diff[Option[V]] -) extends Diff[C[K, V]] { - override def apply( - left: C[K, V], - right: C[K, V], - toIgnore: List[FieldPath] - ): DiffResult = { - val MatchingResults(unMatchedLeftKeys, unMatchedRightKeys, matchedKeys) = - matching[K](left.keySet, right.keySet, matcher, diffKey, toIgnore) - val leftDiffs = this.leftDiffs(left, unMatchedLeftKeys, unMatchedRightKeys) - val rightDiffs = this.rightDiffs(right, unMatchedLeftKeys, unMatchedRightKeys) - val matchedDiffs = this.matchedDiffs(matchedKeys, left, right, toIgnore) - val diffs = leftDiffs ++ rightDiffs ++ matchedDiffs - if (diffs.forall(p => p._1.isIdentical && p._2.isIdentical)) { - Identical(left) - } else { - DiffResultMap(diffs.toMap) - } - } - - private def matchedDiffs( - matchedKeys: scala.collection.Set[(K, K)], - left: C[K, V], - right: C[K, V], - toIgnore: List[FieldPath] - ): List[(DiffResult, DiffResult)] = { - matchedKeys.map { case (lKey, rKey) => - val result = diffKey.apply(lKey, rKey) - result -> diffValue.apply(left.get(lKey), right.get(rKey), toIgnore) - }.toList - } - - private def rightDiffs( - right: C[K, V], - unMatchedLeftKeys: scala.collection.Set[K], - unMatchedRightKeys: scala.collection.Set[K] - ): List[(DiffResult, DiffResult)] = { - unMatchedRightKeys - .diff(unMatchedLeftKeys) - .map(k => DiffResultMissing(k) -> DiffResultMissing(right(k))) - .toList - } - - private def leftDiffs( - left: C[K, V], - unMatchedLeftKeys: scala.collection.Set[K], - unMatchedRightKeys: scala.collection.Set[K] - ): List[(DiffResult, DiffResult)] = { - unMatchedLeftKeys - .diff(unMatchedRightKeys) - .map(k => DiffResultAdditional(k) -> DiffResultAdditional(left(k))) - .toList - } -} diff --git a/core/src/main/scala/com/softwaremill/diffx/DiffForString.scala b/core/src/main/scala/com/softwaremill/diffx/DiffForString.scala deleted file mode 100644 index 28167026..00000000 --- a/core/src/main/scala/com/softwaremill/diffx/DiffForString.scala +++ /dev/null @@ -1,30 +0,0 @@ -package com.softwaremill.diffx -import acyclic.skipped - -class DiffForString extends Diff[String] { - override def apply(left: String, right: String, toIgnore: List[FieldPath]): DiffResult = { - val leftLines = left.split("\n").toList - val rightLines = right.split("\n").toList - val leftAsMap = leftLines.lift - val rightAsMap = rightLines.lift - val maxSize = Math.max(leftLines.length, rightLines.length) - val partialResults = (0 until maxSize).map { i => - (leftAsMap(i), rightAsMap(i)) match { - case (Some(lv), Some(rv)) => - if (lv == rv) { - Identical(lv) - } else { - DiffResultValue(lv, rv) - } - case (Some(lv), None) => DiffResultAdditional(lv) - case (None, Some(rv)) => DiffResultMissing(rv) - case (None, None) => throw new IllegalStateException("That should never happen") - } - }.toList - if (partialResults.forall(_.isIdentical)) { - Identical(left) - } else { - DiffResultString(partialResults) - } - } -} diff --git a/core/src/main/scala/com/softwaremill/diffx/DiffInstances.scala b/core/src/main/scala/com/softwaremill/diffx/DiffInstances.scala deleted file mode 100644 index 8271947c..00000000 --- a/core/src/main/scala/com/softwaremill/diffx/DiffInstances.scala +++ /dev/null @@ -1,94 +0,0 @@ -package com.softwaremill.diffx -import acyclic.skipped -import com.softwaremill.diffx.Matching._ - -import scala.collection.immutable.ListMap - -trait DiffInstances extends DiffMagnoliaDerivation { - implicit def diffForNumeric[T: Numeric]: Derived[Diff[T]] = - Derived((left: T, right: T, _: List[FieldPath]) => { - val numeric = implicitly[Numeric[T]] - if (!numeric.equiv(left, right)) { - DiffResultValue(left, right) - } else { - Identical(left) - } - }) - - implicit def diffForOption[T](implicit ddt: Diff[T]): Derived[Diff[Option[T]]] = - Derived((left: Option[T], right: Option[T], toIgnore: List[FieldPath]) => { - (left, right) match { - case (Some(l), Some(r)) => ddt.apply(l, r, toIgnore) - case (None, None) => Identical(None) - case (l, r) => DiffResultValue(l, r) - } - }) - - implicit def diffForSet[T: ObjectMatcher, C[W] <: scala.collection.Set[W]](implicit - ddt: Diff[T], - matcher: ObjectMatcher[T] - ): Derived[Diff[C[T]]] = - Derived((left: C[T], right: C[T], toIgnore: List[FieldPath]) => { - val MatchingResults(unMatchedLeftInstances, unMatchedRightInstances, matchedInstances) = - matching[T](left.toSet, right.toSet, matcher, ddt, toIgnore) - val leftDiffs = unMatchedLeftInstances - .diff(unMatchedRightInstances) - .map(DiffResultAdditional(_)) - .toList - val rightDiffs = unMatchedRightInstances - .diff(unMatchedLeftInstances) - .map(DiffResultMissing(_)) - .toList - val matchedDiffs = matchedInstances.map { case (l, r) => ddt(l, r, toIgnore) }.toList - diffResultSet(left, leftDiffs, rightDiffs, matchedDiffs) - }) - - private def diffResultSet[T]( - left: T, - leftDiffs: List[DiffResult], - rightDiffs: List[DiffResult], - matchedDiffs: List[DiffResult] - ): DiffResult = { - val diffs = leftDiffs ++ rightDiffs ++ matchedDiffs - if (diffs.forall(_.isIdentical)) { - Identical(left) - } else { - DiffResultSet(diffs) - } - } - - implicit def diffForIterable[T, C[W] <: Iterable[W]](implicit - ddot: Diff[Option[T]] - ): Derived[Diff[C[T]]] = - Derived((left: C[T], right: C[T], toIgnore: List[FieldPath]) => { - val indexes = Range(0, Math.max(left.size, right.size)) - val leftAsMap = left.toList.lift - val rightAsMap = right.toList.lift - val differences = ListMap(indexes.map { index => - index.toString -> (ddot.apply(leftAsMap(index), rightAsMap(index), toIgnore) match { - case DiffResultValue(Some(v), None) => DiffResultAdditional(v) - case DiffResultValue(None, Some(v)) => DiffResultMissing(v) - case d => d - }) - }: _*) - - if (differences.values.forall(_.isIdentical)) { - Identical(left) - } else { - DiffResultObject( - "List", - differences - ) - } - }) - - implicit def diffForMap[K, V, C[KK, VV] <: scala.collection.Map[KK, VV]](implicit - ddot: Diff[Option[V]], - ddk: Diff[K], - matcher: ObjectMatcher[K] - ): Derived[Diff[C[K, V]]] = Derived(new DiffForMap[K, V, C](matcher, ddk, ddot)) - - implicit val diffForString: Derived[Diff[String]] = Derived(new DiffForString) - - implicit val diffForRange: Derived[Diff[Range]] = Derived(Diff.fallback[Range]) -} diff --git a/core/src/main/scala/com/softwaremill/diffx/DiffMagnoliaDerivation.scala b/core/src/main/scala/com/softwaremill/diffx/DiffMagnoliaDerivation.scala deleted file mode 100644 index 5ca339fc..00000000 --- a/core/src/main/scala/com/softwaremill/diffx/DiffMagnoliaDerivation.scala +++ /dev/null @@ -1,58 +0,0 @@ -package com.softwaremill.diffx -import acyclic.skipped -import magnolia._ - -import scala.collection.immutable.ListMap -import scala.language.experimental.macros - -trait DiffMagnoliaDerivation extends LowPriority { - type Typeclass[T] = Derived[Diff[T]] - - def combine[T](ctx: ReadOnlyCaseClass[Typeclass, T]): Derived[Diff[T]] = - Derived(new Diff[T] { - override def apply(left: T, right: T, toIgnore: List[FieldPath]): DiffResult = { - val map = ListMap(ctx.parameters.map { p => - val lType = p.dereference(left) - val pType = p.dereference(right) - if (toIgnore.contains(List(p.label))) { - p.label -> Identical(lType) - } else { - val nestedIgnore = - if (toIgnore.exists(_.headOption.exists(h => h == p.label))) toIgnore.map(_.drop(1)) else Nil - p.label -> p.typeclass.value(lType, pType, nestedIgnore) - } - }: _*) - if (map.values.forall(p => p.isIdentical)) { - Identical(left) - } else { - DiffResultObject(ctx.typeName.short, map) - } - } - }) - - def dispatch[T](ctx: SealedTrait[Typeclass, T]): Derived[Diff[T]] = - Derived({ (left: T, right: T, toIgnore: List[FieldPath]) => - { - val lType = ctx.dispatch(left)(a => a) - val rType = ctx.dispatch(right)(a => a) - if (lType == rType) { - lType.typeclass.value(lType.cast(left), lType.cast(right), toIgnore) - } else { - DiffResultValue(lType.typeName.full, rType.typeName.full) - } - } - }) - - implicit def gen[T]: Derived[Diff[T]] = macro Magnolia.gen[T] -} - -trait LowPriority { - def fallback[T]: Derived[Diff[T]] = - Derived((left: T, right: T, toIgnore: List[FieldPath]) => { - if (left != right) { - DiffResultValue(left, right) - } else { - Identical(left) - } - }) -} diff --git a/core/src/main/scala/com/softwaremill/diffx/DiffResult.scala b/core/src/main/scala/com/softwaremill/diffx/DiffResult.scala index b701b2f9..0fb9f667 100644 --- a/core/src/main/scala/com/softwaremill/diffx/DiffResult.scala +++ b/core/src/main/scala/com/softwaremill/diffx/DiffResult.scala @@ -1,75 +1,131 @@ package com.softwaremill.diffx -import acyclic.skipped + import DiffResult._ trait DiffResult extends Product with Serializable { def isIdentical: Boolean - def show(implicit c: ConsoleColorConfig): String = showIndented(indentLevel) + def show(renderIdentical: Boolean = true)(implicit c: ConsoleColorConfig): String = + showIndented(indentLevel, renderIdentical) + + private[diffx] def showIndented(indent: Int, renderIdentical: Boolean)(implicit c: ConsoleColorConfig): String - private[diffx] def showIndented(indent: Int)(implicit c: ConsoleColorConfig): String + protected def i(indent: Int): String = " " * indent } object DiffResult { private[diffx] final val indentLevel = 5 + val Ignored: IdenticalValue[Any] = IdenticalValue("") } -case class DiffResultObject(name: String, fields: Map[String, DiffResult]) extends DiffResultDifferent { - override private[diffx] def showIndented(indent: Int)(implicit c: ConsoleColorConfig): String = { - val showFields = - fields.map(f => s"${i(indent)}${defaultColor(s"${f._1}:")} ${f._2.showIndented(indent + indentLevel)}") +case class DiffResultObject(name: String, fields: Map[String, DiffResult]) extends DiffResult { + override private[diffx] def showIndented(indent: Int, renderIdentical: Boolean)(implicit + c: ConsoleColorConfig + ): String = { + val showFields = fields + .filter { case (_, v) => + renderIdentical || !v.isIdentical + } + .map { case (field, value) => + renderField(indent, field) + renderValue(indent, renderIdentical, value) + } defaultColor(s"$name(") + s"\n${showFields.mkString(defaultColor(",") + "\n")}" + defaultColor(")") } + + private def renderValue(indent: Int, renderIdentical: Boolean, value: DiffResult)(implicit + c: ConsoleColorConfig + ) = { + s"${value.showIndented(indent + indentLevel, renderIdentical)}" + } + + private def renderField(indent: Int, field: String)(implicit + c: ConsoleColorConfig + ) = { + s"${i(indent)}${defaultColor(s"$field: ")}" + } + + override def isIdentical: Boolean = fields.values.forall(_.isIdentical) } -case class DiffResultMap(fields: Map[DiffResult, DiffResult]) extends DiffResultDifferent { - override private[diffx] def showIndented(indent: Int)(implicit c: ConsoleColorConfig): String = { - val showFields = - fields.map(f => - s"${i(indent)}${defaultColor(s"${f._1.showIndented(indent + indentLevel)}")}" + defaultColor(": ") + s"${f._2 - .showIndented(indent + indentLevel)}" - ) +case class DiffResultMap(entries: Map[DiffResult, DiffResult]) extends DiffResult { + override private[diffx] def showIndented(indent: Int, renderIdentical: Boolean)(implicit + c: ConsoleColorConfig + ): String = { + val showFields = entries + .filter { case (k, v) => + renderIdentical || !v.isIdentical || !k.isIdentical + } + .map { case (k, v) => + val key = renderKey(indent, renderIdentical, k) + val separator = defaultColor(": ") + val value = renderValue(indent, renderIdentical, v) + key + separator + value + } defaultColor("Map(") + s"\n${showFields.mkString(defaultColor(",") + "\n")}" + defaultColor(")") } -} -case class DiffResultSet(diffs: List[DiffResult]) extends DiffResultDifferent { - override private[diffx] def showIndented(indent: Int)(implicit c: ConsoleColorConfig): String = { - val showFields = diffs.map(f => s"${i(indent)}${f.showIndented(indent + indentLevel)}") - showFields.mkString(defaultColor("Set(\n"), ",\n", defaultColor(")")) + private def renderValue(indent: Int, renderIdentical: Boolean, value: DiffResult)(implicit + c: ConsoleColorConfig + ) = { + value.showIndented(indent + indentLevel, renderIdentical) + } + + private def renderKey(indent: Int, renderIdentical: Boolean, key: DiffResult)(implicit + c: ConsoleColorConfig + ) = { + s"${i(indent)}${defaultColor(s"${key.showIndented(indent + indentLevel, renderIdentical)}")}" } + + override def isIdentical: Boolean = entries.forall { case (k, v) => k.isIdentical && v.isIdentical } } -case class DiffResultString(diffs: List[DiffResult]) extends DiffResultDifferent { - override private[diffx] def showIndented(indent: Int)(implicit c: ConsoleColorConfig): String = { - s"${diffs.map(_.showIndented(indent)).mkString("\n")}" +case class DiffResultSet(diffs: List[DiffResult]) extends DiffResult { + override private[diffx] def showIndented(indent: Int, renderIdentical: Boolean)(implicit + c: ConsoleColorConfig + ): String = { + val showFields = diffs + .filter(df => renderIdentical || !df.isIdentical) + .map(f => s"${i(indent)}${f.showIndented(indent + indentLevel, renderIdentical)}") + showFields.mkString(defaultColor("Set(\n"), ",\n", defaultColor(")")) } + + override def isIdentical: Boolean = diffs.forall(_.isIdentical) } -trait DiffResultDifferent extends DiffResult { - override def isIdentical: Boolean = false +case class DiffResultString(diffs: List[DiffResult]) extends DiffResult { + override private[diffx] def showIndented(indent: Int, renderIdentical: Boolean)(implicit + c: ConsoleColorConfig + ): String = { + s"${diffs.map(_.showIndented(indent, renderIdentical)).mkString("\n")}" + } - protected def i(indent: Int): String = " " * indent + override def isIdentical: Boolean = diffs.forall(_.isIdentical) } -case class DiffResultValue[T](left: T, right: T) extends DiffResultDifferent { - override def showIndented(indent: Int)(implicit c: ConsoleColorConfig): String = showChange(s"$left", s"$right") +case class DiffResultValue[T](left: T, right: T) extends DiffResult { + override def showIndented(indent: Int, renderIdentical: Boolean)(implicit c: ConsoleColorConfig): String = + showChange(s"$left", s"$right") + + override def isIdentical: Boolean = false } -case class Identical[T](value: T) extends DiffResult { +case class IdenticalValue[T](value: T) extends DiffResult { override def isIdentical: Boolean = true - override def showIndented(indent: Int)(implicit c: ConsoleColorConfig): String = defaultColor(s"$value") + override def showIndented(indent: Int, renderIdentical: Boolean)(implicit c: ConsoleColorConfig): String = + defaultColor(s"$value") } -case class DiffResultMissing[T](value: T) extends DiffResultDifferent { - override def showIndented(indent: Int)(implicit c: ConsoleColorConfig): String = { +case class DiffResultMissing[T](value: T) extends DiffResult { + override def showIndented(indent: Int, renderIdentical: Boolean)(implicit c: ConsoleColorConfig): String = { rightColor(s"$value") } + override def isIdentical: Boolean = false } -case class DiffResultAdditional[T](value: T) extends DiffResultDifferent { - override def showIndented(indent: Int)(implicit c: ConsoleColorConfig): String = { +case class DiffResultAdditional[T](value: T) extends DiffResult { + override def showIndented(indent: Int, renderIdentical: Boolean)(implicit c: ConsoleColorConfig): String = { leftColor(s"$value") } + override def isIdentical: Boolean = false } diff --git a/core/src/main/scala/com/softwaremill/diffx/DiffxSupport.scala b/core/src/main/scala/com/softwaremill/diffx/DiffxSupport.scala index 671da61c..486eaa94 100644 --- a/core/src/main/scala/com/softwaremill/diffx/DiffxSupport.scala +++ b/core/src/main/scala/com/softwaremill/diffx/DiffxSupport.scala @@ -1,5 +1,4 @@ package com.softwaremill.diffx -import acyclic.skipped import scala.annotation.compileTimeOnly import com.softwaremill.diffx.DiffxSupport._ @@ -8,6 +7,16 @@ trait DiffxSupport extends DiffxEitherSupport with DiffxConsoleSupport with Diff type FieldPath = List[String] def compare[T](left: T, right: T)(implicit d: Diff[T]): DiffResult = d.apply(left, right) + + private[diffx] def nullGuard[T](left: T, right: T)(compareNotNull: (T, T) => DiffResult): DiffResult = { + if ((left == null && right != null) || (left != null && right == null)) { + DiffResultValue(left, right) + } else if (left == null && right == null) { + IdenticalValue(null) + } else { + compareNotNull(left, right) + } + } } object DiffxSupport { diff --git a/core/src/main/scala/com/softwaremill/diffx/Matching.scala b/core/src/main/scala/com/softwaremill/diffx/Matching.scala index 8b92bcaa..a8e37a17 100644 --- a/core/src/main/scala/com/softwaremill/diffx/Matching.scala +++ b/core/src/main/scala/com/softwaremill/diffx/Matching.scala @@ -1,5 +1,4 @@ package com.softwaremill.diffx -import acyclic.skipped private[diffx] object Matching { private[diffx] def matching[T]( @@ -7,11 +6,24 @@ private[diffx] object Matching { right: scala.collection.Set[T], matcher: ObjectMatcher[T], diff: Diff[T], - toIgnore: List[FieldPath] + context: DiffContext ): MatchingResults[T] = { val matchedKeys = left.flatMap(l => right.collectFirst { - case r if matcher.isSameObject(l, r) || diff(l, r, toIgnore).isIdentical => l -> r + case r if matcher.isSameObject(l, r) || diff(l, r, context).isIdentical => l -> r + } + ) + MatchingResults(left.diff(matchedKeys.map(_._1)), right.diff(matchedKeys.map(_._2)), matchedKeys) + } + + private[diffx] def matching[T]( + left: scala.collection.Set[T], + right: scala.collection.Set[T], + matcher: ObjectMatcher[T] + ): MatchingResults[T] = { + val matchedKeys = left.flatMap(l => + right.collectFirst { + case r if matcher.isSameObject(l, r) => l -> r } ) MatchingResults(left.diff(matchedKeys.map(_._1)), right.diff(matchedKeys.map(_._2)), matchedKeys) diff --git a/core/src/main/scala/com/softwaremill/diffx/IgnoreMacro.scala b/core/src/main/scala/com/softwaremill/diffx/ModifyMacro.scala similarity index 50% rename from core/src/main/scala/com/softwaremill/diffx/IgnoreMacro.scala rename to core/src/main/scala/com/softwaremill/diffx/ModifyMacro.scala index 98625139..b9f1d35e 100644 --- a/core/src/main/scala/com/softwaremill/diffx/IgnoreMacro.scala +++ b/core/src/main/scala/com/softwaremill/diffx/ModifyMacro.scala @@ -3,25 +3,62 @@ package com.softwaremill.diffx import scala.annotation.tailrec import scala.reflect.macros.blackbox -object IgnoreMacro { +object ModifyMacro { private val ShapeInfo = "Path must have shape: _.field1.field2.each.field3.(...)" - def ignoreMacro[T: c.WeakTypeTag, U: c.WeakTypeTag]( + def derivedModifyMacro[T: c.WeakTypeTag, U: c.WeakTypeTag]( c: blackbox.Context - )(path: c.Expr[T => U]): c.Tree = applyIgnored[T, U](c)(ignoredFromPathMacro(c)(path)) + )(path: c.Expr[T => U]): c.Tree = + applyDerivedModified[T, U](c)(modifiedFromPathMacro(c)(path)) - private def applyIgnored[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)( + private def applyDerivedModified[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)( path: c.Expr[List[String]] ): c.Tree = { + import c.universe._ + q"""com.softwaremill.diffx.DerivedDiffLens(${c.prefix}.dd.value, $path)""" + } + + def derivedIgnoreMacro[T: c.WeakTypeTag, U: c.WeakTypeTag]( + c: blackbox.Context + )(path: c.Expr[T => U]): c.Tree = + applyIgnoredModified[T, U](c)(modifiedFromPathMacro(c)(path)) + + private def applyIgnoredModified[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)( + path: c.Expr[List[String]] + ): c.Tree = { + import c.universe._ + val lens = applyDerivedModified[T, U](c)(path) + q"""$lens.ignore()""" + } + + def ignoreMacro[T: c.WeakTypeTag, U: c.WeakTypeTag]( + c: blackbox.Context + )(path: c.Expr[T => U]): c.Tree = applyIgnored[T, U](c)(modifiedFromPathMacro(c)(path)) + + private def applyIgnored[T: c.WeakTypeTag, U: c.WeakTypeTag]( + c: blackbox.Context + )(path: c.Expr[List[String]]): c.Tree = { + import c.universe._ + val lens = applyModified[T, U](c)(path) + q"""$lens.ignore()""" + } + + def modifyMacro[T: c.WeakTypeTag, U: c.WeakTypeTag]( + c: blackbox.Context + )(path: c.Expr[T => U]): c.Tree = applyModified[T, U](c)(modifiedFromPathMacro(c)(path)) + + private def applyModified[T: c.WeakTypeTag, U: c.WeakTypeTag]( + c: blackbox.Context + )(path: c.Expr[List[String]]): c.Tree = { import c.universe._ q"""{ - ${c.prefix}.ignoreUnsafe($path:_*) + com.softwaremill.diffx.DiffLens(${c.prefix}.d, $path) }""" } /** Converts path to list of strings */ - def ignoredFromPathMacro[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)( + def modifiedFromPathMacro[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)( path: c.Expr[T => U] ): c.Expr[List[String]] = { import c.universe._ @@ -60,11 +97,17 @@ object IgnoreMacro { case q"($arg) => $pathBody " => collectPathElements(pathBody, Nil) case _ => c.abort(c.enclosingPosition, s"$ShapeInfo, got: ${path.tree}") } - - c.Expr[List[String]](q"${pathEls.collect { case TermPathElement(c) => - c.decodedName.toString - }}") + c.Expr[List[String]]( + q"(${pathEls.collect { + case TermPathElement(c) => c.decodedName.toString + case FunctorPathElement(_, method, _ @_*) if method.decodedName.toString == "eachLeft" => + method.decodedName.toString + case FunctorPathElement(_, method, _ @_*) if method.decodedName.toString == "eachRight" => + method.decodedName.toString + }})" + ) } - private[diffx] def ignoredFromPath[T, U](path: T => U): List[String] = macro ignoredFromPathMacro[T, U] + private[diffx] def modifiedFromPath[T, U](path: T => U): List[String] = + macro modifiedFromPathMacro[T, U] } diff --git a/core/src/main/scala/com/softwaremill/diffx/ObjectMatcher.scala b/core/src/main/scala/com/softwaremill/diffx/ObjectMatcher.scala index 57da2980..483a8c6c 100644 --- a/core/src/main/scala/com/softwaremill/diffx/ObjectMatcher.scala +++ b/core/src/main/scala/com/softwaremill/diffx/ObjectMatcher.scala @@ -1,12 +1,40 @@ package com.softwaremill.diffx -/* - Used to pair elements within unordered containers like sets - */ +/** Defines how the elements within collections are paired + * @tparam T + */ trait ObjectMatcher[T] { def isSameObject(left: T, right: T): Boolean } -object ObjectMatcher { +object ObjectMatcher extends LowPriorityObjectMatcher { + def apply[T: ObjectMatcher]: ObjectMatcher[T] = implicitly[ObjectMatcher[T]] + + /** Given product of type T and its property U, match that products using U's objectMatcher */ + def by[T, U: ObjectMatcher](f: T => U): ObjectMatcher[T] = (left: T, right: T) => + ObjectMatcher[U].isSameObject(f(left), f(right)) + + /** Given key-value (K,V) pairs match them using V's objectMatcher */ + def byValue[K, V: ObjectMatcher]: ObjectMatcher[MapEntry[K, V]] = ObjectMatcher.by[MapEntry[K, V], V](_.value) + + /** Given key-value (K,V) pairs, where V is a type of product and U is a property of V, match them using U's objectMatcher */ + def byValue[K, V, U: ObjectMatcher](f: V => U): ObjectMatcher[MapEntry[K, V]] = + ObjectMatcher.byValue[K, V](ObjectMatcher.by[V, U](f)) + + implicit def optionMatcher[T: ObjectMatcher]: ObjectMatcher[Option[T]] = (left: Option[T], right: Option[T]) => { + (left, right) match { + case (Some(l), Some(r)) => ObjectMatcher[T].isSameObject(l, r) + case _ => false + } + } + + /** Given key-value (K,V) pairs, match them using K's objectMatcher */ + implicit def byKey[K: ObjectMatcher, V]: ObjectMatcher[MapEntry[K, V]] = ObjectMatcher.by[MapEntry[K, V], K](_.key) + + type IterableEntry[T] = MapEntry[Int, T] + case class MapEntry[K, V](key: K, value: V) +} + +trait LowPriorityObjectMatcher { implicit def default[T]: ObjectMatcher[T] = (l: T, r: T) => l == r } diff --git a/core/src/main/scala/com/softwaremill/diffx/TupleInstances.scala b/core/src/main/scala/com/softwaremill/diffx/TupleInstances.scala new file mode 100644 index 00000000..0312208f --- /dev/null +++ b/core/src/main/scala/com/softwaremill/diffx/TupleInstances.scala @@ -0,0 +1,818 @@ +package com.softwaremill.diffx + +trait TupleInstances { + + implicit def dTuple2[T1, T2](implicit d1: Diff[T1], d2: Diff[T2]): Diff[Tuple2[T1, T2]] = new Diff[Tuple2[T1, T2]] { + override def apply( + left: (T1, T2), + right: (T1, T2), + context: DiffContext + ): DiffResult = { + val results = List("_1" -> d1.apply(left._1, right._1), "_2" -> d2.apply(left._2, right._2)).toMap + DiffResultObject("Tuple2", results) + } + } + + implicit def dTuple3[T1, T2, T3](implicit d1: Diff[T1], d2: Diff[T2], d3: Diff[T3]): Diff[Tuple3[T1, T2, T3]] = + new Diff[Tuple3[T1, T2, T3]] { + override def apply( + left: (T1, T2, T3), + right: (T1, T2, T3), + context: DiffContext + ): DiffResult = { + val results = List( + "_1" -> d1.apply(left._1, right._1), + "_2" -> d2.apply(left._2, right._2), + "_3" -> d3.apply(left._3, right._3) + ).toMap + DiffResultObject("Tuple3", results) + } + } + + implicit def dTuple4[T1, T2, T3, T4](implicit + d1: Diff[T1], + d2: Diff[T2], + d3: Diff[T3], + d4: Diff[T4] + ): Diff[Tuple4[T1, T2, T3, T4]] = new Diff[Tuple4[T1, T2, T3, T4]] { + override def apply( + left: (T1, T2, T3, T4), + right: (T1, T2, T3, T4), + context: DiffContext + ): DiffResult = { + val results = List( + "_1" -> d1.apply(left._1, right._1), + "_2" -> d2.apply(left._2, right._2), + "_3" -> d3.apply(left._3, right._3), + "_4" -> d4.apply(left._4, right._4) + ).toMap + DiffResultObject("Tuple4", results) + } + } + + implicit def dTuple5[T1, T2, T3, T4, T5](implicit + d1: Diff[T1], + d2: Diff[T2], + d3: Diff[T3], + d4: Diff[T4], + d5: Diff[T5] + ): Diff[Tuple5[T1, T2, T3, T4, T5]] = new Diff[Tuple5[T1, T2, T3, T4, T5]] { + override def apply( + left: (T1, T2, T3, T4, T5), + right: (T1, T2, T3, T4, T5), + context: DiffContext + ): DiffResult = { + val results = List( + "_1" -> d1.apply(left._1, right._1), + "_2" -> d2.apply(left._2, right._2), + "_3" -> d3.apply(left._3, right._3), + "_4" -> d4.apply(left._4, right._4), + "_5" -> d5.apply(left._5, right._5) + ).toMap + DiffResultObject("Tuple5", results) + } + } + + implicit def dTuple6[T1, T2, T3, T4, T5, T6](implicit + d1: Diff[T1], + d2: Diff[T2], + d3: Diff[T3], + d4: Diff[T4], + d5: Diff[T5], + d6: Diff[T6] + ): Diff[Tuple6[T1, T2, T3, T4, T5, T6]] = new Diff[Tuple6[T1, T2, T3, T4, T5, T6]] { + override def apply( + left: (T1, T2, T3, T4, T5, T6), + right: (T1, T2, T3, T4, T5, T6), + context: DiffContext + ): DiffResult = { + val results = List( + "_1" -> d1.apply(left._1, right._1), + "_2" -> d2.apply(left._2, right._2), + "_3" -> d3.apply(left._3, right._3), + "_4" -> d4.apply(left._4, right._4), + "_5" -> d5.apply(left._5, right._5), + "_6" -> d6.apply(left._6, right._6) + ).toMap + DiffResultObject("Tuple6", results) + } + } + + implicit def dTuple7[T1, T2, T3, T4, T5, T6, T7](implicit + d1: Diff[T1], + d2: Diff[T2], + d3: Diff[T3], + d4: Diff[T4], + d5: Diff[T5], + d6: Diff[T6], + d7: Diff[T7] + ): Diff[Tuple7[T1, T2, T3, T4, T5, T6, T7]] = new Diff[Tuple7[T1, T2, T3, T4, T5, T6, T7]] { + override def apply( + left: (T1, T2, T3, T4, T5, T6, T7), + right: (T1, T2, T3, T4, T5, T6, T7), + context: DiffContext + ): DiffResult = { + val results = List( + "_1" -> d1.apply(left._1, right._1), + "_2" -> d2.apply(left._2, right._2), + "_3" -> d3.apply(left._3, right._3), + "_4" -> d4.apply(left._4, right._4), + "_5" -> d5.apply(left._5, right._5), + "_6" -> d6.apply(left._6, right._6), + "_7" -> d7.apply(left._7, right._7) + ).toMap + DiffResultObject("Tuple7", results) + } + } + + implicit def dTuple8[T1, T2, T3, T4, T5, T6, T7, T8](implicit + d1: Diff[T1], + d2: Diff[T2], + d3: Diff[T3], + d4: Diff[T4], + d5: Diff[T5], + d6: Diff[T6], + d7: Diff[T7], + d8: Diff[T8] + ): Diff[Tuple8[T1, T2, T3, T4, T5, T6, T7, T8]] = new Diff[Tuple8[T1, T2, T3, T4, T5, T6, T7, T8]] { + override def apply( + left: (T1, T2, T3, T4, T5, T6, T7, T8), + right: (T1, T2, T3, T4, T5, T6, T7, T8), + context: DiffContext + ): DiffResult = { + val results = List( + "_1" -> d1.apply(left._1, right._1), + "_2" -> d2.apply(left._2, right._2), + "_3" -> d3.apply(left._3, right._3), + "_4" -> d4.apply(left._4, right._4), + "_5" -> d5.apply(left._5, right._5), + "_6" -> d6.apply(left._6, right._6), + "_7" -> d7.apply(left._7, right._7), + "_8" -> d8.apply(left._8, right._8) + ).toMap + DiffResultObject("Tuple8", results) + } + } + + implicit def dTuple9[T1, T2, T3, T4, T5, T6, T7, T8, T9](implicit + d1: Diff[T1], + d2: Diff[T2], + d3: Diff[T3], + d4: Diff[T4], + d5: Diff[T5], + d6: Diff[T6], + d7: Diff[T7], + d8: Diff[T8], + d9: Diff[T9] + ): Diff[Tuple9[T1, T2, T3, T4, T5, T6, T7, T8, T9]] = new Diff[Tuple9[T1, T2, T3, T4, T5, T6, T7, T8, T9]] { + override def apply( + left: (T1, T2, T3, T4, T5, T6, T7, T8, T9), + right: (T1, T2, T3, T4, T5, T6, T7, T8, T9), + context: DiffContext + ): DiffResult = { + val results = List( + "_1" -> d1.apply(left._1, right._1), + "_2" -> d2.apply(left._2, right._2), + "_3" -> d3.apply(left._3, right._3), + "_4" -> d4.apply(left._4, right._4), + "_5" -> d5.apply(left._5, right._5), + "_6" -> d6.apply(left._6, right._6), + "_7" -> d7.apply(left._7, right._7), + "_8" -> d8.apply(left._8, right._8), + "_9" -> d9.apply(left._9, right._9) + ).toMap + DiffResultObject("Tuple9", results) + } + } + + implicit def dTuple10[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10](implicit + d1: Diff[T1], + d2: Diff[T2], + d3: Diff[T3], + d4: Diff[T4], + d5: Diff[T5], + d6: Diff[T6], + d7: Diff[T7], + d8: Diff[T8], + d9: Diff[T9], + d10: Diff[T10] + ): Diff[Tuple10[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]] = + new Diff[Tuple10[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]] { + override def apply( + left: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10), + right: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10), + context: DiffContext + ): DiffResult = { + val results = List( + "_1" -> d1.apply(left._1, right._1), + "_2" -> d2.apply(left._2, right._2), + "_3" -> d3.apply(left._3, right._3), + "_4" -> d4.apply(left._4, right._4), + "_5" -> d5.apply(left._5, right._5), + "_6" -> d6.apply(left._6, right._6), + "_7" -> d7.apply(left._7, right._7), + "_8" -> d8.apply(left._8, right._8), + "_9" -> d9.apply(left._9, right._9), + "_10" -> d10.apply(left._10, right._10) + ).toMap + DiffResultObject("Tuple10", results) + } + } + + implicit def dTuple11[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11](implicit + d1: Diff[T1], + d2: Diff[T2], + d3: Diff[T3], + d4: Diff[T4], + d5: Diff[T5], + d6: Diff[T6], + d7: Diff[T7], + d8: Diff[T8], + d9: Diff[T9], + d10: Diff[T10], + d11: Diff[T11] + ): Diff[Tuple11[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11]] = + new Diff[Tuple11[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11]] { + override def apply( + left: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11), + right: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11), + context: DiffContext + ): DiffResult = { + val results = List( + "_1" -> d1.apply(left._1, right._1), + "_2" -> d2.apply(left._2, right._2), + "_3" -> d3.apply(left._3, right._3), + "_4" -> d4.apply(left._4, right._4), + "_5" -> d5.apply(left._5, right._5), + "_6" -> d6.apply(left._6, right._6), + "_7" -> d7.apply(left._7, right._7), + "_8" -> d8.apply(left._8, right._8), + "_9" -> d9.apply(left._9, right._9), + "_10" -> d10.apply(left._10, right._10), + "_11" -> d11.apply(left._11, right._11) + ).toMap + DiffResultObject("Tuple11", results) + } + } + + implicit def dTuple12[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12](implicit + d1: Diff[T1], + d2: Diff[T2], + d3: Diff[T3], + d4: Diff[T4], + d5: Diff[T5], + d6: Diff[T6], + d7: Diff[T7], + d8: Diff[T8], + d9: Diff[T9], + d10: Diff[T10], + d11: Diff[T11], + d12: Diff[T12] + ): Diff[Tuple12[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12]] = + new Diff[Tuple12[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12]] { + override def apply( + left: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12), + right: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12), + context: DiffContext + ): DiffResult = { + val results = List( + "_1" -> d1.apply(left._1, right._1), + "_2" -> d2.apply(left._2, right._2), + "_3" -> d3.apply(left._3, right._3), + "_4" -> d4.apply(left._4, right._4), + "_5" -> d5.apply(left._5, right._5), + "_6" -> d6.apply(left._6, right._6), + "_7" -> d7.apply(left._7, right._7), + "_8" -> d8.apply(left._8, right._8), + "_9" -> d9.apply(left._9, right._9), + "_10" -> d10.apply(left._10, right._10), + "_11" -> d11.apply(left._11, right._11), + "_12" -> d12.apply(left._12, right._12) + ).toMap + DiffResultObject("Tuple12", results) + } + } + + implicit def dTuple13[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13](implicit + d1: Diff[T1], + d2: Diff[T2], + d3: Diff[T3], + d4: Diff[T4], + d5: Diff[T5], + d6: Diff[T6], + d7: Diff[T7], + d8: Diff[T8], + d9: Diff[T9], + d10: Diff[T10], + d11: Diff[T11], + d12: Diff[T12], + d13: Diff[T13] + ): Diff[Tuple13[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13]] = + new Diff[Tuple13[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13]] { + override def apply( + left: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13), + right: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13), + context: DiffContext + ): DiffResult = { + val results = List( + "_1" -> d1.apply(left._1, right._1), + "_2" -> d2.apply(left._2, right._2), + "_3" -> d3.apply(left._3, right._3), + "_4" -> d4.apply(left._4, right._4), + "_5" -> d5.apply(left._5, right._5), + "_6" -> d6.apply(left._6, right._6), + "_7" -> d7.apply(left._7, right._7), + "_8" -> d8.apply(left._8, right._8), + "_9" -> d9.apply(left._9, right._9), + "_10" -> d10.apply(left._10, right._10), + "_11" -> d11.apply(left._11, right._11), + "_12" -> d12.apply(left._12, right._12), + "_13" -> d13.apply(left._13, right._13) + ).toMap + if (results.values.forall(_.isIdentical)) { + IdenticalValue(left) + } else { + DiffResultObject("Tuple13", results) + } + } + } + + implicit def dTuple14[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14](implicit + d1: Diff[T1], + d2: Diff[T2], + d3: Diff[T3], + d4: Diff[T4], + d5: Diff[T5], + d6: Diff[T6], + d7: Diff[T7], + d8: Diff[T8], + d9: Diff[T9], + d10: Diff[T10], + d11: Diff[T11], + d12: Diff[T12], + d13: Diff[T13], + d14: Diff[T14] + ): Diff[Tuple14[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14]] = + new Diff[Tuple14[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14]] { + override def apply( + left: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14), + right: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14), + context: DiffContext + ): DiffResult = { + val results = List( + "_1" -> d1.apply(left._1, right._1), + "_2" -> d2.apply(left._2, right._2), + "_3" -> d3.apply(left._3, right._3), + "_4" -> d4.apply(left._4, right._4), + "_5" -> d5.apply(left._5, right._5), + "_6" -> d6.apply(left._6, right._6), + "_7" -> d7.apply(left._7, right._7), + "_8" -> d8.apply(left._8, right._8), + "_9" -> d9.apply(left._9, right._9), + "_10" -> d10.apply(left._10, right._10), + "_11" -> d11.apply(left._11, right._11), + "_12" -> d12.apply(left._12, right._12), + "_13" -> d13.apply(left._13, right._13), + "_14" -> d14.apply(left._14, right._14) + ).toMap + DiffResultObject("Tuple14", results) + } + } + + implicit def dTuple15[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15](implicit + d1: Diff[T1], + d2: Diff[T2], + d3: Diff[T3], + d4: Diff[T4], + d5: Diff[T5], + d6: Diff[T6], + d7: Diff[T7], + d8: Diff[T8], + d9: Diff[T9], + d10: Diff[T10], + d11: Diff[T11], + d12: Diff[T12], + d13: Diff[T13], + d14: Diff[T14], + d15: Diff[T15] + ): Diff[Tuple15[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15]] = + new Diff[Tuple15[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15]] { + override def apply( + left: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15), + right: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15), + context: DiffContext + ): DiffResult = { + val results = List( + "_1" -> d1.apply(left._1, right._1), + "_2" -> d2.apply(left._2, right._2), + "_3" -> d3.apply(left._3, right._3), + "_4" -> d4.apply(left._4, right._4), + "_5" -> d5.apply(left._5, right._5), + "_6" -> d6.apply(left._6, right._6), + "_7" -> d7.apply(left._7, right._7), + "_8" -> d8.apply(left._8, right._8), + "_9" -> d9.apply(left._9, right._9), + "_10" -> d10.apply(left._10, right._10), + "_11" -> d11.apply(left._11, right._11), + "_12" -> d12.apply(left._12, right._12), + "_13" -> d13.apply(left._13, right._13), + "_14" -> d14.apply(left._14, right._14), + "_15" -> d15.apply(left._15, right._15) + ).toMap + DiffResultObject("Tuple15", results) + } + } + + implicit def dTuple16[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16](implicit + d1: Diff[T1], + d2: Diff[T2], + d3: Diff[T3], + d4: Diff[T4], + d5: Diff[T5], + d6: Diff[T6], + d7: Diff[T7], + d8: Diff[T8], + d9: Diff[T9], + d10: Diff[T10], + d11: Diff[T11], + d12: Diff[T12], + d13: Diff[T13], + d14: Diff[T14], + d15: Diff[T15], + d16: Diff[T16] + ): Diff[Tuple16[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16]] = + new Diff[Tuple16[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16]] { + override def apply( + left: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16), + right: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16), + context: DiffContext + ): DiffResult = { + val results = List( + "_1" -> d1.apply(left._1, right._1), + "_2" -> d2.apply(left._2, right._2), + "_3" -> d3.apply(left._3, right._3), + "_4" -> d4.apply(left._4, right._4), + "_5" -> d5.apply(left._5, right._5), + "_6" -> d6.apply(left._6, right._6), + "_7" -> d7.apply(left._7, right._7), + "_8" -> d8.apply(left._8, right._8), + "_9" -> d9.apply(left._9, right._9), + "_10" -> d10.apply(left._10, right._10), + "_11" -> d11.apply(left._11, right._11), + "_12" -> d12.apply(left._12, right._12), + "_13" -> d13.apply(left._13, right._13), + "_14" -> d14.apply(left._14, right._14), + "_15" -> d15.apply(left._15, right._15), + "_16" -> d16.apply(left._16, right._16) + ).toMap + DiffResultObject("Tuple16", results) + } + } + + implicit def dTuple17[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17](implicit + d1: Diff[T1], + d2: Diff[T2], + d3: Diff[T3], + d4: Diff[T4], + d5: Diff[T5], + d6: Diff[T6], + d7: Diff[T7], + d8: Diff[T8], + d9: Diff[T9], + d10: Diff[T10], + d11: Diff[T11], + d12: Diff[T12], + d13: Diff[T13], + d14: Diff[T14], + d15: Diff[T15], + d16: Diff[T16], + d17: Diff[T17] + ): Diff[Tuple17[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17]] = + new Diff[Tuple17[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17]] { + override def apply( + left: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17), + right: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17), + context: DiffContext + ): DiffResult = { + val results = List( + "_1" -> d1.apply(left._1, right._1), + "_2" -> d2.apply(left._2, right._2), + "_3" -> d3.apply(left._3, right._3), + "_4" -> d4.apply(left._4, right._4), + "_5" -> d5.apply(left._5, right._5), + "_6" -> d6.apply(left._6, right._6), + "_7" -> d7.apply(left._7, right._7), + "_8" -> d8.apply(left._8, right._8), + "_9" -> d9.apply(left._9, right._9), + "_10" -> d10.apply(left._10, right._10), + "_11" -> d11.apply(left._11, right._11), + "_12" -> d12.apply(left._12, right._12), + "_13" -> d13.apply(left._13, right._13), + "_14" -> d14.apply(left._14, right._14), + "_15" -> d15.apply(left._15, right._15), + "_16" -> d16.apply(left._16, right._16), + "_17" -> d17.apply(left._17, right._17) + ).toMap + DiffResultObject("Tuple17", results) + } + } + + implicit def dTuple18[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18](implicit + d1: Diff[T1], + d2: Diff[T2], + d3: Diff[T3], + d4: Diff[T4], + d5: Diff[T5], + d6: Diff[T6], + d7: Diff[T7], + d8: Diff[T8], + d9: Diff[T9], + d10: Diff[T10], + d11: Diff[T11], + d12: Diff[T12], + d13: Diff[T13], + d14: Diff[T14], + d15: Diff[T15], + d16: Diff[T16], + d17: Diff[T17], + d18: Diff[T18] + ): Diff[Tuple18[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18]] = + new Diff[Tuple18[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18]] { + override def apply( + left: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18), + right: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18), + context: DiffContext + ): DiffResult = { + val results = List( + "_1" -> d1.apply(left._1, right._1), + "_2" -> d2.apply(left._2, right._2), + "_3" -> d3.apply(left._3, right._3), + "_4" -> d4.apply(left._4, right._4), + "_5" -> d5.apply(left._5, right._5), + "_6" -> d6.apply(left._6, right._6), + "_7" -> d7.apply(left._7, right._7), + "_8" -> d8.apply(left._8, right._8), + "_9" -> d9.apply(left._9, right._9), + "_10" -> d10.apply(left._10, right._10), + "_11" -> d11.apply(left._11, right._11), + "_12" -> d12.apply(left._12, right._12), + "_13" -> d13.apply(left._13, right._13), + "_14" -> d14.apply(left._14, right._14), + "_15" -> d15.apply(left._15, right._15), + "_16" -> d16.apply(left._16, right._16), + "_17" -> d17.apply(left._17, right._17), + "_18" -> d18.apply(left._18, right._18) + ).toMap + DiffResultObject("Tuple18", results) + } + } + + implicit def dTuple19[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19](implicit + d1: Diff[T1], + d2: Diff[T2], + d3: Diff[T3], + d4: Diff[T4], + d5: Diff[T5], + d6: Diff[T6], + d7: Diff[T7], + d8: Diff[T8], + d9: Diff[T9], + d10: Diff[T10], + d11: Diff[T11], + d12: Diff[T12], + d13: Diff[T13], + d14: Diff[T14], + d15: Diff[T15], + d16: Diff[T16], + d17: Diff[T17], + d18: Diff[T18], + d19: Diff[T19] + ): Diff[Tuple19[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19]] = + new Diff[Tuple19[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19]] { + override def apply( + left: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19), + right: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19), + context: DiffContext + ): DiffResult = { + val results = List( + "_1" -> d1.apply(left._1, right._1), + "_2" -> d2.apply(left._2, right._2), + "_3" -> d3.apply(left._3, right._3), + "_4" -> d4.apply(left._4, right._4), + "_5" -> d5.apply(left._5, right._5), + "_6" -> d6.apply(left._6, right._6), + "_7" -> d7.apply(left._7, right._7), + "_8" -> d8.apply(left._8, right._8), + "_9" -> d9.apply(left._9, right._9), + "_10" -> d10.apply(left._10, right._10), + "_11" -> d11.apply(left._11, right._11), + "_12" -> d12.apply(left._12, right._12), + "_13" -> d13.apply(left._13, right._13), + "_14" -> d14.apply(left._14, right._14), + "_15" -> d15.apply(left._15, right._15), + "_16" -> d16.apply(left._16, right._16), + "_17" -> d17.apply(left._17, right._17), + "_18" -> d18.apply(left._18, right._18), + "_19" -> d19.apply(left._19, right._19) + ).toMap + DiffResultObject("Tuple19", results) + } + } + + implicit def dTuple20[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20]( + implicit + d1: Diff[T1], + d2: Diff[T2], + d3: Diff[T3], + d4: Diff[T4], + d5: Diff[T5], + d6: Diff[T6], + d7: Diff[T7], + d8: Diff[T8], + d9: Diff[T9], + d10: Diff[T10], + d11: Diff[T11], + d12: Diff[T12], + d13: Diff[T13], + d14: Diff[T14], + d15: Diff[T15], + d16: Diff[T16], + d17: Diff[T17], + d18: Diff[T18], + d19: Diff[T19], + d20: Diff[T20] + ): Diff[Tuple20[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20]] = + new Diff[Tuple20[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20]] { + override def apply( + left: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20), + right: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20), + context: DiffContext + ): DiffResult = { + val results = List( + "_1" -> d1.apply(left._1, right._1), + "_2" -> d2.apply(left._2, right._2), + "_3" -> d3.apply(left._3, right._3), + "_4" -> d4.apply(left._4, right._4), + "_5" -> d5.apply(left._5, right._5), + "_6" -> d6.apply(left._6, right._6), + "_7" -> d7.apply(left._7, right._7), + "_8" -> d8.apply(left._8, right._8), + "_9" -> d9.apply(left._9, right._9), + "_10" -> d10.apply(left._10, right._10), + "_11" -> d11.apply(left._11, right._11), + "_12" -> d12.apply(left._12, right._12), + "_13" -> d13.apply(left._13, right._13), + "_14" -> d14.apply(left._14, right._14), + "_15" -> d15.apply(left._15, right._15), + "_16" -> d16.apply(left._16, right._16), + "_17" -> d17.apply(left._17, right._17), + "_18" -> d18.apply(left._18, right._18), + "_19" -> d19.apply(left._19, right._19), + "_20" -> d20.apply(left._20, right._20) + ).toMap + DiffResultObject("Tuple20", results) + } + } + + implicit def dTuple21[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21]( + implicit + d1: Diff[T1], + d2: Diff[T2], + d3: Diff[T3], + d4: Diff[T4], + d5: Diff[T5], + d6: Diff[T6], + d7: Diff[T7], + d8: Diff[T8], + d9: Diff[T9], + d10: Diff[T10], + d11: Diff[T11], + d12: Diff[T12], + d13: Diff[T13], + d14: Diff[T14], + d15: Diff[T15], + d16: Diff[T16], + d17: Diff[T17], + d18: Diff[T18], + d19: Diff[T19], + d20: Diff[T20], + d21: Diff[T21] + ): Diff[Tuple21[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21]] = + new Diff[Tuple21[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21]] { + override def apply( + left: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21), + right: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21), + context: DiffContext + ): DiffResult = { + val results = List( + "_1" -> d1.apply(left._1, right._1), + "_2" -> d2.apply(left._2, right._2), + "_3" -> d3.apply(left._3, right._3), + "_4" -> d4.apply(left._4, right._4), + "_5" -> d5.apply(left._5, right._5), + "_6" -> d6.apply(left._6, right._6), + "_7" -> d7.apply(left._7, right._7), + "_8" -> d8.apply(left._8, right._8), + "_9" -> d9.apply(left._9, right._9), + "_10" -> d10.apply(left._10, right._10), + "_11" -> d11.apply(left._11, right._11), + "_12" -> d12.apply(left._12, right._12), + "_13" -> d13.apply(left._13, right._13), + "_14" -> d14.apply(left._14, right._14), + "_15" -> d15.apply(left._15, right._15), + "_16" -> d16.apply(left._16, right._16), + "_17" -> d17.apply(left._17, right._17), + "_18" -> d18.apply(left._18, right._18), + "_19" -> d19.apply(left._19, right._19), + "_20" -> d20.apply(left._20, right._20), + "_21" -> d21.apply(left._21, right._21) + ).toMap + DiffResultObject("Tuple21", results) + } + } + + implicit def dTuple22[ + T1, + T2, + T3, + T4, + T5, + T6, + T7, + T8, + T9, + T10, + T11, + T12, + T13, + T14, + T15, + T16, + T17, + T18, + T19, + T20, + T21, + T22 + ](implicit + d1: Diff[T1], + d2: Diff[T2], + d3: Diff[T3], + d4: Diff[T4], + d5: Diff[T5], + d6: Diff[T6], + d7: Diff[T7], + d8: Diff[T8], + d9: Diff[T9], + d10: Diff[T10], + d11: Diff[T11], + d12: Diff[T12], + d13: Diff[T13], + d14: Diff[T14], + d15: Diff[T15], + d16: Diff[T16], + d17: Diff[T17], + d18: Diff[T18], + d19: Diff[T19], + d20: Diff[T20], + d21: Diff[T21], + d22: Diff[T22] + ): Diff[ + Tuple22[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22] + ] = new Diff[ + Tuple22[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22] + ] { + override def apply( + left: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22), + right: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22), + context: DiffContext + ): DiffResult = { + val results = List( + "_1" -> d1.apply(left._1, right._1), + "_2" -> d2.apply(left._2, right._2), + "_3" -> d3.apply(left._3, right._3), + "_4" -> d4.apply(left._4, right._4), + "_5" -> d5.apply(left._5, right._5), + "_6" -> d6.apply(left._6, right._6), + "_7" -> d7.apply(left._7, right._7), + "_8" -> d8.apply(left._8, right._8), + "_9" -> d9.apply(left._9, right._9), + "_10" -> d10.apply(left._10, right._10), + "_11" -> d11.apply(left._11, right._11), + "_12" -> d12.apply(left._12, right._12), + "_13" -> d13.apply(left._13, right._13), + "_14" -> d14.apply(left._14, right._14), + "_15" -> d15.apply(left._15, right._15), + "_16" -> d16.apply(left._16, right._16), + "_17" -> d17.apply(left._17, right._17), + "_18" -> d18.apply(left._18, right._18), + "_19" -> d19.apply(left._19, right._19), + "_20" -> d20.apply(left._20, right._20), + "_21" -> d21.apply(left._21, right._21), + "_22" -> d22.apply(left._22, right._22) + ).toMap + DiffResultObject("Tuple22", results) + } + } + +} diff --git a/core/src/main/scala/com/softwaremill/diffx/generic/DiffMagnoliaDerivation.scala b/core/src/main/scala/com/softwaremill/diffx/generic/DiffMagnoliaDerivation.scala new file mode 100644 index 00000000..f77456ed --- /dev/null +++ b/core/src/main/scala/com/softwaremill/diffx/generic/DiffMagnoliaDerivation.scala @@ -0,0 +1,53 @@ +package com.softwaremill.diffx.generic + +import com.softwaremill.diffx.{ + Diff, + DiffContext, + DiffResultObject, + DiffResultValue, + FieldPath, + IdenticalValue, + nullGuard +} +import magnolia._ + +import scala.collection.immutable.ListMap + +trait DiffMagnoliaDerivation extends LowPriority { + type Typeclass[T] = Diff[T] + + def combine[T](ctx: ReadOnlyCaseClass[Typeclass, T]): Diff[T] = { (left: T, right: T, context: DiffContext) => + nullGuard(left, right) { (left, right) => + val map = ListMap(ctx.parameters.map { p => + val lType = p.dereference(left) + val pType = p.dereference(right) + val fieldDiff = context.getOverride(p.label).map(_.asInstanceOf[Diff[p.PType]]).getOrElse(p.typeclass) + p.label -> fieldDiff(lType, pType, context.getNextStep(p.label)) + }: _*) + DiffResultObject(ctx.typeName.short, map) + } + } + + def dispatch[T](ctx: SealedTrait[Typeclass, T]): Diff[T] = { (left: T, right: T, context: DiffContext) => + nullGuard(left, right) { (left, right) => + val lType = ctx.dispatch(left)(a => a) + val rType = ctx.dispatch(right)(a => a) + if (lType == rType) { + lType.typeclass(lType.cast(left), lType.cast(right), context) + } else { + DiffResultValue(lType.typeName.full, rType.typeName.full) + } + } + } +} + +trait LowPriority { + def fallback[T]: Diff[T] = + (left: T, right: T, context: DiffContext) => { + if (left != right) { + DiffResultValue(left, right) + } else { + IdenticalValue(left) + } + } +} diff --git a/core/src/main/scala/com/softwaremill/diffx/generic/MagnoliaDerivedMacro.scala b/core/src/main/scala/com/softwaremill/diffx/generic/MagnoliaDerivedMacro.scala new file mode 100644 index 00000000..db45b547 --- /dev/null +++ b/core/src/main/scala/com/softwaremill/diffx/generic/MagnoliaDerivedMacro.scala @@ -0,0 +1,14 @@ +package com.softwaremill.diffx.generic + +import com.softwaremill.diffx.Derived +import magnolia.Magnolia +import scala.language.experimental.macros + +object MagnoliaDerivedMacro { + import scala.reflect.macros.whitebox + + def derivedGen[T: c.WeakTypeTag](c: whitebox.Context): c.Expr[Derived[T]] = { + import c.universe._ + c.Expr[Derived[T]](q"com.softwaremill.diffx.Derived(${Magnolia.gen[T](c)(implicitly[c.WeakTypeTag[T]])})") + } +} diff --git a/core/src/main/scala/com/softwaremill/diffx/generic/auto/AutoDerivation.scala b/core/src/main/scala/com/softwaremill/diffx/generic/auto/AutoDerivation.scala new file mode 100644 index 00000000..2a2b3240 --- /dev/null +++ b/core/src/main/scala/com/softwaremill/diffx/generic/auto/AutoDerivation.scala @@ -0,0 +1,12 @@ +package com.softwaremill.diffx.generic + +import com.softwaremill.diffx.{Derived, Diff} + +package object auto extends AutoDerivation + +trait AutoDerivation extends DiffMagnoliaDerivation { + implicit def diffForCaseClass[T]: Derived[Diff[T]] = macro MagnoliaDerivedMacro.derivedGen[T] + + // Implicit conversion + implicit def unwrapDerivedDiff[T](dd: Derived[Diff[T]]): Diff[T] = dd.value +} diff --git a/core/src/main/scala/com/softwaremill/diffx/instances/ApproximateDiffForNumeric.scala b/core/src/main/scala/com/softwaremill/diffx/instances/ApproximateDiffForNumeric.scala new file mode 100644 index 00000000..e784bc23 --- /dev/null +++ b/core/src/main/scala/com/softwaremill/diffx/instances/ApproximateDiffForNumeric.scala @@ -0,0 +1,14 @@ +package com.softwaremill.diffx.instances + +import com.softwaremill.diffx._ + +private[diffx] class ApproximateDiffForNumeric[T: Numeric](epsilon: T) extends Diff[T] { + override def apply(left: T, right: T, context: DiffContext): DiffResult = { + val numeric = implicitly[Numeric[T]] + if (numeric.lt(epsilon, numeric.abs(numeric.minus(left, right)))) { + DiffResultValue(left, right) + } else { + IdenticalValue(left) + } + } +} diff --git a/core/src/main/scala/com/softwaremill/diffx/instances/DiffForEither.scala b/core/src/main/scala/com/softwaremill/diffx/instances/DiffForEither.scala new file mode 100644 index 00000000..7404ada7 --- /dev/null +++ b/core/src/main/scala/com/softwaremill/diffx/instances/DiffForEither.scala @@ -0,0 +1,17 @@ +package com.softwaremill.diffx.instances + +import com.softwaremill.diffx.{Diff, DiffContext, DiffResult, DiffResultValue} + +private[diffx] class DiffForEither[L, R](ld: Diff[L], rd: Diff[R]) extends Diff[Either[L, R]] { + override def apply( + left: Either[L, R], + right: Either[L, R], + context: DiffContext + ): DiffResult = { + (left, right) match { + case (Left(v1), Left(v2)) => ld.apply(v1, v2, context.getNextStep("eachLeft")) + case (Right(v1), Right(v2)) => rd.apply(v1, v2, context.getNextStep("eachRight")) + case (v1, v2) => DiffResultValue(v1, v2) + } + } +} diff --git a/core/src/main/scala/com/softwaremill/diffx/instances/DiffForIterable.scala b/core/src/main/scala/com/softwaremill/diffx/instances/DiffForIterable.scala new file mode 100644 index 00000000..6ff225df --- /dev/null +++ b/core/src/main/scala/com/softwaremill/diffx/instances/DiffForIterable.scala @@ -0,0 +1,38 @@ +package com.softwaremill.diffx.instances + +import com.softwaremill.diffx.Matching.{MatchingResults, matching} +import com.softwaremill.diffx.ObjectMatcher.{IterableEntry, MapEntry} +import com.softwaremill.diffx._ + +import scala.collection.immutable.{ListMap, ListSet} + +private[diffx] class DiffForIterable[T, C[W] <: Iterable[W]]( + dt: Diff[T], + matcher: ObjectMatcher[IterableEntry[T]] +) extends Diff[C[T]] { + override def apply(left: C[T], right: C[T], context: DiffContext): DiffResult = nullGuard(left, right) { + (left, right) => + val keys = Range(0, Math.max(left.size, right.size)) + + val leftAsMap = left.toList.lift + val rightAsMap = right.toList.lift + val leftv2 = ListSet(keys.map(i => i -> leftAsMap(i)): _*).collect { case (k, Some(v)) => MapEntry(k, v) } + val rightv2 = ListSet(keys.map(i => i -> rightAsMap(i)): _*).collect { case (k, Some(v)) => MapEntry(k, v) } + + val adjustedMatcher = context.getMatcherOverride[IterableEntry[T]].getOrElse(matcher) + val MatchingResults(unMatchedLeftInstances, unMatchedRightInstances, matchedInstances) = + matching(leftv2, rightv2, adjustedMatcher) + val leftDiffs = unMatchedLeftInstances + .diff(unMatchedRightInstances) + .collectFirst { case MapEntry(k, v) => k -> DiffResultAdditional(v) } + .toList + val rightDiffs = unMatchedRightInstances + .diff(unMatchedLeftInstances) + .collectFirst { case MapEntry(k, v) => k -> DiffResultMissing(v) } + .toList + val matchedDiffs = matchedInstances.map { case (l, r) => l.key -> dt(l.value, r.value, context) }.toList + + val diffs = ListMap((matchedDiffs ++ leftDiffs ++ rightDiffs).map { case (k, v) => k.toString -> v }: _*) + DiffResultObject("List", diffs) + } +} diff --git a/core/src/main/scala/com/softwaremill/diffx/instances/DiffForMap.scala b/core/src/main/scala/com/softwaremill/diffx/instances/DiffForMap.scala new file mode 100644 index 00000000..5af3e599 --- /dev/null +++ b/core/src/main/scala/com/softwaremill/diffx/instances/DiffForMap.scala @@ -0,0 +1,42 @@ +package com.softwaremill.diffx.instances + +import com.softwaremill.diffx.Matching._ +import com.softwaremill.diffx.ObjectMatcher.MapEntry +import com.softwaremill.diffx._ + +private[diffx] class DiffForMap[K, V, C[KK, VV] <: scala.collection.Map[KK, VV]]( + matcher: ObjectMatcher[MapEntry[K, V]], + diffKey: Diff[K], + diffValue: Diff[V] +) extends Diff[C[K, V]] { + override def apply( + left: C[K, V], + right: C[K, V], + context: DiffContext + ): DiffResult = nullGuard(left, right) { (left, right) => + val adjustedMatcher = context.getMatcherOverride[MapEntry[K, V]].getOrElse(matcher) + val MatchingResults(unMatchedLeftKeys, unMatchedRightKeys, matchedKeys) = + matching( + left.map { case (k, v) => MapEntry.apply(k, v) }.toSet, + right.map { case (k, v) => MapEntry.apply(k, v) }.toSet, + adjustedMatcher, + diffKey.contramap[MapEntry[K, V]](_.key), + context + ) + + val leftDiffs = unMatchedLeftKeys + .diff(unMatchedRightKeys) + .collectFirst { case MapEntry(k, v) => DiffResultAdditional(k) -> DiffResultAdditional(v) } + .toList + val rightDiffs = unMatchedRightKeys + .diff(unMatchedLeftKeys) + .collectFirst { case MapEntry(k, v) => DiffResultMissing(k) -> DiffResultMissing(v) } + .toList + val matchedDiffs = matchedKeys.map { case (l, r) => + diffKey(l.key, r.key) -> diffValue(l.value, r.value, context) + }.toList + + val diffs = leftDiffs ++ rightDiffs ++ matchedDiffs + DiffResultMap(diffs.toMap) + } +} diff --git a/core/src/main/scala/com/softwaremill/diffx/instances/DiffForNumeric.scala b/core/src/main/scala/com/softwaremill/diffx/instances/DiffForNumeric.scala new file mode 100644 index 00000000..801e77a7 --- /dev/null +++ b/core/src/main/scala/com/softwaremill/diffx/instances/DiffForNumeric.scala @@ -0,0 +1,14 @@ +package com.softwaremill.diffx.instances + +import com.softwaremill.diffx._ + +private[diffx] class DiffForNumeric[T: Numeric] extends Diff[T] { + override def apply(left: T, right: T, context: DiffContext): DiffResult = { + val numeric = implicitly[Numeric[T]] + if (!numeric.equiv(left, right)) { + DiffResultValue(left, right) + } else { + IdenticalValue(left) + } + } +} diff --git a/core/src/main/scala/com/softwaremill/diffx/instances/DiffForOption.scala b/core/src/main/scala/com/softwaremill/diffx/instances/DiffForOption.scala new file mode 100644 index 00000000..6c76bd62 --- /dev/null +++ b/core/src/main/scala/com/softwaremill/diffx/instances/DiffForOption.scala @@ -0,0 +1,13 @@ +package com.softwaremill.diffx.instances + +import com.softwaremill.diffx._ + +private[diffx] class DiffForOption[T](dt: Diff[T]) extends Diff[Option[T]] { + override def apply(left: Option[T], right: Option[T], context: DiffContext): DiffResult = { + (left, right) match { + case (Some(l), Some(r)) => dt.apply(l, r, context) + case (None, None) => IdenticalValue(None) + case (l, r) => DiffResultValue(l, r) + } + } +} diff --git a/core/src/main/scala/com/softwaremill/diffx/instances/DiffForSet.scala b/core/src/main/scala/com/softwaremill/diffx/instances/DiffForSet.scala new file mode 100644 index 00000000..18e9e5fa --- /dev/null +++ b/core/src/main/scala/com/softwaremill/diffx/instances/DiffForSet.scala @@ -0,0 +1,34 @@ +package com.softwaremill.diffx.instances + +import com.softwaremill.diffx.Matching.{MatchingResults, matching} +import com.softwaremill.diffx._ + +private[diffx] class DiffForSet[T, C[W] <: scala.collection.Set[W]](dt: Diff[T], matcher: ObjectMatcher[T]) + extends Diff[C[T]] { + override def apply(left: C[T], right: C[T], context: DiffContext): DiffResult = nullGuard(left, right) { + (left, right) => + val adjustedMatcher = context.getMatcherOverride[T].getOrElse(matcher) + val MatchingResults(unMatchedLeftInstances, unMatchedRightInstances, matchedInstances) = + matching[T](left.toSet, right.toSet, adjustedMatcher, dt, context) + val leftDiffs = unMatchedLeftInstances + .diff(unMatchedRightInstances) + .map(DiffResultAdditional(_)) + .toList + val rightDiffs = unMatchedRightInstances + .diff(unMatchedLeftInstances) + .map(DiffResultMissing(_)) + .toList + val matchedDiffs = matchedInstances.map { case (l, r) => dt(l, r, context) }.toList + diffResultSet(left, leftDiffs, rightDiffs, matchedDiffs) + } + + private def diffResultSet( + left: C[T], + leftDiffs: List[DiffResult], + rightDiffs: List[DiffResult], + matchedDiffs: List[DiffResult] + ): DiffResult = { + val diffs = leftDiffs ++ rightDiffs ++ matchedDiffs + DiffResultSet(diffs) + } +} diff --git a/core/src/main/scala/com/softwaremill/diffx/instances/DiffForString.scala b/core/src/main/scala/com/softwaremill/diffx/instances/DiffForString.scala new file mode 100644 index 00000000..c8a82db2 --- /dev/null +++ b/core/src/main/scala/com/softwaremill/diffx/instances/DiffForString.scala @@ -0,0 +1,32 @@ +package com.softwaremill.diffx.instances + +import com.softwaremill.diffx._ + +private[diffx] class DiffForString extends Diff[String] { + override def apply(left: String, right: String, context: DiffContext): DiffResult = nullGuard(left, right) { + (left, right) => + val leftLines = left.split("\n").toList + val rightLines = right.split("\n").toList + val leftAsMap = leftLines.lift + val rightAsMap = rightLines.lift + val maxSize = Math.max(leftLines.length, rightLines.length) + val partialResults = (0 until maxSize).map { i => + (leftAsMap(i), rightAsMap(i)) match { + case (Some(lv), Some(rv)) => + if (lv == rv) { + IdenticalValue(lv) + } else { + DiffResultValue(lv, rv) + } + case (Some(lv), None) => DiffResultAdditional(lv) + case (None, Some(rv)) => DiffResultMissing(rv) + case (None, None) => throw new IllegalStateException("That should never happen") + } + }.toList + if (partialResults.forall(_.isIdentical)) { + IdenticalValue(left) + } else { + DiffResultString(partialResults) + } + } +} diff --git a/core/src/test/scala/com/softwaremill/diffx/DiffIgnoreIntTest.scala b/core/src/test/scala/com/softwaremill/diffx/DiffIgnoreIntTest.scala deleted file mode 100644 index 4e44c42c..00000000 --- a/core/src/test/scala/com/softwaremill/diffx/DiffIgnoreIntTest.scala +++ /dev/null @@ -1,33 +0,0 @@ -package com.softwaremill.diffx - -import java.time.Instant - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class DiffIgnoreIntTest extends AnyFlatSpec with Matchers { - val instant: Instant = Instant.now() - val p1 = Person("p1", 22, instant) - val p2 = Person("p2", 11, instant) - - it should "allow importing and exporting implicits" in { - implicit val d: Diff[Person] = Derived[Diff[Person]].value.ignore(_.name) - compare(p1, p2) shouldBe DiffResultObject( - "Person", - Map("name" -> Identical("p1"), "age" -> DiffResultValue(22, 11), "in" -> Identical(instant)) - ) - } - - it should "allow importing and exporting implicits using macro on derived instance" in { - implicit val d: Diff[Person] = Derived[Diff[Person]].ignore(_.name) - compare(p1, p2) shouldBe DiffResultObject( - "Person", - Map("name" -> Identical("p1"), "age" -> DiffResultValue(22, 11), "in" -> Identical(instant)) - ) - } - - it should "allow calling ignore multiple times" in { - implicit val d: Diff[Person] = Derived[Diff[Person]].ignore[Person, String](_.name).ignore[Person, Int](_.age) - compare(p1, p2) shouldBe Identical(p1) - } -} diff --git a/core/src/test/scala/com/softwaremill/diffx/DiffResultTest.scala b/core/src/test/scala/com/softwaremill/diffx/DiffResultTest.scala deleted file mode 100644 index d4e608cd..00000000 --- a/core/src/test/scala/com/softwaremill/diffx/DiffResultTest.scala +++ /dev/null @@ -1,88 +0,0 @@ -package com.softwaremill.diffx - -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers - -class DiffResultTest extends AnyFreeSpec with Matchers with DiffxConsoleSupport { - implicit val colorConfig: ConsoleColorConfig = - ConsoleColorConfig(default = identity, arrow = identity, right = identity, left = identity) - - "diff set output" - { - "it should show a simple difference" in { - val output = DiffResultSet(List(Identical("a"), DiffResultValue("1", "2"))).show - output shouldBe - s"""Set( - | a, - | 1 -> 2)""".stripMargin - } - - "it should show an indented difference" in { - val output = DiffResultSet(List(Identical("a"), DiffResultValue("1", "2"))).showIndented(5) - output shouldBe - s"""Set( - | a, - | 1 -> 2)""".stripMargin - } - - "it should show a nested list difference" in { - val output = DiffResultSet(List(Identical("a"), DiffResultSet(List(Identical("b"))))).show - output shouldBe - s"""Set( - | a, - | Set( - | b))""".stripMargin - } - - "it should show null" in { - val output = DiffResultSet(List(Identical(null), DiffResultValue(null, null))).show - output shouldBe - s"""Set( - | null, - | null -> null)""".stripMargin - } - } - - "diff map output" - { - "it should show a simple diff" in { - val output = - DiffResultMap(Map(Identical("a") -> DiffResultValue(1, 2), DiffResultMissing("b") -> DiffResultMissing(3))).show - output shouldBe - s"""Map( - | a: 1 -> 2, - | b: 3)""".stripMargin - } - - "it should show an indented diff" in { - val output = - DiffResultMap(Map(Identical("a") -> DiffResultValue(1, 2), DiffResultMissing("b") -> DiffResultMissing(3))) - .showIndented(5) - output shouldBe - s"""Map( - | a: 1 -> 2, - | b: 3)""".stripMargin - } - - "it should show a nested diff" in { - val output = - DiffResultMap(Map(Identical("a") -> DiffResultMap(Map(Identical("b") -> DiffResultValue(1, 2))))).show - output shouldBe - s"""Map( - | a: Map( - | b: 1 -> 2))""".stripMargin - } - } - - "diff object output" - { - "it should show an indented diff with plus and minus signs" in { - val colorConfigWithPlusMinus: ConsoleColorConfig = - ConsoleColorConfig(default = identity, arrow = identity, right = s => "+" + s, left = s => "-" + s) - - val output = DiffResultObject("List", Map("0" -> DiffResultValue(1234, 123), "1" -> DiffResultMissing(1234))) - .showIndented(5)(colorConfigWithPlusMinus) - output shouldBe - s"""List( - | 0: -1234 -> +123, - | 1: +1234)""".stripMargin - } - } -} diff --git a/core/src/test/scala/com/softwaremill/diffx/DiffTest.scala b/core/src/test/scala/com/softwaremill/diffx/DiffTest.scala deleted file mode 100644 index fb8e93d8..00000000 --- a/core/src/test/scala/com/softwaremill/diffx/DiffTest.scala +++ /dev/null @@ -1,549 +0,0 @@ -package com.softwaremill.diffx - -import java.time.Instant -import java.util.UUID - -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers - -import scala.collection.immutable.ListMap - -class DiffTest extends AnyFreeSpec with Matchers { - private val instant: Instant = Instant.now() - val p1 = Person("p1", 22, instant) - val p2 = Person("p2", 11, instant) - - "simple value" - { - "diff" in { - compare(1, 2) shouldBe DiffResultValue(1, 2) - } - "identity" in { - compare(1, 1) shouldBe Identical(1) - } - "contravariant" in { - compare(Some(1), Option(1)) shouldBe Identical(1) - } - } - - "products" - { - "identity" in { - compare(p1, p1) shouldBe Identical(p1) - } - - "diff" in { - compare(p1, p2) shouldBe DiffResultObject( - "Person", - Map( - "name" -> DiffResultString(List(DiffResultValue(p1.name, p2.name))), - "age" -> DiffResultValue(p1.age, p2.age), - "in" -> Identical(instant) - ) - ) - } - - "ignoring given fields" in { - val d = Diff[Person].ignoreUnsafe("name").ignoreUnsafe("age") - val p3 = p2.copy(in = Instant.now()) - compare(p1, p3)(d) shouldBe DiffResultObject( - "Person", - Map( - "name" -> Identical(p1.name), - "age" -> Identical(p1.age), - "in" -> DiffResultValue(p1.in, p3.in) - ) - ) - } - - "nested products" in { - val f1 = Family(p1, p2) - val f2 = Family(p1, p1) - compare(f1, f2) shouldBe DiffResultObject( - "Family", - Map( - "first" -> Identical(p1), - "second" -> DiffResultObject( - "Person", - Map( - "name" -> DiffResultString(List(DiffResultValue(p2.name, p1.name))), - "age" -> DiffResultValue(p2.age, p1.age), - "in" -> Identical(instant) - ) - ) - ) - ) - } - - "nested products ignoring nested fields" in { - val f1 = Family(p1, p2) - val f2 = Family(p1, p1) - val d = Diff[Family].ignoreUnsafe("second", "name") - compare(f1, f2)(d) shouldBe DiffResultObject( - "Family", - Map( - "first" -> Identical(p1), - "second" -> DiffResultObject( - "Person", - Map( - "name" -> Identical(p2.name), - "age" -> DiffResultValue(p2.age, p1.age), - "in" -> Identical(instant) - ) - ) - ) - ) - } - - "nested products ignoring fields only in given path" in { - val p1p = p1.copy(name = "other") - val f1 = Family(p1, p2) - val f2 = Family(p1p, p2.copy(name = "other")) - val d = Diff[Family].ignoreUnsafe("second", "name") - compare(f1, f2)(d) shouldBe DiffResultObject( - "Family", - Map( - "first" -> DiffResultObject( - "Person", - Map( - "name" -> DiffResultString(List(DiffResultValue(p1.name, p1p.name))), - "age" -> Identical(p1.age), - "in" -> Identical(instant) - ) - ), - "second" -> Identical(p2) - ) - ) - } - - "nested products ignoring nested products" in { - val f1 = Family(p1, p2) - val f2 = Family(p1, p1) - val d = Diff[Family].ignoreUnsafe("second") - compare(f1, f2)(d) shouldBe Identical(f1) - } - - "list of products" in { - val o1 = Organization(List(p1, p2)) - val o2 = Organization(List(p1, p1, p1)) - compare(o1, o2) shouldBe DiffResultObject( - "Organization", - Map( - "people" -> DiffResultObject( - "List", - Map( - "0" -> Identical(p1), - "1" -> DiffResultObject( - "Person", - Map( - "name" -> DiffResultString(List(DiffResultValue(p2.name, p1.name))), - "age" -> DiffResultValue(p2.age, p1.age), - "in" -> Identical(instant) - ) - ), - "2" -> DiffResultMissing(Person(p1.name, p1.age, instant)) - ) - ) - ) - ) - } - } - - "coproducts" - { - val right: Foo = Foo( - Bar("asdf", 5), - List(123, 1234), - Some(Bar("asdf", 5)) - ) - val left: Foo = Foo( - Bar("asdf", 66), - List(1234), - Some(right) - ) - - "sealed trait objects" - { - "identity" in { - compare[TsDirection](TsDirection.Outgoing, TsDirection.Outgoing) shouldBe an[Identical[_]] - } - "diff" in { - compare[TsDirection](TsDirection.Outgoing, TsDirection.Incoming) shouldBe DiffResultValue( - "com.softwaremill.diffx.TsDirection.Outgoing", - "com.softwaremill.diffx.TsDirection.Incoming" - ) - } - } - - "identity" in { - compare(left, left) shouldBe an[Identical[_]] - } - - "diff" in { - compare(left, right) shouldBe DiffResultObject( - "Foo", - Map( - "bar" -> DiffResultObject("Bar", Map("s" -> Identical("asdf"), "i" -> DiffResultValue(66, 5))), - "b" -> DiffResultObject("List", Map("0" -> DiffResultValue(1234, 123), "1" -> DiffResultMissing(1234))), - "parent" -> DiffResultValue("com.softwaremill.diffx.Foo", "com.softwaremill.diffx.Bar") - ) - ) - } - - "coproduct types with ignored fields" in { - sealed trait Base { - def id: Int - def name: String - } - - final case class SubtypeOne(id: Int, name: String) extends Base - final case class SubtypeTwo(id: Int, name: String) extends Base - - val left: Base = SubtypeOne(2, "one") - val right: Base = SubtypeOne(1, "one") - val diff = Derived[Diff[Base]].ignoreUnsafe("id") - compare(left, right)(diff) shouldBe an[Identical[Base]] - } - } - - "collections" - { - "list" - { - "identical" in { - compare(List("a"), List("a")) shouldBe Identical(List("a")) - } - - "diff" in { - compare(List("a"), List("B")) shouldBe DiffResultObject( - "List", - Map("0" -> DiffResultString(List(DiffResultValue("a", "B")))) - ) - } - - "use ignored fields from elements" in { - val o1 = Organization(List(p1, p2)) - val o2 = Organization(List(p1, p1, p1)) - val d = Diff[Organization].ignoreUnsafe("people", "name") - compare(o1, o2)(d) shouldBe DiffResultObject( - "Organization", - Map( - "people" -> DiffResultObject( - "List", - Map( - "0" -> Identical(p1), - "1" -> DiffResultObject( - "Person", - Map( - "name" -> Identical(p2.name), - "age" -> DiffResultValue(p2.age, p1.age), - "in" -> Identical(instant) - ) - ), - "2" -> DiffResultMissing(Person(p1.name, p1.age, instant)) - ) - ) - ) - ) - } - - "compare lists using set-like comparator" in { - val o1 = Organization(List(p1, p2)) - val o2 = Organization(List(p2, p1)) - implicit val om: ObjectMatcher[Person] = (left: Person, right: Person) => left.name == right.name - implicit val dd: Derived[Diff[List[Person]]] = new Derived(Diff[Set[Person]].contramap(_.toSet)) - compare(o1, o2) shouldBe Identical(Organization(List(p1, p2))) - } - - "should preserve order of elements" in { - val l1 = List(1, 2, 3, 4, 5, 6) - val l2 = List(1, 2, 3, 4, 5, 7) - compare(l1, l2) shouldBe DiffResultObject( - "List", - ListMap( - "0" -> Identical(1), - "1" -> Identical(2), - "2" -> Identical(3), - "3" -> Identical(4), - "4" -> Identical(5), - "5" -> DiffResultValue(6, 7) - ) - ) - } - } - "sets" - { - "identity" in { - compare(Set(1), Set(1)) shouldBe an[Identical[_]] - } - - "diff" in { - val diffResult = compare(Set(1, 2, 3, 4, 5), Set(1, 2, 3, 4)).asInstanceOf[DiffResultSet] - diffResult.diffs should contain theSameElementsAs List( - DiffResultAdditional(5), - Identical(4), - Identical(3), - Identical(2), - Identical(1) - ) - } - "ignored fields from elements" in { - val p2m = p2.copy(age = 33, in = Instant.now()) - val d = Diff[Person].ignoreUnsafe("age") - implicit val im: ObjectMatcher[Person] = (left: Person, right: Person) => left.name == right.name - val ds: Derived[Diff[Set[Person]]] = Diff.diffForSet(im, Derived(d), implicitly[ObjectMatcher[Person]]) - compare(Set(p1, p2), Set(p1, p2m))(ds.value) shouldBe DiffResultSet( - List( - Identical(p1), - DiffResultObject( - "Person", - Map( - "name" -> Identical(p2.name), - "age" -> Identical(p2.age), - "in" -> DiffResultValue(p1.in, p2m.in) - ) - ) - ) - ) - } - - "mutable set" in { - import scala.collection.{Set => mSet} - val diffResult = compare(mSet(1, 2, 3, 4, 5), mSet(1, 2, 3, 4)).asInstanceOf[DiffResultSet] - diffResult.diffs should contain theSameElementsAs List( - DiffResultAdditional(5), - Identical(4), - Identical(3), - Identical(2), - Identical(1) - ) - } - - "identical when products are identical using ignored" in { - val p2m = p2.copy(age = 33, in = Instant.now()) - val d = Diff[Person].ignoreUnsafe("age").ignoreUnsafe("in") - val ds: Derived[Diff[Set[Person]]] = - Diff.diffForSet(implicitly[ObjectMatcher[Person]], Derived(d), implicitly[ObjectMatcher[Person]]) - compare(Set(p1, p2), Set(p1, p2m))(ds.value) shouldBe Identical(Set(p1, p2)) - } - - "propagate ignore fields to elements" in { - val p2m = p2.copy(in = Instant.now()) - implicit val im: ObjectMatcher[Person] = (left: Person, right: Person) => left.name == right.name - val ds: Diff[Set[Person]] = - Diff.diffForSet(im, Derived[Diff[Person]], implicitly[ObjectMatcher[Person]]).value.ignoreUnsafe("age") - compare(Set(p1, p2), Set(p1, p2m))(ds) shouldBe DiffResultSet( - List( - Identical(p1), - DiffResultObject( - "Person", - Map( - "name" -> Identical(p2.name), - "age" -> Identical(p2.age), - "in" -> DiffResultValue(p1.in, p2m.in) - ) - ) - ) - ) - } - "set of products" in { - val p2m = p2.copy(age = 33) - compare(Set(p1, p2), Set(p1, p2m)) shouldBe DiffResultSet( - List(DiffResultAdditional(p2), DiffResultMissing(p2m), Identical(p1)) - ) - } - - "set of products using instance matcher" in { - val p2m = p2.copy(age = 33) - implicit val im: ObjectMatcher[Person] = (left: Person, right: Person) => left.name == right.name - compare(Set(p1, p2), Set(p1, p2m)) shouldBe DiffResultSet( - List( - Identical(p1), - DiffResultObject( - "Person", - Map("name" -> Identical(p2.name), "age" -> DiffResultValue(p2.age, p2m.age), "in" -> Identical(p1.in)) - ) - ) - ) - } - } - "maps" - { - "identical" in { - val m1 = Map("a" -> 1) - compare(m1, m1) shouldBe an[Identical[_]] - } - - "simple diff" in { - val m1 = Map("a" -> 1) - val m2 = Map("a" -> 2) - compare(m1, m2) shouldBe DiffResultMap(Map(Identical("a") -> DiffResultValue(1, 2))) - } - - "simple diff - mutable map" in { - val m1 = scala.collection.Map("a" -> 1) - val m2 = scala.collection.Map("a" -> 2) - compare(m1, m2) shouldBe DiffResultMap(Map(Identical("a") -> DiffResultValue(1, 2))) - } - - "propagate ignored fields to elements" in { - val dm = Diff[Map[String, Person]].ignoreUnsafe("age") - compare(Map("first" -> p1), Map("first" -> p2))(dm) shouldBe DiffResultMap( - Map( - Identical("first") -> DiffResultObject( - "Person", - Map( - "name" -> DiffResultString(List(DiffResultValue(p1.name, p2.name))), - "age" -> Identical(p1.age), - "in" -> Identical(p1.in) - ) - ) - ) - ) - } - - "identical when products are identical using ignore" in { - val dm = Diff[Map[String, Person]].ignoreUnsafe("age").ignoreUnsafe("name") - compare(Map("first" -> p1), Map("first" -> p2))(dm) shouldBe Identical(Map("first" -> p1)) - } - - "maps by values" in { - type DD[T] = Derived[Diff[T]] - - implicit def mapWithoutKeys[T, R: DD]: Derived[Diff[Map[T, R]]] = - new Derived(Diff[List[R]].contramap(_.values.toList)) - - val person = Person("123", 11, Instant.now()) - compare( - Map[String, Person]("i1" -> person), - Map[String, Person]("i2" -> person) - ) shouldBe Identical(List(person)) - } - - "ignore part of map's key using keys's diff specification" in { - implicit def dm: Diff[KeyModel] = Derived[Diff[KeyModel]].ignore(_.id) - - val a1 = MyLookup(Map(KeyModel(UUID.randomUUID(), "k1") -> "val1")) - val a2 = MyLookup(Map(KeyModel(UUID.randomUUID(), "k1") -> "val1")) - compare(a1, a2) shouldBe Identical(a1) - } - - "match values using object mapper" in { - implicit val om: ObjectMatcher[KeyModel] = new ObjectMatcher[KeyModel] { - override def isSameObject(left: KeyModel, right: KeyModel): Boolean = left.name == right.name - } - val uuid1 = UUID.randomUUID() - val uuid2 = UUID.randomUUID() - val a1 = MyLookup(Map(KeyModel(uuid1, "k1") -> "val1")) - val a2 = MyLookup(Map(KeyModel(uuid2, "k1") -> "val1")) - compare(a1, a2) shouldBe DiffResultObject( - "MyLookup", - Map( - "map" -> DiffResultMap( - Map( - DiffResultObject( - "KeyModel", - Map( - "id" -> DiffResultValue(uuid1, uuid2), - "name" -> Identical("k1") - ) - ) -> Identical("val1") - ) - ) - ) - ) - } - } - "ranges" - { - "identical" in { - val r1 = 0 until 100 - val r2 = 0 until 100 - compare(r1, r2) shouldBe Identical(r1) - } - "dif" in { - val r1 = 0 until 100 - val r2 = 0 until 99 - compare(r1, r2) shouldBe DiffResultValue(r1, r2) - } - "inclusive vs exclusive" in { - val r1 = 0 until 100 - val r2 = 0 to 100 - compare(r1, r2) shouldBe DiffResultValue(r1, r2) - } - } - } - - "Diff.useEquals" - { - "Uses Object.equals instance for comparison" in { - val a = new HasCustomEquals("aaaa") - val z = new HasCustomEquals("zzzz") - val not = new HasCustomEquals("not") - val diffInstance = Diff.useEquals[HasCustomEquals] - - diffInstance.apply(a, z) shouldBe Identical(a) - diffInstance.apply(a, not) shouldBe DiffResultValue(a, not) - } - } - - "strings" - { - "equal strings should be equal" in { - val left = "scalaIsAwesome" - val right = "scalaIsAwesome" - - compare(left, right) shouldBe Identical(left) - } - - "different strings should be different" in { - val left = "scalaIsAwesome" - val right = "diffxIsAwesome" - - compare(left, right) shouldBe DiffResultString(List(DiffResultValue(left, right))) - } - - "multiline strings should be compared line by line" in { - val left = - """first - |second - |third - |fourth""".stripMargin - val right = - """first - |sec??? - |third""".stripMargin - - compare(left, right) shouldBe DiffResultString( - List( - Identical("first"), - DiffResultValue("second", "sec???"), - Identical("third"), - DiffResultAdditional("fourth") - ) - ) - } - } -} - -case class Person(name: String, age: Int, in: Instant) - -case class Family(first: Person, second: Person) - -case class Organization(people: List[Person]) - -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 - -class HasCustomEquals(val s: String) { - override def equals(obj: Any): Boolean = { - obj match { - case o: HasCustomEquals => this.s.length == o.s.length - case _ => false - } - } -} - -sealed trait TsDirection - -object TsDirection { - case object Incoming extends TsDirection - - case object Outgoing extends TsDirection -} - -case class KeyModel(id: UUID, name: String) - -case class MyLookup(map: Map[KeyModel, String]) diff --git a/core/src/test/scala/com/softwaremill/diffx/IgnoreMacroTest.scala b/core/src/test/scala/com/softwaremill/diffx/IgnoreMacroTest.scala deleted file mode 100644 index 0ad20e42..00000000 --- a/core/src/test/scala/com/softwaremill/diffx/IgnoreMacroTest.scala +++ /dev/null @@ -1,27 +0,0 @@ -package com.softwaremill.diffx - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class IgnoreMacroTest extends AnyFlatSpec with Matchers { - "IgnoreMacroTest" should "ignore field in nested products" in { - IgnoreMacro.ignoredFromPath[Family, String](_.first.name) shouldBe List("first", "name") - } - - it should "ignore fields in list of products" in { - IgnoreMacro.ignoredFromPath[Organization, String](_.people.each.name) shouldBe List("people", "name") - } - - it should "ignore fields in product wrapped with either" in { - IgnoreMacro.ignoredFromPath[Either[Person, Person], String](_.eachRight.name) shouldBe List("name") - IgnoreMacro.ignoredFromPath[Either[Person, Person], String](_.eachLeft.name) shouldBe List("name") - } - - it should "ignore fields in product wrapped with option" in { - IgnoreMacro.ignoredFromPath[Option[Person], String](_.each.name) shouldBe List("name") - } - - it should "ignore fields in map of products" in { - IgnoreMacro.ignoredFromPath[Map[String, Person], String](_.each.name) shouldBe List("name") - } -} diff --git a/core/src/test/scala/com/softwaremill/diffx/test/DiffModifyIntegrationTest.scala b/core/src/test/scala/com/softwaremill/diffx/test/DiffModifyIntegrationTest.scala new file mode 100644 index 00000000..6e759772 --- /dev/null +++ b/core/src/test/scala/com/softwaremill/diffx/test/DiffModifyIntegrationTest.scala @@ -0,0 +1,138 @@ +package com.softwaremill.diffx.test + +import com.softwaremill.diffx._ +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import com.softwaremill.diffx.generic.auto._ + +import java.time.Instant +import java.util.UUID + +class DiffModifyIntegrationTest extends AnyFlatSpec with Matchers { + val instant: Instant = Instant.now() + val p1 = Person("p1", 22, instant) + val p2 = Person("p2", 11, instant) + + it should "allow importing and exporting implicits" in { + implicit val d: Diff[Person] = Derived[Diff[Person]].ignore(_.name) + compare(p1, p2) shouldBe DiffResultObject( + "Person", + Map("name" -> IdenticalValue(""), "age" -> DiffResultValue(22, 11), "in" -> IdenticalValue(instant)) + ) + } + + it should "allow importing and exporting implicits using macro on derived instance" in { + implicit val d: Diff[Person] = Derived[Diff[Person]].ignore(_.name) + compare(p1, p2) shouldBe DiffResultObject( + "Person", + Map("name" -> IdenticalValue(""), "age" -> DiffResultValue(22, 11), "in" -> IdenticalValue(instant)) + ) + } + + it should "allow calling ignore multiple times" in { + implicit val d: Diff[Person] = Derived[Diff[Person]] + .ignore(_.name) + .ignore(_.age) + compare(p1, p2).isIdentical shouldBe true + } + + it should "compare lists using explicit object matcher comparator" in { + val o1 = Organization(List(p1, p2)) + val o2 = Organization(List(p2, p1)) + implicit val orgDiff: Diff[Organization] = Derived[Diff[Organization]] + .modify(_.people) + .withListMatcher( + ObjectMatcher.byValue[Int, Person](ObjectMatcher.by(_.name)) + ) + compare(o1, o2).isIdentical shouldBe true + } + + it should "ignore only on right" in { + case class Wrapper(e: Either[Person, Person]) + val e1 = Wrapper(Right(p1)) + val e2 = Wrapper(Right(p1.copy(name = p1.name + "_modified"))) + + implicit val wrapperDiff: Diff[Wrapper] = Derived[Diff[Wrapper]].ignore(_.e.eachRight.name) + + compare(e1, e2).isIdentical shouldBe true + + val e3 = Wrapper(Left(p1)) + val e4 = Wrapper(Left(p1.copy(name = p1.name + "_modified"))) + + compare(e3, e4).isIdentical shouldBe false + } + + it should "ignore only on left" in { + case class Wrapper(e: Either[Person, Person]) + val e1 = Wrapper(Right(p1)) + val e2 = Wrapper(Right(p1.copy(name = p1.name + "_modified"))) + + implicit val wrapperDiff: Diff[Wrapper] = Derived[Diff[Wrapper]].ignore(_.e.eachLeft.name) + + compare(e1, e2).isIdentical shouldBe false + val e3 = Wrapper(Left(p1)) + val e4 = Wrapper(Left(p1.copy(name = p1.name + "_modified"))) + + compare(e3, e4).isIdentical shouldBe true + } + + it should "match map entries by values" in { + implicit val lookupDiff: Diff[MyLookup] = Derived[Diff[MyLookup]] + .modify(_.map) + .withMapMatcher( + ObjectMatcher.byValue[KeyModel, String] + ) + val uuid1 = UUID.randomUUID() + val uuid2 = UUID.randomUUID() + val a1 = MyLookup(Map(KeyModel(uuid1, "k1") -> "val1")) + val a2 = MyLookup(Map(KeyModel(uuid2, "k1") -> "val1")) + compare(a1, a2) shouldBe DiffResultObject( + "MyLookup", + Map( + "map" -> DiffResultMap( + Map( + DiffResultObject( + "KeyModel", + Map( + "id" -> DiffResultValue(uuid1, uuid2), + "name" -> IdenticalValue("k1") + ) + ) -> IdenticalValue("val1") + ) + ) + ) + ) + } + + it should "use overrided object matcher when comparing set" in { + implicit val lookupDiff: Diff[Startup] = Derived[Diff[Startup]] + .modify(_.workers) + .withSetMatcher[Person](ObjectMatcher.by(_.name)) + val p2m = p2.copy(age = 33) + compare(Startup(Set(p1, p2)), Startup(Set(p1, p2m))) shouldBe DiffResultObject( + "Startup", + Map( + "workers" -> DiffResultSet( + List( + DiffResultObject( + "Person", + Map( + "name" -> IdenticalValue(p1.name), + "age" -> IdenticalValue(p1.age), + "in" -> IdenticalValue(p1.in) + ) + ), + DiffResultObject( + "Person", + Map( + "name" -> IdenticalValue(p2.name), + "age" -> DiffResultValue(p2.age, p2m.age), + "in" -> IdenticalValue(p1.in) + ) + ) + ) + ) + ) + ) + } +} diff --git a/core/src/test/scala/com/softwaremill/diffx/test/DiffResultTest.scala b/core/src/test/scala/com/softwaremill/diffx/test/DiffResultTest.scala new file mode 100644 index 00000000..de292239 --- /dev/null +++ b/core/src/test/scala/com/softwaremill/diffx/test/DiffResultTest.scala @@ -0,0 +1,130 @@ +package com.softwaremill.diffx.test + +import com.softwaremill.diffx._ +import org.scalatest.freespec.AnyFreeSpec +import org.scalatest.matchers.should.Matchers + +class DiffResultTest extends AnyFreeSpec with Matchers with DiffxConsoleSupport { + implicit val colorConfig: ConsoleColorConfig = + ConsoleColorConfig(default = identity, arrow = identity, right = identity, left = identity) + + "diff set output" - { + "it should show a simple difference" in { + val output = DiffResultSet(List(IdenticalValue("a"), DiffResultValue("1", "2"))).show() + output shouldBe + s"""Set( + | a, + | 1 -> 2)""".stripMargin + } + + "it should show an indented difference" in { + val output = + DiffResultSet(List(IdenticalValue("a"), DiffResultValue("1", "2"))).show() + output shouldBe + s"""Set( + | a, + | 1 -> 2)""".stripMargin + } + + "it should show a nested list difference" in { + val output = DiffResultSet(List(IdenticalValue("a"), DiffResultSet(List(IdenticalValue("b"))))).show() + output shouldBe + s"""Set( + | a, + | Set( + | b))""".stripMargin + } + + "it should show null" in { + val output = DiffResultSet(List(IdenticalValue(null), DiffResultValue(null, null))).show() + output shouldBe + s"""Set( + | null, + | null -> null)""".stripMargin + } + "it shouldn't render identical elements" in { + val output = DiffResultSet(List(IdenticalValue("a"), DiffResultValue("1", "2"))).show(renderIdentical = false) + output shouldBe + s"""Set( + | 1 -> 2)""".stripMargin + } + } + + "diff map output" - { + "it should show a simple diff" in { + val output = + DiffResultMap(Map(IdenticalValue("a") -> DiffResultValue(1, 2), DiffResultMissing("b") -> DiffResultMissing(3))) + .show() + output shouldBe + s"""Map( + | a: 1 -> 2, + | b: 3)""".stripMargin + } + + "it should show an indented diff" in { + val output = + DiffResultMap(Map(IdenticalValue("a") -> DiffResultValue(1, 2), DiffResultMissing("b") -> DiffResultMissing(3))) + .show() + output shouldBe + s"""Map( + | a: 1 -> 2, + | b: 3)""".stripMargin + } + + "it should show a nested diff" in { + val output = + DiffResultMap(Map(IdenticalValue("a") -> DiffResultMap(Map(IdenticalValue("b") -> DiffResultValue(1, 2))))) + .show() + output shouldBe + s"""Map( + | a: Map( + | b: 1 -> 2))""".stripMargin + } + + "shouldn't render identical entries" in { + val output = + DiffResultMap( + Map( + IdenticalValue("a") -> DiffResultValue(1, 2), + DiffResultValue("b", "c") -> IdenticalValue(3), + IdenticalValue("d") -> IdenticalValue(4) + ) + ) + .show(renderIdentical = false) + output shouldBe + s"""Map( + | a: 1 -> 2, + | b -> c: 3)""".stripMargin + } + } + + "diff object output" - { + "it should show an indented diff with plus and minus signs" in { + val colorConfigWithPlusMinus: ConsoleColorConfig = + ConsoleColorConfig(default = identity, arrow = identity, right = s => "+" + s, left = s => "-" + s) + + val output = DiffResultObject( + "List", + Map("0" -> DiffResultValue(1234, 123), "1" -> DiffResultMissing(1234), "2" -> IdenticalValue(1234)) + ) + .show()(colorConfigWithPlusMinus) + output shouldBe + s"""List( + | 0: -1234 -> +123, + | 1: +1234, + | 2: 1234)""".stripMargin + } + + "it should not render identical fields" in { + val output = DiffResultObject( + "List", + Map("0" -> DiffResultValue(1234, 123), "1" -> DiffResultMissing(1234), "2" -> IdenticalValue(1234)) + ) + .show(renderIdentical = false) + output shouldBe + s"""List( + | 0: 1234 -> 123, + | 1: 1234)""".stripMargin + } + } +} diff --git a/core/src/test/scala/com/softwaremill/diffx/test/DiffSemiautoTest.scala b/core/src/test/scala/com/softwaremill/diffx/test/DiffSemiautoTest.scala new file mode 100644 index 00000000..8dc87737 --- /dev/null +++ b/core/src/test/scala/com/softwaremill/diffx/test/DiffSemiautoTest.scala @@ -0,0 +1,64 @@ +package com.softwaremill.diffx.test + +import com.softwaremill.diffx.test.ACoproduct.ProductA +import com.softwaremill.diffx.{Derived, Diff, IdenticalValue} +import org.scalatest.freespec.AnyFreeSpec +import org.scalatest.matchers.should.Matchers + +class DiffSemiautoTest extends AnyFreeSpec with Matchers { + "should compile if all required instances are defined" in { + assertCompiles(""" + |import com.softwaremill.diffx._ + |final case class P1(f1: String) + |final case class P2(f1: P1) + | + |implicit val p1: Derived[Diff[P1]] = Diff.derived[P1] + |implicit val p2: Derived[Diff[P2]] = Diff.derived[P2] + |""".stripMargin) + } + + "should not allow to compile if an instance is missing" in { + assertDoesNotCompile(""" + |import com.softwaremill.diffx._ + |final case class P1(f1: String) + |final case class P2(f1: P1) + | + |implicit val p2: Derived[Diff[P2]] = Diff.derived[P2] + |""".stripMargin) + } + + "should compile with generic.auto._" in { + assertCompiles(""" + |import com.softwaremill.diffx._ + |import com.softwaremill.diffx.generic.auto._ + |final case class P1(f1: String) + |final case class P2(f1: P1) + | + |val p2: Diff[P2] = Diff[P2] + |""".stripMargin) + } + + "should work for coproducts" in { + implicit val dACoproduct: Derived[Diff[ACoproduct]] = Diff.derived[ACoproduct] + + Diff.compare[ACoproduct](ProductA("1"), ProductA("1")).isIdentical shouldBe true + } + + "should allow ignoring on derived diffs" in { + implicit val dACoproduct: Derived[Diff[ProductA]] = Diff.derived[ProductA].ignore(_.id) + + Diff.compare[ProductA](ProductA("1"), ProductA("2")).isIdentical shouldBe true + } + + "should allow modifying derived diffs" in { + implicit val dACoproduct: Derived[Diff[ProductA]] = Diff.derived[ProductA].modify(_.id).ignore() + + Diff.compare[ProductA](ProductA("1"), ProductA("2")).isIdentical shouldBe true + } +} + +sealed trait ACoproduct +object ACoproduct { + case class ProductA(id: String) extends ACoproduct + case class ProductB(id: String) extends ACoproduct +} diff --git a/core/src/test/scala/com/softwaremill/diffx/test/DiffTest.scala b/core/src/test/scala/com/softwaremill/diffx/test/DiffTest.scala new file mode 100644 index 00000000..b8864d64 --- /dev/null +++ b/core/src/test/scala/com/softwaremill/diffx/test/DiffTest.scala @@ -0,0 +1,847 @@ +package com.softwaremill.diffx.test + +import com.softwaremill.diffx.ObjectMatcher.{IterableEntry, MapEntry} +import com.softwaremill.diffx.generic.auto._ +import com.softwaremill.diffx._ +import org.scalatest.freespec.AnyFreeSpec +import org.scalatest.matchers.should.Matchers + +import java.time.Instant +import java.util.UUID +import scala.collection.immutable.ListMap + +class DiffTest extends AnyFreeSpec with Matchers { + private val instant: Instant = Instant.now() + val p1 = Person("p1", 22, instant) + val p2 = Person("p2", 11, instant) + + "simple value" - { + "diff" in { + compare(1, 2) shouldBe DiffResultValue(1, 2) + } + "identity" in { + compare(1, 1) shouldBe IdenticalValue(1) + } + "contravariant" in { + compare(Some(1), Option(1)) shouldBe IdenticalValue(1) + } + "approximate - identical" in { + val diff = Diff.approximate[Double](0.05) + diff(0.12, 0.14) shouldBe IdenticalValue(0.12) + } + "approximate - different" in { + val diff = Diff.approximate[Double](0.05) + diff(0.12, 0.19) shouldBe DiffResultValue(0.12, 0.19) + } + } + + "options" - { + "nullable" in { + compare(Option.empty[Int], null: Option[Int]) shouldBe DiffResultValue(Option.empty[Int], null) + } + } + + "products" - { + "identity" in { + compare(p1, p1).isIdentical shouldBe true + } + + "nullable" in { + compare(p1, null) shouldBe DiffResultValue(p1, null) + } + + "diff" in { + compare(p1, p2) shouldBe DiffResultObject( + "Person", + Map( + "name" -> DiffResultString(List(DiffResultValue(p1.name, p2.name))), + "age" -> DiffResultValue(p1.age, p2.age), + "in" -> IdenticalValue(instant) + ) + ) + } + + "difference between null and value" in { + compare(p1.copy(name = null), p2) shouldBe DiffResultObject( + "Person", + Map( + "name" -> DiffResultValue(null, p2.name), + "age" -> DiffResultValue(p1.age, p2.age), + "in" -> IdenticalValue(instant) + ) + ) + } + + "two nulls should be equal" in { + compare(p1.copy(name = null), p1.copy(name = null)).isIdentical shouldBe true + } + + "ignored fields should be different than identical" in { + implicit val d: Diff[Person] = Derived[Diff[Person]].modifyUnsafe("name")(Diff.ignored) + compare(p1, p1.copy(name = "other")) shouldBe DiffResultObject( + "Person", + Map( + "name" -> DiffResult.Ignored, + "age" -> IdenticalValue(p1.age), + "in" -> IdenticalValue(p1.in) + ) + ) + } + + "ignoring given fields" in { + implicit val d: Diff[Person] = + Derived[Diff[Person]].modifyUnsafe("name")(Diff.ignored).modifyUnsafe("age")(Diff.ignored) + val p3 = p2.copy(in = Instant.now()) + compare(p1, p3) shouldBe DiffResultObject( + "Person", + Map( + "name" -> DiffResult.Ignored, + "age" -> DiffResult.Ignored, + "in" -> DiffResultValue(p1.in, p3.in) + ) + ) + } + + "nested products" in { + val f1 = Family(p1, p2) + val f2 = Family(p1, p1) + compare(f1, f2) shouldBe DiffResultObject( + "Family", + Map( + "first" -> DiffResultObject( + "Person", + Map( + "name" -> IdenticalValue(p1.name), + "age" -> IdenticalValue(p1.age), + "in" -> IdenticalValue(p1.in) + ) + ), + "second" -> DiffResultObject( + "Person", + Map( + "name" -> DiffResultString(List(DiffResultValue(p2.name, p1.name))), + "age" -> DiffResultValue(p2.age, p1.age), + "in" -> IdenticalValue(instant) + ) + ) + ) + ) + } + + "nested products ignoring nested fields" in { + val f1 = Family(p1, p2) + val f2 = Family(p1, p1) + implicit val d: Diff[Family] = Derived[Diff[Family]].modifyUnsafe("second", "name")(Diff.ignored) + compare(f1, f2) shouldBe DiffResultObject( + "Family", + Map( + "first" -> DiffResultObject( + "Person", + Map( + "name" -> IdenticalValue(p1.name), + "age" -> IdenticalValue(p1.age), + "in" -> IdenticalValue(p1.in) + ) + ), + "second" -> DiffResultObject( + "Person", + Map( + "name" -> DiffResult.Ignored, + "age" -> DiffResultValue(p2.age, p1.age), + "in" -> IdenticalValue(instant) + ) + ) + ) + ) + } + + "nested products ignoring fields only in given path" in { + val p1p = p1.copy(name = "other") + val f1 = Family(p1, p2) + val f2 = Family(p1p, p2.copy(name = "other")) + implicit val d: Diff[Family] = Derived[Diff[Family]].modifyUnsafe("second", "name")(Diff.ignored) + compare(f1, f2) shouldBe DiffResultObject( + "Family", + Map( + "first" -> DiffResultObject( + "Person", + Map( + "name" -> DiffResultString(List(DiffResultValue(p1.name, p1p.name))), + "age" -> IdenticalValue(p1.age), + "in" -> IdenticalValue(instant) + ) + ), + "second" -> DiffResultObject( + "Person", + Map( + "name" -> DiffResult.Ignored, + "age" -> IdenticalValue(p2.age), + "in" -> IdenticalValue(p2.in) + ) + ) + ) + ) + } + + "nested products ignoring nested products" in { + val f1 = Family(p1, p2) + val f2 = Family(p1, p1) + implicit val d: Diff[Family] = Derived[Diff[Family]].modifyUnsafe("second")(Diff.ignored) + compare(f1, f2).isIdentical shouldBe true + } + + "list of products" in { + val o1 = Organization(List(p1, p2)) + val o2 = Organization(List(p1, p1, p1)) + compare(o1, o2) shouldBe DiffResultObject( + "Organization", + Map( + "people" -> DiffResultObject( + "List", + Map( + "0" -> DiffResultObject( + "Person", + Map( + "name" -> IdenticalValue(p1.name), + "age" -> IdenticalValue(p1.age), + "in" -> IdenticalValue(p1.in) + ) + ), + "1" -> DiffResultObject( + "Person", + Map( + "name" -> DiffResultString(List(DiffResultValue(p2.name, p1.name))), + "age" -> DiffResultValue(p2.age, p1.age), + "in" -> IdenticalValue(instant) + ) + ), + "2" -> DiffResultMissing(Person(p1.name, p1.age, instant)) + ) + ) + ) + ) + } + + "identical list of products" in { + val o1 = Organization(List(p1, p2)) + val o2 = Organization(List(p1, p2)) + compare(o1, o2) shouldBe DiffResultObject( + "Organization", + Map( + "people" -> DiffResultObject( + "List", + Map( + "0" -> DiffResultObject( + "Person", + Map( + "name" -> IdenticalValue(p1.name), + "age" -> IdenticalValue(p1.age), + "in" -> IdenticalValue(p1.in) + ) + ), + "1" -> DiffResultObject( + "Person", + Map( + "name" -> IdenticalValue(p2.name), + "age" -> IdenticalValue(p2.age), + "in" -> IdenticalValue(p2.in) + ) + ) + ) + ) + ) + ) + } + } + + "coproducts" - { + val right: Foo = Foo( + Bar("asdf", 5), + List(123, 1234), + Some(Bar("asdf", 5)) + ) + val left: Foo = Foo( + Bar("asdf", 66), + List(1234), + Some(right) + ) + + "sealed trait objects" - { + "identity" in { + compare[TsDirection](TsDirection.Outgoing, TsDirection.Outgoing).isIdentical shouldBe true + } + "diff" in { + compare[TsDirection](TsDirection.Outgoing, TsDirection.Incoming) shouldBe DiffResultValue( + "com.softwaremill.diffx.test.TsDirection.Outgoing", + "com.softwaremill.diffx.test.TsDirection.Incoming" + ) + } + } + + "identity" in { + compare(left, left).isIdentical shouldBe true + } + + "nullable" in { + compare[TsDirection](TsDirection.Outgoing, null: TsDirection) shouldBe DiffResultValue(TsDirection.Outgoing, null) + } + + "diff" in { + compare(left, right) shouldBe DiffResultObject( + "Foo", + Map( + "bar" -> DiffResultObject("Bar", Map("s" -> IdenticalValue("asdf"), "i" -> DiffResultValue(66, 5))), + "b" -> DiffResultObject("List", Map("0" -> DiffResultValue(1234, 123), "1" -> DiffResultMissing(1234))), + "parent" -> DiffResultValue("com.softwaremill.diffx.test.Foo", "com.softwaremill.diffx.test.Bar") + ) + ) + } +// TODO: uncomment once https://github.com/propensive/magnolia/issues/277 is resolved +// +// "coproduct types with ignored fields" in { +// sealed trait Base { +// def id: Int +// def name: String +// } +// +// final case class SubtypeOne(id: Int, name: String) extends Base +// final case class SubtypeTwo(id: Int, name: String) extends Base +// val left: Base = SubtypeOne(2, "one") +// val right: Base = SubtypeOne(1, "one") +// implicit val diff: Diff[Base] = Derived[Diff[Base]].ignoreUnsafe("id") +// compare(left, right) shouldBe an[Identical[Base]] +// } + } + + "collections" - { + "list" - { + "identical" in { + compare(List("a"), List("a")) shouldBe DiffResultObject("List", Map("0" -> IdenticalValue("a"))) + } + + "nullable" in { + compare(List.empty[Int], null: List[Int]) shouldBe DiffResultValue(List.empty, null) + } + + "diff" in { + compare(List("a"), List("B")) shouldBe DiffResultObject( + "List", + Map("0" -> DiffResultString(List(DiffResultValue("a", "B")))) + ) + } + + "use ignored fields from elements" in { + val o1 = Organization(List(p1, p2)) + val o2 = Organization(List(p1, p1, p1)) + implicit val d: Diff[Organization] = Derived[Diff[Organization]].modifyUnsafe("people", "name")(Diff.ignored) + compare(o1, o2) shouldBe DiffResultObject( + "Organization", + Map( + "people" -> DiffResultObject( + "List", + Map( + "0" -> DiffResultObject( + "Person", + Map( + "name" -> DiffResult.Ignored, + "age" -> IdenticalValue(p1.age), + "in" -> IdenticalValue(p1.in) + ) + ), + "1" -> DiffResultObject( + "Person", + Map( + "name" -> DiffResult.Ignored, + "age" -> DiffResultValue(p2.age, p1.age), + "in" -> IdenticalValue(instant) + ) + ), + "2" -> DiffResultMissing(Person(p1.name, p1.age, instant)) + ) + ) + ) + ) + } + + "compare lists using set-like comparator" in { + val o1 = Organization(List(p1, p2)) + val o2 = Organization(List(p2, p1)) + implicit val om: ObjectMatcher[Person] = ObjectMatcher.by(_.name) + implicit val dd: Diff[List[Person]] = Diff[Set[Person]].contramap(_.toSet) + compare(o1, o2).isIdentical shouldBe true + } + + "compare lists using object matcher comparator" in { + val o1 = Organization(List(p1, p2)) + val o2 = Organization(List(p2, p1)) + implicit val om: ObjectMatcher[IterableEntry[Person]] = ObjectMatcher.byValue(_.name) + compare(o1, o2).isIdentical shouldBe true + } + + "compare lists using object matcher comparator when matching by pair" in { + val p2WithSameNameAsP1 = p2.copy(name = p1.name) + val o1 = Organization(List(p1, p2WithSameNameAsP1)) + val o2 = Organization(List(p2WithSameNameAsP1, p1)) + implicit val om: ObjectMatcher[IterableEntry[Person]] = ObjectMatcher.byValue(p => (p.name, p.age)) + compare(o1, o2).isIdentical shouldBe true + } + + "compare lists using explicit object matcher comparator" in { + val o1 = Organization(List(p1, p2)) + val o2 = Organization(List(p2, p1)) + implicit val orgDiff: Diff[Organization] = Derived[Diff[Organization]].modifyMatcherUnsafe("people")( + ObjectMatcher.byValue[Int, Person](ObjectMatcher.by(_.name)) + ) + compare(o1, o2).isIdentical shouldBe true + } + + "should preserve order of elements" in { + val l1 = List(1, 2, 3, 4, 5, 6) + val l2 = List(1, 2, 3, 4, 5, 7) + compare(l1, l2) shouldBe DiffResultObject( + "List", + ListMap( + "0" -> IdenticalValue(1), + "1" -> IdenticalValue(2), + "2" -> IdenticalValue(3), + "3" -> IdenticalValue(4), + "4" -> IdenticalValue(5), + "5" -> DiffResultValue(6, 7) + ) + ) + } + + "should not use values when matching using default key strategy" in { + val l1 = List(1, 2, 3, 4, 5, 6) + val l2 = List(1, 2, 4, 5, 6) + compare(l1, l2) shouldBe DiffResultObject( + "List", + ListMap( + "0" -> IdenticalValue(1), + "1" -> IdenticalValue(2), + "2" -> DiffResultValue(3, 4), + "3" -> DiffResultValue(4, 5), + "4" -> DiffResultValue(5, 6), + "5" -> DiffResultAdditional(6) + ) + ) + } + } + "sets" - { + "identity" in { + compare(Set(1), Set(1)).isIdentical shouldBe true + } + + "nullable" in { + compare(Set.empty[Int], null: Set[Int]) shouldBe DiffResultValue(Set.empty[Int], null) + } + + "diff" in { + val diffResult = compare(Set(1, 2, 3, 4, 5), Set(1, 2, 3, 4)).asInstanceOf[DiffResultSet] + diffResult.diffs should contain theSameElementsAs List( + DiffResultAdditional(5), + IdenticalValue(4), + IdenticalValue(3), + IdenticalValue(2), + IdenticalValue(1) + ) + } + "ignored fields from elements" in { + val p2m = p2.copy(age = 33, in = Instant.now()) + implicit val d: Diff[Person] = Derived[Diff[Person]].modifyUnsafe("age")(Diff.ignored) + implicit val im: ObjectMatcher[Person] = ObjectMatcher.by(_.name) + compare(Set(p1, p2), Set(p1, p2m)) shouldBe DiffResultSet( + List( + DiffResultObject( + "Person", + Map( + "name" -> IdenticalValue(p1.name), + "age" -> DiffResult.Ignored, + "in" -> IdenticalValue(p1.in) + ) + ), + DiffResultObject( + "Person", + Map( + "name" -> IdenticalValue(p2.name), + "age" -> DiffResult.Ignored, + "in" -> DiffResultValue(p1.in, p2m.in) + ) + ) + ) + ) + } + + "mutable set" in { + import scala.collection.{Set => mSet} + val diffResult = compare(mSet(1, 2, 3, 4, 5), mSet(1, 2, 3, 4)).asInstanceOf[DiffResultSet] + diffResult.diffs should contain theSameElementsAs List( + DiffResultAdditional(5), + IdenticalValue(4), + IdenticalValue(3), + IdenticalValue(2), + IdenticalValue(1) + ) + } + + "identical when products are identical using ignored" in { + val p2m = p2.copy(age = 33, in = Instant.now()) + implicit val d: Diff[Person] = Derived[Diff[Person]] + .modifyUnsafe("age")(Diff.ignored) + .modifyUnsafe("in")(Diff.ignored) + compare(Set(p1, p2), Set(p1, p2m)).isIdentical shouldBe true + } + + "propagate ignore fields to elements" in { + val p2m = p2.copy(in = Instant.now()) + implicit val im: ObjectMatcher[Person] = ObjectMatcher.by(_.name) + implicit val ds: Diff[Person] = Derived[Diff[Person]].modifyUnsafe("age")(Diff.ignored) + compare(Set(p1, p2), Set(p1, p2m)) shouldBe DiffResultSet( + List( + DiffResultObject( + "Person", + Map( + "name" -> IdenticalValue(p1.name), + "age" -> DiffResult.Ignored, + "in" -> IdenticalValue(p1.in) + ) + ), + DiffResultObject( + "Person", + Map( + "name" -> IdenticalValue(p2.name), + "age" -> DiffResult.Ignored, + "in" -> DiffResultValue(p1.in, p2m.in) + ) + ) + ) + ) + } + "set of products" in { + val p2m = p2.copy(age = 33) + compare(Set(p1, p2), Set(p1, p2m)) shouldBe DiffResultSet( + List( + DiffResultAdditional(p2), + DiffResultMissing(p2m), + DiffResultObject( + "Person", + Map( + "name" -> IdenticalValue(p1.name), + "age" -> IdenticalValue(p1.age), + "in" -> IdenticalValue(p1.in) + ) + ) + ) + ) + } + "override set instance" in { + val p2m = p2.copy(age = 33) + implicit def setDiff[T, C[W] <: scala.collection.Set[W]]: Diff[C[T]] = + (left: C[T], _: C[T], _: DiffContext) => IdenticalValue(left) + compare(Set(p1, p2), Set(p1, p2m)).isIdentical shouldBe true + } + + "set of products using instance matcher" in { + val p2m = p2.copy(age = 33) + implicit val im: ObjectMatcher[Person] = ObjectMatcher.by(_.name) + compare(Startup(Set(p1, p2)), Startup(Set(p1, p2m))) shouldBe DiffResultObject( + "Startup", + Map( + "workers" -> DiffResultSet( + List( + DiffResultObject( + "Person", + Map( + "name" -> IdenticalValue(p1.name), + "age" -> IdenticalValue(p1.age), + "in" -> IdenticalValue(p1.in) + ) + ), + DiffResultObject( + "Person", + Map( + "name" -> IdenticalValue(p2.name), + "age" -> DiffResultValue(p2.age, p2m.age), + "in" -> IdenticalValue(p1.in) + ) + ) + ) + ) + ) + ) + } + } + "maps" - { + "identical" in { + val m1 = Map("a" -> 1) + compare(m1, m1).isIdentical shouldBe true + } + + "nullable" in { + compare(Map.empty[Int, Int], null: Map[Int, Int]) shouldBe DiffResultValue(Map.empty[Int, Int], null) + } + + "simple diff" in { + val m1 = Map("a" -> 1) + val m2 = Map("a" -> 2) + compare(m1, m2) shouldBe DiffResultMap(Map(IdenticalValue("a") -> DiffResultValue(1, 2))) + } + + "simple diff - mutable map" in { + val m1 = scala.collection.Map("a" -> 1) + val m2 = scala.collection.Map("a" -> 2) + compare(m1, m2) shouldBe DiffResultMap(Map(IdenticalValue("a") -> DiffResultValue(1, 2))) + } + + "propagate ignored fields to elements" in { + implicit val dm: Diff[Person] = Derived[Diff[Person]].modifyUnsafe("age")(Diff.ignored) + compare(Map("first" -> p1), Map("first" -> p2)) shouldBe DiffResultMap( + Map( + IdenticalValue("first") -> DiffResultObject( + "Person", + Map( + "name" -> DiffResultString(List(DiffResultValue(p1.name, p2.name))), + "age" -> DiffResult.Ignored, + "in" -> IdenticalValue(p1.in) + ) + ) + ) + ) + } + + "identical when products are identical using ignore" in { + implicit val dm: Diff[Person] = + Derived[Diff[Person]] + .modifyUnsafe("age")(Diff.ignored) + .modifyUnsafe("name")(Diff.ignored) + compare(Map("first" -> p1), Map("first" -> p2)).isIdentical shouldBe true + } + + "maps by values" in { + implicit def mapWithoutKeys[T, R: Diff]: Diff[Map[T, R]] = + Diff[List[R]].contramap(_.values.toList) + + val person = Person("123", 11, Instant.now()) + compare( + Map[String, Person]("i1" -> person), + Map[String, Person]("i2" -> person) + ).isIdentical shouldBe true + } + + "ignore part of map's key using keys's diff specification" in { + implicit def dm: Diff[KeyModel] = Derived[Diff[KeyModel]].ignore(_.id) + + val a1 = MyLookup(Map(KeyModel(UUID.randomUUID(), "k1") -> "val1")) + val a2 = MyLookup(Map(KeyModel(UUID.randomUUID(), "k1") -> "val1")) + compare(a1, a2).isIdentical shouldBe true + } + + "match keys using object mapper" in { + implicit val om: ObjectMatcher[KeyModel] = ObjectMatcher.by(_.name) + val uuid1 = UUID.randomUUID() + val uuid2 = UUID.randomUUID() + val a1 = MyLookup(Map(KeyModel(uuid1, "k1") -> "val1")) + val a2 = MyLookup(Map(KeyModel(uuid2, "k1") -> "val1")) + compare(a1, a2) shouldBe DiffResultObject( + "MyLookup", + Map( + "map" -> DiffResultMap( + Map( + DiffResultObject( + "KeyModel", + Map( + "id" -> DiffResultValue(uuid1, uuid2), + "name" -> IdenticalValue("k1") + ) + ) -> IdenticalValue("val1") + ) + ) + ) + ) + } + + "match map entries by values" in { + implicit val om: ObjectMatcher[MapEntry[KeyModel, String]] = ObjectMatcher.byValue + val uuid1 = UUID.randomUUID() + val uuid2 = UUID.randomUUID() + val a1 = MyLookup(Map(KeyModel(uuid1, "k1") -> "val1")) + val a2 = MyLookup(Map(KeyModel(uuid2, "k1") -> "val1")) + compare(a1, a2) shouldBe DiffResultObject( + "MyLookup", + Map( + "map" -> DiffResultMap( + Map( + DiffResultObject( + "KeyModel", + Map( + "id" -> DiffResultValue(uuid1, uuid2), + "name" -> IdenticalValue("k1") + ) + ) -> IdenticalValue("val1") + ) + ) + ) + ) + } + } + "ranges" - { + "identical" in { + val r1 = 0 until 100 + val r2 = 0 until 100 + compare(r1, r2) shouldBe IdenticalValue(r1) + } + "dif" in { + val r1 = 0 until 100 + val r2 = 0 until 99 + compare(r1, r2) shouldBe DiffResultValue(r1, r2) + } + "inclusive vs exclusive" in { + val r1 = 0 until 100 + val r2 = 0 to 100 + compare(r1, r2) shouldBe DiffResultValue(r1, r2) + } + } + } + + "Diff.useEquals" - { + "Uses Object.equals instance for comparison" in { + val a = new HasCustomEquals("aaaa") + val z = new HasCustomEquals("zzzz") + val not = new HasCustomEquals("not") + val diffInstance = Diff.useEquals[HasCustomEquals] + + diffInstance.apply(a, z) shouldBe IdenticalValue(a) + diffInstance.apply(a, not) shouldBe DiffResultValue(a, not) + } + } + + "strings" - { + "equal strings should be equal" in { + val left = "scalaIsAwesome" + val right = "scalaIsAwesome" + + compare(left, right) shouldBe IdenticalValue(left) + } + + "different strings should be different" in { + val left = "scalaIsAwesome" + val right = "diffxIsAwesome" + + compare(left, right) shouldBe DiffResultString(List(DiffResultValue(left, right))) + } + + "multiline strings should be compared line by line" in { + val left = + """first + |second + |third + |fourth""".stripMargin + val right = + """first + |sec??? + |third""".stripMargin + + compare(left, right) shouldBe DiffResultString( + List( + IdenticalValue("first"), + DiffResultValue("second", "sec???"), + IdenticalValue("third"), + DiffResultAdditional("fourth") + ) + ) + } + } + "either" - { + "equal rights should be identical" in { + val e1: Either[String, String] = Right("a") + compare(e1, e1) shouldBe IdenticalValue("a") + + } + "equal lefts should be identical" in { + val e1: Either[String, String] = Left("a") + compare(e1, e1) shouldBe IdenticalValue("a") + } + "left and right should be different" in { + val e1: Either[String, String] = Left("a") + val e2: Either[String, String] = Right("a") + compare(e1, e2) shouldBe DiffResultValue(e1, e2) + } + } + "tuples" - { + "tuple2" - { + "equal tuples should be identical" in { + compare((1, 2), (1, 2)).isIdentical shouldBe true + } + "different first element should make them different" in { + compare((1, 2), (3, 2)) shouldBe DiffResultObject( + "Tuple2", + Map("_1" -> DiffResultValue(1, 3), "_2" -> IdenticalValue(2)) + ) + } + "different second element should make them different" in { + compare((1, 3), (1, 2)) shouldBe DiffResultObject( + "Tuple2", + Map("_1" -> IdenticalValue(1), "_2" -> DiffResultValue(3, 2)) + ) + } + } + "tuple3" - { + "equal tuples should be identical" in { + compare((1, 2, 3), (1, 2, 3)).isIdentical shouldBe true + } + "different first element should make them different" in { + compare((1, 2, 3), (4, 2, 3)) shouldBe DiffResultObject( + "Tuple3", + Map("_1" -> DiffResultValue(1, 4), "_2" -> IdenticalValue(2), "_3" -> IdenticalValue(3)) + ) + } + "different second element should make them different" in { + compare((1, 2, 3), (1, 4, 3)) shouldBe DiffResultObject( + "Tuple3", + Map("_1" -> IdenticalValue(1), "_2" -> DiffResultValue(2, 4), "_3" -> IdenticalValue(3)) + ) + } + "different third element should make them different" in { + compare((1, 2, 3), (1, 2, 4)) shouldBe DiffResultObject( + "Tuple3", + Map("_1" -> IdenticalValue(1), "_2" -> IdenticalValue(2), "_3" -> DiffResultValue(3, 4)) + ) + } + } + } +} + +case class Person(name: String, age: Int, in: Instant) + +case class Family(first: Person, second: Person) + +case class Organization(people: List[Person]) + +case class Startup(workers: Set[Person]) + +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 + +class HasCustomEquals(val s: String) { + override def equals(obj: Any): Boolean = { + obj match { + case o: HasCustomEquals => this.s.length == o.s.length + case _ => false + } + } +} + +sealed trait TsDirection + +object TsDirection { + case object Incoming extends TsDirection + + case object Outgoing extends TsDirection +} + +case class KeyModel(id: UUID, name: String) + +case class MyLookup(map: Map[KeyModel, String]) diff --git a/core/src/test/scala/com/softwaremill/diffx/test/ModifyMacroTest.scala b/core/src/test/scala/com/softwaremill/diffx/test/ModifyMacroTest.scala new file mode 100644 index 00000000..6ecdf51e --- /dev/null +++ b/core/src/test/scala/com/softwaremill/diffx/test/ModifyMacroTest.scala @@ -0,0 +1,28 @@ +package com.softwaremill.diffx.test + +import com.softwaremill.diffx.ModifyMacro +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class ModifyMacroTest extends AnyFlatSpec with Matchers { + it should "ignore field in nested products" in { + ModifyMacro.modifiedFromPath[Family, String](_.first.name) shouldBe List("first", "name") + } + + it should "ignore fields in list of products" in { + ModifyMacro.modifiedFromPath[Organization, String](_.people.each.name) shouldBe List("people", "name") + } + + it should "ignore fields in product wrapped with either" in { + ModifyMacro.modifiedFromPath[Either[Person, Person], String](_.eachRight.name) shouldBe List("eachRight", "name") + ModifyMacro.modifiedFromPath[Either[Person, Person], String](_.eachLeft.name) shouldBe List("eachLeft", "name") + } + + it should "ignore fields in product wrapped with option" in { + ModifyMacro.modifiedFromPath[Option[Person], String](_.each.name) shouldBe List("name") + } + + it should "ignore fields in map of products" in { + ModifyMacro.modifiedFromPath[Map[String, Person], String](_.each.name) shouldBe List("name") + } +} diff --git a/docs-sources/.gitignore b/docs-sources/.gitignore new file mode 100644 index 00000000..7bb92e53 --- /dev/null +++ b/docs-sources/.gitignore @@ -0,0 +1,2 @@ +_build +_build_html \ No newline at end of file diff --git a/docs-sources/.python-version b/docs-sources/.python-version new file mode 100644 index 00000000..0b2eb36f --- /dev/null +++ b/docs-sources/.python-version @@ -0,0 +1 @@ +3.7.2 diff --git a/docs-sources/Makefile b/docs-sources/Makefile new file mode 100644 index 00000000..298ea9e2 --- /dev/null +++ b/docs-sources/Makefile @@ -0,0 +1,19 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs-sources/README.md b/docs-sources/README.md deleted file mode 100644 index 67bd7ac4..00000000 --- a/docs-sources/README.md +++ /dev/null @@ -1,225 +0,0 @@ -![diffx](https://github.com/softwaremill/diffx/raw/master/banner.png) - -[![Build Status](https://travis-ci.org/softwaremill/diffx.svg?branch=master)](https://travis-ci.org/softwaremill/diffx) -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.softwaremill.diffx/diffx-core_2.13/badge.svg)](https://search.maven.org/search?q=g:com.softwaremill.diffx) -[![Gitter](https://badges.gitter.im/softwaremill/diffx.svg)](https://gitter.im/softwaremill/diffx?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) -[![Mergify Status](https://img.shields.io/endpoint.svg?url=https://gh.mergify.io/badges/softwaremill/diffx&style=flat)](https://mergify.io) -[![Scala Steward badge](https://img.shields.io/badge/Scala_Steward-helping-brightgreen.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAMAAAARSr4IAAAAVFBMVEUAAACHjojlOy5NWlrKzcYRKjGFjIbp293YycuLa3pYY2LSqql4f3pCUFTgSjNodYRmcXUsPD/NTTbjRS+2jomhgnzNc223cGvZS0HaSD0XLjbaSjElhIr+AAAAAXRSTlMAQObYZgAAAHlJREFUCNdNyosOwyAIhWHAQS1Vt7a77/3fcxxdmv0xwmckutAR1nkm4ggbyEcg/wWmlGLDAA3oL50xi6fk5ffZ3E2E3QfZDCcCN2YtbEWZt+Drc6u6rlqv7Uk0LdKqqr5rk2UCRXOk0vmQKGfc94nOJyQjouF9H/wCc9gECEYfONoAAAAASUVORK5CYII=)](https://scala-steward.org) - -Pretty diffs for case classes. - -The library is published for Scala 2.12 and 2.13. - -## Table of contents -- [goals of the project](#goals-of-the-project) -- [teaser](#teaser) -- [colors](#colors) -- integrations - - [scalatest](#scalatest-integration) - - [specs2](#specs2-integration) - - [utest](#utest-integration) - - [other](#other-3rd-party-libraries-support) -- [ignoring](#ignoring) -- [customization](#customization) -- [similar projects](#similar-projects) -- [commercial support](#commercial-support) - -## Goals of the project - -- 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 - -## Teaser -Add the following dependency: - -```scala -"com.softwaremill.diffx" %% "diffx-core" % "@VERSION@" -``` - -```scala mdoc -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)) -) - -val left: Foo = Foo( - Bar("asdf", 66), - List(1234), - Some(right) -) - - -import com.softwaremill.diffx._ -compare(left, right) -``` - -Will result in: - -![example](https://github.com/softwaremill/diff-x/blob/master/example.png?raw=true) - - -## Colors - -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. - -## Scalatest integration - -To use with scalatest, add the following dependency: - -```scala -"com.softwaremill.diffx" %% "diffx-scalatest" % "@VERSION@" % 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: - -```scala mdoc:compile-only -import org.scalatest.matchers.should.Matchers._ -import com.softwaremill.diffx.scalatest.DiffMatcher._ - -left should matchTo(right) -``` - -Giving you nice error messages: - -## Specs2 integration - -To use with specs2, add the following dependency: - -```scala -"com.softwaremill.diffx" %% "diffx-specs2" % "@VERSION@" % 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: - -```scala mdoc:compile-only -import org.specs2.matcher.MustMatchers.{left => _, right => _, _} -import com.softwaremill.diffx.specs2.DiffMatcher._ - -left must matchTo(right) -``` - -## Utest integration - -To use with utest, add following dependency: - -```scala -"com.softwaremill.diffx" %% "diffx-utest" % "@VERSION@" % 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: - -```scala mdoc:compile-only -import com.softwaremill.diffx.utest.DiffxAssertions._ -assertEqual(left, right) -``` - -## Ignoring - -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: - -```scala mdoc:compile-only -case class Person(name:String, age:Int) -implicit val modifiedDiff: Diff[Person] = Derived[Diff[Person]].ignore[Person,String](_.name) -``` - -## Customization - -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: - -```scala mdoc:nest -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 - -```scala mdoc:silent -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)) -``` -```scala mdoc -val a1: ABParent = A("1", "X") -val a2: ABParent = A("2", "X") - -compare(a1, a2) -``` - -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](https://github.com/ghik/silencer) compiler plugin to silent the warning -with the compiler option `"-P:silencer:globalFilters=^magnolia: using fallback derivation.*$"` - -## Other 3rd party libraries support - -- [com.softwaremill.common.tagging](https://github.com/softwaremill/scala-common) - ```scala - "com.softwaremill.diffx" %% "diffx-tagging" % "@VERSION@" - ``` - `com.softwaremill.diffx.tagging.DiffTaggingSupport` -- [eu.timepit.refined](https://github.com/fthomas/refined) - ```scala - "com.softwaremill.diffx" %% "diffx-refined" % "@VERSION@" - ``` - `com.softwaremill.diffx.refined.RefinedSupport` -- [org.typelevel.cats](https://github.com/typelevel/cats) - ```scala - "com.softwaremill.diffx" %% "diffx-cats" % "@VERSION@" - ``` - `com.softwaremill.diffx.cats.DiffCatsInstances` - -## Similar projects - -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](https://github.com/xdotai/diff) - based on shapeless, seems not to be activly developed anymore -- [ratatool-diffy](https://github.com/spotify/ratatool/tree/master/ratatool-diffy) - the main purpose is to compare large data sets stored on gs or hdfs - -## Commercial Support - -We offer commercial support for diffx and related technologies, as well as development services. [Contact us](https://softwaremill.com) to learn more about our offer! - -## Copyright - -Copyright (C) 2019 SoftwareMill [https://softwaremill.com](https://softwaremill.com). diff --git a/docs-sources/conf.py b/docs-sources/conf.py new file mode 100644 index 00000000..53a552c9 --- /dev/null +++ b/docs-sources/conf.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +# +# sttp documentation build configuration file, created by +# sphinx-quickstart on Thu Oct 12 15:51:09 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'diffx-scala' +copyright = '2021, SoftwareMill' +author = 'SoftwareMill' + +# The short X.Y version +version = '' +# The full version, including alpha/beta/rc tags +release = '' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +from recommonmark.parser import CommonMarkParser +from recommonmark.transform import AutoStructify + +source_parsers = { + '.md': CommonMarkParser, +} + +source_suffix = ['.rst', '.md'] + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# This is required for the alabaster theme +# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars +html_sidebars = { + '**': [ + 'about.html', + 'navigation.html', + 'relations.html', # needs 'show_related': True theme option to display + 'searchbox.html', + 'donate.html', + ] +} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'diffx-scaladoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'diffx-scala.tex', 'diffx-scala Documentation', + 'SoftwareMill', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'diffx-scala', 'diffx-scala Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'diffx-scala', 'diffx-scala Documentation', + author, 'diffx-scala', 'One line description of project.', + 'Miscellaneous'), +] + +highlight_language = 'scala' + +# configure edit on github: https://docs.readthedocs.io/en/latest/guides/vcs.html +html_context = { + 'display_github': True, # Integrate GitHub + 'github_user': 'softwaremill', # Username + 'github_repo': 'diffx', # Repo name + 'github_version': 'master', # Version + 'conf_py_path': '/docs-sources/', # Path in the checkout to the docs root +} + +# app setup hook +def setup(app): + app.add_config_value('recommonmark_config', { + 'auto_toc_tree_section': 'Contents', + 'enable_auto_doc_ref': False + }, True) + app.add_transform(AutoStructify) + diff --git a/docs-sources/index.md b/docs-sources/index.md new file mode 100644 index 00000000..baa4a378 --- /dev/null +++ b/docs-sources/index.md @@ -0,0 +1,104 @@ +# diffx: Pretty diffs for case classes + +Welcome! + +[diffx](https://github.com/softwaremill/diffx) is an open-source library which aims to display differences between +complex structures in a way that they are easily noticeable. + +Here's a quick example of diffx in action: + +```scala mdoc +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)) +) + +val left: Foo = Foo( + Bar("asdf", 66), + List(1234), + Some(right) +) + +import com.softwaremill.diffx.generic.auto._ +import com.softwaremill.diffx._ +compare(left, right) +``` + +Will result in: + +![](https://github.com/softwaremill/diffx/blob/master/example.png?raw=true) + +`diffx` is available for Scala 2.12 and 2.13 both jvm and js. + +The core of `diffx` comes in a single jar. + +To integrate with the test framework of your choice, you'll need to use one of the integration modules. +See the section on [test-frameworks](test-frameworks/summary.md) for a brief overview of supported test frameworks. + +*Auto-derivation is used throughout the documentation for the sake of clarity. Head over to [derivation](usage/derivation.md) for more details* + +## Tips and tricks + +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](https://github.com/ghik/silencer) compiler plugin to silent the warning +with the compiler option `"-P:silencer:globalFilters=^magnolia: using fallback derivation.*$"` + +## Similar projects + +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](https://github.com/xdotai/diff) - based on shapeless, seems not to be activly developed anymore +- [ratatool-diffy](https://github.com/spotify/ratatool/tree/master/ratatool-diffy) - the main purpose is to compare large data sets stored on gs or hdfs + + +## Sponsors + +Development and maintenance of diffx is sponsored by [SoftwareMill](https://softwaremill.com), +a software development and consulting company. We help clients scale their business through software. Our areas of expertise include backends, distributed systems, blockchain, machine learning and data analytics. + +[![](https://files.softwaremill.com/logo/logo.png "SoftwareMill")](https://softwaremill.com) + +# Table of contents + +```eval_rst +.. toctree:: + :maxdepth: 1 + :caption: Test frameworks + + test-frameworks/scalatest + test-frameworks/specs2 + test-frameworks/utest + test-frameworks/munit + test-frameworks/summary + +.. toctree:: + :maxdepth: 1 + :caption: Integrations + + integrations/cats + integrations/tagging + integrations/refined + +.. toctree:: + :maxdepth: 1 + :caption: usage + + usage/derivation + usage/ignoring + usage/replacing + usage/extending + usage/sequences + usage/output +``` + +## Copyright + +Copyright (C) 2019 SoftwareMill [https://softwaremill.com](https://softwaremill.com). diff --git a/docs-sources/integrations/cats.md b/docs-sources/integrations/cats.md new file mode 100644 index 00000000..93b44157 --- /dev/null +++ b/docs-sources/integrations/cats.md @@ -0,0 +1,36 @@ +# cats + +This module contains integration layer between [org.typelevel.cats](https://github.com/typelevel/cats) and `diffx` + +## sbt + +```scala +"com.softwaremill.diffx" %% "diffx-cats" % "@VERSION@" % Test +``` + +## mill + +```scala +ivy"com.softwaremill.diffx::diffx-cats::@VERSION@" +``` + +## Usage + +Assuming you have some data types from the cats library in your hierarchy: +```scala mdoc:silent +import cats.data._ +case class TestData(ints: NonEmptyList[String]) + +val t1 = TestData(NonEmptyList.one("a")) +val t2 = TestData(NonEmptyList.one("b")) +``` + +all you need to do is to put additional diffx implicits into current scope: + +```scala mdoc +import com.softwaremill.diffx.compare +import com.softwaremill.diffx.generic.auto._ + +import com.softwaremill.diffx.cats._ +compare(t1, t2) +``` \ No newline at end of file diff --git a/docs-sources/integrations/refined.md b/docs-sources/integrations/refined.md new file mode 100644 index 00000000..cf4ad830 --- /dev/null +++ b/docs-sources/integrations/refined.md @@ -0,0 +1,40 @@ +# refined + +This module contains integration layer between [eu.timepit.refined](https://github.com/fthomas/refined) and `diffx` + +## sbt + +```scala +"com.softwaremill.diffx" %% "diffx-refined" % "@VERSION@" % Test +``` + +## mill + +```scala +ivy"com.softwaremill.diffx::diffx-refined::@VERSION@" +``` + +## Usage + +Assuming you have some refined types in your hierarchy: + +```scala mdoc:silent +import eu.timepit.refined.types.numeric.PosInt +import eu.timepit.refined.auto._ +import eu.timepit.refined.types.string.NonEmptyString + +case class TestData(posInt: PosInt, nonEmptyString: NonEmptyString) + +val t1 = TestData(1, "foo") +val t2 = TestData(1, "bar") +``` + +all you need to do is to put additional diffx implicits into current scope: + +```scala mdoc +import com.softwaremill.diffx.compare +import com.softwaremill.diffx.generic.auto._ + +import com.softwaremill.diffx.refined._ +compare(t1, t2) +``` \ No newline at end of file diff --git a/docs-sources/integrations/tagging.md b/docs-sources/integrations/tagging.md new file mode 100644 index 00000000..f64f7767 --- /dev/null +++ b/docs-sources/integrations/tagging.md @@ -0,0 +1,39 @@ +# tagging + +This module contains integration layer between [com.softwaremill.common.tagging](https://github.com/softwaremill/scala-common) and `diffx` + +## sbt + +```scala +"com.softwaremill.diffx" %% "diffx-tagging" % "@VERSION@" +``` + +## mill + +```scala +ivy"com.softwaremill.diffx::diffx-tagging::@VERSION@" +``` + +## Usage + +Assuming you have some tagged types in your hierarchy: + +```scala mdoc:silent +import com.softwaremill.tagging._ +sealed trait T1 +sealed trait T2 +case class TestData(p1: Int @@ T1, p2: Int @@ T2) + +val t1 = TestData(1.taggedWith[T1], 1.taggedWith[T2]) +val t2 = TestData(1.taggedWith[T1], 3.taggedWith[T2]) +``` + +all you need to do is to put additional diffx implicits into current scope: + +```scala mdoc +import com.softwaremill.diffx.compare +import com.softwaremill.diffx.generic.auto._ + +import com.softwaremill.diffx.tagging._ +compare(t1, t2) +``` \ No newline at end of file diff --git a/docs-sources/make.bat b/docs-sources/make.bat new file mode 100644 index 00000000..7893348a --- /dev/null +++ b/docs-sources/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs-sources/requirements.pip b/docs-sources/requirements.pip new file mode 100644 index 00000000..cb4b2b97 --- /dev/null +++ b/docs-sources/requirements.pip @@ -0,0 +1,4 @@ +sphinx_rtd_theme==0.4.3 +recommonmark==0.5.0 +sphinx==2.0.1 +sphinx-autobuild==0.7.1 diff --git a/docs-sources/test-frameworks/munit.md b/docs-sources/test-frameworks/munit.md new file mode 100644 index 00000000..eec4e838 --- /dev/null +++ b/docs-sources/test-frameworks/munit.md @@ -0,0 +1,44 @@ +# munit + +To use with munit, add following dependency: + +## sbt + +```scala +"com.softwaremill.diffx" %% "diffx-munit" % "@VERSION@" % Test +``` + +## mill + +```scala +ivy"com.softwaremill.diffx::diffx-munit::@VERSION@" +``` + +## Usage + +Then, mixin `DiffxAssertions` trait or add `import com.softwaremill.diffx.munit.DiffxAssertions._` to your test code. +To assert using diffx use `assertEquals` as follows: + +```scala mdoc:compile-only +import com.softwaremill.diffx.munit.DiffxAssertions._ +import com.softwaremill.diffx.generic.auto._ + +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)) +) + +val left: Foo = Foo( + Bar("asdf", 66), + List(1234), + Some(right) +) + +assertEqual(left, right) +``` + diff --git a/docs-sources/test-frameworks/scalatest.md b/docs-sources/test-frameworks/scalatest.md new file mode 100644 index 00000000..fac5261f --- /dev/null +++ b/docs-sources/test-frameworks/scalatest.md @@ -0,0 +1,44 @@ +# scalatest + +To use with scalatest, add the following dependency: + +## sbt + +```scala +"com.softwaremill.diffx" %% "diffx-scalatest" % "@VERSION@" % Test +``` + +## mill + +```scala +ivy"com.softwaremill.diffx::diffx-scalatest::@VERSION@" +``` + +## Usage + +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: + +```scala mdoc:compile-only +import org.scalatest.matchers.should.Matchers._ +import com.softwaremill.diffx.scalatest.DiffMatcher._ +import com.softwaremill.diffx.generic.auto._ + +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)) +) + +val left: Foo = Foo( + Bar("asdf", 66), + List(1234), + Some(right) +) + +left should matchTo(right) +``` diff --git a/docs-sources/test-frameworks/specs2.md b/docs-sources/test-frameworks/specs2.md new file mode 100644 index 00000000..3e592e7a --- /dev/null +++ b/docs-sources/test-frameworks/specs2.md @@ -0,0 +1,44 @@ +# specs2 + +To use with specs2, add the following dependency: + +## sbt + +```scala +"com.softwaremill.diffx" %% "diffx-specs2" % "@VERSION@" % Test +``` + +## mill + +```scala +ivy"com.softwaremill.diffx::diffx-specs2::@VERSION@" +``` + +## Usage + +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: + +```scala mdoc:compile-only +import org.specs2.matcher.MustMatchers.{left => _, right => _, _} +import com.softwaremill.diffx.specs2.DiffMatcher._ +import com.softwaremill.diffx.generic.auto._ + +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)) +) + +val left: Foo = Foo( + Bar("asdf", 66), + List(1234), + Some(right) +) + +left must matchTo(right) +``` diff --git a/docs-sources/test-frameworks/summary.md b/docs-sources/test-frameworks/summary.md new file mode 100644 index 00000000..69e091e7 --- /dev/null +++ b/docs-sources/test-frameworks/summary.md @@ -0,0 +1,10 @@ +# summary + +Following test frameworks are supported by diffx: +- [scalatest](scalatest.md) +- [specs2](specs2.md) +- [utest](utest.md) +- [munit](munit.md) + +Didn't find your favourite testing library? Don't hesitate and let us know, or you can add it on your own , +as all that needs to be done is to call `compare` function. \ No newline at end of file diff --git a/docs-sources/test-frameworks/utest.md b/docs-sources/test-frameworks/utest.md new file mode 100644 index 00000000..99d6ebcc --- /dev/null +++ b/docs-sources/test-frameworks/utest.md @@ -0,0 +1,44 @@ +# utest + +To use with utest, add following dependency: + +## sbt + +```scala +"com.softwaremill.diffx" %% "diffx-utest" % "@VERSION@" % Test +``` + +## mill + +```scala +ivy"com.softwaremill.diffx::diffx-utest::@VERSION@" +``` + +## Usage + +Then, mixin `DiffxAssertions` trait or add `import com.softwaremill.diffx.utest.DiffxAssertions._` to your test code. +To assert using diffx use `assertEquals` as follows: + +```scala mdoc:compile-only +import com.softwaremill.diffx.utest.DiffxAssertions._ +import com.softwaremill.diffx.generic.auto._ + +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)) +) + +val left: Foo = Foo( + Bar("asdf", 66), + List(1234), + Some(right) +) + +assertEqual(left, right) +``` + diff --git a/docs-sources/usage/derivation.md b/docs-sources/usage/derivation.md new file mode 100644 index 00000000..8b9ec049 --- /dev/null +++ b/docs-sources/usage/derivation.md @@ -0,0 +1,23 @@ +# derivation + +Diffx supports auto and semi-auto derivation. + +For semi-auto derivation you don't need any additional import, just define your instances using: +```scala mdoc:compile-only +import com.softwaremill.diffx._ +case class Product(name: String) +case class Basket(products: List[Product]) + +implicit val productDiff = Diff.derived[Product] +implicit val basketDiff = Diff.derived[Basket] +``` + +To use auto derivation add following import + +`import com.softwaremill.diffx.generic.auto._` + +or extend trait + +`com.softwaremill.diffx.generic.AutoDerivation` + +**Auto derivation might have a huge impact on compilation times**, because of that it is recommended to use `semi-auto` derivation. diff --git a/docs-sources/usage/extending.md b/docs-sources/usage/extending.md new file mode 100644 index 00000000..1f345964 --- /dev/null +++ b/docs-sources/usage/extending.md @@ -0,0 +1,22 @@ +# extending + +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. + +Consider following example with `NonEmptyList` from cats. `NonEmptyList` is implemented as case class, +so the default behavior of diffx would be to create a `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 (for any iterable to be precise). +All we need 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: + +```scala mdoc:compile-only +import com.softwaremill.diffx._ +import _root_.cats.data.NonEmptyList +implicit def nelDiff[T: Diff]: Diff[NonEmptyList[T]] = + Diff[List[T]].contramap[NonEmptyList[T]](_.toList) +``` + +*Note: There is a [diffx-cats](../integrations/cats.md) module, so you don't have to do this* \ No newline at end of file diff --git a/docs-sources/usage/ignoring.md b/docs-sources/usage/ignoring.md new file mode 100644 index 00000000..bb462e12 --- /dev/null +++ b/docs-sources/usage/ignoring.md @@ -0,0 +1,27 @@ +# ignoring + +```scala mdoc:invisible +import com.softwaremill.diffx.generic.auto._ +import com.softwaremill.diffx._ +``` + +Fields can be excluded from comparison 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. + +```scala mdoc:compile-only +case class Person(name:String, age:Int) +val modifiedDiff: Diff[Person] = Diff[Person].ignore(_.name) +``` + +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: + +```scala mdoc:silent +case class Person(name:String, age:Int) +implicit val modifiedDiff: Diff[Person] = Derived[Diff[Person]].ignore(_.age) +``` +```scala mdoc +compare(Person("bob", 25), Person("bob", 30)) +``` \ No newline at end of file diff --git a/docs-sources/usage/output.md b/docs-sources/usage/output.md new file mode 100644 index 00000000..c022e53f --- /dev/null +++ b/docs-sources/usage/output.md @@ -0,0 +1,58 @@ +# output + +```scala mdoc:invisible +import com.softwaremill.diffx._ +import com.softwaremill.diffx.generic.auto._ +``` + +`diffx` does its best to show the difference in the most readable way, but obviously the default configuration won't +cover all the use-cases. Because of that, there are few ways how you can modify its output. + +## intellij idea + +If you are running tests using **IntelliJ IDEA**'s test runner, you will want +to turn off the red text coloring it uses for test failure outputs because +it interferes with difflicious' color outputs. + +In File | Settings | Editor | Color Scheme | Console Colors | Console | Error Output, uncheck the red foreground color. +(Solution provided by Jacob Wang @jatcwang) + +## colors & signs + +I found it confusing to use the terms `expected`/`actual` as there seems to be no golden rule whether to keep expected on the right side or on the left side. +Because of that, diffx refers to the values that are compared as `left` and `right` value. + +By default, the difference is shown in the following form: + +`leftColor(leftValue) -> rightColor(rightValue)` + +which in terms of missing/additional values e.g. in collections looks as follows: + +`leftColor(additionalValue)` in case the value was present on the left-hand side and absent on the right side +`rightColor(missingValue)` in case the value was absent on the left-hand side and present on the right side + + +Where, by default, `rightColor` is green and `leftColor` is red. + +Colors can be customized providing an implicit instance of `ConsoleColorConfig` class. +In fact `rightColor` and `leftColor` are functions `string => string` so they can be modified to do whatever you want with the output. +One example of that would be to use some special characters instead of colors, which might be useful on some environments like e.g. CI. + +````scala mdoc:compile-only +val colorConfigWithPlusMinus: ConsoleColorConfig = + ConsoleColorConfig(default = identity, arrow = identity, right = s => "+" + s, left = s => "-" + s) +```` + +There are two predefined set of colors - light and dark theme. +The default theme is dark, and it can be changed using environment variable - `DIFFX_COLOR_THEME`(`light`/`dark`). + +## skipping identical + +In some cases it might be desired to skip rendering the identical fields, to do that simple set `showIgnored` to `false`. + +```scala mdoc +case class Person(name:String, age:Int) + +val result = compare(Person("Bob", 23), Person("Alice", 23)) +result.show(renderIdentical = false) +``` \ No newline at end of file diff --git a/docs-sources/usage/replacing.md b/docs-sources/usage/replacing.md new file mode 100644 index 00000000..7304c0e9 --- /dev/null +++ b/docs-sources/usage/replacing.md @@ -0,0 +1,40 @@ +# replacing + +Sometimes you might want to compare some nested values using a different comparator but +the type they share is not unique within that hierarchy. + +Consider following example: +```scala mdoc +case class Person(age: Int, weight: Int) +``` + +If we would like to compare `weight` differently than `age` we would have to introduce a new type for `weight` +in order to provide a different `Diff` typeclass for only that field. While in general, it is a good idea to have your types +very precise it might not always be practical or even possible. Fortunately, diffx comes with a mechanism which allows +the replacement of nested diff instances. + +First we need to acquire a lens at given path using `modify` method, +and then we can call `setTo` to replace a particular instance. + +```scala mdoc:silent +import com.softwaremill.diffx._ +implicit val diffPerson: Derived[Diff[Person]] = Diff.derived[Person].modify(_.weight) + .setTo(Diff.approximate(epsilon=5)) +``` + +```scala mdoc +compare(Person(23, 60), Person(23, 62)) +``` + +In fact, replacement is so powerful that ignoring is implemented as a replacement +with the `Diff.ignore` instance. + +You can use the same mechanism to set particular object matcher for given nested collection in the hierarchy. +Depending, whether it is list, set or map a respective method needs to be called: +```scala mdoc:silent +case class Organization(peopleList: List[Person], peopleSet: Set[Person], peopleMap: Map[String, Person]) +implicit val diffOrg: Derived[Diff[Organization]] = Diff.derived[Organization] + .modify(_.peopleList).withListMatcher[Person](ObjectMatcher.byValue(_.age)) + .modify(_.peopleSet).withSetMatcher[Person](ObjectMatcher.by(_.age)) + .modify(_.peopleMap).withMapMatcher[String,Person](ObjectMatcher.byValue(_.age)) +``` \ No newline at end of file diff --git a/docs-sources/usage/sequences.md b/docs-sources/usage/sequences.md new file mode 100644 index 00000000..f3e5b0c6 --- /dev/null +++ b/docs-sources/usage/sequences.md @@ -0,0 +1,68 @@ +# sequences + +`diffx` provides instances for many containers from scala's standard library (e.g. lists, sets, maps), however +not all collections can be simply compared. Ordered collections like lists or vectors are compared by default by +comparing elements under the same indexes. +Maps, by default, are compared by comparing values under the respective keys. +For unordered collections there is an `ObjectMapper` typeclass which defines how elements should be paired. + +## object matcher + +In general, it is a very simple interface, with a bunch of factory methods. +```scala mdoc:compile-only +trait ObjectMatcher[T] { + def isSameObject(left: T, right: T): Boolean +} +``` + +It is mostly useful when comparing unordered collections like sets: + +```scala mdoc:silent +import com.softwaremill.diffx._ +import com.softwaremill.diffx.generic.auto._ +case class Person(id: String, name: String) + +implicit val personMatcher: ObjectMatcher[Person] = ObjectMatcher.by(_.id) +val bob = Person("1","Bob") +``` +```scala mdoc +compare(Set(bob), Set(bob, Person("2","Alice"))) +``` + +It can be also used to modify how the entries from maps are paired. +In below example we tell `diffx` to compare these maps by paring entries by values using the defined `personMatcher` +```scala mdoc:reset:silent +import com.softwaremill.diffx._ +import com.softwaremill.diffx.generic.auto._ +import com.softwaremill.diffx.ObjectMatcher.MapEntry +case class Person(id: String, name: String) + +val personMatcher: ObjectMatcher[Person] = ObjectMatcher.by(_.id) +implicit val om: ObjectMatcher[MapEntry[String, Person]] = ObjectMatcher.byValue(personMatcher) +val bob = Person("1","Bob") +``` + +```scala mdoc +compare(Map("1" -> bob), Map("2" -> bob)) +``` + +Last but not least you can use `objectMatcher` to customize paring when comparing indexed collections. +Such collections are treated similarly to maps (they use key-value object matcher), +but the key type is bound to `Int` (`IterableEntry` is an alias for `MapEntry[Int,V]`). + +```scala mdoc:reset:silent +import com.softwaremill.diffx._ +import com.softwaremill.diffx.generic.auto._ +import com.softwaremill.diffx.ObjectMatcher.IterableEntry +case class Person(id: String, name: String) + +implicit val personMatcher: ObjectMatcher[IterableEntry[Person]] = ObjectMatcher.byValue(_.id) +val bob = Person("1","Bob") +val alice = Person("2","Alice") +``` +```scala mdoc +compare(List(bob, alice), List(alice, bob)) +``` + +*Note: `ObjectMatcher` can be also passed explicitly, either upon creation or during modification* +*See [replacing](replacing.md) for details.* \ No newline at end of file diff --git a/docs-sources/watch.sh b/docs-sources/watch.sh new file mode 100755 index 00000000..24c43727 --- /dev/null +++ b/docs-sources/watch.sh @@ -0,0 +1,2 @@ +#!/bin/bash +sphinx-autobuild . _build/html diff --git a/generated-docs/out/.gitignore b/generated-docs/out/.gitignore new file mode 100644 index 00000000..7bb92e53 --- /dev/null +++ b/generated-docs/out/.gitignore @@ -0,0 +1,2 @@ +_build +_build_html \ No newline at end of file diff --git a/generated-docs/out/.python-version b/generated-docs/out/.python-version new file mode 100644 index 00000000..0b2eb36f --- /dev/null +++ b/generated-docs/out/.python-version @@ -0,0 +1 @@ +3.7.2 diff --git a/generated-docs/out/Makefile b/generated-docs/out/Makefile new file mode 100644 index 00000000..298ea9e2 --- /dev/null +++ b/generated-docs/out/Makefile @@ -0,0 +1,19 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/generated-docs/out/conf.py b/generated-docs/out/conf.py new file mode 100644 index 00000000..53a552c9 --- /dev/null +++ b/generated-docs/out/conf.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +# +# sttp documentation build configuration file, created by +# sphinx-quickstart on Thu Oct 12 15:51:09 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'diffx-scala' +copyright = '2021, SoftwareMill' +author = 'SoftwareMill' + +# The short X.Y version +version = '' +# The full version, including alpha/beta/rc tags +release = '' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +from recommonmark.parser import CommonMarkParser +from recommonmark.transform import AutoStructify + +source_parsers = { + '.md': CommonMarkParser, +} + +source_suffix = ['.rst', '.md'] + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# This is required for the alabaster theme +# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars +html_sidebars = { + '**': [ + 'about.html', + 'navigation.html', + 'relations.html', # needs 'show_related': True theme option to display + 'searchbox.html', + 'donate.html', + ] +} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'diffx-scaladoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'diffx-scala.tex', 'diffx-scala Documentation', + 'SoftwareMill', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'diffx-scala', 'diffx-scala Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'diffx-scala', 'diffx-scala Documentation', + author, 'diffx-scala', 'One line description of project.', + 'Miscellaneous'), +] + +highlight_language = 'scala' + +# configure edit on github: https://docs.readthedocs.io/en/latest/guides/vcs.html +html_context = { + 'display_github': True, # Integrate GitHub + 'github_user': 'softwaremill', # Username + 'github_repo': 'diffx', # Repo name + 'github_version': 'master', # Version + 'conf_py_path': '/docs-sources/', # Path in the checkout to the docs root +} + +# app setup hook +def setup(app): + app.add_config_value('recommonmark_config', { + 'auto_toc_tree_section': 'Contents', + 'enable_auto_doc_ref': False + }, True) + app.add_transform(AutoStructify) + diff --git a/generated-docs/out/index.md b/generated-docs/out/index.md new file mode 100644 index 00000000..7749e947 --- /dev/null +++ b/generated-docs/out/index.md @@ -0,0 +1,143 @@ +# diffx: Pretty diffs for case classes + +Welcome! + +[diffx](https://github.com/softwaremill/diffx) is an open-source library which aims to display differences between +complex structures in a way that they are easily noticeable. + +Here's a quick example of diffx in action: + +```scala +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 = Bar(s = "asdf", i = 5), +// b = List(123, 1234), +// parent = Some(value = Bar(s = "asdf", i = 5)) +// ) + +val left: Foo = Foo( + Bar("asdf", 66), + List(1234), + Some(right) +) +// left: Foo = Foo( +// bar = Bar(s = "asdf", i = 66), +// b = List(1234), +// parent = Some( +// value = Foo( +// bar = Bar(s = "asdf", i = 5), +// b = List(123, 1234), +// parent = Some(value = Bar(s = "asdf", i = 5)) +// ) +// ) +// ) + +import com.softwaremill.diffx.generic.auto._ +import com.softwaremill.diffx._ +compare(left, right) +// res0: DiffResult = DiffResultObject( +// name = "Foo", +// fields = ListMap( +// "bar" -> DiffResultObject( +// name = "Bar", +// fields = ListMap( +// "s" -> IdenticalValue(value = "asdf"), +// "i" -> DiffResultValue(left = 66, right = 5) +// ) +// ), +// "b" -> DiffResultObject( +// name = "List", +// fields = ListMap( +// "0" -> DiffResultValue(left = 1234, right = 123), +// "1" -> DiffResultMissing(value = 1234) +// ) +// ), +// "parent" -> DiffResultValue( +// left = "repl.MdocSession.App.Foo", +// right = "repl.MdocSession.App.Bar" +// ) +// ) +// ) +``` + +Will result in: + +![](https://github.com/softwaremill/diffx/blob/master/example.png?raw=true) + +`diffx` is available for Scala 2.12 and 2.13 both jvm and js. + +The core of `diffx` comes in a single jar. + +To integrate with the test framework of your choice, you'll need to use one of the integration modules. +See the section on [test-frameworks](test-frameworks/summary.md) for a brief overview of supported test frameworks. + +*Auto-derivation is used throughout the documentation for the sake of clarity. Head over to [derivation](usage/derivation.md) for more details* + +## Tips and tricks + +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](https://github.com/ghik/silencer) compiler plugin to silent the warning +with the compiler option `"-P:silencer:globalFilters=^magnolia: using fallback derivation.*$"` + +## Similar projects + +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](https://github.com/xdotai/diff) - based on shapeless, seems not to be activly developed anymore +- [ratatool-diffy](https://github.com/spotify/ratatool/tree/master/ratatool-diffy) - the main purpose is to compare large data sets stored on gs or hdfs + + +## Sponsors + +Development and maintenance of diffx is sponsored by [SoftwareMill](https://softwaremill.com), +a software development and consulting company. We help clients scale their business through software. Our areas of expertise include backends, distributed systems, blockchain, machine learning and data analytics. + +[![](https://files.softwaremill.com/logo/logo.png "SoftwareMill")](https://softwaremill.com) + +# Table of contents + +```eval_rst +.. toctree:: + :maxdepth: 1 + :caption: Test frameworks + + test-frameworks/scalatest + test-frameworks/specs2 + test-frameworks/utest + test-frameworks/munit + test-frameworks/summary + +.. toctree:: + :maxdepth: 1 + :caption: Integrations + + integrations/cats + integrations/tagging + integrations/refined + +.. toctree:: + :maxdepth: 1 + :caption: usage + + usage/derivation + usage/ignoring + usage/replacing + usage/extending + usage/sequences + usage/output +``` + +## Copyright + +Copyright (C) 2019 SoftwareMill [https://softwaremill.com](https://softwaremill.com). diff --git a/generated-docs/out/integrations/cats.md b/generated-docs/out/integrations/cats.md new file mode 100644 index 00000000..3ceadf7e --- /dev/null +++ b/generated-docs/out/integrations/cats.md @@ -0,0 +1,47 @@ +# cats + +This module contains integration layer between [org.typelevel.cats](https://github.com/typelevel/cats) and `diffx` + +## sbt + +```scala +"com.softwaremill.diffx" %% "diffx-cats" % "0.5.3" % Test +``` + +## mill + +```scala +ivy"com.softwaremill.diffx::diffx-cats::0.5.3" +``` + +## Usage + +Assuming you have some data types from the cats library in your hierarchy: +```scala +import cats.data._ +case class TestData(ints: NonEmptyList[String]) + +val t1 = TestData(NonEmptyList.one("a")) +val t2 = TestData(NonEmptyList.one("b")) +``` + +all you need to do is to put additional diffx implicits into current scope: + +```scala +import com.softwaremill.diffx.compare +import com.softwaremill.diffx.generic.auto._ + +import com.softwaremill.diffx.cats._ +compare(t1, t2) +// res0: com.softwaremill.diffx.DiffResult = DiffResultObject( +// name = "TestData", +// fields = ListMap( +// "ints" -> DiffResultObject( +// name = "List", +// fields = ListMap( +// "0" -> DiffResultString(diffs = List(DiffResultValue(left = "a", right = "b"))) +// ) +// ) +// ) +// ) +``` \ No newline at end of file diff --git a/generated-docs/out/integrations/refined.md b/generated-docs/out/integrations/refined.md new file mode 100644 index 00000000..392d4473 --- /dev/null +++ b/generated-docs/out/integrations/refined.md @@ -0,0 +1,49 @@ +# refined + +This module contains integration layer between [eu.timepit.refined](https://github.com/fthomas/refined) and `diffx` + +## sbt + +```scala +"com.softwaremill.diffx" %% "diffx-refined" % "0.5.3" % Test +``` + +## mill + +```scala +ivy"com.softwaremill.diffx::diffx-refined::0.5.3" +``` + +## Usage + +Assuming you have some refined types in your hierarchy: + +```scala +import eu.timepit.refined.types.numeric.PosInt +import eu.timepit.refined.auto._ +import eu.timepit.refined.types.string.NonEmptyString + +case class TestData(posInt: PosInt, nonEmptyString: NonEmptyString) + +val t1 = TestData(1, "foo") +val t2 = TestData(1, "bar") +``` + +all you need to do is to put additional diffx implicits into current scope: + +```scala +import com.softwaremill.diffx.compare +import com.softwaremill.diffx.generic.auto._ + +import com.softwaremill.diffx.refined._ +compare(t1, t2) +// res0: com.softwaremill.diffx.DiffResult = DiffResultObject( +// name = "TestData", +// fields = ListMap( +// "posInt" -> IdenticalValue(value = 1), +// "nonEmptyString" -> DiffResultString( +// diffs = List(DiffResultValue(left = "foo", right = "bar")) +// ) +// ) +// ) +``` \ No newline at end of file diff --git a/generated-docs/out/integrations/tagging.md b/generated-docs/out/integrations/tagging.md new file mode 100644 index 00000000..4cdb69c3 --- /dev/null +++ b/generated-docs/out/integrations/tagging.md @@ -0,0 +1,46 @@ +# tagging + +This module contains integration layer between [com.softwaremill.common.tagging](https://github.com/softwaremill/scala-common) and `diffx` + +## sbt + +```scala +"com.softwaremill.diffx" %% "diffx-tagging" % "0.5.3" +``` + +## mill + +```scala +ivy"com.softwaremill.diffx::diffx-tagging::0.5.3" +``` + +## Usage + +Assuming you have some tagged types in your hierarchy: + +```scala +import com.softwaremill.tagging._ +sealed trait T1 +sealed trait T2 +case class TestData(p1: Int @@ T1, p2: Int @@ T2) + +val t1 = TestData(1.taggedWith[T1], 1.taggedWith[T2]) +val t2 = TestData(1.taggedWith[T1], 3.taggedWith[T2]) +``` + +all you need to do is to put additional diffx implicits into current scope: + +```scala +import com.softwaremill.diffx.compare +import com.softwaremill.diffx.generic.auto._ + +import com.softwaremill.diffx.tagging._ +compare(t1, t2) +// res0: com.softwaremill.diffx.DiffResult = DiffResultObject( +// name = "TestData", +// fields = ListMap( +// "p1" -> IdenticalValue(value = 1), +// "p2" -> DiffResultValue(left = 1, right = 3) +// ) +// ) +``` \ No newline at end of file diff --git a/generated-docs/out/make.bat b/generated-docs/out/make.bat new file mode 100644 index 00000000..7893348a --- /dev/null +++ b/generated-docs/out/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/generated-docs/out/requirements.pip b/generated-docs/out/requirements.pip new file mode 100644 index 00000000..cb4b2b97 --- /dev/null +++ b/generated-docs/out/requirements.pip @@ -0,0 +1,4 @@ +sphinx_rtd_theme==0.4.3 +recommonmark==0.5.0 +sphinx==2.0.1 +sphinx-autobuild==0.7.1 diff --git a/generated-docs/out/test-frameworks/munit.md b/generated-docs/out/test-frameworks/munit.md new file mode 100644 index 00000000..7d006250 --- /dev/null +++ b/generated-docs/out/test-frameworks/munit.md @@ -0,0 +1,44 @@ +# munit + +To use with munit, add following dependency: + +## sbt + +```scala +"com.softwaremill.diffx" %% "diffx-munit" % "0.5.3" % Test +``` + +## mill + +```scala +ivy"com.softwaremill.diffx::diffx-munit::0.5.3" +``` + +## Usage + +Then, mixin `DiffxAssertions` trait or add `import com.softwaremill.diffx.munit.DiffxAssertions._` to your test code. +To assert using diffx use `assertEquals` as follows: + +```scala +import com.softwaremill.diffx.munit.DiffxAssertions._ +import com.softwaremill.diffx.generic.auto._ + +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)) +) + +val left: Foo = Foo( + Bar("asdf", 66), + List(1234), + Some(right) +) + +assertEqual(left, right) +``` + diff --git a/generated-docs/out/test-frameworks/scalatest.md b/generated-docs/out/test-frameworks/scalatest.md new file mode 100644 index 00000000..707d6d39 --- /dev/null +++ b/generated-docs/out/test-frameworks/scalatest.md @@ -0,0 +1,44 @@ +# scalatest + +To use with scalatest, add the following dependency: + +## sbt + +```scala +"com.softwaremill.diffx" %% "diffx-scalatest" % "0.5.3" % Test +``` + +## mill + +```scala +ivy"com.softwaremill.diffx::diffx-scalatest::0.5.3" +``` + +## Usage + +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: + +```scala +import org.scalatest.matchers.should.Matchers._ +import com.softwaremill.diffx.scalatest.DiffMatcher._ +import com.softwaremill.diffx.generic.auto._ + +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)) +) + +val left: Foo = Foo( + Bar("asdf", 66), + List(1234), + Some(right) +) + +left should matchTo(right) +``` diff --git a/generated-docs/out/test-frameworks/specs2.md b/generated-docs/out/test-frameworks/specs2.md new file mode 100644 index 00000000..8b15ee8d --- /dev/null +++ b/generated-docs/out/test-frameworks/specs2.md @@ -0,0 +1,44 @@ +# specs2 + +To use with specs2, add the following dependency: + +## sbt + +```scala +"com.softwaremill.diffx" %% "diffx-specs2" % "0.5.3" % Test +``` + +## mill + +```scala +ivy"com.softwaremill.diffx::diffx-specs2::0.5.3" +``` + +## Usage + +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: + +```scala +import org.specs2.matcher.MustMatchers.{left => _, right => _, _} +import com.softwaremill.diffx.specs2.DiffMatcher._ +import com.softwaremill.diffx.generic.auto._ + +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)) +) + +val left: Foo = Foo( + Bar("asdf", 66), + List(1234), + Some(right) +) + +left must matchTo(right) +``` diff --git a/generated-docs/out/test-frameworks/summary.md b/generated-docs/out/test-frameworks/summary.md new file mode 100644 index 00000000..69e091e7 --- /dev/null +++ b/generated-docs/out/test-frameworks/summary.md @@ -0,0 +1,10 @@ +# summary + +Following test frameworks are supported by diffx: +- [scalatest](scalatest.md) +- [specs2](specs2.md) +- [utest](utest.md) +- [munit](munit.md) + +Didn't find your favourite testing library? Don't hesitate and let us know, or you can add it on your own , +as all that needs to be done is to call `compare` function. \ No newline at end of file diff --git a/generated-docs/out/test-frameworks/utest.md b/generated-docs/out/test-frameworks/utest.md new file mode 100644 index 00000000..cc076585 --- /dev/null +++ b/generated-docs/out/test-frameworks/utest.md @@ -0,0 +1,44 @@ +# utest + +To use with utest, add following dependency: + +## sbt + +```scala +"com.softwaremill.diffx" %% "diffx-utest" % "0.5.3" % Test +``` + +## mill + +```scala +ivy"com.softwaremill.diffx::diffx-utest::0.5.3" +``` + +## Usage + +Then, mixin `DiffxAssertions` trait or add `import com.softwaremill.diffx.utest.DiffxAssertions._` to your test code. +To assert using diffx use `assertEquals` as follows: + +```scala +import com.softwaremill.diffx.utest.DiffxAssertions._ +import com.softwaremill.diffx.generic.auto._ + +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)) +) + +val left: Foo = Foo( + Bar("asdf", 66), + List(1234), + Some(right) +) + +assertEqual(left, right) +``` + diff --git a/generated-docs/out/usage/derivation.md b/generated-docs/out/usage/derivation.md new file mode 100644 index 00000000..ac477338 --- /dev/null +++ b/generated-docs/out/usage/derivation.md @@ -0,0 +1,23 @@ +# derivation + +Diffx supports auto and semi-auto derivation. + +For semi-auto derivation you don't need any additional import, just define your instances using: +```scala +import com.softwaremill.diffx._ +case class Product(name: String) +case class Basket(products: List[Product]) + +implicit val productDiff = Diff.derived[Product] +implicit val basketDiff = Diff.derived[Basket] +``` + +To use auto derivation add following import + +`import com.softwaremill.diffx.generic.auto._` + +or extend trait + +`com.softwaremill.diffx.generic.AutoDerivation` + +**Auto derivation might have a huge impact on compilation times**, because of that it is recommended to use `semi-auto` derivation. diff --git a/generated-docs/out/usage/extending.md b/generated-docs/out/usage/extending.md new file mode 100644 index 00000000..59c74798 --- /dev/null +++ b/generated-docs/out/usage/extending.md @@ -0,0 +1,22 @@ +# extending + +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. + +Consider following example with `NonEmptyList` from cats. `NonEmptyList` is implemented as case class, +so the default behavior of diffx would be to create a `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 (for any iterable to be precise). +All we need 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: + +```scala +import com.softwaremill.diffx._ +import _root_.cats.data.NonEmptyList +implicit def nelDiff[T: Diff]: Diff[NonEmptyList[T]] = + Diff[List[T]].contramap[NonEmptyList[T]](_.toList) +``` + +*Note: There is a [diffx-cats](../integrations/cats.md) module, so you don't have to do this* \ No newline at end of file diff --git a/generated-docs/out/usage/ignoring.md b/generated-docs/out/usage/ignoring.md new file mode 100644 index 00000000..0f940e06 --- /dev/null +++ b/generated-docs/out/usage/ignoring.md @@ -0,0 +1,30 @@ +# ignoring + + +Fields can be excluded from comparison 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. + +```scala +case class Person(name:String, age:Int) +val modifiedDiff: Diff[Person] = Diff[Person].ignore(_.name) +``` + +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: + +```scala +case class Person(name:String, age:Int) +implicit val modifiedDiff: Diff[Person] = Derived[Diff[Person]].ignore(_.age) +``` +```scala +compare(Person("bob", 25), Person("bob", 30)) +// res1: DiffResult = DiffResultObject( +// name = "Person", +// fields = ListMap( +// "name" -> IdenticalValue(value = "bob"), +// "age" -> IdenticalValue(value = "") +// ) +// ) +``` \ No newline at end of file diff --git a/generated-docs/out/usage/output.md b/generated-docs/out/usage/output.md new file mode 100644 index 00000000..5dabb465 --- /dev/null +++ b/generated-docs/out/usage/output.md @@ -0,0 +1,65 @@ +# output + + +`diffx` does its best to show the difference in the most readable way, but obviously the default configuration won't +cover all the use-cases. Because of that, there are few ways how you can modify its output. + +## intellij idea + +If you are running tests using **IntelliJ IDEA**'s test runner, you will want +to turn off the red text coloring it uses for test failure outputs because +it interferes with difflicious' color outputs. + +In File | Settings | Editor | Color Scheme | Console Colors | Console | Error Output, uncheck the red foreground color. +(Solution provided by Jacob Wang @jatcwang) + +## colors & signs + +I found it confusing to use the terms `expected`/`actual` as there seems to be no golden rule whether to keep expected on the right side or on the left side. +Because of that, diffx refers to the values that are compared as `left` and `right` value. + +By default, the difference is shown in the following form: + +`leftColor(leftValue) -> rightColor(rightValue)` + +which in terms of missing/additional values e.g. in collections looks as follows: + +`leftColor(additionalValue)` in case the value was present on the left-hand side and absent on the right side +`rightColor(missingValue)` in case the value was absent on the left-hand side and present on the right side + + +Where, by default, `rightColor` is green and `leftColor` is red. + +Colors can be customized providing an implicit instance of `ConsoleColorConfig` class. +In fact `rightColor` and `leftColor` are functions `string => string` so they can be modified to do whatever you want with the output. +One example of that would be to use some special characters instead of colors, which might be useful on some environments like e.g. CI. + +````scala +val colorConfigWithPlusMinus: ConsoleColorConfig = + ConsoleColorConfig(default = identity, arrow = identity, right = s => "+" + s, left = s => "-" + s) +```` + +There are two predefined set of colors - light and dark theme. +The default theme is dark, and it can be changed using environment variable - `DIFFX_COLOR_THEME`(`light`/`dark`). + +## skipping identical + +In some cases it might be desired to skip rendering the identical fields, to do that simple set `showIgnored` to `false`. + +```scala +case class Person(name:String, age:Int) + +val result = compare(Person("Bob", 23), Person("Alice", 23)) +// result: DiffResult = DiffResultObject( +// name = "Person", +// fields = ListMap( +// "name" -> DiffResultString( +// diffs = List(DiffResultValue(left = "Bob", right = "Alice")) +// ), +// "age" -> IdenticalValue(value = 23) +// ) +// ) +result.show(renderIdentical = false) +// res1: String = """Person( +// name: Bob -> Alice)""" +``` \ No newline at end of file diff --git a/generated-docs/out/usage/replacing.md b/generated-docs/out/usage/replacing.md new file mode 100644 index 00000000..d666ec41 --- /dev/null +++ b/generated-docs/out/usage/replacing.md @@ -0,0 +1,47 @@ +# replacing + +Sometimes you might want to compare some nested values using a different comparator but +the type they share is not unique within that hierarchy. + +Consider following example: +```scala +case class Person(age: Int, weight: Int) +``` + +If we would like to compare `weight` differently than `age` we would have to introduce a new type for `weight` +in order to provide a different `Diff` typeclass for only that field. While in general, it is a good idea to have your types +very precise it might not always be practical or even possible. Fortunately, diffx comes with a mechanism which allows +the replacement of nested diff instances. + +First we need to acquire a lens at given path using `modify` method, +and then we can call `setTo` to replace a particular instance. + +```scala +import com.softwaremill.diffx._ +implicit val diffPerson: Derived[Diff[Person]] = Diff.derived[Person].modify(_.weight) + .setTo(Diff.approximate(epsilon=5)) +``` + +```scala +compare(Person(23, 60), Person(23, 62)) +// res0: DiffResult = DiffResultObject( +// name = "Person", +// fields = ListMap( +// "age" -> IdenticalValue(value = 23), +// "weight" -> IdenticalValue(value = 60) +// ) +// ) +``` + +In fact, replacement is so powerful that ignoring is implemented as a replacement +with the `Diff.ignore` instance. + +You can use the same mechanism to set particular object matcher for given nested collection in the hierarchy. +Depending, whether it is list, set or map a respective method needs to be called: +```scala +case class Organization(peopleList: List[Person], peopleSet: Set[Person], peopleMap: Map[String, Person]) +implicit val diffOrg: Derived[Diff[Organization]] = Diff.derived[Organization] + .modify(_.peopleList).withListMatcher[Person](ObjectMatcher.byValue(_.age)) + .modify(_.peopleSet).withSetMatcher[Person](ObjectMatcher.by(_.age)) + .modify(_.peopleMap).withMapMatcher[String,Person](ObjectMatcher.byValue(_.age)) +``` \ No newline at end of file diff --git a/generated-docs/out/usage/sequences.md b/generated-docs/out/usage/sequences.md new file mode 100644 index 00000000..7cf33e9e --- /dev/null +++ b/generated-docs/out/usage/sequences.md @@ -0,0 +1,110 @@ +# sequences + +`diffx` provides instances for many containers from scala's standard library (e.g. lists, sets, maps), however +not all collections can be simply compared. Ordered collections like lists or vectors are compared by default by +comparing elements under the same indexes. +Maps, by default, are compared by comparing values under the respective keys. +For unordered collections there is an `ObjectMapper` typeclass which defines how elements should be paired. + +## object matcher + +In general, it is a very simple interface, with a bunch of factory methods. +```scala +trait ObjectMatcher[T] { + def isSameObject(left: T, right: T): Boolean +} +``` + +It is mostly useful when comparing unordered collections like sets: + +```scala +import com.softwaremill.diffx._ +import com.softwaremill.diffx.generic.auto._ +case class Person(id: String, name: String) + +implicit val personMatcher: ObjectMatcher[Person] = ObjectMatcher.by(_.id) +val bob = Person("1","Bob") +``` +```scala +compare(Set(bob), Set(bob, Person("2","Alice"))) +// res1: DiffResult = DiffResultSet( +// diffs = List( +// DiffResultMissing(value = Person(id = "2", name = "Alice")), +// DiffResultObject( +// name = "Person", +// fields = ListMap( +// "id" -> IdenticalValue(value = "1"), +// "name" -> IdenticalValue(value = "Bob") +// ) +// ) +// ) +// ) +``` + +It can be also used to modify how the entries from maps are paired. +In below example we tell `diffx` to compare these maps by paring entries by values using the defined `personMatcher` +```scala +import com.softwaremill.diffx._ +import com.softwaremill.diffx.generic.auto._ +import com.softwaremill.diffx.ObjectMatcher.MapEntry +case class Person(id: String, name: String) + +val personMatcher: ObjectMatcher[Person] = ObjectMatcher.by(_.id) +implicit val om: ObjectMatcher[MapEntry[String, Person]] = ObjectMatcher.byValue(personMatcher) +val bob = Person("1","Bob") +``` + +```scala +compare(Map("1" -> bob), Map("2" -> bob)) +// res3: DiffResult = DiffResultMap( +// entries = Map( +// DiffResultString(diffs = List(DiffResultValue(left = "1", right = "2"))) -> DiffResultObject( +// name = "Person", +// fields = ListMap( +// "id" -> IdenticalValue(value = "1"), +// "name" -> IdenticalValue(value = "Bob") +// ) +// ) +// ) +// ) +``` + +Last but not least you can use `objectMatcher` to customize paring when comparing indexed collections. +Such collections are treated similarly to maps (they use key-value object matcher), +but the key type is bound to `Int` (`IterableEntry` is an alias for `MapEntry[Int,V]`). + +```scala +import com.softwaremill.diffx._ +import com.softwaremill.diffx.generic.auto._ +import com.softwaremill.diffx.ObjectMatcher.IterableEntry +case class Person(id: String, name: String) + +implicit val personMatcher: ObjectMatcher[IterableEntry[Person]] = ObjectMatcher.byValue(_.id) +val bob = Person("1","Bob") +val alice = Person("2","Alice") +``` +```scala +compare(List(bob, alice), List(alice, bob)) +// res5: DiffResult = DiffResultObject( +// name = "List", +// fields = ListMap( +// "0" -> DiffResultObject( +// name = "Person", +// fields = ListMap( +// "id" -> IdenticalValue(value = "1"), +// "name" -> IdenticalValue(value = "Bob") +// ) +// ), +// "1" -> DiffResultObject( +// name = "Person", +// fields = ListMap( +// "id" -> IdenticalValue(value = "2"), +// "name" -> IdenticalValue(value = "Alice") +// ) +// ) +// ) +// ) +``` + +*Note: `ObjectMatcher` can be also passed explicitly, either upon creation or during modification* +*See [replacing](replacing.md) for details.* \ No newline at end of file diff --git a/generated-docs/out/watch.sh b/generated-docs/out/watch.sh new file mode 100755 index 00000000..24c43727 --- /dev/null +++ b/generated-docs/out/watch.sh @@ -0,0 +1,2 @@ +#!/bin/bash +sphinx-autobuild . _build/html diff --git a/munit/src/main/scala/com/softwaremill/diffx/munit/DiffxAssertions.scala b/munit/src/main/scala/com/softwaremill/diffx/munit/DiffxAssertions.scala new file mode 100644 index 00000000..6b0f6018 --- /dev/null +++ b/munit/src/main/scala/com/softwaremill/diffx/munit/DiffxAssertions.scala @@ -0,0 +1,16 @@ +package com.softwaremill.diffx.munit + +import com.softwaremill.diffx.{ConsoleColorConfig, Diff} +import munit.Assertions._ +import munit.Location + +trait DiffxAssertions { + def assertEqual[T: Diff](t1: T, t2: T)(implicit c: ConsoleColorConfig, loc: Location): Unit = { + val result = Diff.compare(t1, t2) + if (!result.isIdentical) { + fail(result.show())(loc) + } + } +} + +object DiffxAssertions extends DiffxAssertions diff --git a/munit/src/test/scala/com/softwaremill/diffx/munit/MunitAssertTest.scala b/munit/src/test/scala/com/softwaremill/diffx/munit/MunitAssertTest.scala new file mode 100644 index 00000000..79c325e4 --- /dev/null +++ b/munit/src/test/scala/com/softwaremill/diffx/munit/MunitAssertTest.scala @@ -0,0 +1,18 @@ +package com.softwaremill.diffx.munit + +import com.softwaremill.diffx.generic.auto._ + +class MunitAssertTest extends munit.FunSuite with DiffxAssertions { +// uncomment to run +// test("failing test") { +// val n = Person("n1", 11) +// assertEqual(n, Person("n2", 12)) +// } + + test("hello") { + val n = Person("n1", 11) + assertEqual(n, Person("n1", 11)) + } +} + +case class Person(name: String, age: Int) diff --git a/project/build.properties b/project/build.properties index 49742172..77df8ac3 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.4.3 \ No newline at end of file +sbt.version=1.5.4 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt index cdf8d551..9b59c86c 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,9 @@ -addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-common" % "1.9.14") -addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-publish" % "1.9.14") -addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-extra" % "1.9.14") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.3.1") -addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.0.0") -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.2.12") +addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-common" % "2.0.5") +addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-publish" % "2.0.5") +addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-extra" % "2.0.5") + +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.5.1") +addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.8.0") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.2.21") + +addSbtPlugin("org.jetbrains.scala" % "sbt-ide-settings" % "1.1.1") diff --git a/refined/shared/src/main/scala/com/softwaremill/diffx/refined/RefinedSupport.scala b/refined/shared/src/main/scala/com/softwaremill/diffx/refined/RefinedSupport.scala deleted file mode 100644 index bf8eb31f..00000000 --- a/refined/shared/src/main/scala/com/softwaremill/diffx/refined/RefinedSupport.scala +++ /dev/null @@ -1,8 +0,0 @@ -package com.softwaremill.diffx.refined - -import com.softwaremill.diffx.{Derived, Diff} -import eu.timepit.refined.api.Refined - -trait RefinedSupport { - implicit def refinedDiff[T: Diff, P]: Derived[Diff[T Refined P]] = Derived(Diff[T].contramap[T Refined P](_.value)) -} diff --git a/refined/src/main/scala/com/softwaremill/diffx/refined/RefinedSupport.scala b/refined/src/main/scala/com/softwaremill/diffx/refined/RefinedSupport.scala new file mode 100644 index 00000000..bb96a725 --- /dev/null +++ b/refined/src/main/scala/com/softwaremill/diffx/refined/RefinedSupport.scala @@ -0,0 +1,8 @@ +package com.softwaremill.diffx.refined + +import com.softwaremill.diffx.Diff +import eu.timepit.refined.api.Refined + +trait RefinedSupport { + implicit def refinedDiff[T: Diff, P]: Diff[T Refined P] = Diff[T].contramap[T Refined P](_.value) +} diff --git a/refined/shared/src/main/scala/com/softwaremill/diffx/refined/package.scala b/refined/src/main/scala/com/softwaremill/diffx/refined/package.scala similarity index 100% rename from refined/shared/src/main/scala/com/softwaremill/diffx/refined/package.scala rename to refined/src/main/scala/com/softwaremill/diffx/refined/package.scala diff --git a/refined/shared/src/test/scala/com/softwaremill/diffx/refined/RefinedSupportTest.scala b/refined/src/test/scala/com/softwaremill/diffx/refined/RefinedSupportTest.scala similarity index 76% rename from refined/shared/src/test/scala/com/softwaremill/diffx/refined/RefinedSupportTest.scala rename to refined/src/test/scala/com/softwaremill/diffx/refined/RefinedSupportTest.scala index 32f89509..84313a6c 100644 --- a/refined/shared/src/test/scala/com/softwaremill/diffx/refined/RefinedSupportTest.scala +++ b/refined/src/test/scala/com/softwaremill/diffx/refined/RefinedSupportTest.scala @@ -1,11 +1,12 @@ package com.softwaremill.diffx.refined -import com.softwaremill.diffx.{DiffResultObject, DiffResultString, DiffResultValue, Identical, _} +import com.softwaremill.diffx.{DiffResultObject, DiffResultString, DiffResultValue, IdenticalValue, _} import eu.timepit.refined.types.numeric.PosInt import eu.timepit.refined.auto._ import eu.timepit.refined.types.string.NonEmptyString import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers +import com.softwaremill.diffx.generic.auto._ class RefinedSupportTest extends AnyFlatSpec with Matchers { it should "work for refined types" in { @@ -13,7 +14,7 @@ class RefinedSupportTest extends AnyFlatSpec with Matchers { val testData2 = TestData(1, "bar") compare(testData1, testData2) shouldBe DiffResultObject( "TestData", - Map("posInt" -> Identical(1), "nonEmptyString" -> DiffResultString(List(DiffResultValue("foo", "bar")))) + Map("posInt" -> IdenticalValue(1), "nonEmptyString" -> DiffResultString(List(DiffResultValue("foo", "bar")))) ) } } diff --git a/scalatest/shared/src/main/scala/com/softwaremill/diffx/scalatest/DiffMatcher.scala b/scalatest/shared/src/main/scala/com/softwaremill/diffx/scalatest/DiffMatcher.scala deleted file mode 100644 index 1a457877..00000000 --- a/scalatest/shared/src/main/scala/com/softwaremill/diffx/scalatest/DiffMatcher.scala +++ /dev/null @@ -1,17 +0,0 @@ -package com.softwaremill.diffx.scalatest - -import com.softwaremill.diffx.{ConsoleColorConfig, Diff, DiffResultDifferent} -import org.scalatest.matchers.{MatchResult, Matcher} - -trait DiffMatcher { - def matchTo[A: Diff](right: A)(implicit c: ConsoleColorConfig): Matcher[A] = { left => - Diff[A].apply(left, right) match { - case c: DiffResultDifferent => - val diff = c.show.split('\n').mkString(Console.RESET, s"${Console.RESET}\n${Console.RESET}", Console.RESET) - MatchResult(matches = false, s"Matching error:\n$diff", "") - case _ => MatchResult(matches = true, "", "") - } - } -} - -object DiffMatcher extends DiffMatcher diff --git a/scalatest/src/main/scala/com/softwaremill/diffx/scalatest/DiffMatcher.scala b/scalatest/src/main/scala/com/softwaremill/diffx/scalatest/DiffMatcher.scala new file mode 100644 index 00000000..8ba7bdff --- /dev/null +++ b/scalatest/src/main/scala/com/softwaremill/diffx/scalatest/DiffMatcher.scala @@ -0,0 +1,18 @@ +package com.softwaremill.diffx.scalatest + +import com.softwaremill.diffx.{ConsoleColorConfig, Diff} +import org.scalatest.matchers.{MatchResult, Matcher} + +trait DiffMatcher { + def matchTo[A: Diff](right: A)(implicit c: ConsoleColorConfig): Matcher[A] = { left => + val result = Diff[A].apply(left, right) + if (!result.isIdentical) { + val diff = result.show().split('\n').mkString(Console.RESET, s"${Console.RESET}\n${Console.RESET}", Console.RESET) + MatchResult(matches = false, s"Matching error:\n$diff", "") + } else { + MatchResult(matches = true, "", "") + } + } +} + +object DiffMatcher extends DiffMatcher diff --git a/scalatest/shared/src/test/scala/com/softwaremill/diffx/scalatest/DiffMatcherTest.scala b/scalatest/src/test/scala/com/softwaremill/diffx/scalatest/DiffMatcherTest.scala similarity index 93% rename from scalatest/shared/src/test/scala/com/softwaremill/diffx/scalatest/DiffMatcherTest.scala rename to scalatest/src/test/scala/com/softwaremill/diffx/scalatest/DiffMatcherTest.scala index e6a7188c..3f32ef29 100644 --- a/scalatest/shared/src/test/scala/com/softwaremill/diffx/scalatest/DiffMatcherTest.scala +++ b/scalatest/src/test/scala/com/softwaremill/diffx/scalatest/DiffMatcherTest.scala @@ -2,6 +2,7 @@ package com.softwaremill.diffx.scalatest import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers +import com.softwaremill.diffx.generic.auto._ class DiffMatcherTest extends AnyFlatSpec with Matchers with DiffMatcher { val right: Foo = Foo( diff --git a/scripts/decrypt_files_if_not_pr.sh b/scripts/decrypt_files_if_not_pr.sh deleted file mode 100644 index b3c4456c..00000000 --- a/scripts/decrypt_files_if_not_pr.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -if [[ "$TRAVIS_PULL_REQUEST" == "false" ]]; then - openssl aes-256-cbc -K $encrypted_5331bd37a5e5_key -iv $encrypted_5331bd37a5e5_iv -in secrets.tar.enc -out secrets.tar -d - tar xvf secrets.tar -fi diff --git a/secrets.tar.enc b/secrets.tar.enc deleted file mode 100644 index 25ae3194..00000000 Binary files a/secrets.tar.enc and /dev/null differ diff --git a/specs2/shared/src/main/scala/com/softwaremill/diffx/specs2/DiffMatcher.scala b/specs2/src/main/scala/com/softwaremill/diffx/specs2/DiffMatcher.scala similarity index 71% rename from specs2/shared/src/main/scala/com/softwaremill/diffx/specs2/DiffMatcher.scala rename to specs2/src/main/scala/com/softwaremill/diffx/specs2/DiffMatcher.scala index 0d965c62..3d4b3714 100644 --- a/specs2/shared/src/main/scala/com/softwaremill/diffx/specs2/DiffMatcher.scala +++ b/specs2/src/main/scala/com/softwaremill/diffx/specs2/DiffMatcher.scala @@ -1,6 +1,6 @@ package com.softwaremill.diffx.specs2 -import com.softwaremill.diffx.{ConsoleColorConfig, Diff, DiffResultDifferent} +import com.softwaremill.diffx.{ConsoleColorConfig, Diff} import org.specs2.matcher.{Expectable, MatchResult, Matcher} trait DiffMatcher { @@ -14,11 +14,13 @@ trait DiffMatcher { diff.apply(left.value, right).isIdentical }, okMessage = "", - koMessage = diff.apply(left.value, right) match { - case c: DiffResultDifferent => - c.show - case _ => + koMessage = { + val diffResult = diff.apply(left.value, right) + if (!diffResult.isIdentical) { + diffResult.show() + } else { "" + } }, left ) diff --git a/specs2/shared/src/test/scala/com/softwaremill/diffx/specs2/DiffMatcherTest.scala b/specs2/src/test/scala/com/softwaremill/diffx/specs2/DiffMatcherTest.scala similarity index 92% rename from specs2/shared/src/test/scala/com/softwaremill/diffx/specs2/DiffMatcherTest.scala rename to specs2/src/test/scala/com/softwaremill/diffx/specs2/DiffMatcherTest.scala index 6e4016ff..f2c27fcb 100644 --- a/specs2/shared/src/test/scala/com/softwaremill/diffx/specs2/DiffMatcherTest.scala +++ b/specs2/src/test/scala/com/softwaremill/diffx/specs2/DiffMatcherTest.scala @@ -1,6 +1,7 @@ package com.softwaremill.diffx.specs2 import org.specs2.Specification +import com.softwaremill.diffx.generic.auto._ class DiffMatcherTest extends Specification with DiffMatcher { override def is = s2"""This is an empty specification""" diff --git a/tagging/shared/src/main/scala/com/softwaremill/diffx/tagging/DiffTaggingSupport.scala b/tagging/shared/src/main/scala/com/softwaremill/diffx/tagging/DiffTaggingSupport.scala deleted file mode 100644 index 3bc2295b..00000000 --- a/tagging/shared/src/main/scala/com/softwaremill/diffx/tagging/DiffTaggingSupport.scala +++ /dev/null @@ -1,8 +0,0 @@ -package com.softwaremill.diffx.tagging - -import com.softwaremill.diffx.{Derived, Diff} -import com.softwaremill.tagging.@@ - -trait DiffTaggingSupport { - implicit def taggedDiff[T: Diff, U]: Derived[Diff[T @@ U]] = Derived(Diff[T].contramap[T @@ U](identity)) -} diff --git a/tagging/src/main/scala/com/softwaremill/diffx/tagging/DiffTaggingSupport.scala b/tagging/src/main/scala/com/softwaremill/diffx/tagging/DiffTaggingSupport.scala new file mode 100644 index 00000000..144e0e15 --- /dev/null +++ b/tagging/src/main/scala/com/softwaremill/diffx/tagging/DiffTaggingSupport.scala @@ -0,0 +1,8 @@ +package com.softwaremill.diffx.tagging + +import com.softwaremill.diffx.Diff +import com.softwaremill.tagging.@@ + +trait DiffTaggingSupport { + implicit def taggedDiff[T: Diff, U]: Diff[T @@ U] = Diff[T].contramap[T @@ U](identity) +} diff --git a/tagging/shared/src/main/scala/com/softwaremill/diffx/tagging/package.scala b/tagging/src/main/scala/com/softwaremill/diffx/tagging/package.scala similarity index 100% rename from tagging/shared/src/main/scala/com/softwaremill/diffx/tagging/package.scala rename to tagging/src/main/scala/com/softwaremill/diffx/tagging/package.scala diff --git a/tagging/shared/src/test/scala/com/softwaremill/diffx/tagging/test/DiffTaggingSupportTest.scala b/tagging/src/test/scala/com/softwaremill/diffx/tagging/test/DiffTaggingSupportTest.scala similarity index 84% rename from tagging/shared/src/test/scala/com/softwaremill/diffx/tagging/test/DiffTaggingSupportTest.scala rename to tagging/src/test/scala/com/softwaremill/diffx/tagging/test/DiffTaggingSupportTest.scala index a04f1648..0a0b1191 100644 --- a/tagging/shared/src/test/scala/com/softwaremill/diffx/tagging/test/DiffTaggingSupportTest.scala +++ b/tagging/src/test/scala/com/softwaremill/diffx/tagging/test/DiffTaggingSupportTest.scala @@ -1,11 +1,12 @@ package com.softwaremill.diffx.tagging.test -import com.softwaremill.diffx.{Diff, DiffResultObject, DiffResultValue, Identical} +import com.softwaremill.diffx.{Diff, DiffResultObject, DiffResultValue, IdenticalValue} import com.softwaremill.diffx.tagging._ import com.softwaremill.tagging._ import com.softwaremill.tagging.@@ import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers +import com.softwaremill.diffx.generic.auto._ class DiffTaggingSupportTest extends AnyFlatSpec with Matchers { it should "work for tagged types" in { @@ -14,7 +15,7 @@ class DiffTaggingSupportTest extends AnyFlatSpec with Matchers { val p2 = 1.taggedWith[T2] compare(TestData(p1, p2), TestData(p11, p2)) shouldBe DiffResultObject( "TestData", - Map("p1" -> DiffResultValue(p1, p11), "p2" -> Identical(p2)) + Map("p1" -> DiffResultValue(p1, p11), "p2" -> IdenticalValue(p2)) ) } diff --git a/utest/shared/src/main/scala/com/softwaremill/diffx/utest/DiffxAssertions.scala b/utest/shared/src/main/scala/com/softwaremill/diffx/utest/DiffxAssertions.scala deleted file mode 100644 index 6def8186..00000000 --- a/utest/shared/src/main/scala/com/softwaremill/diffx/utest/DiffxAssertions.scala +++ /dev/null @@ -1,17 +0,0 @@ -package com.softwaremill.diffx.utest - -import com.softwaremill.diffx.{Diff, DiffResultDifferent} -import utest.AssertionError - -trait DiffxAssertions { - - def assertEqual[T: Diff](t1: T, t2: T): Unit = { - val result = Diff.compare(t1, t2) - result match { - case different: DiffResultDifferent => throw AssertionError(different.show, Seq.empty, null) - case _ => // do nothing - } - } -} - -object DiffxAssertions extends DiffxAssertions diff --git a/utest/src/main/scala/com/softwaremill/diffx/utest/DiffxAssertions.scala b/utest/src/main/scala/com/softwaremill/diffx/utest/DiffxAssertions.scala new file mode 100644 index 00000000..9d06dca4 --- /dev/null +++ b/utest/src/main/scala/com/softwaremill/diffx/utest/DiffxAssertions.scala @@ -0,0 +1,16 @@ +package com.softwaremill.diffx.utest + +import com.softwaremill.diffx.{ConsoleColorConfig, Diff} +import utest.AssertionError + +trait DiffxAssertions { + + def assertEqual[T: Diff](t1: T, t2: T)(implicit c: ConsoleColorConfig): Unit = { + val result = Diff.compare(t1, t2) + if (!result.isIdentical) { + throw AssertionError(result.show(), Seq.empty, null) + } + } +} + +object DiffxAssertions extends DiffxAssertions diff --git a/utest/shared/src/test/scala/com/softwaremill/diffx/utest/UtestAssertTest.scala b/utest/src/test/scala/com/softwaremill/diffx/utest/UtestAssertTest.scala similarity index 90% rename from utest/shared/src/test/scala/com/softwaremill/diffx/utest/UtestAssertTest.scala rename to utest/src/test/scala/com/softwaremill/diffx/utest/UtestAssertTest.scala index 638e7e01..28e9cff2 100644 --- a/utest/shared/src/test/scala/com/softwaremill/diffx/utest/UtestAssertTest.scala +++ b/utest/src/test/scala/com/softwaremill/diffx/utest/UtestAssertTest.scala @@ -1,6 +1,7 @@ package com.softwaremill.diffx.utest import utest._ +import com.softwaremill.diffx.generic.auto._ object UtestAssertTest extends TestSuite with DiffxAssertions { val tests = Tests { diff --git a/version.sbt b/version.sbt deleted file mode 100644 index 5666f5e9..00000000 --- a/version.sbt +++ /dev/null @@ -1 +0,0 @@ -version in ThisBuild := "0.3.30"