Skip to content

Commit

Permalink
Add converge function
Browse files Browse the repository at this point in the history
  • Loading branch information
weavejester committed May 14, 2024
1 parent 638976d commit af2c916
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 28 deletions.
61 changes: 35 additions & 26 deletions src/integrant/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -475,43 +475,55 @@
(reduce-kv (fn [m k v] (assoc m k (if (keyset k) (prep-key k v) v)))
{} config))))

(defn- expansions [[k v]]
(let [m (expand-key k v)
override? (:override (meta m))]
(letfn [(gen-expansions [idx [kn vn] override?]
(defn- converge-values [[k v]]
(let [override? (:override (meta v))]
(letfn [(gen-converges [idx [kn vn] override?]
(if (and (map? vn) (not (reflike? vn)) (seq vn))
(let [override? (or override? (:override (meta vn)))]
(mapcat #(gen-expansions (conj idx kn) % override?) vn))
(mapcat #(gen-converges (conj idx kn) % override?) vn))
(list {:key k
:index (conj idx kn)
:value vn
:override? (and override? (not (map? vn)))})))]
(mapcat #(gen-expansions [] % override?) m))))
(mapcat #(gen-converges [] % override?) v))))

(defn- conflicting-expansions [expansions]
(->> expansions
(group-by :index)
(vals)
(filter #(> (count %) 1))))
(defn- one-element? [coll]
(and (seq coll) (nil? (next coll))))

(defn- conflicting-expands-exception [config expansions]
(defn- converge-conflicts [converges]
(filter (fn [conflicts]
(and (next conflicts)
(not (one-element? (filter :override? conflicts)))))
(vals (group-by :index converges))))

(defn- converge-conflict-exception [config expansions]
(let [index (-> expansions first :index)
keys (map :key expansions)]
(ex-info (str "Conflicting values at index " index " for expansions: "
(ex-info (str "Conflicting values at index " index " when converging: "
(str/join ", " keys) ". Use the ^:override metadata to "
"set the preferred value.")
{:reason ::conflicting-expands
:config config
:conflicting-index index
:expand-keys keys})))

(defn- apply-expansion [config {:keys [index value]}]
(assoc-in config index value))
(defn converge
"Deep-merge the values of a map. Raises an error on conflicting keys, unless
one (and only one) of the values is tagged with the ^:override metadata."
[m]
{:pre [(map? m) (every? map? (vals m))]}
(let [converges (mapcat converge-values m)]
(when-let [conflict (first (converge-conflicts converges))]
(throw (converge-conflict-exception m conflict)))
(->> converges
(sort-by :override?)
(sort-by #(not= (:value %) {}))
(reduce #(assoc-in %1 (:index %2) (:value %2)) {}))))

(defn expand
"Expand modules in the config map prior to initiation. The expand-key method
is applied to each entry in the map, and the results deep-merged together to
produce a new configuration.
is applied to each entry in the map, and the results deep-merged together
using converge to produce a new configuration.
If there are conflicting keys with different values, an exception will be
raised. Conflicts can be resolved by tagging one value with the :override
Expand All @@ -520,15 +532,12 @@
(expand config (keys config)))
([config keys]
{:pre [(map? config)]}
(let [expansions (mapcat expansions (select-keys config keys))
overrides (filter :override? expansions)
override-idxs (set (map :index overrides))
non-overrides (remove (comp override-idxs :index) expansions)]
(when-let [conflict (first (conflicting-expansions non-overrides))]
(throw (conflicting-expands-exception config conflict)))
(reduce apply-expansion
(apply dissoc config keys)
(concat non-overrides overrides)))))
(let [expand? (set keys)
expanded (into {} (for [[k v] config]
(if (expand? k)
[k (expand-key k v)]
[k {k v}])))]
(converge expanded))))

(defn init
"Turn a config map into an system map. Keys are traversed in dependency
Expand Down
48 changes: 46 additions & 2 deletions test/integrant/core_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,48 @@
(is (= (ig/init (ig/prep {::p {:b 2}, ::a 1}))
{::p [{:a [1], :b 2}], ::a [1]}))))

(deftest converge-test
(testing "merge"
(is (= (ig/converge {:a {:x 1}, :b {:y 2}})
{:x 1, :y 2}))
(is (= (ig/converge {:a {:x {:y 1}}, :b {:x {:z 2}}})
{:x {:y 1, :z 2}}))
(is (= (ig/converge {:a {}, :b {:y 2}})
{:y 2}))
(is (= (ig/converge {:a {:x 1}, :b {}})
{:x 1})))

(testing "overrides"
(is (= (ig/converge {:a {:x 1}, :b ^:override {:x 2}})
{:x 2}))
(is (= (ig/converge {:a {:x {:y 1}}, :b {:x ^:override {:y 2}}})
{:x {:y 2}}))
(is (= (ig/converge {:a {:x {:y 1}}, :b ^:override {:x {:y 2}}})
{:x {:y 2}})))

(testing "conflicts"
(is (thrown-with-msg?
#?(:clj clojure.lang.ExceptionInfo :cljs cljs.core.ExceptionInfo)
(re-pattern (str "^Conflicting values at index "
"\\[:x\\] when converging: :a, :b. Use the "
"\\^:override metadata to set the preferred "
"value\\.$"))
(ig/converge {:a {:x 1}, :b {:x 2}})))
(is (thrown-with-msg?
#?(:clj clojure.lang.ExceptionInfo :cljs cljs.core.ExceptionInfo)
(re-pattern (str "^Conflicting values at index "
"\\[:x\\] when converging: :a, :b. Use the "
"\\^:override metadata to set the preferred "
"value\\.$"))
(ig/converge {:a ^:override {:x 1}, :b ^:override {:x 2}})))
(is (thrown-with-msg?
#?(:clj clojure.lang.ExceptionInfo :cljs cljs.core.ExceptionInfo)
(re-pattern (str "^Conflicting values at index "
"\\[:x :y\\] when converging: :a, :b. Use the "
"\\^:override metadata to set the preferred "
"value\\.$"))
(ig/converge {:a {:x {:y 1}}, :b {:x {:y 2, :z 3}}})))))

(deftest expand-test
(testing "default expand"
(is (= (ig/expand {::unique 1})
Expand Down Expand Up @@ -277,7 +319,7 @@
#?(:clj clojure.lang.ExceptionInfo :cljs cljs.core.ExceptionInfo)
(re-pattern (str "^Conflicting values at index "
"\\[:integrant\\.core-test/a\\] "
"for expansions: :integrant\\.core-test/mod, "
"when converging: :integrant\\.core-test/mod, "
":integrant\\.core-test/mod-a\\. Use the "
"\\^:override metadata to set the preferred "
"value\\.$"))
Expand All @@ -287,7 +329,7 @@
#?(:clj clojure.lang.ExceptionInfo :cljs cljs.core.ExceptionInfo)
(re-pattern (str "^Conflicting values at index "
"\\[:integrant\\.core-test/b :v\\] "
"for expansions: :integrant\\.core-test/mod, "
"when converging: :integrant\\.core-test/mod, "
":integrant\\.core-test/mod-b\\. Use the "
"\\^:override metadata to set the preferred "
"value\\.$"))
Expand All @@ -307,6 +349,8 @@
(is (= m (ig/expand m))))
(let [m {::a (ig/refset ::b) ::b 1}]
(is (= m (ig/expand m))))))
;; => #'integrant.core-test/expand-test
;; => #'integrant.core-test/expand-test

(deftest init-test
(testing "without keys"
Expand Down

0 comments on commit af2c916

Please sign in to comment.