Let’s start with an example:
basic duct config
(ns integrant-duct-example.duct-config
(:require [duct.core :as duct]
[integrant.core :as ig]))
(defmethod ig/init-key ::message-store [_ {:keys [message]}]
(atom message))
(defmethod ig/init-key ::printer [_ {:keys [store]}]
(prn (format "%s says: %s" ::printer @store)))
(derive ::message-store ::store)
(duct/load-hierarchy)
(def system-config
(duct/prep-config {:duct.profile/base {::message-store {:message "love yourself, homie"}
::printer {:store (ig/ref ::store)}}}))
(ig/init system-config)
This is almost identical to the Integrant hierarchy code block.
(duct/load-hierarchy)
is new, as is the call to duct/prep-config
.
The function duct/load-hierarchy
looks for files named duct_hierarchy.edn
on
your classpath and uses them to establish keyword hierarchies. These files look
like this:
duct_hierarchy.edn
{:sweet-tooth.endpoint.module/middleware [:duct/module]
:sweet-tooth.endpoint.module/liberator-reitit-router [:duct/module]
:sweet-tooth.endpoint.module.liberator-reitit-router/ring-router [:duct/router]
:sweet-tooth.endpoint.datomic/connection [:duct/database]}
Keys are child keywords and values are vectors of parents that the children
should derive from. It’s as if duct/load-hierarchy
is calling (derive
:sweet-tooth.endpoint.module/middleware :duct/module)
.
duct/prep-config
takes a duct config as its argument and returns an
integrant config. How does a duct config differ from an integrant config?
-
The keys for duct configs name either duct profiles or duct modules. (I
will explain these in the upcoming sections.) The keys for integrant configs
name integrant components.
-
Duct configs are meant to be passed to duct/prep-config
, which returns an
integrant config. Integrant configs are meant to be passed to ig/init
, which
initializes and returns a system.
In the example above, the duct config
{:duct.profile/base {::message-store {:message "love yourself, homie"}
::printer {:store (ig/ref ::store)}}}
yields the integrant config
{::message-store {:message "love yourself, homie"}
::printer {:store (ig/ref ::store)}
:duct.core/environment :production}
This map, where the keys are component names and values are component config,
can be used to initialize an integrant system.
|
The integrant config contains the pair :duct.core/environment :production .
prep-config adds this. What does the :duct.core/environment "component" do?
:duct.core/environment is an example of a config constant. It’s as if the
implementation of the :duct.core/environment "component" is simply the
identify function applied to the component’s config. If another component
references :duct.core/environment , it will receive the value :production . I
recommend trying this out for yourself.
(derive :duct.core/environment :duct/const)
(defmethod ig/init-key :duct/const [_ v] v)
:duct.core/environment derives from :duct/const . Duct implements
ig/init-key for :duct/const , simply returning the config value.
This relies on a cool, oft-overlooked feature of Clojure multimethods, isa?
based dispatch, which you can read about in Multimethods and Hierarchies.
Duct and Integrant make ample use of Clojure’s support for hierarchies, so it’s
worth becoming familiar with how it works. If nothing else, it’ll make you a
better Clojure programmer, putting more cools in your developer toolkit.
|
At this point, the introduction of duct config, with its :duct.profile/base
key, and the function duct/prep-config
kinda seems like a waste of time. It’s
just adding an extra layer that doesn’t do anything.
Let’s look at actually doing something useful with these new tools.