├── .gitignore ├── .travis.yml ├── changelog.md ├── license.txt ├── project.clj ├── readme.md ├── scripts ├── check-release.sh ├── check-versions.sh ├── config.sh ├── deploy-clojars.sh ├── list-jar.sh ├── local-install.sh ├── prepare-jar.sh ├── test-all.sh ├── test-self-host.sh └── update-versions.sh ├── src └── lib │ └── env_config │ ├── core.cljc │ ├── impl │ ├── coerce.cljc │ ├── coercers.cljc │ ├── helpers.cljc │ ├── logging.clj │ ├── logging.cljs │ ├── macros.clj │ ├── macros.cljs │ ├── platform.clj │ ├── platform.cljs │ ├── read.cljc │ ├── report.cljc │ └── types.cljc │ └── version.cljc └── test └── src └── tests └── env_config └── tests ├── main.cljc └── runner.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | pom.xml 3 | target/ 4 | .lein* 5 | pom.xml.* 6 | .idea 7 | .profiles/ 8 | out/ 9 | /.nrepl-port 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: true 2 | dist: trusty 3 | language: clojure 4 | notifications: 5 | email: 6 | - antonin@hildebrand.cz 7 | hipchat: 8 | rooms: 9 | secure: e62F22RwYJVT7BSUdWFSchRdekiTEGX8I56c2wKKY+PoTq1PNs+ZBIJMIseF2+H4VjmRZ7AtTh3KWk2dNOmZZQb5UNRdzLEpFUELOghcjGVCXbeEqa+aJJ+OzrFoHepKSoy2PdFsWH3kiqf3juSyPNGPU++Is5iPhsGefkeovAQ46V44ALqvoGZ9QnPQOgVkiCuI8yLroCCg2w1442d8BuHAs5sIF1aKdyGdMSRgLEEJ3rYci39qRtFNEJiTyzq9KgHIdSKYWOWU8gbLVLbOMUl75HIMZtkQIsyUM4WgaxUqvU5Jz/2uRX8b9rYmIvWuAUW4NQfvJ8YnIuLTjl4oA1vsKLiXjbw4sZGe8r5T+Rbdg+9tVpJBDZjKA0SHhcKzwFca8Oq7Wnli8c4AkqzLWaEZa99/B9/FLEnoqmFJ6XaEIi83CPMYTJkJ9XIOwETPj8KVS2C8pExYm5Fy1lr9s75CicMVGYmztUzr9iAdbGJRyc/UOFrfHZ3PyjH9QM9YcmKbdqwqj83tICSmxjUdvm/Xfe/aU4zxNx6SXE089xpEzYdwPKUPJRqEYfMMhwA+Pec4HgIkplLxfGOdDbG5rE4Oz7x6Fns3ffvK3Kv+zCTp9wAEREdo+hu8M+oelipLPj1V5AVvG/JFIctdxxGawIK1fTeUDDoCdaJUjjl9Vlk= 10 | template: 11 | - '%{repository}#%{build_number}: %{message} (changes)' 12 | format: html 13 | before_script: 14 | - yes y | sudo lein upgrade 15 | - sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test 16 | - sudo add-apt-repository -y ppa:mfikes/planck 17 | - sudo apt-get -qq update 18 | - sudo apt-get install -y planck libstdc++6 19 | - nvm install node 20 | - nvm use node 21 | - npm install -g lumo-cljs 22 | before_install: 23 | - curl -sSL https://raw.githubusercontent.com/cljs-oss/canary/master/scripts/install-canary.sh | bash 24 | script: lein test-all 25 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | See [https://github.com/binaryage/env-config/releases](https://github.com/binaryage/env-config/releases). 2 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) BinaryAge Limited and contributors 2 | https://github.com/binaryage/env-config/graphs/contributors 3 | 4 | MIT License 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (def clojurescript-version (or (System/getenv "CANARY_CLOJURESCRIPT_VERSION") "1.9.946")) 2 | (defproject binaryage/env-config "0.2.2" 3 | :description "Clojure(Script) library for config map overrides via environment variables." 4 | :url "https://github.com/binaryage/env-config" 5 | :license {:name "MIT License" 6 | :url "http://opensource.org/licenses/MIT" 7 | :distribution :repo} 8 | :scm {:name "git" 9 | :url "https://github.com/binaryage/env-config"} 10 | 11 | :dependencies [[org.clojure/clojure "1.9.0" :scope "provided"] 12 | [org.clojure/tools.reader "1.2.1"] 13 | [org.clojure/clojurescript ~clojurescript-version :scope "test"] 14 | [org.clojure/tools.logging "0.4.0" :scope "test"]] 15 | 16 | :clean-targets ^{:protect false} ["target" 17 | "test/resources/.compiled"] 18 | 19 | :plugins [[lein-shell "0.5.0"]] 20 | 21 | ; this is just for IntelliJ + Cursive to play well 22 | :source-paths ["src/lib"] 23 | :test-paths ["test/src/tests"] 24 | :resource-paths ^:replace ["scripts"] 25 | 26 | :cljsbuild {:builds {}} ; prevent https://github.com/emezeske/lein-cljsbuild/issues/413 27 | 28 | :profiles {:dev 29 | {:plugins [[com.jakemccrary/lein-test-refresh "0.20.0"] 30 | [lein-tach "0.3.0"] 31 | [lein-cljsbuild "1.1.6"]]} 32 | 33 | :nuke-aliases 34 | {:aliases ^:replace {}} 35 | 36 | :lib 37 | ^{:pom-scope :provided} ; ! to overcome default jar/pom behaviour, our :dependencies replacement would be ignored for some reason 38 | [:nuke-aliases 39 | {:dependencies ~(let [project-str (slurp "project.clj") 40 | project (->> project-str read-string (drop 3) (apply hash-map)) 41 | test-dep? #(->> % (drop 2) (apply hash-map) :scope (= "test")) 42 | non-test-deps (remove test-dep? (:dependencies project))] 43 | (with-meta (vec non-test-deps) {:replace true})) ; so ugly! 44 | :source-paths ^:replace ["src/lib"] 45 | :resource-paths ^:replace [] 46 | :test-paths ^:replace []}] 47 | 48 | :clojure18 49 | {:dependencies [[org.clojure/clojure "1.8.0" :scope "provided" :upgrade false]]} 50 | 51 | :clojure17 52 | {:dependencies [[org.clojure/clojure "1.7.0" :scope "provided" :upgrade false]]} 53 | 54 | :self-host 55 | {:cljsbuild {:builds [{:id "self-host-test-build" 56 | :source-paths ["src/lib" 57 | "test/src/tests"] 58 | :compiler {:output-to "test/resources/.compiled/tests.js" 59 | :main 'env-config.tests.runner 60 | :target :nodejs 61 | :optimizations :none}}]} 62 | :tach {:debug? false 63 | :force-non-zero-exit-on-test-failure? true}}} 64 | 65 | :aliases {"install" ["do" 66 | ["shell" "scripts/prepare-jar.sh"] 67 | ["shell" "scripts/local-install.sh"]] 68 | "test-all" ["shell" "scripts/test-all.sh"] 69 | "test-self-host" ["shell" "scripts/test-self-host.sh"] 70 | "jar" ["shell" "scripts/prepare-jar.sh"] 71 | "deploy" ["shell" "scripts/deploy-clojars.sh"] 72 | "release" ["do" 73 | ["clean"] 74 | ["shell" "scripts/check-versions.sh"] 75 | ["shell" "scripts/prepare-jar.sh"] 76 | ["shell" "scripts/check-release.sh"] 77 | ["shell" "scripts/deploy-clojars.sh"]]}) 78 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # env-config 2 | 3 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](license.txt) 4 | [![Clojars Project](https://img.shields.io/clojars/v/binaryage/env-config.svg)](https://clojars.org/binaryage/env-config) 5 | [![Travis](https://img.shields.io/travis/binaryage/env-config.svg)](https://travis-ci.org/binaryage/env-config) 6 | 7 | This is a Clojure(Script) library for enabling easy and consistent config map overrides via environment variables. 8 | 9 | This is useful for library authors who want to add some flexibility how their libraries can be configured. 10 | 11 | ## Intro 12 | 13 | Usually library configuration is achieved via a config map specifying keywords with individual config values. 14 | This config map can be provided directly (e.g. passed via an api call), via a build configuration or by some other means. 15 | For example in ClojureScript it could be passed via `:compiler > :external-config`. 16 | 17 | Sometimes for ad-hoc tweaks it would be preferable to be able to override config values 18 | by defining environment variables instead of touching build tool configuration (which is usually under source control). 19 | 20 | This library helps you do that consistently: 21 | 22 | 1. we define a naming scheme how env variables map to config keys 23 | 2. we define a coercion protocol which determines how strings from env variables are converted to Clojure values 24 | 25 | ### Example 26 | 27 | We want to support nested config maps. Let's look at example env variables with some nesting: 28 | 29 | OOPS/COMPILER/MAX_ITERATIONS=10 30 | OOPS/RUNTIME/DEBUG=true 31 | OOPS/RUNTIME/WELCOME-MESSAGE=hello 32 | OOPS/RUNTIME/DATA=~{:some (str "data" " via" " read-string")} 33 | OOPS/RUNTIME/KEY=:some-keyword 34 | OOPS/RUNTIME=something <= this will cause a naming conflic warning 35 | 36 | A call to `(env-config.core/make-config "oops" (get-env-vars))` will return: 37 | 38 | {:compiler {:max-iterations 10} 39 | :runtime {:debug true 40 | :welcome-message "hello" 41 | :key :some-keyword 42 | :data {:some "data via read-string"}} 43 | 44 | You can observe several properties: 45 | 46 | 1. forward slashes are used as separators 47 | 2. to follow Clojure conventions, names are converted to lower-case and underscores turned into dashes 48 | 2. prefix "oops" is stripped because it was specified as a library prefix 49 | 3. values are naturally coerced to booleans, numbers, keywords, etc. 50 | 4. you can use full power of `read-string` if you prepend value with `~` 51 | 52 | Also please note that existence of a variable name which is a prefix of another variable name will cause 53 | naming conflict warning and will be ignored (`OOPS/RUNTIME` is prefix of `OOPS/RUNTIME/DEBUG` in our example above). 54 | 55 | Some shells [like Bash](http://stackoverflow.com/a/2821183/84283) do not allow slashes in variable names, you can use two underscores instead of a slash. 56 | 57 | ### Integration 58 | 59 | You probably want to merge the config coming from env-config over your standard config coming from a build tool. 60 | 61 | For inspiration look at [the commit](https://github.com/binaryage/cljs-oops/commit/1a2a1794f59e47710b5c9e025a420ed25db4d4ed) 62 | which integrated env-config into cljs-oops library. 63 | 64 | Please note that `make-config-with-logging` does not read environment directly. You have to pass it a map with variables. 65 | 66 | I used this simple implementation to get them: 67 | ``` 68 | (defn get-env-vars [] 69 | (-> {} 70 | (into (System/getenv)) 71 | (into (System/getProperties)))) 72 | ``` 73 | 74 | ### Logging 75 | 76 | I needed a way how to report issues with naming conflicts or for example problems when evaluting values via read-string. 77 | 78 | I didn't want to introduce another dependency so I decided to build internal subsystem for collecting "reports". It is up 79 | to you to inspect reports and communicate them somehow. 80 | 81 | For convenience I have implemented a helper function which dynamically checks for availability of `clojure.tools.logging` 82 | and uses it for logging reports. 83 | 84 | To get standard logging for free include dependency on `clojure.tools.logging` into your project and use `make-config-with-logging` 85 | to obtain your configs. 86 | 87 | ### Coercion 88 | 89 | We provide a [standard set of coercion handlers](https://github.com/binaryage/env-config/blob/master/src/lib/env_config/impl/coercers.clj). 90 | As you can see from the `default-coercers` list the rules are pretty simple. You might want to provide your own handlers. 91 | 92 | #### Writing own coercion handlers 93 | 94 | Coercion handlers are asked in the order in which they were specified to `make-config`. 95 | Each handler is passed key path in the config map and raw string value coming from environment. 96 | 97 | The handler should answer either: 98 | 99 | 1. `nil` which means "I'm not interested, pass it to someone else" 100 | 2. `:omit` which means "ignore this value due to an error" 101 | 3. a value wrapped in `Coerced` instance (to distinguish it from `nil` and `:omit`) 102 | 103 | If no handler was interested we use the raw value as-is. 104 | 105 | Look at the example of the most complex standard coercer: 106 | 107 | ```clojure 108 | (defn code-coercer [path val] 109 | (if (string-starts-with? val "~") 110 | (let [code (.substring val 1)] 111 | (try 112 | (->Coerced (read-string code)) 113 | (catch Throwable e 114 | (report/report-warning! (str "unable to read-string from " (make-var-description (meta path)) ", " 115 | "attempted to eval code: '" code "', " 116 | "got problem: " (.getMessage e) ".")) 117 | :omit))))) 118 | ``` 119 | 120 | Please note that the `path` vector has attached some metadata with original raw values which may be handy when 121 | reporting warnings/errors. You should use `env-config.impl.report` functionality to report errors in a standard way. 122 | 123 | ### FAQ 124 | 125 | > My shell does not support variable names with slashes. What now? 126 | 127 | You can use two underscores instead of a slash. Or alternatively you might want to use `env` command to launch your command with 128 | defined variables without shell naming restrictions. See [this stack overflow answer](http://unix.stackexchange.com/a/93533/188074). 129 | 130 | For example: 131 | 132 | env OOPS/COMPILER/MAX_ITERATIONS=10 OOPS/RUNTIME/DEBUG=true command 133 | 134 | I personally use [fish shell](https://fishshell.com) and prefer slashes to visually communicate the nested config structure. 135 | 136 | > Can this be used in self-hosted mode? 137 | 138 | Yes, thanks to [arichiardi](https://github.com/arichiardi). Since v0.2.0 you can use this library to configure scripts running 139 | under [Planck](https://github.com/mfikes/planck) or [Lumo](https://github.com/anmonteiro/lumo). 140 | -------------------------------------------------------------------------------- /scripts/check-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | pushd `dirname "${BASH_SOURCE[0]}"` > /dev/null 6 | source "./config.sh" 7 | 8 | pushd "$ROOT" 9 | 10 | ./scripts/list-jar.sh 11 | 12 | LEIN_VERSION=`cat "$PROJECT_FILE" | grep "defproject" | cut -d' ' -f3 | cut -d\" -f2` 13 | 14 | if [[ "$LEIN_VERSION" =~ "SNAPSHOT" ]]; then 15 | echo "Publishing SNAPSHOT versions is not allowed. Bump current version $LEIN_VERSION to a non-snapshot version." 16 | exit 2 17 | fi 18 | 19 | popd 20 | -------------------------------------------------------------------------------- /scripts/check-versions.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | pushd `dirname "${BASH_SOURCE[0]}"` > /dev/null 6 | source "./config.sh" 7 | 8 | pushd "$ROOT" 9 | 10 | LEIN_VERSION=`cat "$PROJECT_FILE" | grep "defproject" | cut -d' ' -f3 | cut -d\" -f2` 11 | 12 | # same version must be in src/version.cljc 13 | 14 | PROJECT_VERSION=`cat "$PROJECT_VERSION_FILE" | grep "(def current-version" | cut -d" " -f3 | cut -d\" -f2` 15 | if [ -z "$PROJECT_VERSION" ] ; then 16 | echo "Unable to retrieve 'current-version' string from '$PROJECT_VERSION_FILE'" 17 | popd 18 | exit 1 19 | fi 20 | 21 | if [ ! "$LEIN_VERSION" = "$PROJECT_VERSION" ] ; then 22 | echo "Lein's project.clj version differs from version in '$PROJECT_VERSION_FILE': '$LEIN_VERSION' != '$PROJECT_VERSION'" 23 | popd 24 | exit 2 25 | fi 26 | 27 | echo "All version strings are consistent: '$LEIN_VERSION'" 28 | 29 | popd 30 | -------------------------------------------------------------------------------- /scripts/config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | pushd () { 4 | command pushd "$@" > /dev/null 5 | } 6 | 7 | popd () { 8 | command popd "$@" > /dev/null 9 | } 10 | 11 | pushd `dirname "${BASH_SOURCE[0]}"` 12 | 13 | cd .. 14 | 15 | ROOT=`pwd` 16 | PROJECT_VERSION_FILE="src/lib/env_config/version.cljc" 17 | PROJECT_FILE="project.clj" 18 | 19 | popd 20 | -------------------------------------------------------------------------------- /scripts/deploy-clojars.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | pushd `dirname "${BASH_SOURCE[0]}"` > /dev/null 6 | source "./config.sh" 7 | 8 | pushd "$ROOT" 9 | 10 | ./scripts/list-jar.sh 11 | 12 | LEIN_VERSION=`cat "$PROJECT_FILE" | grep "defproject" | cut -d' ' -f3 | cut -d\" -f2` 13 | 14 | # http://stackoverflow.com/a/1885534/84283 15 | echo "Are you sure to publish version ${LEIN_VERSION}? [Yy]" 16 | read -n 1 -r 17 | if [[ "$REPLY" =~ ^[Yy]$ ]]; then 18 | lein with-profile lib deploy clojars 19 | else 20 | exit 1 21 | fi 22 | 23 | popd 24 | 25 | popd 26 | -------------------------------------------------------------------------------- /scripts/list-jar.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | pushd `dirname "${BASH_SOURCE[0]}"` > /dev/null 6 | source "./config.sh" 7 | 8 | pushd "$ROOT" 9 | 10 | ./scripts/check-versions.sh 11 | 12 | LEIN_VERSION=`cat "$PROJECT_FILE" | grep "defproject" | cut -d' ' -f3 | cut -d\" -f2` 13 | 14 | JAR_FILE="target/env-config-$LEIN_VERSION.jar" 15 | 16 | echo "listing content of $JAR_FILE" 17 | echo "" 18 | 19 | unzip -l "$JAR_FILE" 20 | 21 | echo "" 22 | echo "----------------------------" 23 | echo "" 24 | 25 | popd 26 | 27 | popd 28 | -------------------------------------------------------------------------------- /scripts/local-install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | pushd `dirname "${BASH_SOURCE[0]}"` > /dev/null 6 | source "./config.sh" 7 | 8 | pushd "$ROOT" 9 | 10 | ./scripts/list-jar.sh 11 | 12 | lein with-profile lib install 13 | 14 | popd 15 | 16 | popd 17 | -------------------------------------------------------------------------------- /scripts/prepare-jar.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | pushd `dirname "${BASH_SOURCE[0]}"` > /dev/null 6 | source "./config.sh" 7 | 8 | pushd "$ROOT" 9 | 10 | lein with-profile lib jar 11 | 12 | popd 13 | 14 | popd 15 | -------------------------------------------------------------------------------- /scripts/test-all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | pushd `dirname "${BASH_SOURCE[0]}"` > /dev/null 6 | source "./config.sh" 7 | 8 | cd "$ROOT" 9 | 10 | echo 11 | echo "Running tests against Clojure 1.9" 12 | echo "-----------------------------------------------------------------------------------------------------------------------" 13 | lein test 14 | 15 | echo 16 | echo "Running tests against Clojure 1.8" 17 | echo "-----------------------------------------------------------------------------------------------------------------------" 18 | lein with-profile +clojure18 test 19 | 20 | echo 21 | echo "Running tests against Clojure 1.7" 22 | echo "-----------------------------------------------------------------------------------------------------------------------" 23 | lein with-profile +clojure17 test 24 | 25 | echo 26 | echo "Running self-host tests against $(planck --help | head -n 1 | xargs echo -n)" 27 | echo "-----------------------------------------------------------------------------------------------------------------------" 28 | lein with-profile +self-host tach planck self-host-test-build 29 | 30 | echo "Running self-host tests against $(lumo --help | head -n 1 | xargs echo -n)" 31 | echo "-----------------------------------------------------------------------------------------------------------------------" 32 | lein with-profile +self-host tach lumo self-host-test-build 33 | 34 | popd 35 | -------------------------------------------------------------------------------- /scripts/test-self-host.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | pushd `dirname "${BASH_SOURCE[0]}"` > /dev/null 6 | source "./config.sh" 7 | 8 | pushd "$ROOT" 9 | 10 | echo 11 | echo "Running self-host tests against $(lumo --help | head -n 1 | xargs echo -n)" 12 | echo "-----------------------------------------------------------------------------------------------------------------------" 13 | lein with-profile +self-host tach lumo self-host-test-build 14 | 15 | echo 16 | echo "Running self-host tests against $(planck --help | head -n 1 | xargs echo -n)" 17 | echo "-----------------------------------------------------------------------------------------------------------------------" 18 | lein with-profile +self-host tach planck self-host-test-build 19 | 20 | popd 21 | 22 | popd 23 | -------------------------------------------------------------------------------- /scripts/update-versions.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # updates all version strings 4 | 5 | set -e 6 | 7 | pushd `dirname "${BASH_SOURCE[0]}"` > /dev/null 8 | source "./config.sh" 9 | 10 | pushd "$ROOT" 11 | 12 | VERSION=$1 13 | 14 | if [ -z "$VERSION" ] ; then 15 | echo "please specify version as the first argument" 16 | popd 17 | exit 1 18 | fi 19 | 20 | sed -i "" -e "s/defproject binaryage\/env-config \".*\"/defproject binaryage\/env-config \"$VERSION\"/g" "$PROJECT_FILE" 21 | sed -i "" -e "s/def current-version \".*\"/def current-version \"$VERSION\"/g" "$PROJECT_VERSION_FILE" 22 | 23 | # this is just a sanity check 24 | ./scripts/check-versions.sh 25 | 26 | popd 27 | 28 | popd 29 | -------------------------------------------------------------------------------- /src/lib/env_config/core.cljc: -------------------------------------------------------------------------------- 1 | (ns env-config.core 2 | (:require [env-config.impl.read :as read] 3 | [env-config.impl.coerce :as coerce] 4 | [env-config.impl.coercers :as coercers] 5 | [env-config.impl.report :as report])) 6 | 7 | ; -- public api ------------------------------------------------------------------------------------------------------------- 8 | 9 | (def default-coercers coercers/default-coercers) 10 | 11 | (defn read-config [prefix vars] 12 | (read/read-config prefix vars)) 13 | 14 | (defn coerce-config [config & [coercers]] 15 | (coerce/coerce-config config (or coercers default-coercers))) 16 | 17 | (defn prepare-config [prefix vars coercers] 18 | (-> (read-config prefix vars) 19 | (coerce-config coercers))) 20 | 21 | (defn make-config [prefix vars & [coercers reports-atom]] 22 | (binding [report/*reports* reports-atom] 23 | (prepare-config prefix vars coercers))) 24 | 25 | (defn make-config-with-logging [prefix vars & [coercers reporter]] 26 | (let [reports-atom (atom []) 27 | config (make-config prefix vars coercers reports-atom)] 28 | (report/log-reports-if-needed! @reports-atom reporter) 29 | config)) 30 | -------------------------------------------------------------------------------- /src/lib/env_config/impl/coerce.cljc: -------------------------------------------------------------------------------- 1 | (ns env-config.impl.coerce 2 | (:require [env-config.impl.types :as t :refer [coerced? get-value]] 3 | [env-config.impl.macros :refer [try* catch-all]] 4 | [env-config.impl.report :as report] 5 | [env-config.impl.helpers :refer [dissoc-in]] 6 | [env-config.impl.platform :refer [get-ex-message]])) 7 | 8 | ; -- coercion machinery ----------------------------------------------------------------------------------------------------- 9 | 10 | (defn valid-coercion-result? [result] 11 | (or (nil? result) ; this means "not interested" 12 | (= :omit result) ; this means "ignore value because of an error" 13 | (coerced? result))) ; this wraps coercion result 14 | 15 | (defn coerce [path val coercer] 16 | (try* 17 | (let [result (coercer path val)] 18 | (if (valid-coercion-result? result) 19 | result 20 | (throw (ex-info (str "coercer returned an unexpected result: " result " (" (type result) "), " 21 | "allowed are nil, :omit and Coerced instances") {})))) 22 | (catch-all e 23 | (report/report-error! (str "problem with coercer " coercer ": " (get-ex-message e) "."))))) 24 | 25 | (defn apply-coercers [coercers path val] 26 | (if-let [result (some (partial coerce path val) coercers)] 27 | (if (= :omit result) 28 | ::omit 29 | (get-value result)) 30 | val)) ; when no coercer applies, we return it as a string value 31 | 32 | (defn push-key [state key] 33 | (update state :keys conj key)) 34 | 35 | (defn store-keys [state] 36 | (:keys state)) 37 | 38 | (defn restore-keys [state keys] 39 | (assoc state :keys keys)) 40 | 41 | (defn coercion-worker [coercers state key val] 42 | (if (map? val) 43 | (let [current-keys (store-keys state) 44 | new-state (reduce-kv (:reducer state) (push-key state key) val)] 45 | (restore-keys new-state current-keys)) 46 | (let [path (conj (:keys state) key) 47 | metadata (get-in (:metadata state) path) 48 | coerced-val (apply-coercers coercers (with-meta path metadata) val)] 49 | (if (= ::omit coerced-val) 50 | (update state :config dissoc-in path) 51 | (update state :config assoc-in path coerced-val))))) 52 | 53 | ; -- coercer ---------------------------------------------------------------------------------------------------------------- 54 | 55 | (defn naked-coerce-config [config coercers] 56 | (let [reducer (partial coercion-worker coercers) 57 | init {:keys [] 58 | :reducer reducer 59 | :metadata (meta config) 60 | :config {}}] 61 | (:config (reduce-kv reducer init config)))) 62 | 63 | (defn coerce-config [config coercers] 64 | (try* 65 | (naked-coerce-config config coercers) 66 | (catch-all e 67 | (report/report-error! (str "internal error in coerce-config: " (get-ex-message e)))))) 68 | -------------------------------------------------------------------------------- /src/lib/env_config/impl/coercers.cljc: -------------------------------------------------------------------------------- 1 | (ns env-config.impl.coercers 2 | (:require [clojure.string :as string] 3 | [env-config.impl.types :refer [->Coerced]] 4 | [env-config.impl.macros :refer [try* catch-all]] 5 | [env-config.impl.report :as report] 6 | [env-config.impl.helpers :refer [make-var-description]] 7 | [env-config.impl.platform :refer [string-starts-with? get-ex-message read-code-string 8 | coerce-integer coerce-double]])) 9 | 10 | ; -- standard coercers ------------------------------------------------------------------------------------------------------ 11 | 12 | (defn nil-coercer [_path val] 13 | (when (= (string/lower-case val) "nil") 14 | (->Coerced nil))) 15 | 16 | (defn boolean-coercer [_path val] 17 | (condp = (string/lower-case val) 18 | "true" (->Coerced true) 19 | "false" (->Coerced false) 20 | nil)) 21 | 22 | (defn integer-coercer [_path val] 23 | (coerce-integer val)) 24 | 25 | (defn double-coercer [_path val] 26 | (coerce-double val)) 27 | 28 | (defn keyword-coercer [_path val] 29 | (when (string-starts-with? val ":") 30 | (->Coerced (keyword (subs val 1))))) 31 | 32 | (defn symbol-coercer [_path val] 33 | (when (string-starts-with? val "'") 34 | (->Coerced (symbol (subs val 1))))) 35 | 36 | (defn code-coercer [path val] 37 | (when (string-starts-with? val "~") 38 | (let [code (subs val 1)] 39 | (try* 40 | (->Coerced (read-code-string code)) 41 | (catch-all e 42 | (report/report-warning! (str "unable to read-string from " (make-var-description (meta path)) ", " 43 | "attempted to eval code: '" code "', " 44 | "got problem: " (get-ex-message e) ".")) 45 | :omit))))) 46 | 47 | ; -- default coercers ------------------------------------------------------------------------------------------------------- 48 | 49 | (def default-coercers 50 | [nil-coercer 51 | boolean-coercer 52 | integer-coercer 53 | double-coercer ; order counts 54 | keyword-coercer 55 | symbol-coercer 56 | code-coercer]) 57 | -------------------------------------------------------------------------------- /src/lib/env_config/impl/helpers.cljc: -------------------------------------------------------------------------------- 1 | (ns env-config.impl.helpers) 2 | 3 | ; don't want to bring full medley dependency 4 | ; https://github.com/weavejester/medley/blob/d8be5e2c45c61f4846953a5187e623e0ebe800f0/src/medley/core.cljc#L11 5 | (defn dissoc-in 6 | "Dissociate a value in a nested associative structure, identified by a sequence 7 | of keys. Any collections left empty by the operation will be dissociated from 8 | their containing structures." 9 | [m ks] 10 | (if-let [[k & ks] (seq ks)] 11 | (if (seq ks) 12 | (let [v (dissoc-in (get m k) ks)] 13 | (if (empty? v) 14 | (dissoc m k) 15 | (assoc m k v))) 16 | (dissoc m k)) 17 | m)) 18 | 19 | (defn ^:dynamic make-var-description [var-meta] 20 | (let [orig-name (or (:var-name var-meta) "?") 21 | orig-value (:var-value var-meta)] 22 | (str "variable '" orig-name "' with value " (pr-str orig-value)))) 23 | -------------------------------------------------------------------------------- /src/lib/env_config/impl/logging.clj: -------------------------------------------------------------------------------- 1 | (ns env-config.impl.logging) 2 | 3 | ; we don't want to introduce hard dependency on clojure.tools.logging 4 | ; that is why resolve this dynamically 5 | 6 | (def logging-ns-sym 'clojure.tools.logging) 7 | (def logging-api (atom nil)) 8 | 9 | ; -- logging api resolution ------------------------------------------------------------------------------------------------- 10 | 11 | (defn try-resolve-logging-symbol [sym] 12 | (try 13 | (ns-resolve logging-ns-sym sym) 14 | (catch Throwable e 15 | nil))) 16 | 17 | (defn try-resolve-logging-fn [sym] 18 | (let [v (try-resolve-logging-symbol sym)] 19 | (if (var? v) 20 | (if-let [f (var-get v)] 21 | (if (fn? f) 22 | f))))) 23 | 24 | (defn get-logging-api [] 25 | (let [api @logging-api] 26 | (if (nil? api) 27 | (if-let [api (try-resolve-logging-fn 'logp)] 28 | (reset! logging-api api) 29 | (reset! logging-api false)) 30 | api))) 31 | 32 | ; -- macros ----------------------------------------------------------------------------------------------------------------- 33 | 34 | ; note that clojure.tools.logging/logp is a macro, 35 | ; so we have to generate our api call via our own macro 36 | (defmacro gen-log [level-sym message-sym] 37 | (assert (symbol? level-sym)) 38 | (assert (symbol? message-sym)) 39 | (if-let [api (get-logging-api)] 40 | (api &env &form level-sym message-sym))) 41 | 42 | ; -- public ----------------------------------------------------------------------------------------------------------------- 43 | 44 | (defn reporter [messages] 45 | (if (some? @logging-api) 46 | (doseq [[level message] messages] 47 | (gen-log level message)))) 48 | -------------------------------------------------------------------------------- /src/lib/env_config/impl/logging.cljs: -------------------------------------------------------------------------------- 1 | (ns env-config.impl.logging) 2 | 3 | (defn logp [level message] 4 | (case level 5 | :debug (js/console.log message) 6 | :info (js/console.info message) 7 | :warn (js/console.warn message) 8 | :error (js/console.error message) 9 | :fatal (js/console.error message))) 10 | 11 | ; -- public ----------------------------------------------------------------------------------------------------------------- 12 | 13 | (defn reporter [messages] 14 | (doseq [[level message] messages] 15 | (logp level message))) 16 | -------------------------------------------------------------------------------- /src/lib/env_config/impl/macros.clj: -------------------------------------------------------------------------------- 1 | (ns env-config.impl.macros) 2 | 3 | (defn cljs-env? [env] 4 | (some? (:ns env))) 5 | 6 | (defn transform-catch-all [ex-type form] 7 | (if (and (list? form) (= (first form) 'catch-all)) 8 | (concat `(catch ~ex-type) (rest form)) 9 | form)) 10 | 11 | (defmacro try* [& body] 12 | "The purpose of this macro is to expand try-catch based on platform 13 | 14 | (try* ... (catch-all e ...) ...) 15 | 16 | will expand to 17 | 18 | (try ... (catch Throwable e ...)) in Clojure 19 | (try ... (catch :default e ...)) in ClojureScript" 20 | (let [ex-type (if (cljs-env? &env) :default 'Throwable)] 21 | `(try 22 | ~@(map (partial transform-catch-all ex-type) body)))) 23 | 24 | (defmacro catch-all [& body] 25 | (assert "not reachable")) ; just a dummy implementation to make Cursive happy 26 | -------------------------------------------------------------------------------- /src/lib/env_config/impl/macros.cljs: -------------------------------------------------------------------------------- 1 | (ns env-config.impl.macros 2 | (:require-macros [env-config.impl.macros])) 3 | 4 | ; note this namespace exists so that we can rely on simple :refer ns form in ClojureScript 5 | ; and avoid reader conditionals for :refer-macros 6 | ; see http://dev.clojure.org/jira/browse/CLJS-1507 7 | -------------------------------------------------------------------------------- /src/lib/env_config/impl/platform.clj: -------------------------------------------------------------------------------- 1 | (ns env-config.impl.platform 2 | "Platform dependent code: clojure implementation." 3 | (:require [clojure.edn :as edn] 4 | [env-config.impl.types :refer [->Coerced]])) 5 | 6 | ; a backport for Clojure 1.7 7 | (defn string-starts-with? 8 | "True if s starts with substr." 9 | [^CharSequence s ^String substr] 10 | (.startsWith (.toString s) substr)) 11 | 12 | (defn get-ex-message [e] 13 | (.getMessage e)) 14 | 15 | (defn read-code-string [code] 16 | (edn/read-string code)) 17 | 18 | (defn coerce-integer [val] 19 | (try 20 | (->Coerced (Integer/parseInt val)) 21 | (catch NumberFormatException e))) 22 | 23 | (defn coerce-double [val] 24 | (try 25 | (->Coerced (Double/parseDouble val)) 26 | (catch NumberFormatException e))) 27 | -------------------------------------------------------------------------------- /src/lib/env_config/impl/platform.cljs: -------------------------------------------------------------------------------- 1 | (ns env-config.impl.platform 2 | "Platform dependent code: ClojureScript implementation." 3 | (:require [clojure.string :as string] 4 | [cljs.tools.reader :as reader] 5 | [env-config.impl.types :refer [->Coerced]])) 6 | 7 | (def string-starts-with? string/starts-with?) 8 | 9 | (defn get-ex-message [e] 10 | (or (.-message e) "?")) 11 | 12 | (defn read-code-string [code] 13 | (reader/read-string code)) ; throws in case of errors 14 | 15 | (defn coerce-integer [val] 16 | (if (re-matches #"(\+|\-)?([0-9]+|Infinity)" val) ; see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/parseInt 17 | (let [parsed-int (js/parseInt val 10)] 18 | (if-not (js/isNaN parsed-int) 19 | (->Coerced parsed-int))))) 20 | 21 | (defn coerce-double [val] 22 | (if (re-matches #"(\+|\-)?([0-9]+(\.[0-9]+)?|Infinity)" val) ; see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/parseFloat 23 | (let [parsed-float (js/parseFloat val)] 24 | (if-not (js/isNaN parsed-float) 25 | (->Coerced parsed-float))))) 26 | -------------------------------------------------------------------------------- /src/lib/env_config/impl/read.cljc: -------------------------------------------------------------------------------- 1 | (ns env-config.impl.read 2 | (:require [clojure.string :as string] 3 | [env-config.impl.macros :refer [try* catch-all]] 4 | [env-config.impl.report :as report] 5 | [env-config.impl.helpers :refer [make-var-description]] 6 | [env-config.impl.platform :refer [string-starts-with? get-ex-message]])) 7 | 8 | ; -- helpers ---------------------------------------------------------------------------------------------------------------- 9 | 10 | (defn canonical-name [name] 11 | (-> name 12 | (string/lower-case) 13 | (string/replace "_" "-") 14 | (string/replace "--" "/"))) ; some shells do not support "/", double underscore or dash can be used instead 15 | 16 | (defn name-to-keyword [name] 17 | (-> name 18 | (canonical-name) 19 | (keyword))) 20 | 21 | (defn normalize-value [value] 22 | (str value)) ; environ values are expected to be strings, but user could pass anything to read-config 23 | 24 | (defn get-name-segments [name] 25 | (string/split name #"/")) 26 | 27 | (defn matching-var-name? [prefix name] 28 | (string-starts-with? name prefix)) 29 | 30 | (defn strip-prefix [prefix name] 31 | (assert (string-starts-with? name prefix)) 32 | (.substring name (count prefix))) 33 | 34 | (defn report-naming-conflict! [item1 item2] 35 | (report/report-warning! (str "naming conflict: the " (make-var-description (meta item1)) " " 36 | "was shadowed by the " (make-var-description (meta item2)) ". " 37 | "A variable name must not be a prefix of another variable name."))) 38 | 39 | ; -- reducers --------------------------------------------------------------------------------------------------------------- 40 | 41 | (defn filterer-for-matching-vars [prefix state var-name var-value] 42 | (or 43 | (let [prefix+name (canonical-name var-name)] 44 | (if (matching-var-name? prefix prefix+name) 45 | (let [value (normalize-value var-value) 46 | name (strip-prefix prefix prefix+name)] 47 | (conj state (with-meta [name value] {:var-name var-name :var-value var-value}))))) 48 | state)) 49 | 50 | (defn common-prefix? [s prefix] 51 | (if (some? prefix) 52 | (string-starts-with? s (str prefix "/")))) 53 | 54 | (defn filterer-for-naming-conflicts [state item] 55 | (let [prev-item (last state)] 56 | (if (common-prefix? (first item) (first prev-item)) 57 | (do 58 | (report-naming-conflict! prev-item item) 59 | (conj (vec (butlast state)) item)) 60 | (conj state item)))) 61 | 62 | (defn config-builder 63 | ([] {}) 64 | ([config item] 65 | (let [[name value] item] 66 | (let [segments (get-name-segments name) 67 | ks (map name-to-keyword segments) 68 | new-config (assoc-in config ks value) 69 | new-metadata (assoc-in (meta config) ks (meta item))] 70 | (with-meta new-config new-metadata))))) 71 | 72 | ; -- reader ----------------------------------------------------------------------------------------------------------------- 73 | 74 | (defn naked-read-config [prefix vars] 75 | (let [canonical-prefix (str (canonical-name prefix) "/")] 76 | (->> vars 77 | (reduce-kv (partial filterer-for-matching-vars canonical-prefix) []) 78 | (sort-by first) ; we want lexicographical sorting, longer (nested) names overwrite shorter ones 79 | (reduce filterer-for-naming-conflicts []) 80 | (reduce config-builder {})))) 81 | 82 | (defn read-config [prefix vars] 83 | (try* 84 | (naked-read-config prefix vars) 85 | (catch-all e 86 | (report/report-error! (str "internal error in read-config: " (get-ex-message e)))))) 87 | -------------------------------------------------------------------------------- /src/lib/env_config/impl/report.cljc: -------------------------------------------------------------------------------- 1 | (ns env-config.impl.report 2 | (:require [env-config.impl.logging :as logging])) 3 | 4 | (def ^:dynamic *message-prefix* "env-config: ") 5 | (def ^:dynamic *reports* nil) ; should be bound if reporting is desired 6 | 7 | ; -- reporting -------------------------------------------------------------------------------------------------------------- 8 | 9 | (defn apply-prefix [message] 10 | (str *message-prefix* message)) 11 | 12 | (defn report-warning! [message] 13 | (if *reports* 14 | (swap! *reports* conj [:warn (apply-prefix message)])) 15 | nil) 16 | 17 | (defn report-error! [message] 18 | (if *reports* 19 | (swap! *reports* conj [:error (apply-prefix message)])) 20 | nil) 21 | 22 | ; -- standard logging ------------------------------------------------------------------------------------------------------- 23 | 24 | (defn log-reports-if-needed! [reports & [reporter]] 25 | (when (and (or (nil? reporter) (fn? reporter)) (not (empty? reports))) 26 | (let [effective-reporter (or reporter logging/reporter)] 27 | (effective-reporter reports)))) 28 | -------------------------------------------------------------------------------- /src/lib/env_config/impl/types.cljc: -------------------------------------------------------------------------------- 1 | (ns env-config.impl.types) 2 | 3 | (defprotocol IValueWrapper 4 | (get-value [this])) 5 | 6 | (deftype Coerced [val] 7 | IValueWrapper 8 | (get-value [this] val)) 9 | 10 | (defn coerced? [val] 11 | (instance? Coerced val)) 12 | -------------------------------------------------------------------------------- /src/lib/env_config/version.cljc: -------------------------------------------------------------------------------- 1 | (ns env-config.version) 2 | 3 | (def current-version "0.2.2") ; this should match our project.clj 4 | 5 | (defmacro get-current-version [] 6 | current-version) 7 | -------------------------------------------------------------------------------- /test/src/tests/env_config/tests/main.cljc: -------------------------------------------------------------------------------- 1 | (ns env-config.tests.main 2 | (:require [clojure.test :refer [deftest testing is are]] 3 | [env-config.core :refer [read-config coerce-config make-config 4 | make-config-with-logging default-coercers]] 5 | [env-config.impl.types :refer [->Coerced]])) 6 | 7 | #?(:cljs (println "ClojureScript version:" *clojurescript-version*) 8 | :clj (println "Clojure version:" (clojure-version))) 9 | 10 | (deftest test-reading 11 | (testing "basic configs" 12 | (let [vars {"MY-PROJECT/A" "s" 13 | "MY_PROJECT/B" 42 14 | "MY_PROJECT/C_Cx" "normalize key" 15 | "MY_PROJECT/D" :keyword-val 16 | "MY_PROJECT/A1/B1" :nested-key 17 | "my_project/A1/B2" "someval" 18 | "My_Project/A1/B2" "overwrite" 19 | "My_project/A1" "should-not-destroy-subkeys" 20 | "MY_PROJECT__A1__B3" "double-underscore" 21 | "MY_PROJECT--A1-_B4" "double-dash/mixed-dash-underscore" 22 | "NOT-MY-PROJECT/X" "should not see this one" 23 | "MY-PROJECT-NOT/Y" "should not see this one"} 24 | expected-config {:a "s" 25 | :b "42" 26 | :c-cx "normalize key" 27 | :d ":keyword-val" 28 | :a1 {:b1 ":nested-key" 29 | :b2 "overwrite" 30 | :b3 "double-underscore" 31 | :b4 "double-dash/mixed-dash-underscore"}}] 32 | (is (= expected-config (read-config "my-project" vars))))) 33 | (testing "nested prefix" 34 | (is (= {:v "1"} (read-config "project/sub-project/x/y" {"project/sub-project/x/y/v" "1" 35 | "project/sub-project/x/y" "should ignore" 36 | "project/sub-project/x" "outside"}))))) 37 | 38 | (deftest test-coercion 39 | (testing "basic coercion" 40 | (let [config {:a "string" 41 | :b "42" 42 | :c "4.2" 43 | :d ":key" 44 | :e "~{:m {:x \"xxx\"}}" 45 | :f "nil" 46 | :g "true" 47 | :h "false" 48 | :i "True" 49 | :j "FALSE" 50 | :k "'sym" 51 | :l "#!@#xxx" 52 | :m "\\\\x" 53 | :n "~#!@#xxx" 54 | :o "~\"true\"" 55 | :z1 {:z2 {:z3 "nested"}}} 56 | expected-coercion {:a "string" 57 | :b 42 58 | :c 4.2 59 | :d :key 60 | :e {:m {:x "xxx"}} 61 | :f nil 62 | :g true 63 | :h false 64 | :i true 65 | :j false 66 | :k 'sym 67 | :l "#!@#xxx" 68 | :m "\\\\x" 69 | ; :n should be omitted due to eval error 70 | :o "true" 71 | :z1 {:z2 {:z3 "nested"}}}] 72 | (is (= expected-coercion (coerce-config config)))))) 73 | 74 | (deftest test-top-level-api 75 | (testing "make-config" 76 | (are [vars config] (= config (make-config "project" vars)) 77 | {"project/var" "42"} {:var 42} 78 | {"project/x/y/z" "nil"} {:x {:y {:z nil}}})) 79 | (testing "make-config with empty coercions" 80 | (are [vars config] (= config (make-config "project" vars [])) 81 | {"project/var" "42"} {:var "42"} 82 | {"project/x/y/z" "nil"} {:x {:y {:z "nil"}}})) 83 | (testing "make-config with custom coercers" 84 | (let [my-path-based-coercer (fn [path val] 85 | (if (= (first path) :coerce-me) 86 | (->Coerced (str "!" val))))] 87 | (are [vars config] (= config (make-config "project" vars [my-path-based-coercer])) 88 | {"project/dont-coerce-me" "42"} {:dont-coerce-me "42"} 89 | {"project/coerce-me" "42"} {:coerce-me "!42"} 90 | {"project/coerce-me/x" "1"} {:coerce-me {:x "!1"}} 91 | {"project/coerce-me/x/y" "s"} {:coerce-me {:x {:y "!s"}}}))) 92 | (testing "make-config-with-logging" 93 | (let [reports-atom (atom [])] 94 | (make-config-with-logging "p" {"p/c" "~#!@#xxx"} default-coercers (fn [reports] (reset! reports-atom reports))) 95 | (is (= :warn (-> @reports-atom ffirst))) 96 | (is (re-find #"unable to read-string from variable 'p/c' with value \"~#!@#xxx\"" (-> @reports-atom first second)))))) 97 | 98 | (deftest test-problems 99 | (testing "invalid code" 100 | (is (= {} (make-config "p" {"p/c" "~#!@#xxx"})))) 101 | (testing "naming conflicts" 102 | (let [reports (atom []) 103 | vars {"p/a" "1" 104 | "p/a/b" "2" 105 | "p/a/b/c" "3"} 106 | expected {:a {:b {:c 3}}} 107 | errors [[:warn "env-config: naming conflict: the variable 'p/a' with value \"1\" was shadowed by the variable 'p/a/b' with value \"2\". A variable name must not be a prefix of another variable name."] 108 | [:warn "env-config: naming conflict: the variable 'p/a/b' with value \"2\" was shadowed by the variable 'p/a/b/c' with value \"3\". A variable name must not be a prefix of another variable name."]]] 109 | (is (= expected (make-config "p" vars default-coercers reports))) 110 | (is (= errors @reports))))) 111 | -------------------------------------------------------------------------------- /test/src/tests/env_config/tests/runner.cljs: -------------------------------------------------------------------------------- 1 | (ns env-config.tests.runner 2 | (:require [cljs.test :refer-macros [run-tests]] 3 | [env-config.tests.main])) 4 | 5 | (enable-console-print!) 6 | 7 | (run-tests 'env-config.tests.main) 8 | --------------------------------------------------------------------------------