Search code examples
configurationclojureconfigureconfiguration-managementidioms

Idiomatic config management in clojure?


What is an idiomatic way to handle application configuration in clojure?

So far I use this environment:

;; config.clj
{:k1 "v1"
 :k2 2}

;; core.clj
(defn config []
  (let [content (slurp "config.clj")]
    (binding [*read-eval* false]
      (read-string content))))

(defn -main []
  (let [config (config)]
    ...))

Which has many downside:

  • The path to config.clj might not always be resolved correctly
  • No clear way to structure config sections for used libraries/frameworks
  • Not globally accessible (@app/config) (which of course, can be seen as a good functional style way, but makes access to config across source file tedious.

Bigger open-source projects like storm seem to use YAML instead of Clojure and make the config accessible globally via a bit ugly hack: (eval ``(def ~(symbol new-name) (. Config ~(symbol name)))).


Solution

  • First use clojure.edn and in particular clojure.edn/read. E. g.

    (use '(clojure.java [io :as io]))
    (defn from-edn
      [fname]    
      (with-open [rdr (-> (io/resource fname)
                          io/reader
                          java.io.PushbackReader.)]
        (clojure.edn/read rdr)))
    

    Regarding the path of config.edn using io/resource is only one way to deal with this. Since you probably want to save an altered config.edn during runtime, you may want to rely on the fact that the path for file readers and writers constructed with an unqualified filename like

    (io/reader "where-am-i.edn")
    

    defaults to

    (System/getProperty "user.dir")
    

    Considering the fact that you may want to change the config during runtime you could implement a pattern like this (rough sketch)

    ;; myapp.userconfig
    (def default-config {:k1 "v1"
                         :k2 2})
    (def save-config (partial spit "config.edn"))
    (def load-config #(from-edn "config.edn")) ;; see from-edn above
    
    (let [cfg-state (atom (load-config))]
      (add-watch cfg-state :cfg-state-watch
        (fn [_ _ _ new-state]
          (save-config new-state)))
      (def get-userconfig #(deref cfg-state))
      (def alter-userconfig! (partial swap! cfg-state))
      (def reset-userconfig! #(reset! cfg-state default-config)))
    

    Basically this code wraps an atom that is not global and provides set and get access to it. You can read its current state and alter it like atoms with sth. like (alter-userconfig! assoc :k2 3). For global testing, you can reset! the userconfig and also inject various userconfigs into your application (alter-userconfig! (constantly {:k1 300, :k2 212})).

    Functions that need userconfig can be written like (defn do-sth [cfg arg1 arg2 arg3] ...) And be tested with various configs like default-userconfig, testconfig1,2,3... Functions that manipulate the userconfig like in a user-panel would use the get/alter..! functions.

    Also the above let wraps a watch on the userconfig that automatically updates the .edn file every time userconfig is changed. If you don't want to do this, you could add a save-userconfig! function that spits the atoms content into config.edn. However, you may want to create a way to add more watches to the atom (like re-rendering the GUI after a custom font-size has been changed) which in my opinion would break the mold of the above pattern.

    Instead, if you were dealing with a larger application, a better approach would be to define a protocol (with similar functions like in the let block) for userconfig and implement it with various constructors for a file, a database, atom (or whatever you need for testing/different use-scenarios) utilizing reify or defrecord. An instance of this could be passed around in the application and every state-manipulating/io function should use it instead of anything global.