Idiomatyczne zarządzanie konfiguracją w clojure?

Jaki jest idiomatyczny sposób obsługi konfiguracji aplikacji w clojure?

Do tej pory używam tego środowiska:

;; config.clj
{:k1 "v1"
 :k2 2}

;; core.clj
(defn config []
  (let [content (slurp "config.clj")]
    (binding [*read-eval* false]
      (read-string content))))

(defn -main []
  (let [config (config)]
    ...))

Który ma wiele minusów:

  • ścieżka do config.clj nie zawsze może być poprawnie rozwiązana
  • brak jednoznacznej struktury sekcji konfiguracyjnych dla używanych bibliotek / frameworków
  • Nie dostępny globalnie (@app/config) (co oczywiście może być postrzegane jako dobry styl funkcjonalny, ale umożliwia dostęp do konfiguracji w całym pliku źródłowym nudne.

Większe projekty open-source, takie jak storm, używają YAML zamiast Clojure i sprawiają, że config jest dostępny globalnie poprzez nieco brzydki hack: (eval ``(def ~(symbol new-name) (. Config ~(symbol name)))).

Author: Kreisquadratur, 2013-07-18

3 answers

Najpierw użyj clojure.edn, a w szczególności clojure.edn / Czytaj. Np.

(use '(clojure.java [io :as io]))
(defn from-edn
  [fname]    
  (with-open [rdr (-> (io/resource fname)
                      io/reader
                      java.io.PushbackReader.)]
    (clojure.edn/read rdr)))

Odnośnie ścieżki konfiguracji.edn korzystanie z io / resource jest tylko jednym ze sposobów radzenia sobie z tym. Ponieważ prawdopodobnie chcesz zapisać zmienioną konfigurację.edn podczas wykonywania można polegać na fakcie, że ścieżka dla czytników plików i pisarzy zbudowana z niekwalifikowaną nazwą pliku, taką jak

(io/reader "where-am-i.edn")

Domyślnie

(System/getProperty "user.dir")

Biorąc pod uwagę fakt, że możesz chcieć zmienić konfigurację podczas wykonywania może zaimplementować taki wzór (rough sketch)

;; myapp.userconfig
(def default-config {:k1 "v1"
                     :k2 2})
(def save-config (partial spit "config.edn"))
(def load-config #(from-edn "config.edn")) ;; see from-edn above

(let [cfg-state (atom (load-config))]
  (add-watch cfg-state :cfg-state-watch
    (fn [_ _ _ new-state]
      (save-config new-state)))
  (def get-userconfig #(deref cfg-state))
  (def alter-userconfig! (partial swap! cfg-state))
  (def reset-userconfig! #(reset! cfg-state default-config)))

Zasadniczo ten kod zawija atom, który nie jest globalny i zapewnia set i dostęp do niego. Możesz odczytać jego aktualny stan i zmienić go jak atomy z sth. jak (alter-userconfig! assoc :k2 3). W przypadku testów globalnych możesz zresetować! userconfig, a także wstrzyknąć różne userconfig do aplikacji (alter-userconfig! (constantly {:k1 300, :k2 212})).

Funkcje wymagające userconfig można pisać jak (defn do-sth [CFG arg1 arg2 arg3] ...) I być testowane z różnymi konfiguracjami jak domyślnie-userconfig,testconfig1,2, 3... Funkcje, które manipulują userconfig jak w panelu użytkownika będą używać get / alter..! funkcje.

Również powyższe niech zawija zegarek na userconfig, który automatycznie aktualizuje .plik edn przy każdej zmianie userconfig. Jeśli nie chcesz tego robić, możesz dodać save-userconfig! funkcja, która wypluwa zawartość atomów do config.edn. Możesz jednak stworzyć sposób na dodanie większej liczby zegarków do atomu (np. ponowne renderowanie GUI po zmianie niestandardowego rozmiaru czcionki) co moim zdaniem złamałoby formę powyższego wzoru.

Zamiast tego, jeśli masz do czynienia z większą aplikacją, lepszym podejściem byłoby zdefiniowanie protokołu (z podobnymi funkcjami jak w bloku let) dla userconfig i zaimplementowanie go z różnymi konstruktorami dla pliku, bazy danych, atomu (lub czegokolwiek potrzebnego do testowania/różnych scenariuszy użycia) przy użyciu reify lub defrecord. Przykład tego może być przekazywany w aplikacja i każda funkcja manipulująca stanem / io powinna używać jej zamiast czegokolwiek globalnego.

 11
Author: Leon Grapenthin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-27 13:11:15

Nie zawracałbym sobie głowy trzymaniem map konfiguracji jako zasobów w osobnym pliku (dla każdego środowiska). Confijulate ( https://github.com/bbbates/confijulate , tak - jest to projekt osobisty) pozwala zdefiniować całą konfigurację dla każdego środowiska w ramach jednej przestrzeni nazw i przełączać się między nimi za pomocą właściwości systemu. Ale jeśli musisz zmieniać wartości w locie bez przebudowy, Confijulate również ci na to pozwoli.

 1
Author: brendanb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-01-01 23:45:13

Przez ostatni miesiąc robiłam to w pracy. W przypadkach, gdy przekazywanie konfiguracji jest niedopuszczalne, użyliśmy globalnej mapy konfiguracji w atomie. Na początku uruchamiania aplikacji, var konfiguracyjny jest swap!ed z załadowanym config i po tym pozostaje sam. Działa to w praktyce, ponieważ jest skutecznie niezmienny przez cały okres użytkowania aplikacji. Takie podejście może jednak nie działać dobrze w przypadku bibliotek.

Nie jestem pewien, co masz na myśli mówiąc " nie jasny sposób struktura sekcji config dla używanych bibliotek / frameworków". Czy chcesz, aby biblioteki miały dostęp do konfiguracji? Niezależnie od tego, stworzyłem rurociąg loaderów config, który jest przekazywany funkcji, która ustawia konfigurację przy starcie. To pozwala mi na oddzielenie config na podstawie biblioteki i źródła.

 1
Author: Jeremy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-10-25 00:22:34