baum icon indicating copy to clipboard operation
baum copied to clipboard

Extensible EDSL in EDN for building self-contained configuration files

  • Baum

    [[https://circleci.com/gh/rfkm/baum][https://circleci.com/gh/rfkm/baum.svg?style=svg]]

    Baum is an extensible EDSL in EDN for building rich configuration files.

    It is built on top of [[https://github.com/clojure/tools.reader][clojure.tools.reader]] and offers the following features.

    • Basic mechanism for building simple and extensible DSL in EDN
      • Reader macros
      • Post-parsing reductions
        • A transformation of a map which has a special key to trigger it
    • Built-in DSL for writing modular and portable configuration files
      • Access to environment variables, Java system properties, =project.clj=, and so on (powered by [[https://github.com/weavejester/environ][Environ]])
      • Importing external files
      • Local variables
      • Extensible global variables
      • Conditional evaluation
        • if, some, match, ...
        • This allows you to write environment specific configurations.
      • etc...
    • Selectable reader
      • A complete Clojure reader (=clojure.tools.reader/read-string=)
      • An EDN-only reader (=clojure.tools.reader.edn/read-string=)

** Setup

Add the following dependency in your =project.clj= file:

[[http://clojars.org/rkworks/baum][http://clojars.org/rkworks/baum/latest-version.svg]]

** Reading your config file

To read your config files, use =read-file=:

#+begin_src clojure (ns your-ns (:require [baum.core :as b]))

 (def config (b/read-file "path/to/config.edn")) ; a map

 (:foo config)
 (get-in config [:foo :bar])

#+end_src

It also supports other arguments:

#+begin_src clojure (ns your-ns (:require [baum.core :as b] [clojure.java.io :as io]))

 (def config (b/read-file (io/resource "config.edn")))

 (def config2
   (b/read-file (java.io.StringReader. "{:a :b}"))) ; Same as (b/read-string "{:a :b}")

#+end_src

See =clojure.java.io/reader= for a complete list of supported arguments.

Baum uses =clojure.tools.reader/read-string= as a reader by default. If you want to use the EDN-only reader, pass an option as follows:

#+begin_src clojure (ns your-ns (:require [baum.core :as b]))

 (def config (b/read-file "path/to/config.edn"
                          {:edn? true}))

#+end_src

Even if you use the EDN-only reader, some features of Baum may compromise its safety. So you should only load trusted resources.

In addition, to disable =#baum/eval=, set =clojure.tools.reader/read-eval= to false:

#+begin_src clojure (ns your-ns (:require [baum.core :as b] [clojure.tools.reader :as r]))

 (def config (binding [r/*read-eval* false]
               (b/read-file "path/to/config.edn"
                            {:edn? true})))

#+end_src

** Examples

*** Database settings in one file

#+begin_src clojure
  {:db {
        ;; Default settings
        :adapter       "mysql"
        :database-name "baum"
        :server-name   "localhost"
        :port-number   3306
        :username      "root"
        :password      nil

        ;; Override settings per ENV
        :baum/override
        #baum/match [#baum/env :env
                     "prod" {:database-name "baum-prod"

                             ;; When DATABASE_HOST is defined, use it,
                             ;; otherwise use "localhost"
                             :server-name #baum/env [:database-host "localhost"]

                             ;; Same as above. DATABASE_USERNAME or "root"
                             :username #baum/env [:database-username "root"]

                             ;; DATABASE_PASSWORD or nil
                             :password #baum/env :database-password}
                     "dev"  {:database-name "baum-dev"}
                     "test" {:adapter "h2"}]}}
#+end_src

*** Database settings in multiple files (w/ shorthand notation)

For details about the shorthand notation, see
[[#built-in-shorthand-notation][Built-in shorthand notation]].

your_ns.clj:

#+begin_src clojure
  (ns your-ns
    (:require [baum.core :as b]
              [clojure.java.io :as io]))

  (def config (b/read-file (io/resource "config.edn")))
#+end_src

config.edn:

#+begin_src clojure
  {$let [env      #env [:env "prod"]      ; "prod" is fallback value
         env-file #str ["config-" #- env ".edn"]]

   ;; If ENV is "prod", `config-default.edn` and `config-prod.edn` will
   ;; be loaded. These files will be merged deeply (left to right).
   $include ["config-default.edn"
             #- env-file]

   ;; If `config-local.edn` exists, load it. You can put private config
   ;; here.
   $override* "config-local.edn"}
#+end_src

config-default.edn:

#+begin_src clojure
  {:db {:adapter       "mysql"
        :database-name "baum"
        :server-name   "localhost"
        :port-number   3306
        :username      "root"
        :password      nil}}
#+end_src

config-prod.edn:

#+begin_src clojure
  {:db {:database-name "baum-prod"
        :server-name   #env [:database-host "localhost"]
        :username      #env [:database-username "root"]
        :password      #env :database-password}}
#+end_src

config-dev.edn:

#+begin_src clojure
  {:db {:database-name "baum-dev"}}
#+end_src

config-local.edn:

#+begin_src clojure
  {:db {:username "foo"
        :password "mypassword"}}
#+end_src

** Aliasing

If the built-in reader macros or special keys are verbose, you can define aliases for them:

#+begin_src clojure (read-file "path/to/config.edn" {:aliases {'baum/env 'env :baum/let '$let 'baum/ref '-}}) #+end_src

Then you can rewrite your configuration as follows:

Before: #+begin_src clojure {:baum/let [user #baum/env :user loc "home"] :who #baum/ref user :where #baum/ref loc} #+end_src

After: #+begin_src clojure {$let [user #env :user loc "home"] :who #- user :where #- loc} #+end_src

*** Built-in shorthand notation

You can use built-in opinionated aliases if it is not necessary to worry
about the conflict for you. The shorthand notation is enabled by default,
but you can disable it if necessary:

#+begin_src clojure
  (b/read-file "path/to/config.edn"
               {:shorthand? false})
#+end_src

And its content is as follows:

#+begin_src clojure
  {'baum/env       'env
   'baum/str       'str
   'baum/regex     'regex
   'baum/if        'if
   'baum/match     'match
   'baum/resource  'resource
   'baum/file      'file
   'baum/files     'files
   'baum/read      'read
   'baum/read-env  'read-env
   'baum/import    'import
   'baum/import*   'import*
   'baum/some      'some
   'baum/resolve   'resolve
   'baum/eval      'eval
   'baum/ref       '-
   'baum/inspect   'inspect
   :baum/let       '$let
   :baum/include   '$include
   :baum/include*  '$include*
   :baum/override  '$override
   :baum/override* '$override*}
#+end_src

Of course, it is possible to overwrite some of them:

#+begin_src clojure
  (b/read-file "path/to/config.edn"
               {:aliases {'baum/ref '|}})
#+end_src

** Context-aware path resolver

You can refer to external files from your config file by using [[#baumimport][#baum/import]], [[#bauminclude][:baum/include]] or [[#baumoverride][:baum/override]].

Baum resolves specified paths depending on the path of the file being parsed. Paths are resolved as follows:

| parent | path | result | |------------------------------------+--------------+------------------------------------| | foo/bar.edn | baz.edn | PROJECT_ROOT/baz.edn | | foo/bar.edn | ./baz.edn | PROJECT_ROOT/foo/baz.edn | | foo/bar.edn | /tmp/baz.edn | /tmp/baz.edn | | jar:file:/foo/bar.jar!/foo/bar.edn | baz.edn | jar:file:/foo/bar.jar!/baz.edn | | jar:file:/foo/bar.jar!/foo/bar.edn | ./baz.edn | jar:file:/foo/bar.jar!/foo/baz.edn | | jar:file:/foo/bar.jar!/foo/bar.edn | /baz.edn | /baz.edn | | http://example.com/foo/bar.edn | baz.edn | http://example.com/baz.edn | | http://example.com/foo/bar.edn | ./baz.edn | http://example.com/foo/baz.edn | | http://example.com/foo/bar.edn | /baz.edn | /baz.edn | | nil | foo.edn | PROJECT_ROOT/foo.edn | | nil | ./foo.edn | PROJECT_ROOT/foo.edn | | nil | /foo.edn | /foo.edn |

If you need to access local files from files in a jar or a remote server, use [[#baumfile][#baum/file]]:

#+begin_src clojure {:baum/include #baum/file "foo.edn"} #+end_src

** Merging strategies

There are cases where multiple data are merged, such as reading an external file or overwriting a part of the setting depending on the environment. Baum does not simply call Clojure's merge, but deeply merges according to its own strategy.

*** Default merging strategy

The default merging strategy is as follows:

- Recursively merge them if both /left/ and /right/ are maps
- Otherwise, take /right/

*** Controlling merging strategies

A mechanism to control merge strategy by metadata has been added since
version 0.4.0. This is inspired by Leiningen, but the fine behavior is
different.

**** Controlling priorities

 To control priorities, use =:replace=, =:displace=:

#+BEGIN_SRC clojure
  {:a {:b :c}
   :baum/override {:a {:d :e}}}
  ;; => {:a {:b :c, :d :e}}

  {:a {:b :c}
   :baum/override {:a ^:replace {:d :e}}}
  ;; => {:a {:d :e}}

  {:a ^:displace {:b :c}
   :baum/override {:a {:d :e}}}
  ;; => {:a {:d :e}}
#+END_SRC


If you add =:replace= as metadata to /right/, /right/ will always be adopted
without merging them.

If you add =:displace= to /left/, if /left/ does not exist, /left/ is
adopted as it is, but /right/ will always be adopted as it is if /right/
exists.

**** Combining collections

 Unlike Leiningen, Baum only merges maps by default. In the merging of other
 collections like vectors or sets, /right/ is always adopted. If you want to
 combine collections, use =:append= or =:prepend=:

#+BEGIN_SRC clojure
  {:a [1 2 3]
   :baum/override {:a [4 5 6]}}
  ;; => {:a [4 5 6]}

  {:a [1 2 3]
   :baum/override {:a ^:append [4 5 6]}}
  ;; => {:a [1 2 3 4 5 6]}

  {:a [1 2 3]
   :baum/override {:a ^:prepend [4 5 6]}}
  ;; => {:a [4 5 6 1 2 3]}

  {:a #{1 2 3}
   :baum/override {:a ^:append #{4 5 6}}}
  ;; => {:a #{1 2 3 4 5 6}}
#+END_SRC

** Built-in Reader Macros

*** #baum/env

Read environment variables:

#+begin_src clojure
  {:foo #baum/env :user}                  ; => {:foo "rkworks"}
#+end_src

[[https://github.com/weavejester/environ][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 values:

#+begin_src clojure
  #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
#+end_src

*** #baum/read-env

Read environment variables and parse it as Baum-formatted data:

#+begin_src clojure
  #baum/env      :port                    ; "8080"
  #baum/read-env :port                    ; 8080
#+end_src

You can also set a fallback value like a =#baum/env=:

#+begin_src clojure
  #baum/read-env [:non-existent-env 8080]       ; => 8080
  #baum/read-env [:non-existent-env :port 8080] ; => 3000
  #baum/read-env ["foo"]                        ; => "foo"
  #baum/read-env []                             ; => nil
#+end_src

*NB!* The Baum reader does NOT parse fallback values. It parses
only values from environment variables.

*** #baum/read

Parse given strings as Baum-formatted data:

#+begin_src clojure
  #baum/read "100"                        ; => 100
  #baum/read "foo"                        ; => 'foo
  #baum/read "\"foo\""                    ; => "foo"
  #baum/read "{:foo #baum/env :user}"     ; => {:foo "rkworks"}
#+end_src

*** #baum/if

You can use a conditional sentence:

#+begin_src clojure
  {:port #baum/if [#baum/env :dev
                   3000                   ; => for dev
                   8080                   ; => for prod
                   ]}
#+end_src

A then clause is optional:

#+begin_src clojure
  {:port #baum/if [nil
                   3000]}                 ; => {:port nil}
#+end_src

*** #baum/match

You can use pattern matching with =baum/match= thanks to
=core.match=.

#+begin_src clojure
  {:database
   #baum/match [#baum/env :env
                "prod" {:host     "xxxx"
                        :user     "root"
                        :password "aaa"}
                "dev"  {:host     "localhost"
                        :user     "root"
                        :password "bbb"}
                :else  {:host     "localhost"
                        :user     "root"
                        :password nil}]}
#+end_src

=baum/case= accepts a vector and passes it to
=clojure.core.match/match=. In the above example, if
=#baum/env :env= is "prod", the result is:

#+begin_src clojure
  {:database {:host     "xxxx"
              :user     "root"
              :password "aaa"}}
#+end_src

If the value is neither "prod" nor "dev", the result is:

#+begin_src clojure
  {:database {:host     "localhost"
              :user     "root"
              :password nil}}
#+end_src

You can use more complex patterns:

#+begin_src clojure
  #baum/match [[#baum/env :env
                #baum/env :user]
               ["prod" _]        :prod-someone
               ["dev" "rkworks"] :dev-rkworks
               ["dev" _]         :dev-someone
               :else             :unknown]
#+end_src

For more details, see the documentation at
[[https://github.com/clojure/core.match][core.match]].

*** #baum/file

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

#+begin_src clojure
  {:file #baum/file "project.clj"}      ; => {:file #<File project.clj>}
#+end_src

*** #baum/resource

Your can also refer to resource files via =baum/resource=:

#+begin_src clojure
  {:resource #baum/resource "config.edn"}
  ;; => {:resource #<URL file:/path/to/project/resources/config.edn>}
#+end_src

*** #baum/files

You can obtain a list of all the files in a directory by using
=baum/files=:

#+begin_src clojure
  #baum/files "src"
  ;; => [#<File src/baum/core.clj> #<File src/baum/util.clj>]
#+end_src

You can also filter the list if required:

#+begin_src clojure
  #baum/files ["." "\\.clj$"]
  ;; => [#<File ./project.clj>
  ;;     #<File ./src/baum/core.clj>
  ;;     #<File ./src/baum/util.clj>
  ;;     #<File ./test/baum/core_test.clj>]
#+end_src

*** #baum/regex

To get an instance of =java.util.regex.Pattern=, use
=#baum/regex=:

#+begin_src clojure
  #baum/regex "^foo.*\\.clj$"       ; => #"^foo.*\.clj$"
#+end_src

It is useful only when you use the EDN reader because EDN does not
support regex literals.

*** #baum/import

You can use =baum/import= to import config from other files.

child.edn:

#+begin_src clojure
  {:child-key :child-val}
#+end_src

parent.edn:

#+begin_src clojure
  {:parent-key #baum/import "path/to/child.edn"}
  ;; => {:parent-key {:child-key :child-val}}
#+end_src

If you want to import a resource file, use =baum/resource= together:

#+begin_src clojure
  {:a #baum/import #baum/resource "config.edn"}
#+end_src

The following example shows how to import all the files in a specified
directory:

#+begin_src clojure
  #baum/import #baum/files ["config" "\\.edn$"]
#+end_src

*NB:* The reader throws an exception if you try to import a non-existent file.

*** #baum/import*

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

#+begin_src clojure
  {:a #baum/import* "non-existent-config.edn"} ; => {:a nil}
#+end_src

*** #baum/some

=baum/some= returns the first logical true value of a given
vector:

#+begin_src clojure
  #baum/some [nil nil 1 nil]              ; => 1

  #baum/some [#baum/env :non-existent-env
              #baum/env :user]            ; => "rkworks"

#+end_src

In the following example, if =~/.private-conf.clj= exists, the
result is its content, otherwise =:not-found=

#+begin_src clojure
  #baum/some [#baum/import* "~/.private-conf.clj"
              :not-found]
#+end_src

*** #baum/str

Concatenating strings:

#+begin_src clojure
  #baum/str [#baum/env :user ".edn"]      ; => "rkworks.edn"
#+end_src

*** #baum/resolve

=baum/resolve= resolves a given symbol and returns a var:

#+begin_src clojure
  {:handler #baum/resolve my-ns.routes/main-route} ; => {:handler #'my-ns.routes/main-route}
#+end_src

*** #baum/eval

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

#+begin_src clojure
  {:timeout #baum/eval (* 1000 60 60 24 7)} ; => {:timeout 604800000}
#+end_src

When =clojure.tools.reader/*read-eval*= is false, =#baum/eval= is
disabled.


*NB:* While you can use =#== to eval clojure expressions as far as
=clojure.tools.reader/*read-eval*= is true, you should still use Baum's
implementation, that is =#baum/eval=, because the official implementation
doesn't take account into using it with other Baum's reducers/readers. For
example, the following code that uses =baum/let= doesn't work:

#+begin_src clojure
  ;; NG
  {$let [v "foo"]
   :foo #=(str "*" #- v "*")} ; => error!
#+end_src

You can avoid the error using Baum's implementation instead:

#+begin_src clojure
  ;; OK
  {$let [v "foo"]
   :foo #baum/eval (str "*" #- v "*")} ; => {:foo "*foo*"}
#+end_src

*** #baum/ref

You can refer to bound variables with =baum/ref=. For more details,
see the explanation found at [[#baumlet][:baum/let]].

You can also refer to global variables:

#+begin_src clojure
  {:hostname #baum/ref HOSTNAME}          ; => {:hostname "foobar.local"}
#+end_src

Built-in global variables are defined as follows:

| Symbol      | Summary      |
|-------------+--------------|
| HOSTNAME    | host name    |
| HOSTADDRESS | host address |

It is easy to add a new variable. Just implement a new method of
multimethod =refer-global-variable=:

#+begin_src clojure
  (defmethod c/refer-global-variable 'HOME [_]
    (System/getProperty "user.home"))
#+end_src

*** #baum/inspect

=#baum/inspect= is useful for debugging:

#+begin_src clojure
  ;;; config.edn

  {:foo #baum/inspect {:baum/include [{:a :b} {:c :d}]
                       :a :foo
                       :b :bar}
   :bar :baz}


  ;;; your_ns.clj

  (b/read-file "config.edn")
  ;; This returns {:bar :baz, :foo {:a :foo, :b :bar, :c :d}}
  ;; and prints:
  ;;
  ;;  {:baum/include [{:a :b} {:c :d}], :a :foo, :b :bar}
  ;;
  ;;  ↓ ↓ ↓
  ;;
  ;;  {:b :bar, :c :d, :a :foo}
  ;;

#+end_src

** Built-in Reducers

*** :baum/include

=:baum/include= key deeply merges its child with its owner map.

For example:

#+begin_src clojure
  {:baum/include {:a :child}
   :a :parent}                        ; => {:a :parent}
#+end_src

In the above example, a reducer merges ={:a :parent}= into
={:a :child}=.

=:baum/include= also accepts a vector:

#+begin_src clojure
  {:baum/include [{:a :child1} {:a :child2}]
   :b :parent}                            ; => {:a :child2 :b :parent}
#+end_src

In this case, the merging strategy is like the following:

#+begin_src clojure
  (deep-merge {:a :child1} {:a :child2} {:b :parent})
#+end_src

Finally, it accepts all other importable values.

For example:

#+begin_src clojure
  ;; child.edn
  {:a :child
   :b :child}

  ;; config.edn
  {:baum/include "path/to/child.edn"
   :b :parent}                            ; => {:a :child :b :parent}
#+end_src

Of course, it is possible to pass a vector of importable values:

#+begin_src clojure
  {:baum/include ["child.edn"
                  #baum/resource "resource.edn"]
   :b :parent}
#+end_src

*** :baum/include*

Same as =:baum/include=, but ignores FileNotFound errors:

#+begin_src clojure
  ;; child.edn
  {:foo :bar}

  ;; config.edn
  {:baum/include* ["non-existent-file.edn" "child.edn"]
   :parent :qux}                          ; => {:foo :bar :parent :qux}
#+end_src

It is equivalent to the following operation:

#+begin_src clojure
  (deep-merge nil {:foo :bar} {:parent :qux})
#+end_src

*** :baum/override

The only difference between =:baum/override= and =:baum/include=
is the merging strategy. In contrast to =:baum/include=,
=:baum/override= merges child values into a parent map.

In the next example, a reducer merges ={:a :child}= into
={:a :parent}=.

#+begin_src clojure
  {:baum/override {:a :child}
   :a :parent}                            ; => {:a :child}
#+end_src

*** :baum/override*

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

*** :baum/let

You can use =:baum/let= and =baum/ref= to make a part of your
config reusable:

#+begin_src clojure
  {:baum/let [a 100]
   :a #baum/ref a
   :b {:c #baum/ref a}}            ; => {:a 100 :b {:c 100}}
#+end_src

Destructuring is available:

#+begin_src clojure
  {: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}
#+end_src

Of course, you can use other reader macros together:

#+begin_src clojure
  ;;; 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}
#+end_src

=baum/let='s scope is determined by hierarchical structure of
config maps:

#+begin_src clojure
  {: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}
#+end_src

You will get an error if you try to access an unavailable
variable:

#+begin_src clojure
  {:a #baum/ref a
   :b {:baum/let [a 100]}}
  ;; => Error: "Unable to resolve symbol: a in this context"
#+end_src

** Writing your own reader macros

It is very easy to write reader macros. To write your own, use =defreader=.

config.edn:

#+begin_src clojure {:foo #greet "World"} #+end_src

your_ns.clj:

#+begin_src clojure (ns your-ns (:require [baum.core :as b]))

 (b/defreader greeting-reader [v opts]
   (str "Hello, " v "!"))

 ;; Put your reader macro in reader options:
 (b/read-file "config.edn"
              {:readers {'greet greeting-reader}}) ; => {:foo "Hello, World!"}

 ;; Another way to enable your macro:
 (binding [*data-readers* (merge *data-readers*
                                 {'greet greeting-reader})]
   (b/read-file "config.edn"))

#+end_src

For more complex examples, see implementations of built-in readers.

*** Differences from Clojure's reader macro definition

If you have ever written reader macros, you may wonder why you
should use =defreader= to define them even though they are
simple unary functions.

This is because it is necessary to synchronize the evaluation
timing of reducers and reader macros. To achieve this,
=defreader= expands a definition of a reader macro like the
following:

#+begin_src clojure
  (defreader greeting-reader [v opts]
    (str "Hello, " v "!"))

  ;;; ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

  (let [f (fn [v opts]
            (str "Hello, " v "!"))]
    (defn greeting-reader [v]
      {:baum.core/invoke [f v]}))
#+end_src

So, the actual evaluation timing of your implementation is the
reduction phase and this is performed by an internal built-in
reducer.

One more thing, you can access reader options!

** Writing your own reducers

In contrast to reader macros, there is no macro to define reducers. All you need to do is define a ternary function. Consider the following reducer:

#+begin_src clojure {:your-ns/narrow [:a :c] :a :foo :b :bar :c :baz :d :qux}

 ;;; ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

 {:a :foo
  :c :baz}

#+end_src

To implement this, you could write the following:

#+begin_src clojure (ns your-ns (:require [baum.core :as b]))

 (defn narrow [m v opts]
   (select-keys m v))

 ;; Put your reducer in reader options:
 (b/read-file "config.edn"
              {:reducers {:your-ns/narrow narrow}})

#+end_src

In the above example, =v= is a value under the =:your-ns/narrow= key and =m= is a map from which the =:your-ns/narrow= key has been removed. =opts= holds reader options. So =narrow= will be called as follows:

#+begin_src clojure (narrow {:a :foo :b :bar :c :baz :d :qux} [:a :c] {...}) #+end_src

By the way, the trigger key does not have to be a keyword. Therefore, you can write, for example, the following:

#+begin_src clojure ;;; config.edn {narrow [:a :c] :a :foo :b :bar :c :baz :d :qux}

 ;;; your_ns.clj
 (b/read-file "config.edn"
              {:reducers {'narrow narrow}})

#+end_src

** License

Copyright © 2016 Ryo Fukumuro

Distributed under the Eclipse Public License, the same as Clojure.