Skip to content

Instantly share code, notes, and snippets.

@clyfe
Created December 15, 2019 19:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save clyfe/6d281e166f46aa9122fadea71b4ab07f to your computer and use it in GitHub Desktop.
Save clyfe/6d281e166f46aa9122fadea71b4ab07f to your computer and use it in GitHub Desktop.
Module system extracted from Duct Core, in cljc source.
(ns modulant.core
"Make a system out of a system: `(md/exec modules-map) ;; => system-map`."
(:require
[clojure.walk :as walk]
[integrant.core :as ig]
[modulant.merge :as merge]))
;;; Logic
(defn- fold
"Fold modules into an Integrant config map, by calling each module's fn."
;; TODO assert map operations are additive
[system] (ig/fold system (fn [m _ f] (f m)) {}))
(defn build
"Turn a Modulant config map into an Integrant config map."
[config] (-> config ig/prep ig/init fold))
(defn prep
"Prep a Modulant config map into a prepped Integrant config map."
[config] (-> config build ig/prep))
(defn exec
"Init a Modulant config map into an initialized Integrant system."
[config] (-> config prep ig/init))
;;; InertRef(s)
(defrecord InertRef [key])
(defrecord InertRefSet [key])
(defn- deactivate-ref [x]
(cond
(ig/ref? x) (->InertRef (:key x))
(ig/refset? x) (->InertRefSet (:key x))
:else x))
(defn- activate-ref [x]
(cond
(instance? InertRef x) (ig/ref (:key x))
(instance? InertRefSet x) (ig/refset (:key x))
:else x))
;;; Merging profiles
(defn- expand-ancestor-keys [config base]
(reduce-kv
(fn [m k v]
(if-let [ks (seq (keys (ig/find-derived base k)))]
(reduce #(assoc %1 %2 v) m ks)
(assoc m k v)))
{}
config))
(defn- merge-configs* [a b]
(merge/meta-merge (expand-ancestor-keys a b)
(expand-ancestor-keys b a)))
(defn merge-configs
"Intelligently merge multiple configurations. Uses meta-merge and will merge
configurations in order from left to right. Generic top-level keys are merged
into more specific descendants, if the descendants exist."
[& configs]
(merge/unwrap-all (reduce merge-configs* {} configs)))
;;; Integrant
(derive :modulant.profile/base :modulant/profile)
(defmethod ig/init-key :modulant/const [_ v] v)
(defmethod ig/prep-key :modulant/module [_ profile]
;; Init profiles before modules
(assoc profile ::requires (ig/refset :modulant/profile)))
(defmethod ig/prep-key :modulant/profile [k profile]
;; InertRef(s) logic allows to disable ig/ref(s) while initializing modules,
;; such that we don't mix modules and final system during modules init.
(-> (walk/postwalk deactivate-ref profile)
;; init base before other profiles
(cond-> (not (isa? k :modulant.profile/base))
(assoc ::requires (ig/refset :modulant.profile/base)))))
(defmethod ig/init-key :modulant/profile [_ profile]
(let [profile (walk/postwalk activate-ref (dissoc profile ::requires))]
;; Profiles are modules that merge their maps into the resulting system map.
#(merge-configs % profile)))
;; TODO is this needed given ig/prep-key :modulant/profile ?
(defmethod ig/prep-key :modulant.profile/base [_ profile]
(walk/postwalk deactivate-ref profile))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment