├── .gitignore ├── CHANGELOG.org ├── LICENSE ├── README.org ├── circle.yml ├── dev-resources ├── fixtures │ ├── bar.edn │ ├── corrupted.edn │ ├── files │ │ ├── a.clj │ │ ├── b.clj │ │ ├── c.txt │ │ └── d.clj │ ├── foo │ │ └── foo.edn │ └── parent.edn └── test.edn ├── project.clj ├── src └── baum │ ├── core.clj │ ├── merger.clj │ ├── resolver.clj │ └── util.clj └── test └── baum ├── core_test.clj ├── dummy.clj ├── merger_test.clj └── resolver_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | -------------------------------------------------------------------------------- /CHANGELOG.org: -------------------------------------------------------------------------------- 1 | * Changelog 2 | 3 | ** unreleased 4 | 5 | - Update dependencies 6 | - Change the default alias of =#baum/eval= reader from =#== to =#eval= 7 | - This fixes a bug where it is not possible to use the eval-reader with 8 | other Baum's readers/reducers. 9 | - =read-config=, =safe-read-config= is now deprecated. Use =read-file=, 10 | =safe-read-file= instead. The objective of this change is to emphasize Baum 11 | can be used any other purpose. 12 | - Built-in reducers are now public 13 | - Make the merging strategies controllable 14 | - Check if there is unexpanded reducer and expand it if it exists 15 | 16 | ** 0.3.0 / 2015-08-18 17 | 18 | - `safe-read-config` now ignores only =FileNotFoundException= 19 | - The previous design is very error-prone because any exception 20 | is implicitly ignored even if it is a parsing error. It is very 21 | hard for users to figure out why corrupted files are parsed as 22 | nil. 23 | - This change affects =#baum/import*=, =:baum/include*= and 24 | =:baum/override*=. 25 | - Added =#baum/read= 26 | - Added =#baum/read-env= 27 | 28 | ** 0.2.0 / 2015-05-11 29 | 30 | - Added context-aware path resolver 31 | 32 | ** 0.1.4 / 2015-05-11 33 | 34 | - Added =#baum/resolve= 35 | 36 | ** 0.1.3 / 2015-04-02 37 | 38 | - Initial public release 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * Baum 2 | 3 | [[https://circleci.com/gh/rfkm/baum][https://circleci.com/gh/rfkm/baum.svg?style=svg]] 4 | 5 | *Baum* is an extensible EDSL in EDN for building rich configuration files. 6 | 7 | It is built on top of 8 | [[https://github.com/clojure/tools.reader][clojure.tools.reader]] 9 | and offers the following features. 10 | 11 | - Basic mechanism for building simple and extensible DSL in EDN 12 | - Reader macros 13 | - Post-parsing reductions 14 | - A transformation of a map which has a special key to trigger it 15 | - Built-in DSL for writing modular and portable configuration files 16 | - Access to environment variables, Java system properties, 17 | =project.clj=, and so on (powered by [[https://github.com/weavejester/environ][Environ]]) 18 | - Importing external files 19 | - Local variables 20 | - Extensible global variables 21 | - Conditional evaluation 22 | - if, some, match, ... 23 | - This allows you to write environment specific configurations. 24 | - etc... 25 | - Selectable reader 26 | - A complete Clojure reader (=clojure.tools.reader/read-string=) 27 | - An EDN-only reader (=clojure.tools.reader.edn/read-string=) 28 | 29 | ** Setup 30 | 31 | Add the following dependency in your =project.clj= file: 32 | 33 | [[http://clojars.org/rkworks/baum][http://clojars.org/rkworks/baum/latest-version.svg]] 34 | 35 | ** Reading your config file 36 | 37 | To read your config files, use =read-file=: 38 | 39 | #+begin_src clojure 40 | (ns your-ns 41 | (:require [baum.core :as b])) 42 | 43 | (def config (b/read-file "path/to/config.edn")) ; a map 44 | 45 | (:foo config) 46 | (get-in config [:foo :bar]) 47 | #+end_src 48 | 49 | It also supports other arguments: 50 | 51 | #+begin_src clojure 52 | (ns your-ns 53 | (:require [baum.core :as b] 54 | [clojure.java.io :as io])) 55 | 56 | (def config (b/read-file (io/resource "config.edn"))) 57 | 58 | (def config2 59 | (b/read-file (java.io.StringReader. "{:a :b}"))) ; Same as (b/read-string "{:a :b}") 60 | #+end_src 61 | 62 | See =clojure.java.io/reader= for a complete list of supported 63 | arguments. 64 | 65 | Baum uses =clojure.tools.reader/read-string= as a reader by 66 | default. If you want to use the EDN-only reader, pass an option as 67 | follows: 68 | 69 | #+begin_src clojure 70 | (ns your-ns 71 | (:require [baum.core :as b])) 72 | 73 | (def config (b/read-file "path/to/config.edn" 74 | {:edn? true})) 75 | #+end_src 76 | 77 | Even if you use the EDN-only reader, some features of Baum may 78 | compromise its safety. So you should only load trusted resources. 79 | 80 | In addition, to disable =#baum/eval=, set 81 | =clojure.tools.reader/*read-eval*= to false: 82 | 83 | #+begin_src clojure 84 | (ns your-ns 85 | (:require [baum.core :as b] 86 | [clojure.tools.reader :as r])) 87 | 88 | (def config (binding [r/*read-eval* false] 89 | (b/read-file "path/to/config.edn" 90 | {:edn? true}))) 91 | #+end_src 92 | 93 | ** Examples 94 | 95 | *** Database settings in one file 96 | 97 | #+begin_src clojure 98 | {:db { 99 | ;; Default settings 100 | :adapter "mysql" 101 | :database-name "baum" 102 | :server-name "localhost" 103 | :port-number 3306 104 | :username "root" 105 | :password nil 106 | 107 | ;; Override settings per ENV 108 | :baum/override 109 | #baum/match [#baum/env :env 110 | "prod" {:database-name "baum-prod" 111 | 112 | ;; When DATABASE_HOST is defined, use it, 113 | ;; otherwise use "localhost" 114 | :server-name #baum/env [:database-host "localhost"] 115 | 116 | ;; Same as above. DATABASE_USERNAME or "root" 117 | :username #baum/env [:database-username "root"] 118 | 119 | ;; DATABASE_PASSWORD or nil 120 | :password #baum/env :database-password} 121 | "dev" {:database-name "baum-dev"} 122 | "test" {:adapter "h2"}]}} 123 | #+end_src 124 | 125 | *** Database settings in multiple files (w/ shorthand notation) 126 | 127 | For details about the shorthand notation, see 128 | [[#built-in-shorthand-notation][Built-in shorthand notation]]. 129 | 130 | your_ns.clj: 131 | 132 | #+begin_src clojure 133 | (ns your-ns 134 | (:require [baum.core :as b] 135 | [clojure.java.io :as io])) 136 | 137 | (def config (b/read-file (io/resource "config.edn"))) 138 | #+end_src 139 | 140 | config.edn: 141 | 142 | #+begin_src clojure 143 | {$let [env #env [:env "prod"] ; "prod" is fallback value 144 | env-file #str ["config-" #- env ".edn"]] 145 | 146 | ;; If ENV is "prod", `config-default.edn` and `config-prod.edn` will 147 | ;; be loaded. These files will be merged deeply (left to right). 148 | $include ["config-default.edn" 149 | #- env-file] 150 | 151 | ;; If `config-local.edn` exists, load it. You can put private config 152 | ;; here. 153 | $override* "config-local.edn"} 154 | #+end_src 155 | 156 | config-default.edn: 157 | 158 | #+begin_src clojure 159 | {:db {:adapter "mysql" 160 | :database-name "baum" 161 | :server-name "localhost" 162 | :port-number 3306 163 | :username "root" 164 | :password nil}} 165 | #+end_src 166 | 167 | config-prod.edn: 168 | 169 | #+begin_src clojure 170 | {:db {:database-name "baum-prod" 171 | :server-name #env [:database-host "localhost"] 172 | :username #env [:database-username "root"] 173 | :password #env :database-password}} 174 | #+end_src 175 | 176 | config-dev.edn: 177 | 178 | #+begin_src clojure 179 | {:db {:database-name "baum-dev"}} 180 | #+end_src 181 | 182 | config-local.edn: 183 | 184 | #+begin_src clojure 185 | {:db {:username "foo" 186 | :password "mypassword"}} 187 | #+end_src 188 | 189 | ** Aliasing 190 | 191 | If the built-in reader macros or special keys are verbose, you can define 192 | aliases for them: 193 | 194 | #+begin_src clojure 195 | (read-file "path/to/config.edn" 196 | {:aliases {'baum/env 'env 197 | :baum/let '$let 198 | 'baum/ref '-}}) 199 | #+end_src 200 | 201 | Then you can rewrite your configuration as follows: 202 | 203 | Before: 204 | #+begin_src clojure 205 | {:baum/let [user #baum/env :user 206 | loc "home"] 207 | :who #baum/ref user 208 | :where #baum/ref loc} 209 | #+end_src 210 | 211 | After: 212 | #+begin_src clojure 213 | {$let [user #env :user 214 | loc "home"] 215 | :who #- user 216 | :where #- loc} 217 | #+end_src 218 | 219 | *** Built-in shorthand notation 220 | 221 | You can use built-in opinionated aliases if it is not necessary to worry 222 | about the conflict for you. The shorthand notation is enabled by default, 223 | but you can disable it if necessary: 224 | 225 | #+begin_src clojure 226 | (b/read-file "path/to/config.edn" 227 | {:shorthand? false}) 228 | #+end_src 229 | 230 | And its content is as follows: 231 | 232 | #+begin_src clojure 233 | {'baum/env 'env 234 | 'baum/str 'str 235 | 'baum/regex 'regex 236 | 'baum/if 'if 237 | 'baum/match 'match 238 | 'baum/resource 'resource 239 | 'baum/file 'file 240 | 'baum/files 'files 241 | 'baum/read 'read 242 | 'baum/read-env 'read-env 243 | 'baum/import 'import 244 | 'baum/import* 'import* 245 | 'baum/some 'some 246 | 'baum/resolve 'resolve 247 | 'baum/eval 'eval 248 | 'baum/ref '- 249 | 'baum/inspect 'inspect 250 | :baum/let '$let 251 | :baum/include '$include 252 | :baum/include* '$include* 253 | :baum/override '$override 254 | :baum/override* '$override*} 255 | #+end_src 256 | 257 | Of course, it is possible to overwrite some of them: 258 | 259 | #+begin_src clojure 260 | (b/read-file "path/to/config.edn" 261 | {:aliases {'baum/ref '|}}) 262 | #+end_src 263 | 264 | ** Context-aware path resolver 265 | 266 | You can refer to external files from your config file by using [[#baumimport][#baum/import]], 267 | [[#bauminclude][:baum/include]] or [[#baumoverride][:baum/override]]. 268 | 269 | Baum resolves specified paths depending on the path of the file being parsed. 270 | Paths are resolved as follows: 271 | 272 | | parent | path | result | 273 | |------------------------------------+--------------+------------------------------------| 274 | | foo/bar.edn | baz.edn | PROJECT_ROOT/baz.edn | 275 | | foo/bar.edn | ./baz.edn | PROJECT_ROOT/foo/baz.edn | 276 | | foo/bar.edn | /tmp/baz.edn | /tmp/baz.edn | 277 | | jar:file:/foo/bar.jar!/foo/bar.edn | baz.edn | jar:file:/foo/bar.jar!/baz.edn | 278 | | jar:file:/foo/bar.jar!/foo/bar.edn | ./baz.edn | jar:file:/foo/bar.jar!/foo/baz.edn | 279 | | jar:file:/foo/bar.jar!/foo/bar.edn | /baz.edn | /baz.edn | 280 | | http://example.com/foo/bar.edn | baz.edn | http://example.com/baz.edn | 281 | | http://example.com/foo/bar.edn | ./baz.edn | http://example.com/foo/baz.edn | 282 | | http://example.com/foo/bar.edn | /baz.edn | /baz.edn | 283 | | nil | foo.edn | PROJECT_ROOT/foo.edn | 284 | | nil | ./foo.edn | PROJECT_ROOT/foo.edn | 285 | | nil | /foo.edn | /foo.edn | 286 | 287 | If you need to access local files from files in a jar or a remote 288 | server, use [[#baumfile][#baum/file]]: 289 | 290 | #+begin_src clojure 291 | {:baum/include #baum/file "foo.edn"} 292 | #+end_src 293 | 294 | ** Merging strategies 295 | 296 | There are cases where multiple data are merged, such as reading an external 297 | file or overwriting a part of the setting depending on the environment. Baum 298 | does not simply call Clojure's merge, but deeply merges according to its own 299 | strategy. 300 | 301 | *** Default merging strategy 302 | 303 | The default merging strategy is as follows: 304 | 305 | - Recursively merge them if both /left/ and /right/ are maps 306 | - Otherwise, take /right/ 307 | 308 | *** Controlling merging strategies 309 | 310 | A mechanism to control merge strategy by metadata has been added since 311 | version 0.4.0. This is inspired by Leiningen, but the fine behavior is 312 | different. 313 | 314 | **** Controlling priorities 315 | 316 | To control priorities, use =:replace=, =:displace=: 317 | 318 | #+BEGIN_SRC clojure 319 | {:a {:b :c} 320 | :baum/override {:a {:d :e}}} 321 | ;; => {:a {:b :c, :d :e}} 322 | 323 | {:a {:b :c} 324 | :baum/override {:a ^:replace {:d :e}}} 325 | ;; => {:a {:d :e}} 326 | 327 | {:a ^:displace {:b :c} 328 | :baum/override {:a {:d :e}}} 329 | ;; => {:a {:d :e}} 330 | #+END_SRC 331 | 332 | 333 | If you add =:replace= as metadata to /right/, /right/ will always be adopted 334 | without merging them. 335 | 336 | If you add =:displace= to /left/, if /left/ does not exist, /left/ is 337 | adopted as it is, but /right/ will always be adopted as it is if /right/ 338 | exists. 339 | 340 | **** Combining collections 341 | 342 | Unlike Leiningen, Baum only merges maps by default. In the merging of other 343 | collections like vectors or sets, /right/ is always adopted. If you want to 344 | combine collections, use =:append= or =:prepend=: 345 | 346 | #+BEGIN_SRC clojure 347 | {:a [1 2 3] 348 | :baum/override {:a [4 5 6]}} 349 | ;; => {:a [4 5 6]} 350 | 351 | {:a [1 2 3] 352 | :baum/override {:a ^:append [4 5 6]}} 353 | ;; => {:a [1 2 3 4 5 6]} 354 | 355 | {:a [1 2 3] 356 | :baum/override {:a ^:prepend [4 5 6]}} 357 | ;; => {:a [4 5 6 1 2 3]} 358 | 359 | {:a #{1 2 3} 360 | :baum/override {:a ^:append #{4 5 6}}} 361 | ;; => {:a #{1 2 3 4 5 6}} 362 | #+END_SRC 363 | 364 | 365 | ** Built-in Reader Macros 366 | 367 | *** #baum/env 368 | 369 | Read environment variables: 370 | 371 | #+begin_src clojure 372 | {:foo #baum/env :user} ; => {:foo "rkworks"} 373 | #+end_src 374 | 375 | [[https://github.com/weavejester/environ][Environ]] is used 376 | internally. So you can also read Java properties, a =.lein-env= 377 | file, or your =project.clj= (you need =lein-env= plugin). For 378 | more details, see Environ's README. 379 | 380 | You can also set fallback values: 381 | 382 | #+begin_src clojure 383 | #baum/env [:non-existent-env "not-found"] ; => "not-found" 384 | #baum/env [:non-existent-env :user "not-found"] ; => "rkworks" 385 | #baum/env ["foo"] ; => "foo" 386 | #baum/env [] ; => nil 387 | #+end_src 388 | 389 | *** #baum/read-env 390 | 391 | Read environment variables and parse it as Baum-formatted data: 392 | 393 | #+begin_src clojure 394 | #baum/env :port ; "8080" 395 | #baum/read-env :port ; 8080 396 | #+end_src 397 | 398 | You can also set a fallback value like a =#baum/env=: 399 | 400 | #+begin_src clojure 401 | #baum/read-env [:non-existent-env 8080] ; => 8080 402 | #baum/read-env [:non-existent-env :port 8080] ; => 3000 403 | #baum/read-env ["foo"] ; => "foo" 404 | #baum/read-env [] ; => nil 405 | #+end_src 406 | 407 | *NB!* The Baum reader does NOT parse fallback values. It parses 408 | only values from environment variables. 409 | 410 | *** #baum/read 411 | 412 | Parse given strings as Baum-formatted data: 413 | 414 | #+begin_src clojure 415 | #baum/read "100" ; => 100 416 | #baum/read "foo" ; => 'foo 417 | #baum/read "\"foo\"" ; => "foo" 418 | #baum/read "{:foo #baum/env :user}" ; => {:foo "rkworks"} 419 | #+end_src 420 | 421 | *** #baum/if 422 | 423 | You can use a conditional sentence: 424 | 425 | #+begin_src clojure 426 | {:port #baum/if [#baum/env :dev 427 | 3000 ; => for dev 428 | 8080 ; => for prod 429 | ]} 430 | #+end_src 431 | 432 | A then clause is optional: 433 | 434 | #+begin_src clojure 435 | {:port #baum/if [nil 436 | 3000]} ; => {:port nil} 437 | #+end_src 438 | 439 | *** #baum/match 440 | 441 | You can use pattern matching with =baum/match= thanks to 442 | =core.match=. 443 | 444 | #+begin_src clojure 445 | {:database 446 | #baum/match [#baum/env :env 447 | "prod" {:host "xxxx" 448 | :user "root" 449 | :password "aaa"} 450 | "dev" {:host "localhost" 451 | :user "root" 452 | :password "bbb"} 453 | :else {:host "localhost" 454 | :user "root" 455 | :password nil}]} 456 | #+end_src 457 | 458 | =baum/case= accepts a vector and passes it to 459 | =clojure.core.match/match=. In the above example, if 460 | =#baum/env :env= is "prod", the result is: 461 | 462 | #+begin_src clojure 463 | {:database {:host "xxxx" 464 | :user "root" 465 | :password "aaa"}} 466 | #+end_src 467 | 468 | If the value is neither "prod" nor "dev", the result is: 469 | 470 | #+begin_src clojure 471 | {:database {:host "localhost" 472 | :user "root" 473 | :password nil}} 474 | #+end_src 475 | 476 | You can use more complex patterns: 477 | 478 | #+begin_src clojure 479 | #baum/match [[#baum/env :env 480 | #baum/env :user] 481 | ["prod" _] :prod-someone 482 | ["dev" "rkworks"] :dev-rkworks 483 | ["dev" _] :dev-someone 484 | :else :unknown] 485 | #+end_src 486 | 487 | For more details, see the documentation at 488 | [[https://github.com/clojure/core.match][core.match]]. 489 | 490 | *** #baum/file 491 | 492 | To embed File objects in your configuration files, you can use 493 | =baum/file=: 494 | 495 | #+begin_src clojure 496 | {:file #baum/file "project.clj"} ; => {:file #} 497 | #+end_src 498 | 499 | *** #baum/resource 500 | 501 | Your can also refer to resource files via =baum/resource=: 502 | 503 | #+begin_src clojure 504 | {:resource #baum/resource "config.edn"} 505 | ;; => {:resource #} 506 | #+end_src 507 | 508 | *** #baum/files 509 | 510 | You can obtain a list of all the files in a directory by using 511 | =baum/files=: 512 | 513 | #+begin_src clojure 514 | #baum/files "src" 515 | ;; => [# #] 516 | #+end_src 517 | 518 | You can also filter the list if required: 519 | 520 | #+begin_src clojure 521 | #baum/files ["." "\\.clj$"] 522 | ;; => [# 523 | ;; # 524 | ;; # 525 | ;; #] 526 | #+end_src 527 | 528 | *** #baum/regex 529 | 530 | To get an instance of =java.util.regex.Pattern=, use 531 | =#baum/regex=: 532 | 533 | #+begin_src clojure 534 | #baum/regex "^foo.*\\.clj$" ; => #"^foo.*\.clj$" 535 | #+end_src 536 | 537 | It is useful only when you use the EDN reader because EDN does not 538 | support regex literals. 539 | 540 | *** #baum/import 541 | 542 | You can use =baum/import= to import config from other files. 543 | 544 | child.edn: 545 | 546 | #+begin_src clojure 547 | {:child-key :child-val} 548 | #+end_src 549 | 550 | parent.edn: 551 | 552 | #+begin_src clojure 553 | {:parent-key #baum/import "path/to/child.edn"} 554 | ;; => {:parent-key {:child-key :child-val}} 555 | #+end_src 556 | 557 | If you want to import a resource file, use =baum/resource= together: 558 | 559 | #+begin_src clojure 560 | {:a #baum/import #baum/resource "config.edn"} 561 | #+end_src 562 | 563 | The following example shows how to import all the files in a specified 564 | directory: 565 | 566 | #+begin_src clojure 567 | #baum/import #baum/files ["config" "\\.edn$"] 568 | #+end_src 569 | 570 | *NB:* The reader throws an exception if you try to import a non-existent file. 571 | 572 | *** #baum/import* 573 | 574 | Same as =baum/import=, but returns nil when FileNotFound error 575 | occurs: 576 | 577 | #+begin_src clojure 578 | {:a #baum/import* "non-existent-config.edn"} ; => {:a nil} 579 | #+end_src 580 | 581 | *** #baum/some 582 | 583 | =baum/some= returns the first logical true value of a given 584 | vector: 585 | 586 | #+begin_src clojure 587 | #baum/some [nil nil 1 nil] ; => 1 588 | 589 | #baum/some [#baum/env :non-existent-env 590 | #baum/env :user] ; => "rkworks" 591 | 592 | #+end_src 593 | 594 | In the following example, if =~/.private-conf.clj= exists, the 595 | result is its content, otherwise =:not-found= 596 | 597 | #+begin_src clojure 598 | #baum/some [#baum/import* "~/.private-conf.clj" 599 | :not-found] 600 | #+end_src 601 | 602 | *** #baum/str 603 | 604 | Concatenating strings: 605 | 606 | #+begin_src clojure 607 | #baum/str [#baum/env :user ".edn"] ; => "rkworks.edn" 608 | #+end_src 609 | 610 | *** #baum/resolve 611 | 612 | =baum/resolve= resolves a given symbol and returns a var: 613 | 614 | #+begin_src clojure 615 | {:handler #baum/resolve my-ns.routes/main-route} ; => {:handler #'my-ns.routes/main-route} 616 | #+end_src 617 | 618 | *** #baum/eval 619 | 620 | To embed Clojure code in your configuration files, use 621 | =baum/eval=: 622 | 623 | #+begin_src clojure 624 | {:timeout #baum/eval (* 1000 60 60 24 7)} ; => {:timeout 604800000} 625 | #+end_src 626 | 627 | When =clojure.tools.reader/*read-eval*= is false, =#baum/eval= is 628 | disabled. 629 | 630 | 631 | *NB:* While you can use =#== to eval clojure expressions as far as 632 | =clojure.tools.reader/*read-eval*= is true, you should still use Baum's 633 | implementation, that is =#baum/eval=, because the official implementation 634 | doesn't take account into using it with other Baum's reducers/readers. For 635 | example, the following code that uses =baum/let= doesn't work: 636 | 637 | #+begin_src clojure 638 | ;; NG 639 | {$let [v "foo"] 640 | :foo #=(str "*" #- v "*")} ; => error! 641 | #+end_src 642 | 643 | You can avoid the error using Baum's implementation instead: 644 | 645 | #+begin_src clojure 646 | ;; OK 647 | {$let [v "foo"] 648 | :foo #baum/eval (str "*" #- v "*")} ; => {:foo "*foo*"} 649 | #+end_src 650 | 651 | 652 | *** #baum/ref 653 | 654 | You can refer to bound variables with =baum/ref=. For more details, 655 | see the explanation found at [[#baumlet][:baum/let]]. 656 | 657 | You can also refer to global variables: 658 | 659 | #+begin_src clojure 660 | {:hostname #baum/ref HOSTNAME} ; => {:hostname "foobar.local"} 661 | #+end_src 662 | 663 | Built-in global variables are defined as follows: 664 | 665 | | Symbol | Summary | 666 | |-------------+--------------| 667 | | HOSTNAME | host name | 668 | | HOSTADDRESS | host address | 669 | 670 | It is easy to add a new variable. Just implement a new method of 671 | multimethod =refer-global-variable=: 672 | 673 | #+begin_src clojure 674 | (defmethod c/refer-global-variable 'HOME [_] 675 | (System/getProperty "user.home")) 676 | #+end_src 677 | 678 | 679 | *** #baum/inspect 680 | 681 | =#baum/inspect= is useful for debugging: 682 | 683 | #+begin_src clojure 684 | ;;; config.edn 685 | 686 | {:foo #baum/inspect {:baum/include [{:a :b} {:c :d}] 687 | :a :foo 688 | :b :bar} 689 | :bar :baz} 690 | 691 | 692 | ;;; your_ns.clj 693 | 694 | (b/read-file "config.edn") 695 | ;; This returns {:bar :baz, :foo {:a :foo, :b :bar, :c :d}} 696 | ;; and prints: 697 | ;; 698 | ;; {:baum/include [{:a :b} {:c :d}], :a :foo, :b :bar} 699 | ;; 700 | ;; ↓ ↓ ↓ 701 | ;; 702 | ;; {:b :bar, :c :d, :a :foo} 703 | ;; 704 | 705 | #+end_src 706 | 707 | ** Built-in Reducers 708 | 709 | *** :baum/include 710 | 711 | =:baum/include= key deeply merges its child with its owner map. 712 | 713 | For example: 714 | 715 | #+begin_src clojure 716 | {:baum/include {:a :child} 717 | :a :parent} ; => {:a :parent} 718 | #+end_src 719 | 720 | In the above example, a reducer merges ={:a :parent}= into 721 | ={:a :child}=. 722 | 723 | =:baum/include= also accepts a vector: 724 | 725 | #+begin_src clojure 726 | {:baum/include [{:a :child1} {:a :child2}] 727 | :b :parent} ; => {:a :child2 :b :parent} 728 | #+end_src 729 | 730 | In this case, the merging strategy is like the following: 731 | 732 | #+begin_src clojure 733 | (deep-merge {:a :child1} {:a :child2} {:b :parent}) 734 | #+end_src 735 | 736 | Finally, it accepts all other importable values. 737 | 738 | For example: 739 | 740 | #+begin_src clojure 741 | ;; child.edn 742 | {:a :child 743 | :b :child} 744 | 745 | ;; config.edn 746 | {:baum/include "path/to/child.edn" 747 | :b :parent} ; => {:a :child :b :parent} 748 | #+end_src 749 | 750 | Of course, it is possible to pass a vector of importable values: 751 | 752 | #+begin_src clojure 753 | {:baum/include ["child.edn" 754 | #baum/resource "resource.edn"] 755 | :b :parent} 756 | #+end_src 757 | 758 | *** :baum/include* 759 | 760 | Same as =:baum/include=, but ignores FileNotFound errors: 761 | 762 | #+begin_src clojure 763 | ;; child.edn 764 | {:foo :bar} 765 | 766 | ;; config.edn 767 | {:baum/include* ["non-existent-file.edn" "child.edn"] 768 | :parent :qux} ; => {:foo :bar :parent :qux} 769 | #+end_src 770 | 771 | It is equivalent to the following operation: 772 | 773 | #+begin_src clojure 774 | (deep-merge nil {:foo :bar} {:parent :qux}) 775 | #+end_src 776 | 777 | *** :baum/override 778 | 779 | The only difference between =:baum/override= and =:baum/include= 780 | is the merging strategy. In contrast to =:baum/include=, 781 | =:baum/override= merges child values into a parent map. 782 | 783 | In the next example, a reducer merges ={:a :child}= into 784 | ={:a :parent}=. 785 | 786 | #+begin_src clojure 787 | {:baum/override {:a :child} 788 | :a :parent} ; => {:a :child} 789 | #+end_src 790 | 791 | *** :baum/override* 792 | 793 | Same as =:baum/override=, but ignores FileNotFound errors. See 794 | also =:baum/include*=. 795 | 796 | *** :baum/let 797 | 798 | You can use =:baum/let= and =baum/ref= to make a part of your 799 | config reusable: 800 | 801 | #+begin_src clojure 802 | {:baum/let [a 100] 803 | :a #baum/ref a 804 | :b {:c #baum/ref a}} ; => {:a 100 :b {:c 100}} 805 | #+end_src 806 | 807 | Destructuring is available: 808 | 809 | #+begin_src clojure 810 | {:baum/let [{:keys [a b]} {:a 100 :b 200}] 811 | :a #baum/ref a 812 | :b #baum/ref b} 813 | ;; => {:a 100 :b 200} 814 | 815 | {:baum/let [[a b] [100 200]] 816 | :a #baum/ref a 817 | :b #baum/ref b} 818 | ;; => {:a 100 :b 200} 819 | #+end_src 820 | 821 | Of course, you can use other reader macros together: 822 | 823 | #+begin_src clojure 824 | ;;; a.edn 825 | {:foo :bar :baz :qux} 826 | 827 | ;;; config.edn 828 | {:baum/let [{:keys [foo baz]} #baum/import "a.edn"] 829 | :a #baum/ref foo 830 | :b #baum/ref baz} 831 | ;; => {:a :bar :b :qux} 832 | #+end_src 833 | 834 | =baum/let='s scope is determined by hierarchical structure of 835 | config maps: 836 | 837 | #+begin_src clojure 838 | {:baum/let [a :a 839 | b :b] 840 | :d1 {:baum/let [a :d1-a 841 | c :d1-c] 842 | :a #baum/ref a 843 | :b #baum/ref b 844 | :c #baum/ref c} 845 | :a #baum/ref a 846 | :b #baum/ref b} 847 | ;; => {:d1 {:a :d1-a 848 | ;; :b :b 849 | ;; :c :d1-c} 850 | ;; :a :a 851 | ;; :b :b} 852 | #+end_src 853 | 854 | You will get an error if you try to access an unavailable 855 | variable: 856 | 857 | #+begin_src clojure 858 | {:a #baum/ref a 859 | :b {:baum/let [a 100]}} 860 | ;; => Error: "Unable to resolve symbol: a in this context" 861 | #+end_src 862 | 863 | ** Writing your own reader macros 864 | 865 | It is very easy to write reader macros. To write your own, use 866 | =defreader=. 867 | 868 | config.edn: 869 | 870 | #+begin_src clojure 871 | {:foo #greet "World"} 872 | #+end_src 873 | 874 | your_ns.clj: 875 | 876 | #+begin_src clojure 877 | (ns your-ns 878 | (:require [baum.core :as b])) 879 | 880 | (b/defreader greeting-reader [v opts] 881 | (str "Hello, " v "!")) 882 | 883 | ;; Put your reader macro in reader options: 884 | (b/read-file "config.edn" 885 | {:readers {'greet greeting-reader}}) ; => {:foo "Hello, World!"} 886 | 887 | ;; Another way to enable your macro: 888 | (binding [*data-readers* (merge *data-readers* 889 | {'greet greeting-reader})] 890 | (b/read-file "config.edn")) 891 | #+end_src 892 | 893 | For more complex examples, see implementations of built-in 894 | readers. 895 | 896 | *** Differences from Clojure's reader macro definition 897 | 898 | If you have ever written reader macros, you may wonder why you 899 | should use =defreader= to define them even though they are 900 | simple unary functions. 901 | 902 | This is because it is necessary to synchronize the evaluation 903 | timing of reducers and reader macros. To achieve this, 904 | =defreader= expands a definition of a reader macro like the 905 | following: 906 | 907 | #+begin_src clojure 908 | (defreader greeting-reader [v opts] 909 | (str "Hello, " v "!")) 910 | 911 | ;;; ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 912 | 913 | (let [f (fn [v opts] 914 | (str "Hello, " v "!"))] 915 | (defn greeting-reader [v] 916 | {:baum.core/invoke [f v]})) 917 | #+end_src 918 | 919 | So, the actual evaluation timing of your implementation is the 920 | reduction phase and this is performed by an internal built-in 921 | reducer. 922 | 923 | One more thing, you can access reader options! 924 | 925 | ** Writing your own reducers 926 | 927 | In contrast to reader macros, there is no macro to define reducers. 928 | All you need to do is define a ternary function. Consider the 929 | following reducer: 930 | 931 | #+begin_src clojure 932 | {:your-ns/narrow [:a :c] 933 | :a :foo 934 | :b :bar 935 | :c :baz 936 | :d :qux} 937 | 938 | ;;; ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 939 | 940 | {:a :foo 941 | :c :baz} 942 | #+end_src 943 | 944 | To implement this, you could write the following: 945 | 946 | #+begin_src clojure 947 | (ns your-ns 948 | (:require [baum.core :as b])) 949 | 950 | (defn narrow [m v opts] 951 | (select-keys m v)) 952 | 953 | ;; Put your reducer in reader options: 954 | (b/read-file "config.edn" 955 | {:reducers {:your-ns/narrow narrow}}) 956 | #+end_src 957 | 958 | In the above example, =v= is a value under the =:your-ns/narrow= 959 | key and =m= is a map from which the =:your-ns/narrow= key has been 960 | removed. =opts= holds reader options. So =narrow= will be called as 961 | follows: 962 | 963 | #+begin_src clojure 964 | (narrow {:a :foo :b :bar :c :baz :d :qux} 965 | [:a :c] 966 | {...}) 967 | #+end_src 968 | 969 | By the way, the trigger key does not have to be a keyword. Therefore, you can 970 | write, for example, the following: 971 | 972 | #+begin_src clojure 973 | ;;; config.edn 974 | {narrow [:a :c] 975 | :a :foo 976 | :b :bar 977 | :c :baz 978 | :d :qux} 979 | 980 | ;;; your_ns.clj 981 | (b/read-file "config.edn" 982 | {:reducers {'narrow narrow}}) 983 | #+end_src 984 | 985 | ** License 986 | 987 | Copyright © 2016 Ryo Fukumuro 988 | 989 | Distributed under the Eclipse Public License, the same as Clojure. 990 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | java: 3 | version: oraclejdk7 4 | dependencies: 5 | override: 6 | - lein all deps 7 | test: 8 | override: 9 | - lein all midje 10 | deployment: 11 | snapshot: 12 | branch: master 13 | commands: 14 | - lein deploy snapshots -------------------------------------------------------------------------------- /dev-resources/fixtures/bar.edn: -------------------------------------------------------------------------------- 1 | {:bar "bar"} 2 | -------------------------------------------------------------------------------- /dev-resources/fixtures/corrupted.edn: -------------------------------------------------------------------------------- 1 | {:a} 2 | -------------------------------------------------------------------------------- /dev-resources/fixtures/files/a.clj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfkm/baum/8587a683d424e832cd74345076153a12871bd48a/dev-resources/fixtures/files/a.clj -------------------------------------------------------------------------------- /dev-resources/fixtures/files/b.clj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfkm/baum/8587a683d424e832cd74345076153a12871bd48a/dev-resources/fixtures/files/b.clj -------------------------------------------------------------------------------- /dev-resources/fixtures/files/c.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfkm/baum/8587a683d424e832cd74345076153a12871bd48a/dev-resources/fixtures/files/c.txt -------------------------------------------------------------------------------- /dev-resources/fixtures/files/d.clj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfkm/baum/8587a683d424e832cd74345076153a12871bd48a/dev-resources/fixtures/files/d.clj -------------------------------------------------------------------------------- /dev-resources/fixtures/foo/foo.edn: -------------------------------------------------------------------------------- 1 | {:foo #baum/import "../bar.edn"} 2 | -------------------------------------------------------------------------------- /dev-resources/fixtures/parent.edn: -------------------------------------------------------------------------------- 1 | {:parent #baum/import "./foo/foo.edn"} 2 | -------------------------------------------------------------------------------- /dev-resources/test.edn: -------------------------------------------------------------------------------- 1 | {:foo :bar} 2 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject rkworks/baum "0.4.1-SNAPSHOT" 2 | :description "Extensible EDSL in EDN for building self-contained configuration files" 3 | :url "https://github.com/rfkm/baum" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :jvm-opts ["-XX:-OmitStackTraceInFastThrow"] 7 | :repositories {"sonatype-oss-public" "https://oss.sonatype.org/content/groups/public/"} 8 | :deploy-repositories [["snapshots" {:url "https://clojars.org/repo/" 9 | :username [:gpg :env] 10 | :password [:gpg :env]}] 11 | ["releases" {:url "https://clojars.org/repo/" 12 | :creds :gpg}]] 13 | :dependencies [[environ "1.1.0"] 14 | [me.raynes/fs "1.4.6"] 15 | [org.clojure/core.match "0.3.0-alpha4"] 16 | [org.clojure/tools.reader "1.0.0-alpha2"]] 17 | :profiles {:dev {:dependencies [[org.clojure/clojure "1.8.0"] 18 | [midje "1.8.3"]] 19 | :plugins [[lein-midje "3.2.1"] 20 | [lein-environ "1.1.0"]] 21 | :env {:env "dev"}} 22 | :1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]} 23 | :1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]} 24 | :1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]} 25 | :1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]} 26 | :master {:dependencies [[org.clojure/clojure "1.9.0-master-SNAPSHOT"] 27 | [midje "1.9.0-alpha6"]]}} 28 | :aliases {"all" ["with-profile" "+1.5:+1.6:+1.7:+1.8:+master"]}) 29 | -------------------------------------------------------------------------------- /src/baum/core.clj: -------------------------------------------------------------------------------- 1 | (ns baum.core 2 | (:refer-clojure :exclude [read-string]) 3 | (:require [baum.resolver :as resolver] 4 | [baum.merger :as merger] 5 | [baum.util :as u] 6 | [clojure.core.match :as m] 7 | [clojure.java.io :as io] 8 | [clojure.pprint :refer [pprint]] 9 | [clojure.set :as set] 10 | [clojure.tools.reader :as r] 11 | [clojure.tools.reader.edn :as edn] 12 | [clojure.walk :as w] 13 | [environ.core :refer [env]])) 14 | 15 | (def ^:dynamic ^:private *context* 16 | "Holds variables bound by `baum/let`." 17 | {}) 18 | 19 | (defn- delegate-to-reducer [reducer-key name args & body] 20 | `(let [f# (fn ~name ~args ~@body)] 21 | (defn ~name [v#] 22 | {~reducer-key [f# v#]}))) 23 | 24 | (defmacro defreader [name args & body] 25 | (apply delegate-to-reducer ::invoke name args body)) 26 | 27 | (defmacro deflazyreader [name args & body] 28 | (apply delegate-to-reducer ::lazy-invoke name args body)) 29 | 30 | ;;; 31 | ;;; Readers 32 | ;;; 33 | 34 | (declare reduction) 35 | 36 | (deflazyreader inspect-reader [v opts] 37 | (let [reduced (reduction v opts) 38 | before (with-out-str (pprint v)) 39 | after (with-out-str (pprint reduced))] 40 | (printf "\n%s\n↓ ↓ ↓\n\n%s\n" before after) 41 | reduced)) 42 | 43 | (defreader env-reader [v opts] 44 | (let [vs (if-not (vector? v) [v nil] v)] ; Add nil as fallback 45 | (or (some env (butlast vs)) 46 | (last vs)))) 47 | 48 | (defreader resource-reader [v opts] 49 | (io/resource v)) 50 | 51 | (defreader file-reader [v opts] 52 | (io/file v)) 53 | 54 | (defreader files-reader [v opts] 55 | (let [[path & [re]] (u/vectorize v) 56 | re (or re #"") 57 | re (if (string? re) (re-pattern re) re)] 58 | (->> path 59 | io/file 60 | file-seq 61 | (filter #(.isFile ^java.io.File %)) 62 | (filter #(re-find re (str %))) 63 | sort 64 | vec))) 65 | 66 | (defreader str-reader [v opts] 67 | (apply str v)) 68 | 69 | (defreader regex-reader [v opts] 70 | (re-pattern v)) 71 | 72 | (defreader eval-reader [v opts] 73 | (when-not r/*read-eval* 74 | (throw (ex-info "eval-reader not allowed when *read-eval* is false" 75 | {:value v}))) 76 | (eval v)) 77 | 78 | (defreader resolve-reader [v opts] 79 | (u/resolve-var! v)) 80 | 81 | (deflazyreader some-reader [v opts] 82 | (let [vs (u/vectorize v)] 83 | (some #(reduction % opts) vs))) 84 | 85 | (deflazyreader if-reader [[test then & [else]] opts] 86 | (if (reduction test opts) then else)) 87 | 88 | (deflazyreader match-reader [[vars & clauses] opts] 89 | (let [protect (partial u/map-every-nth #(list 'quote %) 2)] 90 | (eval `(m/match ~(reduction vars opts) ~@(protect clauses))))) 91 | 92 | (declare refer-global-variable) 93 | 94 | (defreader ref-reader [v opts] 95 | {:pre ((symbol? v))} 96 | (if (contains? *context* v) 97 | (*context* v) 98 | (refer-global-variable v))) 99 | 100 | (declare read-file safe-read-file) 101 | 102 | (defn- import-multiple [v importer] 103 | (->> v 104 | u/vectorize 105 | (map importer) 106 | (remove nil?) 107 | (reduce merger/merge-tree))) 108 | 109 | (defn- import-file [v opts] 110 | (import-multiple v #(if ((some-fn map? nil?) %) 111 | % 112 | (read-file % opts)))) 113 | 114 | (defn- import-file* [v opts] 115 | (import-multiple v #(if ((some-fn map? nil?) %) 116 | % 117 | (safe-read-file % opts nil)))) 118 | 119 | (defreader import-reader [v opts] 120 | (import-file v opts)) 121 | 122 | (defreader import-reader* [v opts] 123 | (import-file* v opts)) 124 | 125 | (declare read-string) 126 | 127 | (defreader read-reader [v opts] 128 | (read-string opts v)) 129 | 130 | (defreader read-env-reader [v opts] 131 | (let [vs (if-not (vector? v) [v nil] v)] ; Add nil as fallback 132 | (or (read-string (some env (butlast vs))) 133 | (last vs)))) 134 | 135 | 136 | ;;; 137 | ;;; Reduction 138 | ;;; 139 | 140 | (defn- apply-reducers [reducers m get-reducer opts] 141 | (let [key-set (set (keys reducers))] 142 | (reduce (fn [acc-m k] 143 | (if-let [f (get-reducer reducers k)] 144 | (let [reduced (f (dissoc acc-m k) (acc-m k) opts)] 145 | (if (not= reduced acc-m) 146 | (reduction reduced opts) 147 | reduced)) 148 | acc-m)) 149 | m 150 | (or (and (map? m) 151 | (seq (filter key-set (keys m)))) 152 | [])))) 153 | 154 | (defn- apply-lazy-reducer [reducers m opts] 155 | (apply-reducers reducers 156 | m 157 | (fn [hs k] 158 | (let [h (k hs)] 159 | (and (map? h) 160 | (:lazy h)))) 161 | opts)) 162 | 163 | (defn- apply-eager-reducer [reducers m opts] 164 | (apply-reducers reducers 165 | m 166 | (fn [hs k] 167 | (let [h (k hs)] 168 | (or (and (map? h) 169 | (:eager h)) 170 | (and (fn? h) 171 | h)))) 172 | opts)) 173 | 174 | (defn reduction [m opts] 175 | (let [reducers (:reducers opts)] 176 | (w/walk #(reduction % opts) 177 | #(apply-eager-reducer reducers % opts) 178 | (apply-lazy-reducer reducers m opts)))) 179 | 180 | 181 | ;;; 182 | ;;; Reducers 183 | ;;; 184 | 185 | (defn reduce-invoke [m [f v] opts] 186 | (f v opts)) 187 | 188 | (defn reduce-include [m v opts] 189 | (import-file (conj (u/vectorize v) m) opts)) 190 | 191 | (defn reduce-include* [m v opts] 192 | (import-file* (conj (u/vectorize v) m) opts)) 193 | 194 | (defn reduce-override [m v opts] 195 | (import-file (into [m] (u/vectorize v)) opts)) 196 | 197 | (defn reduce-override* [m v opts] 198 | (import-file* (into [m] (u/vectorize v)) opts)) 199 | 200 | (defn- reduce-bindings [bindings opts] 201 | (reduce (fn [acc [k v]] 202 | (binding [*context* (merge *context* 203 | (apply hash-map acc))] 204 | (conj acc k (eval `(let ~acc ~(reduction v opts)))))) 205 | [] 206 | (partition 2 bindings))) 207 | 208 | (defn- create-context [bindings opts] 209 | (let [protect (partial u/map-every-nth #(list 'quote %) 2)] 210 | (-> bindings 211 | protect 212 | destructure 213 | (reduce-bindings opts) 214 | (->> (apply hash-map))))) 215 | 216 | (defn reduce-let [m v opts] 217 | ;; XXX: Lexical scope is better? There is no function call in the 218 | ;; static file world (except for `eval`), so there may 219 | ;; be no difference between lexical scope and dynamic scope 220 | ;; in almost all cases. However, it may be a problem that 221 | ;; scopes are visible from other imported files. 222 | (binding [*context* (merge *context* 223 | (create-context v opts))] 224 | (reduction m opts))) 225 | 226 | ;;; 227 | ;;; Global Variables 228 | ;;; 229 | 230 | (defmulti refer-global-variable identity) 231 | 232 | (defmethod refer-global-variable :default [sym] 233 | (throw (ex-info (format "Unable to resolve symbol: %s in this context" sym) 234 | {:context *context*}))) 235 | 236 | (defmethod refer-global-variable 'HOSTNAME [_] 237 | (.. java.net.InetAddress getLocalHost getHostName)) 238 | 239 | (defmethod refer-global-variable 'HOSTADDRESS [_] 240 | (.. java.net.InetAddress getLocalHost getHostAddress)) 241 | 242 | ;;; -- 243 | 244 | (defn- apply-transformers [m opts] 245 | (reduce (fn [acc f] 246 | (f acc opts)) 247 | m 248 | (:transformers opts))) 249 | 250 | (defn default-readers [& [opts]] 251 | {'baum/env env-reader 252 | 'baum/str str-reader 253 | 'baum/regex regex-reader 254 | 'baum/if if-reader 255 | 'baum/match match-reader 256 | 'baum/resource resource-reader 257 | 'baum/file file-reader 258 | 'baum/files files-reader 259 | 'baum/read read-reader 260 | 'baum/read-env read-env-reader 261 | 'baum/import import-reader 262 | 'baum/import* import-reader* 263 | 'baum/some some-reader 264 | 'baum/resolve resolve-reader 265 | 'baum/eval eval-reader 266 | 'baum/ref ref-reader 267 | 'baum/inspect inspect-reader}) 268 | 269 | (defn default-reducers [& [opts]] 270 | {::invoke reduce-invoke ; internal 271 | ::lazy-invoke {:lazy reduce-invoke} ; internal 272 | :baum/let {:lazy reduce-let} 273 | :baum/include reduce-include 274 | :baum/include* reduce-include* 275 | :baum/override reduce-override 276 | :baum/override* reduce-override*}) 277 | 278 | (def default-aliases 279 | {'baum/env 'env 280 | 'baum/str 'str 281 | 'baum/regex 'regex 282 | 'baum/if 'if 283 | 'baum/match 'match 284 | 'baum/resource 'resource 285 | 'baum/file 'file 286 | 'baum/files 'files 287 | 'baum/read 'read 288 | 'baum/read-env 'read-env 289 | 'baum/import 'import 290 | 'baum/import* 'import* 291 | 'baum/some 'some 292 | 'baum/resolve 'resolve 293 | 'baum/eval 'eval 294 | 'baum/ref '- 295 | 'baum/inspect 'inspect 296 | :baum/let '$let 297 | :baum/include '$include 298 | :baum/include* '$include* 299 | :baum/override '$override 300 | :baum/override* '$override*}) 301 | 302 | (defn default-transformers [& [opts]] 303 | [reduction]) 304 | 305 | (defn default-options [opts] 306 | {:readers (merge (default-readers opts) 307 | *data-readers*) 308 | :reducers (default-reducers opts) 309 | :transformers [] 310 | :aliases {} 311 | :shorthand? true 312 | :edn? false}) 313 | 314 | (defn apply-aliases [{:keys [aliases] :as opts}] 315 | (-> opts 316 | (update-in [:reducers] u/alias-keys aliases) 317 | (update-in [:readers] u/alias-keys aliases))) 318 | 319 | (defn- apply-default-aliases [{:keys [shorthand?] :as opts}] 320 | (if (:shorthand? opts) 321 | (update-in opts [:aliases] #(u/deep-merge default-aliases %)) 322 | opts)) 323 | 324 | (defn update-options [opts] 325 | (-> (default-options opts) 326 | (u/deep-merge (or opts {})) 327 | (update-in [:transformers] concat (default-transformers opts)) 328 | apply-default-aliases 329 | apply-aliases)) 330 | 331 | ;;; 332 | ;;; API 333 | ;;; 334 | 335 | (defn read-string 336 | "Read the given string as a Baum-formatted string. The acceptable options are 337 | the following: 338 | 339 | :readers - A map of readers. See `default-readers`. 340 | :reducers - A map of reducers. See `default-reducers`. 341 | :aliases - A map of aliases. See `default-aliases`. 342 | :shorthand? - Whether to enable the shorthand notation. In other words, 343 | whether to enable the default alias settings. 344 | :edn? - Whether to enable the EDN-only reader." 345 | ([s] 346 | (read-string {:eof nil} s)) 347 | ([opts s] 348 | (let [opts (update-options opts)] 349 | (if (:edn? opts) 350 | (apply-transformers (edn/read-string opts s) opts) 351 | (binding [r/*data-readers* (merge r/*data-readers* (opts :readers))] 352 | (apply-transformers (r/read-string s) opts)))))) 353 | 354 | (defn read-file 355 | "Read a Baum-formatted file. See `read-string` for the list of available 356 | options." 357 | ([file] 358 | (read-file file {})) 359 | ([file opts] 360 | (resolver/with-resolved [target file] 361 | (read-string opts (slurp target))))) 362 | 363 | (defn safe-read-file 364 | "Same as `read-file`, but returns alt when file doesn't exist." 365 | {:arglists 366 | '([file alt] [file opts alt])} 367 | [& args] 368 | (try 369 | (apply read-file (butlast args)) 370 | (catch java.io.FileNotFoundException e 371 | (last args)))) 372 | 373 | (defn read-config 374 | "DEPRECATED: Use 'read-file' instead. 375 | Read a Baum-formatted file. See `read-string` for the list of available 376 | options." 377 | {:deprecated "0.4.0" 378 | :arglists 379 | '([file] [file opts])} 380 | [& args] 381 | (println "Warning: read-config is deprecated; use read-file") 382 | (apply read-file args)) 383 | 384 | (defn safe-read-config 385 | "DEPRECATED: Use 'safe-read-file' instead. 386 | Same as `read-config`, but returns alt when file doesn't exist." 387 | {:deprecated "0.4.0" 388 | :arglists 389 | '([file alt] [file opts alt])} 390 | [& args] 391 | (println "Warning: safe-read-config is deprecated; use safe-read-file") 392 | (apply safe-read-file args)) 393 | -------------------------------------------------------------------------------- /src/baum/merger.clj: -------------------------------------------------------------------------------- 1 | (ns baum.merger 2 | (:require [clojure.set :as set] 3 | [baum.util :as u])) 4 | 5 | ;; Borrowed the idea of controlling the merging strategy based on metadata from 6 | ;; Leiningen 7 | 8 | (def ^:private +priority-exactly+ 2) 9 | (def ^:private +priority-any+ 1) 10 | 11 | (defn- matcher-priority [matcher] 12 | (::priority (meta matcher))) 13 | 14 | (defn- with-matcher-priority [matcher p] 15 | (with-meta matcher {::priority p})) 16 | 17 | (defn- compare-matchers [a b] 18 | (let [[alp arp] (matcher-priority a) 19 | [blp brp] (matcher-priority b) 20 | ret (compare (+ alp arp) 21 | (+ blp brp))] 22 | (if (zero? ret) 23 | ;; right wins 24 | (compare arp brp) 25 | ret))) 26 | 27 | (defn- ->sub-matcher [matcher] 28 | (cond (keyword? matcher) 29 | (with-matcher-priority 30 | #(-> % meta matcher) 31 | +priority-exactly+) 32 | 33 | (vector? matcher) 34 | (with-matcher-priority 35 | (->> matcher 36 | (map ->sub-matcher) 37 | (apply every-pred)) 38 | (* +priority-exactly+ (count matcher))) 39 | 40 | (= matcher '_) 41 | (with-matcher-priority 42 | (constantly true) 43 | +priority-any+) 44 | 45 | (fn? matcher) 46 | (with-matcher-priority 47 | matcher 48 | +priority-exactly+) 49 | 50 | :else 51 | (throw (ex-info "Found an incorrect matcher" {:matcher matcher})))) 52 | 53 | (defn- ->matcher [matcher] 54 | (cond (and (vector? matcher) (= 2 (count matcher))) 55 | (let [[left-matcher right-matcher] (map ->sub-matcher matcher)] 56 | (with-matcher-priority 57 | (fn [left right] 58 | (and (left-matcher left) 59 | (right-matcher right))) 60 | [(matcher-priority left-matcher) 61 | (matcher-priority right-matcher)])) 62 | 63 | (= matcher '_) 64 | (with-matcher-priority 65 | (constantly true) 66 | [+priority-any+ +priority-any+]) 67 | 68 | (fn? matcher) 69 | (with-matcher-priority 70 | matcher 71 | [+priority-exactly+ +priority-exactly+]) 72 | 73 | :else 74 | (throw (ex-info "Found an incorrect matcher" {:matcher matcher})))) 75 | 76 | (defn- dispatch-matcher [multimethod-var left right] 77 | (let [candidates (remove #{:default} (keys (methods (deref multimethod-var))))] 78 | (->> candidates 79 | (filter #((->matcher %) left right)) 80 | (sort-by ->matcher compare-matchers) 81 | reverse 82 | first))) 83 | 84 | (defn need-to-apply-matchers? [multimethod-var left right] 85 | (if-let [matcher (dispatch-matcher multimethod-var left right)] 86 | (not= :default matcher) 87 | false)) 88 | 89 | ;; Using multimethods to implement the merging strategies might be the not good 90 | ;; idea since that makes it harder to control the priority of rules. While I 91 | ;; would like to keep this for the better customizability, I might rethink in 92 | ;; the future. 93 | (defmulti prioritized-merge (partial dispatch-matcher #'prioritized-merge)) 94 | 95 | (defmulti merge-colls (partial dispatch-matcher #'merge-colls)) 96 | 97 | (defn merge-tree 98 | ([] nil) 99 | ([left] left) 100 | ([left right] 101 | (cond 102 | (need-to-apply-matchers? #'prioritized-merge left right) 103 | (prioritized-merge left right) 104 | 105 | (need-to-apply-matchers? #'merge-colls left right) 106 | (merge-colls left right) 107 | 108 | (and (map? left) (map? right)) 109 | (merge-with merge-tree left right) 110 | 111 | :else 112 | right)) 113 | ([left right & more] 114 | (reduce merge-tree (into [left right] more)))) 115 | 116 | 117 | ;; 118 | ;; Rules 119 | ;; 120 | 121 | (defmethod prioritized-merge [:displace '_] [left right] 122 | right) 123 | 124 | (defmethod prioritized-merge ['_ :displace] [left right] 125 | (u/with-meta-safe left (merge (meta left) (meta right)))) 126 | 127 | (defmethod prioritized-merge ['_ :replace] [left right] 128 | right) 129 | 130 | (defmethod merge-colls [coll? [coll? :append]] [left right] 131 | (into (empty right) (concat left right))) 132 | 133 | (defmethod merge-colls [coll? [coll? :prepend]] [left right] 134 | (into (empty right) (concat right left))) 135 | -------------------------------------------------------------------------------- /src/baum/resolver.clj: -------------------------------------------------------------------------------- 1 | (ns baum.resolver 2 | (:refer-clojure :exclude [resolve]) 3 | (:require [clojure.java.io :as io] 4 | [clojure.string :as str] 5 | [me.raynes.fs :as fs]) 6 | (:import [java.io File] 7 | [java.net URI URL MalformedURLException])) 8 | 9 | (def ^:dynamic *current-file* 10 | "A current file being parsed" 11 | nil) 12 | 13 | (defprotocol Resolver 14 | (resolve [parent target])) 15 | 16 | (defn- sep->slash [s] 17 | (str/replace s File/separator "/")) 18 | 19 | (defn- relative-path? [path] 20 | (and (string? path) 21 | (or (.startsWith ^String path "./") 22 | (.startsWith ^String path "../")))) 23 | 24 | (defn- absolute-path? [path] 25 | (and (string? path) 26 | (.startsWith ^String path "/"))) 27 | 28 | (defmulti resolve-uri 29 | (fn [^URI parent ^String target] 30 | (keyword "baum.resolver" (.getScheme parent)))) 31 | 32 | (defmethod resolve-uri ::jar [^URI parent ^String target] 33 | (if (absolute-path? target) 34 | target 35 | (let [target (if (relative-path? target) 36 | target 37 | (str "/" target)) 38 | path (.getSchemeSpecificPart parent) 39 | idx (.lastIndexOf path "!/") 40 | local (subs path (+ 1 idx)) 41 | ctx (subs path 0 idx)] 42 | (str "jar:" ctx "!" 43 | (sep->slash (resolve (io/file local) target)))))) 44 | 45 | (defmethod resolve-uri ::http [^URI parent ^String target] 46 | (if (absolute-path? target) 47 | target 48 | (str (.resolve parent (if (relative-path? target) 49 | target 50 | (str "/" target)))))) 51 | 52 | (derive ::https ::http) 53 | (derive ::ftp ::http) 54 | 55 | (defmethod resolve-uri :default [^URI parent ^String target] 56 | (if (relative-path? target) 57 | (str (.resolve parent (if (relative-path? target) 58 | target 59 | (str "/" target)))) 60 | target)) 61 | 62 | 63 | ;;; 64 | ;;; Default impls 65 | ;;; 66 | 67 | (extend-protocol Resolver 68 | String 69 | (resolve [parent target] 70 | (try 71 | (resolve (URL. parent) target) 72 | (catch MalformedURLException e 73 | (resolve (File. parent) target)))) 74 | 75 | File 76 | (resolve [parent target] 77 | (if (relative-path? target) 78 | (.getCanonicalPath (io/file (.getParentFile parent) target)) 79 | target)) 80 | 81 | URL 82 | (resolve [parent target] 83 | (resolve (.toURI parent) target)) 84 | 85 | URI 86 | (resolve [parent target] 87 | (resolve-uri parent target)) 88 | 89 | nil 90 | (resolve [parent target] 91 | target) 92 | 93 | Object 94 | (resolve [parent target] 95 | target)) 96 | 97 | (defn expand-path [path] 98 | (if (string? path) 99 | (->> (fs/expand-home path) 100 | (resolve *current-file*)) 101 | path)) 102 | 103 | (defmacro with-resolved [[sym target] & body] 104 | `(let [target# (expand-path ~target)] 105 | (binding [*current-file* target#] 106 | (let [~sym target#] 107 | ~@body)))) 108 | -------------------------------------------------------------------------------- /src/baum/util.clj: -------------------------------------------------------------------------------- 1 | (ns baum.util 2 | (:import java.io.FileNotFoundException)) 3 | 4 | (defn with-meta-safe 5 | [obj m] 6 | (if (instance? clojure.lang.IObj obj) 7 | (with-meta obj m) 8 | obj)) 9 | 10 | (defn deep-merge 11 | [& vals] 12 | (if (every? map? vals) 13 | (apply merge-with deep-merge vals) 14 | (last vals))) 15 | 16 | (defn map-every-nth [f n coll] 17 | (map-indexed (fn [i v] 18 | (if (zero? (mod (inc i) n)) 19 | (f v) 20 | v)) 21 | coll)) 22 | 23 | (defn vectorize [v] 24 | (if (vector? v) v [v])) 25 | 26 | (defn alias-key [m [original new]] 27 | (if (contains? m original) 28 | (assoc m new (get m original)) 29 | m)) 30 | 31 | (defn alias-keys [map aliases] 32 | (reduce alias-key map aliases)) 33 | 34 | (defn- parse-ns-var! [ns-var] 35 | (if-let [ns (namespace (symbol ns-var))] 36 | [(symbol ns) (symbol (name ns-var))] 37 | (throw (IllegalArgumentException. 38 | (str "Invalid format:\n\n\t" 39 | ns-var 40 | "\n\nns-var must be of the form: '/'."))))) 41 | 42 | (defn- resolve-ns-var! [ns-sym var-sym] 43 | (try (require ns-sym :reload) 44 | (catch FileNotFoundException e 45 | (throw (IllegalArgumentException. 46 | (format "Unable to load ns: %s/%s" ns-sym var-sym))))) 47 | (or (ns-resolve ns-sym var-sym) 48 | (throw (IllegalArgumentException. 49 | (format "Unable to load var: %s/%s" ns-sym var-sym))))) 50 | 51 | (defn resolve-var! [sym] 52 | (apply resolve-ns-var! (parse-ns-var! sym))) 53 | -------------------------------------------------------------------------------- /test/baum/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns baum.core-test 2 | (:require [baum.core :as c] 3 | baum.dummy 4 | [clojure.java.io :as io] 5 | [clojure.tools.reader :as r] 6 | [environ.core :refer [env]] 7 | [midje.sweet :refer :all])) 8 | 9 | 10 | ;;; 11 | ;;; prep 12 | ;;; 13 | 14 | (defn rs 15 | ([s] (rs {} s)) 16 | ([opts s] 17 | (let [[a b] (map #(c/read-string (merge opts %) s) 18 | [{:edn? true} {:edn? false}])] 19 | (fact a => b) 20 | a))) 21 | 22 | ;;; 23 | ;;; Tests 24 | ;;; 25 | 26 | (facts "can read a simple map" 27 | (let [test-conf-path "dev-resources/test.edn" 28 | ret {:foo :bar}] 29 | (fact "from a string path" 30 | (c/read-file test-conf-path) => ret) 31 | 32 | (fact "from a file object" 33 | (c/read-file (io/file test-conf-path)) => ret) 34 | 35 | (fact "from a url object" 36 | (c/read-file (io/as-url (io/file test-conf-path))) => ret))) 37 | 38 | 39 | (facts "can read files with reader macros" 40 | (fact "inst" 41 | (rs "{:a #inst \"1989-10-29\"}") => {:a #inst "1989-10-29"}) 42 | 43 | (fact "env" 44 | (rs "{:a #baum/env :user}") => {:a (env :user)}) 45 | 46 | (fact "env from project.clj" 47 | (rs "{:a #baum/env :env}") => {:a "dev"}) 48 | 49 | (fact "env with fallback" 50 | (rs "#baum/env [:non-existent-env \"foo\"]") => "foo" 51 | (rs "#baum/env [:non-existent-env :user \"foo\"]") => (env :user) 52 | (rs "#baum/env [\"foo\"]") => "foo" 53 | (rs "#baum/env []") => nil) 54 | 55 | (fact "str" 56 | (rs "#baum/str [\"foo\" \"bar\"]") => "foobar") 57 | 58 | (fact "if" 59 | (rs "#baum/if [#baum/env :user :foo #baum/eval (throw (Exception. \"foo\"))]") => :foo 60 | (rs "#baum/if [#baum/env :non-existent #baum/eval (throw (Exception. \"foo\")) :foo]") => :foo) 61 | 62 | (fact "match" 63 | (let [s "{:a #baum/match [[#baum/env :env 0] 64 | [\"dev\" 0] {:a :a} 65 | [\"prod\" _] {:b :b} 66 | :else #baum/eval (throw (Exception. \"unknown\"))]}"] 67 | (rs s) => {:a {:a :a}} 68 | (provided (env :env) => "dev") 69 | 70 | (rs s) => {:a {:b :b}} 71 | (provided (env :env) => "prod") 72 | 73 | (rs s) => (throws "unknown") 74 | (provided (env :env) => "unknown") 75 | 76 | (rs "{:a #baum/match [#baum/env :env]}") => (throws #"No matching clause"))) 77 | 78 | (fact "match is safe" 79 | (rs "#baum/match [:a 80 | :a (str \"foo\" \"bar\")]") => '(str "foo" "bar")) 81 | 82 | (fact "resource" 83 | (rs "{:a #baum/resource \"test.edn\"}") => {:a (io/resource "test.edn")}) 84 | 85 | (fact "file" 86 | (rs "{:a #baum/file \"dev-resources/test.edn\"}") 87 | => {:a (io/file "dev-resources/test.edn")}) 88 | 89 | (fact "files" 90 | (rs "#baum/files [\"dev-resources/fixtures/files\" \"clj$\"]") 91 | => [(io/file "dev-resources/fixtures/files/a.clj") 92 | (io/file "dev-resources/fixtures/files/b.clj") 93 | (io/file "dev-resources/fixtures/files/d.clj")]) 94 | 95 | (fact "files + filter" 96 | (rs "#baum/files [\"src\" \"core.*clj$\"]") 97 | => [(io/file "src/baum/core.clj")] 98 | 99 | (rs "#baum/files [\"src\" #baum/regex \"core.*clj$\"]") 100 | => [(io/file "src/baum/core.clj")]) 101 | 102 | (fact "some" 103 | (rs "{:a #baum/some [nil nil 1 nil]}") => {:a 1} 104 | (rs "{:a #baum/some [#baum/env :non-existent-env 105 | #baum/env :user]}") 106 | => {:a (env :user)} 107 | 108 | ;; If lazy evaluation works correctly, no exception will occurs. 109 | (rs "#baum/some [#baum/env :user 110 | #baum/eval (throw (Exception. \"foo\"))]") 111 | => "rkworks" 112 | (provided (env :user) => "rkworks")) 113 | 114 | (fact "resolve" 115 | (let [v baum.dummy/foo] 116 | (remove-ns 'baum.dummy) 117 | @(rs "#baum/resolve baum.dummy/foo") => v)) 118 | 119 | (fact "eval" 120 | (rs "{:a #baum/eval (+ 1 2)}") => {:a 3}) 121 | 122 | (fact "*read-eval*" 123 | (binding [r/*read-eval* false] 124 | (rs "{:a #baum/eval (+ 1 2)}")) => (throws #"not allowed")) 125 | 126 | (fact "regex" 127 | (rs "#baum/regex \"^foo*$\" ") => #"^foo*$") 128 | 129 | (fact "read" 130 | (rs "#baum/read \"100\" ") => 100 131 | (rs "#baum/read \"{:a \\\"b\\\"}\" ") => {:a "b"} 132 | (rs "#baum/read \"#baum/read \\\"100\\\"\" ") => 100) 133 | 134 | (fact "read-env" 135 | (rs "#baum/read-env :port") => 100 136 | (rs "#baum/read-env [:port2 100]") => 100 137 | (background 138 | (env :port) => "100" 139 | (env :port2) => nil)) 140 | 141 | (fact "import" 142 | (rs "{:parent #baum/import \"child.edn\"}") => {:parent {:a :b}} 143 | (provided 144 | (slurp "child.edn") => "{:a :b}")) 145 | 146 | (fact "nested import" 147 | (rs "{:parent #baum/import \"child.edn\"}") => {:parent {:a {:a :b}}} 148 | (provided 149 | (slurp "child.edn") => "{:a #baum/import \"child2.edn\"}" 150 | (slurp "child2.edn") => "{:a :b}")) 151 | 152 | (fact "multiple import" 153 | (rs "{:parent #baum/import [\"child.edn\" \"child2.edn\"]}") 154 | => {:parent {:a {:a :b2} :c :d}} 155 | (provided 156 | (slurp "child.edn") => "{:a {:a :b} :c :d}" 157 | (slurp "child2.edn") => "{:a {:a :b2}}")) 158 | 159 | (fact "import with a relative path" 160 | (rs "#baum/import \"dev-resources/fixtures/parent.edn\"") => {:parent {:foo {:bar "bar"}}} 161 | (rs "#baum/import #baum/resource \"fixtures/parent.edn\"") => {:parent {:foo {:bar "bar"}}}) 162 | 163 | (fact "Throws an exception when trying to import non-existent files" 164 | (rs "{:parent #baum/import \"child.edn\"}") => (throws java.io.FileNotFoundException)) 165 | 166 | (fact "Returns nil when trying to import* non-existent files" 167 | (rs "{:parent #baum/import* \"child.edn\"}") => {:parent nil}) 168 | 169 | (fact "Throws an exception when trying to import* corrupted files" 170 | (rs "{:parent #baum/import* \"dev-resources/fixtures/corrupted.edn\"}") => (throws clojure.lang.ExceptionInfo)) 171 | 172 | (fact "inspect" 173 | (let [res "\n{:baum/include {:a :b}, :a :c}\n\n↓ ↓ ↓\n\n{:a :c}\n\n"] 174 | (with-out-str (rs "#baum/inspect {:baum/include {:a :b} :a :c}")) 175 | => (str res res)) 176 | 177 | (binding [*out* (new java.io.StringWriter)] 178 | (rs "#baum/inspect {:baum/include {:a :b} :a :c}")) 179 | => {:a :c}) 180 | 181 | (fact "custom reader" 182 | (rs {:readers {'foo (constantly :foo)}} 183 | "{:a #foo :b}") 184 | => {:a :foo}) 185 | 186 | (fact "custom reader crated by defreader" 187 | (c/defreader constantly-foo [v opts] 188 | :foo) 189 | (rs {:readers {'foo constantly-foo}} 190 | "{:a #foo :b}") 191 | => {:a :foo}) 192 | 193 | (fact "custom reader via *data-readers*" 194 | (binding [*data-readers* {'foo (constantly :foo)}] 195 | (rs "{:a #foo :b}")) 196 | => {:a :foo}) 197 | 198 | (fact "import with custom reader" 199 | (rs {:readers {'foo (constantly :foo)}} 200 | "{:parent #baum/import \"dev-resources/foo.edn\"}") 201 | => {:parent {:a :foo}} 202 | (provided 203 | (slurp "dev-resources/foo.edn") => "{:a #foo :b}")) 204 | 205 | (fact "readers can return nil" 206 | (c/defreader void-reader [v opts] nil) 207 | (c/deflazyreader void-lazy-reader [v opts] nil) 208 | (rs {:readers {'void void-reader}} 209 | "{:a #void :b}") 210 | => {:a nil} 211 | (rs {:readers {'void void-lazy-reader}} 212 | "{:a #void :b}") 213 | => {:a nil})) 214 | 215 | 216 | (facts "can read files with special keys" 217 | (let [f #(c/reduction % {:reducers (c/default-reducers)})] 218 | (fact "include - map" 219 | (f {:baum/include {:a 100 220 | :b 200 221 | :baum/include {:c 400}} 222 | :a 200 223 | :d 500}) => {:a 200 :b 200 :c 400 :d 500}) 224 | 225 | (fact "include - list" 226 | (f {:baum/include {:a 100 :b 200 227 | :baum/include [{:c 400 :c2 200} 228 | {:c2 100}]} 229 | :a 200 230 | :d 500}) => {:a 200 :b 200 :c 400 :c2 100 :d 500}) 231 | 232 | (fact "include - file" 233 | (f {:baum/include "child.edn" 234 | :a {:a :b2}}) 235 | => {:a {:a :b2 :c :d}} 236 | (provided 237 | (slurp "child.edn") => "{:a {:a :b :c :d}}")) 238 | 239 | (fact "include - multiple files" 240 | (f {:baum/include ["child.edn" "child2.edn"] 241 | :a :aa}) 242 | => {:a :aa :b :b2 :c :c} 243 | (provided 244 | (slurp "child.edn") => "{:a :a :b :b :c :c}" 245 | (slurp "child2.edn") => "{:a :a2 :b :b2}")) 246 | 247 | (fact "include - multiple files and some of them don't exsist" 248 | (f {:baum/include ["child.edn" "child2.edn"]}) 249 | => (throws java.io.FileNotFoundException)) 250 | 251 | (fact "throw an exception when given value to include is invalid" 252 | (f {:baum/include "invalid-path"}) => (throws java.io.FileNotFoundException) 253 | (f {:baum/include 100}) => (throws java.lang.IllegalArgumentException)) 254 | 255 | (fact "include* - map" 256 | (f {:baum/include* {:a 100 257 | :b 200} 258 | :a 200}) 259 | => {:a 200 :b 200}) 260 | 261 | (fact "include* - file" 262 | (f {:baum/include* "child.edn" 263 | :a {:a :b2}}) 264 | => {:a {:a :b2 :c :d}} 265 | (provided 266 | (slurp "child.edn") => "{:a {:a :b :c :d}}")) 267 | 268 | (fact "include* - multiple files" 269 | (f {:baum/include* ["child.edn" "child2.edn"] 270 | :a :aa}) 271 | => {:a :aa :b :b2 :c :c} 272 | (provided 273 | (slurp "child.edn") => "{:a :a :b :b :c :c}" 274 | (slurp "child2.edn") => "{:a :a2 :b :b2}")) 275 | 276 | (fact "just ignore invalid path to include*" 277 | (f {:baum/include* "invalid-path"}) => {}) 278 | 279 | (fact "override" 280 | (f {:baum/override {:a 100 281 | :b 200 282 | :baum/override {:c 400}} 283 | :a 200 284 | :d 500}) 285 | => {:a 100 :b 200 :c 400 :d 500}) 286 | 287 | (fact "override - list" 288 | (f {:baum/override {:a 100 289 | :b 200 290 | :baum/override [{:c 400 :c2 200} 291 | {:c2 100}]} 292 | :a 200 293 | :d 500}) 294 | => {:a 100 :b 200 :c 400 :c2 100 :d 500}) 295 | 296 | (fact "override - file" 297 | (f {:baum/override "child.edn" 298 | :a {:a :b2 :c :d}}) 299 | => {:a {:a :b :c :d}} 300 | (provided 301 | (slurp "child.edn") => "{:a {:a :b :c :d}}")) 302 | 303 | (fact "override - multiple files" 304 | (f {:baum/override ["child.edn" "child2.edn"] 305 | :a :aa 306 | :c :c}) 307 | => {:a :a2 :b :b2 :c :c} 308 | (provided 309 | (slurp "child.edn") => "{:a :a :b :b}" 310 | (slurp "child2.edn") => "{:a :a2 :b :b2}")) 311 | 312 | (fact "throw an exception when given value to override is invalid" 313 | (f {:baum/override "invalid-path"}) => (throws java.io.FileNotFoundException) 314 | (f {:baum/override 100}) => (throws java.lang.IllegalArgumentException)) 315 | 316 | (fact "override*" 317 | (f {:baum/override* {:a 100 318 | :b 200 319 | :baum/override* {:c 400}} 320 | :a 200 321 | :d 500}) 322 | => {:a 100 :b 200 :c 400 :d 500}) 323 | 324 | (fact "override* - list" 325 | (f {:baum/override* {:a 100 326 | :b 200 327 | :baum/override* [{:c 400 :c2 200} 328 | {:c2 100}]} 329 | :a 200 330 | :d 500}) 331 | => {:a 100 :b 200 :c 400 :c2 100 :d 500}) 332 | 333 | (fact "override* - file" 334 | (f {:baum/override* "child.edn" 335 | :a {:a :b2 :c :d}}) 336 | => {:a {:a :b :c :d}} 337 | (provided 338 | (slurp "child.edn") => "{:a {:a :b :c :d}}")) 339 | 340 | (fact "override* - multiple files" 341 | (f {:baum/override* ["child.edn" "child2.edn"] 342 | :a :aa 343 | :c :c}) 344 | => {:a :a2 :b :b2 :c :c} 345 | (provided 346 | (slurp "child.edn") => "{:a :a :b :b}" 347 | (slurp "child2.edn") => "{:a :a2 :b :b2}")) 348 | 349 | (fact "Returns an empty map when given path is invalid" 350 | (f {:baum/override* "invalid-path"}) => {} 351 | (f {:baum/override* "invalid-path" 352 | :a :b}) => {:a :b}) 353 | 354 | (fact "custom special key" 355 | (c/reduction {:foo/bar :a} 356 | {:reducers {:foo/bar (fn [m v opts] 357 | (assoc m :bar :foo))}}) 358 | => {:bar :foo}))) 359 | 360 | (facts "let+ref" 361 | (fact "reader macro" 362 | (rs "{:baum/let [a 100] 363 | :a {:c #baum/ref a}}") 364 | => {:a {:c 100}}) 365 | 366 | (fact "destructuring" 367 | (rs "{:baum/let [{:keys [a b]} {:a 100 :b 200}] 368 | :a #baum/ref a 369 | :b #baum/ref b}") 370 | => {:a 100 :b 200} 371 | 372 | (rs "{:baum/let [{:keys [a b]} #baum/import \"a.edn\"] 373 | :a #baum/ref a 374 | :b #baum/ref b}") 375 | => {:a 100 :b 200} 376 | (provided 377 | (slurp "a.edn") => "{:a 100 :b 200}")) 378 | 379 | (fact "nested scope" 380 | (rs "{:baum/let [a :a 381 | b :b] 382 | :d1 {:baum/let [a :d1-a 383 | c :d1-c] 384 | :a #baum/ref a 385 | :b #baum/ref b 386 | :c #baum/ref c} 387 | :a #baum/ref a 388 | :b #baum/ref b}") 389 | => {:d1 {:a :d1-a 390 | :b :b 391 | :c :d1-c} 392 | :a :a 393 | :b :b}) 394 | 395 | (fact "safe" 396 | (rs "{:baum/let [a (str \"a\" \"b\") 397 | a (str #baum/ref a \"c\") 398 | a (str #baum/ref a \"d\") 399 | a (str #baum/ref a \"e\")] 400 | :a #baum/ref a}") 401 | => {:a '(str (str (str (str "a" "b") "c") "d") "e")}) 402 | 403 | (fact "global variable" 404 | (defmethod c/refer-global-variable 'FOO [_] :foo) 405 | (rs "#baum/ref FOO") => :foo) 406 | 407 | (fact "Throws an exception when referring to undefined variables" 408 | (rs "{:a #baum/ref a}") => (throws "Unable to resolve symbol: a in this context")) 409 | 410 | (fact "Cannot access variables defined in inner scopes" 411 | (rs "{:a #baum/ref a 412 | :b {:baum/let [a 100]}}") => (throws "Unable to resolve symbol: a in this context")) 413 | 414 | (fact "Can use a bound value via let with eval" 415 | (rs "{$let [v \"foo\"] 416 | :foo #if [#- v 417 | #eval (str \"*\" #- v \"*\") 418 | \"nothing\"]}") 419 | => {:foo "*foo*"})) 420 | 421 | (facts "aliasiing" 422 | (fact "custom" 423 | (rs {:aliases {'baum/env 'env 424 | :baum/let '$let 425 | 'baum/ref '-}} 426 | "{$let [user #env :user 427 | loc \"home\"] 428 | :who #- user 429 | :where #baum/ref loc}") 430 | => {:who "rkworks" :where "home"} 431 | (provided (env :user) => "rkworks")) 432 | 433 | (fact "built-in" 434 | (rs {:shorthand? true} 435 | "{$let [user #env :user 436 | loc \"home\"] 437 | :who #- user 438 | :where #baum/ref loc}") 439 | => {:who "rkworks" :where "home"} 440 | (provided (env :user) => "rkworks")) 441 | 442 | (fact "Disable built-in aliases" 443 | (rs {:shorthand? false} 444 | "{$let [user #env :user 445 | loc \"home\"] 446 | :who #- user 447 | :where #baum/ref loc}") 448 | => (throws Exception))) 449 | 450 | (facts "Complex examples" 451 | (fact ":baum/include + #baum/match" 452 | (rs "{:a :a 453 | :baum/include #baum/match [#baum/env :env 454 | \"dev\" {:a :a2 :b :b2} 455 | \"prod\" {:a :a3 :b :b3}]}") 456 | => {:a :a :b :b3} 457 | (provided (env :env) => "prod")) 458 | 459 | (fact ":baum/override + #baum/match" 460 | (rs "{:a :a 461 | :baum/override #baum/match [#baum/env :env 462 | \"dev\" {:a :a2 :b :b2} 463 | \"prod\" {:a :a3 :b :b3}]}") 464 | => {:a :a3 :b :b3} 465 | (provided (env :env) => "prod")) 466 | 467 | (fact "reader macro + bound var" 468 | (rs "{:baum/let [a \"a.edn\"] 469 | :foo #baum/import #baum/ref a}") 470 | => {:foo {:a :b}} 471 | (provided (slurp "a.edn") => "{:a :b}") 472 | 473 | (rs "{:baum/let [a :user] 474 | :a #baum/match [#baum/env #baum/ref a 475 | \"foo\" true 476 | :else false]}") 477 | => {:a true} 478 | (provided (env :user) => "foo") 479 | 480 | (rs "{:baum/let [a #baum/match [#baum/env :user 481 | \"foo\" true 482 | :else false]] 483 | :a #baum/ref a}") 484 | => {:a true} 485 | (provided (env :user) => "foo") 486 | 487 | (rs "{:baum/let [user #baum/env :user] 488 | :foo? #baum/match [#baum/ref user 489 | \"foo\" true 490 | :else false]}") 491 | => {:foo? true} 492 | (provided (env :user) => "foo") 493 | 494 | 495 | (rs "{:baum/let [a :user] 496 | :foo #baum/env #baum/ref a}") => {:foo (env :user)}) 497 | 498 | (fact "let chaining" 499 | (rs "{:baum/let [env #baum/env :env 500 | file-name #baum/str [#baum/ref env \".edn\"]] 501 | 502 | :baum/include [\"default.edn\" 503 | #baum/ref file-name] 504 | }") 505 | => {:foo :bar :bar :default} 506 | (provided 507 | (env :env) => "prod" 508 | (slurp "default.edn") => "{:foo :default :bar :default}" 509 | (slurp "prod.edn") => "{:foo :bar}") 510 | 511 | (rs "{:baum/let [env #baum/env :env 512 | file-name (str #baum/ref env \".edn\")] 513 | 514 | :f #baum/ref file-name 515 | }") 516 | => {:f '(str "prod" ".edn")} 517 | (provided 518 | (env :env) => "prod"))) 519 | 520 | 521 | #_(facts "Integration" 522 | (doseq [[in out] (c/read-file (io/resource "whole.edn") {:shorthand? true})] 523 | (fact 524 | in => out))) 525 | -------------------------------------------------------------------------------- /test/baum/dummy.clj: -------------------------------------------------------------------------------- 1 | (ns baum.dummy) 2 | 3 | (def foo "bar") 4 | -------------------------------------------------------------------------------- /test/baum/merger_test.clj: -------------------------------------------------------------------------------- 1 | (ns baum.merger-test 2 | (:require [baum.merger :as sut] 3 | [midje.sweet :refer :all])) 4 | 5 | (facts "merge-tree" 6 | (fact "Maps will be deeply merged" 7 | (sut/merge-tree {:a {:b :c :d :e}} 8 | {:a {:b :d}}) 9 | => 10 | {:a {:b :d :d :e}}) 11 | 12 | (fact "Collections will NOT be concatenated by default" 13 | (sut/merge-tree [:a :b] [:c :d]) 14 | => 15 | [:c :d]) 16 | 17 | (facts "By adding metadata, collections will be concatenated" 18 | (fact "To concatenate collections, use :append" 19 | (reduce sut/merge-tree [[:a :b] 20 | [:c :d] 21 | ^:append [:e :f]]) 22 | => 23 | [:c :d :e :f] 24 | 25 | (reduce sut/merge-tree [[:a :b] 26 | ^:append [:c :d] 27 | [:e :f]]) 28 | => 29 | [:e :f] 30 | 31 | (reduce sut/merge-tree [^:append [:a :b] ; nothing happens 32 | [:c :d]]) 33 | => 34 | [:c :d]) 35 | 36 | (fact ":prepend" 37 | (reduce sut/merge-tree [[:a :b] 38 | [:c :d] 39 | ^:prepend [:e :f]]) 40 | => 41 | [:e :f :c :d] 42 | 43 | (reduce sut/merge-tree [[:a :b] 44 | ^:prepend [:c :d] 45 | [:e :f]]) 46 | => 47 | [:e :f] 48 | 49 | (reduce sut/merge-tree [^:prepend [:a :b] 50 | [:c :d] 51 | [:e :f]]) 52 | => 53 | [:e :f])) 54 | 55 | (fact "Sets will NOT be united by default" 56 | (sut/merge-tree #{:a :b} #{:c :d}) 57 | => 58 | #{:c :d}) 59 | 60 | (fact "The right one's type should be respected." 61 | (sut/merge-tree #{:c :d} ^:append (sorted-set :a :b)) => sorted? 62 | 63 | (sut/merge-tree (sorted-set :a :b) ^:append #{:c :d}) =not=> sorted?) 64 | 65 | (fact "other collection types will be concatenated" 66 | (sut/merge-tree '(:a :b) ^:append [:c :d]) => (every-checker [:a :b :c :d] vector?)) 67 | 68 | (fact "nil handling" 69 | (sut/merge-tree nil 1) => 1 70 | (sut/merge-tree 1 nil) => nil 71 | (sut/merge-tree {:a :b} nil) => nil) 72 | 73 | (fact "displace" 74 | (reduce sut/merge-tree [^:displace {:a :b} 75 | {:c :d} 76 | {:e :f}]) => {:c :d :e :f} 77 | 78 | (reduce sut/merge-tree [{:a :b} 79 | ^:displace {:c :d} 80 | {:e :f}]) => {:e :f} 81 | 82 | (reduce sut/merge-tree [{:a :b} 83 | {:c :d} 84 | ^:displace {:e :f}]) => {:a :b :c :d} 85 | 86 | (reduce sut/merge-tree [^:displace {:c :d} 87 | ^:displace {:e :f}]) => {:c :d}) 88 | 89 | (fact "replace" 90 | (reduce sut/merge-tree [{:a :b} 91 | ^:replace {:c :d} 92 | {:e :f}]) => {:c :d :e :f} 93 | 94 | (reduce sut/merge-tree [^:replace {:a :b} 95 | {:c :d} 96 | {:e :f}]) => {:a :b :c :d :e :f} 97 | 98 | (reduce sut/merge-tree [{:a :b} 99 | {:c :d} 100 | ^:replace {:e :f}]) => {:e :f} 101 | 102 | (reduce sut/merge-tree [^:replace {:c :d} 103 | ^:replace {:e :f}]) => {:e :f}) 104 | 105 | 106 | (fact "edge cases" 107 | (sut/merge-tree ^:displace [:a :b] 108 | ^:append [:c :d]) => [:c :d] 109 | 110 | ;; prioritized-merge wins 111 | (sut/merge-tree [:a :b] 112 | ^:displace ^:append [:c :d]) => [:a :b])) 113 | -------------------------------------------------------------------------------- /test/baum/resolver_test.clj: -------------------------------------------------------------------------------- 1 | (ns baum.resolver-test 2 | (:require [baum.resolver :as resolver] 3 | [clojure.java.io :as io] 4 | [midje.sweet :refer :all]) 5 | (:import [java.net URL])) 6 | 7 | 8 | (facts "#resolve" 9 | (facts "parent = File" 10 | (fact "Normally a given path will be resolved as a relative path to the 11 | project root." 12 | (resolver/resolve (io/file "foo/bar.edn") "bar.edn") => "bar.edn") 13 | 14 | (fact "A path starting with ./ or ../ will be resolved as a relative path to 15 | its parent." 16 | (resolver/resolve (io/file "foo/bar.edn") "./bar.edn") 17 | => (.getCanonicalPath (io/file "foo/bar.edn")) 18 | 19 | (resolver/resolve (io/file "foo/bar.edn") "../baz/../bar.edn") 20 | => (.getCanonicalPath (io/file "bar.edn"))) 21 | 22 | (fact "If an absolute path is given, just returns it as it is." 23 | (resolver/resolve (io/file "foo/bar.edn") "/foo.edn") 24 | => "/foo.edn") 25 | 26 | (fact "just returns a given path as it is")) 27 | 28 | (facts "parent = URL(jar)" 29 | (fact "Normally a given path will be resolved as a relative path to root in 30 | a parent jar" 31 | (resolver/resolve (str (io/resource "midje/sweet.clj")) "project.clj") 32 | => #"^jar:.*midje[^/]*\.jar!/project.clj") 33 | 34 | (fact "A path starting with ./ or ../ will be resolved as a relative path to 35 | its parent." 36 | (resolver/resolve (io/resource "clojure/core.clj") "./string.clj") 37 | => (str (io/resource "clojure/string.clj")) 38 | 39 | (resolver/resolve (io/resource "clojure/java/io.clj") "../foo/../core.clj") 40 | => (str (io/resource "clojure/core.clj"))) 41 | 42 | (fact "If an absolute path is given, just returns it as it is." 43 | (resolver/resolve (io/resource "clojure/java/io.clj") "/foo.edn") 44 | => "/foo.edn")) 45 | 46 | (facts "parent = URL(http/https)" 47 | (fact "Normally a given path will be resolved as a relative path to the root 48 | URL." 49 | (resolver/resolve (URL. "http://example.com/foo/bar.edn") "bar.edn") 50 | => "http://example.com/bar.edn" 51 | 52 | (resolver/resolve (URL. "https://example.com/foo/bar.edn") "bar.edn") 53 | => "https://example.com/bar.edn") 54 | 55 | (fact "A path starting with ./ or ../ will be resolved as a relative path to 56 | its parent." 57 | (resolver/resolve (URL. "http://example.com/foo/bar.edn") "./foo.edn") 58 | => "http://example.com/foo/foo.edn" 59 | 60 | (resolver/resolve (URL. "https://example.com/foo/bar.edn") "../bar/../foo.edn") 61 | => "https://example.com/foo.edn") 62 | 63 | (fact "If an absolute path is given, just returns it as it is." 64 | (resolver/resolve (URL. "https://example.com/foo/bar.edn") "/foo.edn") 65 | => "/foo.edn")) 66 | 67 | (facts "parent = URL(file)" 68 | (fact "Normally a given path will be resolved as a relative path to the 69 | project root." 70 | (resolver/resolve (io/as-url (io/file "foo/bar.edn")) "bar.edn") 71 | => "bar.edn") 72 | 73 | (fact "A path starting with ./ or ../ will be resolved as a relative path to 74 | its parent." 75 | (resolver/resolve (io/as-url (io/file "foo/bar.edn")) "./bar.edn") 76 | => (str (io/as-url (io/file "foo/bar.edn"))) 77 | 78 | (resolver/resolve (io/as-url (io/file "foo/bar.edn")) "../baz/../bar.edn") 79 | => (str (io/as-url (io/file "bar.edn")))) 80 | 81 | (fact "If an absolute path is given, just returns it as it is." 82 | (resolver/resolve (io/as-url (io/file "foo/bar.edn")) "/foo.edn") 83 | => "/foo.edn")) 84 | 85 | (facts "parent = unknown" 86 | (fact "Always just returns a given path as it is" 87 | (resolver/resolve nil "foo.edn") => "foo.edn" 88 | (resolver/resolve nil "./foo.edn") => "./foo.edn" 89 | (resolver/resolve nil "../foo.edn") => "../foo.edn" 90 | (resolver/resolve :foo "../foo.edn") => "../foo.edn"))) 91 | --------------------------------------------------------------------------------