From 6aab668cc2932a86f65667618b990aa94adb51a5 Mon Sep 17 00:00:00 2001 From: "Robert P. Levy" Date: Sat, 2 Feb 2013 01:51:16 -0500 Subject: [PATCH 1/8] decomplect overrides from sources; new multi-source syntax --- project.clj | 2 +- src/milieu/config.clj | 126 +++++++++++++++++++++++------------- test/milieu/config_test.clj | 107 ++++++++++++++++++++---------- 3 files changed, 156 insertions(+), 79 deletions(-) diff --git a/project.clj b/project.clj index 9ca1dce..1aee838 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject milieu "0.9.1" +(defproject milieu "1.0.0-SNAPSHOT" :description "The environmentally friendly configuration tool." :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} diff --git a/src/milieu/config.clj b/src/milieu/config.clj index d92e794..3d7301a 100644 --- a/src/milieu/config.clj +++ b/src/milieu/config.clj @@ -8,7 +8,7 @@ (defonce ^:private configuration (atom {})) -(defonce ^:private overrides (atom {})) +(def ^{:private false :dynamic true} *overrides* {}) (def ^:private env-sysvar-name "MILIEU_ENV") @@ -34,20 +34,92 @@ (or (keyword (getenv env-sysvar-name)) :dev)) +(defn ^:private cli [args] + (let [index-or-key #(if (re-matches #"\d+" %) + (Integer/parseInt %) (keyword %)) + cmdarg->cfgkey + (fn [s] (-<> s + (str/replace <> #"^-+" "") + (str/split <> #"\.") + (map index-or-key <>) + vec)) + ;; TODO: tools.cli would be useful here + read-string' (fn [s] + #_"read-string could be used as it is, but it is tiresome + to enter strings like '\"....\"' in command-line args. + To address this, accept tokens verbatim and string-ify + non-keyword, non-number tokens. Tokens that don't play + well with the reader are also interpreted as strings." + (if (re-find #"\s" s) + s + (let [x (binding [*read-eval* false] + (try (read-string s) (catch Exception e s)))] + (if (or (keyword? x) + (= (class x) java.lang.Boolean) + (isa? (class x) java.lang.Number)) + x + (str s)))))] + (reduce-kv (fn [r cmdarg cmdval] + (into r [(cmdarg->cfgkey cmdarg) (read-string' cmdval)])) + [] + (apply hash-map args)))) + +(defn ^:private resolve-format + "if :as format is not specified, determine the format based on source" + [src-spec] + (when src-spec + (or (:as src-spec) + (let [resolve-format* + (fn [src-spec] + (condp re-find src-spec + #"\.ya?ml$" :yml + #"\.json$" :json + #"\.edn$" :edn + (throw (Exception. "Unable to resolve config format."))))] + (if (string? src-spec) + (resolve-format* src-spec) + (resolve-format* (:src src-spec))))))) + +(defmulti src resolve-format) + +(defmethod src nil [_] {}) + +(defmethod src :yml [src-spec] (throw (Exception. "TODO"))) + +(defmethod src :json [src-spec] (throw (Exception. "TODO"))) + +(defmethod src :edn [src-spec] (throw (Exception. "TODO"))) + +(defmethod src :assoc-in [src-spec] + (reduce-kv assoc-in {} (apply hash-map (:src src-spec)))) + +(defmethod src :cli [src-spec] + (src (assoc (update-in src-spec [:src] cli) + :as :assoc-in))) + +(defmethod src :data [src-spec] + (:src src-spec)) + (defmacro with-env "bind the environment to a value for the calling context. Env can optionally be a vector containing the env and options. Presently the option :only is supported, which stops execution if the provided - env is not in the limited set." + env is not in the limited set. + + Overrides usage: (with-env :dev :overrides {:src :as } ...)" [env & body] - (let [[env {:keys [only] :as options}] - (if (vector? env) - [(first env) (apply hash-map (rest env))] - [env])] + (let [[env {:keys [only] :as options}] (if (vector? env) + [(first env) + (apply hash-map (rest env))] + [env]) + [src-spec body] (if (= (first body) :overrides) + [(second body) (next (next body))] + [nil body])] `(if (and ~only (not ((set ~only) (keyword ~env)))) (throw (Exception. "Access to this environment is prohibited.")) - (binding [*env* (or (keyword ~env) *env*)] + (binding [*env* (or (keyword ~env) *env*) + *overrides* (src ~src-spec)] ~@body)))) (defmacro only-env @@ -73,8 +145,8 @@ (defn value* [[k & ks] & optional?] - (let [env-value (get-in @configuration (concat [*env* k] ks)) - override-value (get-in @overrides (concat [:cmdargs k] ks))] + (let [env-value (get-in @configuration (concat [*env* k] ks)) + override-value (get-in *overrides* (cons k ks))] (cond (or override-value (false? override-value)) override-value (or env-value (false? env-value)) env-value :none-provided (when-not optional? @@ -126,41 +198,5 @@ yaml/parse-string keywordize))))) -(defn ^:private commandline-overrides* [args] - (assert (even? (count args))) - (let [index-or-key #(if (re-matches #"\d+" %) - (Integer/parseInt %) (keyword %)) - cmdarg->cfgkey - (fn [s] (-<> s - (str/replace <> #"^-+" "") - (str/split <> #"\.") - (map index-or-key <>) - vec)) - read-string' (fn [s] - #_"read-string could be used as it is, but it is tiresome - to enter strings like '\"....\"' in command-line args. - To address this, accept tokens verbatim and string-ify - non-keyword, non-number tokens. Tokens that don't play - well with the reader are also interpreted as strings." - (if (re-find #"\s" s) - s - (let [x (binding [*read-eval* false] - (try (read-string s) (catch Exception e s)))] - (if (or (keyword? x) - (= (class x) java.lang.Boolean) - (isa? (class x) java.lang.Number)) - x - (str s)))))] - {:cmdargs - (reduce-kv - #(assoc-in %1 (cmdarg->cfgkey %2) (read-string' %3)) - {} (apply hash-map args))})) - -(defn commandline-overrides! - "override values, regardless of environment. - $ myprogram prod --fou.barre Fred --db.host 127.0.0.1" - [args] - (swap! overrides (fn [m] (merge m (commandline-overrides* args))))) - (when (io/resource default-config-name) ; auto-load if file name convention (load-config default-config-name)) ; for auto-load is followed. diff --git a/test/milieu/config_test.clj b/test/milieu/config_test.clj index 6fff95b..63fecbd 100644 --- a/test/milieu/config_test.clj +++ b/test/milieu/config_test.clj @@ -105,49 +105,90 @@ (provided (#'config/warn* irrelevant irrelevant) => true :times 0)) (facts - "command-line overrides" - (#'config/commandline-overrides* ["--fou.barre" "1" "--skidoo" "(1 2 3)"]) - => {:cmdargs {:skidoo "(1 2 3)", :fou {:barre 1}}} + "cli as source" + (#'config/cli ["--fou.barre" "1", "--skidoo" "1 2 3"]) + => [[:fou :barre] 1, [:skidoo] "1 2 3"] + (#'config/cli ["--fou" "1", "--skidoo" "(1 2 3)"]) + => [[:fou] 1, [:skidoo] "(1 2 3)"]) - (#'config/commandline-overrides* ["--fou" "1" "--skidoo" "(1 2 3)"]) - => {:cmdargs {:skidoo "(1 2 3)", :fou 1}} - - (#'config/commandline-overrides* ["--fou" "1" "--skidoo" "1 2 3"]) - => {:cmdargs {:skidoo "1 2 3", :fou 1}} - - (#'config/commandline-overrides* ["-fou" "1" "-skidoo" "(1 2 3)"]) - => {:cmdargs {:skidoo "(1 2 3)", :fou 1}} - - (#'config/commandline-overrides* ["fou" "1" "skidoo" "(1 2 3)"]) - => {:cmdargs {:skidoo "(1 2 3)", :fou 1}} - - (#'config/commandline-overrides* ["--fou.barre" "skidoo"]) - => {:cmdargs {:fou {:barre "skidoo"}}} - - (#'config/commandline-overrides* ["--fou.barre" "skidoo"]) - => #(= "skidoo" (str (get-in % [:cmdargs :fou :barre]))) +(facts + "overriding values" + (config/with-env :dev + :overrides {:src ["--fou.barre" "1", "--skidoo" "(1 2 3)"] :as :cli} + config/*overrides*) + => {:skidoo "(1 2 3)", :fou {:barre 1}} + + (config/with-env :dev + :overrides {:src ["--fou" "1", "--skidoo" "(1 2 3)"] :as :cli} + config/*overrides*) + => {:skidoo "(1 2 3)", :fou 1} + + (config/with-env :dev + :overrides {:src ["--fou" "1", "--skidoo" "1 2 3"] :as :cli} + config/*overrides*) + => {:skidoo "1 2 3", :fou 1} + + (config/with-env :dev + :overrides {:src ["-fou" "1", "-skidoo" "(1 2 3)"] :as :cli} + config/*overrides*) + => {:skidoo "(1 2 3)", :fou 1} + + (config/with-env :dev + :overrides {:src [[:fou] 1, [:skidoo] "(1 2 3)"] :as :assoc-in} + config/*overrides*) + => {:skidoo "(1 2 3)", :fou 1} + + (config/with-env :dev + :overrides {:src {:fou 1, :skidoo "(1 2 3)"} :as :data} + config/*overrides*) + => {:skidoo "(1 2 3)", :fou 1} + + (config/with-env :dev + :overrides {:src ["fou" "1", "skidoo" "(1 2 3)"] :as :cli} + config/*overrides*) + => {:skidoo "(1 2 3)", :fou 1} + + (config/with-env :dev + :overrides {:src ["--fou.barre" "skidoo"] :as :cli} + config/*overrides*) + => {:fou {:barre "skidoo"}} + + (config/with-env :dev + :overrides {:src ["--fou.barre" "1"] :as :cli} + (config/value :fou :barre)) + => 1 - (#'config/commandline-overrides* ["--smiles.1.mary" ":D"]) - => #(= ":D" (str (get-in % [:cmdargs :smiles 1 :mary]))) + (config/with-env :dev + :overrides {:src ["--smiles.0.fred" ":{)"] :as :cli} + (config/value :smiles 0 :fred)) + => ":{)" - (do (config/commandline-overrides! ["--fou.barre" "1"]) - (config/with-env :cmdargs (config/value :fou :barre))) - => 1 + (config/with-env :dev + :overrides {:src [[:smiles 0 :fred] ":{)"] :as :assoc-in} + (config/value :smiles 0 :fred)) + => ":{)" - (do (config/commandline-overrides! ["--smiles.0.fred" ":{)"]) - (config/with-env :cmdargs (config/value :smiles 0 :fred))) + (config/with-env :dev + :overrides {:src {:smiles [{:fred ":{)"}]} :as :data} + (config/value :smiles 0 :fred)) => ":{)" ;; override means it should take precedence over the active environment (let [_ (swap! @#'config/configuration #(assoc-in % [:prod :fou :my-barre] "127.0.0.1")) barre (config/with-env :prod (config/value :fou :my-barre)) - _ (config/commandline-overrides! ["--fou.my-barre" "1.2.3.4"]) - changed-barre (config/with-env :prod (config/value :fou :my-barre)) - _ (config/commandline-overrides! ["--fou.my-barre" "false"]) - changed-to-false (config/with-env :prod (config/value :fou :my-barre))] - [barre changed-barre changed-to-false]) - => ["127.0.0.1" "1.2.3.4" false]) + [hello barre'] (config/with-env :prod + :overrides {:src [[:hello] 2 + [:fou :my-barre] "1.2.3.4"] + :as :assoc-in} + [(config/value :hello) + (config/value :fou :my-barre)]) + barre'' (config/with-env :prod + :overrides {:src [[:fou :my-barre] false] + :as :assoc-in} + (config/value :fou :my-barre))] + [barre hello barre' barre'']) + => ["127.0.0.1" 2 "1.2.3.4" false]) (facts "about checking environment as valid/existing" From bd3986e78bd8b94833b71dc412a130ec8c5ea8d0 Mon Sep 17 00:00:00 2001 From: "Robert P. Levy" Date: Sat, 2 Feb 2013 01:57:21 -0500 Subject: [PATCH 2/8] update documentation --- README.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d5e8905..7787609 100644 --- a/README.md +++ b/README.md @@ -66,10 +66,22 @@ the popular YAML file format. * Specify the default environment using the MILIEU_ENV system variable. * Override environment-specific settings using arguments to your command-line -application. +application, or other kinds of sources. ```clojure - (config/commandline-overrides! args) - (config/with-env env + (config/with-env :dev + :overrides {:src args + :as :cli} ; args from -main function + ... ) + + (config/with-env :dev + :overrides {:src {:hello {:world 1}} + :as :data} + ... ) + + (config/with-env :dev + :overrides {:src [[:hello] 2, + [:fou :my-barre] "1.2.3.4"] + :as :assoc-in} ... ) ``` From 967532a149d050028229ef7e9d5bd04aa9201c29 Mon Sep 17 00:00:00 2001 From: "Robert P. Levy" Date: Sat, 2 Feb 2013 02:27:52 -0500 Subject: [PATCH 3/8] use new data source spec syntax for load-config as well --- src/milieu/config.clj | 44 +++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/milieu/config.clj b/src/milieu/config.clj index 3d7301a..aba7370 100644 --- a/src/milieu/config.clj +++ b/src/milieu/config.clj @@ -80,11 +80,28 @@ (resolve-format* src-spec) (resolve-format* (:src src-spec))))))) +(defn ^:private keywordize + "recursively convert strings that should be keywords into keywords" + [config-map] + (walk/prewalk + (fn [form] + (cond (and (string? form) (= \: (first form))) + (keyword (apply str (rest form))), + (seq? form) (vec form), + :else form)) + config-map)) + (defmulti src resolve-format) (defmethod src nil [_] {}) -(defmethod src :yml [src-spec] (throw (Exception. "TODO"))) +(defmethod src :yml [src-spec] + (let [config-file (io/resource (if (string? src-spec) + src-spec + (:src src-spec)))] + (if-not config-file + (throw (Exception. "config file not found.")) + (-> config-file, slurp, yaml/parse-string, keywordize)))) (defmethod src :json [src-spec] (throw (Exception. "TODO"))) @@ -175,28 +192,11 @@ (let [ks (keep identity (flatten [ks alt more]))] `(value* [~@ks] :optional)))) -(defn ^:private keywordize - "helper function for load-config" - [config-map] - (walk/prewalk - (fn [form] - (cond (and (string? form) (= \: (first form))) - (keyword (apply str (rest form))), - (seq? form) (vec form), - :else form)) - config-map)) - (defn load-config - "load the yaml config file" - [config-name] - (let [config-file (io/resource config-name)] - (if-not config-file (throw (Exception. "config file not found."))) - (swap! configuration - #(merge % - (-> config-file - slurp - yaml/parse-string - keywordize))))) + "load configuration source" + [src-spec] + ;; TODO: recursive merge with preexisting config + (swap! configuration #(merge % (src src-spec)))) (when (io/resource default-config-name) ; auto-load if file name convention (load-config default-config-name)) ; for auto-load is followed. From cb4ed84037b427e161bd263672a566e54f0affdb Mon Sep 17 00:00:00 2001 From: "Robert P. Levy" Date: Sat, 2 Feb 2013 02:43:54 -0500 Subject: [PATCH 4/8] less awkward way of writing same thing --- src/milieu/config.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/milieu/config.clj b/src/milieu/config.clj index aba7370..af35c60 100644 --- a/src/milieu/config.clj +++ b/src/milieu/config.clj @@ -60,7 +60,7 @@ x (str s)))))] (reduce-kv (fn [r cmdarg cmdval] - (into r [(cmdarg->cfgkey cmdarg) (read-string' cmdval)])) + (conj r (cmdarg->cfgkey cmdarg) (read-string' cmdval))) [] (apply hash-map args)))) From e7ea605dc4e18b89a035434feeda053e7f11648d Mon Sep 17 00:00:00 2001 From: "Robert P. Levy" Date: Mon, 4 Feb 2013 10:45:25 -0500 Subject: [PATCH 5/8] improve with-env doc string --- src/milieu/config.clj | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/milieu/config.clj b/src/milieu/config.clj index af35c60..0bde10b 100644 --- a/src/milieu/config.clj +++ b/src/milieu/config.clj @@ -124,7 +124,11 @@ the option :only is supported, which stops execution if the provided env is not in the limited set. - Overrides usage: (with-env :dev :overrides {:src :as } ...)" + Usage: + + (with-env ...) + (with-env [ :only [, , ... ]] + (with-env :overrides {:src :as } ...)" [env & body] (let [[env {:keys [only] :as options}] (if (vector? env) [(first env) From 9a5fbd92b30e25ecc1afe7ab5afdef43320cdfa0 Mon Sep 17 00:00:00 2001 From: "Robert P. Levy" Date: Wed, 6 Feb 2013 13:15:36 -0500 Subject: [PATCH 6/8] support for additional file formats * json support * edn support * use same specification syntax as overrides * scrap keywordize because clj-yaml has it now --- project.clj | 5 ++-- resources/configure.example.edn | 18 ++++++++++++++ resources/configure.example.json | 25 +++++++++++++++++++ src/milieu/config.clj | 31 ++++++++++------------- test/milieu/config_test.clj | 42 ++++++++++++++++++++++++++------ 5 files changed, 94 insertions(+), 27 deletions(-) create mode 100644 resources/configure.example.edn create mode 100644 resources/configure.example.json diff --git a/project.clj b/project.clj index 1aee838..bd0cb7c 100644 --- a/project.clj +++ b/project.clj @@ -4,7 +4,8 @@ :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.4.0"] [org.clojure/tools.logging "0.2.3"] - [clj-yaml "0.3.1"] - [swiss-arrows "0.3.0"]] + [clj-yaml "0.4.0"] + [cheshire "5.0.1"] + [swiss-arrows "0.5.1"]] :profiles {:dev {:dependencies [[midje "1.4.0"]]}} :plugins [[lein-midje "2.0.0-SNAPSHOT"]]) diff --git a/resources/configure.example.edn b/resources/configure.example.edn new file mode 100644 index 0000000..7ab562f --- /dev/null +++ b/resources/configure.example.edn @@ -0,0 +1,18 @@ +{:dev {:fou + {:barre "127.0.0.1" + :car 54} + :size "small" + :smiles [{:mary "8-)" + :fred ":-|"} + {:mary "*_*" + :fred "-__-"}] + :magic false + :skidoo 23} + :test {:fou + {:barre "9.9.9.9", :car 9}, + :size "medium", + :magic false}, + :prod {:fou + {:barre "7.7.7.7", :car 6}, + :size "large", + :magic true}} diff --git a/resources/configure.example.json b/resources/configure.example.json new file mode 100644 index 0000000..0968a84 --- /dev/null +++ b/resources/configure.example.json @@ -0,0 +1,25 @@ +{"dev": + {"fou": + {"barre":"127.0.0.1", + "car":54}, + "size":"small", + "smiles":[{"mary":"8-)", + "fred":":-|"}, + {"mary":"*_*", + "fred":"-__-"}], + "magic":false, + "skidoo":23}, + + "test": + {"fou": + {"barre":"9.9.9.9", + "car":9}, + "size":"medium", + "magic":false}, + + "prod": + {"fou": + {"barre":"7.7.7.7", + "car":6}, + "size":"large", + "magic":true}} diff --git a/src/milieu/config.clj b/src/milieu/config.clj index 0bde10b..cc6ebdf 100644 --- a/src/milieu/config.clj +++ b/src/milieu/config.clj @@ -3,6 +3,7 @@ [clojure.tools.logging :as log] [clojure.walk :as walk] [clj-yaml.core :as yaml] + [cheshire.core :as json] [clojure.string :as str] [swiss-arrows.core :refer [-<>]])) @@ -80,32 +81,26 @@ (resolve-format* src-spec) (resolve-format* (:src src-spec))))))) -(defn ^:private keywordize - "recursively convert strings that should be keywords into keywords" - [config-map] - (walk/prewalk - (fn [form] - (cond (and (string? form) (= \: (first form))) - (keyword (apply str (rest form))), - (seq? form) (vec form), - :else form)) - config-map)) +(defn ^:private file-src [src-spec process-config] + (let [config-file (io/resource (if (string? src-spec) + src-spec + (:src src-spec)))] + (if-not config-file + (throw (Exception. "config file not found.")) + (process-config config-file)))) (defmulti src resolve-format) (defmethod src nil [_] {}) (defmethod src :yml [src-spec] - (let [config-file (io/resource (if (string? src-spec) - src-spec - (:src src-spec)))] - (if-not config-file - (throw (Exception. "config file not found.")) - (-> config-file, slurp, yaml/parse-string, keywordize)))) + (file-src src-spec #(yaml/parse-string (slurp %)))) -(defmethod src :json [src-spec] (throw (Exception. "TODO"))) +(defmethod src :json [src-spec] + (file-src src-spec #(json/parse-string (slurp %) true))) -(defmethod src :edn [src-spec] (throw (Exception. "TODO"))) +(defmethod src :edn [src-spec] + (file-src src-spec #(binding [*read-eval* false] (read-string (slurp %))))) (defmethod src :assoc-in [src-spec] (reduce-kv assoc-in {} (apply hash-map (:src src-spec)))) diff --git a/test/milieu/config_test.clj b/test/milieu/config_test.clj index 63fecbd..c3ccfcf 100644 --- a/test/milieu/config_test.clj +++ b/test/milieu/config_test.clj @@ -3,20 +3,48 @@ (require [milieu.config :as config])) (facts - ;; make sure we can load and parse a config file + "make sure we can load and parse a config file" (do (swap! @#'config/configuration (constantly {})) @@#'config/configuration) => {} - (config/load-config "non-existent-file.yml") => (throws Exception) + (config/load-config "non-existent-file.yml") => (throws Exception)) - ;; this also sets up state for the following tests - - (do (config/load-config "configure.example.yml") - (:dev @@#'config/configuration)) - => map?) +(facts + "all of the example config files are identical, but in different formats. + all of the config loading methods should result in same data structures." + + ;; and this also sets up state for the following tests + + (let [from-yml (do (reset! @#'config/configuration {}) + (config/load-config "configure.example.yml") + @@#'config/configuration) + from-yml-alt (do (reset! @#'config/configuration {}) + (config/load-config {:src "configure.example.yml" + :as :yml}) + @@#'config/configuration) + from-json (do (reset! @#'config/configuration {}) + (config/load-config "configure.example.json") + @@#'config/configuration) + from-json-alt (do (reset! @#'config/configuration {}) + (config/load-config {:src "configure.example.json" + :as :json}) + @@#'config/configuration) + from-edn (do (reset! @#'config/configuration {}) + (config/load-config "configure.example.edn") + @@#'config/configuration) + from-edn-alt (do (reset! @#'config/configuration {}) + (config/load-config {:src "configure.example.edn" + :as :edn}) + @@#'config/configuration)] + from-json => from-json-alt + from-json-alt => from-yml + from-yml => from-yml-alt + from-yml-alt => from-edn + from-edn => from-edn-alt + from-edn-alt => from-json)) (facts "with-env observes specified restrictions" From a855e4781969f4f84bd87916221f896663bd84f0 Mon Sep 17 00:00:00 2001 From: "Robert P. Levy" Date: Wed, 6 Feb 2013 13:29:14 -0500 Subject: [PATCH 7/8] update documentation --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7787609..a89dfba 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,7 @@ The environmentally friendly configuration tool. ## Features: -* Set up environment-specific configuration for your Clojure application, using -the popular YAML file format. +* Set up environment-specific configuration for your Clojure application. Supported file formats include YAML, JSON, and EDN. Supported non-file methods of configuration include native Clojure data, [(IN PROGRESS) system-environment-variables, and command-line options]. * Access config values: @@ -55,7 +54,14 @@ the popular YAML file format. * Optionally auto-load config file. * using the default filename of "configure.yml" enables autoload - * any other config file name can be specified by calling load-config + * other types of file and non-file data sources can be specified using load-config. Examples: + ```clojure + (config/load-config "my-config.yml") + + (config/load-config {:src "my-config" :as :yml}) + + (config/load-config {:src "my-config" :as :edn}) + ``` * Bind the environment within a calling context using with-env. ```clojure From 4716d827d0c7e50a53392e12f3d035f15c58250a Mon Sep 17 00:00:00 2001 From: "Robert P. Levy" Date: Thu, 7 Feb 2013 13:47:57 -0500 Subject: [PATCH 8/8] (incomplete) working on command-line feature --- src/milieu/config.clj | 79 +++++++++++++++++++++++++++++++++---------- 1 file changed, 61 insertions(+), 18 deletions(-) diff --git a/src/milieu/config.clj b/src/milieu/config.clj index cc6ebdf..fd03cd7 100644 --- a/src/milieu/config.clj +++ b/src/milieu/config.clj @@ -1,7 +1,8 @@ (ns milieu.config (:require [clojure.java.io :as io] - [clojure.tools.logging :as log] - [clojure.walk :as walk] + [clojure.tools + [logging :as log] + [cli :as cli]] [clj-yaml.core :as yaml] [cheshire.core :as json] [clojure.string :as str] @@ -35,22 +36,16 @@ (or (keyword (getenv env-sysvar-name)) :dev)) -(defn ^:private cli [args] - (let [index-or-key #(if (re-matches #"\d+" %) - (Integer/parseInt %) (keyword %)) - cmdarg->cfgkey - (fn [s] (-<> s - (str/replace <> #"^-+" "") - (str/split <> #"\.") - (map index-or-key <>) - vec)) - ;; TODO: tools.cli would be useful here - read-string' (fn [s] +(defn ^:private cli [cli-options args] + (let [read-string' (fn [s] #_"read-string could be used as it is, but it is tiresome to enter strings like '\"....\"' in command-line args. To address this, accept tokens verbatim and string-ify non-keyword, non-number tokens. Tokens that don't play - well with the reader are also interpreted as strings." + well with the reader are also interpreted as strings. + + As an alternative to this 'type guessing' :cli-options + can be specified, applying clojure.tools.cli." (if (re-find #"\s" s) s (let [x (binding [*read-eval* false] @@ -59,9 +54,24 @@ (= (class x) java.lang.Boolean) (isa? (class x) java.lang.Number)) x - (str s)))))] + (str s))))) + generate-options (fn [args] (reduce + (fn [r k] + (conj r [k :parse-fn read-string'])) + [] + (filter #(= (first %) \-) args))) + cmdarg->cfgkey (fn [s] (-<> s + (str/replace <> #"^-+" "") + (str/split <> #"\.") + (map #(if (re-matches #"\d+" %) + (Integer/parseInt %) + (keyword %)) + <>) + vec)) + [cmdargs _ banner] (cli/cli args (or cli-options + (generate-options args))) (reduce-kv (fn [r cmdarg cmdval] - (conj r (cmdarg->cfgkey cmdarg) (read-string' cmdval))) + (conj r (cmdarg->cfgkey cmdarg) cmdval)) [] (apply hash-map args)))) @@ -89,7 +99,37 @@ (throw (Exception. "config file not found.")) (process-config config-file)))) -(defmulti src resolve-format) +(defmulti src + ":tgt (not yet implemented) + specify a target config environment for these values, for cases where it + is not already specified in the source data itself. + + :scope (not yet implemented) + specify a limited scope, such that provided values only apply when the + given scope is indicated at the time of accesing the value. + + :as specifies source type. + :yml|:json|:edn + for file-based configuration + :src \"filenamestring\" + :cli + command-line interface + :src [\"--option\" \"value\" ...] + :cli-options (see clojure.tools.cli documentation) + If not specified, then the values are automatically converted in a way + that maintains parity with the conversions applied parsing file formats + :cli-help-function [:help-key (fn [usage-doc] ... )] + optional hook for printing help/usage using tool.cli-generated usage + string. + :data + directly specify values as data. + :src {:dev {:foo 1}} + :assoc-in + directly specify values in the style used by assoc-in. + :src [[:key1 :key1] value, ...] + :environ (system environment vars and lein config) + (not yet implemented - will integrate weavejester/environ functionality)" + resolve-format) (defmethod src nil [_] {}) @@ -106,7 +146,7 @@ (reduce-kv assoc-in {} (apply hash-map (:src src-spec)))) (defmethod src :cli [src-spec] - (src (assoc (update-in src-spec [:src] cli) + (src (assoc (update-in src-spec [:src] (partial cli (:cli-options src-spec))) :as :assoc-in))) (defmethod src :data [src-spec] @@ -119,6 +159,9 @@ the option :only is supported, which stops execution if the provided env is not in the limited set. + The option :overrides provides the ability to override values for the + specified environment + Usage: (with-env ...)