├── .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 | [](license.txt)
4 | [](https://clojars.org/binaryage/env-config)
5 | [](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 |
--------------------------------------------------------------------------------