Skip to content

Commit

Permalink
Add expand-key and new expand function
Browse files Browse the repository at this point in the history
Add macro-like key expansions for grouping common behavior. See: #102.
  • Loading branch information
weavejester committed Aug 4, 2023
1 parent 98a2953 commit 3b84e55
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 2 deletions.
63 changes: 61 additions & 2 deletions src/integrant/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,14 @@

(defmethod prep-key :default [_ v] v)

(defmulti expand-key
"Expand a config value into a map that is then merged back into the config.
Defaults to returning a map `{key value}`. See: [[expand]]."
{:arglists '([key value])}
(fn [key _value] (normalize-key key)))

(defmethod expand-key :default [k v] {k v})

(defmulti init-key
"Turn a config value associated with a key into a concrete implementation.
For example, a database URL might be turned into a database connection."
Expand Down Expand Up @@ -427,10 +435,61 @@
(reduce-kv (fn [m k v] (assoc m k (if (keyset k) (prep-key k v) v)))
{} config))))

(defn- expansions [[k v]]
(mapcat (fn [[k1 v1]]
(if (map? v1)
(map (fn [[k2 v2]] {:key k, :index [k1 k2], :value v2}) v1)
(list {:key k, :index [k1], :value v1})))
(expand-key k v)))

(defn- override-expansion? [{key :key, [i0] :index}]
(= key i0))

(defn- conflicting-expansions [expansions]
(->> expansions
(group-by :index)
(vals)
(filter #(> (count %) 1))))

(defn- conflicting-expands-exception [config expansions]
(let [index (-> expansions first :index)
keys (map :key expansions)]
(ex-info (str "Conflicting index " index " for expansions: "
(str/join ", " keys))
{:reason ::conflicting-expands
:config config
:conflicting-index index
:expand-keys keys})))

(defn- apply-expansion [config {:keys [index value]}]
(assoc-in config index value))

(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 merged together to
produce a new configuration.
When merging, values that are maps will also be merged. Conflicts between
keys will generate an error, except when the key matches the expansion key;
in that case, the value will be overwritten instead."
([config]
(expand config (keys config)))
([config keys]
{:pre [(map? config)]}
(let [expansions (mapcat expansions (select-keys config keys))
overrides (filter override-expansion? 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)))))

(defn init
"Turn a config map into an system map. Keys are traversed in dependency
order, initiated via the init-key multimethod, then the refs associated with
the key are expanded."
the key are resolved."
([config]
(init config (keys config)))
([config keys]
Expand Down Expand Up @@ -458,7 +517,7 @@
"Turn a config map into a system map, reusing resources from an existing
system when it's possible to do so. Keys are traversed in dependency order,
resumed with the resume-key multimethod, then the refs associated with the
key are expanded."
key are resolved."
([config system]
(resume config system (keys config)))
([config system keys]
Expand Down
39 changes: 39 additions & 0 deletions test/integrant/core_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
(defmethod ig/prep-key ::p [_ v]
(merge {:a (ig/ref ::a)} v))

(defmethod ig/expand-key ::mod [_ v] {::a v, ::b {:v v}})
(defmethod ig/expand-key ::mod-a [_ v] {::a v})
(defmethod ig/expand-key ::mod-b [_ v] {::b {:v v}})

(defmethod ig/init-key :default [k v]
(swap! log conj [:init k v])
[v])
Expand Down Expand Up @@ -218,6 +222,41 @@
(is (= (ig/init (ig/prep {::p {:b 2}, ::a 1}))
{::p [{:a [1], :b 2}], ::a [1]}))))

(deftest expand-test
(testing "single expand"
(is (= (ig/expand {::mod 1})
{::a 1, ::b {:v 1}})))
(testing "expand with unrelated keys"
(is (= (ig/expand {::mod 1, ::b {:x 1}, ::c 2})
{::a 1, ::b {:v 1, :x 1}, ::c 2})))
(testing "expand with direct override"
(is (= (ig/expand {::mod 1, ::a 2})
{::a 2, ::b {:v 1}})))
(testing "expand with nested override"
(is (= (ig/expand {::mod 1, ::b {:v 2}})
{::a 1, ::b {:v 2}})))
(testing "unresolved conflicting index"
(is (thrown-with-msg?
#?(:clj clojure.lang.ExceptionInfo :cljs cljs.core.ExceptionInfo)
(re-pattern (str "^Conflicting index \\[:integrant\\.core-test/a\\] "
"for expansions: :integrant\\.core-test/mod, "
":integrant\\.core-test/mod-a$"))
(ig/expand {::mod 1, ::mod-a 2}))))
(testing "unresolved conflicting nested index"
(is (thrown-with-msg?
#?(:clj clojure.lang.ExceptionInfo :cljs cljs.core.ExceptionInfo)
(re-pattern (str "^Conflicting index "
"\\[:integrant\\.core-test/b :v\\] "
"for expansions: :integrant\\.core-test/mod, "
":integrant\\.core-test/mod-b$"))
(ig/expand {::mod 1, ::mod-b 2}))))
(testing "resolved conflict"
(is (= (ig/expand {::mod 1, ::mod-a 2, ::a 3})
{::a 3, ::b {:v 1}})))
(testing "resolved nested conflict"
(is (= (ig/expand {::mod 1, ::mod-b 2, ::b {:v 3}})
{::a 1, ::b {:v 3}}))))

(deftest init-test
(testing "without keys"
(reset! log [])
Expand Down

0 comments on commit 3b84e55

Please sign in to comment.