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

  1. baum/env

    Read environment variables:

    {:foo #baum/env :user}                  ; => {:foo "rkworks"}
    

    Environ is used internally. So you can also read Java properties, a .lein-env file, or your project.clj (you need lein-env plugin). 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]}
    
  2. 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/cond accepts a vector and transforms it according to the value of its first element. In the above example, if #baum/env :env is "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}}
    
  3. baum/env-cond

    baum/env-cond is syntactic sugar for combination use of baum/cond and baum/env.

    The following example is baum/env-cond version 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}}]}
    
  4. baum/file

    To embed File objects in your configuration files, you can use baum/file:

    {:file #baum/file "project.clj"}      ; => {:file #<File project.clj>}
    
  5. 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>}
    
  6. baum/import

    You can use baum/import to 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/resource together:

    {:a #baum/import #baum/resource "config.edn"}
    

    The reader throws an exception if you try to import a non existent file.

  7. baum/import*

    Same as baum/import, but returns nil when error occurs:

    {:a #baum/import* "non-existent-config.edn"} ; => {:a nil}
    
  8. baum/some

    baum/some returns 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.clj exists, the result is its content, otherwise {:not-found true}

    #baum/some [#baum/import* "~/.private-conf.clj"
                {:not-found true}]
    
  9. baum/eval

    To embed Clojure code in your configuration files, use baum/eval:

    {:timeout #baum/eval (* 1000 60 60 24 7)} ; => {:timeout 604800000}
    
  10. baum/ref

    You can refer bound variables with baum/ref. For more details, see the explanation of :baum/let.

1.3.2 Special keys

  1. :baum/include

    :baum/include key 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/include also 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}
    
  2. :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})
    
  3. :baum/override

    The only difference between :baum/override and :baum/include is the merging strategy. In contrast to :baum/include, :baum/override merges 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}}]}}
    
  4. :baum/override*

    Same as :baum/override, but ignores importing errors. See also :baum/include*.

  5. :baum/let

    You can use :baum/let and baum/ref to 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.

Author: ELSA

Created: 2015-03-21 Sat 07:30

Emacs 24.4.1 (Org mode 8.2.10)

Validate