diff --git a/.VERSION_PREFIX b/.VERSION_PREFIX new file mode 100644 index 0000000..cb94c17 --- /dev/null +++ b/.VERSION_PREFIX @@ -0,0 +1 @@ +2.13 \ No newline at end of file diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 5bc9eda..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,47 +0,0 @@ -version: 2.1 - -orbs: - kaocha: lambdaisland/kaocha@dev:first - clojure: lambdaisland/clojure@dev:first - -commands: - checkout_and_run: - parameters: - clojure_version: - type: string - steps: - - checkout - - clojure/with_cache: - cache_version: << parameters.clojure_version >> - steps: - - run: clojure -e '(println (System/getProperty "java.runtime.name") (System/getProperty "java.runtime.version") "\nClojure" (clojure-version))' - - kaocha/execute: - args: "unit --reporter documentation --plugin cloverage --codecov" - clojure_version: << parameters.clojure_version >> - - kaocha/upload_codecov: - flags: unit - -jobs: - java-11-clojure-1_10: - executor: clojure/openjdk11 - steps: [{checkout_and_run: {clojure_version: "1.10.0"}}] - - java-9-clojure-1_9: - executor: clojure/openjdk9 - steps: [{checkout_and_run: {clojure_version: "1.9.0"}}] - - java-8-clojure-1_10: - executor: clojure/openjdk8 - steps: [{checkout_and_run: {clojure_version: "1.10.0"}}] - - java-8-clojure-1_9: - executor: clojure/openjdk8 - steps: [{checkout_and_run: {clojure_version: "1.9.0"}}] - -workflows: - kaocha_test: - jobs: - - java-11-clojure-1_10 - - java-9-clojure-1_9 - - java-8-clojure-1_10 - - java-8-clojure-1_9 diff --git a/.dir-locals.el b/.dir-locals.el deleted file mode 100644 index c9629d5..0000000 --- a/.dir-locals.el +++ /dev/null @@ -1 +0,0 @@ -((nil . ((cider-clojure-cli-global-options . "-A:dev:test")))) diff --git a/.github/workflows/add_to_project_board.yml b/.github/workflows/add_to_project_board.yml new file mode 100644 index 0000000..39f7c02 --- /dev/null +++ b/.github/workflows/add_to_project_board.yml @@ -0,0 +1,9 @@ +name: Add new pr or issue to project board + +on: [issues] + +jobs: + add-to-project: + uses: lambdaisland/open-source/.github/workflows/add-to-project-board.yml@main + secrets: inherit + diff --git a/.github/workflows/bb.yml b/.github/workflows/bb.yml new file mode 100644 index 0000000..25317de --- /dev/null +++ b/.github/workflows/bb.yml @@ -0,0 +1,52 @@ +name: bb + +on: [push, pull_request] + +jobs: + + clojure: + + strategy: + matrix: + os: [ubuntu-latest] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v3 + + # It is important to install java before installing clojure tools which needs java + # exclusions: babashka, clj-kondo and cljstyle + - name: Prepare java + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '11' + + - name: Install clojure tools + uses: DeLaGuardo/setup-clojure@10.1 + with: + # Install just one or all simultaneously + # The value must indicate a particular version of the tool, or use 'latest' + # to always provision the latest version + bb: latest + + # Optional step: + - name: Cache clojure dependencies + uses: actions/cache@v3 + with: + path: | + ~/.m2/repository + ~/.gitlibs + ~/.deps.clj + # List all files containing dependencies: + key: cljdeps-${{ hashFiles('deps.edn') }} + # key: cljdeps-${{ hashFiles('deps.edn', 'bb.edn') }} + # key: cljdeps-${{ hashFiles('project.clj') }} + # key: cljdeps-${{ hashFiles('build.boot') }} + restore-keys: cljdeps- + + - name: Execute bb tests + run: | + bb test:bb diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..4ca015e --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,44 @@ +name: Continuous Delivery + +on: push + +jobs: + Kaocha: + runs-on: ${{matrix.sys.os}} + + strategy: + matrix: + sys: + # - { os: macos-latest, shell: bash } + - { os: ubuntu-latest, shell: bash } + # - { os: windows-latest, shell: powershell } + + defaults: + run: + shell: ${{matrix.sys.shell}} + + steps: + - uses: actions/checkout@v2 + + - name: 🔧 Install java + uses: actions/setup-java@v1 + with: + java-version: '25' + + - name: 🔧 Install clojure + uses: DeLaGuardo/setup-clojure@master + with: + cli: '1.12.3.1577' + + - name: 🗝 maven cache + uses: actions/cache@v4 + with: + path: | + ~/.m2 + ~/.gitlibs + key: ${{ runner.os }}-maven-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: 🧪 Run tests + run: bin/kaocha diff --git a/.gitignore b/.gitignore index 5ae188f..9faeae1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,22 @@ .nrepl-port target repl -bin scratch.clj +/out/ +/checkouts/ +/target/ +/.cljs_node_repl/ +/node_modules/ +pom.xml +pom.xml.asc +*.jar +*.classout +package.json +yarn.lock +.shadow-cljs +resources/public/ui +resources/public/ +.store +package-lock.json +.cache +*.iml \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 10bb375..05a3f0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,141 @@ ## Changed +# 2.13.231 (2026-04-07 / 8d35b69) + +## Changed + +- General dependency and tooling version bumps + +## Fixed + +- Fixed puget printer for futures + +## Changed + +- Fix `mimimize` when inserting/removing map keys (should preserve associated + value) +- Add `minimise` variant + +# 2.12.219 (2025-02-06 / 9e6942a) + +## Changed + +- [BREAKING] Get smarter about diffing records, instead of simply diffing them + as maps. We now only recurse into records if the two compared values are both + records of the same type. +- Bump dependencies: fipp, rrb-vector + +# 2.11.216 (2024-02-17 / e77c3bf) + +## Added + +- Diff / preserve metadata on collections + +## Fixed + +- Varying key order in maps should produce a consistent diff (#47) + +## Changed + +# 2.9.202 (2023-06-09 / 35494a0) + +## Added + + - Add documentation for using a custom color scheme using custom data printers. + +## Fixed + +- Simplified internals when diffing maps for improved performance on many datasets. (Thanks [@latacora-paul](https://github.com/latacora-paul)!) + +## Changed + +# 2.8.190 (2023-03-30 / 34d5e17) + +## Added + +- Enable print tests in babashka +- Add a `lambdaisland.deep-diff2/minimize` function, which removes any items + that haven't changed from the diff. + +## Fixed + +## Changed + +# 2.7.169 (2022-11-25 / 343811e) + +## Fixed + +- Fix printing of mismatch/deletion/insertion on Babashka + +# 2.6.166 (2022-11-25 / 06fec7e) + +## Fixed + +- Babashka compatibility + +# 2.5.151 (2022-11-21 / 92232a1) + +## Changed + +- [breaking] Fall back to the system printer when no deep-diff2 specific print handler is available for a given type. See the README for details. + +# 2.4.138 (2022-09-01 / 6196130) + +## Fixed + +- Fix issue (Fails with records with deleted keys)[https://github.com/lambdaisland/deep-diff2/issues/29] + +# 2.3.127 (2022-07-01 / a8186a5) + +## Fixed +* Remove "test" directory from the main paths in `deps.edn` to fix Cljdoc builds. This change also makes the artifact (very slightly) smaller, reducing the JAR's size by 3KB, or about 15 percent. + +# 2.2.124 (2022-05-16 / 5a94bec) + +## Fixed + +- Bump clj-diff, to bring back compatibility with earlier java versions + +# 2.1.121 (2022-05-13 / bb0dd63) + +## Fixed + +- Bump clj-diff, which fixes an issue where the diffing would not terminate in + specific cases + +## Changed + +- Bump all dependencies to the latest version + +# 2.0.108 (2020-08-19 / e006fc5) + +## Changed + +- Switch to using lambdaisland/clj-diff, a fork of an upstream fork + +# 2.0.0-93 (2020-04-20 / 6ff9209) + +## Fixed + +- Fix unsupported cljs lookbehind regex in code inherited from Puget (Thanks [@JarrodCTaylor](https://github.com/JarrodCTaylor)!) + +# 2.0.0-84 (2020-04-01 / 9c2af83) + +## Fixed + +- Typos in deep_diff2.cljs resulting from naming changes + +# 2.0.0-72 (2020-03-27 / 2862182) + +## Added + +- Added support for ClojureScript (ported to CLJC) + +## Changed + +- Changed namespace and artifact (jar) names to include a "2" suffix, because of breaking changes. + # 0.0-47 (2019-04-11 / 27cf55c) ## Added diff --git a/README.md b/README.md index ee7b7de..d61b196 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,73 @@ -# lambdaisland/deep-diff +# lambdaisland/deep-diff2 -[![CircleCI](https://circleci.com/gh/lambdaisland/deep-diff.svg?style=svg)](https://circleci.com/gh/lambdaisland/deep-diff) [![cljdoc badge](https://cljdoc.org/badge/lambdaisland/deep-diff)](https://cljdoc.org/d/lambdaisland/deep-diff) [![Clojars Project](https://img.shields.io/clojars/v/lambdaisland/deep-diff.svg)](https://clojars.org/lambdaisland/deep-diff) [![codecov](https://codecov.io/gh/lambdaisland/deep-diff/branch/master/graph/badge.svg)](https://codecov.io/gh/lambdaisland/deep-diff) +[![GitHub Actions](https://github.com/lambdaisland/deep-diff2/actions/workflows/main.yml/badge.svg)](https://github.com/lambdaisland/deep-diff2/actions/workflows/main.yml) [![cljdoc badge](https://cljdoc.org/badge/lambdaisland/deep-diff2)](https://cljdoc.org/d/lambdaisland/deep-diff2) [![Clojars Project](https://img.shields.io/clojars/v/lambdaisland/deep-diff2.svg)](https://clojars.org/lambdaisland/deep-diff2) -Recursively compare Clojure data structures, and produce a colorized diff of the result. +Recursively compare Clojure or ClojureScript data structures, and produce a colorized diff of the result. ![screenshot showing REPL example](screenshot.png) -## Install +Deep-diff2 is foremost intended for creating visual diffs for human consumption, +if you want to programatically diff/patch Clojure data structures then +[Editscript](https://github.com/juji-io/editscript) may be a better fit, see +[this write-up by Huahai Yang](https://juji.io/blog/comparing-clojure-diff-libraries/). -[![Clojars Project](https://img.shields.io/clojars/v/lambdaisland/deep-diff.svg)](https://clojars.org/lambdaisland/deep-diff) + +## Lambda Island Open Source + +Thank you! deep-diff2 is made possible thanks to our generous backers. [Become a +backer on OpenCollective](https://opencollective.com/lambda-island) so that we +can continue to make deep-diff2 better. + + + + + + + +  + +deep-diff2 is part of a growing collection of quality Clojure libraries created and maintained +by the fine folks at [Gaiwan](https://gaiwan.co). + +Pay it forward by [becoming a backer on our OpenCollective](http://opencollective.com/lambda-island), +so that we continue to enjoy a thriving Clojure ecosystem. + +You can find an overview of all our different projects at [lambdaisland/open-source](https://github.com/lambdaisland/open-source). + +  + +  + + +## Installation + +deps.edn + +``` +lambdaisland/deep-diff2 {:mvn/version "2.13.231"} +``` + +project.clj + +``` +[lambdaisland/deep-diff2 "2.13.231"] +``` ## Use -- [API docs](https://cljdoc.org/d/lambdaisland/deep-diff/CURRENT) +- [API docs](https://cljdoc.org/d/lambdaisland/deep-diff2/CURRENT) ``` clojure -(require '[lambdaisland.deep-diff :as ddiff]) +(require '[lambdaisland.deep-diff2 :as ddiff]) (ddiff/pretty-print (ddiff/diff {:a 1 :b 2} {:a 1 :c 3})) ``` ### Diffing -`lambdaisland.deep-diff/diff` takes two arguments and returns a "diff", a data +`lambdaisland.deep-diff2/diff` takes two arguments and returns a "diff", a data structure that contains markers for insertions, deletions, or mismatches. These are records with `-` and `+` fields. @@ -35,7 +78,7 @@ are records with `-` and `+` fields. ### Printing -You can pass this diff to `lambdaisland.deep-diff/pretty-print`. This function +You can pass this diff to `lambdaisland.deep-diff2/pretty-print`. This function uses [Puget](https://github.com/greglook/puget) and [Fipp](https://github.com/brandonbloom/fipp) to format the diff and print the result to standard out. @@ -49,12 +92,187 @@ For fine grained control you can create a custom Puget printer, and supply it to (ddiff/pretty-print (ddiff/diff {:a 1 :b 2} {:a 1 :b 3}) narrow-printer) ``` -For more advanced uses like incorporating diffs into your own Fipp documents, see `lambdaisland.deep-diff.printer/format-doc`, `lambdaisland.deep-diff.printer/print-doc`. +For more advanced uses like incorporating diffs into your own Fipp documents, see `lambdaisland.deep-diff2.printer/format-doc`, `lambdaisland.deep-diff2.printer/print-doc`. -You can register print handlers for new types using -`lambdaisland.deep-diff.printer/register-print-handler!`, or by passing and +### Minimizing + +If you are only interested in the changes, and not in any values that haven't +changed, then you can use `ddiff/minimize` to return a more compact diff. + +This is especially useful for potentially large nested data structures, for +example a JSON response coming from a web service. + +```clj +(-> (ddiff/diff {:a "apple" :b "pear"} {:a "apple" :b "banana"}) + ddiff/minimize + ddiff/pretty-print) +;; {:b -"pear" +"banana"} +``` + +### Print handlers for custom or built-in types + +In recent versions deep-diff2 initializes its internal copy of Puget with +`{:print-fallback :print}`, meaning it will fall back to using the system +printer, which you can extend by extending the `print-method` multimethod. + +This also means that we automatically pick up additional handlers installed by +libraries, such as [time-literals](https://github.com/henryw374/time-literals). + +You can also register print handlers for deep-diff2 specifically by using +`lambdaisland.deep-diff2.printer-impl/register-print-handler!`, or by passing an `:extra-handlers` map to `printer`. +If you are dealing with printing of custom types you might find that there are +multiple print implementations you need to keep up-to-date, see +[lambdaisland.data-printers](https://github.com/lambdaisland/data-printers) for +a high-level API that can work with all the commonly used print implementations. + +#### Example of a custom type + +See [repl_sessions/custom_type.clj](repl_sessions/custom_type.clj) for the full +code and results. + +```clj +(deftype Degrees [amount unit] + Object + (equals [this that] + (and (instance? Degrees that) + (= amount (.-amount that)) + (= unit (.-unit that))))) + +;; Using system handler fallback +(defmethod print-method Degrees [degrees out] + (.write out (str (.-amount degrees) "°" (.-unit degrees)))) + +;; OR Using a Puget-specific handler +(lambdaisland.deep-diff2.printer-impl/register-print-handler! + `Degrees + (fn [printer value] + [:span + (lambdaisland.deep-diff2.puget.color/document printer :number (str (.-amount value))) + (lambdaisland.deep-diff2.puget.color/document printer :tag "°") + (lambdaisland.deep-diff2.puget.color/document printer :keyword (str (.-unit value)))])) +``` + +### Set up a custom print handler with different colors by utilizing Puget library + +Sometimes, we need to tune the colors to: + +- Ensure adequate contrast on a different background. +- Ensure readability by people who are colorblind. +- Match your editor or main diff tool's color scheme. + +#### Config of Puget + +Fortunately, the Puget library included in deep-diff2 already allows customization through a custom printer. + +In the Puget libray, 8-bit scheme is expressed via `[:fg-256 5 n]` where n is between 0 and 255. We can combine foreground and background, for example, like so: `[:fg-256 5 226 :bg-256 5 56]`. + +24-bit scheme is expressed via `[:fg-256 2 r g b]` where r g b are each between 0 and 255. Foreground and background can be combined, for example: `[:fg-256 2 205 236 255 :bg-256 2 110 22 188]`. + +#### An example of customizing color + +For example, if we change the `:lambdaisland.deep-diff2.printer-impl/deletion` from `[:red]` to `[:bg-256 5 11]`, the color code it outputs will change from `\u001b[31m` to `\u001b[48;5;11m` + +``` +user=> (use 'lambdaisland.deep-diff2) +nil +user=> (def color-printer (printer {:color-scheme {:lambdaisland.deep-diff2/deletion [:bg-256 5 11]}})) +#'user/color-printer +user=> (pretty-print (diff {:a 1} {:b 2}) color-printer) +{+:b 2, -:a 1} +``` +That results in the following highlighting: +![screenshot showing color customization](color-scheme.png) + +### Time, data literal + +A common use case is diffing and printing Java date and time objects +(`java.util.Date`, `java.time.*`, `java.sql.Date|Time|DateTime`). + +Chances are you already have print handlers (and data readers) set up for these +via the [time-literals](https://github.com/henryw374/time-literals) library +(perhaps indirectly by pulling in [tick](https://github.com/juxt/tick). In that +case these should _just work_. + +```clj +(ddiff/diff #inst "2019-04-09T14:57:46.128-00:00" + #inst "2019-04-10T14:57:46.128-00:00") +``` +or +```clj +(import '[java.sql Timestamp]) +(ddiff/diff (Timestamp. 0) + (doto (Timestamp. 1000) (.setNanos 101))) +``` + +If you need to diff a rich set of time literal, using + +``` +(require '[time-literals.read-write]) +(require '[lambdaisland.deep-diff2 :as ddiff]) +(time-literals.read-write/print-time-literals-clj!) +(ddiff/pretty-print (ddiff/diff #time/date "2039-01-01" #time/date-time "2018-07-05T08:08:44.026")) +``` + +## Deep-diff 1 vs 2 + +The original deep-diff only worked on Clojure, not ClojureScript. In porting the +code to CLJC we were forced to make some breaking changes. To not break existing +consumers we decided to move both the namespaces and the released artifact to +new names, so the old and new deep-diff can exist side by side. + +We also had to fork Puget to make it cljc compatible. This required breaking +changes as well, making it unlikely these changes will make it upstream, so +instead we vendor our own copy of Puget under `lambdaisland.deep-diff2.puget.*`. +This does mean we don't automatically pick up custom Puget print handlers, +unless they are *also* registered with our own copy of Puget. See above for more +info on that. + +When starting new projects you should use `lambdaisland/deep-diff2`. However if +you have existing code that uses `lambdaisland/deep-diff` and you don't need the +ClojureScript support then it is not necessary to upgrade. The old version still +works fine (on Clojure). + +You can upgrade of course, simply by replacing all namespace names from +`lambdaisland.deep-diff` to `lambdaisland.deep-diff2`. If you are only using the +top-level API (`diff`, `printer`, `pretty-print`) and you aren't using custom +print handlers, then things should work exactly the same. If you find that +deep-diff 2 behaves differently then please file an issue, you may have found a +regression. + +The old code still lives on the `deep-diff-1` branch, and we do accept bugfix +patches there, so we may put out bugfix releases of the original deep-diff in +the future. When in doubt check the CHANGELOG. + + +## Contributing + +We warmly welcome patches to deep-diff2. Please keep in mind the following: + +- adhere to the [LambdaIsland Clojure Style Guide](https://nextjournal.com/lambdaisland/clojure-style-guide) +- write patches that solve a problem +- start by stating the problem, then supply a minimal solution `*` +- by contributing you agree to license your contributions as EPL 1.0 +- don't break the contract with downstream consumers `**` +- don't break the tests + +We would very much appreciate it if you also + +- update the CHANGELOG and README +- add tests for new functionality + +We recommend opening an issue first, before opening a pull request. That way we +can make sure we agree what the problem is, and discuss how best to solve it. +This is especially true if you add new dependencies, or significantly increase +the API surface. In cases like these we need to decide if these changes are in +line with the project's goals. + +`*` This goes for features too, a feature needs to solve a problem. State the problem it solves first, only then move on to solving it. + +`**` Projects that have a version that starts with `0.` may still see breaking changes, although we also consider the level of community adoption. The more widespread a project is, the less likely we're willing to introduce breakage. See [LambdaIsland-flavored Versioning](https://github.com/lambdaisland/open-source#lambdaisland-flavored-versioning) for more info. + + ## Credits This library builds upon @@ -72,8 +290,10 @@ This library was originally developed as part of the Another library that implements a form of data structure diffing is [editscript](https://github.com/juji-io/editscript). + ## License -Copyright © 2018 Arne Brasseur +Copyright © 2018-2025 Arne Brasseur and contributors Available under the terms of the Eclipse Public License 1.0, see LICENSE.txt + \ No newline at end of file diff --git a/bb.edn b/bb.edn new file mode 100644 index 0000000..17f86ff --- /dev/null +++ b/bb.edn @@ -0,0 +1,10 @@ +{:deps + {lambdaisland/deep-diff2 {:local/root "."} + lambdaisland/open-source {:git/url "https://github.com/lambdaisland/open-source" + :git/sha "34ce20d7b2af747227c345b392fe92cb5f4d3cda"}} + :tasks + {test:bb {:doc "Run babashka tests with custom runner" + :extra-paths ["src" "test"] + :extra-deps {current/project {:local/root "."} + org.clojure/test.check {:mvn/version "1.1.1"}} + :task (exec 'lambdaisland.deep-diff2.runner/run-tests)}}} diff --git a/bin/kaocha b/bin/kaocha new file mode 100755 index 0000000..8ddb6f8 --- /dev/null +++ b/bin/kaocha @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +[[ -d "node_modules/ws" ]] || npm install ws + +exec clojure -A:dev:test -M -m kaocha.runner "$@" diff --git a/bin/proj b/bin/proj new file mode 100755 index 0000000..b0582aa --- /dev/null +++ b/bin/proj @@ -0,0 +1,14 @@ +#!/usr/bin/env bb + +(ns proj (:require [lioss.main :as lioss])) + +(lioss/main + {:license :epl + :group-id "lambdaisland" + :inception-year 2018 + :description "Recursively compare Clojure or ClojureScript data structures, and produce a colorized diff of the result."}) + + +;; Local Variables: +;; mode:clojure +;; End: diff --git a/color-scheme.png b/color-scheme.png new file mode 100644 index 0000000..ebaf500 Binary files /dev/null and b/color-scheme.png differ diff --git a/deps.edn b/deps.edn index 55f5500..b8c7832 100644 --- a/deps.edn +++ b/deps.edn @@ -1,15 +1,24 @@ -{:paths ["src" "test"] - :deps {org.clojure/clojure {:mvn/version "1.10.0"} - mvxcvi/puget {:mvn/version "1.1.2"} - fipp {:mvn/version "0.6.17"} - org.clojure/core.rrb-vector {:mvn/version "0.0.14"} - tech.droit/clj-diff {:mvn/version "1.0.1"} - mvxcvi/arrangement {:mvn/version "1.2.0"}} - - :aliases - {:dev - {} - - :test - {:extra-deps {lambdaisland/kaocha {:mvn/version "0.0-413"} - org.clojure/test.check {:mvn/version "0.10.0-alpha4"}}}}} +{:paths ["resources" "src"] + :deps {fipp/fipp {:mvn/version "0.6.29"} + org.clojure/core.rrb-vector {:mvn/version "0.2.1"} + lambdaisland/clj-diff {:mvn/version "1.4.78"} + mvxcvi/arrangement {:mvn/version "2.1.0"}} + + :aliases {:cljs + {:extra-deps {org.clojure/clojurescript {:mvn/version "1.12.134"}}} + + :dev + {} + + :chui + {:extra-deps {lambdaisland/chui {:local/root "../chui"} + thheller/shadow-cljs {:mvn/version "3.3.8"} + garden/garden {:mvn/version "1.3.10"}} + :extra-paths ["../chui/resources" "../chui/dev"]} + + :test + {:extra-paths ["test"] + :extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"} + com.lambdaisland/kaocha-cljs {:mvn/version "1.9.181"} + org.clojure/clojurescript {:mvn/version "1.12.134"} + org.clojure/test.check {:mvn/version "1.1.3"}}}}} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ac9cdbb --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "chui", + "license": "MPL-2.0", + "dependencies": { + "react": "^16.13.1", + "react-dom": "^16.13.1", + "ws": "^8.20.0" + } +} diff --git a/pom.xml b/pom.xml index 27d3bef..b662a33 100644 --- a/pom.xml +++ b/pom.xml @@ -1,17 +1,20 @@ - + 4.0.0 lambdaisland - deep-diff - 0.0-47 - deep-diff - Recursively diff Clojure data structures. - https://github.com/lambdaisland/deep-diff + deep-diff2 + 2.13.231 + deep-diff2 + Recursively compare Clojure or ClojureScript data structures, and produce a colorized diff of the result. + https://github.com/lambdaisland/deep-diff2 2018 Lambda Island https://lambdaisland.com + + UTF-8 + Eclipse Public License 1.0 @@ -19,63 +22,79 @@ - https://github.com/lambdaisland/deep-diff - scm:git:git://github.com/lambdaisland/deep-diff.git - scm:git:ssh://git@github.com/lambdaisland/deep-diff.git - eb1ce3037540b72635e32c3dfc0fbe588d2e195f + https://github.com/lambdaisland/deep-diff2 + scm:git:git://github.com/lambdaisland/deep-diff2.git + scm:git:ssh://git@github.com/lambdaisland/deep-diff2.git + 25bca3d85b2b4a66083de7ff48920af4c92a99fd - - org.clojure - clojure - 1.10.0 - - - mvxcvi - puget - 1.1.2 - fipp fipp - 0.6.17 + 0.6.29 org.clojure core.rrb-vector - 0.0.14 + 0.2.1 - tech.droit + lambdaisland clj-diff - 1.0.1 + 1.4.78 mvxcvi arrangement - 1.2.0 + 2.1.0 - src + resources + + resources + src + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + org.apache.maven.plugins maven-jar-plugin - 2.4 + 3.2.0 - eb1ce3037540b72635e32c3dfc0fbe588d2e195f + 25bca3d85b2b4a66083de7ff48920af4c92a99fd + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + @@ -91,4 +110,4 @@ https://clojars.org/repo - + \ No newline at end of file diff --git a/repl_sessions/custom_types.clj b/repl_sessions/custom_types.clj new file mode 100644 index 0000000..3830ad2 --- /dev/null +++ b/repl_sessions/custom_types.clj @@ -0,0 +1,59 @@ +(ns repl-sessions.custom-types + (:require [lambdaisland.deep-diff2 :as ddiff])) + +;; Demonstration of how to set up print handlers for custom types. + +;; New custom type for reprsenting "degrees" amount, like Celcius or Fahrenheit. +;; Using `deftype` and not `defrecord` because we handle `defrecord` instances +;; already as if they are maps. + +(deftype Degrees [amount unit] + Object + (equals [this that] ; needed for proper diffing + (and (instance? Degrees that) + (= amount (.-amount that)) + (= unit (.-unit that))))) + +;; No custom handler yet, defaults to Object#toString rendering: + +(pr-str (->Degrees 10 "C")) +;; => "#object[custom_types.Degrees 0x75634af8 \"custom_types.Degrees@75634af8\"]" + +;; And so does ddiff +(ddiff/pretty-print (ddiff/diff [(->Degrees 20 \C)] + [(->Degrees 80 \F)])) +;; => +;; [-#object[custom_types.Degrees 0x660a955 "custom_types.Degrees@660a955"] +;; +#object[custom_types.Degrees 0x40241557 "custom_types.Degrees@40241557"]] + + +;; Now we set up a custom handler + +(defmethod print-method Degrees [degrees out] + (.write out (str (.-amount degrees) "°" (.-unit degrees)))) + + +(pr-str (->Degrees 10 "C")) +;; => "10°C" + +(ddiff/pretty-print (ddiff/diff [(->Degrees 20 \C)] + [(->Degrees 20 \C) + (->Degrees 80 \F)])) +;; => [-20°C +80°F] + +;; Add Puget handler, to tap into Puget's rich rendering. Will take precedence +;; over `print-method`. + +(lambdaisland.deep-diff2.printer-impl/register-print-handler! + `Degrees + (fn [printer value] + [:span + (lambdaisland.deep-diff2.puget.color/document printer :number (str (.-amount value))) + (lambdaisland.deep-diff2.puget.color/document printer :tag "°") + (lambdaisland.deep-diff2.puget.color/document printer :keyword (str (.-unit value)))])) + +(ddiff/pretty-print (->Degrees 20 \C)) +;; => 20°C (printed with specific colors) + +(ddiff/pretty-print (->Degrees 20 \C) (ddiff/printer {:color-markup :html-inline})) +;;=> 20°C diff --git a/repl_sessions/poke.clj b/repl_sessions/poke.clj new file mode 100644 index 0000000..11ea252 --- /dev/null +++ b/repl_sessions/poke.clj @@ -0,0 +1,62 @@ +(ns repl-sessions.poke + (:require [lambdaisland.deep-diff2 :as ddiff])) + +(seq #{{:foo 1M} {:bar 2}}) ;; => ({:foo 1M} {:bar 2}) +(seq #{{:foo 1} {:bar 2}}) ;; => ({:bar 2} {:foo 1}) + +(def d1 {{:foo 1M} {:bar 2}}) +(def d2 {{:foo 1} {:bar 2}}) +(ddiff/pretty-print (ddiff/diff d1 d2)) +;; #{+{:foo 1} -{:foo 1M} {:bar 2}} + +(def d1 #{{:foo 1M}}) +(def d2 #{{:foo 1}}) +(ddiff/pretty-print (ddiff/diff d1 d2)) + +(-> (ddiff/diff {:a "apple" :b "pear"} {:a "apple" :b "banana"}) + ddiff/minimize + ddiff/pretty-print) +;; {:b -"pear" +"banana"} + +;; {:b -2 +3} + +[#{1.1197369622161879e-14 1.3019822841584656e-21 0.6875 + #uuid "a907a7fe-d2eb-482d-b1cc-3acfc12daf55" + -30 + :X/*!1:3 + :u7*A/p?2IG5d*!Nl + :**d7ws + "ý" + "ÔB*àñS�¬ÚûV¡ç�¯±·á£H� + �û?'V$ëY;CL�k-oOV" + !U-h_C*A7/x0_n1 + A-*wn./o_?4w18-! + "ìêܼà4�^¤mÐðkt�ê1_ò�· À�4\n@J\"2�9)cd-\t®" + y3W-2 + #uuid "6d507164-f8b9-401d-8c44-d6b0e310c248" + "M" + :cy7-3 + :w4/R.-s?9V5 + #uuid "1bcb00c9-88b9-4eae-9fea-60600dfaefa0" + -20 + #uuid "269ab6f9-f19d-4c9d-a0cb-51150e52e9f7" + -235024979 + :O:m_9.9+A/N+usPa6.HA*G + 228944.657438457 + :x/w? + :__+o+sut9!t/?0l + "�â��«" + false + #uuid "b6295f83-8176-47b5-946e-466f74226629" + e3zQ!E*5 + :T5rb + :++y:2 + -7364 + zG/ex23 + "¡" + -4318364480 + :D+?2?!/Hrc!jA7z_2 + :z-I/!8Uq+d? + -0.5588235294117647 + -0.5925925925925926 + -0.8108108108108109}] diff --git a/repl_sessions/str_rep.clj b/repl_sessions/str_rep.clj new file mode 100644 index 0000000..081a98c --- /dev/null +++ b/repl_sessions/str_rep.clj @@ -0,0 +1,34 @@ +(ns str-rep + (:require [clojure.string :as str])) + +(defn left-pad [s len pad] + (concat (repeat (- len (count s)) pad) s)) + +(defn int->hex [i] + (str/upper-case + (Integer/toHexString i))) + +(defn unicode-rep [char] + (apply str "\\u" (left-pad (int->hex (long char)) 4 \0))) + +(defn char-rep [char] + (cond + (= \backspace char) + "\\b" + (= \tab char) + "\\t" + (= \newline char) + "\\n" + (= \formfeed char) + "\\f" + (= \return char) + "\\r" + (< (long char) 32) + (unicode-rep char) + :else + (str char))) + +(defn str-rep [s] + (str "\"" + (apply str (map char-rep s)) + "\"")) diff --git a/screenshot.png b/screenshot.png index e00e67e..668fe08 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/shadow-cljs.edn b/shadow-cljs.edn new file mode 100644 index 0000000..85c270b --- /dev/null +++ b/shadow-cljs.edn @@ -0,0 +1,17 @@ +{:deps + {:aliases [:dev :chui]} + + :dev-http + {8012 "classpath:public"} + + :builds + {:main + {:target :browser-test + :runner-ns lambdaisland.chui.shadowrun + :test-dir "resources/public" + :asset-path "/ui" + :ns-regexp "-test$" + :devtools {:repl-pprint true}}} + + :cache-blockers #{lambdaisland.chui.styles + lambdaisland.chui.test-data}} diff --git a/src/lambdaisland/deep_diff/diff.clj b/src/lambdaisland/deep_diff/diff.clj deleted file mode 100644 index cf70cf2..0000000 --- a/src/lambdaisland/deep_diff/diff.clj +++ /dev/null @@ -1,196 +0,0 @@ -(ns lambdaisland.deep-diff.diff - (:require [clojure.data :as data] - [clj-diff.core :as seq-diff])) - -(declare diff) - -(defrecord Mismatch [- +]) -(defrecord Deletion [-]) -(defrecord Insertion [+]) - -(defprotocol Diff - (diff-similar [x y])) - -;; For property based testing -(defprotocol Undiff - (left-undiff [x]) - (right-undiff [x])) - -(defn- shift-insertions [ins] - (reduce (fn [res idx] - (let [offset (apply + (map count (vals res)))] - (assoc res (+ idx offset) (get ins idx)))) - {} - (sort (keys ins)))) - -(defn- replacements - "Given a set of deletion indexes and a map of insertion index to value sequence, - match up deletions and insertions into replacements, returning a map of - replacements, a set of deletions, and a map of insertions." - [[del ins]] - ;; Loop over deletions, if they match up with an insertion, turn them into a - ;; replacement. This could be a reduce over (sort del) tbh but it's already a - ;; lot more readable than the first version. - (loop [rep {} - del del - del-rest (sort del) - ins ins] - (if-let [d (first del-rest)] - (if-let [i (seq (get ins d))] ;; matching insertion - (recur (assoc rep d (first i)) - (disj del d) - (next del-rest) - (update ins d next)) - - (if-let [i (seq (get ins (dec d)))] - (recur (assoc rep d (first i)) - (disj del d) - (next del-rest) - (-> ins - (dissoc (dec d)) - (assoc d (seq (concat (next i) - (get ins d)))))) - (recur rep - del - (next del-rest) - ins))) - [rep del (into {} - (remove (comp nil? val)) - (shift-insertions ins))]))) - -(defn- del+ins - "Wrapper around clj-diff that returns deletions and insertions as a set and map - respectively." - [exp act] - (let [{del :- ins :+} (seq-diff/diff exp act)] - [(into #{} del) - (into {} (map (fn [[k & vs]] [k (vec vs)])) ins)])) - -(defn- diff-seq-replacements [replacements s] - (map-indexed - (fn [idx v] - (if (contains? replacements idx) - (diff v (get replacements idx)) - v)) - s)) - -(defn- diff-seq-deletions [del s] - (map - (fn [v idx] - (if (contains? del idx) - (->Deletion v) - v)) - s - (range))) - -(defn- diff-seq-insertions [ins s] - (reduce (fn [res [idx vs]] - (concat (take (inc idx) res) (map ->Insertion vs) (drop (inc idx) res))) - s - ins)) - -(defn- diff-seq [exp act] - (let [[rep del ins] (replacements (del+ins exp act))] - (->> exp - (diff-seq-replacements rep) - (diff-seq-deletions del) - (diff-seq-insertions ins) - (into [])))) - -(defn- val-type [val] - (let [t (type val)] - (if (class? t) - (symbol (.getName ^Class t)) - t))) - -(defn- diff-map [exp act] - (first - (let [exp-ks (keys exp) - act-ks (concat (filter (set (keys act)) exp-ks) - (remove (set exp-ks) (keys act))) - [del ins] (del+ins exp-ks act-ks)] - (reduce - (fn [[m idx] k] - [(cond-> m - (contains? del idx) - (assoc (->Deletion k) (exp k)) - - (not (contains? del idx)) - (assoc k (diff (get exp k) (get act k))) - - (contains? ins idx) - (into (map (juxt ->Insertion (partial get act))) (get ins idx))) - (inc idx)]) - [(if (contains? ins -1) - (into {} (map (juxt ->Insertion (partial get act))) (get ins -1)) - {}) 0] - exp-ks)))) - -(defn- diff-atom [exp act] - (if (= exp act) - exp - (->Mismatch exp act))) - -(defn diff [exp act] - (if (= (data/equality-partition exp) (data/equality-partition act)) - (diff-similar exp act) - (diff-atom exp act))) - -(extend nil - Diff - {:diff-similar diff-atom}) - -(extend Object - Diff - {:diff-similar (fn [exp act] - (if (.isArray (.getClass ^Object exp)) - (diff-seq exp act) - (diff-atom exp act)))}) - -(extend-protocol Diff - java.util.List - (diff-similar [exp act] (diff-seq exp act)) - - java.util.Set - (diff-similar [exp act] - (let [exp-seq (seq exp) - act-seq (seq act)] - (set (diff-seq exp-seq (concat (filter act exp-seq) - (remove exp act-seq)))))) - - java.util.Map - (diff-similar [exp act] (diff-map exp act))) - -(extend-protocol Undiff - java.util.List - (left-undiff [s] (map left-undiff (remove #(instance? Insertion %) s))) - (right-undiff [s] (map right-undiff (remove #(instance? Deletion %) s))) - - java.util.Set - (left-undiff [s] (set (left-undiff (seq s)))) - (right-undiff [s] (set (right-undiff (seq s)))) - - java.util.Map - (left-undiff [m] - (into {} - (comp (remove #(instance? Insertion (key %))) - (map (juxt (comp left-undiff key) (comp left-undiff val)))) - m)) - (right-undiff [m] - (into {} - (comp (remove #(instance? Deletion (key %))) - (map (juxt (comp right-undiff key) (comp right-undiff val)))) - m)) - - Mismatch - (left-undiff [m] (get m :-)) - (right-undiff [m] (get m :+)) - - Insertion - (right-undiff [m] (get m :+)) - - Deletion - (left-undiff [m] (get m :-))) - -(extend nil Undiff {:left-undiff identity :right-undiff identity}) -(extend Object Undiff {:left-undiff identity :right-undiff identity}) diff --git a/src/lambdaisland/deep_diff/printer.clj b/src/lambdaisland/deep_diff/printer.clj deleted file mode 100644 index 50bd5ad..0000000 --- a/src/lambdaisland/deep_diff/printer.clj +++ /dev/null @@ -1,159 +0,0 @@ -(ns lambdaisland.deep-diff.printer - (:require [fipp.engine :as fipp] - [fipp.visit :as fv] - [puget.color :as color] - [puget.dispatch] - [puget.printer :as puget] - [arrangement.core] - [lambdaisland.deep-diff.diff :as diff]) - (:import (java.text SimpleDateFormat) - (java.util TimeZone) - (java.sql Timestamp))) - -(defn print-deletion [printer expr] - (let [no-color (assoc printer :print-color false)] - (color/document printer ::deletion [:span "-" (puget/format-doc no-color (:- expr))]))) - -(defn print-insertion [printer expr] - (let [no-color (assoc printer :print-color false)] - (color/document printer ::insertion [:span "+" (puget/format-doc no-color (:+ expr))]))) - -(defn print-mismatch [printer expr] - [:group - [:span ""] ;; needed here to make this :nest properly in kaocha.report/print-expr '= - [:align - (print-deletion printer expr) :line - (print-insertion printer expr)]]) - -(defn print-other [printer expr] - (let [no-color (assoc printer :print-color false)] - (color/document printer ::other [:span "-" (puget/format-doc no-color expr)]))) - -(defn- map-handler [this value] - (let [ks (#'puget/order-collection (:sort-keys this) value (partial sort-by first arrangement.core/rank)) - entries (map (partial puget/format-doc this) ks)] - [:group - (color/document this :delimiter "{") - [:align (interpose [:span (:map-delimiter this) :line] entries)] - (color/document this :delimiter "}")])) - -(def ^:private ^ThreadLocal thread-local-utc-date-format - (proxy [ThreadLocal] [] - (initialValue [] - (doto (SimpleDateFormat. "yyyy-MM-dd'T'HH:mm:ss.SSS-00:00") - (.setTimeZone (TimeZone/getTimeZone "GMT")))))) - -(def ^:private print-date - (puget/tagged-handler - 'inst - #(.format ^SimpleDateFormat (.get thread-local-utc-date-format) %))) - -(def ^:private ^ThreadLocal thread-local-utc-timestamp-format - (proxy [ThreadLocal] [] - (initialValue [] - (doto (SimpleDateFormat. "yyyy-MM-dd'T'HH:mm:ss") - (.setTimeZone (TimeZone/getTimeZone "GMT")))))) - -(def ^:private print-timestamp - (puget/tagged-handler - 'inst - #(str (.format ^SimpleDateFormat (.get thread-local-utc-timestamp-format) %) - (format ".%09d-00:00" (.getNanos ^Timestamp %))))) - -(def ^:private print-calendar - (puget/tagged-handler - 'inst - #(let [formatted (format "%1$tFT%1$tT.%1$tL%1$tz" %) - offset-minutes (- (.length formatted) 2)] - (str (subs formatted 0 offset-minutes) - ":" - (subs formatted offset-minutes))))) - -(def ^:private print-handlers - {'lambdaisland.deep_diff.diff.Deletion - print-deletion - - 'lambdaisland.deep_diff.diff.Insertion - print-insertion - - 'lambdaisland.deep_diff.diff.Mismatch - print-mismatch - - 'clojure.lang.PersistentArrayMap - map-handler - - 'clojure.lang.PersistentHashMap - map-handler - - 'clojure.lang.MapEntry - (fn [printer value] - (let [k (key value) - v (val value)] - (let [no-color (assoc printer :print-color false)] - (cond - (instance? lambdaisland.deep_diff.diff.Insertion k) - [:span - (print-insertion printer k) - (if (coll? v) (:map-coll-separator printer) " ") - (color/document printer ::insertion (puget/format-doc no-color v))] - - (instance? lambdaisland.deep_diff.diff.Deletion k) - [:span - (print-deletion printer k) - (if (coll? v) (:map-coll-separator printer) " ") - (color/document printer ::deletion (puget/format-doc no-color v))] - - :else - [:span - (puget/format-doc printer k) - (if (coll? v) (:map-coll-separator printer) " ") - (puget/format-doc printer v)])))) - - 'java.util.Date - print-date - - 'java.util.GregorianCalendar - print-calendar - - 'java.sql.Timestamp - print-timestamp - - 'java.util.UUID - (puget/tagged-handler 'uuid str)}) - -(defn- print-handler-resolver [extra-handlers] - (fn [^Class klz] - (and klz (get (merge @#'print-handlers extra-handlers) - (symbol (.getName klz)))))) - -(defn register-print-handler! - "Register an extra print handler. - - `type` must be a symbol of the fully qualified class name. `handler` is a - Puget handler function of two arguments, `printer` and `value`." - [type handler] - (alter-var-root #'print-handlers assoc type handler)) - -(defn puget-printer - ([] - (puget-printer {})) - ([opts] - (let [extra-handlers (:extra-handlers opts)] - (puget/pretty-printer (merge {:width (or *print-length* 100) - :print-color true - :color-scheme {::deletion [:red] - ::insertion [:green] - ::other [:yellow] - ;; puget uses green and red for - ;; boolean/tag, but we want to reserve - ;; those for diffed values. - :boolean [:bold :cyan] - :tag [:magenta]} - :print-handlers (print-handler-resolver extra-handlers)} - (dissoc opts :extra-handlers)))))) - -(defn format-doc [expr printer] - (puget/format-doc printer expr)) - -(defn print-doc [doc printer] - (fipp.engine/pprint-document doc {:width (:width printer)})) diff --git a/src/lambdaisland/deep_diff.clj b/src/lambdaisland/deep_diff2.cljc similarity index 60% rename from src/lambdaisland/deep_diff.clj rename to src/lambdaisland/deep_diff2.cljc index fa27d50..83ffa78 100644 --- a/src/lambdaisland/deep_diff.clj +++ b/src/lambdaisland/deep_diff2.cljc @@ -1,6 +1,9 @@ -(ns lambdaisland.deep-diff - (:require [lambdaisland.deep-diff.diff :as diff] - [lambdaisland.deep-diff.printer :as printer])) +(ns lambdaisland.deep-diff2 + "Diff datastructures deeply, and pretty-print the result" + (:require + [lambdaisland.deep-diff2.diff-impl :as diff-impl] + [lambdaisland.deep-diff2.minimise-impl :as minimise] + [lambdaisland.deep-diff2.printer-impl :as printer-impl])) (defn diff "Compare two values recursively. @@ -17,7 +20,7 @@ Insertions/Deletions in maps are marked by wrapping the key, even though the change applies to the whole map entry." [expected actual] - (diff/diff expected actual)) + (diff-impl/diff expected actual)) (defn printer "Construct a Puget printer instance suitable for printing diffs. @@ -26,9 +29,9 @@ `:extra-handlers` (a map from symbol to function), or by using [[lambdaisland.deep-diff.printer/register-print-handler!]]" ([] - (printer {})) + (printer {:print-fallback :print})) ([opts] - (printer/puget-printer opts))) + (printer-impl/puget-printer opts))) (defn pretty-print "Pretty print a diff. @@ -39,5 +42,15 @@ (pretty-print diff (printer))) ([diff printer] (-> diff - (printer/format-doc printer) - (printer/print-doc printer)))) + (printer-impl/format-doc printer) + (printer-impl/print-doc printer)))) + +(defn minimise + "Return a minimal diff, removing any values that haven't changed." + [diff] + (minimise/minimise diff)) + +(defn minimize + "Return a minimal diff, removing any values that haven't changed." + [diff] + (minimise/minimise diff)) diff --git a/src/lambdaisland/deep_diff2/diff_impl.cljc b/src/lambdaisland/deep_diff2/diff_impl.cljc new file mode 100644 index 0000000..b0d18b6 --- /dev/null +++ b/src/lambdaisland/deep_diff2/diff_impl.cljc @@ -0,0 +1,317 @@ +(ns lambdaisland.deep-diff2.diff-impl + (:require + [clojure.data :as data] + [clojure.set :as set] + [lambdaisland.clj-diff.core :as seq-diff])) + +(declare diff diff-similar diff-meta) + +(defrecord Mismatch [- +]) +(defrecord Deletion [-]) +(defrecord Insertion [+]) + +(defprotocol Diff + (-diff-similar [x y])) + +;; For property based testing +(defprotocol Undiff + (left-undiff [x]) + (right-undiff [x])) + +(defn shift-insertions [ins] + (reduce (fn [res idx] + (let [offset (apply + (map count (vals res)))] + (assoc res (+ idx offset) (get ins idx)))) + {} + (sort (keys ins)))) + +(defn replacements + "Given a set of deletion indexes and a map of insertion index to value sequence, + match up deletions and insertions into replacements, returning a map of + replacements, a set of deletions, and a map of insertions." + [[del ins]] + ;; Loop over deletions, if they match up with an insertion, turn them into a + ;; replacement. This could be a reduce over (sort del) tbh but it's already a + ;; lot more readable than the first version. + (loop [rep {} + del del + del-rest (sort del) + ins ins] + (if-let [d (first del-rest)] + (if-let [i (seq (get ins d))] ;; matching insertion + (recur (assoc rep d (first i)) + (disj del d) + (next del-rest) + (update ins d next)) + + (if-let [i (seq (get ins (dec d)))] + (recur (assoc rep d (first i)) + (disj del d) + (next del-rest) + (-> ins + (dissoc (dec d)) + (assoc d (seq (concat (next i) + (get ins d)))))) + (recur rep + del + (next del-rest) + ins))) + [rep del (into {} + (remove (comp nil? val)) + (shift-insertions ins))]))) + +(defn del+ins + "Wrapper around clj-diff that returns deletions and insertions as a set and map + respectively." + [exp act] + (let [{del :- ins :+} (seq-diff/diff exp act)] + [(into #{} del) + (into {} (map (fn [[k & vs]] [k (vec vs)])) ins)])) + +(defn diff-seq-replacements [replacements s] + (map-indexed + (fn [idx v] + (if (contains? replacements idx) + (diff v (get replacements idx)) + v)) + s)) + +(defn diff-seq-deletions [del s] + (map + (fn [v idx] + (if (contains? del idx) + (->Deletion v) + v)) + s + (range))) + +(defn diff-seq-insertions [ins s] + (reduce (fn [res [idx vs]] + (concat (take (inc idx) res) (map ->Insertion vs) (drop (inc idx) res))) + s + ins)) + +(defn diff-seq [exp act] + (let [[rep del ins] (replacements (del+ins exp act))] + (with-meta + (->> exp + (diff-seq-replacements rep) + (diff-seq-deletions del) + (diff-seq-insertions ins) + (into [])) + (diff-meta exp act)))) + +(defn diff-set [exp act] + (with-meta + (into + (into #{} + (map (fn [e] + (if (contains? act e) + e + (->Deletion e)))) + exp) + (map ->Insertion) + (remove #(contains? exp %) act)) + (diff-meta exp act))) + +(defn diff-map [exp act] + (if (not= (record? exp) (record? act)) + ;; If one of them is a record, and the other one a plain map, that's a + ;; mismatch. The case where both of them are records, but of different + ;; types, is handled in [[diff]] + (->Mismatch exp act) + (with-meta + (let [exp-ks (set (keys exp)) + act-ks (set (keys act))] + (reduce + (fn [m k] + (case [(contains? exp-ks k) (contains? act-ks k)] + [true false] + ;; The `dissoc` is only relevant for records, which at this point + ;; we are certain are of the same type. If the key is present in + ;; one and not in the other, we know it's an optional key (not part + ;; of the record base), and we can safely `dissoc` it while + ;; retaining the record type. + (assoc (dissoc m k) (->Deletion k) (get exp k)) + [false true] + (assoc m (->Insertion k) (get act k)) + [true true] + (assoc m k (diff (get exp k) (get act k))) + ;; `[false false]` will never occur because `k` necessarily + ;; originated from at least one of the two sets + )) + ;; In case of a record, we want to preserve the type, and you can't + ;; call `empty` on records, so we start from `exp` and assoc/dissoc. + (if (record? exp) exp {}) + (set/union exp-ks act-ks))) + (diff-meta exp act)))) + +(defn diff-meta [exp act] + (when (or (meta exp) (meta act)) + (diff-map (meta exp) (meta act)))) + +(defn primitive? [x] + (or (number? x) (string? x) (boolean? x) (inst? x) (keyword? x) (symbol? x))) + +(defn diff-atom [exp act] + (if (= exp act) + exp + (->Mismatch exp act))) + +(defn diff-similar [x y] + (if (primitive? x) + (diff-atom x y) + (-diff-similar x y))) + +(defn diffable? [exp] + (satisfies? Diff exp)) + +;; ClojureScript has this, Clojure doesn't +#?(:clj + (defn array? [x] + (and x (.isArray (class x))))) + +(defn diff [exp act] + (cond + (= exp act) + exp + + (nil? exp) + (diff-atom exp act) + + (record? exp) + (if (= (type exp) (type act)) + (diff-map exp act) + ;; Either act is not a record, or it's a record of a different type, so + ;; that's a mismatch + (->Mismatch exp act)) + + (and (diffable? exp) + (= (data/equality-partition exp) (data/equality-partition act))) + (diff-similar exp act) + + (array? exp) + (diff-seq exp act) + + :else + (diff-atom exp act))) + +(extend-protocol Diff + #?(:clj java.util.Set :cljs cljs.core/PersistentHashSet) + (-diff-similar [exp act] + (diff-set exp act)) + #?@(:clj + [java.util.List + (-diff-similar [exp act] (diff-seq exp act)) + + java.util.Map + (-diff-similar [exp act] (diff-map exp act))] + + :cljs + [cljs.core/List + (-diff-similar [exp act] (diff-seq exp act)) + + cljs.core/PersistentVector + (-diff-similar [exp act] (diff-seq exp act)) + + cljs.core/EmptyList + (-diff-similar [exp act] (diff-seq exp act)) + + cljs.core/PersistentHashMap + (-diff-similar [exp act] (diff-map exp act)) + + cljs.core/PersistentArrayMap + (-diff-similar [exp act] (diff-map exp act))])) + +(extend-protocol Undiff + Mismatch + (left-undiff [m] (get m :-)) + (right-undiff [m] (get m :+)) + + Insertion + (right-undiff [m] (get m :+)) + + Deletion + (left-undiff [m] (get m :-)) + + nil + (left-undiff [m] m) + (right-undiff [m] m) + + #?(:clj Object :cljs default) + (left-undiff [m] m) + (right-undiff [m] m) + + #?@(:clj + [java.util.List + (left-undiff [s] (map left-undiff (remove #(instance? Insertion %) s))) + (right-undiff [s] (map right-undiff (remove #(instance? Deletion %) s))) + + java.util.Set + (left-undiff [s] (set (left-undiff (seq s)))) + (right-undiff [s] (set (right-undiff (seq s)))) + + java.util.Map + (left-undiff [m] + (into {} + (comp (remove #(instance? Insertion (key %))) + (map (juxt (comp left-undiff key) (comp left-undiff val)))) + m)) + (right-undiff [m] + (into {} + (comp (remove #(instance? Deletion (key %))) + (map (juxt (comp right-undiff key) (comp right-undiff val)))) + m))] + + :cljs + [cljs.core/List + (left-undiff [s] (map left-undiff (remove #(instance? Insertion %) s))) + (right-undiff [s] (map right-undiff (remove #(instance? Deletion %) s))) + + cljs.core/EmptyList + (left-undiff [s] (map left-undiff (remove #(instance? Insertion %) s))) + (right-undiff [s] (map right-undiff (remove #(instance? Deletion %) s))) + + cljs.core/PersistentHashSet + (left-undiff [s] (set (left-undiff (seq s)))) + (right-undiff [s] (set (right-undiff (seq s)))) + + cljs.core/PersistentTreeSet + (left-undiff [s] (set (left-undiff (seq s)))) + (right-undiff [s] (set (right-undiff (seq s)))) + + cljs.core/PersistentVector + (left-undiff [s] (map left-undiff (remove #(instance? Insertion %) s))) + (right-undiff [s] (map right-undiff (remove #(instance? Deletion %) s))) + + cljs.core/KeySeq + (left-undiff [s] (map left-undiff (remove #(instance? Insertion %) s))) + (right-undiff [s] (map right-undiff (remove #(instance? Deletion %) s))) + + cljs.core/PersistentArrayMap + (left-undiff [m] + (into {} + (comp (remove #(instance? Insertion (key %))) + (map (juxt (comp left-undiff key) (comp left-undiff val)))) + m)) + (right-undiff [m] + (into {} + (comp (remove #(instance? Deletion (key %))) + (map (juxt (comp right-undiff key) (comp right-undiff val)))) + m)) + + cljs.core/PersistentHashMap + (left-undiff [m] + (into {} + (comp (remove #(instance? Insertion (key %))) + (map (juxt (comp left-undiff key) (comp left-undiff val)))) + m)) + (right-undiff [m] + (into {} + (comp (remove #(instance? Deletion (key %))) + (map (juxt (comp right-undiff key) (comp right-undiff val)))) + m)) + + cljs.core/UUID + (left-undiff [m] m) + (right-undiff [m] m)])) diff --git a/src/lambdaisland/deep_diff2/minimise_impl.cljc b/src/lambdaisland/deep_diff2/minimise_impl.cljc new file mode 100644 index 0000000..fabcfd8 --- /dev/null +++ b/src/lambdaisland/deep_diff2/minimise_impl.cljc @@ -0,0 +1,53 @@ +(ns lambdaisland.deep-diff2.minimise-impl + "Provide API for manipulate the diff structure data " + (:require + [clojure.walk :refer [postwalk]] + #?(:clj [lambdaisland.deep-diff2.diff-impl] + :cljs [lambdaisland.deep-diff2.diff-impl :refer [Mismatch Deletion Insertion]])) + #?(:clj (:import [lambdaisland.deep_diff2.diff_impl Mismatch Deletion Insertion]))) + +(defn diff-item? + "Checks if x is a Mismatch, Deletion, or Insertion" + [x] + (or (instance? Mismatch x) + (instance? Deletion x) + (instance? Insertion x))) + +(defn has-diff-item? + "Checks if there are any diff items in x or sub-tree of x" + [x] + (or (diff-item? x) + (and (map? x) (some #(or (has-diff-item? (key %)) + (has-diff-item? (val %))) x)) + (and (coll? x) (some has-diff-item? x)))) + +(defn minimise + "Postwalk diff, removing values that are unchanged" + [o] + (cond + (diff-item? o) + o + + (map-entry? o) + (cond + (has-diff-item? (key o)) + o + (has-diff-item? (val o)) + #?(:clj + (clojure.lang.MapEntry/create (key o) (minimise (val o))) + :cljs + (MapEntry. (key o) (minimise (val o)) nil))) + + (record? o) + (into o (map minimise) o) + + (map? o) + (into {} (keep minimise) o) + + (coll? o) + (if (has-diff-item? o) + (into (empty o) (keep minimise) o) + (empty o)) + + :else + nil)) diff --git a/src/lambdaisland/deep_diff2/printer_impl.cljc b/src/lambdaisland/deep_diff2/printer_impl.cljc new file mode 100644 index 0000000..959423c --- /dev/null +++ b/src/lambdaisland/deep_diff2/printer_impl.cljc @@ -0,0 +1,167 @@ +(ns lambdaisland.deep-diff2.printer-impl + (:require + [arrangement.core] + [fipp.engine :as fipp] + [lambdaisland.deep-diff2.diff-impl :as diff] + [lambdaisland.deep-diff2.puget.color :as color] + [lambdaisland.deep-diff2.puget.dispatch :as dispatch] + [lambdaisland.deep-diff2.puget.printer :as puget-printer] + #?(:cljs [goog.string :refer [format]])) + #?(:clj + (:import))) + +(defn print-deletion [printer expr] + (let [no-color (assoc printer :print-color false)] + (color/document printer ::deletion [:span "-" (puget-printer/format-doc no-color (:- expr))]))) + +(defn print-insertion [printer expr] + (let [no-color (assoc printer :print-color false)] + (color/document printer ::insertion [:span "+" (puget-printer/format-doc no-color (:+ expr))]))) + +(defn print-mismatch [printer expr] + [:group + [:span ""] ;; needed here to make this :nest properly in kaocha.report/print-expr '= + [:align + (print-deletion printer expr) :line + (print-insertion printer expr)]]) + +(defn print-other [printer expr] + (let [no-color (assoc printer :print-color false)] + (color/document printer ::other [:span "-" (puget-printer/format-doc no-color expr)]))) + +(defn- map-handler [this value] + (let [ks (#'puget-printer/order-collection (:sort-keys this) value (partial sort-by first arrangement.core/rank)) + entries (map (partial puget-printer/format-doc this) ks)] + [:group + (color/document this :delimiter "{") + [:align (interpose [:span (:map-delimiter this) :line] entries)] + (color/document this :delimiter "}")])) + +(defn- map-entry-handler [printer value] + (let [k (key value) + v (val value)] + (let [no-color (assoc printer :print-color false)] + (cond + (instance? lambdaisland.deep_diff2.diff_impl.Insertion k) + [:span + (print-insertion printer k) + (if (coll? v) (:map-coll-separator printer) " ") + (color/document printer ::insertion (puget-printer/format-doc no-color v))] + + (instance? lambdaisland.deep_diff2.diff_impl.Deletion k) + [:span + (print-deletion printer k) + (if (coll? v) (:map-coll-separator printer) " ") + (color/document printer ::deletion (puget-printer/format-doc no-color v))] + + :else + [:span + (puget-printer/format-doc printer k) + (if (coll? v) (:map-coll-separator printer) " ") + (puget-printer/format-doc printer v)])))) + +(def print-handlers + (atom #?(:clj + {'lambdaisland.deep_diff2.diff_impl.Deletion + print-deletion + + 'lambdaisland.deep_diff2.diff_impl.Insertion + print-insertion + + 'lambdaisland.deep_diff2.diff_impl.Mismatch + print-mismatch + + 'clojure.lang.PersistentArrayMap + map-handler + + 'clojure.lang.PersistentHashMap + map-handler + + 'clojure.lang.MapEntry + map-entry-handler} + + :cljs + {'lambdaisland.deep-diff2.diff-impl/Deletion + print-deletion + + 'lambdaisland.deep-diff2.diff-impl/Insertion + print-insertion + + 'lambdaisland.deep-diff2.diff-impl/Mismatch + print-mismatch + + 'cljs.core/PersistentArrayMap + map-handler + + 'cljs.core/PersistentHashMap + map-handler + + 'cljs.core/MapEntry + map-entry-handler}))) + +(defn type-name + "Get the type of the given object as a string. For Clojure, gets the name of + the class of the object. For ClojureScript, gets either the `name` attribute + or the protocol name if the `name` attribute doesn't exist." + [x] + #?(:bb + (symbol (str (type x))) + :clj + (symbol (.getName (class x))) + :cljs + (let [t (type x) + n (.-name t)] + (if (empty? n) + (symbol (pr-str t)) + (symbol n))))) + +(defn- print-handler-resolver [extra-handlers] + (fn [obj] + (and obj (get (merge @print-handlers extra-handlers) + (symbol (type-name obj)))))) + +(defn register-print-handler! + "Register an extra print handler. + + `type` must be a symbol of the fully qualified class name. `handler` is a + Puget handler function of two arguments, `printer` and `value`." + [type handler] + (swap! print-handlers assoc type handler)) + +(defn- color-scheme-mapping + "Translates user-friendly keys to internal namespaced keys." + [colors] + (let [mapping {:lambdaisland.deep-diff2/deletion ::deletion + :lambdaisland.deep-diff2/insertion ::insertion + :lambdaisland.deep-diff2/other ::other}] + (reduce-kv (fn [m k v] + (assoc m (get mapping k k) v)) ;; Fallback to original key if not in mapping + {} + colors))) + +(defn puget-printer + ([] + (puget-printer {})) + ([opts] + (let [opts (update opts :color-scheme color-scheme-mapping) + extra-handlers (:extra-handlers opts)] + (puget-printer/pretty-printer (puget-printer/merge-options {:width (or *print-length* 100) + :print-color true + :color-scheme {::deletion [:red] + ::insertion [:green] + ::other [:yellow] + ;; lambdaisland.deep-diff2.puget uses green and red for + ;; boolean/tag, but we want to reserve + ;; those for diffed values. + :boolean [:bold :cyan] + :tag [:magenta]} + :print-handlers (dispatch/chained-lookup + (print-handler-resolver extra-handlers) + puget-printer/common-handlers)} + (dissoc opts :extra-handlers)))))) + +(defn format-doc [expr printer] + (puget-printer/format-doc printer expr)) + +(defn print-doc [doc printer] + (fipp.engine/pprint-document doc {:width (:width printer)})) diff --git a/src/lambdaisland/deep_diff2/puget/color.cljc b/src/lambdaisland/deep_diff2/puget/color.cljc new file mode 100644 index 0000000..52a0fc3 --- /dev/null +++ b/src/lambdaisland/deep_diff2/puget/color.cljc @@ -0,0 +1,50 @@ +(ns lambdaisland.deep-diff2.puget.color + "Coloring multimethods to format text by adding markup. + + #### Color Options + + `:print-color` + + When true, ouptut colored text from print functions. + + `:color-markup` + + - `:ansi` for color terminal text (default) + - `:html-inline` for inline-styled html + - `:html-classes` for html with semantic classes + + `:color-scheme` + + Map of syntax element keywords to color codes. + ") + +;; ## Coloring Multimethods +(defn dispatch + "Dispatches to coloring multimethods. Element should be a key from + the color-scheme map." + [options element text] + (when (:print-color options) + (:color-markup options))) + +(defmulti document + "Constructs a pretty print document, which may be colored if + `:print-color` is true." + #'dispatch) + +(defmulti text + "Produces text colored according to the active color scheme. This is mostly + useful to clients which want to produce output which matches data printed by + Puget, but which is not directly printed by the library. Note that this + function still obeys the `:print-color` option." + #'dispatch) + +;; ## Default Markup +;; The default transformation when there's no markup specified is to return the +;; text unaltered. +(defmethod document nil + [options element text] + text) + +(defmethod text nil + [options element text] + text) diff --git a/src/lambdaisland/deep_diff2/puget/color/ansi.cljc b/src/lambdaisland/deep_diff2/puget/color/ansi.cljc new file mode 100644 index 0000000..9009db6 --- /dev/null +++ b/src/lambdaisland/deep_diff2/puget/color/ansi.cljc @@ -0,0 +1,74 @@ +(ns lambdaisland.deep-diff2.puget.color.ansi + "Coloring implementation that applies ANSI color codes to text designed to be + output to a terminal. + + Use with a `:color-markup` of `:ansi`." + (:require + [clojure.string :as str] + [lambdaisland.deep-diff2.puget.color :as color])) + +(def sgr-code + "Map of symbols to numeric SGR (select graphic rendition) codes." + {:none 0 + :bold 1 + :underline 3 + :blink 5 + :reverse 7 + :hidden 8 + :strike 9 + :black 30 + :red 31 + :green 32 + :yellow 33 + :blue 34 + :magenta 35 + :cyan 36 + :white 37 + :fg-256 38 + :fg-reset 39 + :bg-black 40 + :bg-red 41 + :bg-green 42 + :bg-yellow 43 + :bg-blue 44 + :bg-magenta 45 + :bg-cyan 46 + :bg-white 47 + :bg-256 48 + :bg-reset 49}) + +(defn esc + "Returns an ANSI escope string which will apply the given collection of SGR + codes." + [codes] + (let [codes (map sgr-code codes codes) + codes (str/join \; codes)] + (str \u001b \[ codes \m))) + +(defn escape + "Returns an ANSI escope string which will enact the given SGR codes." + [& codes] + (esc codes)) + +(defn sgr + "Wraps the given string with SGR escapes to apply the given codes, then reset + the graphics." + [string & codes] + (str (esc codes) string (escape :none))) + +(defn strip + "Removes color codes from the given string." + [string] + (str/replace string #"\u001b\[[0-9;]*[mK]" "")) + +(defmethod color/document :ansi + [options element document] + (if-let [codes (-> options :color-scheme (get element) seq)] + [:span [:pass (esc codes)] document [:pass (escape :none)]] + document)) + +(defmethod color/text :ansi + [options element text] + (if-let [codes (-> options :color-scheme (get element) seq)] + (str (esc codes) text (escape :none)) + text)) diff --git a/src/lambdaisland/deep_diff2/puget/color/html.cljc b/src/lambdaisland/deep_diff2/puget/color/html.cljc new file mode 100644 index 0000000..fb06faf --- /dev/null +++ b/src/lambdaisland/deep_diff2/puget/color/html.cljc @@ -0,0 +1,107 @@ +(ns lambdaisland.deep-diff2.puget.color.html + "Coloring implementation that wraps text in HTML tags to apply color. + + Supports the following modes for `:color-markup`: + + - `:html-inline` applies inline `style` attributes to the tags. + - `:html-classes` adds semantic `class` attributes to the tags." + (:require + [clojure.string :as str] + [clojure.walk :refer [postwalk]] + [lambdaisland.deep-diff2.puget.color :as color])) + +(def style-attribute + "Map from keywords usable in a color-scheme value to vectors + representing css style attributes" + {:none nil + :bold [:font-weight "bold"] + :underline [:text-decoration "underline"] + :blink [:text-decoration "blink"] + :reverse nil + :hidden [:visibility "hidden"] + :strike [:text-decoration "line-through"] + :black [:color "black"] + :red [:color "red"] + :green [:color "green"] + :yellow [:color "yellow"] + :blue [:color "blue"] + :magenta [:color "magenta"] + :cyan [:color "cyan"] + :white [:color "white"] + :fg-256 nil + :fg-reset nil + :bg-black [:background-color "black"] + :bg-red [:background-color "red"] + :bg-green [:background-color "green"] + :bg-yellow [:background-color "yellow"] + :bg-blue [:background-color "blue"] + :bg-magenta [:background-color "magenta"] + :bg-cyan [:background-color "cyan"] + :bg-white [:background-color "white"] + :bg-256 nil + :bg-reset nil}) + +(defn style + "Returns a formatted style attribute for a span given a seq of + keywords usable in a :color-scheme value" + [codes] + (let [attributes (map #(get style-attribute % [:color (name %)]) codes)] + (str "style=\"" + (str/join ";" (map (fn [[k v]] (str (name k) ":" v)) attributes)) + "\""))) + +(defn escape-html-text + "Escapes special characters into HTML entities." + [text] + (str/escape text {\& "&" \< "<" \> ">" \" """})) + +(defn escape-html-node + "Applies HTML escaping to the node if it is a string. Returns a print + document representing the escaped string, or the original node if not." + [node] + (if (string? node) + (let [escaped-text (escape-html-text node) + spans (str/split escaped-text #"(?=&)")] + (reduce (fn [acc span] + (case (first span) + nil acc + \& (let [semicolon-pos ((fnil inc 0) (str/index-of span \;)) + escaped (subs span 0 semicolon-pos) + span (subs span semicolon-pos) + acc (conj acc [:escaped escaped])] + (if (seq span) + (conj acc span) + acc)) + (conj acc span))) + [:span] + spans)) + node)) + +(defn escape-html-document + "Escapes special characters into fipp :span/:escaped nodes" + [document] + (postwalk escape-html-node document)) + +(defmethod color/document :html-inline + [options element document] + (if-let [codes (-> options :color-scheme (get element) seq)] + [:span [:pass ""] + (escape-html-document document) + [:pass ""]] + (escape-html-document document))) + +(defmethod color/text :html-inline + [options element text] + (if-let [codes (-> options :color-scheme (get element) seq)] + (str "" (escape-html-text text) "") + (escape-html-text text))) + +(defmethod color/document :html-classes + [options element document] + [:span [:pass ""] + (escape-html-document document) + [:pass ""]]) + +(defmethod color/text :html-classes + [options element text] + (str "" (escape-html-text text) "")) diff --git a/src/lambdaisland/deep_diff2/puget/dispatch.cljc b/src/lambdaisland/deep_diff2/puget/dispatch.cljc new file mode 100644 index 0000000..b02c108 --- /dev/null +++ b/src/lambdaisland/deep_diff2/puget/dispatch.cljc @@ -0,0 +1,114 @@ +(ns lambdaisland.deep-diff2.puget.dispatch + "Dispatch functions take a `Class` argument and return the looked-up value. + This provides similar functionality to Clojure's protocols, but operates over + locally-constructed logic rather than using a global dispatch table. + + A simple example is a map from classes to values, which can be used directly + as a lookup function." + (:require [clojure.string :as str])) + +;; ## Logical Dispatch +(defn chained-lookup + "Builds a dispatcher which looks up a type by checking multiple dispatchers + in order until a matching entry is found. Takes either a single collection of + dispatchers or a variable list of dispatcher arguments. Ignores nil + dispatchers in the sequence." + ([dispatchers] + {:pre [(sequential? dispatchers)]} + (let [candidates (remove nil? dispatchers) + no-chain-lookup-provided-message "chained-lookup must be provided at least one dispatch function to try."] + (when (empty? candidates) + (throw (ex-info no-chain-lookup-provided-message + {:causes #{:no-chained-lookup-provided}}))) + (if (= 1 (count candidates)) + (first candidates) + (fn lookup + [t] + (some #(% t) candidates))))) + ([a b & more] + (chained-lookup (list* a b more)))) + +(defn caching-lookup + "Builds a dispatcher which caches values returned for each type. This improves + performance when the underlying dispatcher may need to perform complex + lookup logic to determine the dispatched value." + [dispatch] + (let [cache (atom {})] + (fn lookup + [t] + (let [memory @cache] + (if (contains? memory t) + (get memory t) + (let [v (dispatch t)] + (swap! cache assoc t v) + v)))))) + +;; Space for predicate-lookup. ClojureScript support +#?(:cljs + (defn predicate-lookup + "Look up a handler for a value based on a map from predicate to handler" + [types] + (fn lookup [value] + (some (fn [[pred? handler]] + (when (pred? value) + handler)) + types)))) + +;; ## Type Dispatch (Clojure) +#?(:clj + (defn symbolic-lookup + "Builds a dispatcher which looks up a type by checking the underlying lookup + using the type's _symbolic_ name, rather than the class value itself. This is + useful for checking configuration that must be created in situations where the + classes themselves may not be loaded yet." + [dispatch] + (fn lookup + [^Class t] + (dispatch (symbol (.getName t)))))) + +#?(:clj + (defn- lineage + "Returns the ancestry of the given class, starting with the class and + excluding the `java.lang.Object` base class." + [cls] + (take-while #(and (some? %) (not= Object %)) + (iterate #(when (class? %) (.getSuperclass ^Class %)) cls)))) + +#?(:clj + (defn- find-interfaces + "Resolves all of the interfaces implemented by a class, both direct (through + class ancestors) and indirect (through other interfaces)." + [cls] + (let [get-interfaces (fn [^Class c] (.getInterfaces c)) + direct-interfaces (mapcat get-interfaces (lineage cls))] + (loop [queue (vec direct-interfaces) + interfaces #{}] + (if (empty? queue) + interfaces + (let [^Class iface (first queue) + implemented (get-interfaces iface)] + (recur (into (rest queue) + (remove interfaces implemented)) + (conj interfaces iface)))))))) + +#?(:clj + (defn inheritance-lookup + "Builds a dispatcher which looks up a type by looking up the type itself, + then attempting to look up its ancestor classes, implemented interfaces, and + finally `java.lang.Object`." + [dispatch] + (fn lookup + [obj] + (let [t (class obj)] + (or + (some dispatch (lineage t)) + (let [candidates (remove (comp nil? first) + (map (juxt dispatch identity) + (find-interfaces t))) + wrong-number-of-candidates-message "%d candidates found for interfaces on dispatch type %s: %s"] + (case (count candidates) + 0 nil + 1 (ffirst candidates) + (throw (ex-info (format wrong-number-of-candidates-message + (count candidates) t (str/join ", " (map second candidates))))))) + (dispatch Object)))))) diff --git a/src/lambdaisland/deep_diff2/puget/printer.cljc b/src/lambdaisland/deep_diff2/puget/printer.cljc new file mode 100644 index 0000000..96146e6 --- /dev/null +++ b/src/lambdaisland/deep_diff2/puget/printer.cljc @@ -0,0 +1,756 @@ +(ns lambdaisland.deep-diff2.puget.printer + "Enhanced printing functions for rendering Clojure values. The following + options are available to control the printer: + + #### General Rendering + + `:width` + + Number of characters to try to wrap pretty-printed forms at. + + `:print-meta` + + If true, metadata will be printed before values. Defaults to the value of + `*print-meta*` if unset. + + #### Collection Options + + `:sort-keys` + + Print maps and sets with ordered keys. If true, the pretty printer will sort + all unordered collections before printing. If a number, counted collections + will be sorted if they are smaller than the given size. Otherwise + collections are printed in their natural sort order. Sorted collections are + always printed in their natural sort order. + + `:map-delimiter` + + The text placed between key-value pairs in a map. + + `:map-coll-separator` + + The text placed between a map key and a collection value. The keyword :line + will cause line breaks if the whole map does not fit on a single line. + + `:namespace-maps` + + Extract common keyword namespaces from maps using the namespace map literal + syntax. See `*print-namespace-maps*`. + + `:seq-limit` + + If set to a positive number, then lists will only render at most the first n + elements. This can help prevent unintentional realization of infinite lazy + sequences. + + #### Color Options + + `:print-color` + + When true, ouptut colored text from print functions. + + `:color-markup` + + :ansi for ANSI color text (the default) + :html-inline for inline-styled html + :html-classes to use the names of the keys in the :color-scheme map + as class names for spans so styling can be specified via CSS. + + `:color-scheme` + + Map of syntax element keywords to color codes. + + #### Type Handling + + `:print-handlers` + + A lookup function which will return a rendering function for a given class + type. This will be tried before the built-in type logic. See the + `lambdaisland.deep-diff2.puget.dispatch` namespace for some helpful constructors. The returned + function should accept the current printer and the value to be rendered + returning a format document. + + `:print-fallback` + + Keyword argument specifying how to format unknown values. Puget supports a few + different options: + + - `:pretty` renders values with the default colored representation. + - `:print` defers to the standard print method by rendering unknown values + using `pr-str`. + - `:error` will throw an exception when types with no defined handler are + encountered. + - A function value will be called with the current printer options and the + unknown value and is expected to return a formatting document representing + it. + " + (:require [arrangement.core :as order] + [clojure.string :as str] + [fipp.engine :as fe] + [fipp.visit :as fv] + [lambdaisland.deep-diff2.puget.color :as color] + [lambdaisland.deep-diff2.puget.color.ansi] + [lambdaisland.deep-diff2.puget.color.html] + [lambdaisland.deep-diff2.puget.dispatch :as dispatch] + #?(:cljs [goog.object :as gobj])) + (:import #?@(:clj [(java.text SimpleDateFormat) + (java.util TimeZone) + (java.sql Timestamp)] + :cljs [(goog.i18n DateTimeFormat)]))) + +(defn get-type-name + "Get the type of the given object as a string. For Clojure, gets the name of + the class of the object. For ClojureScript, gets either the `name` attribute + or the protocol name if the `name` attribute doesn't exist." + [x] + #?(:clj (.getName (class x)) + :cljs (let [t (type x) + n (.-name t)] + (if (empty? n) + (pr-str t) + n)))) + +(defn get-identity-hashcode + "Get the hashcode for a given object o" + [o] + #?(:clj (System/identityHashCode o) + :cljs (hash o))) + +(defn to-hex-string + "Returns a hex representation of input-string" + [input-string] + #?(:clj (Integer/toHexString input-string) + :cljs (.toString input-string 16))) + +;; ## Control Vars +(def ^:dynamic *options* + "Default options to use when constructing new printers." + {:width 80 + :sort-keys 80 + :map-delimiter "," + :map-coll-separator " " + :namespace-maps false + :print-fallback :pretty + :print-color false + :color-markup :ansi + :color-scheme + {;; syntax elements + :delimiter [:bold :red] + :tag [:red] + + ;; primitive values + :nil [:bold :black] + :boolean [:green] + :number [:cyan] + :string [:bold :magenta] + :character [:bold :magenta] + :keyword [:bold :yellow] + :symbol nil + + ;; special types + :function-symbol [:bold :blue] + :class-delimiter [:blue] + :class-name [:bold :blue]}}) + +(defn merge-options + "Merges maps of printer options, taking care to combine the color scheme + correctly." + [a b] + (let [colors (merge (:color-scheme a) (:color-scheme b))] + (assoc (merge a b) :color-scheme colors))) + +(defmacro with-options + "Executes the given expressions with a set of options merged into the current + option map." + [opts & body] + `(binding [*options* (merge-options *options* ~opts)] + ~@body)) + +(defmacro with-color + "Executes the given expressions with colored output enabled." + [& body] + `(with-options {:print-color true} + ~@body)) + +(defn color-text + "Produces text colored according to the active color scheme. This is mostly + useful to clients which want to produce output which matches data printed by + Puget, but which is not directly printed by the library. Note that this + function still obeys the `:print-color` option." + ([element text] + (color-text *options* element text)) + ([options element text] + (color/text options element text))) + +;; ## Formatting Methods +(defn- order-collection + "Takes a sequence of entries and checks the mode to determine whether to sort + them. Returns an appropriately ordered sequence." + [mode coll sort-fn] + (if (and (not (sorted? coll)) + (or (true? mode) + (and (number? mode) + (counted? coll) + (>= mode (count coll))))) + (sort-fn coll) + (seq coll))) + + +(defn- common-key-ns + "Extract a common namespace from the keys in the map. Returns a tuple of the + ns string and the stripped map, or nil if the keys are not keywords or there + is no sufficiently common namespace." + [m] + (when (every? (every-pred keyword? namespace) (keys m)) + (let [nsf (frequencies (map namespace (keys m))) + [common n] (apply max-key val nsf)] + (when (< (/ (count m) 2) n) + [common + (into (empty m) + (map (fn strip-common + [[k v :as e]] + (if (= common (namespace k)) + [(keyword (name k)) v] + e))) + m)])))) + +(defn format-unknown + "Renders common syntax doc for an unknown representation of a value." + ([printer value] + (format-unknown printer value (str value))) + ([printer value repr] + (format-unknown printer value (get-type-name value) repr)) + ([printer value tag repr] + (let [sys-id (to-hex-string (get-identity-hashcode value))] + [:span + (color/document printer :class-delimiter "#<") + (color/document printer :class-name tag) + (color/document printer :class-delimiter "@") + sys-id + (when (not= repr (str tag "@" sys-id)) + (list " " repr)) + (color/document printer :class-delimiter ">")]))) + +(defn format-doc* + "Formats a document without considering metadata." + [printer value] + (let [lookup (:print-handlers printer) + handler (and lookup (lookup value))] + (if handler + (handler printer value) + (fv/visit* printer value)))) + +(defn format-doc + "Recursively renders a print document for the given value." + [printer value] + (if-let [metadata (meta value)] + (fv/visit-meta printer metadata value) + (format-doc* printer value))) + +;; ## Type Handlers +(defn pr-handler + "Print handler which renders the value with `pr-str`." + [printer value] + (pr-str value)) + +(defn unknown-handler + "Print handler which renders the value using the printer's unknown type logic." + [printer value] + (fv/visit-unknown printer value)) + +(defn tagged-handler + "Generates a print handler function which renders a tagged-literal with the + given tag and a value produced by calling the function." + [tag value-fn] + (when-not (symbol? tag) + (throw (ex-info (str "Cannot create tagged handler with non-symbol tag " + (pr-str tag)) + {:tag tag, :value-fn value-fn}))) + (when-not (ifn? value-fn) + (throw (ex-info (str "Cannot create tagged handler for " tag + " with non-function value transform") + {:tag tag, :value-fn value-fn}))) + (fn handler + [printer value] + (format-doc printer (tagged-literal tag (value-fn value))))) + +(def inst-pattern "yyyy-MM-dd'T'HH:mm:ss.SSS-00:00") + +#?(:cljs + (defn utc-date [date] + (js/Date. + (.getUTCFullYear date) + (.getUTCMonth date) + (.getUTCDate date) + (.getUTCHours date) + (.getUTCMinutes date) + (.getUTCSeconds date) + (.getUTCMilliseconds date)))) + +#?(:clj + (defn utc-timestamp-format ^SimpleDateFormat [] + (doto (SimpleDateFormat. "yyyy-MM-dd'T'HH:mm:ss") + (.setTimeZone (TimeZone/getTimeZone "GMT"))))) + +(def platform-handlers + "Map of print handlers for Java/JavaScript types. This supports syntax for regular + expressions, dates, UUIDs, and futures." + #?(:clj + (-> + {java.lang.Class + (fn class-handler + [printer value] + (format-unknown printer value "Class" (get-type-name value))) + + java.util.concurrent.Future + (fn future-handler + [printer value] + (let [doc (if (future-done? value) + (format-doc printer @value) + (color/document printer :nil "pending"))] + (format-unknown printer value "Future" doc))) + + java.util.UUID + (tagged-handler 'uuid str) + + java.util.Date + (tagged-handler + 'inst + #(-> (java.text.SimpleDateFormat. inst-pattern) + (doto (.setTimeZone (java.util.TimeZone/getTimeZone "GMT"))) + (.format ^java.util.Date %))) + + java.sql.Timestamp + (tagged-handler + 'inst + (fn [ts] + (str (.format ^SimpleDateFormat (utc-timestamp-format) ts) + (format ".%09d-00:00" (.getNanos ^Timestamp ts)))))} + #?(:bb identity + :clj (assoc java.util.GregorianCalendar + (tagged-handler + 'inst + #(let [formatted (format "%1$tFT%1$tT.%1$tL%1$tz" %) + offset-minutes (- (.length formatted) 2)] + (str (subs formatted 0 offset-minutes) + ":" + (subs formatted offset-minutes))))))) + + :cljs + {inst? + (tagged-handler + 'inst + #(.format (DateTimeFormat. inst-pattern) (utc-date %))) + + uuid? + (tagged-handler 'uuid str) + + object? + (tagged-handler + 'js + (fn [x] + ;; non-recursive conversion to map + (reduce (fn [m k] + (assoc m k (gobj/get x k))) + {} + (js/Object.keys x))))})) + +(def clojure-handlers + "Map of print handlers for 'primary' Clojure types. These should take + precedence over the handlers in `clojure-interface-handlers`." + {#?(:clj clojure.lang.Atom + :cljs #(implements? IAtom %)) + (fn atom-handler + [printer value] + (format-unknown printer value "Atom" (format-doc printer @value))) + #?(:clj clojure.lang.Delay + :cljs #(implements? Delay %)) + (fn delay-handler + [printer value] + (let [doc (if (realized? value) + (format-doc printer @value) + (color/document printer :nil "pending"))] + (format-unknown printer value "Delay" doc))) + #?(:clj clojure.lang.ISeq + :cljs seq?) + (fn iseq-handler + [printer value] + (fv/visit-seq printer value))}) + +(def clojure-interface-handlers + "Fallback print handlers for other Clojure interfaces." + {#?(:clj clojure.lang.IPending + :cljs #(implements? IPending %)) + (fn pending-handler + [printer value] + (let [doc (if (realized? value) + (format-doc printer @value) + (color/document printer :nil "pending"))] + (format-unknown printer value doc))) + #?(:clj clojure.lang.Fn + :cljs fn?) + (fn fn-handler + [printer value] + (let [doc (let [[vname & tail] (-> (get-type-name value) + (str/replace-first "$" "/") + (str/split #"\$"))] + (if (seq tail) + (str vname "[" + (->> tail + (map #(first (str/split % #"__"))) + (str/join "/")) + "]") + vname))] + (format-unknown printer value "Fn" doc)))}) + +(def common-handlers + "Print handler dispatch combining Java and Clojure handlers with inheritance + lookups. Provides a similar experience as the standard Clojure + pretty-printer." + #?(:clj (dispatch/chained-lookup + (dispatch/inheritance-lookup platform-handlers) + (dispatch/inheritance-lookup clojure-handlers) + (dispatch/inheritance-lookup clojure-interface-handlers)) + :cljs (dispatch/chained-lookup + (dispatch/predicate-lookup platform-handlers) + (dispatch/predicate-lookup clojure-handlers) + (dispatch/predicate-lookup clojure-interface-handlers)))) + + +;; ## Canonical Printer Implementation +(defrecord CanonicalPrinter [print-handlers] + fv/IVisitor + + ;; Primitive Types + (visit-nil + [this] + "nil") + + (visit-boolean + [this value] + (str value)) + + (visit-number + [this value] + (pr-str value)) + + (visit-character + [this value] + (pr-str value)) + + (visit-string + [this value] + (pr-str value)) + + (visit-keyword + [this value] + (str value)) + + (visit-symbol + [this value] + (str value)) + + ;; Collection Types + (visit-seq + [this value] + (if (seq value) + (let [entries (map (partial format-doc this) value)] + [:group "(" [:align (interpose " " entries)] ")"]) + "()")) + + (visit-vector + [this value] + (if (seq value) + (let [entries (map (partial format-doc this) value)] + [:group "[" [:align (interpose " " entries)] "]"]) + "[]")) + + (visit-set + [this value] + (if (seq value) + (let [entries (map (partial format-doc this) + (sort order/rank value))] + [:group "#{" [:align (interpose " " entries)] "}"]) + "#{}")) + + (visit-map + [this value] + (if (seq value) + (let [entries (map #(vector :span (format-doc this (key %)) + " " (format-doc this (val %))) + (sort-by first order/rank value))] + [:group "{" [:align (interpose " " entries)] "}"]) + "{}")) + + ;; Clojure Types + (visit-meta + [this metadata value] + ;; Metadata is not printed for canonical rendering. + (format-doc* this value)) + + (visit-var + [this value] + ;; Defer to unknown, cover with handler. + (fv/visit-unknown this value)) + + (visit-pattern + [this value] + ;; Defer to unknown, cover with handler. + (fv/visit-unknown this value)) + + (visit-record + [this value] + ;; Defer to unknown, cover with handler. + (fv/visit-unknown this value)) + + ;; Special Types + (visit-tagged + [this value] + [:span (str "#" (:tag value)) " " (format-doc this (:form value))]) + + (visit-unknown + [this value] + (let [not-defined-representation-message (str "No defined representation for " + (get-type-name value) + ": " + (pr-str value))] + (throw (ex-info not-defined-representation-message + {:causes #{:undefined-representation}}))))) + +(defn canonical-printer + "Constructs a new canonical printer with the given handler dispatch." + ([] + (canonical-printer nil)) + ([handlers] + (assoc (CanonicalPrinter. handlers) + :width 0))) + +;; Remove automatic constructor function. +#?(:clj (ns-unmap *ns* '->CanonicalPrinter)) + +;; ## Pretty Printer Implementation +(defrecord PrettyPrinter + + [width + print-meta + sort-keys + map-delimiter + map-coll-separator + namespace-maps + seq-limit + print-color + color-markup + color-scheme + print-handlers + print-fallback] + + fv/IVisitor + + ;; Primitive Types + (visit-nil + [this] + (color/document this :nil "nil")) + + (visit-boolean + [this value] + (color/document this :boolean (str value))) + + (visit-number + [this value] + (color/document this :number (pr-str value))) + + (visit-character + [this value] + (color/document this :character (pr-str value))) + + (visit-string + [this value] + (color/document this :string (pr-str value))) + + (visit-keyword + [this value] + (color/document this :keyword (str value))) + + (visit-symbol + [this value] + (color/document this :symbol (str value))) + + ;; Collection Types + (visit-seq + [this value] + (if (seq value) + (let [[values trimmed?] + (if (and seq-limit (pos? seq-limit)) + (let [head (take seq-limit value)] + [head (<= seq-limit (count head))]) + [(seq value) false]) + elements + (cond-> (if (symbol? (first values)) + (cons (color/document this :function-symbol (str (first values))) + (map (partial format-doc this) (rest values))) + (map (partial format-doc this) values)) + trimmed? (concat [(color/document this :nil "...")]))] + [:group + (color/document this :delimiter "(") + [:align (interpose :line elements)] + (color/document this :delimiter ")")]) + (color/document this :delimiter "()"))) + + (visit-vector + [this value] + (if (seq value) + [:group + (color/document this :delimiter "[") + [:align (interpose :line (map (partial format-doc this) value))] + (color/document this :delimiter "]")] + (color/document this :delimiter "[]"))) + + (visit-set + [this value] + (if (seq value) + (let [entries (order-collection sort-keys value (partial sort order/rank))] + [:group + (color/document this :delimiter "#{") + [:align (interpose :line (map (partial format-doc this) entries))] + (color/document this :delimiter "}")]) + (color/document this :delimiter "#{}"))) + + (visit-map + [this value] + (if (seq value) + (let [[common-ns stripped] (when namespace-maps (common-key-ns value)) + kvs (order-collection sort-keys + (or stripped value) + (partial sort-by first order/rank)) + entries (map (fn [[k v]] + [:span + (format-doc this k) + (if (coll? v) + map-coll-separator + " ") + (format-doc this v)]) + kvs) + map-doc [:group + (color/document this :delimiter "{") + [:align (interpose [:span map-delimiter :line] entries)] + (color/document this :delimiter "}")]] + (if common-ns + [:group (color/document this :tag (str "#:" common-ns)) :line map-doc] + map-doc)) + (color/document this :delimiter "{}"))) + + ;; Clojure Types + (visit-meta + [this metadata value] + (if print-meta + [:align + [:span (color/document this :delimiter "^") (format-doc this metadata)] + :line (format-doc* this value)] + (format-doc* this value))) + + (visit-var + [this value] + [:span + (color/document this :delimiter "#'") + (color/document this :symbol (subs (str value) 2))]) + + (visit-pattern + [this value] + [:span + (color/document this :delimiter "#") + (color/document this :string (str \" value \"))]) + + (visit-record + [this value] + (fv/visit-tagged + this + (tagged-literal (symbol (get-type-name value)) + (into {} value)))) + + ;; Special Types + (visit-tagged + [this value] + (let [{:keys [tag form]} value] + [:group + (color/document this :tag (str "#" (:tag value))) + (if (coll? form) :line " ") + (format-doc this (:form value))])) + + (visit-unknown + [this value] + (case print-fallback + :pretty + (format-unknown this value) + + :print + [:span (pr-str value)] + + :error + (throw (ex-info (str "No defined representation for " (get-type-name value) ": " (pr-str value)) + {:causes #{:undefined-representation}})) + (if (ifn? print-fallback) + (print-fallback this value) + (throw (ex-info (str "Unsupported value for print-fallback: " (pr-str print-fallback)) + {:causes #{:unsupported-value}})))))) + +(defn pretty-printer + "Constructs a new printer from the given configuration." + [opts] + (->> [{:print-meta *print-meta* + :print-handlers common-handlers} + *options* + opts] + (reduce merge-options) + (map->PrettyPrinter))) + +;; Remove automatic constructor function. +#?(:clj (ns-unmap *ns* '->PrettyPrinter)) + +;; ## Printing Functions +(defn render-out + "Prints a value using the given printer." + ([printer value] + (render-out printer value nil)) + ([printer value opts] + (binding [*print-meta* false] + (fe/pprint-document + (format-doc printer value) + (merge {:width (:width printer)} + opts))))) + +(defn render-str + "Renders a value to a string using the given printer." + ^String + [printer value] + (str/trim-newline + (with-out-str + (render-out printer value)))) + +(defn pprint + "Pretty-prints a value to *out*. Options may be passed to override the + default *options* map." + ([value] + (pprint value nil)) + ([value opts] + (render-out (pretty-printer opts) value opts))) + +(defn pprint-str + "Pretty-print a value to a string." + ([value] + (pprint-str value nil)) + ([value opts] + (render-str (pretty-printer opts) value))) + +(defn cprint + "Like pprint, but turns on colored output." + ([value] + (cprint value nil)) + ([value opts] + (pprint value (assoc opts :print-color true)))) + +(defn cprint-str + "Pretty-prints a value to a colored string." + ([value] + (cprint-str value nil)) + ([value opts] + (pprint-str value (assoc opts :print-color true)))) diff --git a/test/lambdaisland/deep_diff/printer_test.clj b/test/lambdaisland/deep_diff/printer_test.clj deleted file mode 100644 index e442ce3..0000000 --- a/test/lambdaisland/deep_diff/printer_test.clj +++ /dev/null @@ -1,40 +0,0 @@ -(ns lambdaisland.deep-diff.printer-test - (:require [clojure.test :refer :all] - [lambdaisland.deep-diff.diff :as diff] - [lambdaisland.deep-diff.printer :as printer]) - (:import (java.sql Timestamp) - (java.util Date - GregorianCalendar - TimeZone))) - -(defn- printed - [diff] - (let [printer (printer/puget-printer {})] - (with-out-str (-> diff - (printer/format-doc printer) - (printer/print-doc printer))))) - -(defn- calendar - [date] - (doto (GregorianCalendar. (TimeZone/getTimeZone "GMT")) - (.setTime date))) - -(deftest print-doc-test - (testing "date" - (is (= "\u001B[31m-#inst \"2019-04-09T14:57:46.128-00:00\"\u001B[0m \u001B[32m+#inst \"2019-04-10T14:57:46.128-00:00\"\u001B[0m\n" - (printed (diff/diff #inst "2019-04-09T14:57:46.128-00:00" - #inst "2019-04-10T14:57:46.128-00:00"))))) - - (testing "timestamp" - (is (= "\u001B[31m-#inst \"1970-01-01T00:00:00.000000000-00:00\"\u001B[0m \u001B[32m+#inst \"1970-01-01T00:00:01.000000101-00:00\"\u001B[0m\n" - (printed (diff/diff (Timestamp. 0) - (doto (Timestamp. 1000) (.setNanos 101))))))) - - (testing "calendar" - (is (= "\u001B[31m-#inst \"1970-01-01T00:00:00.000+00:00\"\u001B[0m \u001B[32m+#inst \"1970-01-01T00:00:01.001+00:00\"\u001B[0m\n" - (printed (diff/diff (calendar (Date. 0)) (calendar (Date. 1001))))))) - - (testing "uuid" - (is (= "\u001B[31m-#uuid \"e41b325a-ce9d-4fdd-b51d-280d9c91314d\"\u001B[0m \u001B[32m+#uuid \"0400be9a-619f-4c6a-a735-6245e4955995\"\u001B[0m\n" - (printed (diff/diff #uuid "e41b325a-ce9d-4fdd-b51d-280d9c91314d" - #uuid "0400be9a-619f-4c6a-a735-6245e4955995")))))) diff --git a/test/lambdaisland/deep_diff/diff_test.clj b/test/lambdaisland/deep_diff2/diff_test.cljc similarity index 72% rename from test/lambdaisland/deep_diff/diff_test.clj rename to test/lambdaisland/deep_diff2/diff_test.cljc index aa7213c..4e0cff2 100644 --- a/test/lambdaisland/deep_diff/diff_test.clj +++ b/test/lambdaisland/deep_diff2/diff_test.cljc @@ -1,20 +1,14 @@ -(ns lambdaisland.deep-diff.diff-test - (:require [clojure.test :refer :all] - [clojure.test.check :as tc] - [clojure.test.check.clojure-test :refer [defspec]] - [clojure.test.check.generators :as gen] - [clojure.test.check.properties :as prop] - [lambdaisland.deep-diff.diff :as diff])) - -(doseq [v [#'diff/diff-seq - #'diff/diff-seq-replacements - #'diff/diff-seq-insertions - #'diff/diff-seq-deletions - #'diff/replacements - #'diff/del+ins]] - (alter-meta! v dissoc :private)) +(ns lambdaisland.deep-diff2.diff-test + (:require + [clojure.test :refer [deftest testing is are]] + [clojure.test.check :as tc] + [clojure.test.check.clojure-test :refer [defspec]] + [clojure.test.check.generators :as gen] + [clojure.test.check.properties :as prop] + [lambdaisland.deep-diff2.diff-impl :as diff])) (defrecord ARecord []) +(defrecord BRecord []) (deftest diff-test (testing "diffing atoms" @@ -39,6 +33,9 @@ (is (= [] (diff/diff [] []))) + (is (= {:meta true} + (meta (diff/diff ^:meta [] ^:meta [])))) + (is (= [1 2 3] (diff/diff (into-array [1 2 3]) [1 2 3]))) @@ -76,6 +73,9 @@ (is (= #{:a} (diff/diff #{:a} #{:a}))) + (is (= {:meta true} + (meta (diff/diff ^:meta #{} ^:meta #{})))) + (is (= #{(diff/->Insertion :a)} (diff/diff #{} #{:a}))) @@ -88,6 +88,9 @@ (testing "maps" (is (= {} (diff/diff {} {}))) + (is (= {:meta true} + (meta (diff/diff ^:meta {} ^:meta {})))) + (is (= {:a (diff/->Mismatch 1 2)} (diff/diff {:a 1} {:a 2}))) @@ -100,11 +103,30 @@ (is (= {:a [1 (diff/->Deletion 2) 3]} (diff/diff {:a [1 2 3]} {:a [1 3]})))) + (testing "map key order doesn't impact diff result" + (is (= {:name (diff/->Mismatch "Alyysa P Hacker" "Alyssa P Hacker"), :age 40} + + (diff/diff (array-map :name "Alyysa P Hacker" :age 40) + (array-map :age 40 :name "Alyssa P Hacker")) + + (diff/diff (array-map :age 40 :name "Alyysa P Hacker") + (array-map :age 40 :name "Alyssa P Hacker"))))) + (testing "records" - (is (= {:a (diff/->Mismatch 1 2)} + (is (= (map->ARecord {:a (diff/->Mismatch 1 2)}) (diff/diff (map->ARecord {:a 1}) (map->ARecord {:a 2})))) - (is (= {(diff/->Insertion :a) 1} - (diff/diff (map->ARecord {}) (map->ARecord {:a 1})))))) + (is (= (map->ARecord {(diff/->Insertion :a) 1}) + (diff/diff (map->ARecord {}) (map->ARecord {:a 1})))) + (is (= (map->ARecord {(diff/->Deletion :a) 1}) + (diff/diff (map->ARecord {:a 1}) + (map->ARecord {})))) + (is (= (diff/->Mismatch (map->ARecord {:a 1}) (map->BRecord {:a 1})) + (diff/diff (map->ARecord {:a 1}) + (map->BRecord {:a 1})))) + (is (= (diff/->Mismatch {:a 1} (map->ARecord {:a 1})) + (diff/diff {:a 1} (map->ARecord {:a 1})))) + (is (= (diff/->Mismatch (map->ARecord {:a 1}) {:a 1}) + (diff/diff (map->ARecord {:a 1}) {:a 1}))))) (is (= [{:x (diff/->Mismatch 1 2)}] (diff/diff [{:x 1}] [{:x 2}]))) @@ -204,12 +226,30 @@ (is (= [#{0 1} {-1 [[]]}] (diff/del+ins [0 0] [[]])))) +;; (not= ##NaN ##NaN), which messes up test results +;; https://stackoverflow.com/questions/16983955/check-for-nan-in-clojurescript +(defn NaN? [node] +;; Need to confirm that it's a Double first. + #?(:clj (and (instance? Double node) (Double/isNaN node)) + :cljs + (and (= (.call js/toString node) (str "[object Number]")) + (js/eval (str node " != +" node ))))) + +(def gen-any-except-NaN (gen/recursive-gen + gen/container-type + (gen/such-that (complement NaN?) gen/simple-type))) + (defspec round-trip-diff 100 - (prop/for-all [x gen/any - y gen/any] - (let [diff (diff/diff x y)] - (= [x y] [(diff/left-undiff diff) (diff/right-undiff diff)])))) + (prop/for-all + [x gen-any-except-NaN + y gen-any-except-NaN] + (let [diff (diff/diff x y)] + (= [x y] [(diff/left-undiff diff) (diff/right-undiff diff)])))) +(defspec diff-same-is-same 100 + (prop/for-all + [x gen-any-except-NaN] + (= x (diff/diff x x)))) (deftest diff-seq-test (is (= [(diff/->Insertion 1) 2 (diff/->Insertion 3)] @@ -265,18 +305,15 @@ (diff/diff-seq [:a :b :c] [:a :c :d])))) - - - (comment (use 'kaocha.repl) (run) - (defmethod clojure.core/print-method lambdaisland.deep-diff.diff.Insertion [v writer] + (defmethod clojure.core/print-method lambdaisland.deep-diff2.diff.Insertion [v writer] (.write writer (pr-str `(diff/->Insertion ~(:+ v))))) - (defmethod clojure.core/print-method lambdaisland.deep-diff.diff.Deletion [v writer] + (defmethod clojure.core/print-method lambdaisland.deep-diff2.diff.Deletion [v writer] (.write writer (pr-str `(diff/->Deletion ~(:- v))))) - (defmethod clojure.core/print-method lambdaisland.deep-diff.diff.Mismatch [v writer] + (defmethod clojure.core/print-method lambdaisland.deep-diff2.diff.Mismatch [v writer] (.write writer (pr-str `(diff/->Mismatch ~(:- v) ~(:+ v)))))) diff --git a/test/lambdaisland/deep_diff2/minimise_test.cljc b/test/lambdaisland/deep_diff2/minimise_test.cljc new file mode 100644 index 0000000..a113317 --- /dev/null +++ b/test/lambdaisland/deep_diff2/minimise_test.cljc @@ -0,0 +1,84 @@ +(ns lambdaisland.deep-diff2.minimise-test + (:require + [clojure.test :refer [deftest testing is are]] + [clojure.test.check.clojure-test :refer [defspec]] + [clojure.test.check.generators :as gen] + [clojure.test.check.properties :as prop] + [lambdaisland.deep-diff2 :as ddiff] + [lambdaisland.deep-diff2.diff-impl :as diff] + [lambdaisland.deep-diff2.diff-test :as diff-test])) + +(deftest basic-strip-test + (testing "diff without minimise" + (let [x {:a 1 :b 2 :d {:e 1} :g [:e [:k 14 :g 15]]} + y {:a 1 :c 3 :d {:e 15} :g [:e [:k 14 :g 15]]}] + (is (= (ddiff/diff x y) + {:a 1 + (diff/->Deletion :b) 2 + :d {:e (diff/->Mismatch 1 15)} + :g [:e [:k 14 :g 15]] + (diff/->Insertion :c) 3})))) + (testing "diff with minimise" + (let [x {:a 1 :b 2 :d {:e 1} :g [:e [:k 14 :g 15]]} + y {:a 1 :c 3 :d {:e 15} :g [:e [:k 14 :g 15]]}] + (is (= (ddiff/minimise (ddiff/diff x y)) + {(diff/->Deletion :b) 2 + :d {:e (diff/->Mismatch 1 15)} + (diff/->Insertion :c) 3}))))) + +(deftest minimise-on-diff-test + (testing "diffing atoms" + (testing "when different" + (is (= (ddiff/minimise + (ddiff/diff :a :b)) + (diff/->Mismatch :a :b)))) + + (testing "when equal" + (is (= (ddiff/minimise + (ddiff/diff :a :a)) + nil)))) + + (testing "diffing collections" + (testing "when different collection types" + (is (= (ddiff/minimise + (ddiff/diff [:a :b] #{:a :b})) + (diff/->Mismatch [:a :b] #{:a :b})))) + + (testing "when equal with clojure set" + (is (= (ddiff/minimise + (ddiff/diff #{:a :b} #{:a :b})) + #{}))) + + (testing "when different with clojure set" + (is (= (ddiff/minimise + (ddiff/diff #{:a :b :c} #{:a :b :d})) + #{(diff/->Insertion :d) (diff/->Deletion :c)}))) + + (testing "when equal with clojure vector" + (is (= (ddiff/minimise + (ddiff/diff [:a :b] [:a :b])) + []))) + + (testing "when equal with clojure hashmap" + (is (= (ddiff/minimise + (ddiff/diff {:a 1} {:a 1})) + {}))) + + (testing "when equal with clojure nesting vector" + (is (= (ddiff/minimise + (ddiff/diff [:a [:b :c :d]] [:a [:b :c :d]])) + []))) + + (testing "inserting a new map" + (is + (= (ddiff/minimise (ddiff/diff {} {:foo {:a 1}})) + {(diff/->Insertion :foo) {:a 1}}))))) + +;; "diff itself and minimise yields empty" +(defspec diff-itself 100 + (prop/for-all + [x diff-test/gen-any-except-NaN] + (if (coll? x) + (= (ddiff/minimise (ddiff/diff x x)) + (empty x)) + (nil? (ddiff/minimise (ddiff/diff x x)))))) diff --git a/test/lambdaisland/deep_diff2/printer_test.cljc b/test/lambdaisland/deep_diff2/printer_test.cljc new file mode 100644 index 0000000..b99f749 --- /dev/null +++ b/test/lambdaisland/deep_diff2/printer_test.cljc @@ -0,0 +1,48 @@ +(ns lambdaisland.deep-diff2.printer-test + (:require [clojure.test :refer [deftest testing is are]] + [lambdaisland.deep-diff2.diff-impl :as diff] + [lambdaisland.deep-diff2.printer-impl :as printer]) + #?(:clj + (:import (java.sql Timestamp) + (java.util Date + TimeZone)))) + +#?(:bb nil ;; GregorianCalender not included in favor of java.time + :clj (import '[java.util GregorianCalendar])) + +(defn- printed + [diff] + (let [printer (printer/puget-printer {})] + (with-out-str (-> diff + (printer/format-doc printer) + (printer/print-doc printer))))) +#?(:bb nil + :clj + (defn- calendar + [date] + (doto (GregorianCalendar. (TimeZone/getTimeZone "GMT")) + (.setTime date)))) + +(deftest print-doc-test + (testing "date" + (is (= "\u001B[31m-#inst \"2019-04-09T14:57:46.128-00:00\"\u001B[0m \u001B[32m+#inst \"2019-04-10T14:57:46.128-00:00\"\u001B[0m\n" + (printed (diff/diff #inst "2019-04-09T14:57:46.128-00:00" + #inst "2019-04-10T14:57:46.128-00:00"))))) + + #?(:bb nil ;; bb TimeStamp constructor not included as of 1.0.166 + :clj + (testing "timestamp" + (is (= "\u001B[31m-#inst \"1970-01-01T00:00:00.000000000-00:00\"\u001B[0m \u001B[32m+#inst \"1970-01-01T00:00:01.000000101-00:00\"\u001B[0m\n" + (printed (diff/diff (Timestamp. 0) + (doto (Timestamp. 1000) (.setNanos 101)))))))) + + #?(:bb nil + :clj + (testing "calendar" + (is (= "\u001B[31m-#inst \"1970-01-01T00:00:00.000+00:00\"\u001B[0m \u001B[32m+#inst \"1970-01-01T00:00:01.001+00:00\"\u001B[0m\n" + (printed (diff/diff (calendar (Date. 0)) (calendar (Date. 1001)))))))) + + (testing "uuid" + (is (= "\u001B[31m-#uuid \"e41b325a-ce9d-4fdd-b51d-280d9c91314d\"\u001B[0m \u001B[32m+#uuid \"0400be9a-619f-4c6a-a735-6245e4955995\"\u001B[0m\n" + (printed (diff/diff #uuid "e41b325a-ce9d-4fdd-b51d-280d9c91314d" + #uuid "0400be9a-619f-4c6a-a735-6245e4955995")))))) diff --git a/test/lambdaisland/deep_diff2/puget_test.cljc b/test/lambdaisland/deep_diff2/puget_test.cljc new file mode 100644 index 0000000..01dac48 --- /dev/null +++ b/test/lambdaisland/deep_diff2/puget_test.cljc @@ -0,0 +1,18 @@ +(ns lambdaisland.deep-diff2.puget-test + (:require [clojure.test :refer [deftest testing is]] + [lambdaisland.deep-diff2.puget.color.html :as sut])) + +(deftest puget-html-test + (testing "properly escape html" + (let [input [""] + expected-result [[:span + [:escaped "<"] "ul id=someList" [:escaped ">"] + [:escaped "<"] "li class=red" [:escaped ">"] + "Item 1" + [:escaped "<"] "/li" [:escaped ">"] + [:escaped "<"] "li" [:escaped ">"] + "Item 2" + [:escaped "<"] "/li" [:escaped ">"] + [:escaped "<"] "/ul" [:escaped ">"]]]] + (is (= expected-result (sut/escape-html-document input)))))) + diff --git a/test/lambdaisland/deep_diff2/runner.clj b/test/lambdaisland/deep_diff2/runner.clj new file mode 100644 index 0000000..f323d7e --- /dev/null +++ b/test/lambdaisland/deep_diff2/runner.clj @@ -0,0 +1,14 @@ +(ns lambdaisland.deep-diff2.runner + "Test runner for babashka, until kaocha works with bb :)" + (:require [clojure.test :as t])) + +(defn run-tests [_] + (let [test-nss '[lambdaisland.deep-diff2.diff-test + lambdaisland.deep-diff2.printer-test + lambdaisland.deep-diff2.puget-test]] + (doseq [test-ns test-nss] + (require test-ns)) + (let [{:keys [fail error]} + (apply t/run-tests test-nss)] + (when (and fail error (pos? (+ fail error))) + (throw (ex-info "Tests failed" {:babashka/exit 1})))))) diff --git a/test/lambdaisland/deep_diff2_test.cljc b/test/lambdaisland/deep_diff2_test.cljc new file mode 100644 index 0000000..15cd703 --- /dev/null +++ b/test/lambdaisland/deep_diff2_test.cljc @@ -0,0 +1,19 @@ +(ns lambdaisland.deep-diff2-test + "Smoke tests of the top level API." + (:require [lambdaisland.deep-diff2 :as ddiff] + [lambdaisland.deep-diff2.diff-impl :as diff-impl] + [clojure.test :refer [is are deftest testing]] + [clojure.string :as str])) + +(deftest diff-test + (is (= [{:foo (diff-impl/->Mismatch 1 2)}] + (ddiff/diff [{:foo 1}] [{:foo 2}])))) + +(deftest printer-test + (is (instance? lambdaisland.deep_diff2.puget.printer.PrettyPrinter + (ddiff/printer)))) + +(deftest pretty-print-test + (is (= "\u001B[1;31m[\u001B[0m\u001B[1;31m{\u001B[0m\u001B[1;33m:foo\u001B[0m \u001B[31m-1\u001B[0m \u001B[32m+2\u001B[0m\u001B[1;31m}\u001B[0m\u001B[1;31m]\u001B[0m\n" + (with-out-str + (ddiff/pretty-print (ddiff/diff [{:foo 1}] [{:foo 2}])))))) diff --git a/tests.edn b/tests.edn index d5e8da4..edd4abc 100644 --- a/tests.edn +++ b/tests.edn @@ -1,4 +1,6 @@ #kaocha/v1 -{:tests [{:id :unit - :test-paths ["test"] - :source-paths ["src"]}]} +{:tests [{:id :clj} + {:id :cljs + :type :kaocha.type/cljs}] + :kaocha/bindings {kaocha.stacktrace/*stacktrace-filters* []} + }