From 7700751a32b53cba99d6cac3fa605c5ee9963790 Mon Sep 17 00:00:00 2001 From: Bryan Maass <bryan.maass@gmail.com> Date: Thu, 29 Dec 2022 12:30:22 -0700 Subject: [PATCH] adds mu/defn, which behaves like s/defn but for malli schemas (#27303) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * adds mu/defn, which behaves like s/defn but for malli schemas - includes link to malli.io with your schema and type.🔗 - making room for some namespaces * require and use malli.destructure in mu/defn * fix require alias * clean ns * fix linter errror * fix linter error again --- .clj-kondo/config.edn | 7 ++ .../mongo/test/metabase/driver/mongo_test.clj | 4 +- .../mongo/test/metabase/test/data/mongo.clj | 4 +- src/metabase/util/malli.clj | 74 +++++++++++++++++++ test/metabase/util/malli_test.clj | 25 +++++++ 5 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 src/metabase/util/malli.clj create mode 100644 test/metabase/util/malli_test.clj diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 3fbc23c70f2..b93eedf16d9 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -119,6 +119,10 @@ honeysql.helpers hh honeysql.types htypes java-time t + malli.core mc + malli.experimental mx + malli.transform mtx + medley.core m metabase-enterprise.audit-app.pages.common common metabase-enterprise.sandbox.api.table table metabase.analytics.stats stats @@ -333,6 +337,7 @@ metabase.util.files u.files metabase.util.i18n i18n metabase.util.i18n.impl i18n.impl + metabase.util.malli mu metabase.util.password u.password metabase.util.schema su metabase.util.ui-logic ui-logic @@ -340,6 +345,7 @@ metabuild-common.core u metabuild-common.output out metabuild-common.shell shell + monger.collection mcoll ring.mock.request ring.mock ring.util.codec codec ring.util.response response @@ -397,6 +403,7 @@ metabase.test/with-temp-file clojure.core/let metabase.test/with-user-in-groups clojure.core/let metabase.util.files/with-open-path-to-resource clojure.core/let + metabase.util.malli/defn schema.core/defn metabase.util.ssh/with-ssh-tunnel clojure.core/let monger.operators/defoperator clojure.core/def potemkin.types/defprotocol+ clojure.core/defprotocol diff --git a/modules/drivers/mongo/test/metabase/driver/mongo_test.clj b/modules/drivers/mongo/test/metabase/driver/mongo_test.clj index a742f096404..38362514ba2 100644 --- a/modules/drivers/mongo/test/metabase/driver/mongo_test.clj +++ b/modules/drivers/mongo/test/metabase/driver/mongo_test.clj @@ -19,7 +19,7 @@ [metabase.test :as mt] [metabase.test.data.interface :as tx] [metabase.test.data.mongo :as tdm] - [monger.collection :as mc] + [monger.collection :as mcoll] [taoensso.nippy :as nippy] [toucan.db :as db]) (:import org.bson.types.ObjectId)) @@ -441,7 +441,7 @@ (doseq [[i row] (map-indexed vector row-maps) :let [row (assoc row :_id (inc i))]] (try - (mc/insert conn collection-name row) + (mcoll/insert conn collection-name row) (catch Throwable e (throw (ex-info (format "Error inserting row: %s" (ex-message e)) {:database database-name, :collection collection-name, :details details, :row row} diff --git a/modules/drivers/mongo/test/metabase/test/data/mongo.clj b/modules/drivers/mongo/test/metabase/test/data/mongo.clj index 13e37f0e7d2..3af27ceae82 100644 --- a/modules/drivers/mongo/test/metabase/test/data/mongo.clj +++ b/modules/drivers/mongo/test/metabase/test/data/mongo.clj @@ -6,7 +6,7 @@ [metabase.driver.ddl.interface :as ddl.i] [metabase.driver.mongo.util :refer [with-mongo-connection]] [metabase.test.data.interface :as tx] - [monger.collection :as mc] + [monger.collection :as mcoll] [monger.core :as mg]) (:import com.fasterxml.jackson.core.JsonGenerator)) @@ -62,7 +62,7 @@ (doseq [[i row] (map-indexed vector rows)] (try ;; Insert each row - (mc/insert mongo-db (name table-name) (into {:_id (inc i)} + (mcoll/insert mongo-db (name table-name) (into {:_id (inc i)} (zipmap field-names row))) ;; If row already exists then nothing to do (catch com.mongodb.MongoException _))))))) diff --git a/src/metabase/util/malli.clj b/src/metabase/util/malli.clj new file mode 100644 index 00000000000..033f5e82cb0 --- /dev/null +++ b/src/metabase/util/malli.clj @@ -0,0 +1,74 @@ +(ns metabase.util.malli + (:refer-clojure :exclude [defn]) + (:require + [clojure.core :as core] + [malli.core :as mc] + [malli.destructure] + [malli.error :as me] + [malli.experimental :as mx] + [malli.generator :as mg] + [malli.instrument :as minst] + [metabase.util :as u] + [ring.util.codec :as codec])) + +(core/defn- ->malli-io-link + ([schema] + (->malli-io-link schema (try (mg/generate schema {:seed 1 :size 1}) + ;; not all schemas can generate values + (catch Exception _ ::none)))) + ([schema value] + (let [url-schema (codec/url-encode (u/pprint-to-str (mc/form schema))) + url-value (if (= ::none value) + "" + (codec/url-encode (u/pprint-to-str value)))] + (str "https://malli.io?schema=" url-schema "&value=" url-value)))) + +(core/defn- explain-fn-fail! + "Used as reporting function to minst/instrument!" + [type data] + (let [{:keys [input args output value]} data] + (throw (ex-info + (str type " " (pr-str data)) + (merge {:type type :data data} + (when data + {:link (cond input (->malli-io-link input args) + output (->malli-io-link output value)) + :humanized + (cond input (me/humanize (mc/explain input args)) + output (me/humanize (mc/explain output value)))})))))) + +;; since a reference to the private var is used in the macro, this will trip the eastwood :unused-private-vars linter, +;; so just harmlessly "use" the var here. +explain-fn-fail! + +(core/defn- -defn [schema args] + (let [{:keys [name return doc meta arities] :as parsed} (mc/parse schema args) + _ (when (= ::mc/invalid parsed) (mc/-fail! ::parse-error {:schema schema, :args args})) + parse (fn [{:keys [args] :as parsed}] (merge (malli.destructure/parse args) parsed)) + ->schema (fn [{:keys [schema]}] [:=> schema (:schema return :any)]) + single (= :single (key arities)) + parglists (if single + (->> arities val parse vector) + (->> arities val :arities (map parse))) + raw-arglists (map :raw-arglist parglists) + schema (as-> (map ->schema parglists) $ (if single (first $) (into [:function] $))) + id (str (gensym "id"))] + `(let [defn# (core/defn + ~name + ~@(some-> doc vector) + ~(assoc meta + :raw-arglists (list 'quote raw-arglists) + :schema schema + :validate! id) + ~@(map (fn [{:keys [arglist prepost body]}] `(~arglist ~prepost ~@body)) parglists) + ~@(when-not single (some->> arities val :meta vector)))] + (mc/=> ~name ~schema) + (minst/instrument! {:filters [(minst/-filter-var #(-> % meta :validate! (= ~id)))] + :report #'explain-fn-fail!}) + defn#))) + +(defmacro defn + "Like s/defn, but for malli. Will always validate input and output without the need for calls to instrumentation (they are emitted automatically). + Calls to minst/unstrument! can remove this, so use a filter that avoids :validate! if you use that." + [& args] + (-defn mx/SchematizedParams args)) diff --git a/test/metabase/util/malli_test.clj b/test/metabase/util/malli_test.clj new file mode 100644 index 00000000000..fe67184e3ad --- /dev/null +++ b/test/metabase/util/malli_test.clj @@ -0,0 +1,25 @@ +(ns metabase.util.malli-test + (:require + [clojure.test :refer [deftest is testing]] + [metabase.util.malli :as mu])) + +(deftest mu-defn-test + (testing "invalid input" + (mu/defn bar [x :- [:map [:x int?] [:y int?]]] (str x)) + (is (= [{:x ["missing required key"] + :y ["missing required key"]}] + (:humanized + (try (bar {}) + (catch Exception e (ex-data e))))) + "when we pass bar an invalid shape um/defn throws") + (ns-unmap *ns* 'bar)) + + (testing "invalid output" + (mu/defn baz :- [:map [:x int?] [:y int?]] [] {:x "3"}) + (is (= {:x ["should be an int"] + :y ["missing required key"]} + (:humanized + (try (baz) + (catch Exception e (ex-data e))))) + "when baz returns an invalid form um/defn throws") + (ns-unmap *ns* 'baz))) -- GitLab