Skip to content

Instantly share code, notes, and snippets.

@candera
Last active February 13, 2024 10:04
Show Gist options
  • Save candera/4565367 to your computer and use it in GitHub Desktop.
Save candera/4565367 to your computer and use it in GitHub Desktop.
Clojure config files

A little Clojure configuration reader

This is a handy bit of code I've written more than once. I thought I'd throw it in here so I can refer back to it later. Basically, it lets you read a config file to produce a Clojure map. The config files themselves can contain one or more forms, each of which can be either a map or a list. Maps are simply merged. Lists correspond to invocations of extension points, which in turn produces a map, which is merged along with everything else.

An Example

Consider the following files:

names.edn

{:first-name "Craig"
 :middle-name "Bert"}

;; Comments are ignored - cool!
{:last-name "Andera"
 :middle-name "Alan"} ; Later entries override earlier ones

addresses.edn

{:real-world {:city "Fairfax" :state "VA"}
 :email "[email protected]"}

craig.edn

(include "names.edn")
(include "addresses.edn")

{:favorites {:colors [:red :black]}}

Calling (read-config "/path/to/craig.edn") would produce the following map:

{:first-name "Craig"
 :last-name "Andera"
 :middle-name "Alan"}
 :real-world {:city "Fairfax" :state "VA"}
 :email "[email protected]"
 :favorites {:colors [:red :black]}}

Note that you can use relative or absolute pathnames in the (include ... calls, and they'll be resolved relative to the file they appear in

(defn form-seq
"Lazy seq of forms read from a reader"
[reader]
(let [form (read reader false reader)]
(when-not (= form reader)
(cons form (lazy-seq (form-seq reader))))))
(defmulti invoke-extension
"An extension point for operations in config files."
(fn [_ operation & _] operation))
(defn mapify
"Turns a form read from a config file at `origin` into a map."
[form origin]
(cond
(map? form) form
(list? form) (let [[operation & args] form]
(invoke-extension origin operation args))
:else (throw (ex-info (str "No support for mapifying form " (pr-str form))
{:reason :cant-mapify
:form form
:origin origin}))))
(defn read-config
"Returns a map read from `path`. Map will be generated by merging all
forms found in file. Lists are interpreted as invocations of the
`invoke-extension` multimethod, dispatched on the symbol in the
first position.
Example:
(include \"something.edn\")
;; Comments are ignored
{:foo :bar
:bar 1234}
Might yield:
{:foo :bar
:from-something 4321
:bar 1234}"
[path]
(let [source (-> path
io/file
java.io.FileReader.
java.io.PushbackReader.)]
(reduce merge (map #(mapify % path) (form-seq source)))))
(defn path-relative-to
"Given two paths `p1` and `p2`, returns a path that is the
combination of them. If `p2` is absolute, `p2` is returned.
Otherwise p1/p2 is returned."
[p1 p2]
(let [f1 (java.io.File. p1)
f2 (java.io.File. p2)]
(if (.isAbsolute f2)
p2
(.getPath (java.io.File. p1 p2)))))
(defn parent
"Returns the parent directory of `path`"
[path]
(.getParent (io/file path)))
;; E.g. (include "foo.edn") => {:some :map}
;; TODO: globbing support
(defmethod invoke-extension 'include
[origin operation [path]]
(read-config (path-relative-to (parent origin) path)))
@candera
Copy link
Author

candera commented Jan 19, 2013

I might do so at some point, but it's far enough down the priority stack that it's not likely to happen soon. I would also encourage someone else to feel free.

@johanatan
Copy link

This is simpler (and passes every test case I've thought of to throw at it)

(def rmerge (partial merge-with merge))
(defn get-config
  ([] (get-config "config.edn"))
  ([filename] (merge-with rmerge
    (edn/read-string (slurp (java.io.FileReader. (io/file (io/resource filename)))))
      (if (.exists (new java.io.File filename)) (edn/read-string (slurp filename)) {}))))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment