Идиоматическое управление конфигурацией в clojure?

Что такое идиоматический способ обработки конфигурации приложения в clojure?

Пока я использую эту среду:

;; 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)]
    ...))

У меня много недостатков:

  • Путь к config.clj может не всегда корректно разрешаться
  • Нет четкого способа структурирования разделов конфигурации для используемых библиотек/фреймворков
  • Не доступен по всему миру (@app/config) (что, конечно же, можно рассматривать как удобный способ функционального стиля, но делает доступ к конфигурации через исходный файл утомительным.

Большие проекты с открытым исходным кодом, такие как шторм, похоже, используют YAML вместо Clojure и делают доступную конфигурацию доступной по всему миру с помощью немного уродливого взлома: (eval ``(def ~(symbol new-name) (. Config ~(symbol name)))).

Ответы

Ответ 1

Сначала используйте clojure.edn и, в частности, clojure.edn/read. E. g.

(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)))

Что касается пути config.edn, использующего io/resource, это единственный способ справиться с этим. Поскольку вы, вероятно, захотите сохранить измененный config.edn во время выполнения, вы можете рассчитывать на то, что путь для чтения файлов и писателей, построенных с использованием неквалифицированного имени файла, например

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

по умолчанию

(System/getProperty "user.dir")

Учитывая тот факт, что вы можете изменить конфигурацию во время выполнения, вы можете реализовать шаблон, подобный этому (приблизительный эскиз)

;; 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)))

В основном этот код обертывает атом, который не является глобальным, и предоставляет набор и получает к нему доступ. Вы можете прочитать его текущее состояние и изменить его как атомы с помощью sth. как (alter-userconfig! assoc :k2 3). Для глобального тестирования вы можете reset! userconfig, а также ввести различные пользовательские настройки в ваше приложение (alter-userconfig! (constantly {:k1 300, :k2 212})).

Функции, которые требуют userconfig, могут быть написаны как   (defn do-sth [cfg arg1 arg2 arg3]     ...) И быть протестированным с помощью различных конфигураций, таких как default-userconfig, testconfig1,2,3... Функции, которые управляют userconfig, как в панели пользователя, будут использовать get/alter..! функции.

Также приведенное выше позволяет обернуть часы на userconfig, который автоматически обновляет файл .edn каждый раз при изменении userconfig. Если вы не хотите этого делать, вы можете добавить save-userconfig! которая объединяет содержимое атомов в config.edn. Тем не менее, вы можете создать способ добавить больше часов к атому (например, повторное рендеринг графического интерфейса после изменения пользовательского размера шрифта), который, на мой взгляд, сломал бы форму вышеуказанного шаблона.

Вместо этого, если бы вы имели дело с большим приложением, лучшим подходом было бы определение протокола (с аналогичными функциями, например, в блоке let) для userconfig, и его реализация с различными конструкторами для файла, базы данных, атома ( или что вам нужно для тестирования/разных сценариев использования), используя reify или defrecord. Экземпляр этого может быть передан в приложении, и каждая функция управления состоянием /io должна использовать его вместо глобального.

Ответ 2

Я потратил немного времени на этот месяц за работу. Для случаев, когда передача конфигурации вокруг неприемлема, мы использовали глобальную конфигурационную карту в атоме. В начале запуска приложения config var swap! ed с загруженной конфигурацией, и после этого он остается один. Это работает на практике, потому что это эффективно неизменно для жизни приложения. Однако этот подход может плохо работать для библиотек.

Я не уверен, что вы подразумеваете под "Нет четкого способа структурирования разделов конфигурации для используемых библиотек/фреймворков". Вы хотите, чтобы библиотеки имели доступ к конфигу? Несмотря на это, я создал конвейер конфигурационных загрузчиков, который предоставляется функции, которая настраивает конфигурацию при запуске. Это позволяет мне разделить конфигурацию на основе библиотеки и источника.

Ответ 3

Я бы не стал даже держать карты конфигурации в качестве ресурсов в отдельном файле (для каждой среды). Confijulate (https://github.com/bbbates/confijulate, да - это персональный проект) позволяет определить всю вашу конфигурацию для каждой среды в пределах одного пространства имен и переключиться между их через свойства системы. Но если вам нужно изменить значения "на лету" без перестройки, Confijulate также позволит вам это сделать.