Skip to content

Instantly share code, notes, and snippets.

@camsaul
Last active April 20, 2022 20:12
Show Gist options
  • Save camsaul/fa4f29bdc2a6aa6f92b2192d398639be to your computer and use it in GitHub Desktop.
Save camsaul/fa4f29bdc2a6aa6f92b2192d398639be to your computer and use it in GitHub Desktop.
defenterprise macro PoC
(ns metabase.public-settings.premium-features.defenterprise
(:require [clojure.string :as str]
[metabase.plugins.classloader :as classloader]
[metabase.public-settings.premium-features :as premium-features]
[metabase.util :as u]))
;;; Map of (qualified) function name -> {:oss <oss/fallback fn>
;;; :ee <ee fn>}
(defonce ^:private registry
(atom {}))
(defn ee-namespace? []
(str/starts-with? (ns-name *ns*) "metabase-enterprise"))
(defn register-fn! [fn-name ee-or-oss f]
(swap! registry update fn-name assoc ee-or-oss f))
(defn dynamic-ee-oss-fn
"Picks the appropriate function `fn-name` defined with [[defenterprise]] when invoked based on whether `feature` is
available."
[fn-name feature]
(fn [& args]
(let [f (or (when (premium-features/has-feature? feature)
(let [ee-namespace (symbol (namespace fn-name))]
(u/ignore-exceptions
(classloader/require ee-namespace)
(get-in @registry [fn-name :ee]))))
(get-in @registry [fn-name :oss]))]
(apply f args))))
(defmacro defenterprise
{:style/indent :defn}
[fn-name feature & fn-tail]
{:pre [(symbol? fn-name) (namespace fn-name) (keyword? feature)]}
(let [ee-or-oss (if (ee-namespace?) :ee :oss)]
`(do
(register-fn! '~fn-name ~ee-or-oss (fn ~@fn-tail))
(def ~(vary-meta (symbol (name fn-name)) assoc :arglists ''([& args])) (dynamic-ee-oss-fn '~fn-name ~feature)))))
;;; Sample usage
(defenterprise metabase-enterprise.core/my-fn :audit-app
[x]
[:oss x])
;; Supporting schema
(defmacro defenterprise-fn
[fn-name feature f]
{:pre [(symbol? fn-name) (namespace fn-name) (keyword? feature)]}
(let [ee-or-oss (if (ee-namespace?) :ee :oss)]
`(do
(register-fn! '~fn-name ~ee-or-oss ~f)
(def ~(vary-meta (symbol (name fn-name)) assoc :arglists ''([& args])) (dynamic-ee-oss-fn '~fn-name ~feature)))))
(defmacro defenterprise
{:style/indent :defn}
[fn-name feature & fn-tail]
`(defenterprise-fn ~fn-name ~feature (fn ~@fn-tail)))
(s/def ::defenterprise-schema-args
(s/cat :return-schema (s/? (s/cat :- (partial = :-)
:schema any?))
:feature (every-pred keyword? (partial not= :-))
:fn-tail (s/* any?)))
(defmacro defenterprise-schema
[fn-name & args]
{:style/indent :defn}
(let [parsed (s/conform ::defenterprise-schema-args args)]
(when (s/invalid? parsed)
(throw (ex-info (s/explain-str ::defenterprise-schema-args args)
(s/explain-data ::defenterprise-schema-args args))))
(let [{:keys [feature fn-tail], {:keys [schema]} :return-schema} parsed]
`(defenterprise-fn ~fn-name ~feature (schema/fn ~@(when schema [:- schema])
~@fn-tail)))))
;;; Sample usage
(defenterprise-schema metabase-enterprise.core/my-fn :- schema/Int :audit-app
[x]
[:oss x])
;; =>
(defenterprise-fn metabase-enterprise.core/my-fn :audit-app (schema/fn :- schema/Int [x] [:oss x]))
@camsaul
Copy link
Author

camsaul commented Apr 20, 2022

Not implemented:

  • Docstring or metadata map support
  • Support for only having an EE impl and conditionally calling it only if we have the appropriate features
  • Support for :any or :none as the feature
  • This doesn't properly propogates metadata on the fn-name symbol like :^private. e.g. defenterprise ^:private metabase-enterprise.core/my-fn defines my-fn but doesn't make it ^:private as well. Should be an easy fix tho

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment