/
config.clj
143 lines (114 loc) · 5.33 KB
/
config.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
(ns com.fulcrologic.fulcro.server.config
"Utilities for managing server configuration via EDN files.
The general design requirements of this support are that you should be able to:
* Specify your configuration as EDN.
* Specify a reasonable set of server config values as \"defaults\" so that specific environments can override
just what matters.
* Override the defaults by deep-merging an environment-specific config file over the defaults.
* Specify individual overrides via environment variables.
** Support rich data types from environment variables, like maps, numerics, etc.
So the basic operation is that you create a default EDN file and one or more environment files (e.g.
`dev.edn`, `prod.edn`, `joes-test-env.edn`, etc. You can then use a combination of runtime parameters,
JVM properties, and environment variables to end up with your runtime configuration.
See `load-config!` for more detailed usage.
"
(:require
[clojure.java.io :as io]
[clojure.edn :as edn]
[clojure.walk :as walk]
[com.fulcrologic.guardrails.core :refer [>defn =>]]
[taoensso.timbre :as log]
[com.fulcrologic.fulcro.algorithms.do-not-use :as util]
[clojure.spec.alpha :as s]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CONFIG
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn get-system-prop [prop-name]
(System/getProperty prop-name))
(defn- load-edn!
"If given a relative path, looks on classpath (via class loader) for the file, reads the content as EDN, and returns it.
If the path is an absolute path, it reads it as EDN and returns that.
If the resource is not found, returns nil.
This function returns the EDN file without further interpretation (no merging or env evaluation). Normally you want
to use `load-config!` instead."
[^String file-path]
(let [?edn-file (io/file file-path)]
(if-let [edn-file (and (.isAbsolute ?edn-file)
(.exists ?edn-file)
(io/file file-path))]
(-> edn-file slurp edn/read-string)
(some-> file-path io/resource .openStream slurp edn/read-string))))
(defn- load-edn-file!
"Calls load-edn on `file-path`,
and throws an ex-info if that failed."
[file-path]
(log/info "Reading configuration file at " file-path)
(if-let [edn (some-> file-path load-edn!)]
edn
(do
(log/error "Unable to read configuration file " file-path "See https://book.fulcrologic.com/#err-config-file-read-err")
(throw (ex-info (str "Invalid config file at '" file-path "'")
{:file-path file-path})))))
(defn- resolve-symbol [sym]
{:pre [(namespace sym)]
:post [(not (nil? %))]}
(or (resolve sym)
(do (-> sym namespace symbol require)
(resolve sym))))
(defn- get-system-env [var-name]
(System/getenv var-name))
(defn load-config!
"Load a configuration file via the given options.
options is a map with keys:
* `:config-path` : The path to the file to load (in addition to the addl behavior described below).
* `:defaults-path` : (optional) A relative or absolute path to the default options that should be the basis of configuration.
Defaults to `config/defaults.edn`. When relative, will come from resources. When absolute, will come from disk.
Reads the defaults from CLASSPATH (default config/defaults.edn), then deep merges the EDN content
of an additional config file you specify into that and evaluates environment variable expansions.
You may use a Java system property to specify (*override*) the `:config-path` option:
```
java -Dconfig=/usr/local/etc/app.edn ...
```
allowing you to affect a packaged JAR application.
Values in the EDN of the form :env/VAR mean to use the raw string value of an environment variable, and
:env.edn/VAR mean to use the `read-string` value of the environment variable as that value.
So the classpath resource config/defaults.edn might contain:
```
{:port 3000
:service :A}
```
and `/usr/local/etc/app.edn` might contain:
```
{:port :env.edn/PORT}
```
and a call to `(load-config! {:config-path \"/usr/local/etc/app.edn\"})` on a system with env variable `PORT=\"8080\"` would return:
```
{:port 8080 ;; as an integer, not a string
:service :A}
```
If your EDN file includes a symbol (which must be namespaced) then it will try to require and resolve
it dynamically as the configuration loads.
"
([] (load-config! {}))
([{:keys [config-path defaults-path]}]
(let [defaults-path (if (seq defaults-path)
defaults-path
"config/defaults.edn")
defaults (load-edn-file! defaults-path)
config (load-edn-file! (or (get-system-prop "config") config-path))]
(->> (util/deep-merge defaults config)
(walk/prewalk #(cond-> % (symbol? %) resolve-symbol
(and (keyword? %) (namespace %)
(re-find #"^env.*" (namespace %)))
(-> name get-system-env
(cond-> (= "env.edn" (namespace %))
(edn/read-string)))))))))
(def ^:deprecated load-config
"Use load-config!"
load-config!)
(def ^:deprecated open-config-file
"Not meant for public consumption"
load-edn-file!)
(def ^:deprecated load-edn
"Not meant for public consumption"
load-edn!)