diff --git a/src/metabase/models/setting/multi_setting.clj b/src/metabase/models/setting/multi_setting.clj new file mode 100644 index 0000000000000000000000000000000000000000..a6997475bf8a69da2aa394a60783b454ab645633 --- /dev/null +++ b/src/metabase/models/setting/multi_setting.clj @@ -0,0 +1,82 @@ +(ns metabase.models.setting.multi-setting + "Helper macros for defining Settings that can have multiple getter/setter implementations. The implementation that + gets used is determined at runtime when the getter or setter is invoked by a dispatch function. + + This functionality was originally intended to facilitate separate EE and OSS versions of Settings, but rather than + restrict the impls to just `:oss` and `:ee`, these macros allow an arbitrary dispatch function and any number of + implementations. + + See PR #16365 for more context." + (:require [metabase.models.setting :as setting] + [metabase.util.i18n :refer [tru]])) + +(defmulti dispatch-multi-setting + "Determine the dispatch value for a multi-Setting defined by `define-multi-setting`." + {:arglists '([setting-key])} + keyword) + +(defmulti get-multi-setting + "Get the value of a multi-Setting defined by `define-multi-setting` for the `impl` obtained by + calling `(dispatch-multi-setting setting-key)`." + {:arglists '([setting-key impl])} + (fn [setting-key impl] + [(keyword setting-key) (keyword impl)])) + +(defmulti set-multi-setting + "Update the value of a multi-Setting defined by `define-multi-setting` for the `impl` obtained by + calling `(dispatch-multi-setting setting-key)`." + {:arglists '([setting-key impl new-value])} + (fn [setting-key impl _] + [(keyword setting-key) (keyword impl)])) + +(defmacro define-multi-setting + "Define a Setting that can have multiple getter/setter implementations. The implementation used is determined by + calling `dispatch-thunk` when the Setting getter or setter is invoked. And `:getter` or `:setter` defined here will + be used for all impls; you can use this to make a multi-Setting read-only, for example by specifying `:setter` none + here. + + defsetting : define-multi-setting :: defn : defmulti" + {:style/indent :defn} + [setting-symbol doc dispatch-thunk & {:as options}] + (let [setting-key (keyword setting-symbol) + options (merge {:getter `(fn [] + (get-multi-setting ~setting-key (dispatch-multi-setting ~setting-key))) + :setter `(fn [new-value#] + (set-multi-setting ~setting-key (dispatch-multi-setting ~setting-key) new-value#))} + options)] + `(do + (let [dispatch-thunk# ~dispatch-thunk] + (defmethod dispatch-multi-setting ~setting-key + [~'_] + (dispatch-thunk#))) + (setting/defsetting ~setting-symbol + ~doc + ~@(mapcat identity options))))) + +(defmacro define-multi-setting-impl + "Define a implementation for a Setting defined by `define-multi-setting`. Accepts options `:getter` (a function that + takes no args) and/or `:setter` (a function that takes a single arg, or the keyword `:none`), the same as + `defsetting`. Note that any of these options defined by `define-multi-setting` will be used for all impls and + ignored here. + + define-multi-setting : define-multi-setting-impl :: defmulti : defmethod + + See `define-multi-setting` for more details." + [setting-symbol dispatch-value & {:keys [getter setter]}] + (let [setting-key (keyword setting-symbol) + dispatch-value (keyword dispatch-value)] + `(do + ~(when getter + `(let [getter# ~getter] + (defmethod get-multi-setting [~setting-key ~dispatch-value] + [~'_ ~'_] + (getter#)))) + ~(when setter + (if (= setter :none) + `(defmethod set-multi-setting [~setting-key ~dispatch-value] + [~'_ ~'_ ~'_] + (throw (UnsupportedOperationException. (tru "You cannot set {0}; it is a read-only setting." ~setting-key)))) + `(let [setter# ~setter] + (defmethod set-multi-setting [~setting-key ~dispatch-value] + [~'_ ~'_ new-value#] + (setter# new-value#)))))))) diff --git a/test/metabase/models/setting/multi_setting_test.clj b/test/metabase/models/setting/multi_setting_test.clj new file mode 100644 index 0000000000000000000000000000000000000000..650dce270616740ac9ecdccf7ff1ab09bc1497f5 --- /dev/null +++ b/test/metabase/models/setting/multi_setting_test.clj @@ -0,0 +1,64 @@ +(ns metabase.models.setting.multi-setting-test + (:require [clojure.test :refer :all] + [metabase.models.setting :as setting] + [metabase.models.setting.multi-setting :as multi-setting] + [metabase.test.fixtures :as fixtures])) + +(use-fixtures :once (fixtures/initialize :db)) + +(def ^:dynamic ^:private *parakeet* :green-friend) + +(multi-setting/define-multi-setting ^:private multi-setting-test-bird-name + "A test Setting." + (fn [] *parakeet*) + :visibility :internal) + +(multi-setting/define-multi-setting-impl multi-setting-test-bird-name :green-friend + :getter (constantly "Green Friend") + :setter :none) + +(multi-setting/define-multi-setting-impl multi-setting-test-bird-name :yellow-friend + :getter (partial setting/get-string :multi-setting-test-bird-name) + :setter (partial setting/set-string! :multi-setting-test-bird-name)) + +(deftest multi-setting-test + (testing :green-friend + (is (= "Green Friend" + (multi-setting-test-bird-name))) + (is (thrown-with-msg? + UnsupportedOperationException + #"You cannot set :multi-setting-test-bird-name; it is a read-only setting" + (multi-setting-test-bird-name "Parroty")))) + (testing :yellow-friend + (binding [*parakeet* :yellow-friend] + (is (= "Yellow Friend" + (multi-setting-test-bird-name "Yellow Friend"))) + (is (= "Yellow Friend" + (multi-setting-test-bird-name)))))) + +(multi-setting/define-multi-setting ^:private multi-setting-read-only + "A test setting that is always read-only." + (fn [] *parakeet*) + :visibility :internal + :getter (constantly "Parroty") + :setter :none) + +(multi-setting/define-multi-setting-impl multi-setting-read-only :green-friend + :getter (constantly "Green Friend") + :setter (partial setting/set-string! :multi-setting-read-only)) + +(multi-setting/define-multi-setting-impl multi-setting-read-only :yellow-friend + :getter (constantly "Yellow Friend") + :setter (partial setting/set-string! :multi-setting-read-only)) + +(deftest keys-in-definition-should-overshadow-keys-in-impls + (testing "Specifying :getter or :setter in `define-multi-setting` should mean ones in any `impl` are ignored" + (doseq [parakeet [:green-friend :yellow-friend :parroty]] + (testing parakeet + (binding [*parakeet* parakeet] + (is (= "Parroty" + (multi-setting-read-only))) + (is (thrown-with-msg? + UnsupportedOperationException + #"You cannot set multi-setting-read-only; it is a read-only setting" + (multi-setting-read-only "Parroty"))))))))