Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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)))).

like image 407
Kreisquadratur Avatar asked Jul 18 '13 09:07

Kreisquadratur


3 Answers

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.

like image 194
Leon Grapenthin Avatar answered Nov 18 '22 10:11

Leon Grapenthin


I wouldn't bother even keeping the configuration maps as resources in a separate file (for each environment). Confijulate (https://github.com/bbbates/confijulate , yes - this is a personal project) lets you define all your configuration for each environment within a single namespace, and switch between them via system properties. But if you need to change values on the fly without rebuilding, Confijulate will allow you do that, too.

like image 1
brendanb Avatar answered Nov 18 '22 09:11

brendanb


I've done a fair bit of this over the past month for work. For the cases where passing a config around is not acceptable, then we've used a global config map in an atom. Early on in the application start up, the config var is swap!ed with the loaded config and after that it is left alone. This works in practice because it is effectively immutable for the life of the application. This approach may not work well for libraries, though.

I'm not sure what you mean by "No clear way to structure config sections for used libraries/frameworks". Do you want libraries to have access to the config? Regardless, I created a pipeline of config loaders that is given to the function that setups the config at start up. This allows me to separate config based on library and source.

like image 1
Jeremy Avatar answered Nov 18 '22 11:11

Jeremy