dependencies
| (this space intentionally left almost blank) | ||||||
Welcome to defenvThis library attempts to simplify the management of environment variables.
Put simply, it's Features include: documentation generation, default values, and custom parsing. Also critical is the use of delayed binding so your application doesn't throw exceptions when loading namespaces (a huge pet peeve!). Take a look at | |||||||
(ns defenv.core (:require [clojure.pprint :refer :all] [clojure.repl :as repl] [clojure.set :refer :all] [slingshot.slingshot :refer [throw+]] [clojure.string :as str])) | |||||||
(defn- env [env-name default-value] (let [val (System/getenv env-name)] (if-not val default-value val))) | |||||||
Transformation convenience functions(for use wherever you see | |||||||
DEPRECATED in favor of parse-bool | (defn parse-bool [s] (Boolean/parseBoolean s)) (defn parse-int [s] (Integer/parseInt s)) (defn parse-long [s] (Long/parseLong s)) (defn parse-float [s] (Float/parseFloat s)) (defn parse-double [s] (Double/parseDouble s)) (defn parse-boolean {:deprecated "0.0.6"} [s] (parse-bool s)) | ||||||
Parses a string containing key-value pairs into a map. Each pair is
separated by a comma, and the key and value by an equal sign. Whitespace
surrounding keys and values is ignored. Example:
You can pass two configuration options into the See | (defn parse-map ([s] (parse-map {} s)) ([{:keys [default-key key-fn] :or {default-key "default" key-fn identity}} s] (if (re-find #"=" s) (let [kvs (str/split s #",")] (reduce (fn [m s] (let [[k v] (map str/trim (str/split s #"=")) k (if (str/blank? k) default-key (key-fn k))] (when (m k) (binding [*out* *err*] (println "**WARNING** Duplicate key found in map:" k))) (assoc m k (or v "")))) {} kvs)) {default-key s}))) | ||||||
On-the-fly documentation | |||||||
Generation | (def ^:private displays {::missing "*REQUIRED*" ::masked "--MASKED--" ::parse-error "*PARSE ERROR*"}) | ||||||
(defn- get-var-status [{:keys [env-name v default masked? optional?] :as doc-map}] (let [e-val (env env-name default)] (-> doc-map (assoc :value (if e-val (if masked? ::masked v) (if optional? "nil" ::missing))) (rename-keys {:env-name :name})))) | |||||||
(defn- get-var-statuses [display-spec] (map get-var-status (vals display-spec))) | |||||||
(defn- convert [v] (let [parsed? (vector? v) parse-difference? (and parsed? (not= (first v) (second v))) displayable (if parsed? (second v) v) displayable (if-let [replacement (displays displayable)] replacement displayable)] (if parse-difference? (format "\"%s\" -> %s" (first v) displayable) displayable))) | |||||||
Convert a display spec into a collection of objects suitable for displaying documentation | (defn display-spec->docs [display-spec] (map #(update % :value convert) (get-var-statuses display-spec))) | ||||||
DEPRECATED in favor of parse-bool | (defn spec->docs {:deprecated "1.0.11"} [display-spec] (display-spec->docs display-spec)) | ||||||
Convert a connection of spec docs into a nice table | (defn doc-table [env-docs] (with-out-str (print-table [:name :value :doc] env-docs))) | ||||||
(defn- make-usage-string [display-spec prefix] (let [env-docs (display-spec->docs display-spec)] (if (empty? env-docs) "No environment variables defined!" (str prefix "\n" (doc-table env-docs))))) | |||||||
Display | |||||||
(def ^:dynamic ^:private print-usage? (atom false)) | |||||||
Enable user-friendly usage printing globally. | (defn set-error-print-enabled! [enabled?] (reset! print-usage? enabled?)) | ||||||
Enable user-friendly usage printing in the current thread. | (defmacro with-usage-print-enabled [& body] `(binding [print-usage? (delay true)] ~@body)) | ||||||
(defn- print-to-stderr [msg] (binding [*out* *err*] (println msg))) (def ^:dynamic ^:private epfn (atom print-to-stderr)) | |||||||
(def ^:private missing "Environment variable(s) missing") | |||||||
(defn print-usage [pfn message display-spec] (pfn (make-usage-string display-spec (str message ":")))) | |||||||
Send user-friendly error messages somewhere else universally. | (defn set-err-print-fn! [f] (reset! epfn f)) | ||||||
Send user-friendly error messages somewhere else in the current thread. | (defmacro with-print-fn [f & body] `(binding [epfn (delay ~f)] ~@body)) | ||||||
(defn- throw-usage-if-missing [display-spec] (let [missing-vals (->> (get-var-statuses display-spec) (filter #(let [{:keys [value]} % [_ parsed-v] (when (vector? value) value)] (case (or parsed-v value) ::missing true ::parse-error true false))) (map :name))] (when (seq missing-vals) (let [missing-msg (format "%s: %s" missing (str/join ", " missing-vals))] (when @print-usage? (print-usage @epfn missing display-spec)) (throw+ {:type ::missing-env :missing missing-vals} missing-msg))))) | |||||||
Core functionality | |||||||
Retrieving environment variable values | |||||||
(defn- parse-env [env-name {:keys [default tfn optional?] :as params}] (let [has-param? (partial contains? params) env-args [env-name] base-value (env env-name default)] {:tfn (if (and (not (nil? base-value)) (has-param? :tfn)) tfn identity) :env-args (if (or optional? (has-param? :default)) (conj env-args default) env-args) :params-to-display (assoc params :env-name env-name) :base-value base-value})) | |||||||
(defn- pretty-demunge [fn-object] (let [demunged (repl/demunge (str fn-object)) pretty (second (re-find #"(.*?\/.*?)@.*" demunged))] (or pretty demunged))) | |||||||
(defn- try-tfn [env-name masked? tfn base-value] (try (tfn base-value) (catch Exception e (@epfn (format "Unable to parse %s='%s' using %s (%s)." env-name (if masked? (::masked displays) base-value) (-> tfn str pretty-demunge) (if masked? "--ERROR MASKED--" (.getMessage e)))) ::parse-error))) | |||||||
(defn- overlay-env [m k raw-params] (let [{:keys [optional? masked? env-name env] :as params} (merge {:masked? true} raw-params) env-name (or env-name env) {:keys [tfn params-to-display base-value]} (parse-env env-name params) v (try-tfn env-name masked? tfn base-value)] (-> m (update :env-map #(if (and (not optional?) (nil? v)) % (assoc % k v))) (update :display-spec assoc env-name (assoc params-to-display :v [base-value v]))))) | |||||||
(defn- get-env-info [env-spec] (reduce-kv overlay-env {:env-map (sorted-map), :display-spec (sorted-map)} (into (sorted-map) env-spec))) | |||||||
Operates much like The map should look something like this:
In this case, the In the case of | (defn env->map [env-spec] (let [{:keys [env-map display-spec]} (get-env-info env-spec)] (throw-usage-if-missing display-spec) env-map)) | ||||||
Get a single environment variable value. You can use any of the params
that you would use in | (defn one [env-name & params] (:e (env->map {:e (into {:env-name env-name} (apply hash-map params))}))) | ||||||
Defining a global environment variable binding | |||||||
(def ^:private global-defined-spec (ref (sorted-map))) (def ^:private global-parsed-env (ref (sorted-map))) (def ^:private global-display-spec (ref (sorted-map))) (def ^:private on-load-handlers (atom [])) | |||||||
(defmacro ^:private with-new-parsed-env! [& body] `(dosync ~@body (alter global-parsed-env empty))) | |||||||
(defn- initialize-global! [] (when (empty? @global-parsed-env) (let [{:keys [env-map display-spec]} (get-env-info @global-defined-spec)] (dosync (ref-set global-parsed-env env-map) (ref-set global-display-spec display-spec)) (doall (map #(% @global-display-spec) @on-load-handlers))))) | |||||||
(defn on-load! [f] (swap! on-load-handlers conj f)) | |||||||
(defmacro ^:private guarantee-global! [& body] `(do (initialize-global!) ~@body)) | |||||||
(defn- add-doc [doc-present? doc-or-env params] (if doc-present? (assoc params :doc doc-or-env) params)) | |||||||
Used primarily by | (defn get-global-env [env-name] (guarantee-global! (if ((set (keys @global-parsed-env)) env-name) (get @global-parsed-env env-name) (throw-usage-if-missing @global-display-spec)))) | ||||||
Used primarily by | (defn add-to-global-defined-spec! [env-name params] (with-new-parsed-env! (alter global-defined-spec assoc env-name params))) | ||||||
Define a binding If Else, we assume If If If you add If you add | (defmacro defenv [b & [doc-or-env env-or-fk & remaining]] (let [doc-present? (every? string? [doc-or-env env-or-fk]) env-name (if doc-present? env-or-fk doc-or-env) params (->> (if doc-present? remaining (when env-or-fk (concat [env-or-fk] remaining))) (apply hash-map) (add-doc doc-present? doc-or-env) (merge {:env-name env-name}))] `(do (add-to-global-defined-spec! ~env-name ~params) (def ~(vary-meta b assoc :dynamic true) (delay (get-global-env ~env-name)))))) | ||||||
Displaying environment information to your users | |||||||
From an environment specification (or nil, if using the global), construct a specification for how to display the environment configuration. | (defn display-spec ([] (display-spec nil)) ([env-spec] (if env-spec (:display-spec (get-env-info env-spec)) (guarantee-global! @global-display-spec)))) | ||||||
(defn- display-env-internal [display-spec display-fn] (print-usage display-fn "Environment" display-spec)) | |||||||
A recipe for doing your own display of environment information | (comment (let [test-spec {:stuff {:env-name "STUFF" :default "bits" :doc "fun"} :answer {:env-name "ANSWER" :default "42" :tfn parse-int :doc "ultimate"} :path {:env-name "PATH" :tfn (fn [v] (str/split v #"[:;]")) :masked? true :doc "System path!"}}] (println "\nInternal use:") (->> test-spec env->map clojure.pprint/pprint) (println "\nExternal view:") (-> test-spec display-spec display-spec->docs doc-table println)) ) | ||||||
Display the current environment to users in a friendly manner. If you call
this function without an Also, in case you want to send your own function (instead of println), you
can call the 2-argument alternative. | (defn display-env ([] (display-env nil println)) ([env-spec] (display-env env-spec println)) ([env-spec out-fn] (-> env-spec display-spec (display-env-internal out-fn)))) | ||||||
Extract the global environment config as a list of maps | (defn extract-global-spec [f] (guarantee-global! (map (comp f second) @global-defined-spec))) | ||||||
Test fixtures | |||||||
(defn reset-defined-env! [] (with-new-parsed-env! (alter global-display-spec empty) (alter global-defined-spec empty))) | |||||||
(ns defenv.docs (:require [defenv.core :as env] [hiccup.core :as h])) | |||||||
Documentation Generation | |||||||
(defn- env-var->html [{:keys [env-name default optional? masked? doc]}] [[:dd env-name] [:dt (when (or default optional? masked?) [:ul (when default [:li {:class "default-value"} "Default: " [:span default]]) (when optional? [:li {:class "optional"} "optional"]) (when masked? [:li {:class "masked"} "masked"])]) [:span {:class "doc"} (or doc [:i "No documentation found"])]]]) | |||||||
(defn- env->html [header] (h/html [:html [:head [:link {:type "text/css" :href "defenv.css" :rel "stylesheet"}]] [:body [:h1 header] (->> env-var->html env/extract-global-spec (reduce concat) (concat [:dl]) (into []))]])) | |||||||
Save the current global environment as an HTML file. The | (defn save-html [header file-name] (spit file-name (env->html header))) | ||||||
(defn- example-usage [] (load-file "src/defenv/usage.clj") (save-html "defenv environment specification" "test.html")) | |||||||
(comment (example-usage) ) | |||||||
Usage example | |||||||
(ns defenv.usage (:require [clojure.string :as str] [defenv.core :as env] [slingshot.slingshot :refer [try+]] [taoensso.timbre :as log]) (:gen-class) (:import (clojure.lang ExceptionInfo))) | |||||||
Environment variables | |||||||
(def sensible-default "A Sensible Defaultâ„¢") | |||||||
Global Bindings | |||||||
An environment variable with a default value. | (env/defenv testing "DEFENV_TESTING" :default sensible-default :masked? false) | ||||||
Shows you what happens when something is missing. | (env/defenv missing "DEFENV_MISSING" :masked? false) | ||||||
DEFENVMISSINGNO_DOCS | (env/defenv missing-no-docs :masked? false) | ||||||
Shows how values can be converted to keywords. | (env/defenv some-keyword "DEFENV_KEYWORD" :default "test" :tfn keyword :masked? false) | ||||||
Shows what happens when you don't unmask a var. | (env/defenv secret-thing "DEFENV_SECRET" :default "oops") | ||||||
A truly optional value. | (env/defenv truly-optional "DEFENV_OPT" :tfn env/parse-int :optional? true :masked? false) | ||||||
(defn parse-broken [x] (throw (ExceptionInfo. (str "I refuse to parse: " x) {}))) | |||||||
Something secret that can't be parsed. | (env/defenv secret-parse-error "DEFENV_UNPARSEABLE_SECRET" :tfn parse-broken :default "secret" :masked? true) | ||||||
Something that can't be parsed. | (env/defenv parse-error "DEFENV_UNPARSEABLE" :tfn parse-broken :default "broken" :masked? false) | ||||||
Local Map | |||||||
(def env-map-spec {:testing {:env-name "DEFENV_TESTING" :default sensible-default :masked? false} :log-level {:env-name "LOG_LEVEL" :doc "Global log level." :tfn keyword :default "info" :masked? false} :should-log? {:env-name "SHOULD_LOG" :doc "Should I log? A boolean." :tfn env/parse-bool :default "false" :masked? false} :optional {:env-name "DEFENV_OPT" :optional? true :doc "A truly optional value." :masked? false}}) (def env-map (env/env->map env-map-spec)) | |||||||
(defmacro handle-exception [& body] `(try+ (printf "this should die: %s%n" ~@body) (catch [:type :defenv.core/missing-env] {missing# :missing} (println "Exception msg:" (.getMessage (:throwable ~'&throw-context))) (println "Variables missing:" (str/join ", " missing#))))) | |||||||
A very simple example of how to use the library. Just run | (defn -main [& _] (println "* Display usage without trying to access a variable *") (env/on-load! (fn [_] (println "** Global Bindings On-Load **"))) (env/on-load! (partial env/print-usage println "Environment On-Load")) (env/on-load! (fn [_] (println "*****************************"))) (println "We should see 2 dumps of the same global environment:") (env/display-env nil println) (println "\n** Local Map **") (println env-map) (env/display-env env-map-spec) (println "\n** Single values **") (println (env/one "PATH")) (handle-exception (env/one "MISSING_SECRET" :masked? true :tfn env/parse-int)) (println "\n* Error Handling *") (env/set-error-print-enabled! true) (env/set-err-print-fn! #(log/error %)) (printf "Exception Printing: %s%n" @testing) (handle-exception @missing)) | ||||||