diff --git a/.gitignore b/.gitignore index e04714b..4e55b20 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ pom.xml.asc *.class /.lein-* /.nrepl-port +.idea +*.iml diff --git a/README.md b/README.md index 12bca1e..8455592 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ JSON Pointer https://tools.ietf.org/html/rfc6901 Usage ----- ```clojure -[clj-json-patch 0.1.7] +[clj-json-patch 0.1.8] ;; From some example namespace: (ns example.namespace diff --git a/project.clj b/project.clj index 0d020db..58f3f77 100644 --- a/project.clj +++ b/project.clj @@ -1,12 +1,12 @@ -(defproject clj-json-patch "0.1.7" +(defproject clj-json-patch "0.1.8" :description "Clojure implementation of http://tools.ietf.org/html/rfc6902" :url "http://github.com/daviddpark/clj-json-patch" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :main clj-json-patch.core - :dependencies [[org.clojure/clojure "1.10.0"] - [cheshire "5.8.0"]] + :dependencies [[org.clojure/clojure "1.11.1"] + [cheshire "5.12.0"]] :deploy-repositories [["releases" :clojars] ["snapshots" :clojars]] - :profiles {:dev {:dependencies [[midje/midje "1.9.5"]] - :plugins [[lein-midje "3.2.1"]]}}) + :profiles {:dev {:dependencies [[midje/midje "1.10.9"]] + :plugins [[lein-midje "3.2.1"]]}}) diff --git a/src/clj_json_patch/util.cljc b/src/clj_json_patch/util.cljc index d024b2e..ed07c19 100644 --- a/src/clj_json_patch/util.cljc +++ b/src/clj_json_patch/util.cljc @@ -369,40 +369,67 @@ (sanitize prefix patch))) (defn diff-vecs [obj1 obj2 prefix] - (loop [v1 obj1 - v2 obj2 - i 0 + (loop [v1 obj1 + v2 obj2 + i 0 ops []] - (cond (and (empty? v1) (empty? v2)) - ops - (and (> (count ops) 0) - (= v2 - (reduce - #(apply-patch %1 %2) v1 - (map (partial sanitize-prefix-in-patch prefix (dec i)) ops)))) - ops - (= (set v1) (set v2)) - (cond (= i (count v1)) - ops - (= (get v1 i) (get v2 i)) - (recur v1 v2 (inc i) ops) - (not= (get v1 i) (get v2 i)) - (let [moved-idx (first (filter (complement nil?) (map-indexed #(if (= (get v1 i) %2) %1) v2)))] - (recur v1 v2 (inc i) - (conj ops {"op" "move" "from" (str prefix i) "path" (str prefix moved-idx)})))) - (= v1 (rest v2)) - (conj ops (gen-op ["add" (str prefix i) (first v2)])) - (= (rest v1) v2) - (conj ops (gen-op ["remove" (str prefix i)])) - (not= (first v1) (first v2)) - (if (and (map? (first v1)) (map? (first v2))) - (recur (rest v1) (rest v2) (inc i) - (conj ops (diff* (first v1) (first v2) (str prefix i "/")))) - (recur (rest v1) (rest v2) (inc i) - (conj ops (gen-op ["replace" (str prefix i) (first v2)])))) - (and (= (first v1) (first v2)) - (not= (rest v1) (rest v2))) - (recur (rest v1) (rest v2) (inc i) ops)))) + (cond + ; Performance optimization: if most diff'ed vectors are likely to contain long shared prefixes or suffixes, + ; a straightforward equality comparison is going to be much faster than performing the + ; reduction with apply-patch as given below. + (= v1 v2) + ops + + ; v1 is a prefix of v2 => append "add" to ops and recur + (or (empty? v1) + (= v1 (rest v2))) + (recur + v1 + (rest v2) + (inc i) + (conj ops (gen-op ["add" (str prefix i) (first v2)]))) + + ; v2 is a prefix of v1 => append "remove" to ops and recur + (or (empty? v2) + (= v2 (rest v1))) + (recur + (rest v1) + v2 + ; Note: intentionally keeping i same between iterations when removing element. + i + (conj ops (gen-op ["remove" (str prefix i)]))) + + ; v2 equals v1+patches => return ops + (and (> (count ops) 0) + (= v2 + (reduce + #(apply-patch %1 %2) v1 + (map (partial sanitize-prefix-in-patch prefix (dec i)) ops)))) + ops + + ; v1 and v2 contain the same items => need to possibly move objects + (= (set v1) (set v2)) + (cond (= i (count v1)) + ops + (= (get v1 i) (get v2 i)) + (recur v1 v2 (inc i) ops) + (not= (get v1 i) (get v2 i)) + (let [moved-idx (first (filter (complement nil?) (map-indexed #(if (= (get v1 i) %2) %1) v2)))] + (recur v1 v2 (inc i) + (conj ops {"op" "move" "from" (str prefix i) "path" (str prefix moved-idx)})))) + + ; Different first elements + (not= (first v1) (first v2)) + (if (and (map? (first v1)) (map? (first v2))) + (recur (rest v1) (rest v2) (inc i) + (conj ops (diff* (first v1) (first v2) (str prefix i "/")))) + (recur (rest v1) (rest v2) (inc i) + (conj ops (gen-op ["replace" (str prefix i) (first v2)])))) + + ; Same first elements, different suffixes + (and (= (first v1) (first v2)) + (not= (rest v1) (rest v2))) + (recur (rest v1) (rest v2) (inc i) ops)))) (defn get-value-path "Traverses obj, looking for a value that matches val, returns path to value." diff --git a/test/clj_json_patch/core_test.clj b/test/clj_json_patch/core_test.clj index 78c2f14..ffd18ae 100644 --- a/test/clj_json_patch/core_test.clj +++ b/test/clj_json_patch/core_test.clj @@ -25,6 +25,12 @@ patches [{"op" "add" "path" "/foo/0" "value" "qux"}]] (fact "Adding an Array Element" (diff obj1 obj2) => patches)) + (let [obj1 {"foo" []} + obj2 {"foo" ["bar" "baz"]} + patches [{"op" "add" "path" "/foo/0" "value" "bar"} + {"op" "add" "path" "/foo/1" "value" "baz"}]] + (fact "Adding two Array Elements" + (diff obj1 obj2) => patches)) (let [obj1 {"foo" ["bar" "baz"]} obj2 {"foo" ["bar" "qux" "baz"]} patches [{"op" "add" "path" "/foo/1" "value" "qux"}]]