diff --git a/project.clj b/project.clj index c86d62e82ec1f63d633b751db5adac0cfbfc197d..edcfda7174218098ead35195ed7b933301a722c3 100644 --- a/project.clj +++ b/project.clj @@ -10,8 +10,7 @@ "test" ["with-profile" "+expectations" "expectations"] "generate-sample-dataset" ["with-profile" "+generate-sample-dataset" "run"] "profile" ["with-profile" "+profile" "run" "profile"] - "h2" ["with-profile" "+h2-shell" "run" "-url" "jdbc:h2:./metabase.db" "-user" "" "-password" "" "-driver" "org.h2.Driver"] - "validate-automagic-dashboards" ["with-profile" "+validate-automagic-dashboards" "run"]} + "h2" ["with-profile" "+h2-shell" "run" "-url" "jdbc:h2:./metabase.db" "-user" "" "-password" "" "-driver" "org.h2.Driver"]} :dependencies [[org.clojure/clojure "1.8.0"] [org.clojure/core.async "0.3.442"] [org.clojure/core.match "0.3.0-alpha4"] ; optimized pattern matching library for Clojure @@ -195,5 +194,4 @@ :profile {:jvm-opts ["-XX:+CITime" ; print time spent in JIT compiler "-XX:+PrintGC"]} ; print a message when garbage collection takes place ;; get the H2 shell with 'lein h2' - :h2-shell {:main org.h2.tools.Shell} - :validate-automagic-dashboards {:main metabase.automagic-dashboards.rules}}) + :h2-shell {:main org.h2.tools.Shell}}) diff --git a/src/metabase/api/automagic_dashboards.clj b/src/metabase/api/automagic_dashboards.clj index acc12f9786b85fb87c17b898082285177bb55520..d46dce5a12d75d3ca3642d22d89ba9bdd59e5f66 100644 --- a/src/metabase/api/automagic_dashboards.clj +++ b/src/metabase/api/automagic_dashboards.clj @@ -30,20 +30,20 @@ (def ^:private Prefix (su/with-api-error-message - (->> ["table" "metric" "field"] - (mapcat rules/load-rules) - (filter :indepth) - (map :rule) - (apply s/enum)) + (s/pred (fn [prefix] + (some #(not-empty (rules/get-rules [% prefix])) ["table" "metric" "field"]))) (tru "invalid value for prefix"))) (def ^:private Rule (su/with-api-error-message - (->> ["table" "metric" "field"] - (mapcat rules/load-rules) - (mapcat :indepth) - (map :rule) - (apply s/enum)) + (s/pred (fn [rule] + (some (fn [toplevel] + (some (comp rules/get-rule + (fn [prefix] + [toplevel prefix rule]) + :rule) + (rules/get-rules [toplevel]))) + ["table" "metric" "field"]))) (tru "invalid value for rule name"))) (def ^:private ^{:arglists '([s])} decode-base64-json @@ -51,13 +51,9 @@ (def ^:private Base64EncodedJSON (su/with-api-error-message - (s/pred decode-base64-json "valid base64 encoded json") + (s/pred decode-base64-json) (tru "value couldn''t be parsed as base64 encoded JSON"))) -(defn- load-rule - [entity prefix rule] - (rules/load-rule (format "%s/%s/%s.yaml" entity prefix rule))) - (api/defendpoint GET "/database/:id/candidates" "Return a list of candidates for automagic dashboards orderd by interestingness." [id] @@ -84,7 +80,7 @@ Table api/check-404 (magic/automagic-analysis - {:rule (load-rule "table" prefix rule) + {:rule ["table" prefix rule] :show (keyword show)}))) (api/defendpoint GET "/segment/:id" @@ -103,7 +99,7 @@ Segment api/check-404 (magic/automagic-analysis - {:rule (load-rule "table" prefix rule) + {:rule ["table" prefix rule] :show (keyword show)}))) (api/defendpoint GET "/question/:id/cell/:cell-query" @@ -130,7 +126,7 @@ Card api/check-404 (magic/automagic-analysis {:show (keyword show) - :rule (load-rule "table" prefix rule) + :rule ["table" prefix rule] :cell-query (decode-base64-json cell-query)}))) (api/defendpoint GET "/metric/:id" @@ -158,7 +154,7 @@ prefix Prefix rule Rule} (-> id Card api/check-404 (magic/automagic-analysis {:show (keyword show) - :rule (load-rule "table" prefix rule)}))) + :rule ["table" prefix rule]}))) (api/defendpoint GET "/adhoc/:query" "Return an automagic dashboard analyzing ad hoc query." @@ -181,7 +177,7 @@ decode-base64-json query/adhoc-query (magic/automagic-analysis {:show (keyword show) - :rule (load-rule "table" prefix rule)}))) + :rule ["table" prefix rule]}))) (api/defendpoint GET "/adhoc/:query/cell/:cell-query" "Return an automagic dashboard analyzing ad hoc query." @@ -211,7 +207,7 @@ query/adhoc-query (magic/automagic-analysis {:show (keyword show) :cell-query cell-query - :rule (load-rule "table" prefix rule)})))) + :rule ["table" prefix rule]})))) (def ^:private valid-comparison-pair? #{["segment" "segment"] diff --git a/src/metabase/automagic_dashboards/core.clj b/src/metabase/automagic_dashboards/core.clj index 4f3ec4670ae0b9aa6fa3607d7cd47e09931dc8cd..861cc111e8157d2fa983c9b3fe7e2b9f8b649da6 100644 --- a/src/metabase/automagic_dashboards/core.clj +++ b/src/metabase/automagic_dashboards/core.clj @@ -568,7 +568,7 @@ [entity] (let [root (->root entity) rule (->> root - (matching-rules (rules/load-rules (:rules-prefix root))) + (matching-rules (rules/get-rules [(:rules-prefix root)])) first) dashboard (make-dashboard root rule)] {:url (:url root) @@ -589,8 +589,7 @@ (defn- indepth [root rule] - (->> rule - :indepth + (->> (rules/get-rules [(:rules-prefix root) (:rule rule)]) (keep (fn [indepth] (when-let [[dashboard _] (apply-rule root indepth)] {:title ((some-fn :short-title :title) dashboard) @@ -609,9 +608,9 @@ "Create dashboards for table `root` using the best matching heuristics." [{:keys [rule show rules-prefix query-filter cell-query full-name] :as root}] (when-let [[dashboard rule] (if rule - (apply-rule root rule) + (apply-rule root (rules/get-rule rule)) (->> root - (matching-rules (rules/load-rules rules-prefix)) + (matching-rules (rules/get-rules [rules-prefix])) (keep (partial apply-rule root)) ;; `matching-rules` returns an `ArraySeq` (via `sort-by`) so ;; `first` realises one element at a time (no chunking). @@ -765,7 +764,7 @@ interestingness of tables they contain (see above)." ([database] (candidate-tables database nil)) ([database schema] - (let [rules (rules/load-rules "table")] + (let [rules (rules/get-rules ["table"])] (->> (apply db/select Table (cond-> [:db_id (u/get-id database) :visibility_type nil] diff --git a/src/metabase/automagic_dashboards/rules.clj b/src/metabase/automagic_dashboards/rules.clj index fb94ba1fe1093f2c0952be39386440a53325ab2a..21870cec9214866dca880ace49e77c472c013736 100644 --- a/src/metabase/automagic_dashboards/rules.clj +++ b/src/metabase/automagic_dashboards/rules.clj @@ -276,79 +276,73 @@ (def ^:private rules-dir "automagic_dashboards/") -(def ^:private ^{:arglists '([f])} file->table-type +(def ^:private ^{:arglists '([f])} file->entity-type (comp (partial re-find #".+(?=\.yaml)") str (memfn ^Path getFileName))) -(def ^:private ^{:arglists '([f])} file->parent-dir - (comp last #(str/split % #"/") str (memfn ^Path getParent))) - -(defmacro ^:private with-resources - [identifier & body] - `(let [uri# (-> rules-dir io/resource .toURI)] - (let [[fs# path#] (-> uri# .toString (str/split #"!" 2))] - (if path# - (with-open [^FileSystem ~identifier - (-> fs# - java.net.URI/create - (FileSystems/newFileSystem (java.util.HashMap.)))] - ~@body) - (let [~identifier (FileSystems/getDefault)] - ~@body))))) - -(defn- resource-path - [^FileSystem fs path] - (when-let [path (some->> path (str rules-dir) io/resource)] - (let [path (if (-> path str (str/starts-with? "jar")) - (-> path str (str/split #"!" 2) second) - (.getPath path))] - (.getPath fs path (into-array String []))))) - -(declare load-rules) - -(defn load-rule - "Load and validate rule from file `f`." - ([f] - (with-resources fs - (some->> f (resource-path fs) (load-rule fs)))) - ([fs ^Path f] - (try - (-> f - .toUri - slurp - yaml/parse-string - (assoc :rule (file->table-type f)) - (update :applies_to #(or % (file->table-type f))) - rules-validator - (assoc :indepth (load-rules fs (format "%s/%s" - (file->parent-dir f) - (file->table-type f))))) - (catch Exception e - (log/error (format "Error parsing %s:\n%s" - (.getFileName f) - (or (some-> e - ex-data - (select-keys [:error :value]) - u/pprint-to-str) - e))) - nil)))) - -(defn load-rules - "Load and validate all rules in dir." - ([dir] - (with-resources fs - (load-rules fs dir))) - ([fs dir] - (when-let [dir (resource-path fs dir)] - (with-open [ds (Files/newDirectoryStream dir)] - (->> ds - (filter #(str/ends-with? (.toString ^Path %) ".yaml")) - (keep (partial load-rule fs)) - doall))))) - -(defn -main - "Entry point for lein task `validate-automagic-dashboards`" - [& _] - (dorun (load-rules "tables")) - (dorun (load-rules "metrics")) - (dorun (load-rules "fields")) - (System/exit 0)) +(defn- load-rule + [^Path f] + (try + (let [entity-type (file->entity-type f)] + (-> f + .toUri + slurp + yaml/parse-string + (assoc :rule entity-type) + (update :applies_to #(or % entity-type)) + rules-validator)) + (catch Exception e + (log/error (format "Error parsing %s:\n%s" + (.getFileName f) + (or (some-> e + ex-data + (select-keys [:error :value]) + u/pprint-to-str) + e))) + nil))) + +(defn- load-rule-dir + ([dir] (load-rule-dir dir [] {})) + ([dir path rules] + (with-open [ds (Files/newDirectoryStream dir)] + (reduce (fn [rules ^Path f] + (cond + (Files/isDirectory f (into-array java.nio.file.LinkOption [])) + (load-rule-dir f (conj path (str (.getFileName f))) rules) + + (file->entity-type f) + (assoc-in rules (concat path [(file->entity-type f) ::leaf]) (load-rule f)) + + :else + rules)) + rules + ds)))) + +(defmacro ^:private with-resource + [[identifier path] & body] + `(let [[jar# path#] (-> ~path .toString (str/split #"!" 2))] + (if path# + (with-open [^FileSystem fs# (-> jar# + java.net.URI/create + (FileSystems/newFileSystem (java.util.HashMap.)))] + (let [~identifier (.getPath fs# path# (into-array String []))] + ~@body)) + (let [~identifier (.getPath (FileSystems/getDefault) (.getPath ~path) (into-array String []))] + ~@body)))) + +(def ^:private rules (delay + (with-resource [path (-> rules-dir io/resource .toURI)] + (into {} (load-rule-dir path))))) + +(defn get-rules + "Get all rules with prefix `prefix`. + prefix is greedy, so [\"table\"] will match table/TransactionTable.yaml, but not + table/TransactionTable/ByCountry.yaml" + [prefix] + (->> prefix + (get-in @rules) + (keep (comp ::leaf val)))) + +(defn get-rule + "Get rule at path `path`." + [path] + (get-in @rules (concat path [::leaf]))) diff --git a/test/metabase/automagic_dashboards/core_test.clj b/test/metabase/automagic_dashboards/core_test.clj index 958899fbb09cbc90dc111658fd611695e8e68d0a..b19692f38655c4286ac0758029dc72f2a1bb09f4 100644 --- a/test/metabase/automagic_dashboards/core_test.clj +++ b/test/metabase/automagic_dashboards/core_test.clj @@ -50,7 +50,7 @@ (->> (data/id :users) Table (#'magic/->root) - (#'magic/matching-rules (rules/load-rules "table")) + (#'magic/matching-rules (rules/get-rules ["table"])) (map (comp first :applies_to)))) ;; Test fallback to GenericTable diff --git a/test/metabase/automagic_dashboards/rules_test.clj b/test/metabase/automagic_dashboards/rules_test.clj index 67dffe23eca2f9dd1fe08aa46ff747719797e964..962e9428256c35bc4c9bd2df500253dec7eab0fd 100644 --- a/test/metabase/automagic_dashboards/rules_test.clj +++ b/test/metabase/automagic_dashboards/rules_test.clj @@ -14,9 +14,12 @@ (expect "ga:foo" (#'rules/->type "ga:foo")) (expect :type/Foo (#'rules/->type "Foo")) -(expect (every? some? (load-rules "table"))) -(expect (every? some? (load-rules "metrics"))) -(expect (every? some? (load-rules "fields"))) +;; This also tests that all the rules are valid (else there would be nils returned) +(expect (every? some? (get-rules ["table"]))) +(expect (every? some? (get-rules ["metrics"]))) +(expect (every? some? (get-rules ["fields"]))) + +(expect (some? (get-rules ["table" "GenericTable" "ByCountry"]))) (expect true (dimension-form? [:dimension "Foo"])) (expect true (dimension-form? ["dimension" "Foo"]))