diff --git a/src/metabase/util/malli/describe.clj b/src/metabase/util/malli/describe.clj index 9c9dd9c44b9448b6f03f59ef01528ea96dc3ce8f..99c9e1f4377b2cab3c740207aeed10f91de8695d 100644 --- a/src/metabase/util/malli/describe.clj +++ b/src/metabase/util/malli/describe.clj @@ -29,6 +29,20 @@ max (str " with length >= " max) :else ""))) +(defn- pluralize-times [n] + (when n + (if (= 1 n) "time" "times"))) + +(defn- repeat-suffix [schema] + (let [{:keys [min max]} (-> schema mc/properties) + min-timez (pluralize-times min) + max-timez (pluralize-times max)] + (cond + (and min max) (str " at least " min " " min-timez ", up to " max " " max-timez) + min (str " at least " min " " min-timez) + max (str " at most " max " " max-timez) + :else ""))) + (defn- min-max-suffix-number [schema] (let [{:keys [min max]} (-> schema mc/properties)] (cond @@ -116,36 +130,36 @@ (defmethod accept 'set? [_ schema children _] (str "set" (titled schema) (length-suffix schema) (of-clause children))) (defmethod accept :set [_ schema children _] (str "set" (titled schema) (length-suffix schema) (of-clause children))) -(defmethod accept 'string? [_ schema _ _] (str "string" (length-suffix schema))) -(defmethod accept :string [_ schema _ _] (str "string" (length-suffix schema))) +(defmethod accept 'string? [_ schema _ _] (str "string" (titled schema) (length-suffix schema))) +(defmethod accept :string [_ schema _ _] (str "string" (titled schema) (length-suffix schema))) -(defmethod accept 'number? [_ schema _ _] (str "number" (min-max-suffix schema))) -(defmethod accept :number [_ schema _ _] (str "number" (min-max-suffix schema))) +(defmethod accept 'number? [_ schema _ _] (str "number" (titled schema) (min-max-suffix schema))) +(defmethod accept :number [_ schema _ _] (str "number" (titled schema) (min-max-suffix schema))) -(defmethod accept 'pos-int? [_ schema _ _] (str "integer greater than 0" (min-max-suffix schema))) -(defmethod accept :pos-int [_ schema _ _] (str "integer greater than 0" (min-max-suffix schema))) +(defmethod accept 'pos-int? [_ schema _ _] (str "integer greater than 0" (titled schema) (min-max-suffix schema))) +(defmethod accept :pos-int [_ schema _ _] (str "integer greater than 0" (titled schema) (min-max-suffix schema))) -(defmethod accept 'neg-int? [_ schema _ _] (str "integer less than 0" (min-max-suffix schema))) -(defmethod accept :neg-int [_ schema _ _] (str "integer less than 0" (min-max-suffix schema))) +(defmethod accept 'neg-int? [_ schema _ _] (str "integer less than 0" (titled schema) (min-max-suffix schema))) +(defmethod accept :neg-int [_ schema _ _] (str "integer less than 0" (titled schema) (min-max-suffix schema))) -(defmethod accept 'nat-int? [_ schema _ _] (str "natural integer" (min-max-suffix schema))) -(defmethod accept :nat-int [_ schema _ _] (str "natural integer" (min-max-suffix schema))) +(defmethod accept 'nat-int? [_ schema _ _] (str "natural integer" (titled schema) (min-max-suffix schema))) +(defmethod accept :nat-int [_ schema _ _] (str "natural integer" (titled schema) (min-max-suffix schema))) -(defmethod accept 'float? [_ schema _ _] (str "float" (min-max-suffix schema))) -(defmethod accept :float [_ schema _ _] (str "float" (min-max-suffix schema))) +(defmethod accept 'float? [_ schema _ _] (str "float" (titled schema) (min-max-suffix schema))) +(defmethod accept :float [_ schema _ _] (str "float" (titled schema) (min-max-suffix schema))) -(defmethod accept 'pos? [_ schema _ _] (str "number greater than 0" (min-max-suffix schema))) -(defmethod accept :pos [_ schema _ _] (str "number greater than 0" (min-max-suffix schema))) +(defmethod accept 'pos? [_ schema _ _] (str "number greater than 0" (titled schema) (min-max-suffix schema))) +(defmethod accept :pos [_ schema _ _] (str "number greater than 0" (titled schema) (min-max-suffix schema))) -(defmethod accept 'neg? [_ schema _ _] (str "number less than 0" (min-max-suffix schema))) -(defmethod accept :neg [_ schema _ _] (str "number less than 0" (min-max-suffix schema))) +(defmethod accept 'neg? [_ schema _ _] (str "number less than 0" (titled schema) (min-max-suffix schema))) +(defmethod accept :neg [_ schema _ _] (str "number less than 0" (titled schema) (min-max-suffix schema))) -(defmethod accept 'integer? [_ schema _ _] (str "integer" (min-max-suffix-number schema))) -(defmethod accept 'int? [_ schema _ _] (str "integer" (min-max-suffix-number schema))) -(defmethod accept :int [_ schema _ _] (str "integer" (min-max-suffix-number schema))) +(defmethod accept 'integer? [_ schema _ _] (str "integer" (titled schema) (min-max-suffix-number schema))) +(defmethod accept 'int? [_ schema _ _] (str "integer" (titled schema) (min-max-suffix-number schema))) +(defmethod accept :int [_ schema _ _] (str "integer" (titled schema) (min-max-suffix-number schema))) -(defmethod accept 'double? [_ schema _ _] (str "double" (min-max-suffix-number schema))) -(defmethod accept :double [_ schema _ _] (str "double" (min-max-suffix-number schema))) +(defmethod accept 'double? [_ schema _ _] (str "double" (titled schema) (min-max-suffix-number schema))) +(defmethod accept :double [_ schema _ _] (str "double" (titled schema) (min-max-suffix-number schema))) (defmethod accept :merge [_ schema _ options] ((::describe options) (mc/deref schema) options)) (defmethod accept :union [_ schema _ options] ((::describe options) (mc/deref schema) options)) @@ -192,11 +206,24 @@ (defmethod accept :function [_ _ _children _] "function") (defmethod accept :fn [_ _ _ _] "function") +(defn- tagged [children] + (map (fn [[tag _ c]] (str c " (tag: " tag ")" )) children)) + (defmethod accept :or [_ _ children _] (str/join ", or " children)) -(defmethod accept :orn [_ _ children _] (str/join ", or " (map (fn [[tag _ c]] (str c " (tag: " tag ")" )) children))) +(defmethod accept :orn [_ _ children _] (str/join ", or " (tagged children))) (defmethod accept :cat [_ _ children _] (str/join ", " children)) -(defmethod accept :catn [_ _ children _] (str/join ", or " (map (fn [[tag _ c]] (str c " (tag: " tag ")" )) children))) +(defmethod accept :catn [_ _ children _] (str/join ", and " (tagged children))) + +(defmethod accept :alt [_ _ children _] (str/join ", or " children)) +(defmethod accept :altn [_ _ children _] (str/join ", or " (tagged children))) + +(defmethod accept :+ [_ _ children _] (str "one or more " (str/join ", " children))) +(defmethod accept :* [_ _ children _] (str "zero or more " (str/join ", " children))) +(defmethod accept :? [_ _ children _] (str "zero or one " (str/join ", " children))) + +(defmethod accept :repeat [_ schema children _] + (str "repeat " (diamond (first children)) (repeat-suffix schema))) (defmethod accept 'boolean? [_ _ _ _] "boolean") (defmethod accept :boolean [_ _ _ _] "boolean") @@ -209,10 +236,11 @@ additional-properties (:closed (mc/properties schema)) kv-description (str/join ", " (map (fn [[k _ s]] (str k (when (contains? optional k) " (optional)") " -> " (diamond s))) children))] (str/trim - (cond-> (str "map ") + (cond-> (str "map " (titled schema)) (seq kv-description) (str "where {" kv-description "} ") additional-properties (str "with no other keys "))))) + (defmethod accept ::mc/val [_ _ children _] (first children)) (defmethod accept 'map? [n schema children o] (-map n schema children o)) (defmethod accept :map [n schema children o] (-map n schema children o)) diff --git a/test/metabase/api/common/internal_test.clj b/test/metabase/api/common/internal_test.clj index 5c24c6bf14a6a45438b3106d333a831a5a676aec..f2922186da0829c699e2b0f7058142ff54f266ad 100644 --- a/test/metabase/api/common/internal_test.clj +++ b/test/metabase/api/common/internal_test.clj @@ -95,14 +95,14 @@ :lonlat [0.0 0.0]}})))) (is (= {:errors - {:address "map where {:id -> <string>, :tags -> <vector of string>, :address -> <map where {:street -> <string>, :city -> <string>, :zip -> <integer>, :lonlat -> <vector with exactly 2 items of type: double, double>}>}"}, - :specific-errors {:address {:id ["missing required key"], + {:address "map (titled: ‘Address’) where {:id -> <string>, :tags -> <vector of string>, :address -> <map where {:street -> <string>, :city -> <string>, :zip -> <integer>, :lonlat -> <vector with exactly 2 items of type: double, double>}>}"}, + :specific-errors {:address {:id ["missing required key"], :tags ["missing required key"], :address ["missing required key"]}}} (:body (post! "/post/test-address" {:x "1"})))) (is (= {:errors - {:address "map where {:id -> <string>, :tags -> <vector of string>, :address -> <map where {:street -> <string>, :city -> <string>, :zip -> <integer>, :lonlat -> <vector with exactly 2 items of type: double, double>}>}"}, + {:address "map (titled: ‘Address’) where {:id -> <string>, :tags -> <vector of string>, :address -> <map where {:street -> <string>, :city -> <string>, :zip -> <integer>, :lonlat -> <vector with exactly 2 items of type: double, double>}>}"}, :specific-errors {:address {:id ["should be a string"] :tags ["invalid type"] @@ -116,7 +116,7 @@ :lonlat [0.0 0.0]}})))) (is (= {:errors - {:address "map where {:id -> <string>, :tags -> <vector of string>, :address -> <map where {:street -> <string>, :city -> <string>, :zip -> <integer>, :lonlat -> <vector with exactly 2 items of type: double, double>} with no other keys>} with no other keys"}, + {:address "map (titled: ‘Address’) where {:id -> <string>, :tags -> <vector of string>, :address -> <map where {:street -> <string>, :city -> <string>, :zip -> <integer>, :lonlat -> <vector with exactly 2 items of type: double, double>} with no other keys>} with no other keys"}, :specific-errors {:address {:address ["missing required key"], :a ["disallowed key"], diff --git a/test/metabase/util/malli/describe_test.clj b/test/metabase/util/malli/describe_test.clj index 29468e51eebcb081113f9ba2f11318470e8b7ef9..c15d4db707b506238c59f155954adccb2086ebfc 100644 --- a/test/metabase/util/malli/describe_test.clj +++ b/test/metabase/util/malli/describe_test.clj @@ -1,98 +1,103 @@ (ns metabase.util.malli.describe-test - (:require [clojure.test :refer [deftest is]] + (:require [clojure.test :refer [deftest is testing]] [metabase.util.malli.describe :as umd])) (deftest descriptor-test - - (is (= "vector" (umd/describe vector?))) - - (is (= "vector of integer" (umd/describe [:vector :int]))) - - (is (= "string with length <= 5" (umd/describe [:string {:min 5}]))) - (is (= "string with length >= 5" (umd/describe [:string {:max 5}]))) - (is (= "string with length between 3 and 5 inclusive" (umd/describe [:string {:min 3 :max 5}]))) - - (is (= "map" (umd/describe map?))) - - (is (= "map where {:x -> <integer>}" - (umd/describe [:map [:x int?]]))) - - (is (= "map where {:x (optional) -> <integer>, :y -> <boolean>}" - (umd/describe [:map [:x {:optional true} int?] [:y :boolean]]))) - - (is (= "map where {:x -> <integer>} with no other keys" - (umd/describe [:map {:closed true} [:x int?]]))) - - (is (= "map where {:x (optional) -> <integer>, :y -> <boolean>} with no other keys" - (umd/describe [:map {:closed true} [:x {:optional true} int?] [:y :boolean]]))) - - (is (= "function that takes input: [integer] and returns integer" - (umd/describe [:=> [:cat int?] int?]))) - - (is (= "map where {:j-code -> <keyword, and has length 4>}" - (umd/describe [:map [:j-code [:and - :keyword - [:fn {:description "has length 4"} #(= 4 (count (name %)))]]]]))) - - (is (= (umd/describe [:map-of {:title "dict"} :int :string]) - "map (titled: ‘dict’) from <integer> to <string>")) - - (is (= (umd/describe [:vector [:sequential [:set :int]]]) - "vector of sequence of set of integer")) - - (is (= "one of <:dog = map where {:x -> <integer>} | :cat = anything> dispatched by the type of animal" - (umd/describe [:multi {:dispatch :type - :dispatch-description "the type of animal"} - [:dog [:map [:x :int]]] - [:cat :any]]))) - - (is (= "one of <:dog = map where {:x -> <integer>} | :cat = anything> dispatched by :type" - (umd/describe [:multi {:dispatch :type} - [:dog [:map [:x :int]]] - [:cat :any]]))) - - (is (= "Order which is: <Country is map where {:name -> <enum of :FI, :PO>, :neighbors (optional) -> <vector of \"Country\">} with no other keys, Burger is map where {:name -> <string>, :description (optional) -> <string>, :origin -> <nullable Country>, :price -> <integer greater than 0>}, OrderLine is map where {:burger -> <Burger>, :amount -> <integer>} with no other keys, Order is map where {:lines -> <vector of OrderLine>, :delivery -> <map where {:delivered -> <boolean>, :address -> <map where {:street -> <string>, :zip -> <integer>, :country -> <Country>}>} with no other keys>} with no other keys>" - (umd/describe [:schema - {:registry - {"Country" [:map - {:closed true} - [:name [:enum :FI :PO]] - [:neighbors - {:optional true} - [:vector [:ref "Country"]]]], - "Burger" [:map - [:name string?] - [:description {:optional true} string?] - [:origin [:maybe "Country"]] - [:price pos-int?]], - "OrderLine" [:map - {:closed true} - [:burger "Burger"] - [:amount int?]], - "Order" [:map - {:closed true} - [:lines [:vector "OrderLine"]] - [:delivery - [:map + (testing "vector" + (is (= "vector" (umd/describe vector?))) + (is (= "vector of integer" (umd/describe [:vector :int])))) + + (testing "string" + (is (= "string with length <= 5" (umd/describe [:string {:min 5}]))) + (is (= "string with length >= 5" (umd/describe [:string {:max 5}]))) + (is (= "string with length between 3 and 5 inclusive" (umd/describe [:string {:min 3 :max 5}])))) + + (testing "function" + (is (= "function that takes input: [integer] and returns integer" + (umd/describe [:=> [:cat int?] int?])))) + + (testing "map" + (is (= "map" (umd/describe map?))) + (is (= "map where {:x -> <integer>}" + (umd/describe [:map [:x int?]]))) + (is (= "map where {:x (optional) -> <integer>, :y -> <boolean>}" + (umd/describe [:map [:x {:optional true} int?] [:y :boolean]]))) + (is (= "map where {:x -> <integer>} with no other keys" + (umd/describe [:map {:closed true} [:x int?]]))) + (is (= "map where {:x (optional) -> <integer>, :y -> <boolean>} with no other keys" + (umd/describe [:map {:closed true} [:x {:optional true} int?] [:y :boolean]]))) + (is (= "map where {:j-code -> <keyword, and has length 4>}" + (umd/describe [:map [:j-code [:and + :keyword + [:fn {:description "has length 4"} #(= 4 (count (name %)))]]]]))) + (is (= "map (titled: ‘dict’) from <integer> to <string>" + (umd/describe [:map-of {:title "dict"} :int :string])))) + + (testing "compound schemas" + (is (= "vector of sequence of set of integer" + (umd/describe [:vector [:sequential [:set :int]]])))) + + (testing "multi" + (is (= "one of <:dog = map where {:x -> <integer>} | :cat = anything> dispatched by the type of animal" + (umd/describe [:multi {:dispatch :type + :dispatch-description "the type of animal"} + [:dog [:map [:x :int]]] + [:cat :any]]))) + (is (= "one of <:dog = map where {:x -> <integer>} | :cat = anything> dispatched by :type" + (umd/describe [:multi {:dispatch :type} + [:dog [:map [:x :int]]] + [:cat :any]])))) + + (testing "schema registry" + (is (= "Order which is: <Country is map where {:name -> <enum of :FI, :PO>, :neighbors (optional) -> <vector of \"Country\">} with no other keys, Burger is map where {:name -> <string>, :description (optional) -> <string>, :origin -> <nullable Country>, :price -> <integer greater than 0>}, OrderLine is map where {:burger -> <Burger>, :amount -> <integer>} with no other keys, Order is map where {:lines -> <vector of OrderLine>, :delivery -> <map where {:delivered -> <boolean>, :address -> <map where {:street -> <string>, :zip -> <integer>, :country -> <Country>}>} with no other keys>} with no other keys>" + (umd/describe [:schema + {:registry + {"Country" + [:map + {:closed true} + [:name [:enum :FI :PO]] + [:neighbors + {:optional true} + [:vector [:ref "Country"]]]], + "Burger" [:map + [:name string?] + [:description {:optional true} string?] + [:origin [:maybe "Country"]] + [:price pos-int?]], + "OrderLine" [:map + {:closed true} + [:burger "Burger"] + [:amount int?]], + "Order" [:map {:closed true} - [:delivered boolean?] - [:address + [:lines [:vector "OrderLine"]] + [:delivery [:map - [:street string?] - [:zip int?] - [:country "Country"]]]]]]}} - "Order"]))) - - (is (= "ConsCell <nullable vector with exactly 2 items of type: integer, \"ConsCell\">" - (umd/describe [:schema - {:registry {"ConsCell" [:maybe [:tuple :int [:ref "ConsCell"]]]}} - "ConsCell"]))) - - (is (= "integer greater than or equal to 0" - (umd/describe [:int {:min 0}]))) - - (is (= "integer less than or equal to 1" - (umd/describe [:int {:max 1}]))) - - (is (= "integer between 0 and 1 inclusive" - (umd/describe [:int {:min 0 :max 1}])))) + {:closed true} + [:delivered boolean?] + [:address + [:map + [:street string?] + [:zip int?] + [:country "Country"]]]]]]}} + "Order"]))) + (is (= "ConsCell <nullable vector with exactly 2 items of type: integer, \"ConsCell\">" + (umd/describe [:schema + {:registry {"ConsCell" [:maybe [:tuple :int [:ref "ConsCell"]]]}} + "ConsCell"])))) + + (testing "int" + (is (= "integer greater than or equal to 0" + (umd/describe [:int {:min 0}]))) + (is (= "integer less than or equal to 1" + (umd/describe [:int {:max 1}]))) + (is (= "integer between 0 and 1 inclusive" + (umd/describe [:int {:min 0 :max 1}])))) + + (testing "repeat" + (is (= "repeat <integer> at least 1 time" + (umd/describe [:repeat {:min 1} int?]))) + (is (= "repeat <integer> at most 7 times" + (umd/describe [:repeat {:max 7} int?]))) + (is (= "repeat <integer> at least 1 time, up to 7 times" + (umd/describe [:repeat {:min 1 :max 7} int?])))))