├── .envrc ├── .github ├── dependabot.yaml ├── release-drafter.yml └── workflows │ ├── main.yaml │ └── scala-steward.yml ├── .gitignore ├── .mergify.yml ├── .readthedocs.yaml ├── .sbtopts ├── .scala-steward.conf ├── .scalafmt.conf ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── banner.png ├── build.sbt ├── cats └── src │ ├── main │ └── scala │ │ └── com │ │ └── softwaremill │ │ └── diffx │ │ └── cats │ │ ├── DiffCatsInstances.scala │ │ └── package.scala │ └── test │ └── scala │ └── com │ └── softwaremill │ └── diffx │ └── cats │ └── DiffxCatsTest.scala ├── core └── src │ ├── main │ ├── boilerplate │ │ └── com │ │ │ └── softwaremill │ │ │ └── diffx │ │ │ └── DiffTupleInstances.scala.template │ ├── scala-2.13+ │ │ └── com.softwaremill.diffx │ │ │ └── package.scala │ ├── scala-2.13- │ │ └── com.softwaremill.diffx │ │ │ └── package.scala │ ├── scala-2 │ │ └── com │ │ │ └── softwaremill │ │ │ └── diffx │ │ │ ├── DiffCompanionMacro.scala │ │ │ ├── DiffMacro.scala │ │ │ ├── ModifyMacro.scala │ │ │ └── generic │ │ │ ├── DiffMagnoliaDerivation.scala │ │ │ ├── MagnoliaDerivedMacro.scala │ │ │ └── auto │ │ │ └── AutoDerivation.scala │ ├── scala-3 │ │ └── com │ │ │ └── softwaremill │ │ │ └── diffx │ │ │ ├── DiffCompanionMacro.scala │ │ │ ├── DiffMacro.scala │ │ │ ├── ModifyMacro.scala │ │ │ ├── generic │ │ │ ├── DiffMagnoliaDerivation.scala │ │ │ └── auto │ │ │ │ ├── DiffAutoDerivationOn.scala │ │ │ │ └── package.scala │ │ │ └── package.scala │ ├── scala │ │ └── com │ │ │ └── softwaremill │ │ │ └── diffx │ │ │ ├── Containers.scala │ │ │ ├── Diff.scala │ │ │ ├── DiffConfiguration.scala │ │ │ ├── DiffContext.scala │ │ │ ├── DiffLensMatchByOps.scala │ │ │ ├── DiffMatchByOps.scala │ │ │ ├── DiffResult.scala │ │ │ ├── DiffResultPrinter.scala │ │ │ ├── DiffTupleInstances.scala │ │ │ ├── DiffxSupport.scala │ │ │ ├── Matching.scala │ │ │ ├── ObjectMatcher.scala │ │ │ └── instances │ │ │ ├── ApproximateDiffForNumeric.scala │ │ │ ├── DiffForEither.scala │ │ │ ├── DiffForMap.scala │ │ │ ├── DiffForNumeric.scala │ │ │ ├── DiffForOption.scala │ │ │ ├── DiffForSeq.scala │ │ │ ├── DiffForSet.scala │ │ │ ├── DiffForString.scala │ │ │ ├── internal │ │ │ ├── IndexedEntry.scala │ │ │ └── MatchResult.scala │ │ │ └── string │ │ │ ├── Chunk.scala │ │ │ ├── Delta.scala │ │ │ ├── DiffRow.scala │ │ │ ├── DiffRowGenerator.scala │ │ │ ├── DiffUtils.scala │ │ │ ├── DifferentiationFailedException.scala │ │ │ ├── MyersDiff.scala │ │ │ ├── Patch.scala │ │ │ └── PathNode.scala │ ├── scalajs │ │ └── diffx │ │ │ └── DiffxPlatformExtensions.scala │ └── scalajvm │ │ └── com │ │ └── softwaremill │ │ └── diffx │ │ └── DiffxPlatformExtensions.scala │ └── test │ ├── scala │ └── com │ │ └── softwaremill │ │ └── diffx │ │ └── test │ │ ├── DiffModifyIntegrationTest.scala │ │ ├── DiffResultTest.scala │ │ ├── DiffSemiautoTest.scala │ │ ├── DiffStringTest.scala │ │ ├── DiffTest.scala │ │ ├── MatchByOpsTest.scala │ │ ├── ModifyMacroTest.scala │ │ ├── ObjectMatcherTest.scala │ │ └── examples.scala │ └── scalajvm │ └── com │ └── softwaremill │ └── diffx │ └── test │ └── DiffxJvmInstancesTest.scala ├── docs-sources ├── .gitignore ├── .python-version ├── Makefile ├── conf.py ├── flake.lock ├── flake.nix ├── index.md ├── integrations │ ├── cats.md │ ├── refined.md │ └── tagging.md ├── make.bat ├── requirements.txt ├── test-frameworks │ ├── munit.md │ ├── scalatest.md │ ├── specs2.md │ ├── summary.md │ ├── utest.md │ └── weaver.md └── usage │ ├── derivation.md │ ├── extending.md │ ├── ignoring.md │ ├── modifying.md │ ├── output.md │ └── sequences.md ├── example.png ├── flake.lock ├── flake.nix ├── generated-docs └── out │ ├── .gitignore │ ├── .python-version │ ├── Makefile │ ├── conf.py │ ├── flake.lock │ ├── flake.nix │ ├── index.md │ ├── integrations │ ├── cats.md │ ├── refined.md │ └── tagging.md │ ├── make.bat │ ├── requirements.txt │ ├── test-frameworks │ ├── munit.md │ ├── scalatest.md │ ├── specs2.md │ ├── summary.md │ ├── utest.md │ └── weaver.md │ └── usage │ ├── derivation.md │ ├── extending.md │ ├── ignoring.md │ ├── modifying.md │ ├── output.md │ ├── replacing.md │ └── sequences.md ├── munit └── src │ ├── main │ └── scala │ │ └── com │ │ └── softwaremill │ │ └── diffx │ │ └── munit │ │ └── DiffxAssertions.scala │ └── test │ └── scala │ └── com │ └── softwaremill │ └── diffx │ └── munit │ └── MunitAssertTest.scala ├── project ├── build.properties └── plugins.sbt ├── refined └── src │ ├── main │ └── scala │ │ └── com │ │ └── softwaremill │ │ └── diffx │ │ └── refined │ │ ├── RefinedSupport.scala │ │ └── package.scala │ └── test │ └── scala │ └── com │ └── softwaremill │ └── diffx │ └── refined │ └── RefinedSupportTest.scala ├── scalatest-must └── src │ ├── main │ ├── scala-2 │ │ └── com │ │ │ └── softwaremill │ │ │ └── diffx │ │ │ └── scalatest │ │ │ └── DiffMustMatcher.scala │ └── scala-3 │ │ └── com │ │ └── softwaremill │ │ └── diffx │ │ └── scalatest │ │ └── DiffMustMatcher.scala │ └── test │ └── scala │ └── com │ └── softwaremill │ └── diffx │ └── scalatest │ └── DiffMustMatcherTest.scala ├── scalatest-should └── src │ ├── main │ ├── scala-2 │ │ └── com │ │ │ └── softwaremill │ │ │ └── diffx │ │ │ └── scalatest │ │ │ └── DiffShouldMatcher.scala │ └── scala-3 │ │ └── com │ │ └── softwaremill │ │ └── diffx │ │ └── scalatest │ │ └── DiffShouldMatcher.scala │ └── test │ └── scala │ └── com │ └── softwaremill │ └── diffx │ └── scalatest │ └── DiffShouldMatcherTest.scala ├── scalatest └── src │ ├── main │ └── scala │ │ └── com │ │ └── softwaremill │ │ └── diffx │ │ └── scalatest │ │ └── DiffMatcher.scala │ └── test │ └── scala │ └── com │ └── softwaremill │ └── diffx │ └── scalatest │ └── DiffMatcherTest.scala ├── shell.nix ├── specs2 └── src │ ├── main │ └── scala │ │ └── com │ │ └── softwaremill │ │ └── diffx │ │ └── specs2 │ │ └── DiffMatcher.scala │ └── test │ └── scala │ └── com │ └── softwaremill │ └── diffx │ └── specs2 │ └── DiffMatcherTest.scala ├── tagging └── src │ ├── main │ └── scala │ │ └── com │ │ └── softwaremill │ │ └── diffx │ │ └── tagging │ │ ├── DiffTaggingSupport.scala │ │ └── package.scala │ └── test │ └── scala │ └── com │ └── softwaremill │ └── diffx │ └── tagging │ └── test │ └── DiffTaggingSupportTest.scala ├── utest └── src │ ├── main │ └── scala │ │ └── com │ │ └── softwaremill │ │ └── diffx │ │ └── utest │ │ └── DiffxAssertions.scala │ └── test │ └── scala │ └── com │ └── softwaremill │ └── diffx │ └── utest │ └── UtestAssertTest.scala └── weaver └── src ├── main └── scala │ └── com │ └── softwaremill │ └── diffx │ └── weaver │ └── DiffxExpectations.scala └── test └── scala └── com └── softwaremill └── diffx └── weaver └── DiffxExpectationsTest.scala /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | categories: 2 | - title: 'Dependency updates' 3 | labels: 4 | - 'dependency' 5 | template: | 6 | ## What’s Changed 7 | $CHANGES -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["**"] 6 | tags: [v*] 7 | pull_request: 8 | branches: ["**"] 9 | env: 10 | # .sbtopts specifies 8g, which is needed to import into IntelliJ, but on GH that exceeds the maximum available memory 11 | SBT_JAVA_OPTS: -J-Xms4g -J-Xmx4g 12 | jobs: 13 | build: 14 | # run on external PRs, but not on internal PRs since those will be run by push to branch 15 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository 16 | runs-on: ubuntu-22.04 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | scala-version: ["2.12", "2.13", "3"] 21 | target-platform: ["JVM", "JS"] 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: coursier/cache-action@v6.4 25 | - name: Set up JDK 11 26 | uses: actions/setup-java@v4 27 | with: 28 | distribution: "temurin" 29 | cache: "sbt" 30 | java-version: 11 31 | - uses: coursier/setup-action@v1.3.4 32 | - name: Compile 33 | run: sbt $SBT_JAVA_OPTS -v "compileScoped ${{ matrix.scala-version }} ${{ matrix.target-platform }}" 34 | - name: Compile docs 35 | if: matrix.target-platform == 'JVM' 36 | run: sbt compileDocs 37 | - name: Test 38 | run: sbt $SBT_JAVA_OPTS -v "testScoped ${{ matrix.scala-version }} ${{ matrix.target-platform }}" 39 | - name: Generate LSIF 40 | if: matrix.target-platform == 'JVM' && matrix.scala-version == 2.13 41 | run: cs launch com.sourcegraph:scip-java_2.13:0.8.2 -- index 42 | - name: Install sourcegraph/src 43 | if: matrix.target-platform == 'JVM' && matrix.scala-version == 2.13 44 | 45 | run: yarn global add @sourcegraph/src 46 | - name: Upload LSIF data 47 | if: matrix.target-platform == 'JVM' && matrix.scala-version == 2.13 48 | 49 | run: src code-intel upload -trace=3 -root . -file index.scip -github-token $GITHUB_TOKEN 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | 53 | mima: 54 | # run on external PRs, but not on internal PRs since those will be run by push to branch 55 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository 56 | runs-on: ubuntu-22.04 57 | steps: 58 | - name: Checkout 59 | uses: actions/checkout@v4 60 | with: 61 | fetch-depth: 0 # checkout tags so that dynver works properly (we need the version for MiMa) 62 | - name: Set up JDK 11 63 | uses: actions/setup-java@v4 64 | with: 65 | distribution: "temurin" 66 | java-version: 11 67 | cache: "sbt" 68 | - name: Check MiMa 69 | run: sbt $SBT_JAVA_OPTS -v mimaReportBinaryIssues 70 | 71 | check-formatting: 72 | # run on external PRs, but not on internal PRs since those will be run by push to branch 73 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository 74 | runs-on: ubuntu-22.04 75 | steps: 76 | - name: Checkout 77 | uses: actions/checkout@v4 78 | with: 79 | fetch-depth: 0 # checkout tags so that dynver works properly (we need the version for MiMa) 80 | - name: Set up JDK 11 81 | uses: actions/setup-java@v4 82 | with: 83 | distribution: "temurin" 84 | java-version: 11 85 | cache: "sbt" 86 | - name: Check formatting 87 | run: sbt $SBT_JAVA_OPTS -v scalafmtCheckAll 88 | 89 | publish: 90 | name: Publish release 91 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) 92 | needs: [build] 93 | runs-on: ubuntu-22.04 94 | steps: 95 | - uses: actions/checkout@v4 96 | with: 97 | fetch-depth: 0 98 | - uses: coursier/cache-action@v6.4 99 | - name: Set up JDK 11 100 | uses: actions/setup-java@v4 101 | with: 102 | distribution: "temurin" 103 | java-version: 11 104 | cache: "sbt" 105 | - name: Compile 106 | run: sbt $SBT_JAVA_OPTS compile 107 | - name: Publish artifacts 108 | run: sbt $SBT_JAVA_OPTS ci-release 109 | env: 110 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 111 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 112 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 113 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 114 | - name: Extract version from commit message 115 | run: | 116 | version=${GITHUB_REF/refs\/tags\/v/} 117 | echo "VERSION=$version" >> $GITHUB_ENV 118 | env: 119 | COMMIT_MSG: ${{ github.event.head_commit.message }} 120 | - name: Publish release notes 121 | uses: release-drafter/release-drafter@v5 122 | with: 123 | config-name: release-drafter.yml 124 | publish: true 125 | name: "v${{ env.VERSION }}" 126 | tag: "v${{ env.VERSION }}" 127 | version: "v${{ env.VERSION }}" 128 | env: 129 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 130 | -------------------------------------------------------------------------------- /.github/workflows/scala-steward.yml: -------------------------------------------------------------------------------- 1 | name: Scala Steward 2 | 3 | # This workflow will launch at 00:00 every day 4 | on: 5 | # disabled as the job fails daily 6 | #schedule: 7 | # - cron: '0 0 * * *' 8 | workflow_dispatch: 9 | 10 | jobs: 11 | scala-steward: 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Set up JDK 11 17 | uses: actions/setup-java@v4 18 | with: 19 | java-version: 11 20 | - name: Cache sbt 21 | uses: actions/cache@v3 22 | with: 23 | path: | 24 | ~/.sbt 25 | ~/.ivy2/cache 26 | ~/.coursier 27 | key: sbt-cache-${{ runner.os }}-JVM-${{ hashFiles('project/build.properties') }} 28 | - name: Launch Scala Steward 29 | uses: scala-steward-org/scala-steward-action@v2 30 | with: 31 | author-name: scala-steward 32 | author-email: scala-steward 33 | github-token: ${{ secrets.REPO_GITHUB_TOKEN }} 34 | repo-config: .scala-steward.conf 35 | ignore-opts-files: false 36 | - name: Cleanup 37 | run: | 38 | rm -rf "$HOME/.ivy2/local" || true 39 | find $HOME/.ivy2/cache -name "ivydata-*.properties" -delete || true 40 | find $HOME/.ivy2/cache -name "*-LM-SNAPSHOT*" -delete || true 41 | find $HOME/.cache/coursier/v1 -name "ivydata-*.properties" -delete || true 42 | find $HOME/.sbt -name "*.lock" -delete || true 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | .cache 5 | .history 6 | .lib/ 7 | dist/* 8 | target 9 | lib_managed/ 10 | src_managed/ 11 | project/boot/ 12 | project/plugins/project/ 13 | 14 | .idea* 15 | .bsp 16 | 17 | # Metals 18 | .metals/ 19 | .bloop/ 20 | metals.sbt 21 | .vscode 22 | 23 | index.scip 24 | 25 | node_modules 26 | package.json 27 | package-lock.json 28 | 29 | .direnv 30 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: delete head branch after merge 3 | conditions: [] 4 | actions: 5 | delete_head_branch: {} 6 | - name: automatic merge for softwaremill-ci pull requests affecting build.sbt 7 | conditions: 8 | - author=softwaremill-ci 9 | - check-success=build 10 | - "#files=1" 11 | - files=build.sbt 12 | actions: 13 | merge: 14 | method: merge 15 | - name: automatic merge for softwaremill-ci pull requests affecting project plugins.sbt 16 | conditions: 17 | - author=softwaremill-ci 18 | - check-success=build 19 | - "#files=1" 20 | - files=project/plugins.sbt 21 | actions: 22 | merge: 23 | method: merge 24 | - name: semi-automatic merge for softwaremill-ci pull requests 25 | conditions: 26 | - author=softwaremill-ci 27 | - check-success=build 28 | - "#approved-reviews-by>=1" 29 | actions: 30 | merge: 31 | method: merge 32 | - name: automatic merge for softwaremill-ci pull requests affecting project build.properties 33 | conditions: 34 | - author=softwaremill-ci 35 | - check-success=build 36 | - "#files=1" 37 | - files=project/build.properties 38 | actions: 39 | merge: 40 | method: merge 41 | - name: automatic merge for softwaremill-ci pull requests affecting .scalafmt.conf 42 | conditions: 43 | - author=softwaremill-ci 44 | - check-success=build 45 | - "#files=1" 46 | - files=.scalafmt.conf 47 | actions: 48 | merge: 49 | method: merge 50 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Required 2 | version: 2 3 | 4 | sphinx: 5 | configuration: generated-docs/out/conf.py 6 | 7 | python: 8 | install: 9 | - requirements: generated-docs/out/requirements.txt 10 | 11 | build: 12 | os: ubuntu-22.04 13 | tools: 14 | python: "3.7" 15 | nodejs: "18" # not sure if needed but better to fix its version 16 | rust: "1.64" # not sure if needed but better to fix its version 17 | golang: "1.19" # not sure if needed but better to fix its version 18 | -------------------------------------------------------------------------------- /.sbtopts: -------------------------------------------------------------------------------- 1 | -J-Xmx8G 2 | -J-Xss2M 3 | -Dsbt.task.timings=false 4 | -------------------------------------------------------------------------------- /.scala-steward.conf: -------------------------------------------------------------------------------- 1 | updates.ignore = [ 2 | {groupId = "org.scala-lang", artifactId = "scala-compiler", version = "2.12."}, 3 | {groupId = "org.scala-lang", artifactId = "scala-compiler", version = "2.13."}, 4 | {groupId = "org.scala-lang", artifactId = "scala-compiler", version = "3."} 5 | ] 6 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.5.8 2 | maxColumn = 120 3 | runner.dialect = scala3 4 | fileOverride { 5 | "glob:**/scala-2/**" { 6 | runner.dialect = scala213 7 | } 8 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Diffx is an early stage project. Everything might change. All suggestions welcome :) 2 | 3 | See the list of [issues](https://github.com/softwaremill/diffx/issues) and pick one! Or report your own. 4 | 5 | If you are having doubts on the why or how something works, don't hesitate to ask a question on [gitter](https://gitter.im/softwaremill/diffx) or via github. This probably means that the documentation, scaladocs or code is unclear and be improved for the benefit of all. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![diffx](https://github.com/softwaremill/diffx/raw/master/banner.png) 2 | 3 | [![Build Status](https://img.shields.io/github/actions/workflow/status/softwaremill/diffx/main.yaml?branch=master)](https://github.com/softwaremill/diffx/actions) 4 | [![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) 5 | [![diffx-core Scala version support](https://index.scala-lang.org/softwaremill/diffx/diffx-core/latest-by-scala-version.svg)](https://index.scala-lang.org/softwaremill/diffx/diffx-core) 6 | 7 | [![Gitter](https://badges.gitter.im/softwaremill/diffx.svg)](https://gitter.im/softwaremill/diffx?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 8 | [![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) 9 | [![Documentation Status](https://readthedocs.org/projects/diffx-scala/badge/?version=latest)](https://diffx-scala.readthedocs.io/en/latest/?badge=latest) 10 | 11 | ## Maintenance mode 12 | 13 | :warning: 14 | Diffx is no longer actively developed. 15 | 16 | No new features will be added. 17 | 18 | Pull requests with bug fixes will still be merged for some time. 19 | 20 | New users are encouraged to try out a better alternative - [difflicious](https://github.com/jatcwang/difflicious) 21 | 22 | Old users should migrate to difflicious by the end of 2024. 23 | 24 | I am planning to archive this project by that time unless someone volunteers to take it over. 25 | 26 | If you spot something that is missing in difflicious that blocks your migration, please don't hesitate and open an issue there. 27 | 28 | If you are curious why I decided to do that here is a short summary: 29 | 30 | - diffx didn't have any development for quite some time. Sure some libraries are just done and there is no need for constant development. In my opinion this is not the case for diffx as 31 | its internals should be refactored. Also there are some open bugs that should be taken care of. 32 | - I simply don't have time and energy to maintain it anymore 33 | - difflicious has better design so there is no point in keeping diffx around 34 | 35 | Thank you all for using this project. 36 | 37 | ## Documentation 38 | 39 | diffx documentation is available at [diffx-scala.readthedocs.io](https://diffx-scala.readthedocs.io). 40 | 41 | ## Modifying documentation 42 | 43 | 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! 44 | 45 | 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. 46 | 47 | That is, in sbt run: `set version := "0.5.0"`, before running `mdoc` in `docs`. 48 | 49 | ## Releasing a new version 50 | 51 | ```sh 52 | $ nix develop 53 | $ sbt release 54 | ``` 55 | 56 | ## Copyright 57 | 58 | Copyright (C) 2019 SoftwareMill [https://softwaremill.com](https://softwaremill.com). 59 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwaremill/diffx/10f841727e4c151186d4e2fbdb45a36acb920f8c/banner.png -------------------------------------------------------------------------------- /cats/src/main/scala/com/softwaremill/diffx/cats/DiffCatsInstances.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.cats 2 | 3 | import cats.data.{Chain, NonEmptyChain, NonEmptyList, NonEmptyMap, NonEmptySet, NonEmptyVector} 4 | import com.softwaremill.diffx.instances.{DiffForMap, DiffForSeq, DiffForSet} 5 | import com.softwaremill.diffx.{Diff, MapLike, MapMatcher, SeqLike, SeqMatcher, SetLike, SetMatcher} 6 | 7 | trait DiffCatsInstances { 8 | implicit def diffNel[T: Diff](implicit 9 | seqMatcher: SeqMatcher[T], 10 | seqLike: SeqLike[NonEmptyList] 11 | ): Diff[NonEmptyList[T]] = 12 | new DiffForSeq[NonEmptyList, T](Diff[T], seqMatcher, seqLike, "NonEmptyList") 13 | 14 | implicit def nelIsLikeSeq: SeqLike[NonEmptyList] = new SeqLike[NonEmptyList] { 15 | override def asSeq[A](c: NonEmptyList[A]): Seq[A] = c.toList 16 | } 17 | 18 | implicit def diffChain[T: Diff](implicit 19 | seqMatcher: SeqMatcher[T], 20 | seqLike: SeqLike[Chain] 21 | ): Diff[Chain[T]] = 22 | new DiffForSeq[Chain, T](Diff[T], seqMatcher, seqLike, "Chain") 23 | 24 | implicit def diffNec[T: Diff](implicit 25 | seqMatcher: SeqMatcher[T], 26 | seqLike: SeqLike[NonEmptyChain] 27 | ): Diff[NonEmptyChain[T]] = 28 | new DiffForSeq[NonEmptyChain, T](Diff[T], seqMatcher, seqLike, "NonEmptyChain") 29 | 30 | implicit def chainIsLikeSeq: SeqLike[Chain] = new SeqLike[Chain] { 31 | override def asSeq[A](c: Chain[A]): Seq[A] = c.toList 32 | } 33 | 34 | implicit def necIsLikeSeq: SeqLike[NonEmptyChain] = new SeqLike[NonEmptyChain] { 35 | override def asSeq[A](c: NonEmptyChain[A]): Seq[A] = c.toChain.toList 36 | } 37 | 38 | implicit def diffNev[T: Diff](implicit 39 | seqMatcher: SeqMatcher[T], 40 | seqLike: SeqLike[NonEmptyVector] 41 | ): Diff[NonEmptyVector[T]] = 42 | new DiffForSeq[NonEmptyVector, T](Diff[T], seqMatcher, seqLike, "NonEmptyVector") 43 | 44 | implicit def nevIsLikeSeq: SeqLike[NonEmptyVector] = new SeqLike[NonEmptyVector] { 45 | override def asSeq[A](c: NonEmptyVector[A]): Seq[A] = c.toVector 46 | } 47 | 48 | implicit def diffNes[T: Diff](implicit 49 | setMatcher: SetMatcher[T], 50 | setLike: SetLike[NonEmptySet] 51 | ): Diff[NonEmptySet[T]] = 52 | new DiffForSet[NonEmptySet, T](Diff[T], setMatcher, setLike, "NonEmptySet") 53 | 54 | implicit def nesIsLikeSet: SetLike[NonEmptySet] = new SetLike[NonEmptySet] { 55 | override def asSet[A](c: NonEmptySet[A]): Set[A] = c.toSortedSet 56 | } 57 | 58 | implicit def diffNem[K: Diff, V: Diff](implicit 59 | mapMatcher: MapMatcher[K, V], 60 | mapLike: MapLike[NonEmptyMap] 61 | ): Diff[NonEmptyMap[K, V]] = new DiffForMap[NonEmptyMap, K, V](mapMatcher, Diff[K], Diff[V], mapLike, "NonEmptyMap") 62 | 63 | implicit def nemIsLikeMap: MapLike[NonEmptyMap] = new MapLike[NonEmptyMap] { 64 | override def asMap[K, V](c: NonEmptyMap[K, V]): Map[K, V] = c.toSortedMap 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /cats/src/main/scala/com/softwaremill/diffx/cats/package.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | package object cats extends DiffCatsInstances 4 | -------------------------------------------------------------------------------- /cats/src/test/scala/com/softwaremill/diffx/cats/DiffxCatsTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.cats 2 | 3 | import cats.data._ 4 | import com.softwaremill.diffx._ 5 | import org.scalatest.freespec.AnyFreeSpec 6 | import org.scalatest.matchers.should.Matchers 7 | 8 | class DiffxCatsTest extends AnyFreeSpec with Matchers { 9 | "nonEmptyList" in { 10 | compare(NonEmptyList.of(1), NonEmptyList.of(2)) shouldBe DiffResultObject( 11 | "NonEmptyList", 12 | Map("0" -> DiffResultValue(1, 2)) 13 | ) 14 | } 15 | 16 | "chain" in { 17 | compare(Chain(1), Chain(2)) shouldBe DiffResultObject( 18 | "Chain", 19 | Map("0" -> DiffResultValue(1, 2)) 20 | ) 21 | } 22 | 23 | "nonEmptyChain" in { 24 | compare(NonEmptyChain.one(1), NonEmptyChain.one(2)) shouldBe DiffResultObject( 25 | "NonEmptyChain", 26 | Map("0" -> DiffResultValue(1, 2)) 27 | ) 28 | } 29 | 30 | "nonEmptySet" in { 31 | compare(NonEmptySet.of(1), NonEmptySet.of(2)) shouldBe DiffResultSet( 32 | "NonEmptySet", 33 | Set(DiffResultAdditional(1), DiffResultMissing(2)) 34 | ) 35 | } 36 | 37 | "nonEmptyVector" in { 38 | compare(NonEmptyVector.of(1), NonEmptyVector.of(2)) shouldBe DiffResultObject( 39 | "NonEmptyVector", 40 | Map("0" -> DiffResultValue(1, 2)) 41 | ) 42 | } 43 | 44 | "nonEmptyMap" in { 45 | compare(NonEmptyMap.of("1" -> 1), NonEmptyMap.of("1" -> 2)) shouldBe DiffResultMap( 46 | "NonEmptyMap", 47 | Map(IdenticalValue("1") -> DiffResultValue(1, 2)) 48 | ) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/src/main/boilerplate/com/softwaremill/diffx/DiffTupleInstances.scala.template: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | trait DiffTupleInstances { 4 | 5 | [2..#implicit def dTuple1[[#T1#]](implicit [#d1: Diff[T1]#]): Diff[Tuple1[[#T1#]]] = new Diff[Tuple1[[#T1#]]] { 6 | override def apply( 7 | left: ([#T1#]), 8 | right: ([#T1#]), 9 | context: DiffContext 10 | ): DiffResult = { 11 | val results = List([#"_1" -> d1.apply(left._1, right._1)#]).toMap 12 | if (results.values.forall(_.isIdentical)) { 13 | Identical(left) 14 | } else { 15 | DiffResultObject("Tuple1", results) 16 | } 17 | } 18 | } 19 | # 20 | ] 21 | } -------------------------------------------------------------------------------- /core/src/main/scala-2.13+/com.softwaremill.diffx/package.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill 2 | 3 | import scala.annotation.compileTimeOnly 4 | import scala.collection.Factory 5 | import com.softwaremill.diffx.DiffxSupport._ 6 | 7 | package object diffx extends DiffxSupport { 8 | implicit def traversableDiffxFunctor[F[_], A](implicit 9 | fac: Factory[A, F[A]], 10 | ev: F[A] => Iterable[A] 11 | ): DiffxFunctor[F, A] = 12 | new DiffxFunctor[F, A] {} 13 | 14 | implicit class DiffxEachMap[F[_, _], K, T](t: F[K, T])(implicit fac: Factory[(K, T), F[K, T]]) { 15 | @compileTimeOnly(canOnlyBeUsedInsideDiffxMacro("eachKey")) 16 | def eachKey: K = sys.error(canOnlyBeUsedInsideDiffxMacro("eachKey")) 17 | @compileTimeOnly(canOnlyBeUsedInsideDiffxMacro("eachValue")) 18 | def eachValue: T = sys.error(canOnlyBeUsedInsideDiffxMacro("eachValue")) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/scala-2.13-/com.softwaremill.diffx/package.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill 2 | 3 | import scala.annotation.compileTimeOnly 4 | import scala.collection.TraversableLike 5 | import scala.collection.generic.CanBuildFrom 6 | import scala.language.experimental.macros 7 | import scala.language.higherKinds 8 | import com.softwaremill.diffx.DiffxSupport._ 9 | 10 | package object diffx extends DiffxSupport { 11 | implicit def traversableDiffxFunctor[F[_], A](implicit 12 | cbf: CanBuildFrom[F[A], A, F[A]], 13 | ev: F[A] => TraversableLike[A, F[A]] 14 | ): DiffxFunctor[F, A] = 15 | new DiffxFunctor[F, A] {} 16 | 17 | trait DiffxMapAtFunctor[F[_, _], K, T] { 18 | @compileTimeOnly(canOnlyBeUsedInsideDiffxMacro("each")) 19 | def each(fa: F[K, T])(f: T => T): F[K, T] = sys.error(canOnlyBeUsedInsideDiffxMacro("each")) 20 | } 21 | 22 | implicit def mapDiffxFunctor[M[KT, TT] <: Map[KT, TT], K, T](implicit 23 | cbf: CanBuildFrom[M[K, T], (K, T), M[K, T]] 24 | ): DiffxMapAtFunctor[M, K, T] = new DiffxMapAtFunctor[M, K, T] {} 25 | 26 | implicit class DiffxEachMap[F[_, _], K, T](t: F[K, T])(implicit f: DiffxMapAtFunctor[F, K, T]) { 27 | @compileTimeOnly(canOnlyBeUsedInsideDiffxMacro("eachKey")) 28 | def eachKey: K = sys.error(canOnlyBeUsedInsideDiffxMacro("eachKey")) 29 | @compileTimeOnly(canOnlyBeUsedInsideDiffxMacro("eachValue")) 30 | def eachValue: T = sys.error(canOnlyBeUsedInsideDiffxMacro("eachValue")) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /core/src/main/scala-2/com/softwaremill/diffx/DiffCompanionMacro.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | import com.softwaremill.diffx.generic.DiffMagnoliaDerivation 4 | import magnolia1.Magnolia 5 | 6 | trait DiffCompanionMacro extends DiffMagnoliaDerivation { 7 | def derived[T]: Diff[T] = macro Magnolia.gen[T] 8 | } 9 | -------------------------------------------------------------------------------- /core/src/main/scala-2/com/softwaremill/diffx/DiffMacro.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | trait DiffMacro[T] { 4 | def modify[U](path: T => U): DiffLens[T, U] = macro ModifyMacro.modifyMacro[T, U] 5 | def ignore[U](path: T => U)(implicit conf: DiffConfiguration): Diff[T] = macro ModifyMacro.ignoreMacro[T, U] 6 | } 7 | -------------------------------------------------------------------------------- /core/src/main/scala-2/com/softwaremill/diffx/ModifyMacro.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | import scala.annotation.tailrec 4 | import scala.reflect.macros.blackbox 5 | 6 | object ModifyMacro { 7 | private val ShapeInfo = "Path must have shape: _.field1.field2.each.field3.(...)" 8 | private val SubtypeShapeInfo = "Path must have shape: _.subtype[T].field1.(...)" 9 | 10 | def ignoreMacro[T: c.WeakTypeTag, U: c.WeakTypeTag]( 11 | c: blackbox.Context 12 | )(path: c.Expr[T => U])(conf: c.Expr[DiffConfiguration]): c.Tree = 13 | applyIgnored[T, U](c)(modifiedFromPathMacro(c)(path), conf) 14 | 15 | private def applyIgnored[T: c.WeakTypeTag, U: c.WeakTypeTag]( 16 | c: blackbox.Context 17 | )(path: c.Expr[List[ModifyPath]], conf: c.Expr[DiffConfiguration]): c.Tree = { 18 | import c.universe._ 19 | val lens = applyModified[T, U](c)(path) 20 | q"""$lens.ignore($conf)""" 21 | } 22 | 23 | def modifyMacro[T: c.WeakTypeTag, U: c.WeakTypeTag]( 24 | c: blackbox.Context 25 | )(path: c.Expr[T => U]): c.Tree = applyModified[T, U](c)(modifiedFromPathMacro(c)(path)) 26 | 27 | private def applyModified[T: c.WeakTypeTag, U: c.WeakTypeTag]( 28 | c: blackbox.Context 29 | )(path: c.Expr[List[ModifyPath]]): c.Tree = { 30 | import c.universe._ 31 | q"""{ 32 | com.softwaremill.diffx.DiffLens(${c.prefix}, $path) 33 | }""" 34 | } 35 | 36 | /** Converts path to list of strings 37 | */ 38 | def modifiedFromPathMacro[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)( 39 | path: c.Expr[T => U] 40 | ): c.Expr[List[ModifyPath]] = { 41 | import c.universe._ 42 | 43 | sealed trait PathElement 44 | case class TermPathElement(term: c.TermName, xargs: c.Tree*) extends PathElement 45 | case class FunctorPathElement(functor: c.Tree, method: c.TermName, xargs: c.Tree*) extends PathElement 46 | case class SubtypePathElement(subtype: c.Symbol) extends PathElement 47 | 48 | /** _.a.b.each.c => List(TPE(a), TPE(b), FPE(functor, each/at/eachWhere, xargs), TPE(c)) 49 | */ 50 | @tailrec 51 | def collectPathElements(tree: c.Tree, acc: List[PathElement]): List[PathElement] = { 52 | def typeSupported(diffxIgnoreType: c.Tree) = 53 | Seq("DiffxEach", "DiffxEither", "DiffxEachMap", "toSubtypeSelector") 54 | .exists(diffxIgnoreType.toString.endsWith) 55 | 56 | tree match { 57 | case q"$parent.$child " => 58 | collectPathElements(parent, TermPathElement(child) :: acc) 59 | case q"$tpname[$supertype]($rest).subtype[$tp]" if typeSupported(tpname) => 60 | collectPathElements(rest, SubtypePathElement(tp.tpe.typeSymbol) :: acc) 61 | case q"$tpname[..$_]($t)($f)" if typeSupported(tpname) => 62 | val newAcc = acc match { 63 | // replace the term controlled by quicklens 64 | case TermPathElement(term, xargs @ _*) :: rest => FunctorPathElement(f, term, xargs: _*) :: rest 65 | case pathEl :: _ => 66 | c.abort(c.enclosingPosition, s"Invalid use of path element $pathEl. $ShapeInfo, got: ${path.tree}") 67 | case Nil => 68 | c.abort(c.enclosingPosition, s"Invalid use of path element(Nil). $ShapeInfo, got: ${path.tree}") 69 | } 70 | collectPathElements(t, newAcc) 71 | case _: Ident => acc 72 | case _ => 73 | c.abort(c.enclosingPosition, s"Unsupported path element. $ShapeInfo, got: $tree") 74 | } 75 | } 76 | 77 | val pathEls = path.tree match { 78 | case q"($arg) => $pathBody " => collectPathElements(pathBody, Nil) 79 | case _ => c.abort(c.enclosingPosition, s"$ShapeInfo, got: ${path.tree}") 80 | } 81 | 82 | def makeSubtype(symbol: c.Symbol): Tree = { 83 | q"_root_.com.softwaremill.diffx.ModifyPath.Subtype(${symbol.owner.fullName}, ${symbol.name.decodedName.toString})" 84 | } 85 | 86 | c.Expr[List[ModifyPath]]( 87 | q"${pathEls.collect { 88 | case TermPathElement(c) => q"_root_.com.softwaremill.diffx.ModifyPath.Field(${c.decodedName.toString})" 89 | case FunctorPathElement(_, method, _ @_*) if method.decodedName.toString == "eachLeft" => 90 | makeSubtype(symbolOf[Left[Any, Any]]) 91 | case FunctorPathElement(_, method, _ @_*) if method.decodedName.toString == "eachRight" => 92 | makeSubtype(symbolOf[Right[Any, Any]]) 93 | case FunctorPathElement(_, method, _ @_*) if method.decodedName.toString == "each" => 94 | q"_root_.com.softwaremill.diffx.ModifyPath.Each" 95 | case FunctorPathElement(_, method, _ @_*) if method.decodedName.toString == "eachKey" => 96 | q"_root_.com.softwaremill.diffx.ModifyPath.EachKey" 97 | case FunctorPathElement(_, method, _ @_*) if method.decodedName.toString == "eachValue" => 98 | q"_root_.com.softwaremill.diffx.ModifyPath.EachValue" 99 | case SubtypePathElement(subtype) => 100 | makeSubtype(subtype) 101 | }}" 102 | ) 103 | } 104 | 105 | private[diffx] def modifiedFromPath[T, U](path: T => U): List[ModifyPath] = 106 | macro modifiedFromPathMacro[T, U] 107 | } 108 | -------------------------------------------------------------------------------- /core/src/main/scala-2/com/softwaremill/diffx/generic/DiffMagnoliaDerivation.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.generic 2 | 3 | import com.softwaremill.diffx.{Diff, DiffContext, DiffResultObject, DiffResultValue, ModifyPath, nullGuard} 4 | import magnolia1._ 5 | 6 | import scala.collection.immutable.ListMap 7 | 8 | trait DiffMagnoliaDerivation { 9 | type Typeclass[T] = Diff[T] 10 | 11 | def join[T](ctx: ReadOnlyCaseClass[Typeclass, T]): Diff[T] = { (left: T, right: T, context: DiffContext) => 12 | nullGuard(left, right) { (left, right) => 13 | val map = ListMap(ctx.parameters.map { p => 14 | val lType = p.dereference(left) 15 | val pType = p.dereference(right) 16 | val fieldDiffMod = 17 | context 18 | .getOverride(ModifyPath.Field(p.label)) 19 | .map(_.asInstanceOf[Diff[p.PType] => Diff[p.PType]]) 20 | .getOrElse(identity[Diff[p.PType]] _) 21 | val fieldDiff = fieldDiffMod(p.typeclass) 22 | p.label -> fieldDiff(lType, pType, context.getNextStep(ModifyPath.Field(p.label))) 23 | }: _*) 24 | DiffResultObject(ctx.typeName.short, map) 25 | } 26 | } 27 | 28 | def split[T](ctx: SealedTrait[Typeclass, T]): Diff[T] = { (left: T, right: T, context: DiffContext) => 29 | nullGuard(left, right) { (left, right) => 30 | val lType = ctx.split(left)(a => a) 31 | val rType = ctx.split(right)(a => a) 32 | if (lType == rType) { 33 | val leftTypeClass = lType.typeclass 34 | val contextPath = ModifyPath.Subtype(lType.typeName.owner, lType.typeName.short) 35 | val modifyFromOverride = context 36 | .getOverride(contextPath) 37 | .map(_.asInstanceOf[leftTypeClass.type => leftTypeClass.type]) 38 | .getOrElse(identity[leftTypeClass.type] _) 39 | modifyFromOverride(leftTypeClass)( 40 | lType.cast(left), 41 | lType.cast(right), 42 | context.getNextStep(contextPath).merge(context) 43 | ) 44 | } else { 45 | DiffResultValue(lType.typeName.full, rType.typeName.full) 46 | } 47 | } 48 | } 49 | 50 | def fallback[T]: Diff[T] = Diff.useEquals[T] 51 | } 52 | -------------------------------------------------------------------------------- /core/src/main/scala-2/com/softwaremill/diffx/generic/MagnoliaDerivedMacro.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.generic 2 | 3 | import com.softwaremill.diffx.Derived 4 | import magnolia1.Magnolia 5 | 6 | object MagnoliaDerivedMacro { 7 | 8 | import scala.reflect.macros.whitebox 9 | 10 | def derivedGen[T: c.WeakTypeTag](c: whitebox.Context): c.Expr[Derived[T]] = { 11 | import c.universe._ 12 | c.Expr[Derived[T]](q"com.softwaremill.diffx.Derived(${Magnolia.gen[T](c)(implicitly[c.WeakTypeTag[T]])})") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/scala-2/com/softwaremill/diffx/generic/auto/AutoDerivation.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.generic 2 | 3 | import com.softwaremill.diffx.{Derived, Diff} 4 | 5 | package object auto extends AutoDerivation 6 | 7 | trait AutoDerivation extends DiffMagnoliaDerivation { 8 | implicit def diffForCaseClass[T]: Derived[Diff[T]] = macro MagnoliaDerivedMacro.derivedGen[T] 9 | 10 | // Implicit conversion 11 | implicit def unwrapDerivedDiff[T](dd: Derived[Diff[T]]): Diff[T] = dd.value 12 | } 13 | -------------------------------------------------------------------------------- /core/src/main/scala-3/com/softwaremill/diffx/DiffCompanionMacro.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | import com.softwaremill.diffx.generic.DiffMagnoliaDerivation 4 | import com.softwaremill.diffx.generic.auto.DiffAutoDerivationOn 5 | 6 | trait DiffCompanionMacro extends DiffMagnoliaDerivation { 7 | given fallback[T](using DiffAutoDerivationOn): Derived[Diff[T]] = Derived(Diff.useEquals[T]) 8 | } 9 | -------------------------------------------------------------------------------- /core/src/main/scala-3/com/softwaremill/diffx/DiffMacro.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | trait DiffMacro[T] { outer: Diff[T] => 4 | inline def modify[U](inline path: T => U): DiffLens[T, U] = ${ ModifyMacro.modifyMacro[T, U]('outer)('path) } 5 | inline def ignore[U](inline path: T => U)(implicit config: DiffConfiguration): Diff[T] = ${ 6 | ModifyMacro.ignoreMacro[T, U]('outer)('path, 'config) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /core/src/main/scala-3/com/softwaremill/diffx/ModifyMacro.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | import scala.quoted.* 4 | 5 | object ModifyMacro { 6 | private val ShapeInfo = "Path must have shape: _.field1.field2.each.field3.(...)" 7 | 8 | def ignoreMacro[T: Type, U: Type]( 9 | base: Expr[Diff[T]] 10 | )(path: Expr[T => U], conf: Expr[DiffConfiguration])(using Quotes): Expr[Diff[T]] = { 11 | '{ ${ modifyMacro(base)(path) }.ignore($conf) } 12 | } 13 | 14 | def modifyMacro[T: Type, U: Type](base: Expr[Diff[T]])(path: Expr[T => U])(using Quotes): Expr[DiffLens[T, U]] = { 15 | '{ 16 | val p = ${ modifiedFromPathImpl(path) } 17 | new com.softwaremill.diffx.DiffLens[T, U]($base, p) 18 | } 19 | } 20 | 21 | private[diffx] inline def modifiedFromPath[S, U](inline path: S => U): List[ModifyPath] = ${ 22 | ModifyMacro.modifiedFromPathImpl[S, U]('path) 23 | } 24 | 25 | def modifiedFromPathImpl[T: Type, U: Type](path: Expr[T => U])(using Quotes): Expr[List[ModifyPath]] = { 26 | import quotes.reflect.* 27 | 28 | enum PathElement { 29 | case TermPathElement(term: String, xargs: String*) extends PathElement 30 | case FunctorPathElement(functor: String, method: String, xargs: String*) extends PathElement 31 | case SubtypePathElement(owner: String, short: String) extends PathElement 32 | } 33 | 34 | given ToExpr[ModifyPath] with { 35 | def apply(mp: ModifyPath)(using Quotes): Expr[ModifyPath] = 36 | mp match { 37 | case com.softwaremill.diffx.ModifyPath.Field(name) => 38 | '{ com.softwaremill.diffx.ModifyPath.Field(${ Expr(name) }) } 39 | case _: com.softwaremill.diffx.ModifyPath.Each.type => '{ com.softwaremill.diffx.ModifyPath.Each } 40 | case _: com.softwaremill.diffx.ModifyPath.EachKey.type => '{ com.softwaremill.diffx.ModifyPath.EachKey } 41 | case _: com.softwaremill.diffx.ModifyPath.EachValue.type => '{ com.softwaremill.diffx.ModifyPath.EachValue } 42 | case com.softwaremill.diffx.ModifyPath.Subtype(owner, short) => 43 | '{ com.softwaremill.diffx.ModifyPath.Subtype(${ Expr(owner) }, ${ Expr(short) }) } 44 | } 45 | } 46 | 47 | def resolveSubTypeName(typeTree: Tree)(using Quotes): String = 48 | typeTree match { 49 | case TypeIdent(name) => name.toString 50 | case TypeSelect(_, name) => name.toString 51 | } 52 | 53 | def toPath(tree: Tree, acc: List[PathElement]): Seq[PathElement] = { 54 | def typeSupported(modifyType: String) = 55 | Seq("DiffxEach", "DiffxEither", "DiffxEachMap", "toSubtypeSelector") 56 | .exists(modifyType.endsWith) 57 | 58 | tree match { 59 | /** Field access */ 60 | case Select(deep, ident) => 61 | toPath(deep, PathElement.TermPathElement(ident) :: acc) 62 | /** Method call with no arguments and using clause */ 63 | case Apply(Apply(TypeApply(Ident(f), _), idents), _) if typeSupported(f) => 64 | val newAcc = acc match { 65 | /** replace the term controlled by quicklens */ 66 | case PathElement.TermPathElement(term, xargs @ _*) :: rest => 67 | PathElement.FunctorPathElement(f, term, xargs: _*) :: rest 68 | case elements => 69 | report.throwError(s"Invalid use of path elements [${elements.mkString(", ")}]. $ShapeInfo, got: ${tree}") 70 | } 71 | idents.flatMap(toPath(_, newAcc)) 72 | case x @ TypeApply(Select(Apply(TypeApply(Ident(f), superType :: Nil), rest :: Nil), _), subtype :: Nil) 73 | if typeSupported(f) => 74 | if (superType.symbol.children.contains(subtype.symbol)) { 75 | toPath( 76 | rest, 77 | PathElement.SubtypePathElement(superType.symbol.fullName.toString, resolveSubTypeName(subtype)) :: acc 78 | ) 79 | } else { 80 | report.throwError( 81 | s"subtype requires that the super type be a sealed trait (enum), and the subtype being a direct children of the super type.", 82 | x.asExpr 83 | ) 84 | } 85 | /** The first segment from path (e.g. `_.age` -> `_`) */ 86 | case i: Ident => 87 | acc 88 | case t => 89 | report.throwError(s"Unsupported path element $t") 90 | } 91 | } 92 | 93 | val pathElements = path.asTerm match { 94 | /** Single inlined path */ 95 | case Inlined(_, _, Block(List(DefDef(_, _, _, Some(p))), _)) => 96 | toPath(p, List.empty) 97 | case _ => 98 | report.throwError(s"Unsupported path [$path]") 99 | } 100 | 101 | '{ 102 | val pathValue = ${ 103 | Expr(pathElements.collect { 104 | case PathElement.TermPathElement(c) => 105 | com.softwaremill.diffx.ModifyPath.Field(c): ModifyPath 106 | case PathElement.FunctorPathElement("DiffxEither", "eachLeft", _ @_*) => 107 | ModifyPath.Subtype("scala.package", "Left") 108 | case PathElement.FunctorPathElement("DiffxEither", "eachRight", _ @_*) => 109 | ModifyPath.Subtype("scala.package", "Right") 110 | case PathElement.FunctorPathElement(_, "each", _ @_*) => ModifyPath.Each 111 | case PathElement.FunctorPathElement(_, "eachKey", _ @_*) => ModifyPath.EachKey 112 | case PathElement.FunctorPathElement(_, "eachValue", _ @_*) => ModifyPath.EachValue 113 | case PathElement.SubtypePathElement(owner, subtype) => ModifyPath.Subtype(owner, subtype) 114 | }.toList) 115 | } 116 | 117 | pathValue 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /core/src/main/scala-3/com/softwaremill/diffx/generic/DiffMagnoliaDerivation.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.generic 2 | 3 | import com.softwaremill.diffx.{ 4 | Derived, 5 | Diff, 6 | DiffContext, 7 | DiffResult, 8 | DiffResultObject, 9 | DiffResultValue, 10 | ModifyPath, 11 | nullGuard 12 | } 13 | 14 | import scala.collection.mutable 15 | import magnolia1.* 16 | 17 | import scala.collection.immutable.ListMap 18 | import scala.deriving.Mirror 19 | 20 | trait DiffMagnoliaDerivation extends Derivation[Diff] { 21 | 22 | def join[T](ctx: CaseClass[Diff, T]): Diff[T] = { 23 | if (!ctx.isValueClass) { 24 | new Diff[T] { 25 | override def apply(left: T, right: T, context: DiffContext): DiffResult = { 26 | nullGuard(left, right) { (left, right) => 27 | val map = ListMap(ctx.params.map { p => 28 | val lType = p.deref(left) 29 | val pType = p.deref(right) 30 | val fieldDiffMod = 31 | context 32 | .getOverride(ModifyPath.Field(p.label)) 33 | .map(_.asInstanceOf[Diff[p.PType] => Diff[p.PType]]) 34 | .getOrElse(identity[Diff[p.PType]] _) 35 | val fieldDiff = fieldDiffMod(p.typeclass) 36 | p.label -> fieldDiff(lType, pType, context.getNextStep(ModifyPath.Field(p.label))) 37 | }: _*) 38 | DiffResultObject(ctx.typeInfo.short, map) 39 | } 40 | } 41 | } 42 | } else { 43 | Diff.useEquals[T] 44 | } 45 | } 46 | 47 | override def split[T](ctx: SealedTrait[Diff, T]): Diff[T] = new Diff[T] { 48 | override def apply(left: T, right: T, context: DiffContext): DiffResult = { 49 | nullGuard(left, right) { (left, right) => 50 | val lType = ctx.choose(left)(a => a) 51 | val rType = ctx.choose(right)(a => a) 52 | if (lType.typeInfo == rType.typeInfo) { 53 | val leftTypeClass = lType.typeclass 54 | val contextPath = ModifyPath.Subtype(lType.typeInfo.owner, lType.typeInfo.short) 55 | val modifyFromOverride = context 56 | .getOverride(contextPath) 57 | .map(_.asInstanceOf[leftTypeClass.type => leftTypeClass.type]) 58 | .getOrElse(identity[leftTypeClass.type] _) 59 | modifyFromOverride(leftTypeClass)( 60 | lType.cast(left), 61 | lType.cast(right), 62 | context.getNextStep(contextPath).merge(context) 63 | ) 64 | } else { 65 | DiffResultValue(lType.typeInfo.full, rType.typeInfo.full) 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /core/src/main/scala-3/com/softwaremill/diffx/generic/auto/DiffAutoDerivationOn.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.generic.auto 2 | 3 | trait DiffAutoDerivationOn 4 | -------------------------------------------------------------------------------- /core/src/main/scala-3/com/softwaremill/diffx/generic/auto/package.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.generic 2 | 3 | import com.softwaremill.diffx.generic.DiffMagnoliaDerivation 4 | import com.softwaremill.diffx.generic.auto.DiffAutoDerivationOn 5 | 6 | import scala.deriving.Mirror 7 | import com.softwaremill.diffx.{Derived, Diff} 8 | 9 | package object auto extends AutoDerivation 10 | 11 | trait AutoDerivation extends DiffMagnoliaDerivation { 12 | inline given diffForCaseClass[T](using Mirror.Of[T]): Derived[Diff[T]] = Derived(derived[T]) 13 | 14 | given indicator: DiffAutoDerivationOn = new DiffAutoDerivationOn {} 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/scala-3/com/softwaremill/diffx/package.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill 2 | 3 | import com.softwaremill.diffx.DiffxSupport.* 4 | 5 | import scala.annotation.compileTimeOnly 6 | import scala.collection.Factory 7 | 8 | package object diffx extends DiffxSupport { 9 | implicit def traversableDiffxFunctor[F[_], A](implicit 10 | fac: Factory[A, F[A]], 11 | ev: F[A] => Iterable[A] 12 | ): DiffxFunctor[F, A] = 13 | new DiffxFunctor[F, A] {} 14 | 15 | implicit class DiffxEachMap[F[_, _], K, V](t: F[K, V])(implicit fac: Factory[(K, V), F[K, V]]) { 16 | @compileTimeOnly(canOnlyBeUsedInsideDiffxMacro("eachKey")) 17 | def eachKey: K = sys.error(canOnlyBeUsedInsideDiffxMacro("eachKey")) 18 | @compileTimeOnly(canOnlyBeUsedInsideDiffxMacro("eachValue")) 19 | def eachValue: V = sys.error(canOnlyBeUsedInsideDiffxMacro("eachValue")) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/Containers.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | // inspired by https://github.com/jatcwang/difflicious/blob/master/modules/core/src/main/scala/difflicious/utils/SeqLike.scala 4 | trait SeqLike[C[_]] { 5 | def asSeq[A](c: C[A]): Seq[A] 6 | } 7 | 8 | object SeqLike { 9 | implicit def seqLike[C[W] <: scala.collection.Seq[W]]: SeqLike[C] = new SeqLike[C] { 10 | override def asSeq[A](c: C[A]): Seq[A] = c.toSeq 11 | } 12 | } 13 | 14 | trait SetLike[C[_]] { 15 | def asSet[A](c: C[A]): Set[A] 16 | } 17 | 18 | object SetLike { 19 | implicit def setLike[C[W] <: scala.collection.Set[W]]: SetLike[C] = new SetLike[C] { 20 | override def asSet[A](c: C[A]): Set[A] = c.toSet 21 | } 22 | } 23 | 24 | trait MapLike[C[_, _]] { 25 | def asMap[K, V](c: C[K, V]): Map[K, V] 26 | } 27 | object MapLike { 28 | implicit def mapLike[C[KK, VV] <: scala.collection.Map[KK, VV]]: MapLike[C] = new MapLike[C] { 29 | override def asMap[K, V](c: C[K, V]): Map[K, V] = c.toMap 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/Diff.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | import com.softwaremill.diffx.ObjectMatcher.{MapEntry, SeqEntry, SetEntry} 4 | import com.softwaremill.diffx.instances._ 5 | 6 | trait Diff[T] extends DiffMacro[T] { outer => 7 | def apply(left: T, right: T): DiffResult = apply(left, right, DiffContext.Empty) 8 | def apply(left: T, right: T, context: DiffContext): DiffResult 9 | 10 | def contramap[R](f: R => T): Diff[R] = 11 | (left: R, right: R, context: DiffContext) => { 12 | outer(f(left), f(right), context) 13 | } 14 | 15 | def modifyUnsafe[U](path: ModifyPath*)(mod: Diff[U] => Diff[U]): Diff[T] = 16 | new Diff[T] { 17 | override def apply(left: T, right: T, context: DiffContext): DiffResult = 18 | outer.apply( 19 | left, 20 | right, 21 | context.merge( 22 | DiffContext.atPath(path.toList, mod.asInstanceOf[Diff[Any] => Diff[Any]]) 23 | ) 24 | ) 25 | } 26 | 27 | def modifyMatcherUnsafe(path: ModifyPath*)(matcher: ObjectMatcher[_]): Diff[T] = 28 | new Diff[T] { 29 | override def apply(left: T, right: T, context: DiffContext): DiffResult = 30 | outer.apply( 31 | left, 32 | right, 33 | context.merge(DiffContext.atPath(path.toList, matcher)) 34 | ) 35 | } 36 | } 37 | 38 | object Diff extends LowPriorityDiff with DiffTupleInstances with DiffxPlatformExtensions with DiffCompanionMacro { 39 | def apply[T: Diff]: Diff[T] = implicitly[Diff[T]] 40 | 41 | def ignored[T]: Diff[T] = (_: T, _: T, _: DiffContext) => DiffResult.Ignored 42 | 43 | def compare[T: Diff](left: T, right: T): DiffResult = apply[T].apply(left, right) 44 | 45 | /** Create a Diff instance using [[Object#equals]] */ 46 | def useEquals[T]: Diff[T] = (left: T, right: T, _: DiffContext) => { 47 | if (left != right) { 48 | DiffResultValue(left, right) 49 | } else { 50 | IdenticalValue(left) 51 | } 52 | } 53 | 54 | def approximate[T: Numeric](epsilon: T): Diff[T] = 55 | new ApproximateDiffForNumeric[T](epsilon) 56 | 57 | implicit val diffForString: Diff[String] = new DiffForString 58 | implicit val diffForRange: Diff[Range] = Diff.useEquals[Range] 59 | implicit val diffForChar: Diff[Char] = Diff.useEquals[Char] 60 | implicit val diffForBoolean: Diff[Boolean] = Diff.useEquals[Boolean] 61 | 62 | implicit def diffForNumeric[T: Numeric]: Diff[T] = new DiffForNumeric[T] 63 | 64 | implicit def diffForMap[C[_, _], K, V](implicit 65 | dv: Diff[V], 66 | dk: Diff[K], 67 | matcher: ObjectMatcher[MapEntry[K, V]], 68 | mapLike: MapLike[C] 69 | ): Diff[C[K, V]] = new DiffForMap[C, K, V](matcher, dk, dv, mapLike) 70 | 71 | implicit def diffForOptional[T](implicit ddt: Diff[T]): Diff[Option[T]] = new DiffForOption[T](ddt) 72 | 73 | implicit def diffForSet[C[_], T](implicit 74 | dt: Diff[T], 75 | matcher: ObjectMatcher[SetEntry[T]], 76 | setLike: SetLike[C] 77 | ): Diff[C[T]] = new DiffForSet[C, T](dt, matcher, setLike) 78 | 79 | implicit def diffForEither[L, R](implicit ld: Diff[L], rd: Diff[R]): Diff[Either[L, R]] = 80 | new DiffForEither[L, R](ld, rd) 81 | 82 | implicit def diffForSeq[C[_], T](implicit 83 | dt: Diff[T], 84 | matcher: ObjectMatcher[SeqEntry[T]], 85 | seqLike: SeqLike[C] 86 | ): Diff[C[T]] = new DiffForSeq[C, T](dt, matcher, seqLike) 87 | } 88 | 89 | trait LowPriorityDiff { 90 | 91 | /** Implicit instance of Diff[T] created from implicit Derived[Diff[T]]. Should not be called explicitly from clients 92 | * code. Use `summon` instead. 93 | * @param dd 94 | * @tparam T 95 | * @return 96 | */ 97 | implicit def derivedDiff[T](implicit dd: Derived[Diff[T]]): Diff[T] = dd.value 98 | 99 | /** Returns unwrapped instance of Diff[T] from implicitly summoned Derived[Diff[T]]. Use this method when you want to 100 | * modify auto derived instance of diff and put it back into the implicit scope. 101 | * @param dd 102 | * @tparam T 103 | * @return 104 | */ 105 | def summon[T](implicit dd: Derived[Diff[T]]): Diff[T] = dd.value 106 | } 107 | 108 | case class Derived[T](value: T) extends AnyVal 109 | 110 | case class DiffLens[T, U](outer: Diff[T], path: List[ModifyPath]) { 111 | def setTo(d: Diff[U]): Diff[T] = using(_ => d) 112 | 113 | def using(mod: Diff[U] => Diff[U]): Diff[T] = { 114 | outer.modifyUnsafe(path: _*)(mod) 115 | } 116 | 117 | def ignore(implicit config: DiffConfiguration): Diff[T] = outer.modifyUnsafe(path: _*)(config.makeIgnored) 118 | } 119 | 120 | sealed trait ModifyPath extends Product with Serializable 121 | object ModifyPath { 122 | case class Field(name: String) extends ModifyPath 123 | case object Each extends ModifyPath 124 | case object EachKey extends ModifyPath 125 | case object EachValue extends ModifyPath 126 | case class Subtype[T](owner: String, short: String) extends ModifyPath 127 | } 128 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/DiffConfiguration.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | case class DiffConfiguration(makeIgnored: Diff[Any] => Diff[Any]) 4 | 5 | object DiffConfiguration { 6 | implicit val Default: DiffConfiguration = DiffConfiguration(makeIgnored = (_: Diff[Any]) => Diff.ignored[Any]) 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/DiffContext.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | case class DiffContext( 4 | overrides: Tree[Diff[Any] => Diff[Any]], 5 | path: List[ModifyPath], 6 | matcherOverrides: Tree[ObjectMatcher[_]] 7 | ) { 8 | def merge(other: DiffContext): DiffContext = { 9 | DiffContext(overrides.merge(other.overrides), List.empty, matcherOverrides.merge(other.matcherOverrides)) 10 | } 11 | 12 | def getOverride(nextPath: ModifyPath): Option[Diff[Any] => Diff[Any]] = { 13 | treeOverride(nextPath, overrides) 14 | } 15 | 16 | def getMatcherOverride[T]: Option[ObjectMatcher[T]] = { 17 | matcherOverrides match { 18 | case Tree.Leaf(v) => Some(v.asInstanceOf[ObjectMatcher[T]]) 19 | case Tree.Node(_) => None 20 | } 21 | } 22 | 23 | private def treeOverride[T](nextPath: ModifyPath, tree: Tree[T]) = { 24 | tree match { 25 | case Tree.Leaf(v) => None 26 | case Tree.Node(tries) => getOverrideFromNode(nextPath, tries) 27 | } 28 | } 29 | 30 | private def getOverrideFromNode[T](nextPath: ModifyPath, tries: Map[ModifyPath, Tree[T]]) = { 31 | tries.get(nextPath) match { 32 | case Some(Tree.Leaf(v)) => Some(v) 33 | case _ => None 34 | } 35 | } 36 | 37 | def getNextStep(label: ModifyPath): DiffContext = { 38 | val currentPath = path :+ label 39 | (getNextOverride(label, overrides), getNextOverride(label, matcherOverrides)) match { 40 | case (Some(d), Some(m)) => DiffContext(d, currentPath, m) 41 | case (None, Some(m)) => DiffContext(Tree.empty, currentPath, m) 42 | case (Some(d), None) => DiffContext(d, currentPath, Tree.empty) 43 | case (None, None) => DiffContext(Tree.empty, currentPath, Tree.empty) 44 | } 45 | } 46 | 47 | private def getNextOverride[T](nextPath: ModifyPath, tree: Tree[T]) = { 48 | tree match { 49 | case Tree.Leaf(_) => None 50 | case Tree.Node(tries) => tries.get(nextPath) 51 | } 52 | } 53 | } 54 | 55 | object DiffContext { 56 | val Empty: DiffContext = DiffContext(Tree.empty, List.empty, Tree.empty) 57 | def atPath(path: List[ModifyPath], mod: Diff[Any] => Diff[Any]): DiffContext = 58 | Empty.copy(overrides = Tree.fromList(path, mod)) 59 | def atPath(path: List[ModifyPath], matcher: ObjectMatcher[_]): DiffContext = 60 | Empty.copy(matcherOverrides = Tree.fromList(path, matcher)) 61 | } 62 | 63 | sealed trait Tree[T] { 64 | def merge(tree: Tree[T]): Tree[T] 65 | } 66 | object Tree { 67 | def empty[T]: Node[T] = Tree.Node[T](Map.empty) 68 | 69 | case class Leaf[T](v: T) extends Tree[T] { 70 | override def merge(tree: Tree[T]): Tree[T] = tree 71 | } 72 | case class Node[T](tries: Map[ModifyPath, Tree[T]]) extends Tree[T] { 73 | override def merge(tree: Tree[T]): Tree[T] = { 74 | tree match { 75 | case Leaf(v) => Leaf(v) 76 | case Node(otherTries) => 77 | val keys = tries.keySet ++ otherTries.keySet 78 | Node(keys.map { k => 79 | k -> ((tries.get(k), otherTries.get(k)) match { 80 | case (Some(t1), Some(t2)) => t1.merge(t2) 81 | case (Some(t1), None) => t1 82 | case (None, Some(t2)) => t2 83 | case (None, None) => throw new IllegalStateException("cannot happen") 84 | }) 85 | }.toMap) 86 | } 87 | } 88 | } 89 | def fromList[T](path: List[ModifyPath], obj: T): Tree[T] = { 90 | path.reverse.foldLeft(Leaf(obj): Tree[T])((acc, item) => Node(Map(item -> acc))) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/DiffLensMatchByOps.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | final class DiffLensSeqMatchByOps[C[_], T, S](lens: DiffLens[S, C[T]]) { 4 | def matchByIndex[U: ObjectMatcher](f: Int => U): Diff[S] = { 5 | lens.outer.modifyMatcherUnsafe(lens.path: _*)(ObjectMatcher.seq[T].byIndex(f)) 6 | } 7 | 8 | def matchByValue[U: ObjectMatcher](f: T => U): Diff[S] = { 9 | lens.outer.modifyMatcherUnsafe(lens.path: _*)(ObjectMatcher.seq[T].byValue(f)) 10 | } 11 | } 12 | 13 | trait DiffLensToSeqMatchByOps { 14 | implicit def lensToSeqMatchByOps[C[_]: SeqLike, T, S](diffLens: DiffLens[S, C[T]]): DiffLensSeqMatchByOps[C, T, S] = 15 | new DiffLensSeqMatchByOps[C, T, S](diffLens) 16 | } 17 | 18 | final class DiffLensSetMatchByOps[C[_], T, S](lens: DiffLens[S, C[T]]) { 19 | def matchBy[U: ObjectMatcher](f: T => U): Diff[S] = { 20 | lens.outer.modifyMatcherUnsafe(lens.path: _*)(ObjectMatcher.set[T].by(f)) 21 | } 22 | } 23 | 24 | trait DiffLensToSetMatchByOps { 25 | implicit def lensToSetMatchByOps[C[_]: SetLike, T, S](diffLens: DiffLens[S, C[T]]): DiffLensSetMatchByOps[C, T, S] = 26 | new DiffLensSetMatchByOps[C, T, S](diffLens) 27 | } 28 | 29 | final class DiffLensMapMatchByOps[C[_, _], K, V, S](lens: DiffLens[S, C[K, V]]) { 30 | def matchByKey[U: ObjectMatcher](f: K => U): Diff[S] = { 31 | lens.outer.modifyMatcherUnsafe(lens.path: _*)(ObjectMatcher.map[K, V].byKey(f)) 32 | } 33 | def matchByValue[U: ObjectMatcher](f: V => U): Diff[S] = { 34 | lens.outer.modifyMatcherUnsafe(lens.path: _*)(ObjectMatcher.map[K, V].byValue(f)) 35 | } 36 | } 37 | 38 | trait DiffLensToMapMatchByOps { 39 | implicit def lensToMapMatchByOps[C[_, _]: MapLike, K, V, S]( 40 | diffLens: DiffLens[S, C[K, V]] 41 | ): DiffLensMapMatchByOps[C, K, V, S] = 42 | new DiffLensMapMatchByOps[C, K, V, S](diffLens) 43 | } 44 | 45 | trait DiffLensToMatchByOps extends DiffLensToMapMatchByOps with DiffLensToSetMatchByOps with DiffLensToSeqMatchByOps 46 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/DiffMatchByOps.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | final class DiffSeqMatchByOps[C[_], T](diff: Diff[C[T]]) { 4 | def matchByKey[U: ObjectMatcher](f: Int => U): Diff[C[T]] = { 5 | diff.modifyMatcherUnsafe()(ObjectMatcher.seq[T].byIndex(f)) 6 | } 7 | 8 | def matchByValue[U: ObjectMatcher](f: T => U): Diff[C[T]] = { 9 | diff.modifyMatcherUnsafe()(ObjectMatcher.seq[T].byValue(f)) 10 | } 11 | } 12 | 13 | trait DiffToSeqMatchByOps { 14 | implicit def toSeqMatchByOps[C[_]: SeqLike, T, S](diff: Diff[C[T]]): DiffSeqMatchByOps[C, T] = 15 | new DiffSeqMatchByOps[C, T](diff) 16 | } 17 | 18 | final class DiffSetMatchByOps[C[_], T](diff: Diff[C[T]]) { 19 | def matchBy[U: ObjectMatcher](f: T => U): Diff[C[T]] = { 20 | diff.modifyMatcherUnsafe()(ObjectMatcher.set[T].by(f)) 21 | } 22 | } 23 | 24 | trait DiffToSetMatchByOps { 25 | implicit def toSetMatchByOps[C[_]: SetLike, T](diff: Diff[C[T]]): DiffSetMatchByOps[C, T] = 26 | new DiffSetMatchByOps[C, T](diff) 27 | } 28 | 29 | final class DiffMapMatchByOps[C[_, _], K, V](diff: Diff[C[K, V]]) { 30 | def matchByKey[U: ObjectMatcher](f: K => U): Diff[C[K, V]] = { 31 | diff.modifyMatcherUnsafe()(ObjectMatcher.map[K, V].byKey(f)) 32 | } 33 | def matchByValue[U: ObjectMatcher](f: V => U): Diff[C[K, V]] = { 34 | diff.modifyMatcherUnsafe()(ObjectMatcher.map[K, V].byValue(f)) 35 | } 36 | } 37 | 38 | trait DiffToMapMatchByOps { 39 | implicit def toMapMatchByOps[C[_, _]: MapLike, K, V]( 40 | diff: Diff[C[K, V]] 41 | ): DiffMapMatchByOps[C, K, V] = new DiffMapMatchByOps[C, K, V](diff) 42 | } 43 | 44 | trait DiffToMatchByOps extends DiffToMapMatchByOps with DiffToSetMatchByOps with DiffToSeqMatchByOps 45 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/DiffResult.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | sealed trait DiffResult extends Product with Serializable { 4 | def isIdentical: Boolean 5 | 6 | def show()(implicit sc: ShowConfig): String = 7 | DiffResultPrinter.showIndented(sc.transformer(this), DiffResultPrinter.IndentLevel) 8 | } 9 | 10 | object DiffResult { 11 | val Ignored: IdenticalValue[Any] = IdenticalValue("") 12 | } 13 | 14 | case class DiffResultObject(name: String, fields: Map[String, DiffResult]) extends DiffResult { 15 | override def isIdentical: Boolean = fields.values.forall(_.isIdentical) 16 | } 17 | 18 | case class DiffResultIterable(typename: String, items: Map[String, DiffResult]) extends DiffResult { 19 | override def isIdentical: Boolean = items.values.forall(_.isIdentical) 20 | } 21 | 22 | case class DiffResultMap(typename: String, entries: Map[DiffResult, DiffResult]) extends DiffResult { 23 | override def isIdentical: Boolean = entries.forall { case (k, v) => k.isIdentical && v.isIdentical } 24 | } 25 | 26 | case class DiffResultSet(typename: String, diffs: Set[DiffResult]) extends DiffResult { 27 | override def isIdentical: Boolean = diffs.forall(_.isIdentical) 28 | } 29 | 30 | case class DiffResultString(diffs: List[DiffResult]) extends DiffResult { 31 | override def isIdentical: Boolean = diffs.forall(_.isIdentical) 32 | } 33 | 34 | case class DiffResultStringLine(diffs: List[DiffResult]) extends DiffResult { 35 | override def isIdentical: Boolean = diffs.forall(_.isIdentical) 36 | } 37 | 38 | case class DiffResultStringWord(diffs: List[DiffResult]) extends DiffResult { 39 | override def isIdentical: Boolean = diffs.forall(_.isIdentical) 40 | } 41 | 42 | case class DiffResultChunk(left: String, right: String) extends DiffResult { 43 | override def isIdentical: Boolean = false 44 | } 45 | 46 | case class DiffResultValue[T](left: T, right: T) extends DiffResult { 47 | override def isIdentical: Boolean = false 48 | } 49 | 50 | case class IdenticalValue[T](value: T) extends DiffResult { 51 | override def isIdentical: Boolean = true 52 | } 53 | 54 | case class DiffResultMissing[T](value: T) extends DiffResult { 55 | override def isIdentical: Boolean = false 56 | } 57 | 58 | case class DiffResultMissingChunk(value: String) extends DiffResult { 59 | override def isIdentical: Boolean = false 60 | } 61 | 62 | case class DiffResultAdditional[T](value: T) extends DiffResult { 63 | override def isIdentical: Boolean = false 64 | } 65 | 66 | case class DiffResultAdditionalChunk(value: String) extends DiffResult { 67 | override def isIdentical: Boolean = false 68 | } 69 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/DiffResultPrinter.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | object DiffResultPrinter { 4 | private[diffx] final val IndentLevel = 5 5 | 6 | def showIndented(diffResult: DiffResult, indent: Int)(implicit sc: ShowConfig): String = { 7 | diffResult match { 8 | case dr: DiffResultObject => showDiffResultObject(dr, indent) 9 | case dr: DiffResultIterable => showDiffResultIterable(dr, indent) 10 | case dr: DiffResultMap => showDiffResultMap(dr, indent) 11 | case dr: DiffResultSet => showDiffResultSet(dr, indent) 12 | case dr: DiffResultString => s"${dr.diffs.map(ds => showIndented(ds, indent)).mkString("\n")}" 13 | case dr: DiffResultStringLine => mergeChunks(dr.diffs).map(ds => showIndented(ds, indent)).mkString 14 | case dr: DiffResultStringWord => mergeChunks(dr.diffs).map(ds => showIndented(ds, indent)).mkString 15 | case dr: DiffResultChunk => arrowColor("[") + showChange(s"${dr.left}", s"${dr.right}", indent) + arrowColor("]") 16 | case dr: DiffResultValue[_] => showChange(s"${dr.left}", s"${dr.right}", indent) 17 | case dr: IdenticalValue[_] => defaultColor(s"${dr.value}", indent) 18 | case dr: DiffResultMissing[_] => missingColor(s"${dr.value}", indent) 19 | case dr: DiffResultMissingChunk => missingColor(s"[${dr.value}]", indent) 20 | case dr: DiffResultAdditional[_] => additionalColor(s"${dr.value}", indent) 21 | case dr: DiffResultAdditionalChunk => additionalColor(s"[${dr.value}]", indent) 22 | } 23 | } 24 | 25 | private def showDiffResultObject(resultObject: DiffResultObject, indent: Int)(implicit sc: ShowConfig): String = { 26 | def renderValue(value: DiffResult) = s"${showIndented(value, indent + IndentLevel)}" 27 | def renderField(field: String) = s"${i(indent)}${defaultColor(s"$field: ")}" 28 | 29 | val showFields = resultObject.fields 30 | .map { case (field, value) => 31 | renderField(field) + renderValue(value) 32 | } 33 | defaultColor(s"${resultObject.name}(") + s"\n${showFields.mkString(defaultColor(",") + "\n")}" + defaultColor(")") 34 | } 35 | 36 | private def showDiffResultIterable(resultObject: DiffResultIterable, indent: Int)(implicit sc: ShowConfig): String = { 37 | def renderValue(value: DiffResult) = s"${showIndented(value, indent + IndentLevel)}" 38 | def renderField(field: String) = s"${i(indent)}${defaultColor(s"$field: ")}" 39 | 40 | val showFields = resultObject.items 41 | .map { case (field, value) => 42 | renderField(field) + renderValue(value) 43 | } 44 | defaultColor(s"${resultObject.typename}(") + s"\n${showFields.mkString(defaultColor(",") + "\n")}" + defaultColor( 45 | ")" 46 | ) 47 | } 48 | 49 | private def showDiffResultMap(diffResultMap: DiffResultMap, indent: Int)(implicit 50 | c: ShowConfig 51 | ): String = { 52 | def renderValue(value: DiffResult) = showIndented(value, indent + IndentLevel) 53 | def renderKey(key: DiffResult) = s"${i(indent)}${defaultColor(s"${showIndented(key, indent + IndentLevel)}")}" 54 | 55 | val showFields = diffResultMap.entries 56 | .map { case (k, v) => 57 | val key = renderKey(k) 58 | val separator = defaultColor(": ") 59 | val value = renderValue(v) 60 | key + separator + value 61 | } 62 | defaultColor(s"${diffResultMap.typename}(") + s"\n${showFields.mkString(defaultColor(",") + "\n")}" + defaultColor( 63 | ")" 64 | ) 65 | } 66 | 67 | private def showDiffResultSet(diffResultSet: DiffResultSet, indent: Int)(implicit 68 | sc: ShowConfig 69 | ): String = { 70 | val showFields = diffResultSet.diffs 71 | .map(f => s"${i(indent)}${showIndented(f, indent + IndentLevel)}") 72 | showFields.mkString(defaultColor(s"${diffResultSet.typename}(\n"), ",\n", defaultColor(")")) 73 | } 74 | 75 | private def i(indent: Int): String = " " * indent 76 | 77 | private def mergeChunks(diffs: List[DiffResult]) = { 78 | diffs 79 | .foldLeft(List.empty[DiffResult]) { (acc, item) => 80 | (acc.lastOption, item) match { 81 | case (Some(d: DiffResultMissingChunk), di: DiffResultMissingChunk) => 82 | acc.dropRight(1) :+ d.copy(value = d.value + di.value) 83 | case (Some(d: DiffResultAdditionalChunk), di: DiffResultAdditionalChunk) => 84 | acc.dropRight(1) :+ d.copy(value = d.value + di.value) 85 | case (Some(d: DiffResultChunk), di: DiffResultChunk) => 86 | acc.dropRight(1) :+ d.copy(left = d.left + di.left, right = d.right + di.right) 87 | case _ => acc :+ item 88 | } 89 | } 90 | } 91 | 92 | private def missingColor(s: String, indent: Int)(implicit c: ShowConfig): String = withColor(s, c.missing, indent) 93 | private def additionalColor(s: String, indent: Int)(implicit c: ShowConfig): String = 94 | withColor(s, c.additional, indent) 95 | private def defaultColor(s: String, indent: Int = 0)(implicit c: ShowConfig): String = withColor(s, c.default, indent) 96 | private def arrowColor(s: String)(implicit c: ShowConfig): String = c.arrow(s) 97 | private def showChange(l: String, r: String, indent: Int)(implicit c: ShowConfig): String = 98 | withColor(l, c.left, indent) + arrowColor(" -> ") + withColor(r, c.right, indent) 99 | 100 | private def withColor(value: String, color: String => String, indent: Int): String = { 101 | value.split("\n", -1).map(color(_)).mkString("\n" + " " * indent) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/DiffxSupport.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | import scala.annotation.compileTimeOnly 4 | import DiffxSupport.canOnlyBeUsedInsideDiffxMacro 5 | 6 | trait DiffxSupport extends DiffxEitherSupport with DiffxOptionSupport with DiffLensToMatchByOps with DiffToMatchByOps { 7 | type FieldPath = List[String] 8 | type SeqMatcher[T] = ObjectMatcher[ObjectMatcher.SeqEntry[T]] 9 | type SetMatcher[T] = ObjectMatcher[ObjectMatcher.SetEntry[T]] 10 | type MapMatcher[K, V] = ObjectMatcher[ObjectMatcher.MapEntry[K, V]] 11 | 12 | def compare[T](left: T, right: T)(implicit d: Diff[T]): DiffResult = d.apply(left, right) 13 | 14 | private[diffx] def nullGuard[T](left: T, right: T)(compareNotNull: (T, T) => DiffResult): DiffResult = { 15 | if ((left == null && right != null) || (left != null && right == null)) { 16 | DiffResultValue(left, right) 17 | } else if (left == null && right == null) { 18 | IdenticalValue(null) 19 | } else { 20 | compareNotNull(left, right) 21 | } 22 | } 23 | 24 | trait DiffxSubtypeSelector[T] { 25 | def subtype[S <: T]: S = sys.error("") 26 | } 27 | 28 | implicit def toSubtypeSelector[A](a: A): DiffxSubtypeSelector[A] = new DiffxSubtypeSelector[A] {} 29 | } 30 | 31 | object DiffxSupport { 32 | def canOnlyBeUsedInsideDiffxMacro(method: String) = 33 | s"$method can only be used inside one of Diffx macros('ignore', 'modify')" 34 | } 35 | 36 | trait DiffxEitherSupport { 37 | implicit class DiffxEither[T[_, _], L, R](e: T[L, R])(implicit f: DiffxEitherFunctor[T, L, R]) { 38 | @compileTimeOnly(canOnlyBeUsedInsideDiffxMacro("eachLeft")) 39 | def eachLeft: L = sys.error(canOnlyBeUsedInsideDiffxMacro("eachLeft")) 40 | 41 | @compileTimeOnly(canOnlyBeUsedInsideDiffxMacro("eachRight")) 42 | def eachRight: R = sys.error(canOnlyBeUsedInsideDiffxMacro("eachRight")) 43 | } 44 | 45 | trait DiffxEitherFunctor[T[_, _], L, R] { 46 | def eachLeft(e: T[L, R])(f: L => L): T[L, R] = sys.error("") 47 | def eachRight(e: T[L, R])(f: R => R): T[L, R] = sys.error("") 48 | } 49 | 50 | implicit def eitherDiffxFunctor[T[_, _], L, R]: DiffxEitherFunctor[Either, L, R] = 51 | new DiffxEitherFunctor[Either, L, R] {} 52 | } 53 | 54 | case class ShowConfig( 55 | left: String => String, 56 | right: String => String, 57 | missing: String => String, 58 | additional: String => String, 59 | default: String => String, 60 | arrow: String => String, 61 | transformer: DiffResultTransformer 62 | ) { 63 | def skipIdentical: ShowConfig = this.copy(transformer = DiffResultTransformer.skipIdentical) 64 | } 65 | 66 | object ShowConfig { 67 | val noColors: ShowConfig = 68 | ShowConfig( 69 | default = identity, 70 | arrow = identity, 71 | right = identity, 72 | left = identity, 73 | missing = s => s"-$s", 74 | additional = s => s"+$s", 75 | transformer = identity(_) 76 | ) 77 | val dark: ShowConfig = ShowConfig( 78 | left = magenta, 79 | right = green, 80 | missing = magenta, 81 | additional = green, 82 | default = cyan, 83 | arrow = red, 84 | transformer = identity(_) 85 | ) 86 | val light: ShowConfig = ShowConfig( 87 | default = black, 88 | arrow = red, 89 | left = magenta, 90 | missing = magenta, 91 | right = blue, 92 | additional = blue, 93 | transformer = identity(_) 94 | ) 95 | val normal: ShowConfig = 96 | ShowConfig( 97 | default = identity, 98 | arrow = red, 99 | right = green, 100 | additional = green, 101 | left = red, 102 | missing = red, 103 | transformer = identity(_) 104 | ) 105 | val envDriven: ShowConfig = Option(System.getenv("DIFFX_COLOR_THEME")) match { 106 | case Some("light") => light 107 | case Some("dark") => dark 108 | case _ => normal 109 | } 110 | implicit val default: ShowConfig = handleNoColorsEnv().getOrElse(envDriven) 111 | 112 | private def handleNoColorsEnv(): Option[ShowConfig] = Option(System.getenv("NO_COLOR")).map(_ => noColors) 113 | 114 | def magenta: String => String = toColor(Console.MAGENTA) 115 | def green: String => String = toColor(Console.GREEN) 116 | def blue: String => String = toColor(Console.BLUE) 117 | def cyan: String => String = toColor(Console.CYAN) 118 | def red: String => String = toColor(Console.RED) 119 | def black: String => String = toColor(Console.BLACK) 120 | 121 | private def toColor(color: String) = { (s: String) => color + s + Console.RESET } 122 | } 123 | 124 | trait DiffResultTransformer { 125 | def apply(diffResult: DiffResult): DiffResult 126 | } 127 | 128 | object DiffResultTransformer { 129 | val skipIdentical: DiffResultTransformer = { 130 | case d: DiffResultObject => d.copy(fields = d.fields.filter { case (_, v) => !v.isIdentical }) 131 | case d: DiffResultMap => 132 | d.copy(entries = d.entries.filter { case (k, v) => !v.isIdentical || !k.isIdentical }) 133 | case d: DiffResultSet => d.copy(diffs = d.diffs.filter(df => !df.isIdentical)) 134 | case d: DiffResultIterable => d.copy(items = d.items.filter { case (_, v) => !v.isIdentical }) 135 | case other => other 136 | } 137 | } 138 | 139 | trait DiffxOptionSupport { 140 | implicit class DiffxEach[F[_], T](t: F[T])(implicit f: DiffxFunctor[F, T]) { 141 | @compileTimeOnly(canOnlyBeUsedInsideDiffxMacro("each")) 142 | def each: T = sys.error(canOnlyBeUsedInsideDiffxMacro("each")) 143 | } 144 | 145 | trait DiffxFunctor[F[_], A] { 146 | @compileTimeOnly(canOnlyBeUsedInsideDiffxMacro("each")) 147 | def each(fa: F[A])(f: A => A): F[A] = sys.error(canOnlyBeUsedInsideDiffxMacro("each")) 148 | } 149 | 150 | implicit def optionDiffxFunctor[A]: DiffxFunctor[Option, A] = 151 | new DiffxFunctor[Option, A] {} 152 | } 153 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/Matching.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | private[diffx] object Matching { 4 | private[diffx] def matching[T]( 5 | left: scala.collection.Set[T], 6 | right: scala.collection.Set[T], 7 | matcher: ObjectMatcher[T], 8 | diff: Diff[T], 9 | context: DiffContext 10 | ): MatchingResults[T] = { 11 | val matchedKeys = left.flatMap(l => 12 | right.collectFirst { 13 | case r if matcher.isSameObject(l, r) || diff(l, r, context).isIdentical => l -> r 14 | } 15 | ) 16 | MatchingResults(left.diff(matchedKeys.map(_._1)), right.diff(matchedKeys.map(_._2)), matchedKeys) 17 | } 18 | 19 | private[diffx] case class MatchingResults[T]( 20 | unmatchedLeft: scala.collection.Set[T], 21 | unmatchedRight: scala.collection.Set[T], 22 | matched: scala.collection.Set[(T, T)] 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/ObjectMatcher.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | /** Defines how the elements within collections are paired 4 | * 5 | * @tparam T 6 | * type of the collection element 7 | */ 8 | trait ObjectMatcher[T] { 9 | def isSameObject(left: T, right: T): Boolean 10 | } 11 | 12 | object ObjectMatcher extends LowPriorityObjectMatcher { 13 | def apply[T: ObjectMatcher]: ObjectMatcher[T] = implicitly[ObjectMatcher[T]] 14 | 15 | private def by[T, U: ObjectMatcher](f: T => U): ObjectMatcher[T] = (left: T, right: T) => 16 | ObjectMatcher[U].isSameObject(f(left), f(right)) 17 | 18 | /** Given MapEntry[K,V], match them using K's objectMatcher */ 19 | implicit def mapEntryByKey[K: ObjectMatcher, V]: ObjectMatcher[MapEntry[K, V]] = 20 | ObjectMatcher.by[MapEntry[K, V], K](_.key) 21 | 22 | implicit def setEntryByValue[T: ObjectMatcher]: ObjectMatcher[SetEntry[T]] = ObjectMatcher.by[SetEntry[T], T](_.t) 23 | 24 | type SeqEntry[T] = MapEntry[Int, T] 25 | 26 | case class SetEntry[T](t: T) 27 | 28 | case class MapEntry[K, V](key: K, value: V) 29 | 30 | /** Matcher for all ordered collections e.g. [[List]], [[Seq]]. There has to exist an implicit instance of 31 | * [[com.softwaremill.diffx.SeqLike]] for such collection 32 | * @tparam T 33 | * type of the element 34 | * @return 35 | */ 36 | def seq[T] = new ObjectMatcherSeqHelper[T] 37 | 38 | class ObjectMatcherSeqHelper[V] { 39 | def byValue[U: ObjectMatcher](f: V => U): SeqMatcher[V] = byValue(ObjectMatcher.by[V, U](f)) 40 | 41 | def byValue(implicit ev: ObjectMatcher[V]): SeqMatcher[V] = 42 | ObjectMatcher.by[SeqEntry[V], V](_.value) 43 | 44 | def byIndex[U: ObjectMatcher](f: Int => U): SeqMatcher[V] = byIndex(ObjectMatcher.by(f)) 45 | 46 | def byIndex(implicit indexMatcher: ObjectMatcher[Int]): SeqMatcher[V] = ObjectMatcher.by(_.key) 47 | } 48 | 49 | /** Matcher for unordered collections like e.g. [[Set]]. There has to exist an implicit instance of 50 | * [[com.softwaremill.diffx.SetLike]] for such collection 51 | * 52 | * @tparam T 53 | * type of the element 54 | * @return 55 | */ 56 | def set[T] = new ObjectMatcherSetHelper[T] 57 | 58 | class ObjectMatcherSetHelper[T] { 59 | def by[U: ObjectMatcher](f: T => U): SetMatcher[T] = setEntryByValue(ObjectMatcher.by(f)) 60 | } 61 | 62 | /** Matcher for map like collections e.g. [[Map]]. There has to exist an implicit instance of 63 | * [[com.softwaremill.diffx.MapLike]] for such collection 64 | * @tparam K 65 | * type of the key 66 | * @tparam V 67 | * type of the value 68 | * @return 69 | */ 70 | def map[K, V] = new ObjectMatcherMapHelper[K, V] 71 | 72 | class ObjectMatcherMapHelper[K, V] { 73 | def byValue[U: ObjectMatcher](f: V => U): MapMatcher[K, V] = byValue(ObjectMatcher.by(f)) 74 | 75 | def byValue(implicit ev: ObjectMatcher[V]): MapMatcher[K, V] = ObjectMatcher.by(_.value) 76 | 77 | def byKey[U: ObjectMatcher](f: K => U): MapMatcher[K, V] = byKey(ObjectMatcher.by(f)) 78 | 79 | def byKey(implicit ko: ObjectMatcher[K]): MapMatcher[K, V] = ObjectMatcher.by(_.key) 80 | } 81 | } 82 | 83 | trait LowPriorityObjectMatcher { 84 | implicit def default[T]: ObjectMatcher[T] = (l: T, r: T) => l == r 85 | } 86 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/instances/ApproximateDiffForNumeric.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.instances 2 | 3 | import com.softwaremill.diffx._ 4 | 5 | private[diffx] class ApproximateDiffForNumeric[T: Numeric](epsilon: T) extends Diff[T] { 6 | override def apply(left: T, right: T, context: DiffContext): DiffResult = { 7 | val numeric = implicitly[Numeric[T]] 8 | if (numeric.lt(epsilon, numeric.abs(numeric.minus(left, right)))) { 9 | DiffResultValue(left, right) 10 | } else { 11 | IdenticalValue(left) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/instances/DiffForEither.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.instances 2 | 3 | import com.softwaremill.diffx.{Diff, DiffContext, DiffResult, DiffResultValue, ModifyPath} 4 | 5 | private[diffx] class DiffForEither[L, R](ld: Diff[L], rd: Diff[R]) extends Diff[Either[L, R]] { 6 | override def apply( 7 | left: Either[L, R], 8 | right: Either[L, R], 9 | context: DiffContext 10 | ): DiffResult = { 11 | (left, right) match { 12 | case (Left(v1), Left(v2)) => 13 | ld.apply(v1, v2, context.getNextStep(ModifyPath.Subtype("scala.package", "Left"))) 14 | case (Right(v1), Right(v2)) => 15 | rd.apply(v1, v2, context.getNextStep(ModifyPath.Subtype("scala.package", "Right"))) 16 | case (v1, v2) => DiffResultValue(v1, v2) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/instances/DiffForMap.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.instances 2 | 3 | import com.softwaremill.diffx.ObjectMatcher.MapEntry 4 | import com.softwaremill.diffx._ 5 | import com.softwaremill.diffx.instances.internal.MatchResult 6 | 7 | import scala.annotation.tailrec 8 | 9 | private[diffx] class DiffForMap[C[_, _], K, V]( 10 | matcher: ObjectMatcher[MapEntry[K, V]], 11 | diffKey: Diff[K], 12 | diffValue: Diff[V], 13 | mapLike: MapLike[C], 14 | typename: String = "Map" 15 | ) extends Diff[C[K, V]] { 16 | override def apply( 17 | left: C[K, V], 18 | right: C[K, V], 19 | context: DiffContext 20 | ): DiffResult = nullGuard(left, right) { (left, right) => 21 | val adjustedMatcher = context.getMatcherOverride[MapEntry[K, V]].getOrElse(matcher) 22 | val matches = matchPairs( 23 | mapLike.asMap(left).toList.map { case (k, v) => MapEntry(k, v) }, 24 | mapLike.asMap(right).toList.map { case (k, v) => MapEntry(k, v) }, 25 | adjustedMatcher, 26 | List.empty, 27 | context 28 | ) 29 | val diffs = matches.map { 30 | case MatchResult.UnmatchedLeft(entry) => DiffResultAdditional(entry.key) -> DiffResultAdditional(entry.value) 31 | case MatchResult.UnmatchedRight(entry) => DiffResultMissing(entry.key) -> DiffResultMissing(entry.value) 32 | case MatchResult.Matched(lEntry, rEntry) => 33 | diffKey(lEntry.key, rEntry.key, context.getNextStep(ModifyPath.EachKey)) -> diffValue( 34 | lEntry.value, 35 | rEntry.value, 36 | context.getNextStep(ModifyPath.EachValue) 37 | ) 38 | } 39 | DiffResultMap(typename, diffs.toMap) 40 | } 41 | 42 | @tailrec 43 | private def matchPairs( 44 | left: List[MapEntry[K, V]], 45 | right: List[MapEntry[K, V]], 46 | matcher: ObjectMatcher[MapEntry[K, V]], 47 | matched: List[MatchResult[MapEntry[K, V]]], 48 | context: DiffContext 49 | ): List[MatchResult[MapEntry[K, V]]] = { 50 | right match { 51 | case rHead :: rTail => 52 | val maybeMatched = left 53 | .map { l => 54 | val isSame = matcher.isSameObject(rHead, l) 55 | val isIdentical = diffKey.apply(l.key, rHead.key, context).isIdentical 56 | (l -> rHead, isSame, isIdentical) 57 | } 58 | .filter { case (_, isSame, isIdentical) => isSame || isIdentical } 59 | .sortBy { case (_, _, isIdentical) => !isIdentical } 60 | .map { case (lr, _, _) => lr } 61 | .headOption 62 | maybeMatched match { 63 | case Some((lm, rm)) => 64 | matchPairs( 65 | left.filterNot(l => l.key == lm.key), 66 | rTail, 67 | matcher, 68 | matched :+ MatchResult.Matched(lm, rm), 69 | context 70 | ) 71 | case None => matchPairs(left, rTail, matcher, matched :+ MatchResult.UnmatchedRight(rHead), context) 72 | } 73 | case Nil => matched ++ left.map(l => MatchResult.UnmatchedLeft(l)) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/instances/DiffForNumeric.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.instances 2 | 3 | import com.softwaremill.diffx._ 4 | 5 | private[diffx] class DiffForNumeric[T: Numeric] extends Diff[T] { 6 | override def apply(left: T, right: T, context: DiffContext): DiffResult = { 7 | val numeric = implicitly[Numeric[T]] 8 | if (!numeric.equiv(left, right)) { 9 | DiffResultValue(left, right) 10 | } else { 11 | IdenticalValue(left) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/instances/DiffForOption.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.instances 2 | 3 | import com.softwaremill.diffx._ 4 | 5 | private[diffx] class DiffForOption[T](dt: Diff[T]) extends Diff[Option[T]] { 6 | override def apply(left: Option[T], right: Option[T], context: DiffContext): DiffResult = { 7 | (left, right) match { 8 | case (Some(l), Some(r)) => dt.apply(l, r, context) 9 | case (None, None) => IdenticalValue(None) 10 | case (l, r) => DiffResultValue(l, r) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/instances/DiffForSeq.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.instances 2 | 3 | import com.softwaremill.diffx.ObjectMatcher.{SeqEntry, MapEntry} 4 | import com.softwaremill.diffx._ 5 | import com.softwaremill.diffx.instances.DiffForSeq._ 6 | import com.softwaremill.diffx.instances.internal.MatchResult 7 | 8 | import scala.annotation.tailrec 9 | import scala.collection.immutable.ListMap 10 | 11 | class DiffForSeq[C[_], T]( 12 | dt: Diff[T], 13 | matcher: ObjectMatcher[SeqEntry[T]], 14 | seqLike: SeqLike[C], 15 | typeName: String = "List" 16 | ) extends Diff[C[T]] { 17 | override def apply(left: C[T], right: C[T], context: DiffContext): DiffResult = nullGuard(left, right) { 18 | (left, right) => 19 | val adjustedMatcher = context.getMatcherOverride[SeqEntry[T]].getOrElse(matcher) 20 | val leftWithIndex = seqLike.asSeq(left).zipWithIndex.map { case (lv, i) => MapEntry(i, lv) } 21 | val rightWithIndex = seqLike.asSeq(right).zipWithIndex.map { case (rv, i) => MapEntry(i, rv) } 22 | 23 | val (matched, unmatched) = matchPairs(leftWithIndex, rightWithIndex, adjustedMatcher, List.empty, context) 24 | 25 | val allMatches = unmatched 26 | .foldLeft(matched.sorted(diffResultOrdering[T].reverse)) { (acc, item) => 27 | insertUnmatchedLeft(acc, item, Nil) 28 | } 29 | .reverse 30 | 31 | val rawDiffs = allMatches.map { 32 | case MatchResult.UnmatchedLeft(v) => DiffResultAdditional(v.value) 33 | case MatchResult.UnmatchedRight(v) => DiffResultMissing(v.value) 34 | case MatchResult.Matched(l, r) => dt.apply(l.value, r.value, context.getNextStep(ModifyPath.Each)) 35 | } 36 | val reindexed = rawDiffs.zipWithIndex.map(_.swap) 37 | val diffs = ListMap(reindexed.map { case (k, v) => k.toString -> v }: _*) 38 | DiffResultObject(typeName, diffs) 39 | } 40 | 41 | @tailrec 42 | private def insertUnmatchedLeft( 43 | matches: Seq[MatchResult[SeqEntry[T]]], 44 | item: MatchResult.UnmatchedLeft[SeqEntry[T]], 45 | bigHead: Seq[MatchResult[SeqEntry[T]]] 46 | ): Seq[MatchResult[SeqEntry[T]]] = { 47 | matches.toList match { 48 | case ::(head, tl) => 49 | val shouldBeAfter = head match { 50 | case MatchResult.UnmatchedRight(_) => true 51 | case MatchResult.UnmatchedLeft(v) => v.key > item.v.key 52 | case MatchResult.Matched(l, _) => l.key > item.v.key 53 | } 54 | if (shouldBeAfter) { 55 | insertUnmatchedLeft(tl, item, bigHead :+ head) 56 | } else { 57 | bigHead ++ (item :: (head :: tl)) 58 | } 59 | case Nil => bigHead :+ item 60 | } 61 | } 62 | 63 | @tailrec 64 | private def matchPairs( 65 | left: Seq[SeqEntry[T]], 66 | right: Seq[SeqEntry[T]], 67 | matcher: ObjectMatcher[SeqEntry[T]], 68 | matched: List[MatchResult[SeqEntry[T]]], 69 | context: DiffContext 70 | ): (Seq[MatchResult[SeqEntry[T]]], Seq[MatchResult.UnmatchedLeft[SeqEntry[T]]]) = { 71 | right.toList match { 72 | case ::(rHead, tailRight) => 73 | val maybeMatched = left 74 | .collect { case l if matcher.isSameObject(rHead, l) => l -> rHead } 75 | .sortBy { case (l, r) => !dt.apply(l.value, r.value, context).isIdentical } 76 | .headOption 77 | maybeMatched match { 78 | case Some((lm, rm)) => 79 | matchPairs( 80 | left.filterNot(l => l.key == lm.key), 81 | tailRight, 82 | matcher, 83 | matched :+ MatchResult.Matched(lm, rm), 84 | context 85 | ) 86 | case None => matchPairs(left, tailRight, matcher, matched :+ MatchResult.UnmatchedRight(rHead), context) 87 | } 88 | case Nil => matched -> left.map(l => MatchResult.UnmatchedLeft(l)) 89 | } 90 | } 91 | } 92 | 93 | object DiffForSeq { 94 | implicit def iterableEntryOrdering[T]: Ordering[SeqEntry[T]] = Ordering.by(_.key) 95 | 96 | implicit def diffResultOrdering[T]: Ordering[MatchResult[SeqEntry[T]]] = 97 | new Ordering[MatchResult[SeqEntry[T]]] { 98 | override def compare(x: MatchResult[SeqEntry[T]], y: MatchResult[SeqEntry[T]]): Int = { 99 | (x, y) match { 100 | case (ur: MatchResult.UnmatchedRight[SeqEntry[T]], m: MatchResult.Matched[SeqEntry[T]]) => 101 | iterableEntryOrdering.compare(ur.v, m.r) 102 | case (m: MatchResult.Matched[SeqEntry[T]], ur: MatchResult.UnmatchedRight[SeqEntry[T]]) => 103 | iterableEntryOrdering.compare(m.r, ur.v) 104 | case (ur: MatchResult.UnmatchedRight[SeqEntry[T]], ur2: MatchResult.UnmatchedRight[SeqEntry[T]]) => 105 | iterableEntryOrdering.compare(ur.v, ur2.v) 106 | case (m1: MatchResult.Matched[SeqEntry[T]], m2: MatchResult.Matched[SeqEntry[T]]) => 107 | Ordering 108 | .by[MatchResult.Matched[SeqEntry[T]], (SeqEntry[T], SeqEntry[T])](m => (m.r, m.l)) 109 | .compare(m1, m2) 110 | case (_: MatchResult.UnmatchedLeft[_], _) => throw new IllegalStateException("cannot happen") 111 | case (_, _: MatchResult.UnmatchedLeft[_]) => throw new IllegalStateException("cannot happen") 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/instances/DiffForSet.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.instances 2 | 3 | import com.softwaremill.diffx.ObjectMatcher.SetEntry 4 | import com.softwaremill.diffx._ 5 | import com.softwaremill.diffx.instances.internal.{IndexedEntry, MatchResult} 6 | 7 | import scala.annotation.tailrec 8 | 9 | private[diffx] class DiffForSet[C[_], T]( 10 | dt: Diff[T], 11 | matcher: ObjectMatcher[SetEntry[T]], 12 | setLike: SetLike[C], 13 | typename: String = "Set" 14 | ) extends Diff[C[T]] { 15 | override def apply(left: C[T], right: C[T], context: DiffContext): DiffResult = nullGuard(left, right) { 16 | (left, right) => 17 | val adjustedMatcher = context.getMatcherOverride[SetEntry[T]].getOrElse(matcher) 18 | val matches = matchPairs( 19 | setLike.asSet(left).toList.zipWithIndex.map(p => IndexedEntry(p._2, SetEntry(p._1))), 20 | setLike.asSet(right).toList.zipWithIndex.map(p => IndexedEntry(p._2, SetEntry(p._1))), 21 | adjustedMatcher, 22 | List.empty, 23 | context 24 | ) 25 | val diffs = matches.map { 26 | case MatchResult.UnmatchedLeft(v) => DiffResultAdditional(v) 27 | case MatchResult.UnmatchedRight(v) => DiffResultMissing(v) 28 | case MatchResult.Matched(l, r) => dt.apply(l, r, context.getNextStep(ModifyPath.Each)) 29 | } 30 | DiffResultSet(typename, diffs.toSet) 31 | } 32 | @tailrec 33 | private def matchPairs( 34 | left: List[IndexedEntry[SetEntry[T]]], 35 | right: List[IndexedEntry[SetEntry[T]]], 36 | matcher: ObjectMatcher[SetEntry[T]], 37 | matched: List[MatchResult[T]], 38 | context: DiffContext 39 | ): List[MatchResult[T]] = { 40 | right match { 41 | case rHead :: rTail => 42 | val maybeMatched = left 43 | .map { l => 44 | val isSame = matcher.isSameObject(rHead.value, l.value) 45 | val isIdentical = dt.apply(l.value.t, rHead.value.t, context).isIdentical 46 | (l -> rHead, isSame, isIdentical) 47 | } 48 | .filter { case (_, isSame, isIdentical) => isSame || isIdentical } 49 | .sortBy { case (_, _, isIdentical) => !isIdentical } 50 | .map { case (lr, _, _) => lr } 51 | .headOption 52 | maybeMatched match { 53 | case Some((lm, rm)) => 54 | matchPairs( 55 | left.filterNot(l => l.index == lm.index), 56 | rTail, 57 | matcher, 58 | matched :+ MatchResult.Matched(lm.value.t, rm.value.t), 59 | context 60 | ) 61 | case None => matchPairs(left, rTail, matcher, matched :+ MatchResult.UnmatchedRight(rHead.value.t), context) 62 | } 63 | case Nil => matched ++ left.map(l => MatchResult.UnmatchedLeft(l.value.t)) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/instances/internal/IndexedEntry.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.instances.internal 2 | 3 | private[instances] case class IndexedEntry[T](index: Int, value: T) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/instances/internal/MatchResult.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.instances.internal 2 | 3 | private[instances] sealed trait MatchResult[T] 4 | 5 | private[instances] object MatchResult { 6 | case class UnmatchedLeft[T](v: T) extends MatchResult[T] 7 | case class UnmatchedRight[T](v: T) extends MatchResult[T] 8 | case class Matched[T](l: T, r: T) extends MatchResult[T] 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/instances/string/Chunk.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.instances.string 2 | 3 | private[instances] case class Chunk[T](position: Int, lines: List[T]) { 4 | def size: Int = lines.size 5 | def last: Int = position + size - 1 6 | } 7 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/instances/string/Delta.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.instances.string 2 | 3 | private[instances] sealed trait Delta[T] { 4 | def original: Chunk[T] 5 | def revised: Chunk[T] 6 | def getSource: Chunk[T] = original 7 | def getTarget: Chunk[T] = revised 8 | } 9 | 10 | private[instances] case class ChangeDelta[T](original: Chunk[T], revised: Chunk[T]) extends Delta[T] 11 | private[instances] case class InsertDelta[T](original: Chunk[T], revised: Chunk[T]) extends Delta[T] 12 | private[instances] case class DeleteDelta[T](original: Chunk[T], revised: Chunk[T]) extends Delta[T] 13 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/instances/string/DiffRow.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.instances.string 2 | 3 | private[instances] sealed trait DiffRow[T] 4 | private[instances] object DiffRow { 5 | case class Insert[T](newLine: T) extends DiffRow[T] 6 | case class Delete[T](oldLine: T) extends DiffRow[T] 7 | case class Change[T](oldLine: T, newLine: T) extends DiffRow[T] 8 | case class Equal[T](oldLine: T, newLine: T) extends DiffRow[T] 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/instances/string/DiffRowGenerator.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.instances.string 2 | 3 | import java.util 4 | import java.util.Collections 5 | import scala.collection.JavaConverters._ 6 | 7 | private[instances] class DiffRowGenerator { 8 | 9 | /** Get the DiffRows describing the difference between original and revised texts using the given patch. Useful for 10 | * displaying side-by-side diff. 11 | * 12 | * @param original 13 | * the original text 14 | * @param revised 15 | * the revised text 16 | * @return 17 | * the DiffRows between original and revised texts 18 | */ 19 | def generateDiffRows[T]( 20 | original: List[T], 21 | revised: List[T], 22 | equalizer: (T, T) => Boolean = (t1: T, t2: T) => t1 == t2 23 | ): List[DiffRow[T]] = { 24 | val patch = DiffUtils.diff(original, revised, equalizer) 25 | generateDiffRowsFromPatch(original.asJava, patch, revised.asJava).asScala.toList 26 | } 27 | 28 | private def generateDiffRowsFromPatch[T]( 29 | original: util.List[T], 30 | patch: Patch[T], 31 | revised: util.List[T] 32 | ): util.List[DiffRow[T]] = { 33 | val diffRows = new util.ArrayList[DiffRow[T]] 34 | var endPos = 0 35 | val deltaList = patch.getDeltas 36 | for (originalDelta <- deltaList.asScala) { 37 | for (delta <- decompressDeltas(originalDelta).asScala) { 38 | endPos = transformDeltaIntoDiffRow(original, endPos, diffRows, delta, revised) 39 | } 40 | } 41 | // Copy the final matching chunk if any. 42 | for (line <- original.subList(endPos, original.size).asScala) { 43 | diffRows.add(DiffRow.Equal(line, line)) 44 | } 45 | diffRows 46 | } 47 | 48 | /** Transforms one patch delta into a DiffRow object. 49 | */ 50 | private def transformDeltaIntoDiffRow[T]( 51 | original: util.List[T], 52 | endPos: Int, 53 | diffRows: util.List[DiffRow[T]], 54 | delta: Delta[T], 55 | revised: util.List[T] 56 | ) = { 57 | val orig = delta.getSource 58 | val rev = delta.getTarget 59 | for (line <- original.subList(endPos, orig.position).asScala) { 60 | diffRows.add(DiffRow.Equal(line, revised.get(rev.position - 1))) 61 | } 62 | delta match { 63 | case InsertDelta(_, revised) => 64 | for (line <- revised.lines) { 65 | diffRows.add(DiffRow.Insert(line)) 66 | } 67 | 68 | case DeleteDelta(original, _) => 69 | for (line <- original.lines) { 70 | diffRows.add(DiffRow.Delete(line)) 71 | } 72 | 73 | case ChangeDelta(original, revised) => 74 | for (j <- 0 until Math.max(original.size, revised.size)) { 75 | diffRows.add( 76 | DiffRow.Change(original.lines(j), revised.lines(j)) 77 | ) 78 | } 79 | } 80 | orig.last + 1 81 | } 82 | 83 | /** Decompresses ChangeDeltas with different source and target size to a ChangeDelta with same size and a following 84 | * InsertDelta or DeleteDelta. With this problems of building DiffRows getting smaller. 85 | * 86 | * @param deltaList 87 | */ 88 | private def decompressDeltas[T](delta: Delta[T]): util.List[Delta[T]] = { 89 | if (delta.isInstanceOf[ChangeDelta[T]] && delta.getSource.size != delta.getTarget.size) { 90 | val deltas = new util.ArrayList[Delta[T]] 91 | val minSize = Math.min(delta.getSource.size, delta.getTarget.size) 92 | val orig = delta.getSource 93 | val rev = delta.getTarget 94 | deltas.add( 95 | new ChangeDelta[T]( 96 | new Chunk[T](orig.position, orig.lines.slice(0, minSize)), 97 | new Chunk[T](rev.position, rev.lines.slice(0, minSize)) 98 | ) 99 | ) 100 | if (orig.lines.size < rev.lines.size) 101 | deltas.add( 102 | new InsertDelta[T]( 103 | new Chunk[T](orig.position + minSize, scala.List.empty), 104 | new Chunk[T](rev.position + minSize, rev.lines.slice(minSize, rev.lines.size)) 105 | ) 106 | ) 107 | else 108 | deltas.add( 109 | new DeleteDelta[T]( 110 | new Chunk[T](orig.position + minSize, orig.lines.slice(minSize, orig.lines.size)), 111 | new Chunk[T](rev.position + minSize, scala.List.empty) 112 | ) 113 | ) 114 | return deltas 115 | } 116 | Collections.singletonList(delta) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/instances/string/DiffUtils.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.instances.string 2 | 3 | import scala.collection.JavaConverters._ 4 | 5 | private[instances] object DiffUtils { 6 | def diff[T]( 7 | original: List[T], 8 | revised: List[T], 9 | equalizer: (T, T) => Boolean 10 | ): Patch[T] = 11 | new MyersDiff[T](equalizer).diff(original.asJava, revised.asJava) 12 | } 13 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/instances/string/DifferentiationFailedException.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.instances.string 2 | 3 | private[instances] class DifferentiationFailedException(message: String) extends RuntimeException(message) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/instances/string/MyersDiff.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.instances.string 2 | 3 | import java.util 4 | import scala.collection.JavaConverters._ 5 | 6 | // copied from https://github.com/java-diff-utils/java-diff-utils 7 | private[instances] class MyersDiff[T](equalizer: (T, T) => Boolean) { 8 | def diff( 9 | original: util.List[T], 10 | revised: util.List[T] 11 | ): Patch[T] = { 12 | try { 13 | buildRevision(buildPath(original, revised), original, revised) 14 | } catch { 15 | case e: DifferentiationFailedException => 16 | e.printStackTrace() 17 | new Patch[T]() 18 | } 19 | } 20 | private def buildRevision( 21 | _path: PathNode, 22 | orig: util.List[T], 23 | rev: util.List[T] 24 | ) = { 25 | var path = _path 26 | val patch = new Patch[T] 27 | if (path.isSnake) path = path.prev 28 | while ( 29 | path != null && 30 | path.prev != null && 31 | path.prev.j >= 0 32 | ) { 33 | if (path.isSnake) 34 | throw new IllegalStateException( 35 | "bad diffpath: found snake when looking for diff" 36 | ) 37 | val i = path.i 38 | val j = path.j 39 | path = path.prev 40 | val ianchor = path.i 41 | val janchor = path.j 42 | val original = 43 | new Chunk[T]( 44 | ianchor, 45 | copyOfRange(orig, ianchor, i).asScala.toList 46 | ) 47 | val revised = 48 | new Chunk[T]( 49 | janchor, 50 | copyOfRange(rev, janchor, j).asScala.toList 51 | ) 52 | val delta: Delta[T] = 53 | if (original.size == 0 && revised.size != 0) { 54 | new InsertDelta[T](original, revised) 55 | } else if (original.size > 0 && revised.size == 0) { 56 | new DeleteDelta[T](original, revised) 57 | } else { 58 | new ChangeDelta[T](original, revised) 59 | } 60 | patch.addDelta(delta) 61 | if (path.isSnake) { 62 | path = path.prev 63 | } 64 | } 65 | patch 66 | } 67 | 68 | private def copyOfRange(original: util.List[T], fromIndex: Int, to: Int) = 69 | new util.ArrayList[T](original.subList(fromIndex, to)) 70 | 71 | def buildPath( 72 | orig: util.List[T], 73 | rev: util.List[T] 74 | ): PathNode = { 75 | 76 | val N = orig.size() 77 | val M = rev.size() 78 | 79 | val MAX = N + M + 1 80 | val size = 1 + 2 * MAX 81 | val middle = size / 2 82 | val diagonal = new Array[PathNode](size) 83 | 84 | diagonal(middle + 1) = new Snake(0, -1, null) 85 | var d = 0 86 | while (d < MAX) { 87 | var k = -d 88 | while (k <= d) { 89 | val kmiddle = middle + k 90 | val kplus = kmiddle + 1 91 | val kminus = kmiddle - 1 92 | var prev: PathNode = null 93 | var i = 0 94 | if ((k == -d) || (k != d && diagonal(kminus).i < diagonal(kplus).i)) { 95 | i = diagonal(kplus).i 96 | prev = diagonal(kplus) 97 | } else { 98 | i = diagonal(kminus).i + 1 99 | prev = diagonal(kminus) 100 | } 101 | diagonal(kminus) = null // no longer used 102 | 103 | var j = i - k 104 | var node: PathNode = new DiffNode(i, j, prev) 105 | // orig and rev are zero-based 106 | // but the algorithm is one-based 107 | // that's why there's no +1 when indexing the sequences 108 | while ( 109 | i < N && 110 | j < M && 111 | equalizer(orig.get(i), rev.get(j)) 112 | ) { 113 | i += 1 114 | j += 1 115 | } 116 | if (i > node.i) { 117 | node = new Snake(i, j, node) 118 | } 119 | diagonal(kmiddle) = node 120 | if (i >= N && j >= M) { 121 | return diagonal(kmiddle) 122 | } 123 | 124 | k += 2 125 | } 126 | diagonal(middle + d - 1) = null 127 | d += 1 128 | } 129 | // According to Myers, this cannot happen 130 | throw new DifferentiationFailedException("could not find a diff path") 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/instances/string/Patch.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.instances.string 2 | 3 | import java.util 4 | import java.util.{Collections, Comparator} 5 | 6 | private[instances] class Patch[T] { 7 | private val deltas: util.List[Delta[T]] = new util.ArrayList() 8 | private val comparator: Comparator[Delta[T]] = new Comparator[Delta[T]] { 9 | override def compare(o1: Delta[T], o2: Delta[T]): Int = 10 | o1.original.position.compareTo(o2.original.position) 11 | } 12 | def addDelta(delta: Delta[T]): Unit = { 13 | deltas.add(delta) 14 | } 15 | def getDeltas: util.List[Delta[T]] = { 16 | Collections.sort(deltas, comparator) 17 | deltas 18 | } 19 | 20 | override def toString: String = s"Patch($deltas)" 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/scala/com/softwaremill/diffx/instances/string/PathNode.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.instances.string 2 | 3 | private[instances] sealed abstract class PathNode(val i: Int, val j: Int, val prev: PathNode) { 4 | 5 | def isSnake: Boolean 6 | final def isBootstrap: Boolean = { 7 | i < 0 || j < 0 8 | } 9 | final def previousSnake: PathNode = { 10 | if (isBootstrap) null 11 | else if (!isSnake && prev != null) prev.previousSnake 12 | else this 13 | } 14 | 15 | override def toString: String = { 16 | val buf = new StringBuffer("[") 17 | var node = this 18 | while (node != null) { 19 | buf.append("(") 20 | buf.append(Integer.toString(node.i)) 21 | buf.append(",") 22 | buf.append(Integer.toString(node.j)) 23 | buf.append(")") 24 | node = node.prev 25 | } 26 | buf.append("]") 27 | buf.toString 28 | } 29 | } 30 | 31 | final class DiffNode(i: Int, j: Int, prev: PathNode) 32 | extends PathNode(i, j, if (prev == null) null else prev.previousSnake) { 33 | override def isSnake: Boolean = false 34 | } 35 | 36 | final class Snake(i: Int, j: Int, prev: PathNode) extends PathNode(i, j, prev) { 37 | override def isSnake: Boolean = true 38 | } 39 | -------------------------------------------------------------------------------- /core/src/main/scalajs/diffx/DiffxPlatformExtensions.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | trait DiffxPlatformExtensions 4 | -------------------------------------------------------------------------------- /core/src/main/scalajvm/com/softwaremill/diffx/DiffxPlatformExtensions.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | import java.time._ 4 | import java.util.UUID 5 | 6 | trait DiffxPlatformExtensions { 7 | implicit val diffUuid: Diff[UUID] = Diff.useEquals 8 | 9 | implicit val diffInstant: Diff[Instant] = Diff.useEquals 10 | implicit val diffLocalDate: Diff[LocalDate] = Diff.useEquals 11 | implicit val diffLocalTime: Diff[LocalTime] = Diff.useEquals 12 | implicit val diffLocalDateTime: Diff[LocalDateTime] = Diff.useEquals 13 | implicit val diffOffsetDateTime: Diff[OffsetDateTime] = Diff.useEquals 14 | implicit val diffZonedDateTime: Diff[ZonedDateTime] = Diff.useEquals 15 | } 16 | -------------------------------------------------------------------------------- /core/src/test/scala/com/softwaremill/diffx/test/DiffSemiautoTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.test 2 | 3 | import com.softwaremill.diffx.test.ACoproduct.ProductA 4 | import com.softwaremill.diffx.Diff 5 | import org.scalatest.freespec.AnyFreeSpec 6 | import org.scalatest.matchers.should.Matchers 7 | 8 | class DiffSemiautoTest extends AnyFreeSpec with Matchers { 9 | "should compile if all required instances are defined" in { 10 | assertCompiles(""" 11 | |import com.softwaremill.diffx._ 12 | |final case class P1(f1: String) 13 | |final case class P2(f1: P1) 14 | | 15 | |implicit val p1: Diff[P1] = Diff.derived[P1] 16 | |implicit val p2: Diff[P2] = Diff.derived[P2] 17 | |""".stripMargin) 18 | } 19 | 20 | "should not allow to compile if an instance is missing" in { 21 | assertDoesNotCompile(""" 22 | |import com.softwaremill.diffx._ 23 | |final case class P1(f1: String) 24 | |final case class P2(f1: P1) 25 | | 26 | |implicit val p2: Diff[P2] = Diff.derived[P2] 27 | |""".stripMargin) 28 | } 29 | 30 | "should compile with generic.auto._" in { 31 | assertCompiles(""" 32 | |import com.softwaremill.diffx._ 33 | |import com.softwaremill.diffx.generic.auto.diffForCaseClass 34 | |final case class P1(f1: String) 35 | |final case class P2(f1: P1) 36 | | 37 | |val p2: Diff[P2] = Diff[P2] 38 | |""".stripMargin) 39 | } 40 | 41 | "should work for coproducts" in { 42 | implicit val dACoproduct: Diff[ACoproduct] = Diff.derived[ACoproduct] 43 | 44 | Diff.compare[ACoproduct](ProductA("1"), ProductA("1")).isIdentical shouldBe true 45 | } 46 | 47 | "should allow ignoring on derived diffs" in { 48 | implicit val dACoproduct: Diff[ProductA] = Diff.derived[ProductA].ignore(_.id) 49 | 50 | Diff.compare[ProductA](ProductA("1"), ProductA("2")).isIdentical shouldBe true 51 | } 52 | 53 | "should allow modifying derived diffs" in { 54 | implicit val dACoproduct: Diff[ProductA] = Diff.derived[ProductA].modify(_.id).ignore 55 | 56 | Diff.compare[ProductA](ProductA("1"), ProductA("2")).isIdentical shouldBe true 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /core/src/test/scala/com/softwaremill/diffx/test/MatchByOpsTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.test 2 | 3 | import com.softwaremill.diffx.generic.AutoDerivation 4 | import com.softwaremill.diffx.test.ACoproduct.{ProductA, ProductB} 5 | import org.scalatest.flatspec.AnyFlatSpec 6 | import org.scalatest.matchers.should.Matchers 7 | import com.softwaremill.diffx._ 8 | 9 | class MatchByOpsTest extends AnyFlatSpec with AutoDerivation with Matchers { 10 | it should "compile when using lens extensions to modify matcher" in { 11 | Diff.summon[Organization].modify(_.people).matchByValue(_.age) 12 | } 13 | 14 | it should "compile when using diff extensions to modify matcher" in { 15 | Diff.summon[List[Person]].matchByValue(_.age) 16 | } 17 | 18 | it should "allow to ignore property on a subtype" in { 19 | import com.softwaremill.diffx._ 20 | val coproductDiff = Diff[ACoproduct].modify(_.subtype[ProductA].id).ignore 21 | 22 | coproductDiff(ProductA("ignored"), ProductA("ignored-again")).isIdentical shouldBe true 23 | coproductDiff(ProductB("ignored"), ProductB("ignored-again")).isIdentical shouldBe false 24 | } 25 | 26 | it should "should ignore diffs on particular subtype" in { 27 | import com.softwaremill.diffx._ 28 | val coproductDiff = Diff[ACoproduct].modify(_.subtype[ProductA]).ignore 29 | 30 | coproductDiff(ProductA("ignored"), ProductA("ignored-again")).isIdentical shouldBe true 31 | coproductDiff(ProductB("ignored"), ProductB("ignored-again")).isIdentical shouldBe false 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /core/src/test/scala/com/softwaremill/diffx/test/ModifyMacroTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.test 2 | 3 | import com.softwaremill.diffx._ 4 | import com.softwaremill.diffx.ModifyMacro 5 | import org.scalatest.flatspec.AnyFlatSpec 6 | import org.scalatest.matchers.should.Matchers 7 | 8 | class ModifyMacroTest extends AnyFlatSpec with Matchers { 9 | it should "ignore field in nested products" in { 10 | ModifyMacro.modifiedFromPath[Family, String](_.first.name) shouldBe List( 11 | ModifyPath.Field("first"), 12 | ModifyPath.Field("name") 13 | ) 14 | } 15 | 16 | it should "ignore fields in list of products" in { 17 | ModifyMacro.modifiedFromPath[Organization, String](_.people.each.name) shouldBe List( 18 | ModifyPath.Field("people"), 19 | ModifyPath.Each, 20 | ModifyPath.Field("name") 21 | ) 22 | } 23 | 24 | it should "ignore fields in product wrapped with either" in { 25 | ModifyMacro.modifiedFromPath[Either[Person, Person], String](_.eachRight.name) shouldBe List( 26 | ModifyPath.Subtype("scala.package", "Right"), 27 | ModifyPath.Field("name") 28 | ) 29 | ModifyMacro.modifiedFromPath[Either[Person, Person], String](_.eachLeft.name) shouldBe List( 30 | ModifyPath.Subtype("scala.package", "Left"), 31 | ModifyPath.Field("name") 32 | ) 33 | } 34 | 35 | it should "ignore fields in product wrapped with option" in { 36 | ModifyMacro.modifiedFromPath[Option[Person], String](_.each.name) shouldBe List( 37 | ModifyPath.Each, 38 | ModifyPath.Field("name") 39 | ) 40 | } 41 | 42 | it should "ignore part of map value" in { 43 | ModifyMacro.modifiedFromPath[Map[String, Person], String](_.eachValue.name) shouldBe List( 44 | ModifyPath.EachValue, 45 | ModifyPath.Field("name") 46 | ) 47 | } 48 | 49 | it should "ignore part of map key" in { 50 | ModifyMacro.modifiedFromPath[Map[Person, String], String](_.eachKey.name) shouldBe List( 51 | ModifyPath.EachKey, 52 | ModifyPath.Field("name") 53 | ) 54 | } 55 | 56 | it should "ignore part of set value" in { 57 | ModifyMacro.modifiedFromPath[Set[Person], String](_.each.name) shouldBe List( 58 | ModifyPath.Each, 59 | ModifyPath.Field("name") 60 | ) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/src/test/scala/com/softwaremill/diffx/test/examples.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.test 2 | 3 | import java.time.Instant 4 | import java.util.UUID 5 | 6 | sealed trait ACoproduct 7 | object ACoproduct { 8 | case class ProductA(id: String) extends ACoproduct 9 | case class ProductB(id: String) extends ACoproduct 10 | } 11 | 12 | case class Person(name: String, age: Int, in: Instant) 13 | 14 | case class Family(first: Person, second: Person) 15 | 16 | case class Organization(people: List[Person]) 17 | 18 | case class Startup(workers: Set[Person]) 19 | 20 | sealed trait Parent 21 | 22 | case class Bar(s: String, i: Int) extends Parent 23 | 24 | case class Foo(bar: Bar, b: List[Int], parent: Option[Parent]) extends Parent 25 | 26 | class HasCustomEquals(val s: String) { 27 | override def equals(obj: Any): Boolean = { 28 | obj match { 29 | case o: HasCustomEquals => this.s.length == o.s.length 30 | case _ => false 31 | } 32 | } 33 | } 34 | 35 | sealed trait TsDirection 36 | 37 | object TsDirection { 38 | case object Incoming extends TsDirection 39 | 40 | case object Outgoing extends TsDirection 41 | } 42 | 43 | case class KeyModel(id: UUID, name: String) 44 | 45 | case class MyLookup(map: Map[KeyModel, String]) 46 | case class MyLookupReversed(map: Map[String, KeyModel]) 47 | 48 | class NonCaseClass(private val field: String) { 49 | override def toString: String = s"NonCaseClass($field)" 50 | 51 | override def equals(obj: Any): Boolean = { 52 | if (obj.isInstanceOf[NonCaseClass]) { 53 | val other = obj.asInstanceOf[NonCaseClass] 54 | return other.field == this.field 55 | } else { 56 | return false 57 | } 58 | } 59 | } 60 | 61 | class VerboseNonCaseClass(private val key: String, private val value: Int) { 62 | override def toString: String = 63 | s"""VerboseNonCaseClass( 64 | | key: $key, 65 | | value: $value 66 | |)""".stripMargin 67 | 68 | override def equals(obj: Any): Boolean = obj match { 69 | case other: VerboseNonCaseClass => other.key == key && other.value == value 70 | case _ => false 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /core/src/test/scalajvm/com/softwaremill/diffx/test/DiffxJvmInstancesTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.test 2 | 3 | import com.softwaremill.diffx.{Diff, DiffResult} 4 | import org.scalatest.freespec.AnyFreeSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | import java.time._ 8 | import java.util.UUID 9 | 10 | class DiffxJvmInstancesTest extends AnyFreeSpec with Matchers { 11 | val cases = List( 12 | TestCase("LocalDate", LocalDate.now(), LocalDate.now().plusDays(1)), 13 | TestCase("LocalTime", LocalTime.now(), LocalTime.now().plusHours(1)), 14 | TestCase("LocalDateTime", LocalDateTime.now(), LocalDateTime.now().plusDays(1)), 15 | TestCase("OffsetDateTime", OffsetDateTime.now(), OffsetDateTime.now().plusDays(1)), 16 | TestCase("ZonedDateTime", ZonedDateTime.now(), ZonedDateTime.now().plusDays(1)), 17 | TestCase("Instant", Instant.now(), Instant.now().plusMillis(100)), 18 | TestCase("UUID", UUID.randomUUID(), UUID.randomUUID()) 19 | ) 20 | 21 | cases.foreach { tc => 22 | tc.clazz - { 23 | s"identical ${tc.clazz}s should be identical" in { 24 | tc.compareIdentical.isIdentical shouldBe true 25 | } 26 | 27 | s"different ${tc.clazz}s should be different" in { 28 | tc.compareDifferent.isIdentical shouldBe false 29 | } 30 | } 31 | } 32 | 33 | case class TestCase[T: Diff](clazz: String, v1: T, v2: T) { 34 | def compareIdentical: DiffResult = Diff.compare(v1, v1) 35 | def compareDifferent: DiffResult = Diff.compare(v1, v2) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /docs-sources/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | _build_html -------------------------------------------------------------------------------- /docs-sources/.python-version: -------------------------------------------------------------------------------- 1 | 3.7.2 2 | -------------------------------------------------------------------------------- /docs-sources/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs-sources/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1694529238, 9 | "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "flake-utils_2": { 22 | "locked": { 23 | "lastModified": 1642700792, 24 | "narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=", 25 | "owner": "numtide", 26 | "repo": "flake-utils", 27 | "rev": "846b2ae0fc4cc943637d3d1def4454213e203cba", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "numtide", 32 | "repo": "flake-utils", 33 | "type": "github" 34 | } 35 | }, 36 | "mach-nix": { 37 | "inputs": { 38 | "flake-utils": "flake-utils_2", 39 | "nixpkgs": "nixpkgs", 40 | "pypi-deps-db": "pypi-deps-db" 41 | }, 42 | "locked": { 43 | "lastModified": 1694857725, 44 | "narHash": "sha256-Ob4gMVo5uiSRhdDAD6k85jy5ys7dbc/KC4DPdSZm9Rc=", 45 | "owner": "davhau", 46 | "repo": "mach-nix", 47 | "rev": "0fb2c80ad2a74261315939849e1e8bf4278b7178", 48 | "type": "github" 49 | }, 50 | "original": { 51 | "owner": "davhau", 52 | "repo": "mach-nix", 53 | "type": "github" 54 | } 55 | }, 56 | "nixpkgs": { 57 | "locked": { 58 | "lastModified": 1643805626, 59 | "narHash": "sha256-AXLDVMG+UaAGsGSpOtQHPIKB+IZ0KSd9WS77aanGzgc=", 60 | "owner": "NixOS", 61 | "repo": "nixpkgs", 62 | "rev": "554d2d8aa25b6e583575459c297ec23750adb6cb", 63 | "type": "github" 64 | }, 65 | "original": { 66 | "id": "nixpkgs", 67 | "ref": "nixos-unstable", 68 | "type": "indirect" 69 | } 70 | }, 71 | "nixpkgs_2": { 72 | "locked": { 73 | "lastModified": 1695360818, 74 | "narHash": "sha256-JlkN3R/SSoMTa+CasbxS1gq+GpGxXQlNZRUh9+LIy/0=", 75 | "owner": "nixos", 76 | "repo": "nixpkgs", 77 | "rev": "e35dcc04a3853da485a396bdd332217d0ac9054f", 78 | "type": "github" 79 | }, 80 | "original": { 81 | "owner": "nixos", 82 | "ref": "nixos-unstable", 83 | "repo": "nixpkgs", 84 | "type": "github" 85 | } 86 | }, 87 | "pypi-deps-db": { 88 | "flake": false, 89 | "locked": { 90 | "lastModified": 1685526402, 91 | "narHash": "sha256-V0SXx0dWlUBL3E/wHWTszrkK2dOnuYYnBc7n6e0+NQU=", 92 | "owner": "DavHau", 93 | "repo": "pypi-deps-db", 94 | "rev": "ba35683c35218acb5258b69a9916994979dc73a9", 95 | "type": "github" 96 | }, 97 | "original": { 98 | "owner": "DavHau", 99 | "repo": "pypi-deps-db", 100 | "type": "github" 101 | } 102 | }, 103 | "root": { 104 | "inputs": { 105 | "flake-utils": "flake-utils", 106 | "mach-nix": "mach-nix", 107 | "nixpkgs": "nixpkgs_2" 108 | } 109 | }, 110 | "systems": { 111 | "locked": { 112 | "lastModified": 1681028828, 113 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 114 | "owner": "nix-systems", 115 | "repo": "default", 116 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 117 | "type": "github" 118 | }, 119 | "original": { 120 | "owner": "nix-systems", 121 | "repo": "default", 122 | "type": "github" 123 | } 124 | } 125 | }, 126 | "root": "root", 127 | "version": 7 128 | } 129 | -------------------------------------------------------------------------------- /docs-sources/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Python shell flake"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | 8 | mach-nix.url = "github:davhau/mach-nix"; 9 | }; 10 | 11 | outputs = { self, nixpkgs, mach-nix, flake-utils, ... }: 12 | let 13 | pythonVersion = "python37"; 14 | in 15 | flake-utils.lib.eachDefaultSystem (system: 16 | let 17 | pkgs = nixpkgs.legacyPackages.${system}; 18 | mach = mach-nix.lib.${system}; 19 | 20 | pythonEnv = mach.mkPython { 21 | python = pythonVersion; 22 | requirements = builtins.readFile ./requirements.txt; 23 | }; 24 | watchDocs = 25 | let 26 | name = "watchDocs"; 27 | src = pkgs.writeShellScript name '' 28 | sphinx-autobuild . _build/html 29 | ''; 30 | in 31 | pkgs.stdenv.mkDerivation 32 | { 33 | inherit name src; 34 | 35 | phases = [ "installPhase" "patchPhase" ]; 36 | 37 | installPhase = '' 38 | mkdir -p $out/bin 39 | cp $src $out/bin/${name} 40 | chmod +x $out/bin/${name} 41 | ''; 42 | }; 43 | in 44 | { 45 | devShells.default = pkgs.mkShellNoCC { 46 | packages = [ pythonEnv watchDocs ]; 47 | 48 | shellHook = '' 49 | export PYTHONPATH="${pythonEnv}/bin/python" 50 | ''; 51 | }; 52 | } 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /docs-sources/index.md: -------------------------------------------------------------------------------- 1 | # diffx: Pretty diffs for case classes 2 | 3 | Welcome! 4 | 5 | [diffx](https://github.com/softwaremill/diffx) is an open-source library which aims to display differences between 6 | complex structures in a way that they are easily noticeable. 7 | 8 | Here's a quick example of diffx in action: 9 | 10 | ```scala mdoc 11 | sealed trait Parent 12 | case class Bar(s: String, i: Int) extends Parent 13 | case class Foo(bar: Bar, b: List[Int], parent: Option[Parent]) extends Parent 14 | 15 | val right: Foo = Foo( 16 | Bar("asdf", 5), 17 | List(123, 1234), 18 | Some(Bar("asdf", 5)) 19 | ) 20 | 21 | val left: Foo = Foo( 22 | Bar("asdf", 66), 23 | List(1234), 24 | Some(right) 25 | ) 26 | 27 | import com.softwaremill.diffx.generic.auto._ 28 | import com.softwaremill.diffx._ 29 | compare(left, right) 30 | ``` 31 | 32 | Will result in: 33 | 34 | ![](https://github.com/softwaremill/diffx/blob/master/example.png?raw=true) 35 | 36 | `diffx` is available for Scala 3, 2.13 and 2.12 both jvm and js. 37 | 38 | The core of `diffx` comes in a single jar. 39 | 40 | To integrate with the test framework of your choice, you'll need to use one of the integration modules. 41 | See the section on [test-frameworks](test-frameworks/summary.md) for a brief overview of supported test frameworks. 42 | 43 | *Auto-derivation is used throughout the documentation for the sake of clarity. Head over to [derivation](usage/derivation.md) for more details* 44 | 45 | ## Tips and tricks 46 | 47 | You may need to add `-Wmacros:after` Scala compiler option to make sure to check for unused implicits 48 | after macro expansion. 49 | If you get warnings from Magnolia which looks like `magnolia: using fallback derivation for TYPE`, 50 | you can use the [Silencer](https://github.com/ghik/silencer) compiler plugin to silent the warning 51 | with the compiler option `"-P:silencer:globalFilters=^magnolia: using fallback derivation.*$"` 52 | 53 | ## Similar projects 54 | 55 | There is a number of similar projects from which diffx draws inspiration. 56 | 57 | Below is a list of some of them, which I am aware of, with their main differences: 58 | - [xotai/diff](https://github.com/xdotai/diff) - based on shapeless, seems not to be activly developed anymore 59 | - [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 60 | - [difflicious](https://github.com/jatcwang/difflicious) - very similar feature set, different design under the hood, no auto-derivation 61 | 62 | ## Sponsors 63 | 64 | Development and maintenance of diffx is sponsored by [SoftwareMill](https://softwaremill.com), 65 | 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. 66 | 67 | [![](https://files.softwaremill.com/logo/logo.png "SoftwareMill")](https://softwaremill.com) 68 | 69 | # Table of contents 70 | 71 | ```eval_rst 72 | .. toctree:: 73 | :maxdepth: 1 74 | :caption: Test frameworks 75 | 76 | test-frameworks/scalatest 77 | test-frameworks/specs2 78 | test-frameworks/utest 79 | test-frameworks/munit 80 | test-frameworks/weaver 81 | test-frameworks/summary 82 | 83 | .. toctree:: 84 | :maxdepth: 1 85 | :caption: Integrations 86 | 87 | integrations/cats 88 | integrations/tagging 89 | integrations/refined 90 | 91 | .. toctree:: 92 | :maxdepth: 1 93 | :caption: usage 94 | 95 | usage/derivation 96 | usage/ignoring 97 | usage/modifying 98 | usage/extending 99 | usage/sequences 100 | usage/output 101 | ``` 102 | 103 | ## Copyright 104 | 105 | Copyright (C) 2019 SoftwareMill [https://softwaremill.com](https://softwaremill.com). 106 | -------------------------------------------------------------------------------- /docs-sources/integrations/cats.md: -------------------------------------------------------------------------------- 1 | # cats 2 | 3 | This module contains integration layer between [org.typelevel.cats](https://github.com/typelevel/cats) and `diffx` 4 | 5 | ## sbt 6 | 7 | ```scala 8 | "com.softwaremill.diffx" %% "diffx-cats" % "@VERSION@" % Test 9 | ``` 10 | 11 | ## mill 12 | 13 | ```scala 14 | ivy"com.softwaremill.diffx::diffx-cats::@VERSION@" 15 | ``` 16 | 17 | ## Usage 18 | 19 | Assuming you have some data types from the cats library in your hierarchy: 20 | 21 | ```scala mdoc:silent 22 | import cats.data._ 23 | case class TestData(ints: NonEmptyList[String]) 24 | 25 | val t1 = TestData(NonEmptyList.one("a")) 26 | val t2 = TestData(NonEmptyList.one("b")) 27 | ``` 28 | 29 | all you need to do is to put additional diffx implicits into current scope: 30 | 31 | ```scala mdoc 32 | import com.softwaremill.diffx.compare 33 | import com.softwaremill.diffx.generic.auto._ 34 | 35 | import com.softwaremill.diffx.cats._ 36 | compare(t1, t2) 37 | ``` 38 | -------------------------------------------------------------------------------- /docs-sources/integrations/refined.md: -------------------------------------------------------------------------------- 1 | # refined 2 | 3 | This module contains integration layer between [eu.timepit.refined](https://github.com/fthomas/refined) and `diffx` 4 | 5 | ## sbt 6 | 7 | ```scala 8 | "com.softwaremill.diffx" %% "diffx-refined" % "@VERSION@" % Test 9 | ``` 10 | 11 | ## mill 12 | 13 | ```scala 14 | ivy"com.softwaremill.diffx::diffx-refined::@VERSION@" 15 | ``` 16 | 17 | ## Usage 18 | 19 | Assuming you have some refined types in your hierarchy: 20 | 21 | ```scala mdoc:silent 22 | import eu.timepit.refined.types.numeric.PosInt 23 | import eu.timepit.refined.auto._ 24 | import eu.timepit.refined.types.string.NonEmptyString 25 | 26 | case class TestData(posInt: PosInt, nonEmptyString: NonEmptyString) 27 | 28 | val t1 = TestData(1, "foo") 29 | val t2 = TestData(1, "bar") 30 | ``` 31 | 32 | all you need to do is to put additional diffx implicits into current scope: 33 | 34 | ```scala mdoc 35 | import com.softwaremill.diffx.compare 36 | import com.softwaremill.diffx.generic.auto._ 37 | 38 | import com.softwaremill.diffx.refined._ 39 | compare(t1, t2) 40 | ``` 41 | -------------------------------------------------------------------------------- /docs-sources/integrations/tagging.md: -------------------------------------------------------------------------------- 1 | # tagging 2 | 3 | This module contains integration layer between [com.softwaremill.common.tagging](https://github.com/softwaremill/scala-common) and `diffx` 4 | 5 | ## sbt 6 | 7 | ```scala 8 | "com.softwaremill.diffx" %% "diffx-tagging" % "@VERSION@" 9 | ``` 10 | 11 | ## mill 12 | 13 | ```scala 14 | ivy"com.softwaremill.diffx::diffx-tagging::@VERSION@" 15 | ``` 16 | 17 | ## Usage 18 | 19 | Assuming you have some tagged types in your hierarchy: 20 | 21 | ```scala mdoc:silent 22 | import com.softwaremill.tagging._ 23 | sealed trait T1 24 | sealed trait T2 25 | case class TestData(p1: Int @@ T1, p2: Int @@ T2) 26 | 27 | val t1 = TestData(1.taggedWith[T1], 1.taggedWith[T2]) 28 | val t2 = TestData(1.taggedWith[T1], 3.taggedWith[T2]) 29 | ``` 30 | 31 | all you need to do is to put additional diffx implicits into current scope: 32 | 33 | ```scala mdoc 34 | import com.softwaremill.diffx.compare 35 | import com.softwaremill.diffx.generic.auto._ 36 | 37 | import com.softwaremill.diffx.tagging._ 38 | compare(t1, t2) 39 | ``` 40 | -------------------------------------------------------------------------------- /docs-sources/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs-sources/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_rtd_theme==1.0.0 2 | recommonmark==0.7.1 3 | sphinx==4.2.0 4 | sphinx-autobuild==2021.3.14 5 | myst-parser==0.15.2 6 | -------------------------------------------------------------------------------- /docs-sources/test-frameworks/munit.md: -------------------------------------------------------------------------------- 1 | # munit 2 | 3 | To use with munit, add following dependency: 4 | 5 | ## sbt 6 | 7 | ```scala 8 | "com.softwaremill.diffx" %% "diffx-munit" % "@VERSION@" % Test 9 | ``` 10 | 11 | ## mill 12 | 13 | ```scala 14 | ivy"com.softwaremill.diffx::diffx-munit::@VERSION@" 15 | ``` 16 | 17 | ## Usage 18 | 19 | Then, mixin `DiffxAssertions` trait or add `import com.softwaremill.diffx.munit.DiffxAssertions._` to your test code. 20 | To assert using diffx use `assertEquals` as follows: 21 | 22 | ```scala mdoc:compile-only 23 | import com.softwaremill.diffx.munit.DiffxAssertions._ 24 | import com.softwaremill.diffx.generic.auto._ 25 | 26 | sealed trait Parent 27 | case class Bar(s: String, i: Int) extends Parent 28 | case class Foo(bar: Bar, b: List[Int], parent: Option[Parent]) extends Parent 29 | 30 | val right: Foo = Foo( 31 | Bar("asdf", 5), 32 | List(123, 1234), 33 | Some(Bar("asdf", 5)) 34 | ) 35 | 36 | val left: Foo = Foo( 37 | Bar("asdf", 66), 38 | List(1234), 39 | Some(right) 40 | ) 41 | 42 | assertEqual(left, right) 43 | ``` 44 | 45 | ## Versions matrix 46 | 47 | Below table shows past Diffx releases with the corresponding munit version they were build with. 48 | For newer versions checkout the release changelog. 49 | 50 | | Diffx | scalatest | 51 | | ----- | :-------: | 52 | | 0.7.0 | 0.7.29 | 53 | | 0.6.0 | 0.7.29 | 54 | | 0.5.6 | 0.7.28 | 55 | | 0.5.5 | 0.7.27 | 56 | | 0.5.4 | 0.7.27 | 57 | | 0.5.3 | 0.7.26 | 58 | | 0.5.2 | 0.7.26 | 59 | -------------------------------------------------------------------------------- /docs-sources/test-frameworks/scalatest.md: -------------------------------------------------------------------------------- 1 | # scalatest 2 | 3 | To use with scalatest, add the following dependency: 4 | 5 | ## sbt 6 | 7 | For use with `should` matchers: 8 | 9 | ```scala 10 | "com.softwaremill.diffx" %% "diffx-scalatest-should" % "@VERSION@" % Test 11 | ``` 12 | 13 | For use with `must` matchers: 14 | 15 | ```scala 16 | "com.softwaremill.diffx" %% "diffx-scalatest-must" % "@VERSION@" % Test 17 | ``` 18 | 19 | ## mill 20 | 21 | For use with `should` matchers: 22 | 23 | ```scala 24 | ivy"com.softwaremill.diffx::diffx-scalatest-must::@VERSION@" 25 | ``` 26 | 27 | For use with `must` matchers: 28 | 29 | ```scala 30 | ivy"com.softwaremill.diffx::diffx-scalatest-must::@VERSION@" 31 | ``` 32 | 33 | ## Usage 34 | 35 | Then, depending on the chosen matcher style extend or import relevant trait/object: 36 | 37 | - should -> `com.softwaremill.diffx.scalatest.DiffShouldMatcher` 38 | - must -> `com.softwaremill.diffx.scalatest.DiffMustMatcher` 39 | 40 | After that you will be able to use syntax such as: 41 | 42 | ```scala mdoc:compile-only 43 | import com.softwaremill.diffx.scalatest.DiffShouldMatcher._ 44 | import com.softwaremill.diffx.generic.auto._ 45 | 46 | sealed trait Parent 47 | case class Bar(s: String, i: Int) extends Parent 48 | case class Foo(bar: Bar, b: List[Int], parent: Option[Parent]) extends Parent 49 | 50 | val right: Foo = Foo( 51 | Bar("asdf", 5), 52 | List(123, 1234), 53 | Some(Bar("asdf", 5)) 54 | ) 55 | 56 | val left: Foo = Foo( 57 | Bar("asdf", 66), 58 | List(1234), 59 | Some(right) 60 | ) 61 | 62 | left shouldMatchTo(right) 63 | ``` 64 | 65 | ## Versions matrix 66 | 67 | Below table shows past Diffx releases with the corresponding scalatest version they were build with. 68 | For newer versions checkout the release changelog. 69 | 70 | | Diffx | scalatest | 71 | | ------ | :-------: | 72 | | 0.7.0 | 3.2.10 | 73 | | 0.6.0 | 3.2.10 | 74 | | 0.5.x | 3.2.9 | 75 | | 0.4.5 | 3.2.6 | 76 | | 0.4.4 | 3.2.4 | 77 | | 0.4.3 | 3.2.4 | 78 | | 0.4.2 | 3.2.4 | 79 | | 0.4.1 | 3.2.3 | 80 | | 0.4.0 | 3.2.3 | 81 | | 0.3.30 | 3.2.3 | 82 | | 0.3.29 | 3.1.2 | 83 | | 0.3.28 | 3.1.1 | 84 | -------------------------------------------------------------------------------- /docs-sources/test-frameworks/specs2.md: -------------------------------------------------------------------------------- 1 | # specs2 2 | 3 | To use with specs2, add the following dependency: 4 | 5 | ## sbt 6 | 7 | ```scala 8 | "com.softwaremill.diffx" %% "diffx-specs2" % "@VERSION@" % Test 9 | ``` 10 | 11 | ## mill 12 | 13 | ```scala 14 | ivy"com.softwaremill.diffx::diffx-specs2::@VERSION@" 15 | ``` 16 | 17 | ## Usage 18 | 19 | Then, extend the `com.softwaremill.diffx.specs2.DiffMatcher` trait or `import com.softwaremill.diffx.specs2.DiffMatcher._`. 20 | After that you will be able to use syntax such as: 21 | 22 | ```scala mdoc:compile-only 23 | import org.specs2.matcher.MustMatchers.{left => _, right => _, _} 24 | import com.softwaremill.diffx.specs2.DiffMatcher._ 25 | import com.softwaremill.diffx.generic.auto._ 26 | 27 | sealed trait Parent 28 | case class Bar(s: String, i: Int) extends Parent 29 | case class Foo(bar: Bar, b: List[Int], parent: Option[Parent]) extends Parent 30 | 31 | val right: Foo = Foo( 32 | Bar("asdf", 5), 33 | List(123, 1234), 34 | Some(Bar("asdf", 5)) 35 | ) 36 | 37 | val left: Foo = Foo( 38 | Bar("asdf", 66), 39 | List(1234), 40 | Some(right) 41 | ) 42 | 43 | left must matchTo(right) 44 | ``` 45 | 46 | ## Versions matrix 47 | 48 | Below table shows past Diffx releases with the corresponding specs2 version they were build with. 49 | For newer versions checkout the release changelog. 50 | 51 | | Diffx | scalatest | 52 | | ------ | :----------: | 53 | | 0.7.0 | 4.13.1 | 54 | | 0.6.0 | 4.13.0 | 55 | | 0.5.6 | 4.12.4-js-ec | 56 | | 0.5.5 | 4.12.4-js-ec | 57 | | 0.5.4 | 4.12.3 | 58 | | 0.5.3 | 4.12.1 | 59 | | 0.5.2 | 4.12.1 | 60 | | 0.5.1 | 4.12.1 | 61 | | 0.5.0 | 4.12.1 | 62 | | 0.4.5 | 4.10.6 | 63 | | 0.4.4 | 4.10.6 | 64 | | 0.4.3 | 4.10.6 | 65 | | 0.4.2 | 4.10.6 | 66 | | 0.4.1 | 4.10.6 | 67 | | 0.4.0 | 4.10.5 | 68 | | 0.3.30 | 4.10.5 | 69 | | 0.3.29 | 4.9.4 | 70 | | 0.3.28 | 4.9.3 | 71 | -------------------------------------------------------------------------------- /docs-sources/test-frameworks/summary.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Following test frameworks are supported by diffx: 4 | - [scalatest](scalatest.md) 5 | - [specs2](specs2.md) 6 | - [utest](utest.md) 7 | - [munit](munit.md) 8 | 9 | Didn't find your favourite testing library? Don't hesitate and let us know, or you can add it on your own , 10 | as all that needs to be done is to call `compare` function. -------------------------------------------------------------------------------- /docs-sources/test-frameworks/utest.md: -------------------------------------------------------------------------------- 1 | # utest 2 | 3 | To use with utest, add following dependency: 4 | 5 | ## sbt 6 | 7 | ```scala 8 | "com.softwaremill.diffx" %% "diffx-utest" % "@VERSION@" % Test 9 | ``` 10 | 11 | ## mill 12 | 13 | ```scala 14 | ivy"com.softwaremill.diffx::diffx-utest::@VERSION@" 15 | ``` 16 | 17 | ## Usage 18 | 19 | Then, mixin `DiffxAssertions` trait or add `import com.softwaremill.diffx.utest.DiffxAssertions._` to your test code. 20 | To assert using diffx use `assertEquals` as follows: 21 | 22 | ```scala mdoc:compile-only 23 | import com.softwaremill.diffx.utest.DiffxAssertions._ 24 | import com.softwaremill.diffx.generic.auto._ 25 | 26 | sealed trait Parent 27 | case class Bar(s: String, i: Int) extends Parent 28 | case class Foo(bar: Bar, b: List[Int], parent: Option[Parent]) extends Parent 29 | 30 | val right: Foo = Foo( 31 | Bar("asdf", 5), 32 | List(123, 1234), 33 | Some(Bar("asdf", 5)) 34 | ) 35 | 36 | val left: Foo = Foo( 37 | Bar("asdf", 66), 38 | List(1234), 39 | Some(right) 40 | ) 41 | 42 | assertEqual(left, right) 43 | ``` 44 | 45 | ## Versions matrix 46 | 47 | Below table shows past Diffx releases with the corresponding utest version they were build with. 48 | For newer versions checkout the release changelog. 49 | 50 | | Diffx | scalatest | 51 | | ------ | :-------: | 52 | | 0.7.0 | 0.7.10 | 53 | | 0.6.0 | 0.7.10 | 54 | | 0.5.x | 0.7.10 | 55 | | 0.4.5 | 0.7.7 | 56 | | 0.4.4 | 0.7.7 | 57 | | 0.4.3 | 0.7.7 | 58 | | 0.4.2 | 0.7.7 | 59 | | 0.4.1 | 0.7.7 | 60 | | 0.4.0 | 0.7.5 | 61 | | 0.3.30 | 0.7.5 | 62 | | 0.3.29 | 0.7.4 | 63 | | 0.3.28 | 0.7.4 | 64 | -------------------------------------------------------------------------------- /docs-sources/test-frameworks/weaver.md: -------------------------------------------------------------------------------- 1 | # weaver 2 | 3 | To use with weaver, add the following dependency: 4 | 5 | ## sbt 6 | 7 | ```scala 8 | "com.softwaremill.diffx" %% "diffx-weaver" % "@VERSION@" % Test 9 | ``` 10 | 11 | ## mill 12 | 13 | ```scala 14 | ivy"com.softwaremill.diffx::diffx-weaver::@VERSION@" 15 | ``` 16 | 17 | ## Usage 18 | 19 | Then, mixin `DiffxExpectations` trait or add `import com.softwaremill.diffx.weaver.DiffxExpectations._` to your test code. 20 | To assert using diffx use `expectEqual` as follows: 21 | 22 | ```scala mdoc:compile-only 23 | import com.softwaremill.diffx.weaver.DiffxExpectations._ 24 | import com.softwaremill.diffx.generic.auto._ 25 | 26 | sealed trait Parent 27 | case class Bar(s: String, i: Int) extends Parent 28 | case class Foo(bar: Bar, b: List[Int], parent: Option[Parent]) extends Parent 29 | 30 | val right: Foo = Foo( 31 | Bar("asdf", 5), 32 | List(123, 1234), 33 | Some(Bar("asdf", 5)) 34 | ) 35 | 36 | val left: Foo = Foo( 37 | Bar("asdf", 66), 38 | List(1234), 39 | Some(right) 40 | ) 41 | 42 | expectEqual(left, right) 43 | ``` 44 | 45 | ## Versions matrix 46 | 47 | Below table shows past Diffx releases with the corresponding weaver version they were build with. 48 | For newer versions checkout the release changelog. 49 | 50 | | Diffx | weaver | 51 | | ----- | :----: | 52 | | 0.9.0 | 0.8.3 | 53 | -------------------------------------------------------------------------------- /docs-sources/usage/derivation.md: -------------------------------------------------------------------------------- 1 | # derivation 2 | 3 | Diffx supports auto and semi-auto derivation. 4 | 5 | For semi-auto derivation you don't need any additional import, just define your instances using: 6 | ```scala mdoc:compile-only 7 | import com.softwaremill.diffx._ 8 | case class Product(name: String) 9 | case class Basket(products: List[Product]) 10 | 11 | implicit val productDiff = Diff.derived[Product] 12 | implicit val basketDiff = Diff.derived[Basket] 13 | ``` 14 | 15 | To use auto derivation add following import 16 | 17 | `import com.softwaremill.diffx.generic.auto._` 18 | 19 | or extend trait 20 | 21 | `com.softwaremill.diffx.generic.auto.AutoDerivation` 22 | 23 | **Auto derivation might have a huge impact on compilation times**, because of that it is recommended to use `semi-auto` derivation. 24 | 25 | 26 | Given that you have auto-derivation enabled you can summon diff instances as you would summon any other implicit type-class by using 27 | `implictly[Diff[T]]`. You can also write a shorter version `Diff[T]` which will be equivalent. 28 | However, if you would like to modify somehow (see [ignoring](./ignoring.md) and [modifying](./modifying.md)) given instance and 29 | put it back into to the implicit scope: 30 | ```scala 31 | implict val diffForMyClass: Diff[MyClass] = Diff[MyClass].doSomething 32 | ``` 33 | you will get a forward reference error. 34 | 35 | To overcome that issue there is a `Derived` wrapper which allows you to summon a wrapped instance. 36 | ```scala 37 | implict val diffForMyClass: Diff[MyClass] = implicitly[Derived[Diff[MyClass]]].value.doSomething 38 | ``` 39 | There is a `summon` method to make it more convenient. Below code is equivalent to the one above. 40 | ```scala 41 | implict val diffForMyClass: Diff[MyClass] = Diff.summon[MyClass].doSomething 42 | ``` -------------------------------------------------------------------------------- /docs-sources/usage/extending.md: -------------------------------------------------------------------------------- 1 | # extending 2 | 3 | If you'd like to implement custom matching logic for the given type, create an implicit `Diff` instance for that 4 | type, and make sure it's in scope when any `Diff` instances depending on that type are created. 5 | 6 | Consider following example with `NonEmptyList` from cats. `NonEmptyList` is implemented as case class, 7 | so the default behavior of diffx would be to create a `Diff[NonEmptyList]` typeclass instance using magnolia derivation. 8 | 9 | Obviously that's not what we usually want. In most of the cases we would like `NonEmptyList` to be compared as a list. 10 | Diffx already has an instance of a typeclass for a list (for any iterable to be precise). 11 | All we need to do is to use that typeclass by converting `NonEmptyList` to list which can be done using `contramap` method. 12 | 13 | The final code looks as follows: 14 | 15 | ```scala mdoc:compile-only 16 | import com.softwaremill.diffx._ 17 | import _root_.cats.data.NonEmptyList 18 | implicit def nelDiff[T: Diff]: Diff[NonEmptyList[T]] = 19 | Diff[List[T]].contramap[NonEmptyList[T]](_.toList) 20 | ``` 21 | 22 | *Note: There is a [diffx-cats](../integrations/cats.md) module, so you don't have to do this* -------------------------------------------------------------------------------- /docs-sources/usage/ignoring.md: -------------------------------------------------------------------------------- 1 | # ignoring 2 | 3 | ```scala mdoc:invisible 4 | import com.softwaremill.diffx.generic.auto._ 5 | import com.softwaremill.diffx._ 6 | ``` 7 | 8 | Fields can be excluded from comparison by calling the `ignore` method on the `Diff` instance. 9 | Since `Diff` instances are immutable, the `ignore` method creates a copy of the instance with modified logic. 10 | You can use this instance explicitly. 11 | 12 | ```scala mdoc:compile-only 13 | case class Person(name:String, age:Int) 14 | val modifiedDiff: Diff[Person] = Diff[Person].ignore(_.name) 15 | ``` 16 | 17 | If you still would like to use it implicitly, you first need to summon the instance of the `Diff` typeclass using 18 | the `Derived` typeclass wrapper: `Derived[Diff[Person]]`. Thanks to that trick, later you will be able to put your modified 19 | instance of the `Diff` typeclass into the implicit scope. The whole process looks as follows: 20 | 21 | ```scala mdoc:silent 22 | case class Person(name:String, age:Int) 23 | implicit val modifiedDiff: Diff[Person] = Diff.derived[Person].ignore(_.age) 24 | ``` 25 | ```scala mdoc 26 | compare(Person("bob", 25), Person("bob", 30)) 27 | ``` 28 | 29 | Starting from `diffx` 0.5.5 it is possible to globally customize how ignoring works. By default, an instance of 30 | `Diff` under a particular path gets replaced by `Diff.ignored` instance. `Diff.ignored` is configured to always produce 31 | identical results with fixed placeholder `""` no-matter what it gets. To customize that behavior one has to 32 | create an implicit instance of `DiffConfiguration` with desired behavior. Below is an example of how to include results of 33 | original comparison into ignored output: 34 | 35 | ```scala mdoc:nest 36 | implicit val conf: DiffConfiguration = DiffConfiguration(makeIgnored = 37 | (original: Diff[Any]) => 38 | (left: Any, right: Any, context: DiffContext) => { 39 | IdenticalValue(s"Ignored but was: ${original.apply(left, right, context).show()(ShowConfig.noColors)}") 40 | } 41 | ) 42 | val d = Diff[Person].ignore(_.age) 43 | d(Person("bob", 25), Person("bob", 30)) 44 | ``` -------------------------------------------------------------------------------- /docs-sources/usage/modifying.md: -------------------------------------------------------------------------------- 1 | # modifying 2 | 3 | Sometimes you might want to compare some nested values using a different comparator but 4 | the type they share is not unique within that hierarchy. 5 | 6 | Consider following example: 7 | ```scala mdoc 8 | import com.softwaremill.diffx._ 9 | import com.softwaremill.diffx.generic.auto._ 10 | 11 | case class Person(age: Int, weight: Int) 12 | ``` 13 | 14 | If we would like to compare `weight` differently than `age` we would have to introduce a new type for `weight` 15 | in order to provide a different `Diff` typeclass for only that field. While in general, it is a good idea to have your types 16 | very precise it might not always be practical or even possible. Fortunately, diffx comes with a mechanism which allows 17 | the replacement of nested diff instances. 18 | 19 | First we need to acquire a lens at given path using `modify` method, 20 | and then we can call `setTo` to replace a particular instance. 21 | 22 | ```scala mdoc:silent 23 | implicit val diffPerson: Diff[Person] = Diff.summon[Person].modify(_.weight) 24 | .setTo(Diff.approximate(epsilon=5)) 25 | ``` 26 | 27 | ```scala mdoc 28 | compare(Person(23, 60), Person(23, 62)) 29 | ``` 30 | 31 | In fact, replacement is so powerful that ignoring is implemented as a replacement 32 | with the `Diff.ignore` instance. 33 | 34 | 35 | ## collection support 36 | 37 | Specify how objects within particular collection within particular diff instance should be matched. 38 | We distinguish three main types of collections: 39 | - seqLike collections where elements are indexed collections 40 | - setLike collections where elements aren't indexed 41 | - mapLike collections where elements(values) are indexed by some keys 42 | 43 | Each collection should fall into one of above categories. 44 | Each category exposes different set of methods. 45 | 46 | ```scala mdoc:silent 47 | case class Organization(peopleList: List[Person], peopleSet: Set[Person], peopleMap: Map[Person, Person]) 48 | implicit val diffOrg: Diff[Organization] = Diff.summon[Organization] 49 | // seqLike methods: 50 | .modify(_.peopleList).matchByValue(_.age) 51 | .modify(_.peopleList).matchByIndex(index => index % 2) 52 | // setLike methods: 53 | .modify(_.peopleSet).matchBy(_.age) 54 | // mapLike methods: 55 | .modify(_.peopleMap).matchByValue(_.age) 56 | .modify(_.peopleMap).matchByKey(_.weight) 57 | ``` 58 | -------------------------------------------------------------------------------- /docs-sources/usage/output.md: -------------------------------------------------------------------------------- 1 | # output 2 | 3 | ```scala mdoc:invisible 4 | import com.softwaremill.diffx._ 5 | import com.softwaremill.diffx.generic.auto._ 6 | ``` 7 | 8 | `diffx` does its best to show the difference in the most readable way, but obviously the default configuration won't 9 | cover all the use-cases. Because of that, there are few ways how you can modify its output. 10 | 11 | ## intellij idea 12 | 13 | If you are running tests using **IntelliJ IDEA**'s test runner, you will want 14 | to turn off the red text coloring it uses for test failure outputs because 15 | it interferes with difflicious' color outputs. 16 | 17 | In File | Settings | Editor | Color Scheme | Console Colors | Console | Error Output, uncheck the red foreground color. 18 | (Solution provided by Jacob Wang @jatcwang) 19 | 20 | ## colors & signs 21 | 22 | Diffx refers to the values that are compared as `left` and `right`, but you can think of them as `actual` and `expected`. 23 | 24 | By default, the difference is shown in the following form: 25 | 26 | `leftColor(leftValue) -> rightColor(rightValue)` 27 | 28 | When comparing collection types the difference is calculated against the `right` value 29 | 30 | `additionalColor(additionalValue)` when there is an additional entity on the left-hand side 31 | `missingColor(missingValue)` when there is a missing entity on the left-hand side 32 | 33 | Colors can be customized providing an implicit instance of `ShowConfig` class. 34 | In fact `rightColor` and `leftColor` are functions `string => string` so they can be modified to do whatever you want with the output. 35 | 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. 36 | 37 | ````scala mdoc:compile-only 38 | val showConfigWithPlusMinus: ShowConfig = 39 | ShowConfig.default.copy(default = identity, arrow = identity, right = s => "+" + s, left = s => "-" + s) 40 | ```` 41 | 42 | There are two predefined set of colors - light and dark theme. 43 | The default theme is dark, and it can be changed using environment variable - `DIFFX_COLOR_THEME`(`light`/`dark`). 44 | 45 | ## skipping identical 46 | 47 | In some cases it might be desired to skip rendering identical fields. 48 | This can be achieved by using a specific DiffResult transformer. DiffResult transformer is a part of `ShowConfig`. 49 | By default, it is set to an identical function. 50 | 51 | ```scala mdoc 52 | implicit val showConfig = ShowConfig.default.copy(transformer = DiffResultTransformer.skipIdentical) 53 | case class Person(name:String, age:Int) 54 | 55 | val result = compare(Person("Bob", 23), Person("Alice", 23)) 56 | result.show() 57 | ``` 58 | 59 | There is a convenient method in `ShowConfig` called `skipIdentical` which does exactly that, so the relevant line from 60 | the example can be shortened to `ShowConfig.default.skipIdentical` -------------------------------------------------------------------------------- /docs-sources/usage/sequences.md: -------------------------------------------------------------------------------- 1 | # sequences 2 | 3 | `diffx` provides instances for many containers from scala's standard library (e.g. lists, sets, maps), however 4 | not all collections can be simply compared. Ordered collections like lists or vectors are compared by default by 5 | comparing elements under the same indexes. 6 | On the other hand maps, by default, are compared by comparing values under the respective keys. 7 | For unordered collections there is an `ObjectMapper` typeclass which defines how elements should be paired. 8 | 9 | ## object matcher 10 | 11 | In general, it is a very simple interface, with a bunch of factory methods. 12 | ```scala mdoc:compile-only 13 | trait ObjectMatcher[T] { 14 | def isSameObject(left: T, right: T): Boolean 15 | } 16 | ``` 17 | 18 | It is mostly useful when comparing unordered collections like sets: 19 | 20 | ```scala mdoc:silent 21 | import com.softwaremill.diffx._ 22 | import com.softwaremill.diffx.generic.auto._ 23 | 24 | case class Person(id: String, name: String) 25 | 26 | implicit val personMatcher = ObjectMatcher.set[Person].by(_.id) 27 | val bob = Person("1","Bob") 28 | ``` 29 | ```scala mdoc 30 | compare(Set(bob), Set(bob, Person("2","Alice"))) 31 | ``` 32 | 33 | It can be also used to modify how the entries from maps are paired. 34 | In below example we tell `diffx` to compare these maps by paring entries by values using the defined `personMatcher` 35 | ```scala mdoc:reset:silent 36 | import com.softwaremill.diffx._ 37 | import com.softwaremill.diffx.generic.auto._ 38 | 39 | case class Person(id: String, name: String) 40 | 41 | implicit val om = ObjectMatcher.map[String, Person].byValue(_.id) 42 | val bob = Person("1","Bob") 43 | ``` 44 | 45 | ```scala mdoc 46 | compare(Map("1" -> bob), Map("2" -> bob)) 47 | ``` 48 | 49 | Last but not least you can use `objectMatcher` to customize paring when comparing indexed collections. 50 | Such collections are treated similarly to maps (they use key-value object matcher), 51 | but the key type is bound to `Int` (`IterableEntry` is an alias for `MapEntry[Int,V]`). 52 | 53 | ```scala mdoc:reset:silent 54 | import com.softwaremill.diffx._ 55 | import com.softwaremill.diffx.generic.auto._ 56 | 57 | case class Person(id: String, name: String) 58 | 59 | implicit val personMatcher = ObjectMatcher.seq[Person].byValue(_.id) 60 | val bob = Person("1","Bob") 61 | val alice = Person("2","Alice") 62 | ``` 63 | ```scala mdoc 64 | compare(List(bob, alice), List(alice, bob)) 65 | ``` 66 | 67 | *Note: `ObjectMatcher` can be also passed explicitly, either upon creation or during modification* 68 | *See [modifying](modifying.md) for details.* -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwaremill/diffx/10f841727e4c151186d4e2fbdb45a36acb920f8c/example.png -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1680946745, 6 | "narHash": "sha256-KqGlwg9UTDsFBZZB8wzXgMnc8XQm95LtSbFvBsnqkPI=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "946da791763db1c306b86a8bd3828bf5814a1247", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "nixpkgs": { 19 | "locked": { 20 | "lastModified": 1680947395, 21 | "narHash": "sha256-2QxeCDNelyWzbJVgiMppqVbSSLPzg91XTyl427PfbSg=", 22 | "owner": "nixos", 23 | "repo": "nixpkgs", 24 | "rev": "a50962e4c4ee491a6c706a8432c3f36693c95662", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "nixos", 29 | "repo": "nixpkgs", 30 | "type": "github" 31 | } 32 | }, 33 | "root": { 34 | "inputs": { 35 | "flake-utils": "flake-utils", 36 | "nixpkgs": "nixpkgs" 37 | } 38 | } 39 | }, 40 | "root": "root", 41 | "version": 7 42 | } 43 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs.nixpkgs.url = "github:nixos/nixpkgs"; 3 | inputs.flake-utils.url = "github:numtide/flake-utils"; 4 | 5 | outputs = { self, nixpkgs, flake-utils, ... }@inputs: 6 | flake-utils.lib.eachDefaultSystem (system: 7 | let 8 | pkgs = import nixpkgs { inherit system; }; 9 | in 10 | { 11 | devShell = pkgs.mkShell { 12 | buildInputs = with pkgs; [ 13 | (sbt.override { 14 | jre = temurin-jre-bin-17; 15 | }) 16 | nodejs 17 | yarn 18 | ]; 19 | welcomeMessage = '' 20 | Welcome to the Diffx Nix shell! 👋 21 | ''; 22 | 23 | shellHook = '' 24 | echo "$welcomeMessage" 25 | ''; 26 | }; 27 | } 28 | ); 29 | } 30 | 31 | -------------------------------------------------------------------------------- /generated-docs/out/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | _build_html -------------------------------------------------------------------------------- /generated-docs/out/.python-version: -------------------------------------------------------------------------------- 1 | 3.7.2 2 | -------------------------------------------------------------------------------- /generated-docs/out/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /generated-docs/out/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1694529238, 9 | "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "flake-utils_2": { 22 | "locked": { 23 | "lastModified": 1642700792, 24 | "narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=", 25 | "owner": "numtide", 26 | "repo": "flake-utils", 27 | "rev": "846b2ae0fc4cc943637d3d1def4454213e203cba", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "numtide", 32 | "repo": "flake-utils", 33 | "type": "github" 34 | } 35 | }, 36 | "mach-nix": { 37 | "inputs": { 38 | "flake-utils": "flake-utils_2", 39 | "nixpkgs": "nixpkgs", 40 | "pypi-deps-db": "pypi-deps-db" 41 | }, 42 | "locked": { 43 | "lastModified": 1694857725, 44 | "narHash": "sha256-Ob4gMVo5uiSRhdDAD6k85jy5ys7dbc/KC4DPdSZm9Rc=", 45 | "owner": "davhau", 46 | "repo": "mach-nix", 47 | "rev": "0fb2c80ad2a74261315939849e1e8bf4278b7178", 48 | "type": "github" 49 | }, 50 | "original": { 51 | "owner": "davhau", 52 | "repo": "mach-nix", 53 | "type": "github" 54 | } 55 | }, 56 | "nixpkgs": { 57 | "locked": { 58 | "lastModified": 1643805626, 59 | "narHash": "sha256-AXLDVMG+UaAGsGSpOtQHPIKB+IZ0KSd9WS77aanGzgc=", 60 | "owner": "NixOS", 61 | "repo": "nixpkgs", 62 | "rev": "554d2d8aa25b6e583575459c297ec23750adb6cb", 63 | "type": "github" 64 | }, 65 | "original": { 66 | "id": "nixpkgs", 67 | "ref": "nixos-unstable", 68 | "type": "indirect" 69 | } 70 | }, 71 | "nixpkgs_2": { 72 | "locked": { 73 | "lastModified": 1695360818, 74 | "narHash": "sha256-JlkN3R/SSoMTa+CasbxS1gq+GpGxXQlNZRUh9+LIy/0=", 75 | "owner": "nixos", 76 | "repo": "nixpkgs", 77 | "rev": "e35dcc04a3853da485a396bdd332217d0ac9054f", 78 | "type": "github" 79 | }, 80 | "original": { 81 | "owner": "nixos", 82 | "ref": "nixos-unstable", 83 | "repo": "nixpkgs", 84 | "type": "github" 85 | } 86 | }, 87 | "pypi-deps-db": { 88 | "flake": false, 89 | "locked": { 90 | "lastModified": 1685526402, 91 | "narHash": "sha256-V0SXx0dWlUBL3E/wHWTszrkK2dOnuYYnBc7n6e0+NQU=", 92 | "owner": "DavHau", 93 | "repo": "pypi-deps-db", 94 | "rev": "ba35683c35218acb5258b69a9916994979dc73a9", 95 | "type": "github" 96 | }, 97 | "original": { 98 | "owner": "DavHau", 99 | "repo": "pypi-deps-db", 100 | "type": "github" 101 | } 102 | }, 103 | "root": { 104 | "inputs": { 105 | "flake-utils": "flake-utils", 106 | "mach-nix": "mach-nix", 107 | "nixpkgs": "nixpkgs_2" 108 | } 109 | }, 110 | "systems": { 111 | "locked": { 112 | "lastModified": 1681028828, 113 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 114 | "owner": "nix-systems", 115 | "repo": "default", 116 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 117 | "type": "github" 118 | }, 119 | "original": { 120 | "owner": "nix-systems", 121 | "repo": "default", 122 | "type": "github" 123 | } 124 | } 125 | }, 126 | "root": "root", 127 | "version": 7 128 | } 129 | -------------------------------------------------------------------------------- /generated-docs/out/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Python shell flake"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | 8 | mach-nix.url = "github:davhau/mach-nix"; 9 | }; 10 | 11 | outputs = { self, nixpkgs, mach-nix, flake-utils, ... }: 12 | let 13 | pythonVersion = "python37"; 14 | in 15 | flake-utils.lib.eachDefaultSystem (system: 16 | let 17 | pkgs = nixpkgs.legacyPackages.${system}; 18 | mach = mach-nix.lib.${system}; 19 | 20 | pythonEnv = mach.mkPython { 21 | python = pythonVersion; 22 | requirements = builtins.readFile ./requirements.txt; 23 | }; 24 | watchDocs = 25 | let 26 | name = "watchDocs"; 27 | src = pkgs.writeShellScript name '' 28 | sphinx-autobuild . _build/html 29 | ''; 30 | in 31 | pkgs.stdenv.mkDerivation 32 | { 33 | inherit name src; 34 | 35 | phases = [ "installPhase" "patchPhase" ]; 36 | 37 | installPhase = '' 38 | mkdir -p $out/bin 39 | cp $src $out/bin/${name} 40 | chmod +x $out/bin/${name} 41 | ''; 42 | }; 43 | in 44 | { 45 | devShells.default = pkgs.mkShellNoCC { 46 | packages = [ pythonEnv watchDocs ]; 47 | 48 | shellHook = '' 49 | export PYTHONPATH="${pythonEnv}/bin/python" 50 | ''; 51 | }; 52 | } 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /generated-docs/out/index.md: -------------------------------------------------------------------------------- 1 | # diffx: Pretty diffs for case classes 2 | 3 | Welcome! 4 | 5 | [diffx](https://github.com/softwaremill/diffx) is an open-source library which aims to display differences between 6 | complex structures in a way that they are easily noticeable. 7 | 8 | Here's a quick example of diffx in action: 9 | 10 | ```scala 11 | sealed trait Parent 12 | case class Bar(s: String, i: Int) extends Parent 13 | case class Foo(bar: Bar, b: List[Int], parent: Option[Parent]) extends Parent 14 | 15 | val right: Foo = Foo( 16 | Bar("asdf", 5), 17 | List(123, 1234), 18 | Some(Bar("asdf", 5)) 19 | ) 20 | // right: Foo = Foo( 21 | // bar = Bar(s = "asdf", i = 5), 22 | // b = List(123, 1234), 23 | // parent = Some(value = Bar(s = "asdf", i = 5)) 24 | // ) 25 | 26 | val left: Foo = Foo( 27 | Bar("asdf", 66), 28 | List(1234), 29 | Some(right) 30 | ) 31 | // left: Foo = Foo( 32 | // bar = Bar(s = "asdf", i = 66), 33 | // b = List(1234), 34 | // parent = Some( 35 | // value = Foo( 36 | // bar = Bar(s = "asdf", i = 5), 37 | // b = List(123, 1234), 38 | // parent = Some(value = Bar(s = "asdf", i = 5)) 39 | // ) 40 | // ) 41 | // ) 42 | 43 | import com.softwaremill.diffx.generic.auto._ 44 | import com.softwaremill.diffx._ 45 | compare(left, right) 46 | // res0: DiffResult = DiffResultObject( 47 | // name = "Foo", 48 | // fields = ListMap( 49 | // "bar" -> DiffResultObject( 50 | // name = "Bar", 51 | // fields = ListMap( 52 | // "s" -> IdenticalValue(value = "asdf"), 53 | // "i" -> DiffResultValue(left = 66, right = 5) 54 | // ) 55 | // ), 56 | // "b" -> DiffResultObject( 57 | // name = "List", 58 | // fields = ListMap( 59 | // "0" -> DiffResultValue(left = 1234, right = 123), 60 | // "1" -> DiffResultMissing(value = 1234) 61 | // ) 62 | // ), 63 | // "parent" -> DiffResultValue( 64 | // left = "repl.MdocSession.MdocApp.Foo", 65 | // right = "repl.MdocSession.MdocApp.Bar" 66 | // ) 67 | // ) 68 | // ) 69 | ``` 70 | 71 | Will result in: 72 | 73 | ![](https://github.com/softwaremill/diffx/blob/master/example.png?raw=true) 74 | 75 | `diffx` is available for Scala 3, 2.13 and 2.12 both jvm and js. 76 | 77 | The core of `diffx` comes in a single jar. 78 | 79 | To integrate with the test framework of your choice, you'll need to use one of the integration modules. 80 | See the section on [test-frameworks](test-frameworks/summary.md) for a brief overview of supported test frameworks. 81 | 82 | *Auto-derivation is used throughout the documentation for the sake of clarity. Head over to [derivation](usage/derivation.md) for more details* 83 | 84 | ## Tips and tricks 85 | 86 | You may need to add `-Wmacros:after` Scala compiler option to make sure to check for unused implicits 87 | after macro expansion. 88 | If you get warnings from Magnolia which looks like `magnolia: using fallback derivation for TYPE`, 89 | you can use the [Silencer](https://github.com/ghik/silencer) compiler plugin to silent the warning 90 | with the compiler option `"-P:silencer:globalFilters=^magnolia: using fallback derivation.*$"` 91 | 92 | ## Similar projects 93 | 94 | There is a number of similar projects from which diffx draws inspiration. 95 | 96 | Below is a list of some of them, which I am aware of, with their main differences: 97 | - [xotai/diff](https://github.com/xdotai/diff) - based on shapeless, seems not to be activly developed anymore 98 | - [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 99 | - [difflicious](https://github.com/jatcwang/difflicious) - very similar feature set, different design under the hood, no auto-derivation 100 | 101 | ## Sponsors 102 | 103 | Development and maintenance of diffx is sponsored by [SoftwareMill](https://softwaremill.com), 104 | 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. 105 | 106 | [![](https://files.softwaremill.com/logo/logo.png "SoftwareMill")](https://softwaremill.com) 107 | 108 | # Table of contents 109 | 110 | ```eval_rst 111 | .. toctree:: 112 | :maxdepth: 1 113 | :caption: Test frameworks 114 | 115 | test-frameworks/scalatest 116 | test-frameworks/specs2 117 | test-frameworks/utest 118 | test-frameworks/munit 119 | test-frameworks/weaver 120 | test-frameworks/summary 121 | 122 | .. toctree:: 123 | :maxdepth: 1 124 | :caption: Integrations 125 | 126 | integrations/cats 127 | integrations/tagging 128 | integrations/refined 129 | 130 | .. toctree:: 131 | :maxdepth: 1 132 | :caption: usage 133 | 134 | usage/derivation 135 | usage/ignoring 136 | usage/modifying 137 | usage/extending 138 | usage/sequences 139 | usage/output 140 | ``` 141 | 142 | ## Copyright 143 | 144 | Copyright (C) 2019 SoftwareMill [https://softwaremill.com](https://softwaremill.com). 145 | -------------------------------------------------------------------------------- /generated-docs/out/integrations/cats.md: -------------------------------------------------------------------------------- 1 | # cats 2 | 3 | This module contains integration layer between [org.typelevel.cats](https://github.com/typelevel/cats) and `diffx` 4 | 5 | ## sbt 6 | 7 | ```scala 8 | "com.softwaremill.diffx" %% "diffx-cats" % "0.9.0" % Test 9 | ``` 10 | 11 | ## mill 12 | 13 | ```scala 14 | ivy"com.softwaremill.diffx::diffx-cats::0.9.0" 15 | ``` 16 | 17 | ## Usage 18 | 19 | Assuming you have some data types from the cats library in your hierarchy: 20 | 21 | ```scala 22 | import cats.data._ 23 | case class TestData(ints: NonEmptyList[String]) 24 | 25 | val t1 = TestData(NonEmptyList.one("a")) 26 | val t2 = TestData(NonEmptyList.one("b")) 27 | ``` 28 | 29 | all you need to do is to put additional diffx implicits into current scope: 30 | 31 | ```scala 32 | import com.softwaremill.diffx.compare 33 | import com.softwaremill.diffx.generic.auto._ 34 | 35 | import com.softwaremill.diffx.cats._ 36 | compare(t1, t2) 37 | // res0: com.softwaremill.diffx.DiffResult = DiffResultObject( 38 | // name = "TestData", 39 | // fields = ListMap( 40 | // "ints" -> DiffResultObject( 41 | // name = "NonEmptyList", 42 | // fields = ListMap( 43 | // "0" -> DiffResultString( 44 | // diffs = List( 45 | // DiffResultStringLine( 46 | // diffs = List(DiffResultValue(left = "a", right = "b")) 47 | // ) 48 | // ) 49 | // ) 50 | // ) 51 | // ) 52 | // ) 53 | // ) 54 | ``` 55 | -------------------------------------------------------------------------------- /generated-docs/out/integrations/refined.md: -------------------------------------------------------------------------------- 1 | # refined 2 | 3 | This module contains integration layer between [eu.timepit.refined](https://github.com/fthomas/refined) and `diffx` 4 | 5 | ## sbt 6 | 7 | ```scala 8 | "com.softwaremill.diffx" %% "diffx-refined" % "0.9.0" % Test 9 | ``` 10 | 11 | ## mill 12 | 13 | ```scala 14 | ivy"com.softwaremill.diffx::diffx-refined::0.9.0" 15 | ``` 16 | 17 | ## Usage 18 | 19 | Assuming you have some refined types in your hierarchy: 20 | 21 | ```scala 22 | import eu.timepit.refined.types.numeric.PosInt 23 | import eu.timepit.refined.auto._ 24 | import eu.timepit.refined.types.string.NonEmptyString 25 | 26 | case class TestData(posInt: PosInt, nonEmptyString: NonEmptyString) 27 | 28 | val t1 = TestData(1, "foo") 29 | val t2 = TestData(1, "bar") 30 | ``` 31 | 32 | all you need to do is to put additional diffx implicits into current scope: 33 | 34 | ```scala 35 | import com.softwaremill.diffx.compare 36 | import com.softwaremill.diffx.generic.auto._ 37 | 38 | import com.softwaremill.diffx.refined._ 39 | compare(t1, t2) 40 | // res0: com.softwaremill.diffx.DiffResult = DiffResultObject( 41 | // name = "TestData", 42 | // fields = ListMap( 43 | // "posInt" -> IdenticalValue(value = 1), 44 | // "nonEmptyString" -> DiffResultString( 45 | // diffs = List( 46 | // DiffResultStringLine( 47 | // diffs = List(DiffResultValue(left = "foo", right = "bar")) 48 | // ) 49 | // ) 50 | // ) 51 | // ) 52 | // ) 53 | ``` 54 | -------------------------------------------------------------------------------- /generated-docs/out/integrations/tagging.md: -------------------------------------------------------------------------------- 1 | # tagging 2 | 3 | This module contains integration layer between [com.softwaremill.common.tagging](https://github.com/softwaremill/scala-common) and `diffx` 4 | 5 | ## sbt 6 | 7 | ```scala 8 | "com.softwaremill.diffx" %% "diffx-tagging" % "0.9.0" 9 | ``` 10 | 11 | ## mill 12 | 13 | ```scala 14 | ivy"com.softwaremill.diffx::diffx-tagging::0.9.0" 15 | ``` 16 | 17 | ## Usage 18 | 19 | Assuming you have some tagged types in your hierarchy: 20 | 21 | ```scala 22 | import com.softwaremill.tagging._ 23 | sealed trait T1 24 | sealed trait T2 25 | case class TestData(p1: Int @@ T1, p2: Int @@ T2) 26 | 27 | val t1 = TestData(1.taggedWith[T1], 1.taggedWith[T2]) 28 | val t2 = TestData(1.taggedWith[T1], 3.taggedWith[T2]) 29 | ``` 30 | 31 | all you need to do is to put additional diffx implicits into current scope: 32 | 33 | ```scala 34 | import com.softwaremill.diffx.compare 35 | import com.softwaremill.diffx.generic.auto._ 36 | 37 | import com.softwaremill.diffx.tagging._ 38 | compare(t1, t2) 39 | // res0: com.softwaremill.diffx.DiffResult = DiffResultObject( 40 | // name = "TestData", 41 | // fields = ListMap( 42 | // "p1" -> IdenticalValue(value = 1), 43 | // "p2" -> DiffResultValue(left = 1, right = 3) 44 | // ) 45 | // ) 46 | ``` 47 | -------------------------------------------------------------------------------- /generated-docs/out/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /generated-docs/out/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_rtd_theme==1.0.0 2 | recommonmark==0.7.1 3 | sphinx==4.2.0 4 | sphinx-autobuild==2021.3.14 5 | myst-parser==0.15.2 6 | -------------------------------------------------------------------------------- /generated-docs/out/test-frameworks/munit.md: -------------------------------------------------------------------------------- 1 | # munit 2 | 3 | To use with munit, add following dependency: 4 | 5 | ## sbt 6 | 7 | ```scala 8 | "com.softwaremill.diffx" %% "diffx-munit" % "0.9.0" % Test 9 | ``` 10 | 11 | ## mill 12 | 13 | ```scala 14 | ivy"com.softwaremill.diffx::diffx-munit::0.9.0" 15 | ``` 16 | 17 | ## Usage 18 | 19 | Then, mixin `DiffxAssertions` trait or add `import com.softwaremill.diffx.munit.DiffxAssertions._` to your test code. 20 | To assert using diffx use `assertEquals` as follows: 21 | 22 | ```scala 23 | import com.softwaremill.diffx.munit.DiffxAssertions._ 24 | import com.softwaremill.diffx.generic.auto._ 25 | 26 | sealed trait Parent 27 | case class Bar(s: String, i: Int) extends Parent 28 | case class Foo(bar: Bar, b: List[Int], parent: Option[Parent]) extends Parent 29 | 30 | val right: Foo = Foo( 31 | Bar("asdf", 5), 32 | List(123, 1234), 33 | Some(Bar("asdf", 5)) 34 | ) 35 | 36 | val left: Foo = Foo( 37 | Bar("asdf", 66), 38 | List(1234), 39 | Some(right) 40 | ) 41 | 42 | assertEqual(left, right) 43 | ``` 44 | 45 | ## Versions matrix 46 | 47 | Below table shows past Diffx releases with the corresponding munit version they were build with. 48 | For newer versions checkout the release changelog. 49 | 50 | | Diffx | scalatest | 51 | | ----- | :-------: | 52 | | 0.7.0 | 0.7.29 | 53 | | 0.6.0 | 0.7.29 | 54 | | 0.5.6 | 0.7.28 | 55 | | 0.5.5 | 0.7.27 | 56 | | 0.5.4 | 0.7.27 | 57 | | 0.5.3 | 0.7.26 | 58 | | 0.5.2 | 0.7.26 | 59 | -------------------------------------------------------------------------------- /generated-docs/out/test-frameworks/scalatest.md: -------------------------------------------------------------------------------- 1 | # scalatest 2 | 3 | To use with scalatest, add the following dependency: 4 | 5 | ## sbt 6 | 7 | For use with `should` matchers: 8 | 9 | ```scala 10 | "com.softwaremill.diffx" %% "diffx-scalatest-should" % "0.9.0" % Test 11 | ``` 12 | 13 | For use with `must` matchers: 14 | 15 | ```scala 16 | "com.softwaremill.diffx" %% "diffx-scalatest-must" % "0.9.0" % Test 17 | ``` 18 | 19 | ## mill 20 | 21 | For use with `should` matchers: 22 | 23 | ```scala 24 | ivy"com.softwaremill.diffx::diffx-scalatest-must::0.9.0" 25 | ``` 26 | 27 | For use with `must` matchers: 28 | 29 | ```scala 30 | ivy"com.softwaremill.diffx::diffx-scalatest-must::0.9.0" 31 | ``` 32 | 33 | ## Usage 34 | 35 | Then, depending on the chosen matcher style extend or import relevant trait/object: 36 | 37 | - should -> `com.softwaremill.diffx.scalatest.DiffShouldMatcher` 38 | - must -> `com.softwaremill.diffx.scalatest.DiffMustMatcher` 39 | 40 | After that you will be able to use syntax such as: 41 | 42 | ```scala 43 | import com.softwaremill.diffx.scalatest.DiffShouldMatcher._ 44 | import com.softwaremill.diffx.generic.auto._ 45 | 46 | sealed trait Parent 47 | case class Bar(s: String, i: Int) extends Parent 48 | case class Foo(bar: Bar, b: List[Int], parent: Option[Parent]) extends Parent 49 | 50 | val right: Foo = Foo( 51 | Bar("asdf", 5), 52 | List(123, 1234), 53 | Some(Bar("asdf", 5)) 54 | ) 55 | 56 | val left: Foo = Foo( 57 | Bar("asdf", 66), 58 | List(1234), 59 | Some(right) 60 | ) 61 | 62 | left shouldMatchTo(right) 63 | ``` 64 | 65 | ## Versions matrix 66 | 67 | Below table shows past Diffx releases with the corresponding scalatest version they were build with. 68 | For newer versions checkout the release changelog. 69 | 70 | | Diffx | scalatest | 71 | | ------ | :-------: | 72 | | 0.7.0 | 3.2.10 | 73 | | 0.6.0 | 3.2.10 | 74 | | 0.5.x | 3.2.9 | 75 | | 0.4.5 | 3.2.6 | 76 | | 0.4.4 | 3.2.4 | 77 | | 0.4.3 | 3.2.4 | 78 | | 0.4.2 | 3.2.4 | 79 | | 0.4.1 | 3.2.3 | 80 | | 0.4.0 | 3.2.3 | 81 | | 0.3.30 | 3.2.3 | 82 | | 0.3.29 | 3.1.2 | 83 | | 0.3.28 | 3.1.1 | 84 | -------------------------------------------------------------------------------- /generated-docs/out/test-frameworks/specs2.md: -------------------------------------------------------------------------------- 1 | # specs2 2 | 3 | To use with specs2, add the following dependency: 4 | 5 | ## sbt 6 | 7 | ```scala 8 | "com.softwaremill.diffx" %% "diffx-specs2" % "0.9.0" % Test 9 | ``` 10 | 11 | ## mill 12 | 13 | ```scala 14 | ivy"com.softwaremill.diffx::diffx-specs2::0.9.0" 15 | ``` 16 | 17 | ## Usage 18 | 19 | Then, extend the `com.softwaremill.diffx.specs2.DiffMatcher` trait or `import com.softwaremill.diffx.specs2.DiffMatcher._`. 20 | After that you will be able to use syntax such as: 21 | 22 | ```scala 23 | import org.specs2.matcher.MustMatchers.{left => _, right => _, _} 24 | import com.softwaremill.diffx.specs2.DiffMatcher._ 25 | import com.softwaremill.diffx.generic.auto._ 26 | 27 | sealed trait Parent 28 | case class Bar(s: String, i: Int) extends Parent 29 | case class Foo(bar: Bar, b: List[Int], parent: Option[Parent]) extends Parent 30 | 31 | val right: Foo = Foo( 32 | Bar("asdf", 5), 33 | List(123, 1234), 34 | Some(Bar("asdf", 5)) 35 | ) 36 | 37 | val left: Foo = Foo( 38 | Bar("asdf", 66), 39 | List(1234), 40 | Some(right) 41 | ) 42 | 43 | left must matchTo(right) 44 | ``` 45 | 46 | ## Versions matrix 47 | 48 | Below table shows past Diffx releases with the corresponding specs2 version they were build with. 49 | For newer versions checkout the release changelog. 50 | 51 | | Diffx | scalatest | 52 | | ------ | :----------: | 53 | | 0.7.0 | 4.13.1 | 54 | | 0.6.0 | 4.13.0 | 55 | | 0.5.6 | 4.12.4-js-ec | 56 | | 0.5.5 | 4.12.4-js-ec | 57 | | 0.5.4 | 4.12.3 | 58 | | 0.5.3 | 4.12.1 | 59 | | 0.5.2 | 4.12.1 | 60 | | 0.5.1 | 4.12.1 | 61 | | 0.5.0 | 4.12.1 | 62 | | 0.4.5 | 4.10.6 | 63 | | 0.4.4 | 4.10.6 | 64 | | 0.4.3 | 4.10.6 | 65 | | 0.4.2 | 4.10.6 | 66 | | 0.4.1 | 4.10.6 | 67 | | 0.4.0 | 4.10.5 | 68 | | 0.3.30 | 4.10.5 | 69 | | 0.3.29 | 4.9.4 | 70 | | 0.3.28 | 4.9.3 | 71 | -------------------------------------------------------------------------------- /generated-docs/out/test-frameworks/summary.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Following test frameworks are supported by diffx: 4 | - [scalatest](scalatest.md) 5 | - [specs2](specs2.md) 6 | - [utest](utest.md) 7 | - [munit](munit.md) 8 | 9 | Didn't find your favourite testing library? Don't hesitate and let us know, or you can add it on your own , 10 | as all that needs to be done is to call `compare` function. -------------------------------------------------------------------------------- /generated-docs/out/test-frameworks/utest.md: -------------------------------------------------------------------------------- 1 | # utest 2 | 3 | To use with utest, add following dependency: 4 | 5 | ## sbt 6 | 7 | ```scala 8 | "com.softwaremill.diffx" %% "diffx-utest" % "0.9.0" % Test 9 | ``` 10 | 11 | ## mill 12 | 13 | ```scala 14 | ivy"com.softwaremill.diffx::diffx-utest::0.9.0" 15 | ``` 16 | 17 | ## Usage 18 | 19 | Then, mixin `DiffxAssertions` trait or add `import com.softwaremill.diffx.utest.DiffxAssertions._` to your test code. 20 | To assert using diffx use `assertEquals` as follows: 21 | 22 | ```scala 23 | import com.softwaremill.diffx.utest.DiffxAssertions._ 24 | import com.softwaremill.diffx.generic.auto._ 25 | 26 | sealed trait Parent 27 | case class Bar(s: String, i: Int) extends Parent 28 | case class Foo(bar: Bar, b: List[Int], parent: Option[Parent]) extends Parent 29 | 30 | val right: Foo = Foo( 31 | Bar("asdf", 5), 32 | List(123, 1234), 33 | Some(Bar("asdf", 5)) 34 | ) 35 | 36 | val left: Foo = Foo( 37 | Bar("asdf", 66), 38 | List(1234), 39 | Some(right) 40 | ) 41 | 42 | assertEqual(left, right) 43 | ``` 44 | 45 | ## Versions matrix 46 | 47 | Below table shows past Diffx releases with the corresponding utest version they were build with. 48 | For newer versions checkout the release changelog. 49 | 50 | | Diffx | scalatest | 51 | | ------ | :-------: | 52 | | 0.7.0 | 0.7.10 | 53 | | 0.6.0 | 0.7.10 | 54 | | 0.5.x | 0.7.10 | 55 | | 0.4.5 | 0.7.7 | 56 | | 0.4.4 | 0.7.7 | 57 | | 0.4.3 | 0.7.7 | 58 | | 0.4.2 | 0.7.7 | 59 | | 0.4.1 | 0.7.7 | 60 | | 0.4.0 | 0.7.5 | 61 | | 0.3.30 | 0.7.5 | 62 | | 0.3.29 | 0.7.4 | 63 | | 0.3.28 | 0.7.4 | 64 | -------------------------------------------------------------------------------- /generated-docs/out/test-frameworks/weaver.md: -------------------------------------------------------------------------------- 1 | # weaver 2 | 3 | To use with weaver, add the following dependency: 4 | 5 | ## sbt 6 | 7 | ```scala 8 | "com.softwaremill.diffx" %% "diffx-weaver" % "0.9.0" % Test 9 | ``` 10 | 11 | ## mill 12 | 13 | ```scala 14 | ivy"com.softwaremill.diffx::diffx-weaver::0.9.0" 15 | ``` 16 | 17 | ## Usage 18 | 19 | Then, mixin `DiffxExpectations` trait or add `import com.softwaremill.diffx.weaver.DiffxExpectations._` to your test code. 20 | To assert using diffx use `expectEqual` as follows: 21 | 22 | ```scala 23 | import com.softwaremill.diffx.weaver.DiffxExpectations._ 24 | import com.softwaremill.diffx.generic.auto._ 25 | 26 | sealed trait Parent 27 | case class Bar(s: String, i: Int) extends Parent 28 | case class Foo(bar: Bar, b: List[Int], parent: Option[Parent]) extends Parent 29 | 30 | val right: Foo = Foo( 31 | Bar("asdf", 5), 32 | List(123, 1234), 33 | Some(Bar("asdf", 5)) 34 | ) 35 | 36 | val left: Foo = Foo( 37 | Bar("asdf", 66), 38 | List(1234), 39 | Some(right) 40 | ) 41 | 42 | expectEqual(left, right) 43 | ``` 44 | 45 | ## Versions matrix 46 | 47 | Below table shows past Diffx releases with the corresponding weaver version they were build with. 48 | For newer versions checkout the release changelog. 49 | 50 | | Diffx | weaver | 51 | | ----- | :----: | 52 | | 0.9.0 | 0.8.3 | 53 | -------------------------------------------------------------------------------- /generated-docs/out/usage/derivation.md: -------------------------------------------------------------------------------- 1 | # derivation 2 | 3 | Diffx supports auto and semi-auto derivation. 4 | 5 | For semi-auto derivation you don't need any additional import, just define your instances using: 6 | ```scala 7 | import com.softwaremill.diffx._ 8 | case class Product(name: String) 9 | case class Basket(products: List[Product]) 10 | 11 | implicit val productDiff = Diff.derived[Product] 12 | implicit val basketDiff = Diff.derived[Basket] 13 | ``` 14 | 15 | To use auto derivation add following import 16 | 17 | `import com.softwaremill.diffx.generic.auto._` 18 | 19 | or extend trait 20 | 21 | `com.softwaremill.diffx.generic.auto.AutoDerivation` 22 | 23 | **Auto derivation might have a huge impact on compilation times**, because of that it is recommended to use `semi-auto` derivation. 24 | 25 | 26 | Given that you have auto-derivation enabled you can summon diff instances as you would summon any other implicit type-class by using 27 | `implictly[Diff[T]]`. You can also write a shorter version `Diff[T]` which will be equivalent. 28 | However, if you would like to modify somehow (see [ignoring](./ignoring.md) and [modifying](./modifying.md)) given instance and 29 | put it back into to the implicit scope: 30 | ```scala 31 | implict val diffForMyClass: Diff[MyClass] = Diff[MyClass].doSomething 32 | ``` 33 | you will get a forward reference error. 34 | 35 | To overcome that issue there is a `Derived` wrapper which allows you to summon a wrapped instance. 36 | ```scala 37 | implict val diffForMyClass: Diff[MyClass] = implicitly[Derived[Diff[MyClass]]].value.doSomething 38 | ``` 39 | There is a `summon` method to make it more convenient. Below code is equivalent to the one above. 40 | ```scala 41 | implict val diffForMyClass: Diff[MyClass] = Diff.summon[MyClass].doSomething 42 | ``` -------------------------------------------------------------------------------- /generated-docs/out/usage/extending.md: -------------------------------------------------------------------------------- 1 | # extending 2 | 3 | If you'd like to implement custom matching logic for the given type, create an implicit `Diff` instance for that 4 | type, and make sure it's in scope when any `Diff` instances depending on that type are created. 5 | 6 | Consider following example with `NonEmptyList` from cats. `NonEmptyList` is implemented as case class, 7 | so the default behavior of diffx would be to create a `Diff[NonEmptyList]` typeclass instance using magnolia derivation. 8 | 9 | Obviously that's not what we usually want. In most of the cases we would like `NonEmptyList` to be compared as a list. 10 | Diffx already has an instance of a typeclass for a list (for any iterable to be precise). 11 | All we need to do is to use that typeclass by converting `NonEmptyList` to list which can be done using `contramap` method. 12 | 13 | The final code looks as follows: 14 | 15 | ```scala 16 | import com.softwaremill.diffx._ 17 | import _root_.cats.data.NonEmptyList 18 | implicit def nelDiff[T: Diff]: Diff[NonEmptyList[T]] = 19 | Diff[List[T]].contramap[NonEmptyList[T]](_.toList) 20 | ``` 21 | 22 | *Note: There is a [diffx-cats](../integrations/cats.md) module, so you don't have to do this* -------------------------------------------------------------------------------- /generated-docs/out/usage/ignoring.md: -------------------------------------------------------------------------------- 1 | # ignoring 2 | 3 | 4 | Fields can be excluded from comparison by calling the `ignore` method on the `Diff` instance. 5 | Since `Diff` instances are immutable, the `ignore` method creates a copy of the instance with modified logic. 6 | You can use this instance explicitly. 7 | 8 | ```scala 9 | case class Person(name:String, age:Int) 10 | val modifiedDiff: Diff[Person] = Diff[Person].ignore(_.name) 11 | ``` 12 | 13 | If you still would like to use it implicitly, you first need to summon the instance of the `Diff` typeclass using 14 | the `Derived` typeclass wrapper: `Derived[Diff[Person]]`. Thanks to that trick, later you will be able to put your modified 15 | instance of the `Diff` typeclass into the implicit scope. The whole process looks as follows: 16 | 17 | ```scala 18 | case class Person(name:String, age:Int) 19 | implicit val modifiedDiff: Diff[Person] = Diff.derived[Person].ignore(_.age) 20 | ``` 21 | ```scala 22 | compare(Person("bob", 25), Person("bob", 30)) 23 | // res1: DiffResult = DiffResultObject( 24 | // name = "Person", 25 | // fields = ListMap( 26 | // "name" -> IdenticalValue(value = "bob"), 27 | // "age" -> IdenticalValue(value = "") 28 | // ) 29 | // ) 30 | ``` 31 | 32 | Starting from `diffx` 0.5.5 it is possible to globally customize how ignoring works. By default, an instance of 33 | `Diff` under a particular path gets replaced by `Diff.ignored` instance. `Diff.ignored` is configured to always produce 34 | identical results with fixed placeholder `""` no-matter what it gets. To customize that behavior one has to 35 | create an implicit instance of `DiffConfiguration` with desired behavior. Below is an example of how to include results of 36 | original comparison into ignored output: 37 | 38 | ```scala 39 | implicit val conf: DiffConfiguration = DiffConfiguration(makeIgnored = 40 | (original: Diff[Any]) => 41 | (left: Any, right: Any, context: DiffContext) => { 42 | IdenticalValue(s"Ignored but was: ${original.apply(left, right, context).show()(ShowConfig.noColors)}") 43 | } 44 | ) 45 | // conf: DiffConfiguration = DiffConfiguration(makeIgnored = ) 46 | val d = Diff[Person].ignore(_.age) 47 | // d: Diff[Person] = com.softwaremill.diffx.Diff$$anon$1@5a5ef32b 48 | d(Person("bob", 25), Person("bob", 30)) 49 | // res2: DiffResult = DiffResultObject( 50 | // name = "Person", 51 | // fields = ListMap( 52 | // "name" -> IdenticalValue(value = "bob"), 53 | // "age" -> IdenticalValue(value = "") 54 | // ) 55 | // ) 56 | ``` -------------------------------------------------------------------------------- /generated-docs/out/usage/modifying.md: -------------------------------------------------------------------------------- 1 | # modifying 2 | 3 | Sometimes you might want to compare some nested values using a different comparator but 4 | the type they share is not unique within that hierarchy. 5 | 6 | Consider following example: 7 | ```scala 8 | import com.softwaremill.diffx._ 9 | import com.softwaremill.diffx.generic.auto._ 10 | 11 | case class Person(age: Int, weight: Int) 12 | ``` 13 | 14 | If we would like to compare `weight` differently than `age` we would have to introduce a new type for `weight` 15 | in order to provide a different `Diff` typeclass for only that field. While in general, it is a good idea to have your types 16 | very precise it might not always be practical or even possible. Fortunately, diffx comes with a mechanism which allows 17 | the replacement of nested diff instances. 18 | 19 | First we need to acquire a lens at given path using `modify` method, 20 | and then we can call `setTo` to replace a particular instance. 21 | 22 | ```scala 23 | implicit val diffPerson: Diff[Person] = Diff.summon[Person].modify(_.weight) 24 | .setTo(Diff.approximate(epsilon=5)) 25 | ``` 26 | 27 | ```scala 28 | compare(Person(23, 60), Person(23, 62)) 29 | // res0: DiffResult = DiffResultObject( 30 | // name = "Person", 31 | // fields = ListMap( 32 | // "age" -> IdenticalValue(value = 23), 33 | // "weight" -> IdenticalValue(value = 60) 34 | // ) 35 | // ) 36 | ``` 37 | 38 | In fact, replacement is so powerful that ignoring is implemented as a replacement 39 | with the `Diff.ignore` instance. 40 | 41 | 42 | ## collection support 43 | 44 | Specify how objects within particular collection within particular diff instance should be matched. 45 | We distinguish three main types of collections: 46 | - seqLike collections where elements are indexed collections 47 | - setLike collections where elements aren't indexed 48 | - mapLike collections where elements(values) are indexed by some keys 49 | 50 | Each collection should fall into one of above categories. 51 | Each category exposes different set of methods. 52 | 53 | ```scala 54 | case class Organization(peopleList: List[Person], peopleSet: Set[Person], peopleMap: Map[Person, Person]) 55 | implicit val diffOrg: Diff[Organization] = Diff.summon[Organization] 56 | // seqLike methods: 57 | .modify(_.peopleList).matchByValue(_.age) 58 | .modify(_.peopleList).matchByIndex(index => index % 2) 59 | // setLike methods: 60 | .modify(_.peopleSet).matchBy(_.age) 61 | // mapLike methods: 62 | .modify(_.peopleMap).matchByValue(_.age) 63 | .modify(_.peopleMap).matchByKey(_.weight) 64 | ``` 65 | -------------------------------------------------------------------------------- /generated-docs/out/usage/output.md: -------------------------------------------------------------------------------- 1 | # output 2 | 3 | 4 | `diffx` does its best to show the difference in the most readable way, but obviously the default configuration won't 5 | cover all the use-cases. Because of that, there are few ways how you can modify its output. 6 | 7 | ## intellij idea 8 | 9 | If you are running tests using **IntelliJ IDEA**'s test runner, you will want 10 | to turn off the red text coloring it uses for test failure outputs because 11 | it interferes with difflicious' color outputs. 12 | 13 | In File | Settings | Editor | Color Scheme | Console Colors | Console | Error Output, uncheck the red foreground color. 14 | (Solution provided by Jacob Wang @jatcwang) 15 | 16 | ## colors & signs 17 | 18 | Diffx refers to the values that are compared as `left` and `right`, but you can think of them as `actual` and `expected`. 19 | 20 | By default, the difference is shown in the following form: 21 | 22 | `leftColor(leftValue) -> rightColor(rightValue)` 23 | 24 | When comparing collection types the difference is calculated against the `right` value 25 | 26 | `additionalColor(additionalValue)` when there is an additional entity on the left-hand side 27 | `missingColor(missingValue)` when there is a missing entity on the left-hand side 28 | 29 | Colors can be customized providing an implicit instance of `ShowConfig` class. 30 | In fact `rightColor` and `leftColor` are functions `string => string` so they can be modified to do whatever you want with the output. 31 | 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. 32 | 33 | ````scala 34 | val showConfigWithPlusMinus: ShowConfig = 35 | ShowConfig.default.copy(default = identity, arrow = identity, right = s => "+" + s, left = s => "-" + s) 36 | ```` 37 | 38 | There are two predefined set of colors - light and dark theme. 39 | The default theme is dark, and it can be changed using environment variable - `DIFFX_COLOR_THEME`(`light`/`dark`). 40 | 41 | ## skipping identical 42 | 43 | In some cases it might be desired to skip rendering identical fields. 44 | This can be achieved by using a specific DiffResult transformer. DiffResult transformer is a part of `ShowConfig`. 45 | By default, it is set to an identical function. 46 | 47 | ```scala 48 | implicit val showConfig = ShowConfig.default.copy(transformer = DiffResultTransformer.skipIdentical) 49 | // showConfig: ShowConfig = ShowConfig( 50 | // left = com.softwaremill.diffx.ShowConfig$$$Lambda$8615/0x0000000802442040@681cf3e3, 51 | // right = com.softwaremill.diffx.ShowConfig$$$Lambda$8615/0x0000000802442040@243d6896, 52 | // missing = com.softwaremill.diffx.ShowConfig$$$Lambda$8615/0x0000000802442040@48486ddd, 53 | // additional = com.softwaremill.diffx.ShowConfig$$$Lambda$8615/0x0000000802442040@6596e0ee, 54 | // default = com.softwaremill.diffx.ShowConfig$$$Lambda$8618/0x0000000802450040@6a1a9454, 55 | // arrow = com.softwaremill.diffx.ShowConfig$$$Lambda$8615/0x0000000802442040@25ac2859, 56 | // transformer = com.softwaremill.diffx.DiffResultTransformer$$$Lambda$8607/0x0000000802447840@2e018fb4 57 | // ) 58 | case class Person(name:String, age:Int) 59 | 60 | val result = compare(Person("Bob", 23), Person("Alice", 23)) 61 | // result: DiffResult = DiffResultObject( 62 | // name = "Person", 63 | // fields = ListMap( 64 | // "name" -> DiffResultString( 65 | // diffs = List( 66 | // DiffResultStringLine( 67 | // diffs = List(DiffResultValue(left = "Bob", right = "Alice")) 68 | // ) 69 | // ) 70 | // ), 71 | // "age" -> IdenticalValue(value = 23) 72 | // ) 73 | // ) 74 | result.show() 75 | // res1: String = """Person( 76 | // name: Bob -> Alice)""" 77 | ``` 78 | 79 | There is a convenient method in `ShowConfig` called `skipIdentical` which does exactly that, so the relevant line from 80 | the example can be shortened to `ShowConfig.default.skipIdentical` -------------------------------------------------------------------------------- /generated-docs/out/usage/replacing.md: -------------------------------------------------------------------------------- 1 | # replacing 2 | 3 | Sometimes you might want to compare some nested values using a different comparator but 4 | the type they share is not unique within that hierarchy. 5 | 6 | Consider following example: 7 | ```scala 8 | case class Person(age: Int, weight: Int) 9 | ``` 10 | 11 | If we would like to compare `weight` differently than `age` we would have to introduce a new type for `weight` 12 | in order to provide a different `Diff` typeclass for only that field. While in general, it is a good idea to have your types 13 | very precise it might not always be practical or even possible. Fortunately, diffx comes with a mechanism which allows 14 | the replacement of nested diff instances. 15 | 16 | First we need to acquire a lens at given path using `modify` method, 17 | and then we can call `setTo` to replace a particular instance. 18 | 19 | ```scala 20 | import com.softwaremill.diffx._ 21 | implicit val diffPerson: Derived[Diff[Person]] = Diff.derived[Person].modify(_.weight) 22 | .setTo(Diff.approximate(epsilon=5)) 23 | ``` 24 | 25 | ```scala 26 | compare(Person(23, 60), Person(23, 62)) 27 | // res0: DiffResult = DiffResultObject( 28 | // name = "Person", 29 | // fields = ListMap( 30 | // "age" -> IdenticalValue(value = 23), 31 | // "weight" -> IdenticalValue(value = 60) 32 | // ) 33 | // ) 34 | ``` 35 | 36 | In fact, replacement is so powerful that ignoring is implemented as a replacement 37 | with the `Diff.ignore` instance. 38 | 39 | You can use the same mechanism to set particular object matcher for given nested collection in the hierarchy. 40 | ```scala 41 | case class Organization(peopleList: List[Person], peopleSet: Set[Person], peopleMap: Map[String, Person]) 42 | implicit val diffOrg: Derived[Diff[Organization]] = Diff.derived[Organization] 43 | .modify(_.peopleList).useMatcher(ObjectMatcher.list[Person].byValue(_.age)) 44 | .modify(_.peopleSet).useMatcher(ObjectMatcher.set[Person].by(_.age)) 45 | .modify(_.peopleMap).useMatcher(ObjectMatcher.map[String, Person].byValue(_.age)) 46 | ``` -------------------------------------------------------------------------------- /generated-docs/out/usage/sequences.md: -------------------------------------------------------------------------------- 1 | # sequences 2 | 3 | `diffx` provides instances for many containers from scala's standard library (e.g. lists, sets, maps), however 4 | not all collections can be simply compared. Ordered collections like lists or vectors are compared by default by 5 | comparing elements under the same indexes. 6 | On the other hand maps, by default, are compared by comparing values under the respective keys. 7 | For unordered collections there is an `ObjectMapper` typeclass which defines how elements should be paired. 8 | 9 | ## object matcher 10 | 11 | In general, it is a very simple interface, with a bunch of factory methods. 12 | ```scala 13 | trait ObjectMatcher[T] { 14 | def isSameObject(left: T, right: T): Boolean 15 | } 16 | ``` 17 | 18 | It is mostly useful when comparing unordered collections like sets: 19 | 20 | ```scala 21 | import com.softwaremill.diffx._ 22 | import com.softwaremill.diffx.generic.auto._ 23 | 24 | case class Person(id: String, name: String) 25 | 26 | implicit val personMatcher = ObjectMatcher.set[Person].by(_.id) 27 | val bob = Person("1","Bob") 28 | ``` 29 | ```scala 30 | compare(Set(bob), Set(bob, Person("2","Alice"))) 31 | // res1: DiffResult = DiffResultSet( 32 | // typename = "Set", 33 | // diffs = Set( 34 | // DiffResultObject( 35 | // name = "Person", 36 | // fields = ListMap( 37 | // "id" -> IdenticalValue(value = "1"), 38 | // "name" -> IdenticalValue(value = "Bob") 39 | // ) 40 | // ), 41 | // DiffResultMissing(value = Person(id = "2", name = "Alice")) 42 | // ) 43 | // ) 44 | ``` 45 | 46 | It can be also used to modify how the entries from maps are paired. 47 | In below example we tell `diffx` to compare these maps by paring entries by values using the defined `personMatcher` 48 | ```scala 49 | import com.softwaremill.diffx._ 50 | import com.softwaremill.diffx.generic.auto._ 51 | 52 | case class Person(id: String, name: String) 53 | 54 | implicit val om = ObjectMatcher.map[String, Person].byValue(_.id) 55 | val bob = Person("1","Bob") 56 | ``` 57 | 58 | ```scala 59 | compare(Map("1" -> bob), Map("2" -> bob)) 60 | // res3: DiffResult = DiffResultMap( 61 | // typename = "Map", 62 | // entries = Map( 63 | // DiffResultString( 64 | // diffs = List( 65 | // DiffResultStringLine( 66 | // diffs = List(DiffResultValue(left = "1", right = "2")) 67 | // ) 68 | // ) 69 | // ) -> DiffResultObject( 70 | // name = "Person", 71 | // fields = ListMap( 72 | // "id" -> IdenticalValue(value = "1"), 73 | // "name" -> IdenticalValue(value = "Bob") 74 | // ) 75 | // ) 76 | // ) 77 | // ) 78 | ``` 79 | 80 | Last but not least you can use `objectMatcher` to customize paring when comparing indexed collections. 81 | Such collections are treated similarly to maps (they use key-value object matcher), 82 | but the key type is bound to `Int` (`IterableEntry` is an alias for `MapEntry[Int,V]`). 83 | 84 | ```scala 85 | import com.softwaremill.diffx._ 86 | import com.softwaremill.diffx.generic.auto._ 87 | 88 | case class Person(id: String, name: String) 89 | 90 | implicit val personMatcher = ObjectMatcher.seq[Person].byValue(_.id) 91 | val bob = Person("1","Bob") 92 | val alice = Person("2","Alice") 93 | ``` 94 | ```scala 95 | compare(List(bob, alice), List(alice, bob)) 96 | // res5: DiffResult = DiffResultObject( 97 | // name = "List", 98 | // fields = ListMap( 99 | // "0" -> DiffResultObject( 100 | // name = "Person", 101 | // fields = ListMap( 102 | // "id" -> IdenticalValue(value = "2"), 103 | // "name" -> IdenticalValue(value = "Alice") 104 | // ) 105 | // ), 106 | // "1" -> DiffResultObject( 107 | // name = "Person", 108 | // fields = ListMap( 109 | // "id" -> IdenticalValue(value = "1"), 110 | // "name" -> IdenticalValue(value = "Bob") 111 | // ) 112 | // ) 113 | // ) 114 | // ) 115 | ``` 116 | 117 | *Note: `ObjectMatcher` can be also passed explicitly, either upon creation or during modification* 118 | *See [modifying](modifying.md) for details.* -------------------------------------------------------------------------------- /munit/src/main/scala/com/softwaremill/diffx/munit/DiffxAssertions.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.munit 2 | 3 | import com.softwaremill.diffx.{ShowConfig, Diff} 4 | import munit.Assertions._ 5 | import munit.Location 6 | 7 | trait DiffxAssertions { 8 | def assertEqual[T: Diff](t1: T, t2: T)(implicit c: ShowConfig, loc: Location): Unit = { 9 | val result = Diff.compare(t1, t2) 10 | if (!result.isIdentical) { 11 | fail(result.show())(loc) 12 | } 13 | } 14 | } 15 | 16 | object DiffxAssertions extends DiffxAssertions 17 | -------------------------------------------------------------------------------- /munit/src/test/scala/com/softwaremill/diffx/munit/MunitAssertTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.munit 2 | 3 | import com.softwaremill.diffx.generic.AutoDerivation 4 | 5 | class MunitAssertTest extends munit.FunSuite with DiffxAssertions with AutoDerivation { 6 | // uncomment to run 7 | // test("failing test") { 8 | // val n = Person("n1", 11) 9 | // assertEqual(n, Person("n2", 12)) 10 | // } 11 | 12 | test("hello") { 13 | val n = Person("n1", 11) 14 | assertEqual(n, Person("n1", 11)) 15 | } 16 | } 17 | 18 | case class Person(name: String, age: Int) 19 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.6.2 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-common" % "2.0.9") 2 | addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-publish" % "2.0.9") 3 | addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-extra" % "2.0.9") 4 | 5 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") 6 | addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.9.1") 7 | addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.7") 8 | 9 | addSbtPlugin("org.jetbrains.scala" % "sbt-ide-settings" % "1.1.1") 10 | addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.3") 11 | -------------------------------------------------------------------------------- /refined/src/main/scala/com/softwaremill/diffx/refined/RefinedSupport.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.refined 2 | 3 | import com.softwaremill.diffx.Diff 4 | import eu.timepit.refined.api.Refined 5 | 6 | trait RefinedSupport { 7 | implicit def refinedDiff[T: Diff, P]: Diff[T Refined P] = Diff[T].contramap[T Refined P](_.value) 8 | } 9 | -------------------------------------------------------------------------------- /refined/src/main/scala/com/softwaremill/diffx/refined/package.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | package object refined extends RefinedSupport {} 4 | -------------------------------------------------------------------------------- /refined/src/test/scala/com/softwaremill/diffx/refined/RefinedSupportTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.refined 2 | 3 | import com.softwaremill.diffx.{DiffResultObject, DiffResultString, DiffResultValue, IdenticalValue, _} 4 | import eu.timepit.refined.types.numeric.PosInt 5 | import eu.timepit.refined._ 6 | import eu.timepit.refined.auto._ 7 | import eu.timepit.refined.numeric._ 8 | import com.softwaremill.diffx.generic.auto.diffForCaseClass 9 | import eu.timepit.refined.types.string.NonEmptyString 10 | import eu.timepit.refined.collection.NonEmpty 11 | import org.scalatest.flatspec.AnyFlatSpec 12 | import org.scalatest.matchers.should.Matchers 13 | 14 | import org.scalatest.EitherValues 15 | 16 | class RefinedSupportTest extends AnyFlatSpec with Matchers with EitherValues { 17 | it should "work for refined types" in { 18 | 19 | /** We have to use refineV here because these test are also run against scala3 where better refined goodies are not 20 | * yet supported 21 | */ 22 | val posInt = refineV[Positive](1).value 23 | val nonEmptyFoo = refineV[NonEmpty]("foo").value 24 | val nonEmptyBar = refineV[NonEmpty]("bar").value 25 | val testData1 = TestData(posInt, nonEmptyFoo) 26 | val testData2 = TestData(posInt, nonEmptyBar) 27 | compare(testData1, testData2) shouldBe DiffResultObject( 28 | "TestData", 29 | Map( 30 | "posInt" -> IdenticalValue(1), 31 | "nonEmptyString" -> DiffResultString(List(DiffResultStringLine(List(DiffResultValue("foo", "bar"))))) 32 | ) 33 | ) 34 | } 35 | } 36 | 37 | case class TestData(posInt: PosInt, nonEmptyString: NonEmptyString) 38 | -------------------------------------------------------------------------------- /scalatest-must/src/main/scala-2/com/softwaremill/diffx/scalatest/DiffMustMatcher.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.scalatest 2 | 3 | import com.softwaremill.diffx.{ShowConfig, Diff} 4 | import org.scalactic.{Prettifier, source} 5 | import org.scalatest.Assertion 6 | import org.scalatest.matchers.must.Matchers 7 | import org.scalatest.matchers.{MatchResult, Matcher} 8 | 9 | trait DiffMustMatcher { 10 | 11 | implicit def convertToAnyMustMatcher[T: Diff]( 12 | any: T 13 | )(implicit pos: source.Position, prettifier: Prettifier, consoleColorConfig: ShowConfig): AnyMustWrapper[T] = 14 | new AnyMustWrapper[T](any) 15 | 16 | final class AnyMustWrapper[T](val leftValue: T)(implicit 17 | val pos: source.Position, 18 | val prettifier: Prettifier, 19 | val consoleColorConfig: ShowConfig, 20 | val diff: Diff[T] 21 | ) extends Matchers { 22 | 23 | def mustMatchTo(rightValue: T): Assertion = { 24 | leftValue must matchTo[T](rightValue) 25 | } 26 | 27 | private def matchTo[A: Diff](right: A): Matcher[A] = { left => 28 | val result = Diff[A].apply(left, right) 29 | if (!result.isIdentical) { 30 | val diff = 31 | result.show().split('\n').mkString(Console.RESET, s"${Console.RESET}\n${Console.RESET}", Console.RESET) 32 | MatchResult(matches = false, s"Matching error:\n$diff", "") 33 | } else { 34 | MatchResult(matches = true, "", "") 35 | } 36 | } 37 | } 38 | } 39 | 40 | object DiffMustMatcher extends DiffMustMatcher 41 | -------------------------------------------------------------------------------- /scalatest-must/src/main/scala-3/com/softwaremill/diffx/scalatest/DiffMustMatcher.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.scalatest 2 | 3 | import com.softwaremill.diffx.{ShowConfig, Diff} 4 | import org.scalactic.{Prettifier, source} 5 | import org.scalatest.Assertion 6 | import org.scalatest.matchers.must.Matchers 7 | import org.scalatest.matchers.{MatchResult, Matcher} 8 | 9 | trait DiffMustMatcher { 10 | 11 | extension [T]( 12 | leftSideValue: T 13 | )(using pos: source.Position, prettifier: Prettifier, diff: Diff[T], c: ShowConfig) 14 | def mustMatchTo(rightValue: T): Assertion = { 15 | import Matchers.must 16 | leftSideValue must matchTo(rightValue) 17 | } 18 | 19 | private def matchTo[A: Diff](right: A)(implicit c: ShowConfig): Matcher[A] = { left => 20 | val result = Diff[A].apply(left, right) 21 | if (!result.isIdentical) { 22 | val diff = 23 | result.show().split('\n').mkString(Console.RESET, s"${Console.RESET}\n${Console.RESET}", Console.RESET) 24 | MatchResult(matches = false, s"Matching error:\n$diff", "") 25 | } else { 26 | MatchResult(matches = true, "", "") 27 | } 28 | } 29 | } 30 | 31 | object DiffMustMatcher extends DiffMustMatcher 32 | -------------------------------------------------------------------------------- /scalatest-must/src/test/scala/com/softwaremill/diffx/scalatest/DiffMustMatcherTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.scalatest 2 | 3 | import com.softwaremill.diffx.generic.AutoDerivation 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.must.Matchers 6 | 7 | class DiffMatcherTest extends AnyFlatSpec with Matchers with DiffMustMatcher with AutoDerivation { 8 | val right: Foo = Foo( 9 | Bar("asdf", 5, Map("a" -> 2)), 10 | List(123, 1234) 11 | ) 12 | val left: Foo = Foo( 13 | Bar("asdf", 66, Map("b" -> 3)), 14 | List(1234) 15 | ) 16 | 17 | ignore should "work" in { 18 | left mustMatchTo (right) 19 | } 20 | 21 | it should "work with option and some" in { 22 | Option("test") mustMatchTo (Some("test")) 23 | } 24 | } 25 | sealed trait Parent 26 | case class Bar(s: String, i: Int, ss: Map[String, Int]) extends Parent 27 | case class Foo(bar: Bar, b: List[Int]) extends Parent 28 | -------------------------------------------------------------------------------- /scalatest-should/src/main/scala-2/com/softwaremill/diffx/scalatest/DiffShouldMatcher.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.scalatest 2 | 3 | import com.softwaremill.diffx.{ShowConfig, Diff} 4 | import org.scalactic.{Prettifier, source} 5 | import org.scalatest.Assertion 6 | import org.scalatest.matchers.should.Matchers 7 | import org.scalatest.matchers.{MatchResult, Matcher} 8 | 9 | trait DiffShouldMatcher { 10 | implicit def convertToAnyShouldMatcher[T: Diff]( 11 | any: T 12 | )(implicit pos: source.Position, prettifier: Prettifier, c: ShowConfig): DiffAnyShouldWrapper[T] = 13 | new DiffAnyShouldWrapper[T](any) 14 | 15 | final class DiffAnyShouldWrapper[T](val leftValue: T)(implicit 16 | val pos: source.Position, 17 | val prettifier: Prettifier, 18 | val c: ShowConfig, 19 | val d: Diff[T] 20 | ) extends Matchers { 21 | 22 | def shouldMatchTo(rightValue: T): Assertion = { 23 | leftValue should matchTo[T](rightValue) 24 | } 25 | 26 | private def matchTo[A: Diff](right: A): Matcher[A] = { left => 27 | val result = Diff[A].apply(left, right) 28 | if (!result.isIdentical) { 29 | val diff = 30 | result.show().split('\n').mkString(Console.RESET, s"${Console.RESET}\n${Console.RESET}", Console.RESET) 31 | MatchResult(matches = false, s"Matching error:\n$diff", "") 32 | } else { 33 | MatchResult(matches = true, "", "") 34 | } 35 | } 36 | } 37 | } 38 | 39 | object DiffShouldMatcher extends DiffShouldMatcher 40 | -------------------------------------------------------------------------------- /scalatest-should/src/main/scala-3/com/softwaremill/diffx/scalatest/DiffShouldMatcher.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.scalatest 2 | 3 | import com.softwaremill.diffx.{ShowConfig, Diff} 4 | import org.scalactic.{Prettifier, source} 5 | import org.scalatest.Assertion 6 | import org.scalatest.matchers.should.Matchers 7 | import org.scalatest.matchers.{MatchResult, Matcher} 8 | 9 | trait DiffShouldMatcher { 10 | extension [T]( 11 | leftSideValue: T 12 | )(using pos: source.Position, prettifier: Prettifier, diff: Diff[T], c: ShowConfig) 13 | def shouldMatchTo(rightValue: T): Assertion = { 14 | import Matchers.should 15 | leftSideValue should matchTo(rightValue) 16 | } 17 | 18 | private def matchTo[A: Diff](right: A)(implicit c: ShowConfig): Matcher[A] = { left => 19 | val result = Diff[A].apply(left, right) 20 | if (!result.isIdentical) { 21 | val diff = 22 | result.show().split('\n').mkString(Console.RESET, s"${Console.RESET}\n${Console.RESET}", Console.RESET) 23 | MatchResult(matches = false, s"Matching error:\n$diff", "") 24 | } else { 25 | MatchResult(matches = true, "", "") 26 | } 27 | } 28 | } 29 | 30 | object DiffShouldMatcher extends DiffShouldMatcher 31 | -------------------------------------------------------------------------------- /scalatest-should/src/test/scala/com/softwaremill/diffx/scalatest/DiffShouldMatcherTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.scalatest 2 | 3 | import com.softwaremill.diffx.generic.AutoDerivation 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class DiffShouldMatcherTest extends AnyFlatSpec with DiffShouldMatcher with Matchers with AutoDerivation { 8 | val right: Foo = Foo( 9 | Bar("asdf", 5, Map("a" -> 2)), 10 | List(123, 1234) 11 | ) 12 | val left: Foo = Foo( 13 | Bar("asdf", 66, Map("b" -> 3)), 14 | List(1234) 15 | ) 16 | 17 | ignore should "work" in { 18 | left shouldMatchTo (right) 19 | } 20 | 21 | it should "work with option and some" in { 22 | Option("test") shouldMatchTo Some("test") 23 | } 24 | } 25 | sealed trait Parent 26 | case class Bar(s: String, i: Int, ss: Map[String, Int]) extends Parent 27 | case class Foo(bar: Bar, b: List[Int]) extends Parent 28 | -------------------------------------------------------------------------------- /scalatest/src/main/scala/com/softwaremill/diffx/scalatest/DiffMatcher.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.scalatest 2 | 3 | import com.softwaremill.diffx.{ShowConfig, Diff} 4 | import org.scalatest.matchers.{MatchResult, Matcher} 5 | 6 | @deprecated("Use DiffShouldMatcher or DiffMustMatcher instead") 7 | trait DiffMatcher { 8 | def matchTo[A: Diff](right: A)(implicit c: ShowConfig): Matcher[A] = { left => 9 | val result = Diff[A].apply(left, right) 10 | if (!result.isIdentical) { 11 | val diff = 12 | result.show().split('\n').mkString(Console.RESET, s"${Console.RESET}\n${Console.RESET}", Console.RESET) 13 | MatchResult(matches = false, s"Matching error:\n$diff", "") 14 | } else { 15 | MatchResult(matches = true, "", "") 16 | } 17 | } 18 | } 19 | 20 | @deprecated("Use DiffShouldMatcher or DiffMustMatcher instead") 21 | object DiffMatcher extends DiffMatcher 22 | -------------------------------------------------------------------------------- /scalatest/src/test/scala/com/softwaremill/diffx/scalatest/DiffMatcherTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.scalatest 2 | 3 | import com.softwaremill.diffx.generic.AutoDerivation 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class DiffMatcherTest extends AnyFlatSpec with Matchers with DiffMatcher with AutoDerivation { 8 | val right: Foo = Foo( 9 | Bar("asdf", 5, Map("a" -> 2)), 10 | List(123, 1234) 11 | ) 12 | val left: Foo = Foo( 13 | Bar("asdf", 66, Map("b" -> 3)), 14 | List(1234) 15 | ) 16 | 17 | ignore should "work" in { 18 | left should matchTo(right) 19 | } 20 | } 21 | sealed trait Parent 22 | case class Bar(s: String, i: Int, ss: Map[String, Int]) extends Parent 23 | case class Foo(bar: Bar, b: List[Int]) extends Parent 24 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | # For compatibility with non-flake-enabled Nix versions 2 | { system ? builtins.currentSystem, ... }: 3 | ( 4 | import 5 | ( 6 | fetchTarball { 7 | url = "https://github.com/edolstra/flake-compat/archive/b7547d3eed6f32d06102ead8991ec52ab0a4f1a7.tar.gz"; 8 | sha256 = "09ln95rvvjxjsnmvzrvyc2ji2l5lz17s671z51f9z8cl4m23ndp2"; 9 | } 10 | ) 11 | { src = ./.; } 12 | ).shellNix -------------------------------------------------------------------------------- /specs2/src/main/scala/com/softwaremill/diffx/specs2/DiffMatcher.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.specs2 2 | 3 | import com.softwaremill.diffx.{ShowConfig, Diff} 4 | import org.specs2.matcher.{Expectable, MatchResult, Matcher} 5 | 6 | trait DiffMatcher { 7 | def matchTo[A: Diff](left: A)(implicit c: ShowConfig): DiffForMatcher[A] = DiffForMatcher(left) 8 | 9 | case class DiffForMatcher[A: Diff](right: A) extends Matcher[A] { 10 | override def apply[S <: A](left: Expectable[S]): MatchResult[S] = { 11 | val diff = Diff[A] 12 | result( 13 | test = { 14 | diff.apply(left.value, right).isIdentical 15 | }, 16 | okMessage = "", 17 | koMessage = { 18 | val diffResult = diff.apply(left.value, right) 19 | if (!diffResult.isIdentical) { 20 | diffResult.show() 21 | } else { 22 | "" 23 | } 24 | }, 25 | left 26 | ) 27 | } 28 | } 29 | } 30 | 31 | object DiffMatcher extends DiffMatcher 32 | -------------------------------------------------------------------------------- /specs2/src/test/scala/com/softwaremill/diffx/specs2/DiffMatcherTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.specs2 2 | 3 | import org.specs2.Specification 4 | import com.softwaremill.diffx.generic.AutoDerivation 5 | 6 | class DiffMatcherTest extends Specification with DiffMatcher with AutoDerivation { 7 | override def is = s2"""This is an empty specification""" 8 | 9 | val right: Foo = Foo( 10 | Bar("asdf", 5), 11 | List(123, 1234), 12 | Some(Bar("asdf", 5)) 13 | ) 14 | val left: Foo = Foo( 15 | Bar("asdf", 66), 16 | List(1234), 17 | Some(right) 18 | ) 19 | 20 | def ignore = left must matchTo(right) 21 | } 22 | sealed trait Parent 23 | case class Bar(s: String, i: Int) extends Parent 24 | case class Foo(bar: Bar, b: List[Int], parent: Option[Parent]) extends Parent 25 | -------------------------------------------------------------------------------- /tagging/src/main/scala/com/softwaremill/diffx/tagging/DiffTaggingSupport.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.tagging 2 | 3 | import com.softwaremill.diffx.Diff 4 | import com.softwaremill.tagging.@@ 5 | 6 | trait DiffTaggingSupport { 7 | implicit def taggedDiff[T: Diff, U]: Diff[T @@ U] = Diff[T].contramap[T @@ U](identity) 8 | } 9 | -------------------------------------------------------------------------------- /tagging/src/main/scala/com/softwaremill/diffx/tagging/package.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx 2 | 3 | package object tagging extends DiffTaggingSupport {} 4 | -------------------------------------------------------------------------------- /tagging/src/test/scala/com/softwaremill/diffx/tagging/test/DiffTaggingSupportTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.tagging.test 2 | 3 | import com.softwaremill.diffx.generic.AutoDerivation 4 | import com.softwaremill.diffx.{Diff, DiffResultObject, DiffResultValue, IdenticalValue} 5 | import com.softwaremill.diffx.tagging.taggedDiff 6 | import com.softwaremill.tagging._ 7 | import com.softwaremill.tagging.@@ 8 | import org.scalatest.flatspec.AnyFlatSpec 9 | import org.scalatest.matchers.should.Matchers 10 | 11 | class DiffTaggingSupportTest extends AnyFlatSpec with Matchers with AutoDerivation { 12 | it should "work for tagged types" in { 13 | val p1 = 1.taggedWith[T1] 14 | val p11 = 2.taggedWith[T1] 15 | val p2 = 1.taggedWith[T2] 16 | compare(TestData(p1, p2), TestData(p11, p2)) shouldBe DiffResultObject( 17 | "TestData", 18 | Map("p1" -> DiffResultValue(p1, p11), "p2" -> IdenticalValue(p2)) 19 | ) 20 | } 21 | 22 | private def compare[T](t1: T, t2: T)(implicit d: Diff[T]) = d.apply(t1, t2) 23 | } 24 | 25 | sealed trait T1 26 | sealed trait T2 27 | case class TestData(p1: Int @@ T1, p2: Int @@ T2) 28 | -------------------------------------------------------------------------------- /utest/src/main/scala/com/softwaremill/diffx/utest/DiffxAssertions.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.utest 2 | 3 | import com.softwaremill.diffx.{ShowConfig, Diff} 4 | import utest.AssertionError 5 | 6 | trait DiffxAssertions { 7 | 8 | def assertEqual[T: Diff](t1: T, t2: T)(implicit c: ShowConfig): Unit = { 9 | val result = Diff.compare(t1, t2) 10 | if (!result.isIdentical) { 11 | throw AssertionError(result.show(), Seq.empty, null) 12 | } 13 | } 14 | } 15 | 16 | object DiffxAssertions extends DiffxAssertions 17 | -------------------------------------------------------------------------------- /utest/src/test/scala/com/softwaremill/diffx/utest/UtestAssertTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.utest 2 | 3 | import com.softwaremill.diffx.generic.AutoDerivation 4 | import utest._ 5 | 6 | object UtestAssertTest extends TestSuite with DiffxAssertions with AutoDerivation { 7 | val tests = Tests { 8 | 9 | // uncomment to run 10 | // test("failing test") { 11 | // val n = Person("n1", 11) 12 | // assertEqual(n, Person("n2", 12)) 13 | // } 14 | 15 | test("passing test") { 16 | val n = Person("n1", 11) 17 | assertEqual(n, Person("n1", 11)) 18 | } 19 | } 20 | } 21 | 22 | case class Person(name: String, age: Int) 23 | -------------------------------------------------------------------------------- /weaver/src/main/scala/com/softwaremill/diffx/weaver/DiffxExpectations.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.weaver 2 | 3 | import com.softwaremill.diffx.{Diff, ShowConfig} 4 | import weaver.Expectations.Helpers.{failure, success} 5 | import weaver.{Expectations, SourceLocation} 6 | 7 | trait DiffxExpectations { 8 | def expectEqual[T: Diff](t1: T, t2: T)(implicit c: ShowConfig, loc: SourceLocation): Expectations = { 9 | val result = Diff.compare(t1, t2) 10 | if (result.isIdentical) 11 | success 12 | else 13 | failure(result.show())(loc) 14 | } 15 | } 16 | 17 | object DiffxExpectations extends DiffxExpectations 18 | -------------------------------------------------------------------------------- /weaver/src/test/scala/com/softwaremill/diffx/weaver/DiffxExpectationsTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.diffx.weaver 2 | 3 | import cats.data.Validated 4 | import com.softwaremill.diffx.generic.AutoDerivation 5 | import weaver.FunSuite 6 | 7 | object DiffxExpectationsTest extends FunSuite with DiffxExpectations with AutoDerivation { 8 | test("expectEqual should fail when there are differences") { 9 | val n = Person("n1", 11) 10 | expectEqual(n, Person("n2", 12)).run match { 11 | case Validated.Valid(_) => failure(s"Expected a failure but succeeded") 12 | case Validated.Invalid(_) => success 13 | } 14 | } 15 | 16 | test("expectEqual should succeed when there are no differences") { 17 | val n = Person("n1", 11) 18 | expectEqual(n, Person("n1", 11)) 19 | } 20 | } 21 | 22 | case class Person(name: String, age: Int) 23 | --------------------------------------------------------------------------------