Skip to content

Commit 9e6942a

Browse files
committed
[BREAKING] Get smarter about diffing records
Before we treated them as maps, which meant maps and records with the same key/values were considered equal. We now only compare records if the two compared values are both records, and are of the same type. Otherwise it's a Mismatch.
1 parent 0c04153 commit 9e6942a

File tree

7 files changed

+72
-42
lines changed

7 files changed

+72
-42
lines changed

CHANGELOG.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# Unreleased
22

3-
## Added
4-
5-
## Fixed
6-
73
## Changed
84

5+
- [BREAKING] Get smarter about diffing records, instead of simply diffing them
6+
as maps. We now only recurse into records if the two compared values are both
7+
records of the same type.
8+
- Bump dependencies: fipp, rrb-vector
9+
910
# 2.11.216 (2024-02-17 / e77c3bf)
1011

1112
## Added
@@ -14,7 +15,7 @@
1415

1516
## Fixed
1617

17-
Varying key order in maps should produce a consistent diff (#47)
18+
- Varying key order in maps should produce a consistent diff (#47)
1819

1920
## Changed
2021

@@ -180,4 +181,4 @@ Varying key order in maps should produce a consistent diff (#47)
180181

181182
## Added
182183

183-
- Extracted from Kaocha, and added a top-level namespace.
184+
- Extracted from Kaocha, and added a top-level namespace.

bb.edn

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{:deps
22
{lambdaisland/deep-diff2 {:local/root "."}
33
lambdaisland/open-source {:git/url "https://github.com/lambdaisland/open-source"
4-
:git/sha "7ce125cbd14888590742da7ab3b6be9bba46fc7a"}}
4+
:git/sha "4cb6231e7df947ee8f5434e7ad48ffd8e273bcf5"}}
55
:tasks
66
{test:bb {:doc "Run babashka tests with custom runner"
77
:extra-paths ["src" "test"]

bin/kaocha

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
[[ -d "node_modules/ws" ]] || npm install ws
44

5-
exec clojure -A:dev:test -m kaocha.runner "$@"
5+
exec clojure -A:dev:test -M -m kaocha.runner "$@"

deps.edn

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{:paths ["resources" "src"]
2-
:deps {fipp/fipp {:mvn/version "0.6.26"}
3-
org.clojure/core.rrb-vector {:mvn/version "0.1.2"}
2+
:deps {fipp/fipp {:mvn/version "0.6.27"}
3+
org.clojure/core.rrb-vector {:mvn/version "0.2.0"}
44
lambdaisland/clj-diff {:mvn/version "1.4.78"}
55
mvxcvi/arrangement {:mvn/version "2.1.0"}}
66

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
"dependencies": {
55
"react": "^16.13.1",
66
"react-dom": "^16.13.1",
7-
"ws": "^8.13.0"
7+
"ws": "^8.18.0"
88
}
99
}

src/lambdaisland/deep_diff2/diff_impl.cljc

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -114,24 +114,36 @@
114114
(diff-meta exp act)))
115115

116116
(defn diff-map [exp act]
117-
(with-meta
118-
(let [exp-ks (set (keys exp))
119-
act-ks (set (keys act))]
120-
(reduce
121-
(fn [m k]
122-
(case [(contains? exp-ks k) (contains? act-ks k)]
123-
[true false]
124-
(assoc m (->Deletion k) (get exp k))
125-
[false true]
126-
(assoc m (->Insertion k) (get act k))
127-
[true true]
128-
(assoc m k (diff (get exp k) (get act k)))
129-
; `[false false]` will never occur because `k` necessarily
130-
; originated from at least one of the two sets
131-
))
132-
{}
133-
(set/union exp-ks act-ks)))
134-
(diff-meta exp act)))
117+
(if (not= (record? exp) (record? act))
118+
;; If one of them is a record, and the other one a plain map, that's a
119+
;; mismatch. The case where both of them are records, but of different
120+
;; types, is handled in [[diff]]
121+
(->Mismatch exp act)
122+
(with-meta
123+
(let [exp-ks (set (keys exp))
124+
act-ks (set (keys act))]
125+
(reduce
126+
(fn [m k]
127+
(case [(contains? exp-ks k) (contains? act-ks k)]
128+
[true false]
129+
;; The `dissoc` is only relevant for records, which at this point
130+
;; we are certain are of the same type. If the key is present in
131+
;; one and not in the other, we know it's an optional key (not part
132+
;; of the record base), and we can safely `dissoc` it while
133+
;; retaining the record type.
134+
(assoc (dissoc m k) (->Deletion k) (get exp k))
135+
[false true]
136+
(assoc m (->Insertion k) (get act k))
137+
[true true]
138+
(assoc m k (diff (get exp k) (get act k)))
139+
;; `[false false]` will never occur because `k` necessarily
140+
;; originated from at least one of the two sets
141+
))
142+
;; In case of a record, we want to preserve the type, and you can't
143+
;; call `empty` on records, so we start from `exp` and assoc/dissoc.
144+
(if (record? exp) exp {})
145+
(set/union exp-ks act-ks)))
146+
(diff-meta exp act))))
135147

136148
(defn diff-meta [exp act]
137149
(when (or (meta exp) (meta act))
@@ -160,19 +172,26 @@
160172

161173
(defn diff [exp act]
162174
(cond
175+
(= exp act)
176+
exp
177+
163178
(nil? exp)
164179
(diff-atom exp act)
165180

181+
(record? exp)
182+
(if (= (type exp) (type act))
183+
(diff-map exp act)
184+
;; Either act is not a record, or it's a record of a different type, so
185+
;; that's a mismatch
186+
(->Mismatch exp act))
187+
166188
(and (diffable? exp)
167189
(= (data/equality-partition exp) (data/equality-partition act)))
168190
(diff-similar exp act)
169191

170192
(array? exp)
171193
(diff-seq exp act)
172194

173-
(record? exp)
174-
(diff-map exp act)
175-
176195
:else
177196
(diff-atom exp act)))
178197

test/lambdaisland/deep_diff2/diff_test.cljc

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
(ns lambdaisland.deep-diff2.diff-test
2-
(:require [clojure.test :refer [deftest testing is are]]
3-
[clojure.test.check :as tc]
4-
[clojure.test.check.clojure-test :refer [defspec]]
5-
[clojure.test.check.generators :as gen]
6-
[clojure.test.check.properties :as prop]
7-
[lambdaisland.deep-diff2.diff-impl :as diff]))
2+
(:require
3+
[clojure.test :refer [deftest testing is are]]
4+
[clojure.test.check :as tc]
5+
[clojure.test.check.clojure-test :refer [defspec]]
6+
[clojure.test.check.generators :as gen]
7+
[clojure.test.check.properties :as prop]
8+
[lambdaisland.deep-diff2.diff-impl :as diff]))
89

910
(defrecord ARecord [])
11+
(defrecord BRecord [])
1012

1113
(deftest diff-test
1214
(testing "diffing atoms"
@@ -111,12 +113,20 @@
111113
(array-map :age 40 :name "Alyssa P Hacker")))))
112114

113115
(testing "records"
114-
(is (= {:a (diff/->Mismatch 1 2)}
116+
(is (= (map->ARecord {:a (diff/->Mismatch 1 2)})
115117
(diff/diff (map->ARecord {:a 1}) (map->ARecord {:a 2}))))
116-
(is (= {(diff/->Insertion :a) 1}
118+
(is (= (map->ARecord {(diff/->Insertion :a) 1})
117119
(diff/diff (map->ARecord {}) (map->ARecord {:a 1}))))
118-
(is (= {(diff/->Deletion :a) 1}
119-
(diff/diff (map->ARecord {:a 1}) (map->ARecord {}))))))
120+
(is (= (map->ARecord {(diff/->Deletion :a) 1})
121+
(diff/diff (map->ARecord {:a 1})
122+
(map->ARecord {}))))
123+
(is (= (diff/->Mismatch (map->ARecord {:a 1}) (map->BRecord {:a 1}))
124+
(diff/diff (map->ARecord {:a 1})
125+
(map->BRecord {:a 1}))))
126+
(is (= (diff/->Mismatch {:a 1} (map->ARecord {:a 1}))
127+
(diff/diff {:a 1} (map->ARecord {:a 1}))))
128+
(is (= (diff/->Mismatch (map->ARecord {:a 1}) {:a 1})
129+
(diff/diff (map->ARecord {:a 1}) {:a 1})))))
120130

121131
(is (= [{:x (diff/->Mismatch 1 2)}]
122132
(diff/diff [{:x 1}] [{:x 2}])))

0 commit comments

Comments
 (0)