diff --git a/src/integrant/core.cljc b/src/integrant/core.cljc index eb6a2a5..17c7747 100644 --- a/src/integrant/core.cljc +++ b/src/integrant/core.cljc @@ -475,29 +475,31 @@ (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 @@ -505,13 +507,23 @@ :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 @@ -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 diff --git a/test/integrant/core_test.cljc b/test/integrant/core_test.cljc index 4e542ea..735e030 100644 --- a/test/integrant/core_test.cljc +++ b/test/integrant/core_test.cljc @@ -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}) @@ -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\\.$")) @@ -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\\.$")) @@ -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"