├── .gitignore ├── .gitlab-ci.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── project.clj ├── src └── differ │ ├── core.cljc │ ├── diff.cljc │ └── patch.cljc └── test └── differ ├── core_test.cljc ├── diff_test.cljc └── patch_test.cljc /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_store 2 | /target 3 | /lib 4 | /classes 5 | /checkouts 6 | pom.xml 7 | pom.xml.asc 8 | *.jar 9 | *.class 10 | .lein-deps-sum 11 | .lein-failures 12 | .lein-plugins 13 | .lein-repl-history 14 | .nrepl-port 15 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | before_script: 2 | - apt-get update -y 3 | - apt-get install default-jre -y 4 | - wget https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein 5 | - chmod a+x lein 6 | - export LEIN_ROOT=1 7 | - PATH=$PATH:. 8 | - lein deps 9 | 10 | test: 11 | script: 12 | - lein all-tests -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.3.3 4 | 5 | - Update dependencies and build targets 6 | - Fix runtime crash where old value was vector and new value was nil 7 | 8 | ## 0.3.2 9 | 10 | Change in metadata 11 | 12 | ## 0.3.1 13 | 14 | Only a change in meta data (project moved from github to gitlab). 15 | 16 | ## 0.3 17 | 18 | This release bumps the required version of Clojure and Clojurescript to 1.7.x. 19 | 20 | * Issue 20: Patch doesn't retain metadata 21 | * Issue 19: Vector patch fails with null pointer exception 22 | * Issue 16: Switch from cljx to cljc 23 | 24 | ## 0.2.2 25 | 26 | * Issue 15: Edge case with `nil` 27 | 28 | ## 0.2.1 29 | 30 | * Issue 13 and 14: Edge cases with `nil` 31 | 32 | ## 0.2 33 | 34 | First release with support for all built-in data structures 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contribution Guidelines 2 | 3 | Bug reports and feature requests are always appriciated, but please keep these things in mind: 4 | 5 | - Make sure that a similar issue hasn't already been raised 6 | - When submitting bugs, please include detailed steps on how to reproduce 7 | 8 | If you'd like to contribute with code or documentation, please follow these simple steps. 9 | 10 | - Select an unassigned issue labeled 'help-wanted' 11 | - If you want to submit new code without an associated issue, submit an issue first. Nothing is worse than doing work that isn't accepted 12 | - Comment on the issue that you are working on it 13 | - Fork this repo (if you haven't already) and do all your work in a new branch 14 | - Submit your work as a pull request 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2014-2019 Robin Heggelund Hansen 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Differ 2 | 3 | A library for diffing, and patching, Clojure(script) datastructures. 4 | 5 | ## Motivation 6 | 7 | I wanted to implement an auto-save feature for my Clojurescript web-app. To be efficient, only the actual changes should be sent to the backend. `clojure.data/diff` is not the easiest function to work with for several reasons, and there didn't seem to be any good alternatives, so differ was born. 8 | 9 | ## Setup 10 | 11 | Add the following the to your `project.clj`: 12 | 13 | [![Clojars Project](http://clojars.org/differ/latest-version.svg)](http://clojars.org/differ) 14 | 15 | ## Usage 16 | 17 | First of all, you need to require the proper namespace: 18 | 19 | ```clojure 20 | (ns some.ns 21 | (:require [differ.core :as differ])) 22 | ``` 23 | 24 | You can create a diff using the `differ.core/diff` function: 25 | 26 | ```clojure 27 | (def person-map {:name "Robin" 28 | :age 25 29 | :gender :male 30 | :phone {:home 99999999 31 | :work 12121212}) 32 | 33 | (def person-diff (differ/diff person-map {:name "Robin Heggelund Hansen" 34 | :age 26 35 | :phone {:home 99999999}) 36 | 37 | ;; person-diff will now be [{:name "Robin Heggelund Hansen" 38 | ;; :age 26} 39 | ;; {:gender 0 40 | ;; :phone {:work 0}] 41 | ``` 42 | 43 | `differ.core/diff` will return a data structure of the same type that is given, and will work with nested data structures. If you only want alterations, or removals, instead of both, please check the `differ.diff` and `differ.patch` namespaces. 44 | 45 | To apply the diff, you can use the `differ.core/patch` function. This function works on any similar data structure: 46 | 47 | ```clojure 48 | (differ/patch {:species :human 49 | :gender :female} 50 | person-diff) 51 | 52 | ;; Will return {:name "Robin Heggelund Hansen" 53 | ;; :age 26 54 | ;; :species :human} 55 | ``` 56 | 57 | ## Maps 58 | 59 | Maps are probably the best supported, and most straight forward type to diff. Alterations are a simple map of the key-value pairs missing. Removals are a map of keys where the value is 0, or a nested data structure. Check the "Usage" section for a decent example. 60 | 61 | ## Sequential types 62 | 63 | Differ works by checking what values have changed for a given key. For sequential types (vectors, lists and seqs) this means that alterations is represented as a sequential type of `[index diff]` for every key that has a changed value. This unfortunetly means that differ does not detect if elements have simply changed places. 64 | 65 | Removals are represented as a sequential type containing number of elements to drop from the end of the sequence, and `[index diff]` for every nested type that contains removals (everything else is an alteration). 66 | 67 | Differ does diff between sequential types, but remains the correct type of the new state. 68 | 69 | ```clojure 70 | (ns test 71 | (:require [differ.diff :as diff])) 72 | 73 | (diff/alterations '(1 2 3) [1 2 2 4]) 74 | ;; [2 2 :+ 4] 75 | 76 | (diff/removals [1 {:a 2} 3] '(1 {})) 77 | ;; (1 1 {:a 0}) 78 | ``` 79 | 80 | ## Sets 81 | 82 | Because differ works by checking if the value for a given key has changed, sets does not support nesting (every element is it's own key). Differ can therefore only detect if elements have been added or removed from a set, and not if they have changed. If you have sets in your datastructure, you should keep them shallow to avoid a large diff. 83 | 84 | ```clojure 85 | (ns test 86 | (:require [differ.diff :as diff])) 87 | 88 | (diff/alterations #{1 2 3} #{1 2 3 4}) 89 | ;; #{4} 90 | 91 | (diff/removals #{1 {:a 2} 3} #{{} 1}) 92 | ;; #{{:a 2} 3} <-- does not pick up changes 93 | ``` 94 | 95 | ## Contributing 96 | 97 | Feedback to both this library and this guide is welcome. Plese read `CONTRIBUTING.md` for more information. 98 | 99 | ### Running the tests 100 | 101 | Differ is assumed to work with Clojure 1.8 and up, as well as a recent Clojurescript version. 102 | 103 | There is a leiningen alias that makes it easy to run the tests against supported Clojure versions: 104 | 105 | ```bash 106 | > lein all-tests 107 | ``` 108 | 109 | ## License 110 | 111 | Copyright © 2014-2019 Robin Heggelund Hansen. 112 | 113 | Distributed under the [MIT License](http://opensource.org/licenses/MIT). 114 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject differ "0.3.3" 2 | :description "A library for diffing, and patching, Clojure(script) data structures" 3 | :url "https://github.com/Skinney/differ" 4 | :license {:name "MIT" 5 | :url "http://opensource.org/licenses/MIT"} 6 | 7 | :signing {:gpg-key "robin.heggelund@icloud.com"} 8 | 9 | :dependencies [[org.clojure/clojure "1.8.0" :scope "provided"]] 10 | 11 | :profiles {:1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]} 12 | :1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]} 13 | :1.10 {:dependencies [[org.clojure/clojure "1.10.1"]]} 14 | :cljs {:dependencies [[org.clojure/clojurescript "1.10.520"]] 15 | :plugins [[lein-cljsbuild "1.1.2"]] 16 | :cljsbuild {:test-commands {"phantom" ["phantomjs" :runner "target/testable.js"]} 17 | :builds [{:source-paths ["src" "test"] 18 | :compiler {:output-to "target/testable.js" 19 | :optimizations :none}}]} 20 | :prep-tasks [["cljsbuild" "once"]]}} 21 | 22 | :aliases {"all-tests" ["with-profile" "cljs:1.8:1.9:1.10" "test"]}) 23 | -------------------------------------------------------------------------------- /src/differ/core.cljc: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014-2019 Robin Heggelund Hansen. 2 | ;; Distributed under the MIT License (http://opensource.org/licenses/MIT). 3 | 4 | (ns differ.core 5 | "This namespace has two functions: diff and patch. 6 | 7 | Diff allows you to compare two datastructures, and returns elements that 8 | are different or non-existant. The result of this comparision is represented 9 | by a vector containing alterations and removals, respectively. 10 | 11 | Once you have a diff like this, you can apply it to any similar data- 12 | structure with the patch function. If you only want a diff of alterations, 13 | or only removals, you can use the alteration and removal functions in the 14 | differ.diff and differ.patch namespaces." 15 | (:require [differ.diff :as diff] 16 | [differ.patch :as patch])) 17 | 18 | (defn diff 19 | "Returns a vector containing the differing, and non-existant elements, of 20 | two clojure datastructures." 21 | [state new-state] 22 | [(diff/alterations state new-state) 23 | (diff/removals state new-state)]) 24 | 25 | (defn patch 26 | "Applies a diff, as created by the diff function, to any datastructure." 27 | [state [alterations removals]] 28 | (-> state 29 | (patch/removals removals) 30 | (patch/alterations alterations))) 31 | -------------------------------------------------------------------------------- /src/differ/diff.cljc: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014-2019 Robin Heggelund Hansen. 2 | ;; Distributed under the MIT License (http://opensource.org/licenses/MIT). 3 | 4 | (ns differ.diff 5 | "Provides functions to compare two clojure datastructures and return the 6 | difference between them. Alterations will return the elements that differ, 7 | the removals will return elements that only exist in one collection." 8 | (:require [clojure.set :as set])) 9 | 10 | (declare alterations removals) 11 | 12 | 13 | (defn- map-alterations [state new-state] 14 | (loop [[k & ks] (keys new-state) 15 | diff (transient {})] 16 | (if-not k 17 | (persistent! diff) 18 | (let [old-val (get state k ::none) 19 | new-val (alterations old-val (get new-state k))] 20 | (cond (and (coll? old-val) (coll? new-val) (empty? new-val)) 21 | (recur ks diff) 22 | 23 | (= old-val new-val) 24 | (recur ks diff) 25 | 26 | :else 27 | (recur ks (assoc! diff k new-val))))))) 28 | 29 | (defn- vec-alterations [state new-state] 30 | (loop [idx 0 31 | [old-val & old-rest :as old-coll] state 32 | [new-val & new-rest :as new-coll] new-state 33 | diff (transient [])] 34 | (if-not (seq new-coll) 35 | (persistent! diff) 36 | (let [val-diff (alterations old-val new-val)] 37 | (cond (empty? old-coll) 38 | (recur (inc idx) old-rest new-rest (conj! (conj! diff :+) val-diff)) 39 | 40 | (= old-val new-val) 41 | (recur (inc idx) old-rest new-rest diff) 42 | 43 | :else 44 | (recur (inc idx) old-rest new-rest (conj! (conj! diff idx) val-diff))))))) 45 | 46 | (defn alterations 47 | "Find elements that are different in new-state, when compared to state. 48 | The datastructure returned will be of the same type as the first argument 49 | passed. Works recursively on nested datastructures." 50 | [state new-state] 51 | (cond (and (map? state) (map? new-state)) 52 | (map-alterations state new-state) 53 | 54 | (and (sequential? state) (sequential? new-state)) 55 | (if (vector? new-state) 56 | (vec-alterations state new-state) 57 | (into (list) (reverse (vec-alterations state new-state)))) 58 | 59 | (and (set? state) (set? new-state)) 60 | (set/difference new-state state) 61 | 62 | :else 63 | new-state)) 64 | 65 | 66 | (defn- map-removals [state new-state] 67 | (let [new-keys (set (keys new-state))] 68 | (loop [[k & ks] (keys state) 69 | diff (transient {})] 70 | (if-not k 71 | (persistent! diff) 72 | (if-not (contains? new-keys k) 73 | (recur ks (assoc! diff k 0)) 74 | (let [old-val (get state k) 75 | new-val (get new-state k) 76 | rms (removals old-val new-val)] 77 | (if (and (coll? rms) (seq rms)) 78 | (recur ks (assoc! diff k rms)) 79 | (recur ks diff)))))))) 80 | 81 | (defn- vec-removals [state new-state] 82 | (let [diff (- (count state) (count new-state)) 83 | empty-state []] 84 | (loop [idx 0 85 | [old-val & old-rest :as old-coll] state 86 | [new-val & new-rest :as new-coll] new-state 87 | rem (transient (conj empty-state diff))] 88 | (if-not (and (seq old-coll) (seq new-coll)) 89 | (let [base (persistent! rem)] 90 | (if (and (= 1 (count base)) 91 | (>= 0 (first base))) 92 | empty-state 93 | base)) 94 | (let [new-rem (removals old-val new-val)] 95 | (if (or (and (coll? new-rem) (empty? new-rem)) 96 | (= old-val new-rem)) 97 | (recur (inc idx) old-rest new-rest rem) 98 | (recur (inc idx) old-rest new-rest (conj! (conj! rem idx) new-rem)))))))) 99 | 100 | (defn removals 101 | "Find elements that are in state, but not in new-state. 102 | The datastructure returned will be of the same type as the first argument 103 | passed. Works recursively on nested datastructures." 104 | [state new-state] 105 | (cond (and (coll? state) (nil? new-state)) 106 | nil 107 | 108 | (not (and (coll? state) (coll? new-state))) 109 | state 110 | 111 | (and (map? state) (map? new-state)) 112 | (map-removals state new-state) 113 | 114 | (and (sequential? state) (sequential? new-state)) 115 | (if (vector? new-state) 116 | (vec-removals state new-state) 117 | (into (list) (reverse (vec-removals state new-state)))) 118 | 119 | (and (set? state) (set? new-state)) 120 | (set/difference state new-state) 121 | 122 | :else 123 | (empty state))) 124 | -------------------------------------------------------------------------------- /src/differ/patch.cljc: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014-2019 Robin Heggelund Hansen. 2 | ;; Distributed under the MIT License (http://opensource.org/licenses/MIT). 3 | 4 | (ns differ.patch 5 | "Use the functions in this namespace to apply diffs, created by functions 6 | in the differ.diff namespace, to similar datastructures." 7 | (:require [clojure.set :as set])) 8 | 9 | (declare alterations removals) 10 | 11 | 12 | (defn- map-alterations [state diff] 13 | (loop [[k & ks] (keys diff) 14 | result (transient state)] 15 | (if-not k 16 | (with-meta (persistent! result) (meta state)) 17 | (let [old-val (get result k) 18 | diff-val (get diff k)] 19 | (recur ks (assoc! result k (alterations old-val diff-val))))))) 20 | 21 | (defn- vec-alterations [state diff] 22 | (loop [idx 0 23 | [old-val & old-rest :as old-coll] state 24 | [diff-idx diff-val & diff-rest :as diff-coll] diff 25 | result (transient [])] 26 | (let [old-empty? (empty? old-coll) 27 | diff-empty? (empty? diff-coll)] 28 | (cond (and old-empty? diff-empty?) 29 | (with-meta (persistent! result) (meta state)) 30 | 31 | diff-empty? 32 | (recur (inc idx) old-rest diff-rest (conj! result old-val)) 33 | 34 | (or (= idx diff-idx) old-empty?) 35 | (recur (inc idx) old-rest diff-rest (conj! result (alterations old-val diff-val))) 36 | 37 | :else 38 | (recur (inc idx) old-rest diff-coll (conj! result old-val)))))) 39 | 40 | (defn alterations 41 | "Returns a new datastructure, containing the changes in the provided diff." 42 | [state diff] 43 | (cond (and (map? state) (map? diff)) 44 | (map-alterations state diff) 45 | 46 | (and (sequential? state) (sequential? diff)) 47 | (if (vector? diff) 48 | (vec-alterations state diff) 49 | (with-meta 50 | (into (list) (reverse (vec-alterations state diff))) 51 | (meta state))) 52 | 53 | (and (set? state) (set? diff)) 54 | (with-meta 55 | (set/union state diff) 56 | (meta state)) 57 | 58 | :else 59 | diff)) 60 | 61 | 62 | (defn- map-removals [state diff] 63 | (loop [[k & ks] (keys diff) 64 | result (transient state)] 65 | (if-not k 66 | (with-meta (persistent! result) (meta state)) 67 | (let [old-val (get result k) 68 | diff-val (get diff k)] 69 | (if (= 0 diff-val) 70 | (recur ks (dissoc! result k)) 71 | (recur ks (assoc! result k (removals old-val diff-val)))))))) 72 | 73 | (defn- vec-removals [state diff] 74 | (if-not (seq diff) 75 | state 76 | (let [max-index (- (count state) (first diff))] 77 | (loop [index 0 78 | [old-val & old-rest :as old-coll] state 79 | [diff-index diff-val & diff-rest :as diff-coll] (rest diff) 80 | result (transient [])] 81 | (cond (or (= index max-index) (empty? old-coll)) 82 | (with-meta (persistent! result) (meta state)) 83 | 84 | (= index diff-index) 85 | (recur (inc index) old-rest diff-rest (conj! result (removals old-val diff-val))) 86 | 87 | :else 88 | (recur (inc index) old-rest diff-coll (conj! result old-val))))))) 89 | 90 | (defn removals 91 | "Returns a new datastructure, not containing the elements in the 92 | provided diff." 93 | [state diff] 94 | (cond (and (map? state) (map? diff)) 95 | (map-removals state diff) 96 | 97 | (and (sequential? state) (sequential? diff)) 98 | (if (vector? diff) 99 | (vec-removals state diff) 100 | (with-meta 101 | (into (list) (reverse (vec-removals state diff))) 102 | (meta state))) 103 | 104 | (and (set? state) (set? diff)) 105 | (with-meta 106 | (set/difference state diff) 107 | (meta state)) 108 | 109 | :else 110 | state)) 111 | -------------------------------------------------------------------------------- /test/differ/core_test.cljc: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014-2019 Robin Heggelund Hansen. 2 | ;; Distributed under the MIT License (http://opensource.org/licenses/MIT). 3 | 4 | (ns differ.core-test 5 | (:require [differ.core :as core] 6 | #?(:clj [clojure.test :refer [is deftest testing]] 7 | :cljs [cljs.test :refer-macros [is deftest testing]]))) 8 | 9 | (let [old-state {:modifyMap {:stringModify "tt" 10 | :numberModify 34 11 | :map {:numberModify 3 12 | :stringModify "ss" 13 | :deepMap {:a 3} 14 | :numberAdd 4 15 | :number-nil 3 16 | :nil-number nil 17 | :string-nil "sn" 18 | :nil-string nil 19 | :stringAdd "ss" 20 | :map-nil {:a 1} 21 | :map-empty {:a 2} 22 | :nil-map nil 23 | :empty-map {} 24 | :nil-emptyMap nil 25 | :nil-none nil} 26 | :mapUnchange {:a "ss"}} 27 | :modifyVector {:vector-nil [1 {:a "ddd"} "ss"] 28 | :vectorEmpty-nil [] 29 | :vector-empty [1 {:a "ddd"} "ss"] 30 | :nil-emptyvector nil 31 | :nil-vector nil 32 | :empty-vector [] 33 | :vectorModify [1 2 3 4 34 | {:a "tt"} 35 | {:numberUnchange 3 36 | :numberModify 3 37 | :f "s"}]} 38 | :modifySet {:set-nil #{1 {:a "ddd"} "ss"} 39 | :setEmpty-nil #{} 40 | :set-empty #{1 {:a "ddd"} "ss"} 41 | :nil-emptyset nil 42 | :nil-set nil 43 | :empty-set #{} 44 | :setModify #{1 2 3 4 45 | {:a "tt"} 46 | {:numberUnchange 3 47 | :numberModify 3 48 | :f "s"}}}} 49 | 50 | new-state {:modifyMap {:stringModify "ttt" 51 | :numberModify 342 52 | :map {:numberModify 33 53 | :stringModify "ssa" 54 | :deepMap {:a 32} 55 | :numberRemove 45 56 | :number-nil nil 57 | :nil-number 4 58 | :string-nil nil 59 | :nil-string "ns" 60 | :stringRemove "aa" 61 | :map-nil nil 62 | :map-empty {} 63 | :nil-map {:a 1} 64 | :empty-map {:a 2} 65 | :nil-emptyMap {} 66 | :none-nil nil} 67 | :mapUnchange {:a "ss"}} 68 | :modifyVector {:vector-nil nil 69 | :vectorEmpty-nil nil 70 | :vector-empty [] 71 | :nil-emptyvector [] 72 | :nil-vector [1 {:a "ddd"} "ss"] 73 | :empty-vector [1 {:a "ddd"} "ss"] 74 | :vectorModify [1 4 3 5 6 75 | {:numberUnchange 3 76 | :numberModify 4 77 | :a "ss"} 78 | 4]} 79 | :modifySet {:set-nil nil 80 | :setEmpty-nil nil 81 | :set-empty #{} 82 | :nil-emptyset #{} 83 | :nil-set #{1 {:a "ddd"} "ss"} 84 | :empty-set #{3 "dd"} 85 | :setModify #{1 4 3 5 6 86 | {:numberUnchange 3 87 | :numberModify 4 88 | :a "ss"}}}} 89 | 90 | diff-state (core/diff old-state new-state) 91 | 92 | old-simple-state {:one 1 93 | :two {:three 3 94 | :four "test"}} 95 | new-simple-state {:one 2 96 | :five "5" 97 | :two {:four "nice"}} 98 | alter {:one 2 99 | :five "5" 100 | :two {:four "nice"}} 101 | remo {:two {:three 0}}] 102 | 103 | (deftest diff 104 | (is (= [alter remo] (core/diff old-simple-state new-simple-state))) 105 | (is (= [[:+ 4] []] (core/diff [1 2 3] [1 2 3 4])))) 106 | 107 | (deftest patch 108 | (is (= new-simple-state (core/patch old-simple-state [alter remo]))) 109 | (is (= new-state (core/patch old-state diff-state))) 110 | (is (= [1 2 3 4] (core/patch [1 2 3] [[:+ 4] []])))) 111 | 112 | (deftest metadata 113 | (let [map-meta {:type :differ/map} 114 | vec-meta {:type :differ/vec} 115 | map-test (with-meta {:name "Robin" 116 | :hobbies [:soccer]} 117 | map-meta) 118 | vec-test (with-meta [1 2 3] vec-meta)] 119 | 120 | (is (= map-meta (meta (core/patch map-test [{:name "Nibor"} {:hobby 0}])))) 121 | (is (= vec-meta (meta (core/patch vec-test [[] [1]])))))) 122 | 123 | (deftest vector-nil-replacement 124 | (let [vector-diff-old {:words ["blah"]} 125 | vector-diff-new {:words nil} 126 | vector-diff (core/diff vector-diff-old vector-diff-new)] 127 | (is (= vector-diff-new (core/patch vector-diff-old vector-diff)))))) 128 | -------------------------------------------------------------------------------- /test/differ/diff_test.cljc: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014-2019 Robin Heggelund Hansen. 2 | ;; Distributed under the MIT License (http://opensource.org/licenses/MIT). 3 | 4 | (ns differ.diff-test 5 | (:require [differ.diff :as diff] 6 | #?(:clj [clojure.test :refer [is deftest testing]] 7 | :cljs [cljs.test :refer-macros [is deftest testing]]))) 8 | 9 | 10 | (let [state {:one 1 11 | :two {:three 2 12 | :four {:five "five" 13 | :six true}} 14 | :seven 3 15 | :vector [1 2 true 4] 16 | :list '(4 "by" 2) 17 | :set #{:b}}] 18 | 19 | (deftest alterations 20 | (testing "empty coll when there are no changes" 21 | (is (= {} (diff/alterations {:a :a} {:a :a}))) 22 | (is (= [] (diff/alterations [1 2] [1 2]))) 23 | (is (= #{} (diff/alterations #{1 2} #{1 2}))) 24 | (is (= '() (diff/alterations '(1 2) '(1 2))))) 25 | 26 | (testing "if types are different, returns new-state" 27 | (is (= [1 2] (diff/alterations {:a 2} [1 2]))) 28 | (is (= #{2 4} (diff/alterations {"test" true} #{2 4}))) 29 | (is (= 1 (diff/alterations [1 2 3] 1)))) 30 | 31 | (testing "sequential types are treated equal" 32 | (is (= '(1 2) (diff/alterations [1 1 1] '(1 2 1)))) 33 | (is (= [1 2] (diff/alterations '(1 1 1) [1 2 1])))) 34 | 35 | (testing "returns new-state if values are not equal, but not diffable" 36 | (is (= 2 (diff/alterations 1 2))) 37 | (is (= true (diff/alterations false true))) 38 | (is (= "first" (diff/alterations "who's on" "first"))))) 39 | 40 | 41 | (deftest map-alterations 42 | (testing "alterations" 43 | (is (= {:one 2} (diff/alterations state (assoc state :one 2)))) 44 | (is (= {:one 2, :seven 5} (diff/alterations state (assoc state :seven 5, :one 2))))) 45 | 46 | (testing "works with nesting" 47 | (is (= {:two {:four {:five 2}}} 48 | (diff/alterations state (assoc-in state [:two :four :five] 2)))) 49 | (is (= {:one 5, :two {:four 6}} 50 | (diff/alterations state (-> state 51 | (assoc :one 5) 52 | (assoc-in [:two :four] 6)))))) 53 | 54 | (testing "keys can be added" 55 | (is (= {:two {:four {:eight 4}}} 56 | (diff/alterations state (assoc-in state [:two :four :eight] 4))))) 57 | 58 | (testing "ignore values which are not changes or additions" 59 | (is (= {} 60 | (diff/alterations (assoc-in state [:two :four :eight] 4) state)))) 61 | 62 | (testing "nil has no special treatment" 63 | (is (= {:a 2, :b "x", :d nil, :e 2} 64 | (diff/alterations {:a 1 :b 2 :c nil :d 1 :e nil} 65 | {:a 2 :b "x" :c nil :d nil :e 2}))) 66 | (is (= {:a 2 :b []} 67 | (diff/alterations {:a 1 :b nil} 68 | {:a 2 :b []}))))) 69 | 70 | (deftest vec-alterations 71 | (testing "alterations" 72 | (is (= [2 2] (diff/alterations [1 2 3 4] [1 2 2 4]))) 73 | (is (= [0 5 3 1] (diff/alterations [1 2 3 4] [5 2 3 1]))) 74 | (is (= {:vector [0 2]} 75 | (diff/alterations state (assoc-in state [:vector 0] 2)))) 76 | (is (= {:vector [0 5 1 3]} 77 | (diff/alterations state (assoc state :vector [5 3]))))) 78 | 79 | (testing "works with nesting" 80 | (is (= [1 [:+ 5]] (diff/alterations [1 []] [1 [5]]))) 81 | (is (= [2 {:a 5}] (diff/alterations [1 2 {:a 4, :b 10}] 82 | [1 2 {:a 5, :b 10}]))) 83 | (is (= [] (diff/alterations [5 [1 2]] [5 [1 2]])))) 84 | 85 | (testing "values can be added" 86 | (is (= [:+ 1] (diff/alterations [] [1]))) 87 | (is (= [:+ 3 :+ 5] (diff/alterations [1] [1 3 5]))) 88 | (is (= [1 2 :+ 2] (diff/alterations [1 1] [1 2 2]))))) 89 | 90 | (deftest list-alterations 91 | (testing "alterations" 92 | (is (= '(2 2) (diff/alterations '(1 2 3 4) '(1 2 2 4)))) 93 | (is (= '(0 5 3 1) (diff/alterations '(1 2 3 4) '(5 2 3 1)))) 94 | (is (= {:list '(1 "x")} 95 | (diff/alterations state (assoc state :list '(4 "x" 2))))) 96 | (is (= {:list '(0 3 2 4)} 97 | (diff/alterations state (assoc state :list '(3 "by" 4)))))) 98 | 99 | (testing "works with nesting" 100 | (is (= '(1 [:+ 5]) (diff/alterations '(1 []) '(1 [5])))) 101 | (is (= '(2 {:a 5}) (diff/alterations '(1 2 {:a 4, :b 10}) 102 | '(1 2 {:a 5, :b 10})))) 103 | (is (= '() (diff/alterations '(5 [1 2]) '(5 [1 2]))))) 104 | 105 | (testing "values can be added" 106 | (is (= '(:+ 1) (diff/alterations '() '(1)))) 107 | (is (= '(:+ 3 :+ 5) (diff/alterations '(1) '(1 3 5)))) 108 | (is (= '(1 2 :+ 2) (diff/alterations '(1 1) '(1 2 2)))))) 109 | 110 | (deftest set-alterations 111 | (testing "Values can only be added, and there is no nesting" 112 | (is (= #{:a} (diff/alterations #{:c :d} #{:a :c :d}))) 113 | (is (= {:set #{:a}} 114 | (diff/alterations state (assoc state :set #{:a :b})))))) 115 | 116 | 117 | (deftest removals 118 | (testing "empty coll when there are no changes" 119 | (is (= {} (diff/removals {:a :a} {:a :a}))) 120 | (is (= [] (diff/removals [1 2] [1 2]))) 121 | (is (= #{} (diff/removals #{1 2} #{1 2}))) 122 | (is (= '() (diff/removals '(1 2) '(1 2))))) 123 | 124 | (testing "if types are different, returns empty state of same type" 125 | (is (= {} (diff/removals {:a 2} [:a 2]))) 126 | (is (= {} (diff/removals {"test" true} #{2 4}))) 127 | (is (= [] (diff/removals [1 5] '(1 5))))) 128 | 129 | (testing "return state when values are not collections" 130 | (is (= 1 (diff/removals 1 2))) 131 | (is (= true (diff/removals true false))))) 132 | 133 | (deftest map-removals 134 | (testing "removals" 135 | (is (= {:two 0, :seven 0, :vector 0, :list 0, :set 0} 136 | (diff/removals state {:one 1})))) 137 | 138 | (testing "works with nesting" 139 | (is (= {:two {:four {:five 0}}} 140 | (diff/removals state (-> state 141 | (update-in [:two :four] dissoc :five) 142 | (assoc-in [:two :four :six] false))))))) 143 | 144 | (deftest vec-removals 145 | (testing "removals" 146 | (is (= [] (diff/removals [1 2 3] [3 2 1]))) 147 | (is (= [] (diff/removals [1 2 3] [4 3 2 1]))) 148 | (is (= [2] (diff/removals [1 2 3] [1])))) 149 | 150 | (testing "works with nesting" 151 | (is (= [1 1 [1]] (diff/removals [1 [3 4 5] 6] [1 [3 5]]))) 152 | (is (= [0 1 {:a 0}] (diff/removals [1 {:a 2} 3] [1 {} 3]))))) 153 | 154 | (deftest list-removals 155 | (testing "removals" 156 | (is (= '() (diff/removals '(1 2 3) '(3 2 1)))) 157 | (is (= '() (diff/removals '(1 2 3) '(4 3 2 1)))) 158 | (is (= '(2) (diff/removals '(1 2 3) '(1)))) 159 | (is (= '(2) (diff/removals [1 2 3] '(1))))) 160 | 161 | (testing "works with nesting" 162 | (is (= '(1 1 (1)) (diff/removals '(1 (3 4 5) 6) '(1 (3 5))))) 163 | (is (= '(0 1 {:a 0}) (diff/removals '(1 {:a 2} 3) '(1 {} 3)))) 164 | (is (= '(0 1 {:a 0}) (diff/removals [1 {:a 2} 3] '(1 {} 3)))))) 165 | 166 | (deftest set-removals 167 | (testing "can only remove elements, does not support nesting" 168 | (is (= #{true} (diff/removals #{1 true "game"} #{1 "game"}))) 169 | (is (= {:set #{:b}} 170 | (diff/removals state (assoc state :set #{}))))))) 171 | -------------------------------------------------------------------------------- /test/differ/patch_test.cljc: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2014-2019 Robin Heggelund Hansen. 2 | ;; Distributed under the MIT License (http://opensource.org/licenses/MIT). 3 | 4 | (ns differ.patch-test 5 | (:require [differ.patch :as patch] 6 | #?(:clj [clojure.test :refer [is deftest testing]] 7 | :cljs [cljs.test :refer-macros [is deftest testing]]))) 8 | 9 | (let [state {:one 1 10 | :two {:three 2 11 | :four {:five "five" 12 | :six true}} 13 | :seven 3 14 | :vector [1 2 {:a 3, :some-more [3 4 true]}] 15 | :list [1 2 {:a 3, :some-more [3 4 true]}] 16 | :set #{1 true "false"}}] 17 | 18 | (deftest alterations 19 | (testing "overwrite old value when types do not match, or aren't patchable" 20 | (is (= 5 (patch/alterations 3 5))) 21 | (is (= true (patch/alterations 1 true))) 22 | (is (= {:a 1} (patch/alterations {:a {:b 2}} {:a 1}))) 23 | (is (= {:c 3, :a [2 3]} 24 | (patch/alterations {:c 3, :a {:b 2}} {:a [2 3]})))) 25 | 26 | (testing "maps" 27 | (is (= (assoc state :one 2) 28 | (patch/alterations state {:one 2}))) 29 | (is (= (-> state 30 | (assoc :seven 7) 31 | (assoc-in [:two :three] {:booya "boom"})) 32 | (patch/alterations state {:seven 7, :two {:three {:booya "boom"}}}))) 33 | (is (= (assoc state :eight [{}]) 34 | (patch/alterations state {:eight [{}]})))) 35 | 36 | (testing "vectors" 37 | (is (= [1 3 3 5 5] (patch/alterations [1 2 3 4 5] [1 3 3 5]))) 38 | (is (= [5] (patch/alterations [] [:+ 5]))) 39 | (is (= [1 2 5] (patch/alterations [2 2] [0 1 :+ 5]))) 40 | (is (= (assoc state :vector [2 2 {:a 3, :some-more [3 5 true]}]) 41 | (patch/alterations state {:vector [0 2 2 {:some-more [1 5]}]})))) 42 | 43 | (testing "lists" 44 | (is (= '(1 3 3 5 5) (patch/alterations '(1 2 3 4 5) '(1 3 3 5)))) 45 | (is (= '(5) (patch/alterations '() '(:+ 5)))) 46 | (is (= '(1 2 5) (patch/alterations '(2 2) '(0 1 :+ 5)))) 47 | (is (= (assoc state :list '(2 2 {:a 3, :some-more [3 5 true]})) 48 | (patch/alterations state {:list '(0 2 2 {:some-more [1 5]})})))) 49 | 50 | (testing "lists and vectors" 51 | (is (= [1 3 3 5 5] (patch/alterations '(1 2 3 4 5) [1 3 3 5]))) 52 | (is (= '(5) (patch/alterations [] '(:+ 5))))) 53 | 54 | (testing "sets" 55 | (is (= #{:a 4 "third"} (patch/alterations #{4 :a} #{"third"}))) 56 | (is (= (assoc state :set #{1 true "false" 2}) 57 | (patch/alterations state {:set #{2}}))))) 58 | 59 | (deftest removals 60 | (testing "maps" 61 | (is (= (dissoc state :one) 62 | (patch/removals state {:one 0}))) 63 | (is (= (-> state 64 | (dissoc :one) 65 | (update-in [:two] dissoc :four)) 66 | (patch/removals state {:one 0, :two {:four 0}})))) 67 | 68 | (testing "vectors" 69 | (is (= (assoc state :vector [1 2]) 70 | (patch/removals state {:vector [1]}))) 71 | (is (= (assoc state :vector [1 2 {:some-more [3]}]) 72 | (patch/removals state {:vector [0 2 {:a 0, :some-more [2]}]})))) 73 | 74 | (testing "lists" 75 | (is (= (assoc state :list '(1 2)) 76 | (patch/removals state {:list '(1)}))) 77 | (is (= (assoc state :list '(1 2 {:some-more [3]})) 78 | (patch/removals state {:list '(0 2 {:a 0, :some-more [2]})})))) 79 | 80 | (testing "vectors and lists" 81 | (is (= '(1) (patch/removals [1 2 3] '(2)))) 82 | (is (= [{}] (patch/removals '({:a 2} 2) [1 0 {:a 0}])))) 83 | 84 | (testing "sets" 85 | (is (= #{1} (patch/removals #{1 false} #{false}))) 86 | (is (= (assoc state :set #{1}) 87 | (patch/removals state {:set #{"false" true}})))))) 88 | --------------------------------------------------------------------------------