README
Table of Contents
1 Baum
Baum is an extensible configuration library for Clojure.
Baum is built on top of clojure.tools.reader.edn and offers
following features with extended EDN notation.
- referring to envirionment variables
- including other files
- binding subtree to a symbol and refer it later
- environment specific config
- etc
Actually Baum does not extend EDN syntax itself. It achives these features by reader macros and post-parsing transformations.
1.1 Setup
Add the following dependency in your project.clj file:
[rkworks/baum "0.0.1-SNAPSHOT"]
1.2 Usage
To read your config files, use read-config:
(ns user (:require [baum.core :as b])) (def config (b/read-config "path/to/config.edn")) ; a map (:foo config) (get-in config [:foo :bar])
Because slurp is used to read a file, you can pass other types:
(ns user (:require [baum.core :as b] [clojure.java.io :as io])) (def config (b/read-config (io/resource "config.edn"))) (def config2 (b/read-config (java.io.StringReader. "{:a :b}")))
1.3 Reference
1.3.1 Reader Macros
- baum/env
Read environment variables:
{:foo #baum/env :user} ; => {:foo "rkworks"}
Environ is used internally. So you can also read Java properties, a
.lein-envfile, or yourproject.clj(you needlein-envplugin). For more details, see Environ's README.You can also set fallback value:
#baum/env [:non-existent-env "not-found"] ; => "not-found" #baum/env [:non-existent-env :user "not-found"] ; => "rkworks" #baum/env ["foo"] ; => "foo" #baum/env [] ; => nil ;; real-world example {:port #baum/env [:port 3000]}
- baum/cond
You can write conditional configurations with
baum/cond.{:database #baum/cond [#baum/env :env {"prod" {:host "xxxx" :user "root" :password "aaa"} "dev" {:host "localhost" :user "root" :password "bbb"} :else {:host "localhost" :user "root" :password nil}}]}
baum/condaccepts a vector and transforms it according to the value of its first element. In the above example, if#baum/env :envis "prod", the result is:{:database {:host "xxxx" :user "root" :password "aaa"}}
If the value is neither "prod" nor "dev", the result is:
{:database {:host "localhost" :user "root" :password nil}}
- baum/env-cond
baum/env-condis syntactic sugar for combination use ofbaum/condandbaum/env.The following example is
baum/env-condversion of the above example.{:database #baum/env-cond [:env {"prod" {:host "xxxx" :user "root" :password "aaa"} "dev" {:host "localhost" :user "root" :password "bbb"} :else {:host "localhost" :user "root" :password nil}}]}
- baum/file
To embed File objects in your configuration files, you can use
baum/file:{:file #baum/file "project.clj"} ; => {:file #<File project.clj>}
- baum/resource
Your can also refer resouce files via
baum/resource:{:resource #baum/resource "config.edn"} ;; => {:resource #<URL file:/path/to/project/resources/config.edn>}
- baum/import
You can use
baum/importto import config from other files.child.edn:
{:child-key :child-val}
parent.edn:
{:parent-key #baum/import "path/to/child.edn"} ;; => {:parent-key {:child-key :child-val}}
If you want to import a resouce file, use
baum/resourcetogether:{:a #baum/import #baum/resource "config.edn"}
The reader throws an exception if you try to import a non existent file.
- baum/import*
Same as
baum/import, but returns nil when error occurs:{:a #baum/import* "non-existent-config.edn"} ; => {:a nil}
- baum/some
baum/somereturns the first logical true value of a given vector:#baum/some [nil nil 1 nil] ; => 1 #baum/some [#baum/env :non-existent-env #baum/env :user] ; => "rkworks"
Let's see the next example. In this case, if
~/.private-conf.cljexists, the result is its content, otherwise{:not-found true}#baum/some [#baum/import* "~/.private-conf.clj" {:not-found true}]
- baum/eval
To embed Clojure code in your configuration files, use
baum/eval:{:timeout #baum/eval (* 1000 60 60 24 7)} ; => {:timeout 604800000}
- baum/ref
You can refer bound variables with
baum/ref. For more details, see the explanation of :baum/let.
1.3.2 Special keys
- :baum/include
:baum/includekey merges deeply its child with its owner map.For example:
{:baum/include {:a :child} :a :parent} ; => {:a :parent}
In the above example, a reducer merges
{:a :parent}into{:a :child}.:baum/includealso accepts a vector:{:baum/include [{:a :child1} {:a :child2}] :b :parent} ; => {:a :child2 :b :parent}
In this case, the merging strategy is like the following:
(deep-merge {:a :child1} {:a :child2} {:b :parent})
Finally, it accepts all other importable values.
For example:
;; child.edn {:a :child :b :child} ;; config.edn {:baum/include "path/to/child.edn" :b :parent} ; => {:a :child :b :parent}
Of course it is possible to pass a vector of importable values:
{:baum/include ["child.edn" #baum/resource "resource.edn"] :b :parent}
- :baum/include*
Same as
:baum/include, but ignores importing errors:;; child.edn {:foo :bar} ;; config.edn {:baum/include* ["non-existent-file.edn" "child.edn"] :parent :qux} ; => {:foo :bar :parent :qux}
It is equivalent to the following operation:
(deep-merge nil {:foo :bar} {:parent :qux})
- :baum/override
The only difference between
:baum/overrideand:baum/includeis the merging strategy. In contrast to:baum/include,:baum/overridemerges children value into a parent map.In the next example, a reducer merges
{:a :child}into{:a :parent}.{:baum/include {:a :child} :a :parent} ; => {:a :child}
{:database {:host "localhost" :user "root" :password nil :adapter :h2 :baum/override #baum/cond-env [:env {"prod" #baum/import "prod-db-conf.edn" "dev" {:adapter :mysql} "test" {:user #baum/env [:db-user "root"] :password #baum/env :db-pass}}]}}
- :baum/override*
Same as
:baum/override, but ignores importing errors. See also:baum/include*. - :baum/let
You can use
:baum/letandbaum/refto make a part of your config reusable:{:baum/let [a 100] :a #baum/ref a :b {:c #baum/ref a}} ; => {:a 100 :b {:c 100}}
Destructuring is available:
{:baum/let [{:keys [a b]} {:a 100 :b 200}] :a #baum/ref a :b #baum/ref b} ;; => {:a 100 :b 200} {:baum/let [[a b] [100 200]] :a #baum/ref a :b #baum/ref b} ;; => {:a 100 :b 200}
Of course, you can use other reader macros together:
;;; a.edn {:foo :bar :baz :qux} ;;; config.edn {:baum/let [{:keys [foo baz]} #baum/import "a.edn"] :a #baum/ref foo :b #baum/ref baz} ;; => {:a :bar :b :qux}
baum/let's scope is determined by hierarchical structure of config maps:{:baum/let [a :a b :b] :d1 {:baum/let [a :d1-a c :d1-c] :a #baum/ref a :b #baum/ref b :c #baum/ref c} :a #baum/ref a :b #baum/ref b} ;; => {:d1 {:a :d1-a ;; :b :b ;; :c :d1-c} ;; :a :a ;; :b :b}
Trying to access an unavailable variable will cause an exception
{:a #baum/ref a :b {:baum/let [a 100]}} ;; => Error: "Unable to resolve symbol: a in this context"
1.3.3 Writing your own reader macros
TODO: write
1.3.4 Writing your own special keys
TODO: write
1.4 License
Copyright © 2015 Ryo Fukumuro
Distributed under the Eclipse Public License, the same as Clojure.